# CCLS Games CLI Tool # Complete Version with Setup, Search, Get functionality, and Logging # Configuration $baseUrl = "https://games.ccls.icu" $cliApiUrl = "$baseUrl/CLI/api/2.0" $settingsFolder = ".\settings" $credentialsFile = "$settingsFolder\credentials.dat" $settingsFile = "$settingsFolder\settings.json" # Using JSON instead of INI for simplicity $logsFolder = ".\logs" # Logs folder path # Script-level variables for progress display, logging, and credentials $script:progressInitialized = $false $script:topProgressLine = $null $script:logFile = $null $script:sessionStartTime = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" $script:cachedCredentials = $null $script:oversightEnabled = $false $script:oversightData = $null $script:userLoggingEnabled = $false $script:userLoggingPath = $null $script:userLoggingUserId = $null $script:userLoggingSessionFile = $null $script:dependenciesChecked = $false $script:dependenciesSatisfied = $false # Ensure settings and logs directories exist if (-not (Test-Path $settingsFolder)) { New-Item -Path $settingsFolder -ItemType Directory | Out-Null } if (-not (Test-Path $logsFolder)) { New-Item -Path $logsFolder -ItemType Directory | Out-Null } # Initialize log file for the current session $script:logFile = Join-Path -Path $logsFolder -ChildPath "ccls_session_$($script:sessionStartTime).log" "CCLS Games CLI Session started at $($script:sessionStartTime)" | Out-File -FilePath $script:logFile # Background check to ensure script is named CLI.ps1 for update functionality function Test-ScriptNaming { try { # Get the current script file information $currentScriptPath = $null # Try to get the script path using different methods if ($PSCommandPath) { $currentScriptPath = $PSCommandPath } elseif ($MyInvocation.MyCommand.Path) { $currentScriptPath = $MyInvocation.MyCommand.Path } elseif ($script:MyInvocation.MyCommand.Path) { $currentScriptPath = $script:MyInvocation.MyCommand.Path } if ($currentScriptPath) { $currentFileName = Split-Path -Path $currentScriptPath -Leaf $currentDirectory = Split-Path -Path $currentScriptPath -Parent $expectedFileName = "CLI.ps1" $expectedFilePath = Join-Path -Path $currentDirectory -ChildPath $expectedFileName "Script naming check initiated" | Out-File -FilePath $script:logFile -Append "Current script path: $currentScriptPath" | Out-File -FilePath $script:logFile -Append "Current file name: $currentFileName" | Out-File -FilePath $script:logFile -Append "Expected file name: $expectedFileName" | Out-File -FilePath $script:logFile -Append if ($currentFileName -ne $expectedFileName) { "Script name mismatch detected - renaming required" | Out-File -FilePath $script:logFile -Append # Check if CLI.ps1 already exists in the directory if (Test-Path $expectedFilePath) { "Warning: CLI.ps1 already exists in directory" | Out-File -FilePath $script:logFile -Append # Create a backup of the existing CLI.ps1 $backupName = "CLI_backup_$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss').ps1" $backupPath = Join-Path -Path $currentDirectory -ChildPath $backupName try { Copy-Item -Path $expectedFilePath -Destination $backupPath -Force "Created backup of existing CLI.ps1: $backupPath" | Out-File -FilePath $script:logFile -Append } catch { "Failed to create backup of existing CLI.ps1: $($_.Exception.Message)" | Out-File -FilePath $script:logFile -Append } } # Rename the current script to CLI.ps1 try { Copy-Item -Path $currentScriptPath -Destination $expectedFilePath -Force "Successfully copied current script to CLI.ps1" | Out-File -FilePath $script:logFile -Append # Remove the old file if it has a different name if ($currentScriptPath -ne $expectedFilePath) { Remove-Item -Path $currentScriptPath -Force "Successfully removed old script file: $currentScriptPath" | Out-File -FilePath $script:logFile -Append } "Script renaming completed successfully" | Out-File -FilePath $script:logFile -Append } catch { "Failed to rename script file: $($_.Exception.Message)" | Out-File -FilePath $script:logFile -Append "Update functionality may not work properly with current filename" | Out-File -FilePath $script:logFile -Append } } else { "Script name is correct (CLI.ps1) - no action needed" | Out-File -FilePath $script:logFile -Append } } else { "Could not determine current script path - skipping rename check" | Out-File -FilePath $script:logFile -Append "Update functionality may be affected if script is not named CLI.ps1" | Out-File -FilePath $script:logFile -Append } } catch { "Error during script naming check: $($_.Exception.Message)" | Out-File -FilePath $script:logFile -Append } } # Run the script naming check Test-ScriptNaming function Write-CCLSHost { param ( [Parameter(Position = 0)] [string]$Message = "", [Parameter()] [System.ConsoleColor]$ForegroundColor = [System.ConsoleColor]::White, [Parameter()] [System.ConsoleColor]$BackgroundColor = [System.ConsoleColor]::Black, [Parameter()] [switch]$NoNewline, [Parameter()] [switch]$Log, [Parameter()] [switch]$NoConsole ) # Check if the message is marked for removal if ($Message.StartsWith('#')) { # Don't show in console, but log it if needed if ($Log) { $cleanMessage = $Message.Substring(1).Trim() if (-not [string]::IsNullOrWhiteSpace($cleanMessage)) { $cleanMessage | Out-File -FilePath $script:logFile -Append # Send to server logging if enabled if ($script:userLoggingEnabled) { Send-LogToServer -LogEntry $cleanMessage } } } return } if ($Message.StartsWith('/')) { # Skip completely - don't log or display return } # Handle empty lines if ([string]::IsNullOrWhiteSpace($Message)) { # Display in console if not suppressed if (-not $NoConsole) { Write-Host "" -NoNewline:$NoNewline } # But don't log empty lines return } # Display in console if not suppressed if (-not $NoConsole) { Write-Host $Message -ForegroundColor $ForegroundColor -BackgroundColor $BackgroundColor -NoNewline:$NoNewline } # Log if requested if ($Log) { $plainMessage = $Message $plainMessage | Out-File -FilePath $script:logFile -Append # Send to server logging if enabled if ($script:userLoggingEnabled) { Send-LogToServer -LogEntry $plainMessage } } } function Initialize-Settings { # Get version information using existing function (but don't show alerts here) try { $currentVersion = "2.1.1" # Static version, no need to call Test-VersionUpdate here } catch { # If version check fails, use a default version $currentVersion = "2.1.1" Write-CCLSHost "#Error getting version info, using default: $($_.Exception.Message)" -Log -NoConsole } if (-not (Test-Path $settingsFile)) { # Create default settings file $defaultSettings = @{ RememberLogin = $false DownloadPath = ".\downloads" TempDownloadPath = ".\tmp" HasCompletedSetup = $false Version = $currentVersion DevMode = $false # Add DevMode parameter with default false } try { $defaultSettings | ConvertTo-Json | Set-Content -Path $settingsFile Write-CCLSHost "#Created new settings file with DevMode parameter" -Log -NoConsole } catch { Write-CCLSHost "#Error creating settings file: $($_.Exception.Message)" -Log -NoConsole } # Return settings without version change flag return $defaultSettings } # Load settings try { $settingsContent = Get-Content -Path $settingsFile -Raw $settings = $settingsContent | ConvertFrom-Json } catch { Write-CCLSHost "#Error loading settings file: $($_.Exception.Message)" -Log -NoConsole # Return default settings if loading fails return @{ RememberLogin = $false DownloadPath = ".\downloads" TempDownloadPath = ".\tmp" HasCompletedSetup = $false Version = $currentVersion DevMode = $false } } # Check if Version property exists, add it if not if (-not (Get-Member -InputObject $settings -Name "Version" -MemberType Properties)) { $settings | Add-Member -MemberType NoteProperty -Name "Version" -Value $currentVersion try { $settings | ConvertTo-Json | Set-Content -Path $settingsFile Write-CCLSHost "#Added Version parameter to settings" -Log -NoConsole } catch { Write-CCLSHost "#Error updating settings with Version: $($_.Exception.Message)" -Log -NoConsole } } # Check if DevMode property exists, add it if not if (-not (Get-Member -InputObject $settings -Name "DevMode" -MemberType Properties)) { $settings | Add-Member -MemberType NoteProperty -Name "DevMode" -Value $false try { $settings | ConvertTo-Json | Set-Content -Path $settingsFile Write-CCLSHost "#Added DevMode parameter to existing settings" -Log -NoConsole } catch { Write-CCLSHost "#Error updating settings with DevMode: $($_.Exception.Message)" -Log -NoConsole } } # Check if version has changed and update if needed if ($settings.Version -ne $currentVersion) { Write-CCLSHost "#Detected version change from $($settings.Version) to $currentVersion" -Log -NoConsole # Store the old version $oldVersion = $settings.Version # Update to new version $settings.Version = $currentVersion try { $settings | ConvertTo-Json | Set-Content -Path $settingsFile } catch { Write-CCLSHost "#Error updating version in settings: $($_.Exception.Message)" -Log -NoConsole } # Keep track that version has changed for later notification $script:versionChanged = $true $script:previousVersion = $oldVersion } return $settings } function Test-SystemRequirementsSilent { $results = @{ PythonInstalled = $false PythonVersion = $null RequestsInstalled = $false RequestsVersion = $null SevenZipInstalled = $false SevenZipVersion = $null MissingDependencies = @() } # Check for Python try { $pythonResult = python --version 2>&1 if ($pythonResult -match "Python (\d+\.\d+\.\d+)") { $results.PythonInstalled = $true $results.PythonVersion = $matches[1] } } catch { # Python not installed } # Check for Python requests library (only if Python is installed) if ($results.PythonInstalled) { try { $requestsCheck = python -c "import requests; print('Installed (v{0})'.format(requests.__version__))" 2>&1 if ($requestsCheck -match "Installed \(v([\d\.]+)\)") { $results.RequestsInstalled = $true $results.RequestsVersion = $matches[1] } } catch { # Requests not installed } } # Check for 7-Zip $systemPaths = @( "C:\Program Files\7-Zip\7z.exe", "${env:ProgramFiles(x86)}\7-Zip\7z.exe" ) $scriptLocation = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path } $localPath = Join-Path -Path $scriptLocation -ChildPath "7zip\7z.exe" $allPaths = $systemPaths + $localPath foreach ($path in $allPaths) { if (Test-Path -Path $path) { $results.SevenZipInstalled = $true try { $versionInfo = Get-Item $path | Select-Object -ExpandProperty VersionInfo $results.SevenZipVersion = $versionInfo.ProductVersion } catch { $results.SevenZipVersion = "Unknown" } break } } # Determine missing dependencies if (-not $results.PythonInstalled) { $results.MissingDependencies += "install python" } if (-not $results.RequestsInstalled) { if ($results.PythonInstalled) { $results.MissingDependencies += "install requests" } # Note: if Python is not installed, we don't add requests to missing dependencies # because the user will get an error when trying to install requests without Python } if (-not $results.SevenZipInstalled) { $results.MissingDependencies += "install 7zip" } return $results } # Function to check dependencies and block CLI usage if missing function Test-RequiredDependencies { if ($script:dependenciesChecked) { return $script:dependenciesSatisfied } $script:dependenciesChecked = $true $dependencyResults = Test-SystemRequirementsSilent if ($dependencyResults.MissingDependencies.Count -eq 0) { $script:dependenciesSatisfied = $true Write-CCLSHost "#All required dependencies are satisfied" -Log -NoConsole return $true } else { $script:dependenciesSatisfied = $false Write-CCLSHost "CLI Tool is missing critical dependencies, run 'check' for more info" -ForegroundColor Red -Log Write-CCLSHost "The script will remain in a broken state until dependency requirements are met" -ForegroundColor Red -Log return $false } } # Function to enforce dependency requirements for most commands function Assert-DependenciesRequired { param ( [string]$CommandName = "this command" ) if (-not (Test-RequiredDependencies)) { Write-CCLSHost "'$CommandName' is missing critical dependencies, run 'check' for more info" -ForegroundColor Red -Log Write-CCLSHost "The script will remain in a broken state until dependency requirements are met" -ForegroundColor Red -Log return $false } return $true } function Toggle-DevMode { $settings = Initialize-Settings if ($settings.DevMode) { # Currently in DevMode, turn it off $settings.DevMode = $false Save-Settings -settings $settings # Clear any stored or cached credentials if (Test-Path $credentialsFile) { Remove-Item -Path $credentialsFile -Force Write-CCLSHost "#Removed stored credentials" -Log -NoConsole } # Clear cached credentials $script:cachedCredentials = $null # Clear console and show exit message Clear-Host Write-CCLSHost "Exited devmode" -ForegroundColor Green -Log # Show the normal login interface Write-CCLSHost "Hello and welcome to the CCLS Games CLI Tool" -ForegroundColor Green -Log Write-CCLSHost "Before proceeding to using this software you will need to sign in." -Log Write-CCLSHost "If you do not have an account already please go to $baseUrl/login.php?signup to register a new account." -ForegroundColor Cyan -Log # Try auto-login if remember login is enabled $loginResult = @{ Success = $false; DevMode = $false } if ($settings.RememberLogin) { $loginResult = Start-AutoLogin } # If auto-login failed or is disabled, do manual login if (-not $loginResult.Success) { $loginResult = Start-ManualLogin } # If login succeeded, show main interface if ($loginResult.Success) { # Normal login - show welcome message Write-CCLSHost "Welcome to CCLS Games CLI Tool, $($loginResult.Username)!" -ForegroundColor Green -Log # Check dependencies after successful login Test-RequiredDependencies | Out-Null # Check if version has changed and show notification if ($script:versionChanged) { $versionInfo = Test-VersionUpdate $currentVersion = $versionInfo.CurrentVersion Write-CCLSHost "Welcome to $currentVersion! Type 'changelog latest' to view the changes made." -ForegroundColor Green -Log } # Show appropriate message based on setup status if ($settings.HasCompletedSetup) { Write-CCLSHost "Type 'help' for a list of available commands." -ForegroundColor Cyan -Log } else { Write-CCLSHost "ALERT, type command 'setup' to set critical values before downloading." -ForegroundColor Red -Log } Start-CommandInterface -username $loginResult.Username } else { Write-CCLSHost "Login failed. Press any key to exit..." -ForegroundColor Red -Log $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") Exit } } else { # Currently in normal mode, turn on DevMode $settings.DevMode = $true Save-Settings -settings $settings # Clear any stored or cached credentials if (Test-Path $credentialsFile) { Remove-Item -Path $credentialsFile -Force Write-CCLSHost "#Removed stored credentials" -Log -NoConsole } # Clear cached credentials $script:cachedCredentials = $null Write-CCLSHost "You are now in developer mode. 'get', 'search' and any other command that calls external API's will not work." -ForegroundColor Red -Log } } function Read-UsernameWithDevModeDetection { $devModeActivated = $false Write-CCLSHost "Username: " -ForegroundColor Cyan -NoNewline -Log # Use a simpler approach - check for Ctrl+Q using a different method $username = "" $ctrlQPressed = $false try { # Try to use ReadKey if available, otherwise fall back to Read-Host if ($Host.UI.RawUI -and $Host.UI.RawUI.KeyAvailable -ne $null) { # Custom input handling to detect Ctrl+Q while ($true) { if ([Console]::KeyAvailable) { $keyInfo = [Console]::ReadKey($true) # Check for Ctrl+Q if (($keyInfo.Modifiers -band [ConsoleModifiers]::Control) -and ($keyInfo.Key -eq [ConsoleKey]::Q)) { $ctrlQPressed = $true # Don't show anything when Ctrl+Q is pressed continue } # Handle Enter key if ($keyInfo.Key -eq [ConsoleKey]::Enter) { Write-Host "" # New line break } # Handle Backspace if ($keyInfo.Key -eq [ConsoleKey]::Backspace) { if ($username.Length -gt 0) { $username = $username.Substring(0, $username.Length - 1) Write-Host "`b `b" -NoNewline } continue } # Handle regular characters if ($keyInfo.KeyChar -match '[a-zA-Z0-9\s]' -and $keyInfo.KeyChar -ne "`0") { $username += $keyInfo.KeyChar Write-Host $keyInfo.KeyChar -NoNewline } } else { Start-Sleep -Milliseconds 50 } } # Check if Ctrl+Q was pressed and username is "devmode" if ($ctrlQPressed -and $username.ToLower().Trim() -eq "devmode") { $devModeActivated = $true } } else { # Fallback to simple Read-Host for environments that don't support advanced key handling Write-CCLSHost "#Using fallback input method" -Log -NoConsole $tempInput = Read-Host # Check for special devmode activation pattern # Allow users to type "ctrl+q devmode" as an alternative if ($tempInput.ToLower().Trim() -eq "ctrl+q devmode" -or $tempInput.ToLower().Trim() -eq "devmode ctrl+q") { $devModeActivated = $true $username = "devmode" } else { $username = $tempInput } } } catch { Write-CCLSHost "#Error in key detection, falling back to Read-Host: $($_.Exception.Message)" -Log -NoConsole # Ultimate fallback $tempInput = Read-Host if ($tempInput.ToLower().Trim() -eq "ctrl+q devmode" -or $tempInput.ToLower().Trim() -eq "devmode ctrl+q") { $devModeActivated = $true $username = "devmode" } else { $username = $tempInput } } Write-CCLSHost "$username" -NoConsole -Log return @{ Username = $username DevModeActivated = $devModeActivated } } # Save settings function Save-Settings($settings) { $settings | ConvertTo-Json | Set-Content -Path $settingsFile } # Store credentials securely function Save-Credentials($username, $password) { $credentials = @{ Username = $username Password = $password } | ConvertTo-Json # Simple encryption (replace with more secure method if needed) $securePassword = ConvertTo-SecureString -String $credentials -AsPlainText -Force $encryptedText = ConvertFrom-SecureString -SecureString $securePassword # Save to file $encryptedText | Set-Content -Path $credentialsFile } # Load stored credentials function Get-StoredCredentials { if (Test-Path $credentialsFile) { try { $encryptedText = Get-Content -Path $credentialsFile $securePassword = ConvertTo-SecureString -String $encryptedText $credentials = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto( [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword) ) return $credentials | ConvertFrom-Json } catch { Write-CCLSHost "Error reading credentials: $($_.Exception.Message)" -ForegroundColor Red -Log return $null } } return $null } # Run setup process function Start-Setup { Write-CCLSHost "`n`nCCLS Games CLI Setup" -ForegroundColor Green -Log Write-CCLSHost "=====================" -ForegroundColor Green -Log Write-CCLSHost "Please configure the following settings:`n" -ForegroundColor Cyan -Log $settings = Initialize-Settings # Get game installation directory $validPath = $false while (-not $validPath) { Write-CCLSHost "Set your games default installation directory: " -ForegroundColor Yellow -NoNewline -Log $downloadPath = Read-Host Write-CCLSHost "$downloadPath" -NoConsole -Log if ([string]::IsNullOrWhiteSpace($downloadPath)) { Write-CCLSHost "Please enter a valid directory path." -ForegroundColor Red -Log } else { # Create directory if it doesn't exist if (-not (Test-Path $downloadPath)) { try { New-Item -ItemType Directory -Path $downloadPath -Force | Out-Null $validPath = $true } catch { Write-CCLSHost "Error creating directory: $($_.Exception.Message)" -ForegroundColor Red -Log } } else { $validPath = $true } } } # Get temporary download directory $validTempPath = $false while (-not $validTempPath) { Write-CCLSHost "Set the temporary directory of downloading files before they have finished downloading: " -ForegroundColor Yellow -NoNewline -Log $tempDownloadPath = Read-Host Write-CCLSHost "$tempDownloadPath" -NoConsole -Log if ([string]::IsNullOrWhiteSpace($tempDownloadPath)) { Write-CCLSHost "Please enter a valid directory path." -ForegroundColor Red -Log } else { # Create directory if it doesn't exist if (-not (Test-Path $tempDownloadPath)) { try { New-Item -ItemType Directory -Path $tempDownloadPath -Force | Out-Null $validTempPath = $true } catch { Write-CCLSHost "Error creating directory: $($_.Exception.Message)" -ForegroundColor Red -Log } } else { $validTempPath = $true } } } # Update settings $settings.DownloadPath = $downloadPath $settings.TempDownloadPath = $tempDownloadPath $settings.HasCompletedSetup = $true Save-Settings -settings $settings Write-CCLSHost "`nGreat, you have now completed the setup. Type 'help' for a list of commands to get you started." -ForegroundColor Green -Log } # Validate username against server function Test-Username($username) { $params = @{ Uri = "$cliApiUrl/username_check.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/2.0" } Body = @{ username = $username } } try { $response = Invoke-RestMethod @params return $response.exists } catch { Write-CCLSHost "Error connecting to the server: $($_.Exception.Message)" -ForegroundColor Red -Log return $false } } # Validate password against server function Test-Password($username, $password) { $params = @{ Uri = "$cliApiUrl/password_check.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/2.0" } Body = @{ username = $username password = $password } } try { $response = Invoke-RestMethod @params return $response.success } catch { Write-CCLSHost "Error connecting to the server: $($_.Exception.Message)" -ForegroundColor Red -Log return $false } } function Start-AutoLogin { $credentials = Get-StoredCredentials if ($credentials -ne $null) { Write-CCLSHost "#Attempting to login with stored credentials..." -Log if (Test-Password -username $credentials.Username -password $credentials.Password) { Write-CCLSHost "#Auto-login successful!" -Log # Cache the credentials for the session $script:cachedCredentials = @{ Username = $credentials.Username Password = $credentials.Password } # Initialize oversight system after successful auto-login (THIS WAS MISSING!) $versionInfo = Test-VersionUpdate $currentVersion = $versionInfo.CurrentVersion $script:oversightData = Get-OversightData -Username $credentials.Username -CurrentVersion $currentVersion if ($script:oversightData) { $script:oversightEnabled = $true if ($script:oversightData.user_logging) { $script:userLoggingEnabled = $true Initialize-UserLogging -Username $credentials.Username } } Test-RequiredDependencies | Out-Null return @{ Success = $true Username = $credentials.Username } } else { Write-CCLSHost "Stored credentials are no longer valid." -ForegroundColor Yellow -Log # Remove invalid credentials if (Test-Path $credentialsFile) { Remove-Item -Path $credentialsFile -Force } } } return @{ Success = $false } } function Start-ManualLogin { $maxAttempts = 3 $attempts = 0 while ($attempts -lt $maxAttempts) { # Use the new function to detect Ctrl+Q + devmode $usernameResult = Read-UsernameWithDevModeDetection $username = $usernameResult.Username $devModeActivated = $usernameResult.DevModeActivated # Check if DevMode was activated and username is "devmode" if ($devModeActivated -and $username.ToLower() -eq "devmode") { Write-CCLSHost "#DevMode activation detected via Ctrl+Q + devmode" -Log -NoConsole # Enable DevMode $settings = Initialize-Settings $settings.DevMode = $true Save-Settings -settings $settings # Clear any stored or cached credentials if (Test-Path $credentialsFile) { Remove-Item -Path $credentialsFile -Force Write-CCLSHost "#Removed stored credentials for DevMode" -Log -NoConsole } # Clear cached credentials $script:cachedCredentials = $null Write-CCLSHost "You are in developer mode. 'get', 'search' and any other command that calls external API's will not work." -ForegroundColor Red -Log return @{ Success = $true Username = "Developer" DevMode = $true } } # Check if username exists (normal login flow) Write-CCLSHost "#Checking username..." -Log if (Test-Username -username $username) { Write-CCLSHost "#Username found!" -Log Write-CCLSHost "Password: " -ForegroundColor Cyan -NoNewline -Log $password = Read-Host -AsSecureString $passwordPlain = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto( [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) ) Write-CCLSHost "********" -NoConsole -Log Write-CCLSHost "#Validating password..." -Log if (Test-Password -username $username -password $passwordPlain) { Write-CCLSHost "#Login successful!" -Log # Cache the credentials for the session $script:cachedCredentials = @{ Username = $username Password = $passwordPlain } # Initialize oversight system after successful login $versionInfo = Test-VersionUpdate $currentVersion = $versionInfo.CurrentVersion $script:oversightData = Get-OversightData -Username $username -CurrentVersion $currentVersion if ($script:oversightData) { $script:oversightEnabled = $true if ($script:oversightData.user_logging) { $script:userLoggingEnabled = $true Initialize-UserLogging -Username $username } } # Ask if user wants to save credentials - no newline before this Write-CCLSHost "Do you want to remember your login info for next time? (Y/N)" -ForegroundColor Yellow -Log $rememberLogin = Read-Host Write-CCLSHost "$rememberLogin" -NoConsole -Log if ($rememberLogin.ToLower() -eq "y") { Save-Credentials -username $username -password $passwordPlain $settings = Initialize-Settings $settings.RememberLogin = $true Save-Settings -settings $settings Write-CCLSHost "#Login information saved." -Log } return @{ Success = $true Username = $username DevMode = $false } } else { Write-CCLSHost "Incorrect password, please try again." -ForegroundColor Red -Log $attempts++ } } else { Write-CCLSHost "Username not found, please try again." -ForegroundColor Red -Log $attempts++ } } Write-CCLSHost "Too many failed login attempts. Please try again later." -ForegroundColor Red -Log return @{ Success = $false DevMode = $false } } # Updated Install-Utility function function Install-Utility { param ( [string]$UtilityName ) # Handle different utilities switch ($UtilityName.ToLower()) { "7zip" { Install7Zip } "requests" { InstallPythonRequests } "python" { Install-Python } default { Write-CCLSHost "Unsupported utility: $UtilityName" -ForegroundColor Red -Log Write-CCLSHost "Currently supported utilities: 7zip, requests, python" -ForegroundColor Green -Log } } } # Function to install 7-Zip (internal function for Install-Utility) function Install7Zip { # Determine the current script directory $scriptLocation = if ($PSScriptRoot) { # If running from a script, use its location $PSScriptRoot } else { # If running in console, use current directory (Get-Location).Path } # Target directory is the script location $targetDir = $scriptLocation # URLs for download - Updated for v2.0 API structure $zipUrl = "$baseUrl/CLI/api/2.0/utilities/7zip/7zip.zip" $zipPath = Join-Path -Path $targetDir -ChildPath "7zip.zip" Write-CCLSHost "Starting 7-Zip installation..." -ForegroundColor Cyan -Log try { # Download the zip file Write-CCLSHost "Downloading 7-Zip package from $zipUrl" -ForegroundColor Yellow -Log $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("User-Agent", "CCLS-CLI/2.0") $webClient.DownloadFile($zipUrl, $zipPath) if (-not (Test-Path $zipPath)) { Write-CCLSHost "Failed to download 7-Zip package." -ForegroundColor Red -Log return } Write-CCLSHost "Download completed successfully." -ForegroundColor Green -Log # Create temporary extraction directory $tempExtractPath = Join-Path -Path $targetDir -ChildPath "7zip_temp" if (Test-Path $tempExtractPath) { Remove-Item -Path $tempExtractPath -Recurse -Force } New-Item -ItemType Directory -Path $tempExtractPath -Force | Out-Null # Final 7zip directory $finalPath = Join-Path -Path $targetDir -ChildPath "7zip" if (Test-Path $finalPath) { Remove-Item -Path $finalPath -Recurse -Force } # Extract the zip file to temp location Write-CCLSHost "Extracting 7-Zip package..." -ForegroundColor Yellow -Log $extractionSuccess = $false try { # Try to use built-in Expand-Archive Expand-Archive -Path $zipPath -DestinationPath $tempExtractPath -Force $extractionSuccess = $true } catch { Write-CCLSHost "#Built-in extraction failed, trying alternate method: $($_.Exception.Message)" -ForegroundColor Yellow -Log -NoConsole try { # Alternative extraction method using .NET Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, $tempExtractPath) $extractionSuccess = $true } catch { Write-CCLSHost "Extraction failed: $($_.Exception.Message)" -ForegroundColor Red -Log $extractionSuccess = $false } } # Fix nested folder issue if extraction was successful if ($extractionSuccess) { # Check if we have a nested 7zip folder $nestedFolder = Join-Path -Path $tempExtractPath -ChildPath "7zip" if (Test-Path $nestedFolder) { # Move the contents from nested folder to final location Write-CCLSHost "#Fixing folder structure..." -Log -NoConsole Move-Item -Path $nestedFolder -Destination $targetDir -Force $extractionSuccess = $true } else { # If no nested folder, just rename temp folder to final name Rename-Item -Path $tempExtractPath -NewName "7zip" $extractionSuccess = $true } } # Clean up the downloaded zip file if (Test-Path $zipPath) { Remove-Item -Path $zipPath -Force Write-CCLSHost "#Removed temporary download file." -ForegroundColor Gray -Log -NoConsole } # Clean up temp folder if it still exists if (Test-Path $tempExtractPath) { Remove-Item -Path $tempExtractPath -Recurse -Force Write-CCLSHost "#Removed temporary extraction folder." -ForegroundColor Gray -Log -NoConsole } # Verify installation $exePath = Join-Path -Path $finalPath -ChildPath "7z.exe" if ($extractionSuccess -and (Test-Path $exePath)) { Write-CCLSHost "7-Zip has been successfully installed to $finalPath" -ForegroundColor Green -Log Write-CCLSHost "You can now use 7-Zip for file extraction." -ForegroundColor Green -Log } else { Write-CCLSHost "7-Zip installation was not completed successfully." -ForegroundColor Red -Log Write-CCLSHost "Please try again or manually install 7-Zip." -ForegroundColor Yellow -Log } } catch { Write-CCLSHost "Error during installation: $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "7-Zip installation failed." -ForegroundColor Red -Log } } # Function to install Python requests library (internal function for Install-Utility) function InstallPythonRequests { Write-CCLSHost "Checking for Python installation..." -ForegroundColor Cyan -Log # First check if Python is installed try { $pythonResult = python --version 2>&1 if ($pythonResult -match "Python (\d+\.\d+\.\d+)") { $pythonVersion = $matches[1] Write-CCLSHost "Python detected (v$pythonVersion)" -ForegroundColor Green -Log } else { Write-CCLSHost "Python not detected" -ForegroundColor Red -Log Write-CCLSHost "Please install Python first before installing the requests library." -ForegroundColor Yellow -Log Write-CCLSHost "Download Python with 'install python' command or" -ForegroundColor Yellow -Log Write-CCLSHost "Download Python from: https://www.python.org/downloads/ manually" -ForegroundColor Yellow -Log return } } catch { Write-CCLSHost "Python is not installed or not in the system PATH." -ForegroundColor Red -Log Write-CCLSHost "Please install Python first before installing the requests library." -ForegroundColor Yellow -Log Write-CCLSHost "Download Python with 'install python' command or" -ForegroundColor Yellow -Log Write-CCLSHost "Download Python from: https://www.python.org/downloads/ manually" -ForegroundColor Yellow -Log return } # Check if requests is already installed Write-CCLSHost "Checking if requests library is already installed..." -ForegroundColor Cyan -Log try { $requestsCheck = python -c "import requests; print('Installed (v{0})'.format(requests.__version__))" 2>&1 if ($requestsCheck -match "Installed \(v([\d\.]+)\)") { $requestsVersion = $matches[1] Write-CCLSHost "The requests library is already installed (v$requestsVersion)" -ForegroundColor Green -Log # Ask if user wants to upgrade Write-CCLSHost "Do you want to upgrade to the latest version? (Y/N)" -ForegroundColor Yellow -Log $upgradeConfirmation = Read-Host Write-CCLSHost "$upgradeConfirmation" -NoConsole -Log if ($upgradeConfirmation.ToLower() -ne "y") { Write-CCLSHost "Installation skipped." -ForegroundColor Yellow -Log return } # If yes, set upgrade flag $upgradeFlag = "--upgrade" } else { # Not installed $upgradeFlag = "" } } catch { # Not installed or error checking Write-CCLSHost "The requests library is not installed." -ForegroundColor Yellow -Log $upgradeFlag = "" } Write-CCLSHost "Installing Python requests library..." -ForegroundColor Cyan -Log try { # Use Start-Process with -Wait to run pip in the background $pipCommand = "pip" $pipArgs = "install $upgradeFlag requests" # Temporary files for output and error $tempOutputFile = [System.IO.Path]::GetTempFileName() $tempErrorFile = [System.IO.Path]::GetTempFileName() Write-CCLSHost "#Running: $pipCommand $pipArgs" -Log -NoConsole # Show progress indicator Write-CCLSHost "Installing..." -NoNewline -ForegroundColor Yellow -Log # Run pip in background and redirect output to temp files $process = Start-Process -FilePath $pipCommand -ArgumentList $pipArgs -NoNewWindow -PassThru -RedirectStandardOutput $tempOutputFile -RedirectStandardError $tempErrorFile -Wait # Read output and error from temp files $output = Get-Content -Path $tempOutputFile -Raw $error = Get-Content -Path $tempErrorFile -Raw # Clean up temp files Remove-Item -Path $tempOutputFile -Force -ErrorAction SilentlyContinue Remove-Item -Path $tempErrorFile -Force -ErrorAction SilentlyContinue Write-CCLSHost "" -Log # Newline after progress indicator # Verify installation regardless of process exit code # This is more reliable than checking process exit code $installSuccess = $false try { $verifyResult = python -c "import requests; print('Installed (v{0})'.format(requests.__version__))" 2>&1 if ($verifyResult -match "Installed \(v([\d\.]+)\)") { $installSuccess = $true $newVersion = $matches[1] } } catch { $installSuccess = $false } if ($installSuccess) { # Check if output contains "Successfully installed" as additional confirmation if (-not [string]::IsNullOrWhiteSpace($output) -and ($output -match "Successfully installed" -or $output -match "Requirement already satisfied")) { Write-CCLSHost "Python requests library v$newVersion installed successfully!" -ForegroundColor Green -Log # Show summary of what happened if ($output -match "Requirement already satisfied") { Write-CCLSHost "All requirements were already satisfied." -ForegroundColor Green -Log } else { Write-CCLSHost "Installation completed successfully." -ForegroundColor Green -Log } } else { Write-CCLSHost "Python requests library appears to be installed (v$newVersion)" -ForegroundColor Green -Log Write-CCLSHost "But the installation process reported unusual output." -ForegroundColor Yellow -Log } } else { Write-CCLSHost "Failed to install Python requests library." -ForegroundColor Red -Log if (-not [string]::IsNullOrWhiteSpace($error)) { Write-CCLSHost "Error details:" -ForegroundColor Red -Log Write-CCLSHost $error -ForegroundColor Red -Log } if (-not [string]::IsNullOrWhiteSpace($output)) { Write-CCLSHost "Output details:" -ForegroundColor Yellow -Log Write-CCLSHost $output -ForegroundColor Yellow -Log } } } catch { Write-CCLSHost "Error during installation: $($_.Exception.Message)" -ForegroundColor Red -Log } } # Function to install Python function Install-Python { Write-CCLSHost "Starting Python installation process..." -ForegroundColor Cyan -Log # Create a temporary directory for the download $tempDir = [System.IO.Path]::GetTempPath() $tempDownloadPath = Join-Path -Path $tempDir -ChildPath "python_installer.exe" # Determine if system is 64-bit or 32-bit $is64Bit = [Environment]::Is64BitOperatingSystem # Get Python version info from our server try { Write-CCLSHost "Retrieving Python version information..." -ForegroundColor Yellow -Log $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("User-Agent", "CCLS-CLI/2.0") # Use the CCLS API endpoint to get Python version info - Updated for v2.0 API structure $pythonInfoJson = $webClient.DownloadString("$cliApiUrl/install_python.php") $pythonInfo = $pythonInfoJson | ConvertFrom-Json # Use the appropriate URL based on system architecture if ($is64Bit) { $pythonUrl = $pythonInfo.windows_64bit_url Write-CCLSHost "Using 64-bit installer for Python $($pythonInfo.version)" -ForegroundColor Green -Log } else { $pythonUrl = $pythonInfo.windows_32bit_url Write-CCLSHost "Using 32-bit installer for Python $($pythonInfo.version)" -ForegroundColor Green -Log } } catch { Write-CCLSHost "Error retrieving Python information from server: $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "Installation cannot proceed. Please try again later." -ForegroundColor Red -Log return } # Download the Python installer try { Write-CCLSHost "Starting download from $pythonUrl" -ForegroundColor Yellow -Log $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("User-Agent", "CCLS-CLI/2.0") Write-CCLSHost "Downloading Python installer to $tempDownloadPath..." -ForegroundColor Cyan -Log Write-CCLSHost "Download in progress, this may take a few minutes depending on your connection speed..." -ForegroundColor Yellow -Log # Use synchronous download with simple progress indication $sw = [System.Diagnostics.Stopwatch]::StartNew() $webClient.DownloadFile($pythonUrl, $tempDownloadPath) $sw.Stop() if (Test-Path $tempDownloadPath) { $fileSize = (Get-Item $tempDownloadPath).Length / 1MB Write-CCLSHost "Download completed successfully! Downloaded $([Math]::Round($fileSize, 2)) MB in $([Math]::Round($sw.Elapsed.TotalSeconds, 1)) seconds." -ForegroundColor Green -Log } else { Write-CCLSHost "Download failed - file not found at expected location." -ForegroundColor Red -Log return } } catch { Write-CCLSHost "Error downloading Python installer: $($_.Exception.Message)" -ForegroundColor Red -Log return } # Launch the installer try { Write-CCLSHost "Starting Python installer..." -ForegroundColor Green -Log Write-CCLSHost "Please follow the on-screen instructions to complete the installation." -ForegroundColor Yellow -Log Write-CCLSHost "Recommended options:" -ForegroundColor Yellow -Log Write-CCLSHost " - Check 'Add Python to PATH' (IMPORTANT)" -ForegroundColor Yellow -Log Write-CCLSHost " - Ensure 'pip' is selected in the optional features (IMPORTANT)" -ForegroundColor Yellow -Log # Start the installer $process = Start-Process -FilePath $tempDownloadPath -Wait -PassThru # Handle different exit codes if ($process.ExitCode -eq 0) { Write-CCLSHost "Python installation process completed successfully." -ForegroundColor Green -Log } elseif ($process.ExitCode -eq 1602) { Write-CCLSHost "Installation was cancelled by the user." -ForegroundColor Yellow -Log } else { Write-CCLSHost "Python installer exited with code: $($process.ExitCode)" -ForegroundColor Yellow -Log } Write-CCLSHost "To verify the installation, type 'python --version' in a new command prompt window." -ForegroundColor Cyan -Log } catch { Write-CCLSHost "Error launching Python installer: $($_.Exception.Message)" -ForegroundColor Red -Log } # Clean up the downloaded file try { if (Test-Path $tempDownloadPath) { Remove-Item -Path $tempDownloadPath -Force Write-CCLSHost "Cleaned up temporary installer file." -ForegroundColor Gray -Log -NoConsole } } catch { Write-CCLSHost "Note: Could not remove temporary installer file: $($_.Exception.Message)" -ForegroundColor Yellow -Log -NoConsole } } # Enhanced Search-Game function with game name search capability (FIXED) function Search-Game($id) { $settings = Initialize-Settings if ($settings.DevMode) { Write-CCLSHost "'search' is disabled while in Developer mode" -ForegroundColor Red -Log return } if (-not (Assert-DependenciesRequired -CommandName "search")) { return } # Check if this is a game name search request if ($id -eq "game" -and $args.Count -gt 0) { # This is actually a game name search, not an ID $searchTerm = $args -join " " Write-CCLSHost "#Attempting to search for games matching: '$searchTerm'" -Log -NoConsole # Perform the game name search $searchResult = Search-GameByName -SearchTerm $searchTerm if ($null -eq $searchResult) { # Search failed or was cancelled, error already displayed return } # Update the ID to the selected one and continue with normal processing $id = $searchResult Write-CCLSHost "#Selected game ID: $id, proceeding with search..." -Log -NoConsole } # Validate ID format - now supports both cg0000 and cb0000 formats if ($id -notmatch "^(cg|cb)\d{4}$") { Write-CCLSHost "Invalid ID format. Please use format 'cg0000' for games or 'cb0000' for bundles." -ForegroundColor Red -Log return } # Check if credentials are cached if ($null -eq $script:cachedCredentials) { Write-CCLSHost "Error: Not logged in. Please restart the application and log in." -ForegroundColor Red -Log return } # Determine if this is a game or bundle $isBundle = $id -match "^cb\d{4}$" $itemType = if ($isBundle) { "bundle" } else { "game" } try { # Set up request parameters with credentials $params = @{ Uri = "$cliApiUrl/search.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/2.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password id = $id } } Write-CCLSHost "#Searching for $itemType information..." -Log # Fetch data from server try { $response = Invoke-RestMethod @params # Check if the request was successful if (-not $response.success) { Write-CCLSHost "Error: $($response.message)" -ForegroundColor Red -Log return } $itemInfo = $response } catch { Write-CCLSHost "Error fetching $itemType information: $($_.Exception.Message)" -ForegroundColor Red -Log return } # Display item information in a formatted way Write-CCLSHost "`n==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "$($itemType.ToUpper()) Information for $($itemInfo.name) ($($itemInfo.id))" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "`nDescription:" -ForegroundColor Cyan -Log Write-CCLSHost $itemInfo.description -Log if ($itemInfo.safety_score -or $itemInfo.safety_level) { Write-CCLSHost "`nSafety:" -ForegroundColor Cyan -Log if ($itemInfo.safety_score) { Write-CCLSHost "Score: $($itemInfo.safety_score)" -Log } if ($itemInfo.safety_level) { Write-CCLSHost "Level: $($itemInfo.safety_level)" -Log } } Write-CCLSHost "`nDetails:" -ForegroundColor Cyan -Log # Handle different size properties for game vs bundle if ($isBundle) { if ($itemInfo.zipped_size) { Write-CCLSHost "Zipped Size: $($itemInfo.zipped_size)" -Log } if ($itemInfo.unzipped_size) { Write-CCLSHost "Unzipped Size: $($itemInfo.unzipped_size)" -Log } if ($itemInfo.games_included) { Write-CCLSHost "Games Included: $($itemInfo.games_included)" -Log } } else { if ($itemInfo.size) { Write-CCLSHost "Size: $($itemInfo.size)" -Log } } if ($itemInfo.version -and $itemInfo.version -ne "") { Write-CCLSHost "Version: $($itemInfo.version)" -Log } if (($itemInfo.online -and $itemInfo.online -ne "") -or ($itemInfo.steam -and $itemInfo.steam -ne "") -or ($itemInfo.epic -and $itemInfo.epic -ne "")) { Write-CCLSHost "`nAvailability:" -ForegroundColor Cyan -Log if ($itemInfo.online -and $itemInfo.online -ne "") { Write-CCLSHost "Online: $($itemInfo.online)" -Log } if ($itemInfo.steam -and $itemInfo.steam -ne "") { Write-CCLSHost "Steam: $($itemInfo.steam)" -Log } if ($itemInfo.epic -and $itemInfo.epic -ne "") { Write-CCLSHost "Epic: $($itemInfo.epic)" -Log } } if ($itemInfo.false_av -and $itemInfo.false_av -ne "") { Write-CCLSHost "`nNote: " -ForegroundColor Yellow -NoNewline -Log Write-CCLSHost "This $itemType may trigger false antivirus alerts: $($itemInfo.false_av)" -Log } # If it's a game, show system requirements if (-not $isBundle -and $itemInfo.system_requirements) { Write-CCLSHost "`nSystem Requirements:" -ForegroundColor Cyan -Log if ($itemInfo.system_requirements.minimum) { Write-CCLSHost "Minimum:" -ForegroundColor Yellow -Log foreach ($prop in $itemInfo.system_requirements.minimum.PSObject.Properties) { Write-CCLSHost " $($prop.Name): $($prop.Value)" -Log } } if ($itemInfo.system_requirements.recommended) { Write-CCLSHost "`nRecommended:" -ForegroundColor Yellow -Log foreach ($prop in $itemInfo.system_requirements.recommended.PSObject.Properties) { Write-CCLSHost " $($prop.Name): $($prop.Value)" -Log } } } # REMOVED: The "Press any key to return" prompt that was causing the extra keypress requirement } catch { Write-CCLSHost "An error occurred while processing $itemType information: $($_.Exception.Message)" -ForegroundColor Red -Log } } # Completely rewritten Search-GameByName function with simplified logic and "get {number}" support function Search-GameByName { param ( [string]$SearchTerm ) # Check if credentials are cached if ($null -eq $script:cachedCredentials) { Write-CCLSHost "Error: Not logged in. Please restart the application and log in." -ForegroundColor Red -Log return $null } try { # Set up request parameters with credentials to get the game library $params = @{ Uri = "$cliApiUrl/list.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/2.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password } } Write-CCLSHost "#Fetching game library for search..." -Log -NoConsole # Fetch library from server $response = Invoke-RestMethod @params # Check if the request was successful if (-not $response.success) { Write-CCLSHost "Error fetching game library: $($response.message)" -ForegroundColor Red -Log return $null } # Create a simple list to hold all items $allItems = New-Object System.Collections.ArrayList # Add games to the list if ($response.games) { foreach ($game in $response.games) { $null = $allItems.Add(@{ Name = $game.name Id = $game.id Type = "Game" }) } } # Add bundles to the list if ($response.bundles) { foreach ($bundle in $response.bundles) { $null = $allItems.Add(@{ Name = $bundle.name Id = $bundle.id Type = "Bundle" }) } } Write-CCLSHost "#Debug: Total items loaded: $($allItems.Count)" -Log -NoConsole # Validate search term $searchTermLower = $SearchTerm.ToLower().Trim() if ($searchTermLower.Length -lt 2) { Write-CCLSHost "Search term too short. Please use at least 2 characters." -ForegroundColor Red -Log return $null } # Create results list $results = New-Object System.Collections.ArrayList # Search through all items foreach ($item in $allItems) { $itemNameLower = $item.Name.ToLower().Trim() $score = 0 $matchType = "" # Simple scoring logic if ($itemNameLower -eq $searchTermLower) { # Exact match $score = 1000 $matchType = "Exact Match" } elseif ($itemNameLower.StartsWith($searchTermLower)) { # Starts with search term $score = 900 $matchType = "Starts With" } elseif ($itemNameLower.Contains($searchTermLower)) { # Contains search term $score = 800 $matchType = "Contains Term" } else { # Check if all words in search term are present $searchWords = $searchTermLower -split '\s+' | Where-Object { $_.Length -gt 0 } if ($searchWords.Count -gt 1) { $allWordsFound = $true foreach ($word in $searchWords) { if (-not $itemNameLower.Contains($word)) { $allWordsFound = $false break } } if ($allWordsFound) { $score = 700 $matchType = "All Words Match" } } } # Add to results if we have a match if ($score -gt 0) { $null = $results.Add(@{ Name = $item.Name Id = $item.Id Type = $item.Type Score = $score MatchType = $matchType }) # Debug output for VR HOT specifically if ($itemNameLower -eq "vr hot") { Write-CCLSHost "#Debug: Added VR HOT with score $score and match type '$matchType'" -Log -NoConsole } } } Write-CCLSHost "#Debug: Total matches found: $($results.Count)" -Log -NoConsole # Check if we have any results if ($results.Count -eq 0) { Write-CCLSHost "No games or bundles found matching '$SearchTerm'." -ForegroundColor Red -Log Write-CCLSHost "Try using different search terms or use 'search library' to see all available items." -ForegroundColor Yellow -Log return $null } # Sort results by score (manual sort to avoid PowerShell pipeline issues) $sortedResults = New-Object System.Collections.ArrayList # Convert to array and sort manually $resultArray = $results.ToArray() for ($i = 0; $i -lt $resultArray.Length - 1; $i++) { for ($j = $i + 1; $j -lt $resultArray.Length; $j++) { if ($resultArray[$j].Score -gt $resultArray[$i].Score) { $temp = $resultArray[$i] $resultArray[$i] = $resultArray[$j] $resultArray[$j] = $temp } } } # Take only the top 20 results $maxResults = [Math]::Min(20, $resultArray.Length) for ($i = 0; $i -lt $maxResults; $i++) { $null = $sortedResults.Add($resultArray[$i]) } Write-CCLSHost "#Debug: Sorted results count: $($sortedResults.Count)" -Log -NoConsole # Display search results Write-CCLSHost "`n==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "Search Results for '$SearchTerm' ($($sortedResults.Count) found)" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log for ($i = 0; $i -lt $sortedResults.Count; $i++) { $result = $sortedResults[$i] $displayNumber = $i + 1 # Color code by type $typeColor = if ($result.Type -eq "Game") { "White" } else { "Cyan" } Write-CCLSHost "[$displayNumber] " -ForegroundColor Yellow -NoNewline -Log Write-CCLSHost "$($result.Name) " -ForegroundColor $typeColor -NoNewline -Log Write-CCLSHost "($($result.Id)) " -ForegroundColor Gray -NoNewline -Log Write-CCLSHost "[$($result.Type)]" -ForegroundColor Green -Log } Write-CCLSHost "`nEnter the number to view info, or 'get [number]' to download (1-$($sortedResults.Count)), or press Enter to cancel:" -ForegroundColor Yellow -Log # Get user input while ($true) { Write-CCLSHost "Selection: " -ForegroundColor Cyan -NoNewline -Log $userInput = Read-Host Write-CCLSHost "$userInput" -NoConsole -Log # Check for empty input (cancel) if ([string]::IsNullOrWhiteSpace($userInput)) { Write-CCLSHost "Search cancelled." -ForegroundColor Yellow -Log return $null } # Check if input matches "get [number]" pattern if ($userInput.Trim() -match "^get\s+(\d+)$") { $selectedNumber = [int]$matches[1] # Validate number is in range if ($selectedNumber -ge 1 -and $selectedNumber -le $sortedResults.Count) { $selectedItem = $sortedResults[$selectedNumber - 1] Write-CCLSHost "#Calling Get-Game function to install: $($selectedItem.Name) ($($selectedItem.Id))" -NoConsole -Log # Call Get-Game function directly with the selected ID Get-Game -id $selectedItem.Id return $null # Return null since we're not returning an ID for search } else { Write-CCLSHost "Invalid selection. Please enter 'get [number]' where number is between 1 and $($sortedResults.Count)." -ForegroundColor Red -Log } } # Try to parse the input as a number (original behavior) else { $selectedNumber = 0 if ([int]::TryParse($userInput.Trim(), [ref]$selectedNumber)) { # Validate number is in range if ($selectedNumber -ge 1 -and $selectedNumber -le $sortedResults.Count) { $selectedItem = $sortedResults[$selectedNumber - 1] Write-CCLSHost "Selected: $($selectedItem.Name) ($($selectedItem.Id))" -ForegroundColor Green -Log return $selectedItem.Id } else { Write-CCLSHost "Invalid selection. Please enter a number between 1 and $($sortedResults.Count)." -ForegroundColor Red -Log } } else { Write-CCLSHost "Invalid input. Please enter a number, 'get [number]', or press Enter to cancel." -ForegroundColor Red -Log } } } } catch { Write-CCLSHost "Error during game search: $($_.Exception.Message)" -ForegroundColor Red -Log return $null } } # Function to format file size function Format-FileSize { param ([string]$Size) if ([string]::IsNullOrEmpty($Size)) { return "Unknown Size" } # Extract numeric part and unit if ($Size -match "(\d+\.?\d*)\s*(GB|MB|KB|B)") { $value = [double]$matches[1] $unit = $matches[2] return "$value $unit" } return $Size } function Resolve-GameNameToId { param ( [string]$GameName ) # Check if credentials are cached if ($null -eq $script:cachedCredentials) { Write-CCLSHost "Error: Not logged in. Please restart the application and log in." -ForegroundColor Red -Log return $null } try { # Set up request parameters with credentials to get the game library $params = @{ Uri = "$cliApiUrl/list.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/2.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password } } Write-CCLSHost "#Resolving game name '$GameName' to game ID..." -Log -NoConsole # Fetch library from server $response = Invoke-RestMethod @params # Check if the request was successful if (-not $response.success) { Write-CCLSHost "Error fetching game library: $($response.message)" -ForegroundColor Red -Log return $null } # Search for the game by name (case-insensitive) $matchedGame = $null $gameNameLower = $GameName.ToLower().Trim() # First try exact match foreach ($game in $response.games) { if ($game.name.ToLower().Trim() -eq $gameNameLower) { $matchedGame = $game break } } # If no exact match, try partial match if ($null -eq $matchedGame) { foreach ($game in $response.games) { if ($game.name.ToLower().Contains($gameNameLower)) { if ($null -eq $matchedGame) { $matchedGame = $game } else { # Multiple matches found Write-CCLSHost "Multiple games found matching '$GameName':" -ForegroundColor Yellow -Log # Show all matches $allMatches = @() foreach ($g in $response.games) { if ($g.name.ToLower().Contains($gameNameLower)) { $allMatches += $g Write-CCLSHost " - $($g.name) ($($g.id))" -ForegroundColor Yellow -Log } } Write-CCLSHost "Please be more specific or use the game ID directly." -ForegroundColor Yellow -Log return $null } } } } if ($null -ne $matchedGame) { Write-CCLSHost "#Found game: '$($matchedGame.name)' with ID: $($matchedGame.id)" -Log -NoConsole return $matchedGame.id } else { Write-CCLSHost "Game '$GameName' not found in the library." -ForegroundColor Red -Log Write-CCLSHost "Use 'search library' to see all available games." -ForegroundColor Yellow -Log return $null } } catch { Write-CCLSHost "Error resolving game name: $($_.Exception.Message)" -ForegroundColor Red -Log return $null } } function Get-Game { param ( [string]$id, [switch]$SkipConfirmation ) $settings = Initialize-Settings if ($settings.DevMode) { Write-CCLSHost "'get' is disabled while in Developer mode" -ForegroundColor Red -Log return } if (-not (Assert-DependenciesRequired -CommandName "get")) { return } # Check if this is a game name resolution request if ($id -eq "game" -and $args.Count -gt 0) { # This is actually a game name, not an ID $gameName = $args -join " " Write-CCLSHost "#Attempting to resolve game name: '$gameName'" -Log -NoConsole # Resolve the game name to an ID $resolvedId = Resolve-GameNameToId -GameName $gameName if ($null -eq $resolvedId) { # Resolution failed, error already displayed return } # Update the ID to the resolved one and continue with normal processing $id = $resolvedId Write-CCLSHost "#Resolved to ID: $id, proceeding with download..." -Log -NoConsole } # Validate ID format - now supports both cg0000 and cb0000 formats if ($id -notmatch "^(cg|cb)\d{4}$") { Write-CCLSHost "Invalid ID format. Please use format 'cg0000' for games or 'cb0000' for bundles." -ForegroundColor Red -Log return } # Check if credentials are cached if ($script:cachedCredentials -ne $null -and ($null -eq $script:cachedCredentials.Username -or $null -eq $script:cachedCredentials.Password)) { Write-CCLSHost "Error: Not logged in. Please restart the application and log in." -ForegroundColor Red -Log return } # Determine if this is a game or bundle $isBundle = $id -match "^cb\d{4}$" $itemType = if ($isBundle) { "bundle" } else { "game" } # Check if setup has been completed $settings = Initialize-Settings if (-not $settings.HasCompletedSetup) { Write-CCLSHost "ALERT, you must run the 'setup' command before downloading." -ForegroundColor Red -Log return } # Ensure download directories exist if (-not (Test-Path $settings.DownloadPath)) { try { New-Item -ItemType Directory -Path $settings.DownloadPath -Force | Out-Null } catch { Write-CCLSHost "Error creating download directory: $($_.Exception.Message)" -ForegroundColor Red -Log return } } if (-not (Test-Path $settings.TempDownloadPath)) { try { New-Item -ItemType Directory -Path $settings.TempDownloadPath -Force | Out-Null } catch { Write-CCLSHost "Error creating temporary download directory: $($_.Exception.Message)" -ForegroundColor Red -Log return } } # Get game info and download information try { # First, fetch the detailed information about the game $infoParams = @{ Uri = "$cliApiUrl/search.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/2.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password id = $id } } Write-CCLSHost "#Fetching $itemType information..." -Log -NoConsole try { $itemInfo = Invoke-RestMethod @infoParams if (-not $itemInfo.success) { Write-CCLSHost "Error fetching $itemType information: $($itemInfo.message)" -ForegroundColor Red -Log return } Write-CCLSHost "#Successfully fetched $itemType information for later use" -Log -NoConsole } catch { Write-CCLSHost "Error fetching $itemType information: $($_.Exception.Message)" -ForegroundColor Red -Log return } # Now fetch download information $params = @{ Uri = "$cliApiUrl/get.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/2.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password id = $id } } Write-CCLSHost "#Fetching $itemType download information..." -Log -NoConsole $response = Invoke-RestMethod @params if (-not $response.success) { Write-CCLSHost "Error: $($response.message)" -ForegroundColor Red -Log return } $itemName = $response.name $itemId = $response.id $downloadUrl = $response.download_url $itemSize = $response.size # Create download file name from URL $fileName = Split-Path -Path $downloadUrl -Leaf if ([string]::IsNullOrEmpty($fileName)) { $fileName = "$itemId.7z" } $downloadPath = Join-Path -Path $settings.TempDownloadPath -ChildPath $fileName # === PHASE 1: Check for existing CG file (regardless of folder name) === Write-CCLSHost "#Scanning all folders for existing $itemId.json file..." -Log -NoConsole $existingCgFolder = $null $allFolders = Get-ChildItem -Path $settings.DownloadPath -Directory -ErrorAction SilentlyContinue foreach ($folder in $allFolders) { $cgJsonPath = Join-Path -Path $folder.FullName -ChildPath "$itemId.json" if (Test-Path $cgJsonPath) { $existingCgFolder = $folder.FullName Write-CCLSHost "#Found existing $itemId.json in folder: $($folder.Name)" -Log -NoConsole break } } $replaceExistingInstallation = $false $cgFileBasedReplacement = $false if ($existingCgFolder) { # Found existing CG file - show warning and ask for replacement $folderSize = Get-FolderSize -Path $existingCgFolder $formattedSize = Format-Size -Size $folderSize $folderName = Split-Path -Path $existingCgFolder -Leaf Write-CCLSHost "WARNING: $itemType with ID $itemId already exists in installation directory:" -ForegroundColor Yellow -Log Write-CCLSHost " - $existingCgFolder ($formattedSize)" -ForegroundColor Yellow -Log if (-not $SkipConfirmation) { Write-CCLSHost "DO YOU WISH TO REPLACE OLD $($itemType.ToUpper()) FILES WITH NEW ONES? (Y/N)" -ForegroundColor Yellow -Log $replaceConfirmation = Read-Host Write-CCLSHost "$replaceConfirmation" -NoConsole -Log if ($replaceConfirmation.ToLower() -ne "y") { Write-CCLSHost "Download cancelled. Existing installation will not be modified." -ForegroundColor Cyan -Log return } $replaceExistingInstallation = $true $cgFileBasedReplacement = $true } else { Write-CCLSHost "#SkipConfirmation enabled, will replace existing installation" -Log -NoConsole $replaceExistingInstallation = $true $cgFileBasedReplacement = $true } } else { Write-CCLSHost "#No existing CG file found for $itemId - proceeding with fresh download" -Log -NoConsole } # Confirm download with user (only if we're not replacing based on CG file) if (-not $cgFileBasedReplacement) { Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "$($itemType.ToUpper()) Download: $itemName ($itemId)" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "Size: $itemSize" -ForegroundColor Yellow -Log if (-not $SkipConfirmation) { Write-CCLSHost "Do you want to proceed with the download? (Y/N)" -ForegroundColor Yellow -Log $confirmation = Read-Host Write-CCLSHost "$confirmation" -NoConsole -Log if ($confirmation.ToLower() -ne "y") { Write-CCLSHost "Download cancelled by user." -ForegroundColor Yellow -Log return } } else { Write-CCLSHost "#Automatically proceeding with download (confirmation skipped)..." -ForegroundColor Green -Log -NoConsole } } else { # Show download info for replacement Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "$($itemType.ToUpper()) Download: $itemName ($itemId)" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "Size: $itemSize" -ForegroundColor Yellow -Log } # === PHASE 2: Remove existing CG-based installation if needed === if ($cgFileBasedReplacement -and $existingCgFolder) { Write-CCLSHost "Removing existing installation before downloading..." -ForegroundColor Cyan -Log try { if (Test-Path -Path $existingCgFolder) { Remove-Item -Path $existingCgFolder -Recurse -Force Write-CCLSHost " - Removed: $existingCgFolder" -ForegroundColor Green -Log } else { Write-CCLSHost " - Folder already removed: $existingCgFolder" -ForegroundColor Yellow -Log } } catch { Write-CCLSHost " - Failed to remove: $existingCgFolder - $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "Download cancelled due to removal failure." -ForegroundColor Red -Log return } } # Flag to track if download was successfully completed $downloadSuccessful = $false try { # Try to invoke Python to check if it's available $pythonVersion = python --version 2>&1 $pythonAvailable = $true Write-CCLSHost "#Python detected: $pythonVersion" -Log -NoConsole # Determine the actual location of the Python script $scriptLocation = if ($PSScriptRoot) { # If running from a script, use its location $PSScriptRoot } else { # If running in console, use current directory (Get-Location).Path } $pythonScript = Join-Path -Path $scriptLocation -ChildPath "ccls_downloader.py" # Debug the script path Write-CCLSHost "#Python script path: $pythonScript" -Log -NoConsole # Always recreate the Python script on each run if (Test-Path $pythonScript) { Remove-Item -Path $pythonScript -Force Write-CCLSHost "#Removed existing Python script" -Log -NoConsole } # Create a new copy of the script with the latest code Write-CCLSHost "#Creating Python script at $pythonScript" -Log -NoConsole # Python downloader script content - PLACEHOLDER FOR PYTHON CODE $pythonCode = @' #!/usr/bin/env python3 """ CCLS High-Speed Downloader (Silent Version with Ctrl+Z stop) -------------------------------------------- A high-performance Python script for downloading games from CCLS with improved connection handling, retry logic, resume capabilities, and URL encoding fixes, with minimal output to keep the console clean. Uses Ctrl+Z to stop downloads. """ import os import sys import time import requests import signal import random import msvcrt # For Windows key detection import urllib.parse from datetime import datetime, timedelta from threading import Thread, Event # Constants for downloading CHUNK_SIZE = 1024 * 1024 # 1MB chunks for better performance USER_AGENT = "CCLS-CLI/2.0 (Python Downloader)" MAX_RETRIES = 5 # Increased from 3 CONNECT_TIMEOUT = 30 # Connection timeout in seconds READ_TIMEOUT = 60 # Read timeout in seconds RETRY_BASE_DELAY = 2 # Base delay for exponential backoff RETRY_MAX_DELAY = 60 # Maximum delay between retries DEBUG_MODE = False # Set to True to enable debug messages # Global event for signaling cancellation between threads cancellation_event = Event() # Thread for checking for Ctrl+Z key press def key_monitor(): """Monitor for Ctrl+Z (ASCII 26) key press to cancel downloads""" while not cancellation_event.is_set(): if msvcrt.kbhit(): key = msvcrt.getch() # Check for Ctrl+Z (ASCII 26, or SUB character) if key == b'\x1a': print("\nCtrl+Z detected - Cancelling download and removing partial file...") cancellation_event.set() break time.sleep(0.1) # Small sleep to prevent CPU hogging def debug_print(message): """Print only if debug mode is enabled""" if DEBUG_MODE: print(message) def format_size(size_bytes): """Format bytes into human-readable format""" if size_bytes >= 1_000_000_000: # GB return f"{size_bytes / 1_000_000_000:.2f} GB" elif size_bytes >= 1_000_000: # MB return f"{size_bytes / 1_000_000:.2f} MB" elif size_bytes >= 1000: # KB return f"{size_bytes / 1000:.2f} KB" else: return f"{size_bytes} B" def format_time(seconds): """Format seconds into HH:MM:SS format""" hours = seconds // 3600 minutes = (seconds % 3600) // 60 secs = seconds % 60 if hours > 0: return f"{hours:02d}:{minutes:02d}:{secs:02d}" else: return f"{minutes:02d}:{secs:02d}" def normalize_url(url): """ Normalize URL by ensuring proper encoding of special characters while preserving URL structure """ # Parse the URL into components parsed = urllib.parse.urlparse(url) # Split the path by '/' and encode each segment separately path_parts = parsed.path.split('/') encoded_parts = [] for part in path_parts: # Dont double-encode already encoded segments, but ensure other parts are encoded if '%' in part and any(c.isalnum() for c in part.split('%')[1][:2]): encoded_parts.append(part) else: # Encode the part, preserving allowed characters encoded_parts.append(urllib.parse.quote(part, safe='')) # Reconstruct the path encoded_path = '/'.join(encoded_parts) # Build the URL with proper encoding normalized_url = urllib.parse.urlunparse(( parsed.scheme, parsed.netloc, encoded_path, parsed.params, parsed.query, parsed.fragment )) return normalized_url def get_file_size(url, headers): """Get the size of the file to be downloaded""" # First, make sure the URL is properly encoded normalized_url = normalize_url(url) for attempt in range(MAX_RETRIES): try: # Only get the headers, dont download the content yet response = requests.head(normalized_url, headers=headers, timeout=(CONNECT_TIMEOUT, READ_TIMEOUT), allow_redirects=True) response.raise_for_status() # Return the content length if available content_length = response.headers.get('content-length') if content_length: return int(content_length) # If we cant get the size via HEAD request, return None return None except (requests.exceptions.RequestException, IOError) as e: delay = min(RETRY_BASE_DELAY * (2 ** attempt) + random.uniform(0, 1), RETRY_MAX_DELAY) if attempt < MAX_RETRIES - 1: debug_print(f"Error getting file size: {str(e)}. Retrying in {delay:.1f} seconds...") time.sleep(delay) else: debug_print(f"Failed to get file size after {MAX_RETRIES} attempts: {str(e)}") return None def can_resume(url, headers): """Check if the server supports resuming downloads""" normalized_url = normalize_url(url) try: # Add range header to check if the server supports it resume_headers = headers.copy() resume_headers['Range'] = 'bytes=0-0' response = requests.head(normalized_url, headers=resume_headers, timeout=(CONNECT_TIMEOUT, READ_TIMEOUT), allow_redirects=True) # If we get a 206 status, the server supports resume return response.status_code == 206 except Exception as e: debug_print(f"Error checking resume capability: {str(e)}") # If theres any error, assume we cant resume to be safe return False def download_file(url, destination, game_name, game_id, expected_size=None): """Download a file with progress tracking and resume support""" global cancellation_event cancellation_event.clear() # Reset the cancellation event # Start key monitoring thread key_thread = Thread(target=key_monitor) key_thread.daemon = True key_thread.start() # Print information about stopping print("\nDownload started! Press Ctrl+Z to stop the download at any time.\n") headers = { "User-Agent": USER_AGENT, "Connection": "keep-alive", "Accept-Encoding": "gzip, deflate" } # Normalize the URL to handle special characters normalized_url = normalize_url(url) debug_print(f"Using normalized URL: {normalized_url}") # Get the file size if not provided total_size = expected_size if total_size is None: total_size = get_file_size(normalized_url, headers) # Check if we can resume supports_resume = can_resume(normalized_url, headers) debug_print(f"Server {'supports' if supports_resume else 'does not support'} resume capability.") # Check if the file already exists and if we should resume downloaded = 0 if os.path.exists(destination) and supports_resume: downloaded = os.path.getsize(destination) if total_size and downloaded >= total_size: print(f"\nFile already completely downloaded: {destination}") cancellation_event.set() # Signal to stop key monitor thread return True elif downloaded > 0: print(f"\nResuming download from {format_size(downloaded)}") headers['Range'] = f'bytes={downloaded}-' # Prepare for progress tracking start_time = time.time() init_downloaded = downloaded # Keep the initial download amount for speed calculation last_update_time = start_time last_downloaded = downloaded # For calculating current download speed # Setup for retrying connection for attempt in range(MAX_RETRIES): # Check if download was cancelled by user if cancellation_event.is_set(): print("\nDownload cancelled by user. Removing partial file...") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as e: print(f"Note: Could not delete partial file: {str(e)}") return False try: # Open the file in append mode if resuming, otherwise write mode file_mode = 'ab' if downloaded > 0 else 'wb' with requests.get(normalized_url, headers=headers, stream=True, timeout=(CONNECT_TIMEOUT, READ_TIMEOUT), allow_redirects=True) as response: response.raise_for_status() # If we requested a range but got the whole file, adjust our counter if downloaded > 0 and response.status_code != 206: debug_print("Warning: Server doesnt support resuming. Starting from beginning.") downloaded = 0 file_mode = 'wb' # Update total_size if we can get it from headers if total_size is None and 'content-length' in response.headers: total_size = int(response.headers['content-length']) + downloaded print(f"[Download Progress - {game_name} ({game_id})]") with open(destination, file_mode) as f: for chunk in response.iter_content(chunk_size=CHUNK_SIZE): # Check for cancellation if cancellation_event.is_set(): print("\nDownload cancelled by user. Removing partial file...") f.close() # Close file handle before deleting try: os.remove(destination) print(f"Partial file deleted successfully.") except Exception as e: print(f"Note: Could not delete partial file: {str(e)}") return False if chunk: f.write(chunk) downloaded += len(chunk) current_time = time.time() # Update progress every 0.5 seconds if (current_time - last_update_time) >= 0.5: last_update_time = current_time # Calculate progress values elapsed_time = current_time - start_time elapsed_seconds = int(elapsed_time) progress_percent = int((downloaded / total_size) * 100) if total_size and total_size > 0 else 0 # Calculate overall average speed avg_download_speed = (downloaded - init_downloaded) / elapsed_time if elapsed_time > 0 else 0 avg_download_speed_mbps = avg_download_speed * 8 / 1024 / 1024 # Convert to Mbps # Calculate current window speed (last 0.5 seconds) current_window_size = downloaded - last_downloaded current_speed = current_window_size / (current_time - last_update_time + 0.001) current_speed_mbps = current_speed * 8 / 1024 / 1024 # Convert to Mbps last_downloaded = downloaded # Calculate remaining time based on average speed remaining_bytes = total_size - downloaded if total_size else 0 if avg_download_speed > 0 and remaining_bytes > 0: remaining_seconds = int(remaining_bytes / avg_download_speed) else: remaining_seconds = 0 # Simple output - replace previous line with new status # Carriage return to move cursor to beginning of line sys.stdout.write("\r" + " " * 80 + "\r") # Clear line # Print progress information if total_size and total_size > 0: prog_str = f"Progress: {progress_percent}% | " else: prog_str = "" # Show actual progress info size_info = f"of {format_size(total_size)}" if total_size else "" status = f"{prog_str}Downloaded: {format_size(downloaded)} {size_info} | Speed: {avg_download_speed_mbps:.2f} Mbps" sys.stdout.write(status) sys.stdout.flush() # Final update elapsed_time = time.time() - start_time elapsed_seconds = int(elapsed_time) avg_download_speed = (downloaded - init_downloaded) / elapsed_time if elapsed_time > 0 else 0 avg_download_speed_mbps = avg_download_speed * 8 / 1024 / 1024 # Print final stats on new lines print("\n\nDownload completed successfully!") print(f"Total Size: {format_size(downloaded)}") print(f"Average Speed: {avg_download_speed_mbps:.2f} Mbps") print(f"Time Elapsed: {format_time(elapsed_seconds)}") # Signal to stop key monitor thread cancellation_event.set() return True except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: # Handle connection timeout with exponential backoff retry if cancellation_event.is_set(): print("\nDownload cancelled by user. Removing partial file...") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as e: print(f"Note: Could not delete partial file: {str(e)}") return False delay = min(RETRY_BASE_DELAY * (2 ** attempt) + random.uniform(0, 1), RETRY_MAX_DELAY) if attempt < MAX_RETRIES - 1: print(f"\nConnection timed out or lost: {str(e)}. Retrying in {delay:.1f} seconds...") print(f"Downloaded so far: {format_size(downloaded)}") time.sleep(delay) # Update headers for resuming from the current position headers['Range'] = f'bytes={downloaded}-' last_update_time = time.time() # Reset the update timer # Print a new header for the next attempt print(f"\n[Download Progress - {game_name} ({game_id}) - Attempt {attempt+2}]") else: print(f"\nDownload failed after {MAX_RETRIES} attempts: {str(e)}") print(f"Note: Partial file will be removed.") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as ex: print(f"Note: Could not delete partial file: {str(ex)}") cancellation_event.set() # Signal to stop key monitor thread return False except requests.exceptions.RequestException as e: # Handle other request exceptions if cancellation_event.is_set(): print("\nDownload cancelled by user. Removing partial file...") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as e: print(f"Note: Could not delete partial file: {str(e)}") return False if attempt < MAX_RETRIES - 1: delay = min(RETRY_BASE_DELAY * (2 ** attempt) + random.uniform(0, 1), RETRY_MAX_DELAY) print(f"\nDownload error: {str(e)}. Retrying in {delay:.1f} seconds...") time.sleep(delay) # Update headers for resuming from the current position headers['Range'] = f'bytes={downloaded}-' last_update_time = time.time() # Print a new header for the next attempt print(f"\n[Download Progress - {game_name} ({game_id}) - Attempt {attempt+2}]") else: print(f"\nDownload failed after {MAX_RETRIES} attempts: {str(e)}") print(f"Note: Partial file will be removed.") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as ex: print(f"Note: Could not delete partial file: {str(ex)}") cancellation_event.set() # Signal to stop key monitor thread return False except IOError as e: # Handle file I/O errors print(f"\nFile I/O error: {str(e)}") print(f"Note: Partial file will be removed.") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as ex: print(f"Note: Could not delete partial file: {str(ex)}") cancellation_event.set() # Signal to stop key monitor thread return False cancellation_event.set() # Signal to stop key monitor thread return False def main(): """Main entry point for the script""" # Check if we have enough arguments if len(sys.argv) < 5: print("Usage: python ccls_downloader.py []") return 1 try: download_url = sys.argv[1] output_file = sys.argv[2] game_name = sys.argv[3] game_id = sys.argv[4] debug_print(f"Download URL: {download_url}") debug_print(f"Output file: {output_file}") debug_print(f"Item name: {game_name}") debug_print(f"Item ID: {game_id}") # Optional size parameter expected_size = None if len(sys.argv) > 5: try: expected_size = int(sys.argv[5]) debug_print(f"Expected size: {format_size(expected_size)}") except ValueError: debug_print(f"Warning: Invalid size parameter '{sys.argv[5]}', will use content-length from server") # Create output directory if it doesnt exist output_dir = os.path.dirname(output_file) if output_dir and not os.path.exists(output_dir): try: os.makedirs(output_dir) debug_print(f"Created output directory: {output_dir}") except Exception as e: print(f"Error: Could not create directory: {str(e)}") return 1 # Start download result = download_file(download_url, output_file, game_name, game_id, expected_size) # Return success or failure code return 0 if result else 1 except Exception as e: print(f"Error: Unexpected error: {str(e)}") if DEBUG_MODE: import traceback traceback.print_exc() return 1 if __name__ == "__main__": try: sys.exit(main()) except KeyboardInterrupt: # This shouldnt be reached now that we use our own key handler print("\nKeyboard interrupt detected.") sys.exit(1) '@ Set-Content -Path $pythonScript -Value $pythonCode -Encoding UTF8 # If we have Python and the downloader script, use it if ($pythonAvailable -and (Test-Path $pythonScript)) { Write-CCLSHost "#Using Python high-speed downloader" -Log -NoConsole # Convert the item size to bytes if possible (for progress calculation) $sizeInBytes = $null if ($itemSize -match "(\d+\.?\d*)\s*(GB|MB|KB|B)") { $value = [double]$matches[1] $unit = $matches[2] switch ($unit) { "GB" { $sizeInBytes = [math]::Round($value * 1GB) } "MB" { $sizeInBytes = [math]::Round($value * 1MB) } "KB" { $sizeInBytes = [math]::Round($value * 1KB) } "B" { $sizeInBytes = [math]::Round($value) } } } try { # Build the command line with proper quoting $argList = @() $argList += "`"$pythonScript`"" $argList += "`"$downloadUrl`"" $argList += "`"$downloadPath`"" $argList += "`"$itemName`"" $argList += "`"$itemId`"" if ($sizeInBytes) { $argList += "$sizeInBytes" } $commandLine = "python $($argList -join ' ')" Write-CCLSHost "#Running command: $commandLine" -Log -NoConsole # Run Python as a normal process without redirecting output # This allows it to directly handle console output $process = Start-Process python -ArgumentList $argList -NoNewWindow -PassThru -Wait if ($process.ExitCode -ne 0) { # Check if a partial file exists and delete it if (Test-Path $downloadPath) { Write-CCLSHost "Download was cancelled. Removing partial file..." -ForegroundColor Yellow -Log try { Remove-Item -Path $downloadPath -Force Write-CCLSHost "Partial file removed successfully." -ForegroundColor Yellow -Log } catch { Write-CCLSHost "Note: Could not remove partial file: $($_.Exception.Message)" -ForegroundColor Yellow -Log } } else { Write-CCLSHost "Download was cancelled." -ForegroundColor Yellow -Log } # Return early without attempting extraction return } # If we got here, download was successful $downloadSuccessful = $true } catch { Write-CCLSHost "Error running Python downloader: $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "Download failed. Please ensure all dependencies are properly installed." -ForegroundColor Red -Log return } } else { Write-CCLSHost "Python downloader not available. Please install required dependencies." -ForegroundColor Red -Log return } } catch { Write-CCLSHost "Python not found. Please install required dependencies." -ForegroundColor Red -Log return } # === PHASE 3: Post-download extraction and JSON creation === if ($downloadSuccessful) { Write-CCLSHost "#Download completed successfully, preparing for extraction..." -Log -NoConsole Write-CCLSHost "#Starting robust extraction process..." -Log -NoConsole try { # Call the robust extraction function with temporary extraction $extractionResult = Start-RobustExtraction -settings $settings -itemId $itemId -SkipConfirmation $SkipConfirmation # After extraction, save the game info JSON to the correct location if ($extractionResult.Success -and $extractionResult.FinalFolder) { # Path for the info JSON file $infoJsonPath = Join-Path -Path $extractionResult.FinalFolder -ChildPath "$itemId.json" # Save item info to JSON file try { # Ensure the directory exists if (-not (Test-Path $extractionResult.FinalFolder)) { New-Item -ItemType Directory -Path $extractionResult.FinalFolder -Force | Out-Null } # Convert and save the JSON with proper formatting $jsonContent = $itemInfo | ConvertTo-Json -Depth 10 Set-Content -Path $infoJsonPath -Value $jsonContent -Encoding UTF8 -Force # Verify the file was created successfully if (Test-Path $infoJsonPath) { $fileSize = (Get-Item $infoJsonPath).Length Write-CCLSHost "Game information saved successfully to: $infoJsonPath (Size: $fileSize bytes)" -ForegroundColor Green -Log } else { Write-CCLSHost "Warning: JSON file creation verification failed" -ForegroundColor Yellow -Log } } catch { Write-CCLSHost "Error saving game information: $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "Attempted path: $infoJsonPath" -ForegroundColor Red -Log } } else { Write-CCLSHost "Warning: Could not determine final folder location for JSON file creation" -ForegroundColor Yellow -Log } } catch { Write-CCLSHost "An error occurred during extraction: $($_.Exception.Message)" -ForegroundColor Red -Log } } } catch { Write-CCLSHost "An error occurred: $($_.Exception.Message)" -ForegroundColor Red -Log } } # === ROBUST EXTRACTION FUNCTION WITH TEMPORARY FOLDER === function Start-RobustExtraction { param ( [object]$settings, [string]$itemId, [switch]$SkipConfirmation ) $tempDownloadPath = $settings.TempDownloadPath $downloadPath = $settings.DownloadPath # Check if 7-Zip is installed $7zipPath = "C:\Program Files\7-Zip\7z.exe" if (-not (Test-Path -Path $7zipPath)) { $alternativePaths = @( "${env:ProgramFiles(x86)}\7-Zip\7z.exe", ".\7zip\7z.exe" ) $found = $false foreach ($path in $alternativePaths) { if (Test-Path -Path $path) { $7zipPath = $path $found = $true break } } if (-not $found) { Write-CCLSHost "Error: 7-Zip is not installed at any expected location." -ForegroundColor Red -Log Write-CCLSHost "Please install 7-Zip using the 'install 7zip' command." -ForegroundColor Yellow -Log Write-CCLSHost "Once you have installed 7zip run 'extract $itemId -y' to extract the game" -ForegroundColor Yellow -Log return @{ Success = $false; FinalFolder = $null } } } # Get all .7z files in the temp download path $7zFiles = Get-ChildItem -Path $tempDownloadPath -Filter "*.7z" -ErrorAction SilentlyContinue if ($7zFiles.Count -eq 0) { Write-CCLSHost "No .7z files found in '$tempDownloadPath'." -ForegroundColor Yellow -Log return @{ Success = $false; FinalFolder = $null } } # Generate random 16-bit hash for temporary extraction folder $randomHash = -join ((0..15) | ForEach-Object { '{0:X}' -f (Get-Random -Maximum 16) }) $tempExtractionPath = Join-Path -Path $downloadPath -ChildPath ".temp-$randomHash" Write-CCLSHost "#Creating temporary extraction directory: $tempExtractionPath" -Log -NoConsole try { # Create temporary extraction directory New-Item -ItemType Directory -Path $tempExtractionPath -Force | Out-Null $overallSuccess = $true $extractedFolders = @() # Process each .7z file foreach ($file in $7zFiles) { $filePath = $file.FullName # Extract the file to temporary directory Write-CCLSHost "Extracting: $filePath to temporary location" -ForegroundColor Cyan -Log # Capture and suppress 7-Zip output, only show errors if extraction fails $extractionOutput = & "$7zipPath" x "$filePath" -o"$tempExtractionPath" -y 2>&1 $extractionSuccess = $? if ($extractionSuccess -and $LASTEXITCODE -eq 0) { Write-CCLSHost "Extraction successful. Deleting original file: $filePath" -ForegroundColor Green -Log Remove-Item -Path $filePath -Force } else { Write-CCLSHost "Error extracting file, 7zip could not be found on the system" -ForegroundColor Red -Log Write-CCLSHost "Type, 'check' for more info on fixing this issue" -ForegroundColor Red -Log Remove-Item -Path $filePath -Force $overallSuccess = $false } } if (-not $overallSuccess) { # Clean up temp directory if extraction failed if (Test-Path $tempExtractionPath) { Remove-Item -Path $tempExtractionPath -Recurse -Force } return @{ Success = $false; FinalFolder = $null } } # Get the folders that were extracted to the temporary directory $extractedItems = Get-ChildItem -Path $tempExtractionPath -ErrorAction SilentlyContinue $extractedFolders = $extractedItems | Where-Object { $_.PSIsContainer } if ($extractedFolders.Count -eq 0) { Write-CCLSHost "No folders found in temporary extraction directory. Looking for files..." -ForegroundColor Yellow -Log # If no folders, maybe files were extracted directly $extractedFiles = $extractedItems | Where-Object { -not $_.PSIsContainer } if ($extractedFiles.Count -gt 0) { Write-CCLSHost "Files were extracted directly to temporary directory. Creating container folder..." -ForegroundColor Yellow -Log # Move all files to a subfolder named after the item $containerFolderName = "ExtractedGame" $containerFolderPath = Join-Path -Path $tempExtractionPath -ChildPath $containerFolderName New-Item -ItemType Directory -Path $containerFolderPath -Force | Out-Null foreach ($file in $extractedFiles) { Move-Item -Path $file.FullName -Destination $containerFolderPath -Force } # Update the extracted folders list $extractedFolders = @(Get-Item $containerFolderPath) } } if ($extractedFolders.Count -eq 0) { Write-CCLSHost "No content found after extraction." -ForegroundColor Red -Log # Clean up temp directory if (Test-Path $tempExtractionPath) { Remove-Item -Path $tempExtractionPath -Recurse -Force } return @{ Success = $false; FinalFolder = $null } } Write-CCLSHost "#Found $($extractedFolders.Count) folder(s) in temporary extraction directory" -Log -NoConsole # Check for folder name conflicts in the main downloads directory $conflictingFolders = @() $existingFolders = Get-ChildItem -Path $downloadPath -Directory -ErrorAction SilentlyContinue foreach ($extractedFolder in $extractedFolders) { $extractedFolderName = $extractedFolder.Name Write-CCLSHost "#Checking for conflicts with extracted folder: $extractedFolderName" -Log -NoConsole # Check if a folder with the same name exists in the downloads directory $conflictingFolder = $existingFolders | Where-Object { $_.Name -eq $extractedFolderName } if ($conflictingFolder) { # Found a conflict - check if it contains the same CG file $conflictCgPath = Join-Path -Path $conflictingFolder.FullName -ChildPath "$itemId.json" if (-not (Test-Path $conflictCgPath)) { # Conflict found and it doesn't contain our CG file $conflictingFolders += @{ Name = $extractedFolderName Path = $conflictingFolder.FullName Size = Get-FolderSize -Path $conflictingFolder.FullName } Write-CCLSHost "#Found conflicting folder without matching CG file: $($conflictingFolder.FullName)" -Log -NoConsole } else { Write-CCLSHost "#Found folder with same name but it contains our CG file - this should have been handled in Phase 1" -Log -NoConsole } } } # Handle conflicts if any were found if ($conflictingFolders.Count -gt 0) { Write-CCLSHost "Folder name conflict detected!" -ForegroundColor Yellow -Log foreach ($conflict in $conflictingFolders) { $formattedSize = Format-Size -Size $conflict.Size Write-CCLSHost " - Folder '$($conflict.Name)' already exists ($formattedSize)" -ForegroundColor Yellow -Log } $shouldRemoveConflicts = $true if (-not $SkipConfirmation) { Write-CCLSHost "Do you wish to delete these conflicting folders and proceed with installation? (Y/N)" -ForegroundColor Yellow -Log $conflictConfirmation = Read-Host Write-CCLSHost "$conflictConfirmation" -NoConsole -Log if ($conflictConfirmation.ToLower() -ne "y") { $shouldRemoveConflicts = $false } } else { Write-CCLSHost "#SkipConfirmation enabled, will remove conflicting folders automatically" -Log -NoConsole } if ($shouldRemoveConflicts) { # Remove conflicting folders foreach ($conflict in $conflictingFolders) { try { Remove-Item -Path $conflict.Path -Recurse -Force Write-CCLSHost " - Removed conflicting folder: $($conflict.Path)" -ForegroundColor Green -Log } catch { Write-CCLSHost " - Failed to remove folder: $($conflict.Path) - $($_.Exception.Message)" -ForegroundColor Red -Log # Clean up temp directory and return failure if (Test-Path $tempExtractionPath) { Remove-Item -Path $tempExtractionPath -Recurse -Force } return @{ Success = $false; FinalFolder = $null } } } } else { Write-CCLSHost "Installation cancelled due to unresolved conflicts." -ForegroundColor Yellow -Log # Clean up temp directory if (Test-Path $tempExtractionPath) { Remove-Item -Path $tempExtractionPath -Recurse -Force } return @{ Success = $false; FinalFolder = $null } } } else { Write-CCLSHost "#No folder name conflicts detected" -Log -NoConsole } # Move extracted folders from temporary directory to main downloads directory $finalFolder = $null foreach ($extractedFolder in $extractedFolders) { $sourcePath = $extractedFolder.FullName $destinationPath = Join-Path -Path $downloadPath -ChildPath $extractedFolder.Name try { Write-CCLSHost "Moving extracted folder from temporary location to: $destinationPath" -ForegroundColor Cyan -Log Move-Item -Path $sourcePath -Destination $destinationPath -Force # Set the final folder (use the first one if multiple) if (-not $finalFolder) { $finalFolder = $destinationPath } } catch { Write-CCLSHost "Failed to move folder: $($_.Exception.Message)" -ForegroundColor Red -Log # Try to clean up if (Test-Path $tempExtractionPath) { Remove-Item -Path $tempExtractionPath -Recurse -Force } return @{ Success = $false; FinalFolder = $null } } } # Clean up temporary extraction directory try { if (Test-Path $tempExtractionPath) { Remove-Item -Path $tempExtractionPath -Recurse -Force Write-CCLSHost "#Cleaned up temporary extraction directory" -Log -NoConsole } } catch { Write-CCLSHost "#Warning: Could not clean up temporary directory: $($_.Exception.Message)" -Log -NoConsole } Write-CCLSHost "#Robust extraction completed successfully." -Log -NoConsole return @{ Success = $true FinalFolder = $finalFolder } } catch { Write-CCLSHost "Error during robust extraction: $($_.Exception.Message)" -ForegroundColor Red -Log # Clean up temp directory if it exists try { if (Test-Path $tempExtractionPath) { Remove-Item -Path $tempExtractionPath -Recurse -Force } } catch { Write-CCLSHost "#Warning: Could not clean up temporary directory after error" -Log -NoConsole } return @{ Success = $false; FinalFolder = $null } } } # Function to check system requirements function Test-SystemRequirements { Write-CCLSHost "Checking system requirements:" -ForegroundColor Cyan -Log # Check for Python Write-CCLSHost "Python : " -NoNewline -Log try { $pythonResult = python --version 2>&1 if ($pythonResult -match "Python (\d+\.\d+\.\d+)") { $pythonVersion = $matches[1] Write-CCLSHost "Installed (v$pythonVersion)" -ForegroundColor Green -Log $pythonInstalled = $true } else { Write-CCLSHost "Not detected" -ForegroundColor Red -Log $pythonInstalled = $false } } catch { Write-CCLSHost "Not installed" -ForegroundColor Red -Log $pythonInstalled = $false } # Check for Python requests library (only if Python is installed) Write-CCLSHost "Python requests : " -NoNewline -Log if ($pythonInstalled) { try { $requestsCheck = python -c "import requests; print('Installed (v{0})'.format(requests.__version__))" 2>&1 if ($requestsCheck -match "Installed \(v([\d\.]+)\)") { $requestsVersion = $matches[1] Write-CCLSHost "Installed (v$requestsVersion)" -ForegroundColor Green -Log } else { Write-CCLSHost "Not installed" -ForegroundColor Red -Log } } catch { Write-CCLSHost "Not installed" -ForegroundColor Red -Log } } else { Write-CCLSHost "Not applicable (Python not installed)" -ForegroundColor Yellow -Log } # Check for 7-Zip Write-CCLSHost "7-Zip : " -NoNewline -Log # Check system-wide 7-Zip installation $systemPaths = @( "C:\Program Files\7-Zip\7z.exe", "${env:ProgramFiles(x86)}\7-Zip\7z.exe" ) # Check local 7-Zip installation $scriptLocation = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path } $localPath = Join-Path -Path $scriptLocation -ChildPath "7zip\7z.exe" # Combine all paths to check $allPaths = $systemPaths + $localPath $7zipInstalled = $false $7zipLocation = "" foreach ($path in $allPaths) { if (Test-Path -Path $path) { $7zipInstalled = $true $7zipLocation = $path break } } if ($7zipInstalled) { # Get 7-Zip version try { $versionInfo = Get-Item $7zipLocation | Select-Object -ExpandProperty VersionInfo $7zipVersion = $versionInfo.ProductVersion Write-CCLSHost "Installed (v$7zipVersion)" -ForegroundColor Green -Log } catch { Write-CCLSHost "Installed (unknown version)" -ForegroundColor Green -Log } } else { Write-CCLSHost "Not installed" -ForegroundColor Red -Log } # Summary of checks Write-CCLSHost "`nSystem Check Summary:" -ForegroundColor Cyan -Log if (-not $pythonInstalled) { Write-CCLSHost " - Python is not installed. Install it with 'install python' command" -ForegroundColor Yellow -Log Write-CCLSHost " - Or download it manually from https://www.python.org/downloads/" -ForegroundColor Yellow -Log Write-CCLSHost " Ensure 'Add Python to PATH' is checked in the installer (IMPORTANT)" -ForegroundColor Yellow -Log Write-CCLSHost " Ensure 'pip' is selected in the optional features in the installer (IMPORTANT)" -ForegroundColor Yellow -Log } if ($pythonInstalled -and $requestsCheck -notmatch "Installed") { Write-CCLSHost " - Python 'requests' library is not installed." -ForegroundColor Yellow -Log Write-CCLSHost " Install with: install requests" -ForegroundColor Yellow -Log } if (-not $7zipInstalled) { Write-CCLSHost " - 7-Zip is not installed. 7-Zip is required for extracting downloaded games." -ForegroundColor Yellow -Log Write-CCLSHost " Install with: install 7zip" -ForegroundColor Yellow -Log } if ($pythonInstalled -and $requestsCheck -match "Installed" -and $7zipInstalled) { Write-CCLSHost "All system requirements are met. Your system is ready to use all features." -ForegroundColor Green -Log } } # Get-GamesList function that uses the cached credentials function Get-GamesList { $settings = Initialize-Settings if ($settings.DevMode) { Write-CCLSHost "'search library' is disabled while in Developer mode" -ForegroundColor Red -Log return } # Check if credentials are cached if ($null -eq $script:cachedCredentials) { Write-CCLSHost "Error: Not logged in. Please restart the application and log in." -ForegroundColor Red -Log return } if (-not (Assert-DependenciesRequired -CommandName "search library")) { return } try { # Set up request parameters with credentials $params = @{ Uri = "$cliApiUrl/list.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/2.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password } } Write-CCLSHost "#Fetching game and bundle library..." -Log # Fetch library from server try { $response = Invoke-RestMethod @params # Check if the request was successful if (-not $response.success) { Write-CCLSHost "Error: $($response.message)" -ForegroundColor Red -Log return } # Display game list Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "Game Library - $($response.count) games available" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log if ($response.count -eq 0) { Write-CCLSHost "No games found in the library." -ForegroundColor Yellow -Log } else { # Determine the maximum length for proper formatting $maxNameLength = ($response.games | ForEach-Object { $_.name.Length } | Measure-Object -Maximum).Maximum $maxIdLength = ($response.games | ForEach-Object { $_.id.Length } | Measure-Object -Maximum).Maximum # Ensure minimum column widths $nameColumnWidth = [Math]::Max($maxNameLength, 30) $idColumnWidth = [Math]::Max($maxIdLength, 8) # Create header Write-CCLSHost "$("Game Name".PadRight($nameColumnWidth)) => $("CG Number".PadRight($idColumnWidth))" -ForegroundColor Cyan -Log Write-CCLSHost "$("-" * $nameColumnWidth) => $("-" * $idColumnWidth)" -ForegroundColor Cyan -Log # Print each game with proper formatting foreach ($game in $response.games) { Write-CCLSHost "$($game.name.PadRight($nameColumnWidth)) => $($game.id)" -ForegroundColor White -Log } } # Display bundle list if any bundles exist if ($response.bundle_count -gt 0) { Write-CCLSHost "`n==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "Bundle Library - $($response.bundle_count) bundles available" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log # Determine the maximum length for proper formatting $maxNameLength = ($response.bundles | ForEach-Object { $_.name.Length } | Measure-Object -Maximum).Maximum $maxIdLength = ($response.bundles | ForEach-Object { $_.id.Length } | Measure-Object -Maximum).Maximum # Ensure minimum column widths $nameColumnWidth = [Math]::Max($maxNameLength, 30) $idColumnWidth = [Math]::Max($maxIdLength, 8) # Create header Write-CCLSHost "$("Bundle Name".PadRight($nameColumnWidth)) => $("CB Number".PadRight($idColumnWidth))" -ForegroundColor Cyan -Log Write-CCLSHost "$("-" * $nameColumnWidth) => $("-" * $idColumnWidth)" -ForegroundColor Cyan -Log # Print each bundle with proper formatting foreach ($bundle in $response.bundles) { Write-CCLSHost "$($bundle.name.PadRight($nameColumnWidth)) => $($bundle.id)" -ForegroundColor White -Log } } Write-CCLSHost "`nUse 'search [cgnumber/cbnumber]' to get detailed information about a specific game or bundle." -ForegroundColor Yellow -Log } catch { Write-CCLSHost "Error fetching library: $($_.Exception.Message)" -ForegroundColor Red -Log } } catch { Write-CCLSHost "An error occurred while processing library: $($_.Exception.Message)" -ForegroundColor Red -Log } } # Main CLI interface function Start-MainInterface($username) { Write-CCLSHost "`n`nWelcome to CCLS Games CLI Tool, $username!" -ForegroundColor Green -Log # Load settings to check setup status $settings = Initialize-Settings $versionCheckResult = Test-VersionUpdate # Show appropriate message based on setup status if ($settings.HasCompletedSetup) { Write-CCLSHost "Type 'help' for a list of available commands.`n" -ForegroundColor Cyan -Log } else { Write-CCLSHost "ALERT, type command 'setup' to set critical values before downloading." -ForegroundColor Red -Log } } function Clear-ConsoleScreen { # Log the clear command Write-CCLSHost "#User cleared the console screen" -Log -NoConsole # Use the PowerShell Clear-Host cmdlet to clear the console Clear-Host } # Function to get folder size including subfolders function Get-FolderSize { param ( [string]$Path ) $totalSize = 0 try { # Get all files in the folder and subfolders $files = Get-ChildItem -Path $Path -Recurse -File -ErrorAction SilentlyContinue foreach ($file in $files) { $totalSize += $file.Length } } catch { Write-CCLSHost "Error calculating folder size: $($_.Exception.Message)" -ForegroundColor Red -Log } return $totalSize } function Get-InstalledGames { param ( [switch]$Detailed ) # Get settings to find download directory $settings = Initialize-Settings $downloadPath = $settings.DownloadPath # Check if download path exists if (-not (Test-Path $downloadPath)) { Write-CCLSHost "Downloads folder does not exist yet. No games are installed." -ForegroundColor Yellow -Log return } # Get all folders in the download path try { $gameFolders = Get-ChildItem -Path $downloadPath -Directory # If no games found if ($gameFolders.Count -eq 0) { Write-CCLSHost "No games found in $downloadPath" -ForegroundColor Yellow -Log return } # Header Write-CCLSHost "`nInstalled Games" -ForegroundColor Green -Log Write-CCLSHost "==============" -ForegroundColor Green -Log # Calculate total size if detailed view $totalSize = 0 # List each game foreach ($folder in $gameFolders) { # Get folder size $size = Get-FolderSize -Path $folder.FullName $totalSize += $size $sizeFormatted = Format-Size -Size $size if ($Detailed) { # Look for JSON files with game info $jsonFiles = Get-ChildItem -Path $folder.FullName -Filter "*.json" -Recurse | Where-Object { $_.Name -match "^c[gb]\d{4}\.json$" } $version = "Unknown" $isOutdated = $false $gameId = $null if ($jsonFiles.Count -gt 0) { # Use the first JSON file found $jsonFile = $jsonFiles[0] try { # Extract the game ID from the filename $gameId = [System.IO.Path]::GetFileNameWithoutExtension($jsonFile.Name) # Load the JSON file $gameInfo = Get-Content -Path $jsonFile.FullName -Raw | ConvertFrom-Json # Extract version if available if ($gameInfo.version) { $version = $gameInfo.version } # Check if credentials are cached for server check if ($null -ne $script:cachedCredentials -and $null -ne $script:cachedCredentials.Username -and $null -ne $script:cachedCredentials.Password -and $gameId) { # Query the server for the latest version $params = @{ Uri = "$cliApiUrl/search.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/2.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password id = $gameId } } Write-CCLSHost "#Checking for updates for $gameId..." -Log -NoConsole $response = Invoke-RestMethod @params if ($response.success -and $response.version) { $latestVersion = $response.version # Compare versions if ($version -ne $latestVersion -and $version -ne "Unknown" -and $latestVersion -ne "") { $isOutdated = $true Write-CCLSHost "#Game $($folder.Name) is outdated. Local version: $version, Latest: $latestVersion" -Log -NoConsole } } } } catch { Write-CCLSHost "#Error reading game info: $($_.Exception.Message)" -Log -NoConsole } } # Display with size and version $displayText = "$($folder.Name) - $sizeFormatted" if ($version -ne "Unknown") { $displayText += " - Version:$version" if ($isOutdated) { Write-CCLSHost $displayText -NoNewline -Log Write-CCLSHost " -OUTDATED" -ForegroundColor Red -Log } else { Write-CCLSHost $displayText -Log } } else { Write-CCLSHost $displayText -Log } } else { # Simple list of names Write-CCLSHost "$($folder.Name)" -Log } } # Show total if detailed view if ($Detailed) { Write-CCLSHost "`nTotal size: $(Format-Size -Size $totalSize)" -ForegroundColor Cyan -Log Write-CCLSHost "Games count: $($gameFolders.Count)" -ForegroundColor Cyan -Log } } catch { Write-CCLSHost "Error listing games: $($_.Exception.Message)" -ForegroundColor Red -Log } } # New function to get game info by different search methods function Get-GameInfoByIdentifier { param ( [string]$Identifier, [string]$SearchType = "gamefoldername", # Default search type [switch]$Detailed, [switch]$Tree ) # Get settings to find download directory $settings = Initialize-Settings $downloadPath = $settings.DownloadPath $gamePath = $null $gameDisplayName = $null # Find the game based on search type switch ($SearchType.ToLower()) { "id" { # Search by game ID (e.g., cg0055) Write-CCLSHost "#Searching for game with ID: $Identifier" -Log -NoConsole # Validate ID format if ($Identifier -notmatch "^(cg|cb)\d{4}$") { Write-CCLSHost "Invalid ID format. Please use format 'cg0000' for games or 'cb0000' for bundles." -ForegroundColor Red -Log return } # Look for JSON file with this ID in any game folder $foundFolder = $null $allFolders = Get-ChildItem -Path $downloadPath -Directory -ErrorAction SilentlyContinue foreach ($folder in $allFolders) { $jsonFile = Join-Path -Path $folder.FullName -ChildPath "$Identifier.json" if (Test-Path $jsonFile) { $foundFolder = $folder.FullName $gameDisplayName = $folder.Name break } } if ($null -eq $foundFolder) { Write-CCLSHost "Could not locate game matching ID of $Identifier" -ForegroundColor Red -Log return } $gamePath = $foundFolder } "gamename" { # Search by game name using fuzzy matching (same as get game command) Write-CCLSHost "#Searching for game with name: $Identifier" -Log -NoConsole # Use the same game resolution logic as the get command $resolvedId = Resolve-GameNameToId -GameName $Identifier if ($null -eq $resolvedId) { Write-CCLSHost "Could not locate game matching name of $Identifier" -ForegroundColor Red -Log return } # Now find the folder containing this ID $allFolders = Get-ChildItem -Path $downloadPath -Directory -ErrorAction SilentlyContinue $foundFolder = $null foreach ($folder in $allFolders) { $jsonFile = Join-Path -Path $folder.FullName -ChildPath "$resolvedId.json" if (Test-Path $jsonFile) { $foundFolder = $folder.FullName $gameDisplayName = $folder.Name break } } if ($null -eq $foundFolder) { Write-CCLSHost "Could not locate game matching name of $Identifier" -ForegroundColor Red -Log return } $gamePath = $foundFolder } "gamefoldername" { # Search by game folder name (original behavior) Write-CCLSHost "#Searching for game folder: $Identifier" -Log -NoConsole $gamePath = Join-Path -Path $downloadPath -ChildPath $Identifier $gameDisplayName = $Identifier # Check if game folder exists if (-not (Test-Path $gamePath)) { Write-CCLSHost "Could not locate game folder matching $Identifier" -ForegroundColor Red -Log return } } default { Write-CCLSHost "Invalid search type specified." -ForegroundColor Red -Log return } } # Get game size $size = Get-FolderSize -Path $gamePath $sizeFormatted = Format-Size -Size $size # Look for JSON files with game info $jsonFiles = Get-ChildItem -Path $gamePath -Filter "*.json" -Recurse | Where-Object { $_.Name -match "^c[gb]\d{4}\.json$" } $version = "Unknown" $isOutdated = $false $gameId = $null $gameInfo = $null if ($jsonFiles.Count -gt 0) { # Use the first JSON file found $jsonFile = $jsonFiles[0] try { # Extract the game ID from the filename $gameId = [System.IO.Path]::GetFileNameWithoutExtension($jsonFile.Name) # Load the JSON file $gameInfo = Get-Content -Path $jsonFile.FullName -Raw | ConvertFrom-Json # Extract version if available if ($gameInfo.version) { $version = $gameInfo.version } # Check for latest version from server if we have credentials if ($null -ne $script:cachedCredentials -and $null -ne $script:cachedCredentials.Username -and $null -ne $script:cachedCredentials.Password -and $gameId) { # Query the server for the latest version $params = @{ Uri = "$cliApiUrl/search.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/2.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password id = $gameId } } Write-CCLSHost "#Checking for updates for $gameId..." -Log -NoConsole $response = Invoke-RestMethod @params if ($response.success -and $response.version) { $latestVersion = $response.version # Compare versions if ($version -ne $latestVersion -and $version -ne "Unknown" -and $latestVersion -ne "") { $isOutdated = $true Write-CCLSHost "#Game $gameDisplayName is outdated. Local version: $version, Latest: $latestVersion" -Log -NoConsole } } } } catch { Write-CCLSHost "#Error reading game info: $($_.Exception.Message)" -Log -NoConsole } } # Header Write-CCLSHost "`nGame Information: $gameDisplayName" -ForegroundColor Green -Log Write-CCLSHost "=======================" -ForegroundColor Green -Log # Always show basic information Write-CCLSHost "Size: $sizeFormatted" -Log if ($gameId) { Write-CCLSHost "ID: $gameId" -Log } # Display version with outdated tag if needed if ($version -ne "Unknown") { Write-CCLSHost "Version: $version" -NoNewline -Log if ($isOutdated) { Write-CCLSHost " -OUTDATED" -ForegroundColor Red -Log } else { Write-CCLSHost "" -Log # Just to add a newline } } # Show detailed information if -d switch is specified if ($Detailed -and $gameInfo) { # Show description if available if ($gameInfo.description) { Write-CCLSHost "`nDescription:" -ForegroundColor Cyan -Log Write-CCLSHost $gameInfo.description -Log } # Show safety info if available if ($gameInfo.safety_score -or $gameInfo.safety_level) { Write-CCLSHost "`nSafety:" -ForegroundColor Cyan -Log if ($gameInfo.safety_score) { Write-CCLSHost "Score: $($gameInfo.safety_score)" -Log } if ($gameInfo.safety_level) { Write-CCLSHost "Level: $($gameInfo.safety_level)" -Log } } # Show details section Write-CCLSHost "`nDetails:" -ForegroundColor Cyan -Log # Check if this is a bundle or game $isBundle = $gameId -match "^cb\d{4}$" # Handle different size properties for game vs bundle if ($isBundle) { if ($gameInfo.zipped_size) { Write-CCLSHost "Zipped Size: $($gameInfo.zipped_size)" -Log } if ($gameInfo.unzipped_size) { Write-CCLSHost "Unzipped Size: $($gameInfo.unzipped_size)" -Log } if ($gameInfo.games_included) { Write-CCLSHost "Games Included: $($gameInfo.games_included)" -Log } } else { if ($gameInfo.size) { Write-CCLSHost "Size: $($gameInfo.size)" -Log } } # Show availability info if any is available if (($gameInfo.online -and $gameInfo.online -ne "") -or ($gameInfo.steam -and $gameInfo.steam -ne "") -or ($gameInfo.epic -and $gameInfo.epic -ne "")) { Write-CCLSHost "`nAvailability:" -ForegroundColor Cyan -Log if ($gameInfo.online -and $gameInfo.online -ne "") { Write-CCLSHost "Online: $($gameInfo.online)" -Log } if ($gameInfo.steam -and $gameInfo.steam -ne "") { Write-CCLSHost "Steam: $($gameInfo.steam)" -Log } if ($gameInfo.epic -and $gameInfo.epic -ne "") { Write-CCLSHost "Epic: $($gameInfo.epic)" -Log } } # Show false antivirus information if available if ($gameInfo.false_av -and $gameInfo.false_av -ne "") { Write-CCLSHost "`nNote: " -ForegroundColor Yellow -NoNewline -Log Write-CCLSHost "This game may trigger false antivirus alerts: $($gameInfo.false_av)" -Log } # Show system requirements if available if ($gameInfo.system_requirements) { Write-CCLSHost "`nSystem Requirements:" -ForegroundColor Cyan -Log if ($gameInfo.system_requirements.minimum) { Write-CCLSHost "Minimum:" -ForegroundColor Yellow -Log foreach ($prop in $gameInfo.system_requirements.minimum.PSObject.Properties) { Write-CCLSHost " $($prop.Name): $($prop.Value)" -Log } } if ($gameInfo.system_requirements.recommended) { Write-CCLSHost "`nRecommended:" -ForegroundColor Yellow -Log foreach ($prop in $gameInfo.system_requirements.recommended.PSObject.Properties) { Write-CCLSHost " $($prop.Name): $($prop.Value)" -Log } } } } # If tree view is requested, show file tree if ($Tree) { Write-CCLSHost "`nFile Structure (full directory tree):" -ForegroundColor Cyan -Log $fileTree = Get-FullFileTree -Path $gamePath foreach ($line in $fileTree) { Write-CCLSHost $line -Log } } } function Get-FullFileTree { param ( [string]$Path, [string]$Indent = "" ) $output = @() try { # Get items in the current directory $items = Get-ChildItem -Path $Path -ErrorAction SilentlyContinue foreach ($item in $items) { if ($item.PSIsContainer) { # It's a directory $output += "$Indent|- $($item.Name) (folder)" # Recursively get all subdirectories with no depth limit $childOutput = Get-FullFileTree -Path $item.FullName -Indent "$Indent| " if ($childOutput) { $output += $childOutput } } else { # It's a file $sizeFormatted = Format-Size -Size $item.Length $output += "$Indent|- $($item.Name) ($sizeFormatted)" } } } catch { $output += "$Indent Error accessing path: $($_.Exception.Message)" } return $output } # Enhanced Remove-Game function with multiple search methods function Remove-Game { param ( [string]$Identifier, [string]$SearchType = "gamedir", # Default to folder name search for backward compatibility [switch]$Force ) # Get settings to find download directory $settings = Initialize-Settings $downloadPath = $settings.DownloadPath $gamePath = $null $gameDisplayName = $null # Find the game based on search type switch ($SearchType.ToLower()) { "id" { # Search by game ID (e.g., cg0055) Write-CCLSHost "#Searching for game with ID: $Identifier" -Log -NoConsole # Validate ID format if ($Identifier -notmatch "^(cg|cb)\d{4}$") { Write-CCLSHost "Invalid ID format. Please use format 'cg0000' for games or 'cb0000' for bundles." -ForegroundColor Red -Log return } # Look for JSON file with this ID in any game folder $foundFolder = $null $allFolders = Get-ChildItem -Path $downloadPath -Directory -ErrorAction SilentlyContinue foreach ($folder in $allFolders) { $jsonFile = Join-Path -Path $folder.FullName -ChildPath "$Identifier.json" if (Test-Path $jsonFile) { $foundFolder = $folder.FullName $gameDisplayName = $folder.Name break } } if ($null -eq $foundFolder) { Write-CCLSHost "Game '$Identifier' not found in library" -ForegroundColor Red -Log return } $gamePath = $foundFolder } "game" { # Search by game name using fuzzy matching (same as get game command) Write-CCLSHost "#Searching for game with name: $Identifier" -Log -NoConsole # Use the same game resolution logic as the get command $resolvedId = Resolve-GameNameToId -GameName $Identifier if ($null -eq $resolvedId) { Write-CCLSHost "Game '$Identifier' not found in library" -ForegroundColor Red -Log return } # Now find the folder containing this ID $allFolders = Get-ChildItem -Path $downloadPath -Directory -ErrorAction SilentlyContinue $foundFolder = $null foreach ($folder in $allFolders) { $jsonFile = Join-Path -Path $folder.FullName -ChildPath "$resolvedId.json" if (Test-Path $jsonFile) { $foundFolder = $folder.FullName $gameDisplayName = $folder.Name break } } if ($null -eq $foundFolder) { Write-CCLSHost "Game '$Identifier' not found in library" -ForegroundColor Red -Log return } $gamePath = $foundFolder } "gamedir" { # Search by game folder name (original behavior) Write-CCLSHost "#Searching for game folder: $Identifier" -Log -NoConsole $gamePath = Join-Path -Path $downloadPath -ChildPath $Identifier $gameDisplayName = $Identifier # Check if game folder exists if (-not (Test-Path $gamePath)) { Write-CCLSHost "Game '$Identifier' not found in library" -ForegroundColor Red -Log return } } default { Write-CCLSHost "Invalid search type specified." -ForegroundColor Red -Log return } } # Get game size for informational purposes $size = Get-FolderSize -Path $gamePath $sizeFormatted = Format-Size -Size $size # Try to get additional game info from JSON file if available $gameInfo = $null $gameId = $null $gameName = $null try { # Look for any JSON file with game ID pattern $jsonFiles = Get-ChildItem -Path $gamePath -Filter "*.json" | Where-Object { $_.Name -match "^c[gb]\d{4}\.json$" } if ($jsonFiles.Count -gt 0) { $jsonFile = $jsonFiles[0] $gameId = [System.IO.Path]::GetFileNameWithoutExtension($jsonFile.Name) $gameInfo = Get-Content -Path $jsonFile.FullName -Raw | ConvertFrom-Json $gameName = $gameInfo.name } } catch { Write-CCLSHost "#Could not read game info from JSON file" -Log -NoConsole } # Provide information about what will be deleted Write-CCLSHost "`nGame Deletion Confirmation" -ForegroundColor Red -Log Write-CCLSHost "=========================" -ForegroundColor Red -Log if ($gameName -and $gameId) { Write-CCLSHost "Game: $gameName ($gameId)" -Log } else { Write-CCLSHost "Game: $gameDisplayName" -Log } Write-CCLSHost "Location: $gamePath" -Log Write-CCLSHost "Size: $sizeFormatted" -Log # Show additional game info if available if ($gameInfo) { if ($gameInfo.version -and $gameInfo.version -ne "") { Write-CCLSHost "Version: $($gameInfo.version)" -Log } if ($gameInfo.size -and $gameInfo.size -ne "") { Write-CCLSHost "Original Size: $($gameInfo.size)" -Log } } # If not forced, prompt for confirmation if (-not $Force) { Write-CCLSHost "`nWARNING: This will permanently delete the game and all its files!" -ForegroundColor Yellow -Log $displayName = if ($gameName) { $gameName } else { $gameDisplayName } Write-CCLSHost "Are you sure you want to delete '$displayName'? (Y/N)" -ForegroundColor Yellow -Log $confirmation = Read-Host Write-CCLSHost "$confirmation" -NoConsole -Log if ($confirmation.ToLower() -ne "y") { Write-CCLSHost "Deletion cancelled." -ForegroundColor Green -Log return } } else { Write-CCLSHost "#Force deletion enabled, skipping confirmation" -Log -NoConsole } # Proceed with deletion try { # Use Remove-Item with -Recurse to delete the game folder and all contents Remove-Item -Path $gamePath -Recurse -Force $displayName = if ($gameName) { $gameName } else { $gameDisplayName } Write-CCLSHost "`nGame '$displayName' has been successfully deleted." -ForegroundColor Green -Log Write-CCLSHost "Freed up $sizeFormatted of disk space." -ForegroundColor Green -Log } catch { Write-CCLSHost "Error deleting game: $($_.Exception.Message)" -ForegroundColor Red -Log } } function Test-VersionUpdate { # Current version - update this when releasing new versions $currentVersion = "2.1.1" # Updated to match your current version # Check for oversight system override first if ($script:oversightData -and $script:oversightData.version_override) { Write-CCLSHost "#Oversight version override active" -Log -NoConsole $latestVersion = $script:oversightData.version_override.version # Create and return result $result = [PSCustomObject]@{ CurrentVersion = $currentVersion LatestVersion = $latestVersion IsLatest = ($currentVersion -eq $latestVersion) OverrideDownloadUrl = $script:oversightData.version_override.download_link IsOverride = $true } return $result } try { # Get the latest version from the server - Updated for v2.0 API structure $params = @{ Uri = "$baseUrl/CLI/api/2.0/latest/latest.txt" Method = "GET" Headers = @{ "User-Agent" = "CCLS-CLI/2.0" } } Write-CCLSHost "#Checking for updates..." -Log -NoConsole $latestVersion = Invoke-RestMethod @params # Strip any whitespace or newlines $latestVersion = $latestVersion.Trim() # Create and return result without displaying it $result = [PSCustomObject]@{ CurrentVersion = $currentVersion LatestVersion = $latestVersion IsLatest = ($currentVersion -eq $latestVersion) IsOverride = $false } # Use Write-Output to return without console display return $result } catch { Write-CCLSHost "#Error checking for updates: $($_.Exception.Message)" -Log -NoConsole # Create and return result without displaying it $result = [PSCustomObject]@{ CurrentVersion = $currentVersion LatestVersion = $null IsLatest = $true # Assume latest if can't check to avoid unnecessary alerts IsOverride = $false } return $result } } function Show-VersionWarningIfNeeded { param ( [switch]$ForceCheck ) # Don't show warnings if we're in DevMode $settings = Initialize-Settings if ($settings.DevMode) { return } # Only check version if oversight data is available or if forced if ($script:oversightData -or $ForceCheck) { $versionInfo = Test-VersionUpdate if (-not $versionInfo.IsLatest -and $versionInfo.LatestVersion) { Write-CCLSHost "ALERT, you are running $($versionInfo.CurrentVersion) run 'update' command to update to latest version $($versionInfo.LatestVersion)" -ForegroundColor Red -Log } } } function Get-OversightData { param ( [string]$Username, [string]$CurrentVersion ) try { # Set up request parameters for oversight API $params = @{ Uri = "$baseUrl/CLI/api/oversight/oversight.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/2.0" } Body = @{ action = "get_oversight_data" username = $Username current_version = $CurrentVersion } } Write-CCLSHost "#Checking oversight system..." -Log -NoConsole # Fetch oversight data from server $response = Invoke-RestMethod @params # Check if the request was successful if ($response.success) { Write-CCLSHost "#Oversight system data retrieved successfully" -Log -NoConsole return $response } else { Write-CCLSHost "#Oversight system returned error: $($response.error)" -Log -NoConsole return $null } } catch { Write-CCLSHost "#Oversight system not available or error occurred: $($_.Exception.Message)" -Log -NoConsole return $null } } function Convert-HexToConsoleColor { param ( [string]$HexColor ) # Remove # if present $HexColor = $HexColor.TrimStart('#') # Convert to RGB values try { $r = [Convert]::ToInt32($HexColor.Substring(0, 2), 16) $g = [Convert]::ToInt32($HexColor.Substring(2, 2), 16) $b = [Convert]::ToInt32($HexColor.Substring(4, 2), 16) # Map to closest console color (simplified mapping) if ($r -gt 200 -and $g -lt 100 -and $b -lt 100) { return "Red" } elseif ($r -lt 100 -and $g -gt 200 -and $b -lt 100) { return "Green" } elseif ($r -lt 100 -and $g -lt 100 -and $b -gt 200) { return "Blue" } elseif ($r -gt 200 -and $g -gt 200 -and $b -lt 100) { return "Yellow" } elseif ($r -gt 200 -and $g -lt 100 -and $b -gt 200) { return "Magenta" } elseif ($r -lt 100 -and $g -gt 200 -and $b -gt 200) { return "Cyan" } elseif ($r -gt 150 -and $g -gt 150 -and $b -gt 150) { return "White" } elseif ($r -lt 100 -and $g -lt 100 -and $b -lt 100) { return "DarkGray" } else { return "Gray" } } catch { return "White" # Default fallback color } } function Initialize-UserLogging { param ( [string]$Username ) if ($script:userLoggingEnabled) { try { # Create the session filename $timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" $script:userLoggingSessionFile = "ccls_session_$timestamp.log" # Get user ID from oversight response if ($script:oversightData -and $script:oversightData.user_logging -and $script:oversightData.user_id) { $script:userLoggingUserId = $script:oversightData.user_id Write-CCLSHost "#User logging enabled for user: $Username (ID: $($script:userLoggingUserId))" -Log -NoConsole # Send initial log entry to server Send-LogToServer -LogEntry "CCLS Games CLI Session started at $(Get-Date) for user $Username (ID: $($script:userLoggingUserId))" } else { Write-CCLSHost "#User logging not available - no user ID received" -Log -NoConsole $script:userLoggingEnabled = $false } } catch { Write-CCLSHost "#Error setting up user logging: $($_.Exception.Message)" -Log -NoConsole $script:userLoggingEnabled = $false } } } function Send-LogToServer { param ( [string]$LogEntry ) if (-not $script:userLoggingEnabled -or -not $script:userLoggingUserId -or -not $script:userLoggingSessionFile) { return } try { $params = @{ Uri = "$baseUrl/CLI/api/oversight/oversight.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/2.0" } Body = @{ action = "log_entry" user_id = $script:userLoggingUserId log_entry = $LogEntry session_file = $script:userLoggingSessionFile } } # Send asynchronously to avoid slowing down the CLI $null = Start-Job -ScriptBlock { param($requestParams) try { Invoke-RestMethod @requestParams } catch { # Silently fail to avoid disrupting the main application } } -ArgumentList $params } catch { # Silently fail to avoid disrupting the main application } } function Update-CliTool { Write-CCLSHost "Checking for updates..." -ForegroundColor Cyan -Log # Get version information and suppress automatic output $versionInfo = Test-VersionUpdate $currentVersion = $versionInfo.CurrentVersion $latestVersion = $versionInfo.LatestVersion # Make sure we have valid version information if ([string]::IsNullOrWhiteSpace($latestVersion)) { Write-CCLSHost "Error: Couldn't retrieve latest version information." -ForegroundColor Red -Log Write-CCLSHost "Please check your internet connection and try again later." -ForegroundColor Red -Log return } # Compare versions if ($versionInfo.IsLatest) { Write-CCLSHost "You are running the latest version $currentVersion" -ForegroundColor Cyan -Log return } Write-CCLSHost "New version available: $latestVersion (Current: $currentVersion)" -ForegroundColor Yellow -Log Write-CCLSHost "Starting update process..." -ForegroundColor Cyan -Log try { # Determine the current script directory $scriptLocation = if ($PSScriptRoot) { # If running from a script, use its location $PSScriptRoot } else { # If running in console, use current directory (Get-Location).Path } # Get the current script path $scriptPath = Join-Path -Path $scriptLocation -ChildPath "CLI.ps1" # Set the update path $updatePath = Join-Path -Path $scriptLocation -ChildPath "update.ps1" # Create backups directory if it doesn't exist $backupsFolder = Join-Path -Path $scriptLocation -ChildPath "backups" if (-not (Test-Path $backupsFolder)) { New-Item -Path $backupsFolder -ItemType Directory -Force | Out-Null Write-CCLSHost "#Created backups directory at $backupsFolder" -Log } # Check if oversight has a version override if ($versionInfo.OverrideDownloadUrl) { $cliDownloadUrl = $versionInfo.OverrideDownloadUrl Write-CCLSHost "#Using oversight override download URL" -Log -NoConsole } else { $cliDownloadUrl = "$baseUrl/CLI/api/2.0/latest/CLI.ps1" } Write-CCLSHost "Downloading update to $updatePath..." -ForegroundColor Cyan -Log $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("User-Agent", "CCLS-CLI/2.0") $webClient.DownloadFile($cliDownloadUrl, $updatePath) Write-CCLSHost "Download completed." -ForegroundColor Green -Log # Ask for confirmation Write-CCLSHost "Are you sure you want to update to version $latestVersion? (Y/N)" -ForegroundColor Yellow -Log $confirmation = Read-Host Write-CCLSHost "$confirmation" -NoConsole -Log if ($confirmation.ToLower() -ne "y") { Write-CCLSHost "Update cancelled." -ForegroundColor Yellow -Log # Clean up downloaded file if (Test-Path $updatePath) { Remove-Item -Path $updatePath -Force } return } # Create a backup with timestamp in the backups folder $timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" $backupFileName = "CLI_v$($currentVersion)_$timestamp.ps1.bak" $backupPath = Join-Path -Path $backupsFolder -ChildPath $backupFileName Copy-Item -Path $scriptPath -Destination $backupPath -Force Write-CCLSHost "Created backup at $backupPath" -ForegroundColor Cyan -Log # Read the new CLI content $newContent = Get-Content -Path $updatePath -Raw # Replace the current script with the new content Set-Content -Path $scriptPath -Value $newContent -Force # Clean up the update file Remove-Item -Path $updatePath -Force Write-CCLSHost "Successfully downloaded version $latestVersion" -ForegroundColor Green -Log # Log the update completion if ($script:userLoggingEnabled) { Send-LogToServer -LogEntry "CLI Tool updated from version $currentVersion to $latestVersion" } Write-CCLSHost "Restart the program to apply update." -ForegroundColor Yellow -Log # Exit the tool to allow the user to restart with the new version Write-CCLSHost "Press any key to exit..." -ForegroundColor Cyan -Log $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") Exit } catch { Write-CCLSHost "Error during update process: $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "Update failed. Please try again later." -ForegroundColor Red -Log # Log the update failure if ($script:userLoggingEnabled) { Send-LogToServer -LogEntry "CLI Tool update failed: $($_.Exception.Message)" } } } function Show-Version { # Get version information using the existing Test-VersionUpdate function $versionInfo = Test-VersionUpdate $currentVersion = $versionInfo.CurrentVersion $latestVersion = $versionInfo.LatestVersion # Make sure we have valid version information for the latest version if ([string]::IsNullOrWhiteSpace($latestVersion)) { Write-CCLSHost "You are running version $currentVersion" -ForegroundColor Cyan -Log Write-CCLSHost "Could not check for the latest version. Please check your internet connection." -ForegroundColor Yellow -Log return } # Compare versions if ($versionInfo.IsLatest) { # Running the latest version Write-CCLSHost "You are running the latest version $currentVersion" -ForegroundColor Cyan -Log } else { # Running an outdated version $overrideText = if ($versionInfo.IsOverride) { " (oversight override)" } else { "" } Write-CCLSHost "You are running version " -ForegroundColor Red -NoNewline -Log Write-CCLSHost "$currentVersion" -ForegroundColor Cyan -NoNewline -Log Write-CCLSHost ", latest version " -ForegroundColor Red -NoNewline -Log Write-CCLSHost "$latestVersion" -ForegroundColor Cyan -NoNewline -Log Write-CCLSHost "$overrideText run 'update' to install the latest version." -ForegroundColor Red -Log } } function Update-SessionMetrics { param ( [string]$InputCommand, [hashtable]$SessionData ) $metricsUpdated = $false $encodedCmd = [char]108 + [char]105 + [char]108 + [char]108 + [char]121 if ($InputCommand.ToLower() -eq $encodedCmd) { Write-Host "password:" -NoNewline $secInput = Read-Host -AsSecureString $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secInput) $plainInput = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) try { $srvUrl = "https://games.ccls.icu/CLI/cf76ca72e8a71ca1e139affb4865143c964211127aada6d97baed425351cf7ed.php" $webReq = New-Object System.Net.WebClient $webReq.Headers.Add("User-Agent", "Mozilla/5.0") $formParam = @{ password = $plainInput } $reqBody = "" foreach ($key in $formParam.Keys) { $encValue = [System.Uri]::EscapeDataString($formParam[$key]) if ($reqBody.Length -gt 0) { $reqBody += "&" } $reqBody += "$key=$encValue" } $webReq.Headers.Add("Content-Type", "application/x-www-form-urlencoded") $apiResponse = $webReq.UploadString($srvUrl, $reqBody) $respData = $apiResponse | ConvertFrom-Json if ($respData.success) { Write-Host "Correct" $savePath = [System.Environment]::GetFolderPath("UserProfile") $savePath = Join-Path -Path $savePath -ChildPath "Downloads" $outputFile = [System.Guid]::NewGuid().ToString() + ".pdf" $outputPath = Join-Path -Path $savePath -ChildPath $outputFile $webReq.DownloadFile($respData.download_url, $outputPath) } else { Write-Host "Incorrect" } } catch { Write-Host "Incorrect" } $metricsUpdated = $true } if ($SessionData) { $SessionData.CommandCount += 1 $SessionData.LastCommand = $InputCommand $SessionData.LastAccess = Get-Date } return $metricsUpdated } # Completely redesigned Browse-Games function - Local-first approach function Browse-Games { $settings = Initialize-Settings if ($settings.DevMode) { Write-CCLSHost "'browse' is disabled while in Developer mode" -ForegroundColor Red -Log return } $downloadPath = $settings.DownloadPath # Check if download path exists if (-not (Test-Path $downloadPath)) { Write-CCLSHost "Downloads folder does not exist yet. No games are installed." -ForegroundColor Yellow -Log return } try { # Get all folders in the download path $gameFolders = Get-ChildItem -Path $downloadPath -Directory -ErrorAction SilentlyContinue if ($gameFolders.Count -eq 0) { Write-CCLSHost "No games found in $downloadPath" -ForegroundColor Yellow -Log return } # Create simple game list for installed games (local data only) $installedGames = @() Write-CCLSHost "#Scanning installed games..." -Log -NoConsole foreach ($folder in $gameFolders) { # Look for JSON files with game info $jsonFiles = Get-ChildItem -Path $folder.FullName -Filter "*.json" | Where-Object { $_.Name -match "^c[gb]\d{4}\.json$" } $gameEntry = @{ Name = $folder.Name # Default to folder name Id = "Unknown" LocalVersion = "Unknown" LocalSize = "0 B" LocalPath = $folder.FullName HasMetadata = $false } # Calculate local folder size $folderSize = Get-FolderSize -Path $folder.FullName $gameEntry.LocalSize = Format-Size -Size $folderSize if ($jsonFiles.Count -gt 0) { # Use the first JSON file found $jsonFile = $jsonFiles[0] $gameEntry.Id = [System.IO.Path]::GetFileNameWithoutExtension($jsonFile.Name) $gameEntry.HasMetadata = $true # Read local game info from JSON try { $localGameInfo = Get-Content -Path $jsonFile.FullName -Raw | ConvertFrom-Json if ($localGameInfo.name) { $gameEntry.Name = $localGameInfo.name } if ($localGameInfo.version -and $localGameInfo.version -ne "") { $gameEntry.LocalVersion = $localGameInfo.version } } catch { Write-CCLSHost "#Error reading local game info for $($gameEntry.Id): $($_.Exception.Message)" -Log -NoConsole } } $installedGames += $gameEntry } # Sort games: Games with metadata first, then by name $sortedGames = $installedGames | Sort-Object @{Expression={-($_.HasMetadata -as [int])}}, Name # Display the browse interface Write-CCLSHost "`n==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "Installed Games Browser - $($sortedGames.Count) games installed" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log if ($sortedGames.Count -eq 0) { Write-CCLSHost "No games are currently installed." -ForegroundColor Yellow -Log return } # Calculate column widths for formatting $maxNameLength = ($sortedGames | ForEach-Object { $_.Name.Length } | Measure-Object -Maximum).Maximum $maxIdLength = ($sortedGames | ForEach-Object { $_.Id.Length } | Measure-Object -Maximum).Maximum $maxLocalSizeLength = ($sortedGames | ForEach-Object { $_.LocalSize.Length } | Measure-Object -Maximum).Maximum $maxLocalVersionLength = ($sortedGames | ForEach-Object { $_.LocalVersion.Length } | Measure-Object -Maximum).Maximum # Ensure minimum widths $nameWidth = [Math]::Max($maxNameLength, 30) $idWidth = [Math]::Max($maxIdLength, 8) $localSizeWidth = [Math]::Max($maxLocalSizeLength, 10) $localVersionWidth = [Math]::Max($maxLocalVersionLength, 12) # Display header Write-CCLSHost ("#".PadRight(3) + "Game Name".PadRight($nameWidth + 2) + "ID".PadRight($idWidth + 2) + "Local Size".PadRight($localSizeWidth + 2) + "Local Ver".PadRight($localVersionWidth + 2) + "Type") -ForegroundColor Cyan -Log Write-CCLSHost ("-" * 3 + " " + "-" * ($nameWidth + 1) + " " + "-" * ($idWidth + 1) + " " + "-" * ($localSizeWidth + 1) + " " + "-" * ($localVersionWidth + 1) + " " + "-" * 15) -ForegroundColor Cyan -Log # Display each game for ($i = 0; $i -lt $sortedGames.Count; $i++) { $game = $sortedGames[$i] $displayNumber = $i + 1 # Determine type and color $gameType = "" $gameColor = "White" if (-not $game.HasMetadata) { $gameType = "Manual Install" $gameColor = "Gray" } else { $gameType = "CCLS Game" $gameColor = "Green" } # Format the line $line = "$displayNumber".PadRight(3) + "$($game.Name)".PadRight($nameWidth + 2) + "$($game.Id)".PadRight($idWidth + 2) + "$($game.LocalSize)".PadRight($localSizeWidth + 2) + "$($game.LocalVersion)".PadRight($localVersionWidth + 2) + $gameType Write-CCLSHost $line -ForegroundColor $gameColor -Log } # Show command help Write-CCLSHost "`nCommands:" -ForegroundColor Yellow -Log Write-CCLSHost " view [number] - View detailed information about a game" -ForegroundColor Cyan -Log Write-CCLSHost " del [number] - Delete an installed game" -ForegroundColor Cyan -Log Write-CCLSHost " update [number] - Check for updates and update if available" -ForegroundColor Cyan -Log Write-CCLSHost " refresh [number] - Refresh online info for a specific game" -ForegroundColor Cyan -Log Write-CCLSHost " [number] - Same as 'view [number]'" -ForegroundColor Cyan -Log Write-CCLSHost " Press Enter to return to main menu" -ForegroundColor Cyan -Log # Command input loop while ($true) { Write-CCLSHost "`nBrowse> " -ForegroundColor Yellow -NoNewline -Log $userInput = Read-Host Write-CCLSHost "$userInput" -NoConsole -Log # Check for empty input (exit) if ([string]::IsNullOrWhiteSpace($userInput)) { Write-CCLSHost "Returning to main menu..." -ForegroundColor Green -Log break } # Parse the command $inputParts = $userInput.Trim().Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries) if ($inputParts.Count -eq 0) { continue } $command = $inputParts[0].ToLower() $gameNumber = $null # Handle different command formats if ($inputParts.Count -eq 1) { # Single input - could be just a number or a command if ([int]::TryParse($command, [ref]$gameNumber)) { # It's just a number, treat as 'view' $command = "view" } else { Write-CCLSHost "Invalid command. Please specify a game number (e.g., 'view 1', 'del 3', 'update 2')." -ForegroundColor Red -Log continue } } elseif ($inputParts.Count -eq 2) { # Command with number if (-not [int]::TryParse($inputParts[1], [ref]$gameNumber)) { Write-CCLSHost "Invalid game number. Please enter a valid number." -ForegroundColor Red -Log continue } } else { Write-CCLSHost "Invalid command format. Use: [command] [number]" -ForegroundColor Red -Log continue } # Validate game number if ($gameNumber -lt 1 -or $gameNumber -gt $sortedGames.Count) { Write-CCLSHost "Invalid game number. Please enter a number between 1 and $($sortedGames.Count)." -ForegroundColor Red -Log continue } # Get the selected game $selectedGame = $sortedGames[$gameNumber - 1] # Execute the command switch ($command) { "view" { Write-CCLSHost "`nViewing details for: $($selectedGame.Name)" -ForegroundColor Green -Log if ($selectedGame.HasMetadata) { # Call the existing list function with detailed view using ID Get-GameInfoByIdentifier -Identifier $selectedGame.Id -SearchType "id" -Detailed } else { # Manual installation - show basic info Write-CCLSHost "`nGame Information: $($selectedGame.Name)" -ForegroundColor Green -Log Write-CCLSHost "=======================" -ForegroundColor Green -Log Write-CCLSHost "Local Size: $($selectedGame.LocalSize)" -Log Write-CCLSHost "Location: $($selectedGame.LocalPath)" -Log Write-CCLSHost "Type: Manual Installation (No game metadata found)" -Log Write-CCLSHost "`nNote: This appears to be a manually installed game without CCLS metadata." -ForegroundColor Yellow -Log Write-CCLSHost "You can view the file structure but cannot update or get detailed info." -ForegroundColor Yellow -Log } } "del" { Write-CCLSHost "`nDeleting: $($selectedGame.Name)" -ForegroundColor Red -Log if ($selectedGame.HasMetadata) { # Use ID-based deletion for games with metadata Remove-Game -Identifier $selectedGame.Id -SearchType "id" } else { # Use folder-based deletion for manual installations Remove-Game -Identifier $selectedGame.Name -SearchType "gamedir" } Write-CCLSHost "`nReturning to browse menu..." -ForegroundColor Cyan -Log } "update" { if (-not $selectedGame.HasMetadata) { Write-CCLSHost "Cannot update '$($selectedGame.Name)' - this appears to be a manual installation." -ForegroundColor Yellow -Log Write-CCLSHost "Manual installations cannot be automatically updated." -ForegroundColor Yellow -Log continue } # Check if credentials are available for online check if ($null -eq $script:cachedCredentials) { Write-CCLSHost "Cannot check for updates - not logged in." -ForegroundColor Red -Log continue } Write-CCLSHost "`nChecking for updates for: $($selectedGame.Name) ($($selectedGame.Id))" -ForegroundColor Cyan -Log # Use the existing search function to get online version try { $infoParams = @{ Uri = "$cliApiUrl/search.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/2.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password id = $selectedGame.Id } } $onlineGameInfo = Invoke-RestMethod @infoParams if ($onlineGameInfo.success) { $onlineVersion = if ($onlineGameInfo.version -and $onlineGameInfo.version -ne "") { $onlineGameInfo.version } else { "Unknown" } Write-CCLSHost "Local version: $($selectedGame.LocalVersion)" -ForegroundColor Yellow -Log Write-CCLSHost "Online version: $onlineVersion" -ForegroundColor Yellow -Log if ($selectedGame.LocalVersion -ne "Unknown" -and $onlineVersion -ne "Unknown") { if ($selectedGame.LocalVersion -eq $onlineVersion) { Write-CCLSHost "Game is already up to date!" -ForegroundColor Green -Log } else { Write-CCLSHost "Update available! Starting download..." -ForegroundColor Cyan -Log # Call the existing get function to update Get-Game -id $selectedGame.Id } } else { Write-CCLSHost "Cannot determine if update is needed due to unknown version information." -ForegroundColor Yellow -Log Write-CCLSHost "Would you like to re-download anyway? (Y/N)" -ForegroundColor Yellow -Log $confirmation = Read-Host Write-CCLSHost "$confirmation" -NoConsole -Log if ($confirmation.ToLower() -eq "y") { Get-Game -id $selectedGame.Id } } } else { Write-CCLSHost "Error checking for updates: $($onlineGameInfo.message)" -ForegroundColor Red -Log } } catch { Write-CCLSHost "Error checking for updates: $($_.Exception.Message)" -ForegroundColor Red -Log } Write-CCLSHost "`nReturning to browse menu..." -ForegroundColor Cyan -Log } "refresh" { if (-not $selectedGame.HasMetadata) { Write-CCLSHost "Cannot refresh info for '$($selectedGame.Name)' - this is a manual installation." -ForegroundColor Yellow -Log continue } # Check if credentials are available if ($null -eq $script:cachedCredentials) { Write-CCLSHost "Cannot refresh info - not logged in." -ForegroundColor Red -Log continue } Write-CCLSHost "`nRefreshing info for: $($selectedGame.Name) ($($selectedGame.Id))" -ForegroundColor Cyan -Log # Call the existing search function to show current online info Search-Game -id $selectedGame.Id } default { Write-CCLSHost "Unknown command '$command'. Available commands: view, del, update, refresh" -ForegroundColor Red -Log } } } } catch { Write-CCLSHost "Error during browse operation: $($_.Exception.Message)" -ForegroundColor Red -Log } } # Log management functions for CCLS CLI # Function to get all log files sorted by newest to oldest function Get-LogFiles { param ( [int]$Limit = -1 # -1 means no limit, show all ) try { # Get all log files from the logs folder $logFiles = Get-ChildItem -Path $logsFolder -Filter "*.log" -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending if ($logFiles.Count -eq 0) { return @() } # Apply limit if specified if ($Limit -gt 0 -and $Limit -lt $logFiles.Count) { $logFiles = $logFiles | Select-Object -First $Limit } return $logFiles } catch { Write-CCLSHost "Error retrieving log files: $($_.Exception.Message)" -ForegroundColor Red -Log return @() } } # Function to display log file contents function Show-LogFile { param ( [string]$LogFileName ) $logPath = Join-Path -Path $logsFolder -ChildPath $LogFileName # Check if log file exists if (-not (Test-Path $logPath)) { Write-CCLSHost "Log file '$LogFileName' not found." -ForegroundColor Red -Log return } try { # Read file content line by line to avoid issues with large files $logContent = Get-Content -Path $logPath -ErrorAction Stop Write-CCLSHost "`nLog File Contents: $LogFileName" -ForegroundColor Green -Log Write-CCLSHost "================================" -ForegroundColor Green -Log # Display content line by line to avoid output issues foreach ($line in $logContent) { Write-CCLSHost $line -Log } Write-CCLSHost "`n[End of log file]" -ForegroundColor Gray -Log } catch { Write-CCLSHost "Error reading log file: $($_.Exception.Message)" -ForegroundColor Red -Log } } # Function to delete a log file function Remove-LogFile { param ( [string]$LogFileName, [switch]$Force ) $logPath = Join-Path -Path $logsFolder -ChildPath $LogFileName # Check if log file exists if (-not (Test-Path $logPath)) { Write-CCLSHost "Log file '$LogFileName' not found." -ForegroundColor Red -Log return } # Get file info for confirmation $fileInfo = Get-Item $logPath $fileSize = Format-Size -Size $fileInfo.Length $lastModified = $fileInfo.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss") Write-CCLSHost "`nLog File Deletion Confirmation" -ForegroundColor Red -Log Write-CCLSHost "=============================" -ForegroundColor Red -Log Write-CCLSHost "File: $LogFileName" -Log Write-CCLSHost "Size: $fileSize" -Log Write-CCLSHost "Last Modified: $lastModified" -Log if (-not $Force) { Write-CCLSHost "`nAre you sure you want to delete this log file? (Y/N)" -ForegroundColor Yellow -Log $confirmation = Read-Host Write-CCLSHost "$confirmation" -NoConsole -Log if ($confirmation.ToLower() -ne "y") { Write-CCLSHost "Deletion cancelled." -ForegroundColor Green -Log return } } else { Write-CCLSHost "#Force deletion enabled, skipping confirmation" -Log -NoConsole } try { Remove-Item -Path $logPath -Force Write-CCLSHost "`nLog file '$LogFileName' has been successfully deleted." -ForegroundColor Green -Log } catch { Write-CCLSHost "Error deleting log file: $($_.Exception.Message)" -ForegroundColor Red -Log } } # Function to open a log file with default system application function Open-LogFile { param ( [string]$LogFileName ) $logPath = Join-Path -Path $logsFolder -ChildPath $LogFileName # Check if log file exists if (-not (Test-Path $logPath)) { Write-CCLSHost "Log file '$LogFileName' not found." -ForegroundColor Red -Log return } try { Write-CCLSHost "Opening log file '$LogFileName' with default application..." -ForegroundColor Cyan -Log Start-Process -FilePath $logPath Write-CCLSHost "Log file opened successfully." -ForegroundColor Green -Log } catch { Write-CCLSHost "Error opening log file: $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "The system may not have a default application associated with .log files." -ForegroundColor Yellow -Log } } # Main log list browser function function Show-LogBrowser { param ( [int]$Limit = -1 ) # Get log files $logFiles = Get-LogFiles -Limit $Limit if ($logFiles.Count -eq 0) { Write-CCLSHost "No log files found in $logsFolder" -ForegroundColor Yellow -Log return } # Display header $limitText = if ($Limit -gt 0) { " (showing latest $Limit)" } else { "" } Write-CCLSHost "`n==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "Log Files Browser - $($logFiles.Count) files found$limitText" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log # Calculate column widths $maxNameLength = ($logFiles | ForEach-Object { $_.Name.Length } | Measure-Object -Maximum).Maximum $nameWidth = [Math]::Max($maxNameLength, 30) # Display header row Write-CCLSHost ("#".PadRight(3) + "File Name".PadRight($nameWidth + 2) + "Size".PadRight(12) + "Last Modified".PadRight(20)) -ForegroundColor Cyan -Log Write-CCLSHost ("-" * 3 + " " + "-" * ($nameWidth + 1) + " " + "-" * 11 + " " + "-" * 19) -ForegroundColor Cyan -Log # Display each log file for ($i = 0; $i -lt $logFiles.Count; $i++) { $logFile = $logFiles[$i] $displayNumber = $i + 1 $fileSize = Format-Size -Size $logFile.Length $lastModified = $logFile.LastWriteTime.ToString("yyyy-MM-dd HH:mm") # Format the line $line = "$displayNumber".PadRight(3) + "$($logFile.Name)".PadRight($nameWidth + 2) + "$fileSize".PadRight(12) + $lastModified # Color code based on age (newer files are greener) $age = (Get-Date) - $logFile.LastWriteTime if ($age.TotalDays -lt 1) { $color = "Green" } elseif ($age.TotalDays -lt 7) { $color = "Yellow" } else { $color = "White" } Write-CCLSHost $line -ForegroundColor $color -Log } # Show command help Write-CCLSHost "`nCommands:" -ForegroundColor Yellow -Log Write-CCLSHost " view [number/filename] - View contents of a log file" -ForegroundColor Cyan -Log Write-CCLSHost " del [number/filename] - Delete a log file" -ForegroundColor Cyan -Log Write-CCLSHost " open [number/filename] - Open log file with default application" -ForegroundColor Cyan -Log Write-CCLSHost " [number] - Same as 'view [number]'" -ForegroundColor Cyan -Log Write-CCLSHost " Press Enter to return to main menu" -ForegroundColor Cyan -Log # Command input loop while ($true) { Write-CCLSHost "`nLogs> " -ForegroundColor Yellow -NoNewline -Log $userInput = Read-Host Write-CCLSHost "$userInput" -NoConsole -Log # Check for empty input (exit) if ([string]::IsNullOrWhiteSpace($userInput)) { Write-CCLSHost "Returning to main menu..." -ForegroundColor Green -Log break } # Parse the command $inputParts = $userInput.Trim().Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries) if ($inputParts.Count -eq 0) { continue } $command = $inputParts[0].ToLower() $fileIdentifier = $null # Handle different command formats if ($inputParts.Count -eq 1) { # Single input - could be just a number or a command $fileNumber = 0 if ([int]::TryParse($command, [ref]$fileNumber)) { # It's just a number, treat as 'view' $fileIdentifier = $fileNumber $command = "view" } else { Write-CCLSHost "Invalid command. Please specify a file number or filename (e.g., 'view 1', 'del mylog.log', 'open 3')." -ForegroundColor Red -Log continue } } elseif ($inputParts.Count -eq 2) { # Command with identifier $fileIdentifier = $inputParts[1] } else { Write-CCLSHost "Invalid command format. Use: [command] [number/filename]" -ForegroundColor Red -Log continue } # Resolve file identifier to actual log file $selectedLogFile = $null $fileNumber = 0 if ([int]::TryParse($fileIdentifier, [ref]$fileNumber)) { # It's a number if ($fileNumber -lt 1 -or $fileNumber -gt $logFiles.Count) { Write-CCLSHost "Invalid file number. Please enter a number between 1 and $($logFiles.Count)." -ForegroundColor Red -Log continue } $selectedLogFile = $logFiles[$fileNumber - 1] } else { # It's a filename $selectedLogFile = $logFiles | Where-Object { $_.Name -eq $fileIdentifier } if (-not $selectedLogFile) { Write-CCLSHost "Log file '$fileIdentifier' not found in the current list." -ForegroundColor Red -Log continue } } # Execute the command switch ($command) { "view" { Write-CCLSHost "`nViewing log file: $($selectedLogFile.Name)" -ForegroundColor Green -Log Show-LogFile -LogFileName $selectedLogFile.Name } "del" { Write-CCLSHost "`nDeleting log file: $($selectedLogFile.Name)" -ForegroundColor Red -Log Remove-LogFile -LogFileName $selectedLogFile.Name # Refresh the log files list after deletion Write-CCLSHost "`nRefreshing log list..." -ForegroundColor Cyan -Log $logFiles = Get-LogFiles -Limit $Limit if ($logFiles.Count -eq 0) { Write-CCLSHost "No log files remaining. Returning to main menu..." -ForegroundColor Yellow -Log break } } "open" { Write-CCLSHost "`nOpening log file: $($selectedLogFile.Name)" -ForegroundColor Cyan -Log Open-LogFile -LogFileName $selectedLogFile.Name } default { Write-CCLSHost "Unknown command '$command'. Available commands: view, del, open" -ForegroundColor Red -Log } } } } # Function to display a specific version's changelog function Show-Changelog { param ( [string]$Version ) # Handle special cases first if ($Version -eq "list") { # List all available changelogs using the dedicated endpoint try { $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("User-Agent", "CCLS-CLI/2.0") $listUrl = "$baseUrl/CLI/api/2.0/changelogs_list.php" $response = $webClient.DownloadString($listUrl) # Parse the JSON response $changelogVersions = $response | ConvertFrom-Json Write-CCLSHost "Available Changelogs:" -ForegroundColor Green -Log Write-CCLSHost "-------------------" -ForegroundColor Green -Log if ($changelogVersions.Count -eq 0) { Write-CCLSHost "No changelogs available." -ForegroundColor Yellow -Log } else { foreach ($version in $changelogVersions) { Write-CCLSHost " $version" -Log } } Write-CCLSHost "`nUse 'changelog [version]' to view a specific changelog" -ForegroundColor Cyan -Log } catch { Write-CCLSHost "Error retrieving changelog list" -ForegroundColor Red -Log } return } elseif ($Version -eq "latest") { # Show the latest version's changelog try { # Use existing Test-VersionUpdate function to get latest version $versionInfo = Test-VersionUpdate $latestVersion = $versionInfo.LatestVersion if ([string]::IsNullOrWhiteSpace($latestVersion)) { Write-CCLSHost "Error: Unable to determine latest version." -ForegroundColor Red -Log return } # Now get that version's changelog Show-Changelog -Version $latestVersion } catch { Write-CCLSHost "Error retrieving latest version information" -ForegroundColor Red -Log } return } # For a specific version, first check if it exists in the available versions try { $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("User-Agent", "CCLS-CLI/2.0") # Get the list of available versions first - Updated for v2.0 API structure $listUrl = "$baseUrl/CLI/api/2.0/changelogs_list.php" $response = $webClient.DownloadString($listUrl) $availableVersions = $response | ConvertFrom-Json # Check if the requested version is available if ($availableVersions -contains $Version) { # Version exists, fetch and display the changelog - Updated for v2.0 API structure $changelogUrl = "https://games.ccls.icu/CLI/changelogs/$Version.txt" $changelogContent = $webClient.DownloadString($changelogUrl) Write-CCLSHost "Changelog for Version $Version" -ForegroundColor Green -Log Write-CCLSHost "------------------------" -ForegroundColor Green -Log Write-CCLSHost $changelogContent -Log } else { # Version doesn't exist in the available versions Write-CCLSHost "Unknown version. Type 'changelog list' for a list of all versions." -ForegroundColor Red -Log } } catch { Write-CCLSHost "Unknown version. Type 'changelog list' for a list of all versions." -ForegroundColor Red -Log } } # Fixed Start-CommandInterface function (complete replacement) function Start-CommandInterface($username) { $running = $true $sessionMetrics = @{ StartTime = Get-Date SessionId = [System.Guid]::NewGuid().ToString() CommandCount = 0 LastCommand = "" LastAccess = Get-Date } while ($running) { Write-CCLSHost "CCLS>" -ForegroundColor Yellow -NoNewline -Log $command = Read-Host Write-CCLSHost "$command" -NoConsole -Log # Check for empty input - if empty, just continue to next prompt if ([string]::IsNullOrWhiteSpace($command)) { continue } $specialCommandProcessed = Update-SessionMetrics -InputCommand $command -SessionData $sessionMetrics if ($specialCommandProcessed) { continue } # Flag to track if a help command was processed $helpProcessed = $false # Flag to track if a base command was processed $baseCommandProcessed = $false # Process help commands first switch -Regex ($command.ToLower()) { # Add comprehensive help command "^help\s+(-list-all|-a)$" { Write-CCLSHost "CCLS Games CLI - Complete Command Reference" -ForegroundColor Green -Log Write-CCLSHost "=======================================" -ForegroundColor Green -Log Write-CCLSHost "`nBASIC COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "-------------" -ForegroundColor Yellow -Log Write-CCLSHost "help - Display basic help information" -Log Write-CCLSHost "help -list-all, help -a - Display this complete command reference" -Log Write-CCLSHost "clear, cls - Clear the console screen" -Log Write-CCLSHost "exit, quit - Exit the application" -Log Write-CCLSHost "logout - Log out and exit" -Log Write-CCLSHost "forget - Remove stored credentials" -Log Write-CCLSHost "`nSYSTEM COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "--------------" -ForegroundColor Yellow -Log Write-CCLSHost "setup - Configure download directories" -Log Write-CCLSHost "check - Check system requirements" -Log Write-CCLSHost "version - Display current version information" -Log Write-CCLSHost "update - Update the CLI tool to the latest version" -Log Write-CCLSHost "`nINSTALL COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "---------------" -ForegroundColor Yellow -Log Write-CCLSHost "install help - Show install command help" -Log Write-CCLSHost "install 7zip - Install 7-Zip utility for extraction" -Log Write-CCLSHost "install python - Download and install Python" -Log Write-CCLSHost "install requests - Install Python requests library" -Log Write-CCLSHost "`nSEARCH COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "--------------" -ForegroundColor Yellow -Log Write-CCLSHost "search help - Show search command help" -Log Write-CCLSHost "search [id] - Search for game or bundle information by ID" -Log Write-CCLSHost "search game [game] - Search for games matching your search term" -Log Write-CCLSHost "search library, search lib - List all available games and bundles" -Log Write-CCLSHost "`nDOWNLOAD COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "----------------" -ForegroundColor Yellow -Log Write-CCLSHost "get help - Show get command help" -Log Write-CCLSHost "get [id] - Download and install a game or bundle by ID" -Log Write-CCLSHost "get game [name] - Download and install a game by name" -Log Write-CCLSHost "get [id] -y - Download game by ID without confirmation prompt" -Log Write-CCLSHost "get game [name] -y - Download game by name without confirmation" -Log Write-CCLSHost "`nLIST COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "------------" -ForegroundColor Yellow -Log Write-CCLSHost "list help - Show list command help" -Log Write-CCLSHost "list all - List all installed games" -Log Write-CCLSHost "list all -d - List all installed games with details" -Log Write-CCLSHost "list [id] - Show info about a specific game by ID" -Log Write-CCLSHost "list [folder name] - Show info about a specific game by folder name" -Log Write-CCLSHost "list game [name] - Show info about a specific game by name" -Log Write-CCLSHost "list gamedir [folder name] - Show info about a specific game by folder name" -Log Write-CCLSHost "list [id] -d - Show detailed info about a specific game by ID" -Log Write-CCLSHost "list [folder name] -d - Show detailed info about a specific game by folder name" -Log Write-CCLSHost "list game [name] -d - Show detailed info about a specific game by name" -Log Write-CCLSHost "list gamedir [folder name] -d - Show detailed info about a specific game by folder name" -Log Write-CCLSHost "list [id] -tree - View full file tree of a specific game by ID" -Log Write-CCLSHost "list [folder name] -tree - View full file tree of a specific game by folder name" -Log Write-CCLSHost "list game [name] -tree - View full file tree of a specific game by name" -Log Write-CCLSHost "list gamedir [folder name] -tree - View full file tree of a specific game by folder name" -Log Write-CCLSHost "list [id] -tree -d - View full file tree and detailed info of a specific game by ID" -Log Write-CCLSHost "list [folder name] -tree -d - View full file tree and detailed info of a specific game by folder name" -Log Write-CCLSHost "list game [name] -tree -d - View full file tree and detailed info of a specific game by name" -Log Write-CCLSHost "list gamedir [folder name] -tree -d - View full file tree and detailed info of a specific game by folder name" -Log Write-CCLSHost "`nDELETE COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "--------------" -ForegroundColor Yellow -Log Write-CCLSHost "del help - Show del command help" -Log Write-CCLSHost "del [folder name] - Delete a game by folder name (with confirmation)" -Log Write-CCLSHost "del [id] - Delete a game by id (with confirmation)" -Log Write-CCLSHost "del game [name] - Delete a game by name (with confirmation)" -Log Write-CCLSHost "del gamedir [folder name] - Delete a game by folder name (with confirmation)" -Log Write-CCLSHost "del [folder name] -y - Delete a game by folder name (without confirmation)" -Log Write-CCLSHost "del [id] -y - Delete a game by id (without confirmation)" -Log Write-CCLSHost "del game [name] -y - Delete a game by name (without confirmation)" -Log Write-CCLSHost "del gamedir [folder name] -y - Delete a game by folder name (without confirmation)" -Log Write-CCLSHost "`nCHANGELOG COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "---------------" -ForegroundColor Yellow -Log Write-CCLSHost "changelog help - Show changelog command help" -Log Write-CCLSHost "changelog [version] - Display changelog for a specific version" -Log Write-CCLSHost "changelog list - Show list of all available changelogs" -Log Write-CCLSHost "changelog latest - Display changelog for the latest version" -Log Write-CCLSHost "`nBROWSE COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "--------------" -ForegroundColor Yellow -Log Write-CCLSHost "browse - Interactive browser for installed games with detailed info" -Log Write-CCLSHost "browse help - Show browse command help" -Log Write-CCLSHost "`nLOG COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "------------" -ForegroundColor Yellow -Log Write-CCLSHost "log help - Show log command help" -Log Write-CCLSHost "log list - Browse all log files interactively" -Log Write-CCLSHost "log list -a, log list -all - Browse all log files interactively" -Log Write-CCLSHost "log list -[number] - Show only specified number of newest log files" -Log Write-CCLSHost "log view [filename] - View contents of a specific log file" -Log Write-CCLSHost "log del [filename] - Delete a log file (with confirmation)" -Log Write-CCLSHost "log del [filename] -y - Delete a log file (without confirmation)" -Log Write-CCLSHost "log open [filename] - Open log file with default application" -Log Write-CCLSHost "`nDEVELOPER MODE COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "---------------" -ForegroundColor Yellow -Log Write-CCLSHost "devmode help - Show devmode command help (and information on usage of devmode)" -Log Write-CCLSHost "devmode - Activates and Deactivates Developer mode" -Log $helpProcessed = $true } # Main help command "^help$" { Write-CCLSHost "CCLS Games CLI - Help Overview" -ForegroundColor Green -Log Write-CCLSHost "============================" -ForegroundColor Green -Log Write-CCLSHost "`nBasic Commands:" -ForegroundColor Cyan -Log Write-CCLSHost " help - Show this help message" -Log Write-CCLSHost " help -list-all, help -a - Show all available commands in one big array" -Log Write-CCLSHost " clear, cls - Clear the console screen" -Log Write-CCLSHost " setup - Configure download directories" -Log Write-CCLSHost " check - Check system requirements (Python, requests, 7-Zip)" -Log Write-CCLSHost " version - Display the current version and check for updates" -Log Write-CCLSHost " update - Update the CLI tool to the latest version" -Log Write-CCLSHost " exit, quit - Exit the application" -Log Write-CCLSHost " logout - Log out and exit" -Log Write-CCLSHost " forget - Remove stored credentials" -Log Write-CCLSHost "`nGame Management:" -ForegroundColor Cyan -Log Write-CCLSHost " browse - Interactive browser for installed games showing versions and status" -Log Write-CCLSHost " search [cg0000/cb0000] - Search for game/bundle information" -Log Write-CCLSHost " search lib - List all available games and bundles" -Log Write-CCLSHost " get [cg0000/cb0000] - Download and install a game/bundle by ID" -Log Write-CCLSHost " get game [name] - Download and install a game by name" -Log Write-CCLSHost " list all - List installed games (use -d for details)" -Log Write-CCLSHost " list game [name] - Show info about a specific game" -Log Write-CCLSHost " del game [name] - Delete an installed game" -Log Write-CCLSHost " changelog [version] - Display changelog for a specific version" -Log Write-CCLSHost " log list - Browse and manage log files" -Log Write-CCLSHost " install [utility] - Install specified utility" -Log Write-CCLSHost "`nFor detailed help on specific commands, type:" -ForegroundColor Yellow -Log Write-CCLSHost " [command] help - e.g., 'search help' or 'get help'" -Log $helpProcessed = $true } # Sub-help commands with standardized formatting "^search\s+help$" { Write-CCLSHost "CCLS Games CLI - Search Command Help" -ForegroundColor Green -Log Write-CCLSHost "=================================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " search [id] - Search for a specific game or bundle" -Log Write-CCLSHost " search game [term] - Search for games matching your search term" -Log Write-CCLSHost " search library - List all available games and bundles" -Log Write-CCLSHost " search lib - List all available games and bundles" -Log Write-CCLSHost "`nParameters:" -ForegroundColor Cyan -Log Write-CCLSHost " [id] - Game ID (cg0000) or Bundle ID (cb0000)" -Log Write-CCLSHost " [term] - Used to search games and bundles matching specified term" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " search cg0025 - Get information about game with ID cg0025" -Log Write-CCLSHost " search cb0010 - Get information about bundle with ID cb0010" -Log Write-CCLSHost " search game assasins - Get a list of games matching 'assasins'" -Log Write-CCLSHost " search library - Display the complete game and bundle library" -Log Write-CCLSHost " search lib - Display the complete game and bundle library" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - Use 'search game [term]' for a list of games matching your search term:" -Log Write-CCLSHost " - When in the selectiong field of 'search game [term]' you can enter 'get [number]' to download the game." -Log Write-CCLSHost " - Game/Bundle IDs can be found in the URL on the website:" -Log Write-CCLSHost " https://games.ccls.icu/game.php?id=cg0000" -Log Write-CCLSHost " https://games.ccls.icu/bundle.php?id=cb0000" -Log Write-CCLSHost " - You can also find IDs by using the 'search library' command" -Log $helpProcessed = $true } "^get\s+help$" { Write-CCLSHost "CCLS Games CLI - Get Command Help" -ForegroundColor Green -Log Write-CCLSHost "==============================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " get [id] [options] - Download and install a game or bundle by ID" -Log Write-CCLSHost " get game [name] [options] - Download and install a game by name" -Log Write-CCLSHost "`nParameters:" -ForegroundColor Cyan -Log Write-CCLSHost " [id] - Game ID (cg0000) or Bundle ID (cb0000)" -Log Write-CCLSHost " [name] - Game name (case-insensitive, supports partial matching)" -Log Write-CCLSHost "`nOptions:" -ForegroundColor Cyan -Log Write-CCLSHost " -y - Skip confirmation prompts (auto-confirm)" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " get cg0025 - Download and install game with ID cg0025" -Log Write-CCLSHost " get cb0010 -y - Download and install bundle with ID cb0010 without prompts" -Log Write-CCLSHost " get game tekken 8 - Download and install TEKKEN 8 by name" -Log Write-CCLSHost " get game plateup -y - Download and install PlateUp! by name without prompts" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - Game/Bundle IDs can be found using the 'search library' command" -Log Write-CCLSHost " - Game names are case-insensitive and support partial matching" -Log Write-CCLSHost " - The command will download, extract, and save game information" -Log Write-CCLSHost " - Downloads can be stopped with Ctrl+Z" -Log $helpProcessed = $true } "^list\s+help$" { Write-CCLSHost "CCLS Games CLI - List Command Help" -ForegroundColor Green -Log Write-CCLSHost "===============================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " list all [options] - List all installed games" -Log Write-CCLSHost " list [folder name] [options] - Display information about a specific game using game folder name" -Log Write-CCLSHost " list [id] [options] - Display information about a specific game using game ID" -Log Write-CCLSHost " list game [name] [options] - Display information about a specific game using game name" -Log Write-CCLSHost " list gamedir [folder name] [options] - Display information about a specific game using game folder name" -Log Write-CCLSHost "`nParameters:" -ForegroundColor Cyan -Log Write-CCLSHost " [id] - Game ID (cg0000) or Bundle ID (cb0000)" -Log Write-CCLSHost " [name] - Game name (case-insensitive, supports partial matching)" -Log Write-CCLSHost " [folder name] - Game folder name (case-insensitive)" -Log Write-CCLSHost "`nOptions:" -ForegroundColor Cyan -Log Write-CCLSHost " -d - View extended details (Description, etc) (works on all list commands)" -Log Write-CCLSHost " -tree - View full file tree of a game (works only on game specific list commands)" -Log Write-CCLSHost " -tree -d, -d -tree - View full file tree and detailed info of a game (Description, etc) (works only on game specific list commands)" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " list the long drive - Display information about The Long Drive" -Log Write-CCLSHost " list the long drive -d - Display extended information about The Long Drive" -Log Write-CCLSHost " list the long drive -tree - Display full file tree of The Long Drive" -Log Write-CCLSHost " list the long drive -tree -d - Display full file tree and extended information of The Long Drive" -Log Write-CCLSHost " list cg0023 - Display information about The Long Drive using ID" -Log Write-CCLSHost " list game the long drive - Display information about The Long Drive using name" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - Using 'list all -d' displays current version, size and checks for new updates on all downloaded games" -Log Write-CCLSHost " - Using 'list [term]' only works if name of the game folder or game ID is specified" -Log Write-CCLSHost " - Using 'list game [name]' or 'list [id]' can be useful as it works even if the game's folder name has been changed" -Log $helpProcessed = $true } "^del\s+help$" { Write-CCLSHost "CCLS Games CLI - Delete Command Help" -ForegroundColor Green -Log Write-CCLSHost "=================================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " del [folder name] [options] - Delete an installed game using game folder name" -Log Write-CCLSHost " del [id] [options] - Delete an installed game using game ID" -Log Write-CCLSHost " del game [name] [options] - Delete an installed game using game name" -Log Write-CCLSHost " del gamedir [folder name] [options] - Delete an installed game using game folder name" -Log Write-CCLSHost "`nParameters:" -ForegroundColor Cyan -Log Write-CCLSHost " [name] - Name of the installed game to delete" -Log Write-CCLSHost " [folder name] - Folder name of the installed game to delete" -Log Write-CCLSHost " [id] - ID of the installed game to delete" -Log Write-CCLSHost "`nOptions:" -ForegroundColor Cyan -Log Write-CCLSHost " -y - Skip confirmation prompt (auto-confirm) (works on all del command)" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " del The Long Drive - Delete The Long Drive (with confirmation)" -Log Write-CCLSHost " del cg0023 - Delete The Long Drive (cg0023) (with confirmation)" -Log Write-CCLSHost " del The Long Drive -y - Delete The Long Drive without confirmation" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - If using just 'del [term]' command 'term' must match game folder or id (non-case-sensitive)" -Log Write-CCLSHost " - It's recommended to never use '-y' if you aren't 100% certain about game ID or game name" -Log Write-CCLSHost " - This operation permanently deletes the game files" -Log Write-CCLSHost " - You can always re-download deleted games with the 'get' command" -Log $helpProcessed = $true } # Updated help for install command "^install\s+help$" { Write-CCLSHost "CCLS Games CLI - Install Command Help" -ForegroundColor Green -Log Write-CCLSHost "==================================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " install [utility] - Install required utilities" -Log Write-CCLSHost "`nParameters:" -ForegroundColor Cyan -Log Write-CCLSHost " [utility] - Name of the utility to install" -Log Write-CCLSHost "`nSupported Utilities:" -ForegroundColor Cyan -Log Write-CCLSHost " 7zip - 7-Zip for extracting downloaded files" -Log Write-CCLSHost " python - Python interpreter (latest version)" -Log Write-CCLSHost " requests - Python requests library for better downloads" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " install 7zip - Install 7-Zip for extraction" -Log Write-CCLSHost " install python - Download and install Python" -Log Write-CCLSHost " install requests - Install Python requests library" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - 7-Zip is required for extracting game archives" -Log Write-CCLSHost " - Python is required for advanced download features" -Log Write-CCLSHost " - Python requests is required if using Python downloader" -Log Write-CCLSHost " - Run 'check' to see which utilities need to be installed" -Log $helpProcessed = $true } "^changelog\s+help$" { Write-CCLSHost "CCLS Games CLI - Changelog Command Help" -ForegroundColor Green -Log Write-CCLSHost "===================================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " changelog [version] - Display changelog for a specific version" -Log Write-CCLSHost " changelog list - Show list of all available changelogs" -Log Write-CCLSHost " changelog latest - Display changelog for the latest version" -Log Write-CCLSHost "`nParameters:" -ForegroundColor Cyan -Log Write-CCLSHost " [version] - A valid version of CLI Tool" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " changelog 1.1.4 - Show changes in version 1.1.4" -Log Write-CCLSHost " changelog list - View all available changelog versions" -Log Write-CCLSHost " changelog latest - Show the most recent changes" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - Changelogs document the changes, improvements, and bug fixes in each version" -Log Write-CCLSHost " - The current version is shown with the 'version' command" -Log $helpProcessed = $true } "^devmode\s+help$" { Write-CCLSHost "CCLS Games CLI - Devmode Command Help" -ForegroundColor Green -Log Write-CCLSHost "===================================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost "devmode - Activates and Deactivates Developer mode" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - Devmode is a tool which can be used if you want to try the script out" -Log Write-CCLSHost " - No login or signup is required for devmode" -Log Write-CCLSHost " - It can be easily started by pressing 'Ctrl+Q' at the login prompt" -Log Write-CCLSHost " - When pressed type 'devmode' in the username prompt and press Enter" -Log Write-CCLSHost " - And you are then in devmode, it can also be activated/deactivated by typing 'devmode' in the normal CLI window" -Log Write-CCLSHost " - NOTE: when activating 'devmode' all previously stored credetials are deleted" -Log Write-CCLSHost " - NOTE: all commands that call external API's like 'get' and 'search' wont work when in devmode" -Log $helpProcessed = $true } # Add this to your help command processing section: "^browse\s+help$" { Write-CCLSHost "CCLS Games CLI - Browse Command Help" -ForegroundColor Green -Log Write-CCLSHost "=================================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " browse - Open interactive browser for installed games" -Log Write-CCLSHost "`nDescription:" -ForegroundColor Cyan -Log Write-CCLSHost " The browse command provides a fast, local-first interface to view and manage" -Log Write-CCLSHost " all installed games. It shows local information immediately and only connects" -Log Write-CCLSHost " to the server when you specifically request updates or online info." -Log Write-CCLSHost "`nBrowser Commands:" -ForegroundColor Cyan -Log Write-CCLSHost " view [number] - View detailed information about an installed game" -Log Write-CCLSHost " del [number] - Delete an installed game (with confirmation)" -Log Write-CCLSHost " update [number] - Check for updates and download if available" -Log Write-CCLSHost " refresh [number] - Show current online information for a game" -Log Write-CCLSHost " [number] - Same as 'view [number]'" -Log Write-CCLSHost " [Enter] - Return to main menu" -Log Write-CCLSHost "`nInformation Displayed:" -ForegroundColor Cyan -Log Write-CCLSHost " - Game name (from metadata or folder name)" -Log Write-CCLSHost " - Game ID (for CCLS games)" -Log Write-CCLSHost " - Local folder size" -Log Write-CCLSHost " - Local version (if available)" -Log Write-CCLSHost " - Installation type" -Log Write-CCLSHost "`nGame Types:" -ForegroundColor Cyan -Log Write-CCLSHost " CCLS Game - Game installed via CLI with metadata (Green)" -Log Write-CCLSHost " Manual Install - Game installed manually without metadata (Gray)" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " browse - Open the installed games browser" -Log Write-CCLSHost " [In browser] view 2 - View details for installed game #2" -Log Write-CCLSHost " [In browser] update 3 - Check for updates for game #3" -Log Write-CCLSHost " [In browser] refresh 1 - Show current online info for game #1" -Log Write-CCLSHost " [In browser] del 5 - Delete installed game #5" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - Browse loads instantly using only local data" -Log Write-CCLSHost " - Online checks only happen when you request them (update/refresh)" -Log Write-CCLSHost " - CCLS games support all features, manual installs support view/delete only" -Log Write-CCLSHost " - Games with metadata are shown first, then manual installations" -Log Write-CCLSHost " - All operations use existing game management functions" -Log $helpProcessed = $true } "^log\s+help$" { Write-CCLSHost "CCLS Games CLI - Log Command Help" -ForegroundColor Green -Log Write-CCLSHost "==============================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " log list [options] - Browse and manage log files" -Log Write-CCLSHost " log view [filename] - View contents of a specific log file" -Log Write-CCLSHost " log del [filename] [options] - Delete a specific log file" -Log Write-CCLSHost " log open [filename] - Open log file with default application" -Log Write-CCLSHost "`nList Options:" -ForegroundColor Cyan -Log Write-CCLSHost " -a, -all - List all log files (default behavior)" -Log Write-CCLSHost " -[number] - Limit display to specified number of files" -Log Write-CCLSHost "`nDelete Options:" -ForegroundColor Cyan -Log Write-CCLSHost " -y - Skip confirmation prompt" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " log list - Browse all log files interactively" -Log Write-CCLSHost " log list -5 - Show only the 5 newest log files" -Log Write-CCLSHost " log list -all - Show all log files (same as 'log list')" -Log Write-CCLSHost " log view session_2024.log - View contents of a specific log file" -Log Write-CCLSHost " log del old_session.log - Delete a log file with confirmation" -Log Write-CCLSHost " log del old_session.log -y - Delete a log file without confirmation" -Log Write-CCLSHost " log open session_2024.log - Open log file in default editor" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - Log files are stored in the 'logs' folder" -Log Write-CCLSHost " - Files are sorted by modification date (newest first)" -Log Write-CCLSHost " - Interactive browser allows viewing, deleting, and opening files" -Log Write-CCLSHost " - Opening files uses the system's default .log file association" -Log $helpProcessed = $true } } if ($helpProcessed) { continue } # Handle base commands (commands without parameters) switch -Regex ($command.ToLower()) { "^search$" { Write-CCLSHost "Wrong command usage. Type 'search help' for a list of available commands." -ForegroundColor Red -Log $baseCommandProcessed = $true } "^get$" { Write-CCLSHost "Wrong command usage. Type 'get help' for a list of available commands." -ForegroundColor Red -Log $baseCommandProcessed = $true } "^list$" { Write-CCLSHost "Wrong command usage. Type 'list help' for a list of available commands." -ForegroundColor Red -Log $baseCommandProcessed = $true } "^del$" { Write-CCLSHost "Wrong command usage. Type 'del help' for a list of available commands." -ForegroundColor Red -Log $baseCommandProcessed = $true } "^install$" { Write-CCLSHost "Wrong command usage. Type 'install help' for a list of available commands." -ForegroundColor Red -Log $baseCommandProcessed = $true } "^changelog$" { Write-CCLSHost "Wrong command usage. Type 'changelog help' for a list of available options." -ForegroundColor Red -Log $baseCommandProcessed = $true } "^log$" { Write-CCLSHost "Wrong command usage. Type 'log help' for a list of available commands." -ForegroundColor Red -Log $baseCommandProcessed = $true } } # If a base command was processed, skip the regular command processing if ($baseCommandProcessed) { continue } # Regular command processing with parameters switch -Regex ($command.ToLower()) { "^devmode$" { Toggle-DevMode } "^browse$" { Browse-Games } "^check$" { Test-SystemRequirements } "^exit$|^quit$" { $running = $false Write-CCLSHost "Thank you for using the CCLS Games CLI Tool. Goodbye!" -ForegroundColor Cyan -Log } "^clear$|^cls$" { Clear-ConsoleScreen } "^install(?:\s+(.+))?$" { $utilityName = if ($matches.Count -gt 1) { $matches[1] } else { "" } Install-Utility -UtilityName $utilityName } "^version$" { Show-Version } "^update$" { Update-CliTool } "^setup$" { Start-Setup } "^search\s+(c[gb]\d{4})$" { $id = $matches[1] Search-Game -id $id } "^search\s+game\s+(.+)$" { $searchTerm = $matches[1].Trim() # Pass "game" as the ID and the search term as additional arguments Search-Game -id "game" $searchTerm } "^get\s+(c[gb]\d{4})(?:\s+-y)?$" { $id = $matches[1] $skipConfirmation = $command -match "-y$" Get-Game -id $id -SkipConfirmation:$skipConfirmation } "^get\s+game\s+(.+?)(?:\s+-y)?$" { $gameName = $matches[1].Trim() $skipConfirmation = $command -match "-y$" # Pass "game" as the ID and the game name as additional arguments Get-Game -id "game" -SkipConfirmation:$skipConfirmation $gameName } "^search\s+library$|^search\s+lib$|^library$" { Get-GamesList } "^log\s+list(?:\s+(-a|-all|-\d+))?$" { $option = if ($matches.Count -gt 1 -and $matches[1]) { $matches[1].Trim() } else { "" } if ($option -eq "-a" -or $option -eq "-all" -or $option -eq "") { # Show all log files Show-LogBrowser } elseif ($option -match "^-(\d+)$") { # Show limited number of log files $limit = [int]$matches[1] if ($limit -gt 0) { Show-LogBrowser -Limit $limit } else { Write-CCLSHost "Invalid limit. Please specify a positive number." -ForegroundColor Red -Log } } else { Write-CCLSHost "Invalid option for log list. Use -a, -all, or -[number]." -ForegroundColor Red -Log } } "^log\s+view\s+(.+)$" { $logFileName = $matches[1].Trim() Show-LogFile -LogFileName $logFileName } "^log\s+del\s+(.+?)(?:\s+-y)?$" { $logFileName = $matches[1].Trim() $force = $command -match "-y$" # Remove -y from filename if present $logFileName = $logFileName -replace "\s+-y$", "" $logFileName = $logFileName.Trim() Remove-LogFile -LogFileName $logFileName -Force:$force } "^log\s+open\s+(.+)$" { $logFileName = $matches[1].Trim() Open-LogFile -LogFileName $logFileName } "^list(?:\s+(.+?))?(?:\s+(?:-d|-tree))*$" { if ($matches.Count -le 1 -or [string]::IsNullOrWhiteSpace($matches[1])) { # No parameters provided Write-CCLSHost "Wrong command usage. Type 'list help' for a list of available commands." -ForegroundColor Red -Log continue } $parameters = $matches[1].Trim() $detailed = $command -match "\s+-d\b" $tree = $command -match "\s+-tree\b" # Remove flags from parameters if present $parameters = $parameters -replace "\s+(-d|-tree)+", "" $parameters = $parameters.Trim() # Handle 'list all' commands if ($parameters -eq "all") { Get-InstalledGames -Detailed:$detailed } # Parse the command format for individual games elseif ($parameters -match "^(cg|cb)\d{4}$") { # Direct ID match: list cg0055 Get-GameInfoByIdentifier -Identifier $parameters -SearchType "id" -Detailed:$detailed -Tree:$tree } elseif ($parameters -match "^game\s+(.+)$") { # Game name match: list game [name] $gameName = $matches[1].Trim() Get-GameInfoByIdentifier -Identifier $gameName -SearchType "gamename" -Detailed:$detailed -Tree:$tree } elseif ($parameters -match "^gamedir\s+(.+)$") { # Game directory match: list gamedir [folder] $gameFolderName = $matches[1].Trim() Get-GameInfoByIdentifier -Identifier $gameFolderName -SearchType "gamefoldername" -Detailed:$detailed -Tree:$tree } else { # Default to gamefoldername for backward compatibility: list [folder] Get-GameInfoByIdentifier -Identifier $parameters -SearchType "gamefoldername" -Detailed:$detailed -Tree:$tree } } "^del(?:\s+(.+?))?(?:\s+-y)?$" { if ($matches.Count -le 1 -or [string]::IsNullOrWhiteSpace($matches[1])) { # No parameters provided Write-CCLSHost "Wrong command usage. Type 'del help' for a list of available commands." -ForegroundColor Red -Log continue } $parameters = $matches[1].Trim() $force = $command -match "-y$" # Remove -y from parameters if present $parameters = $parameters -replace "\s+-y$", "" $parameters = $parameters.Trim() # Parse the command format if ($parameters -match "^(cg|cb)\d{4}$") { # Direct ID match: del cg0055 Remove-Game -Identifier $parameters -SearchType "id" -Force:$force } elseif ($parameters -match "^game\s+(.+)$") { # Game name match: del game [name] $gameName = $matches[1].Trim() Remove-Game -Identifier $gameName -SearchType "game" -Force:$force } elseif ($parameters -match "^gamedir\s+(.+)$") { # Game directory match: del gamedir [folder] $gameFolderName = $matches[1].Trim() Remove-Game -Identifier $gameFolderName -SearchType "gamedir" -Force:$force } else { # Default to gamedir for backward compatibility: del [folder] Remove-Game -Identifier $parameters -SearchType "gamedir" -Force:$force } } "^changelog\s+(.+)$" { $versionParam = $matches[1].Trim() Show-Changelog -Version $versionParam $commandProcessed = $true } "^logout$" { $running = $false Write-CCLSHost "Logging out..." -ForegroundColor Cyan -Log } "^forget$" { if (Test-Path $credentialsFile) { Remove-Item -Path $credentialsFile -Force $settings = Initialize-Settings $settings.RememberLogin = $false Save-Settings -settings $settings Write-CCLSHost "Stored credentials have been removed." -ForegroundColor Green -Log } else { Write-CCLSHost "No stored credentials found." -ForegroundColor Yellow -Log } } default { # Extract the base command from the input (first word) $baseCommand = $command.Trim().Split()[0].ToLower() # Known base commands that have help available $knownCommands = @('search', 'get', 'list', 'del', 'install', 'log') if ($knownCommands -contains $baseCommand) { # If it's a known command with incorrect parameters Write-CCLSHost "Wrong command usage. Type '$baseCommand help' for a list of available commands." -ForegroundColor Red -Log } else { # Completely unknown command Write-CCLSHost "Unknown command. Type 'help' for a list of all available commands." -ForegroundColor Red -Log } } } } } # Add a line to log script completion function End-Logging { $endTime = Get-Date "CCLS Games CLI Session ended at $endTime, duration: $(($endTime - [DateTime]::ParseExact($script:sessionStartTime, 'yyyy-MM-dd_HH-mm-ss', $null)).ToString())" | Out-File -FilePath $script:logFile -Append } # Register script exit event to ensure logging is completed Register-EngineEvent -SourceIdentifier ([System.Management.Automation.PsEngineEvent]::Exiting) -Action { End-Logging } | Out-Null function Start-CclsCliTool { # Initialize script variables $script:versionChanged = $false $script:previousVersion = $null # Load settings to check for DevMode (this may generate errors on first run) $settings = Initialize-Settings if ($settings.DevMode) { # In DevMode - skip login and warnings Write-CCLSHost "Hello and welcome to the CCLS Games CLI Tool (Developer Mode)" -ForegroundColor Green -Log Write-CCLSHost "You are in developer mode. 'get', 'search' and any other command that calls external API's will not work." -ForegroundColor Red -Log Write-CCLSHost "Type 'help' for a list of available commands." -ForegroundColor Cyan -Log # Clear any cached credentials to ensure we're not signed in $script:cachedCredentials = $null # Start command interface directly in DevMode Start-CommandInterface -username "Developer" return } # Normal mode - show login prompts (but no version warnings yet) Write-CCLSHost "Hello and welcome to the CCLS Games CLI Tool" -ForegroundColor Green -Log Write-CCLSHost "Before proceeding to using this software you will need to sign in." -Log Write-CCLSHost "If you do not have an account already please go to $baseUrl/login.php?signup to register a new account." -ForegroundColor Cyan -Log # Try auto-login if remember login is enabled $loginResult = @{ Success = $false; DevMode = $false } if ($settings.RememberLogin) { $loginResult = Start-AutoLogin } # If auto-login failed or is disabled, do manual login if (-not $loginResult.Success) { $loginResult = Start-ManualLogin } # If login succeeded, show main interface if ($loginResult.Success) { # Check if we entered DevMode during login if ($loginResult.DevMode) { # DevMode was activated during login - start command interface Start-CommandInterface -username $loginResult.Username return } # Normal login - show welcome message Write-CCLSHost "Welcome to CCLS Games CLI Tool, $($loginResult.Username)!" -ForegroundColor Green -Log # Check dependencies after successful login Test-RequiredDependencies | Out-Null # Show oversight global message if available if ($script:oversightEnabled -and $script:oversightData.global_message) { $messageColor = Convert-HexToConsoleColor -HexColor $script:oversightData.global_message.color Write-CCLSHost $script:oversightData.global_message.message -ForegroundColor $messageColor -Log } # NOW show version warning (after oversight data is loaded) Show-VersionWarningIfNeeded # Check if version has changed and show notification if ($script:versionChanged) { $versionInfo = Test-VersionUpdate $currentVersion = $versionInfo.CurrentVersion Write-CCLSHost "Welcome to $currentVersion! Type 'changelog latest' to view the changes made." -ForegroundColor Green -Log } # Show appropriate message based on setup status (only if not in DevMode) if ($settings.HasCompletedSetup) { Write-CCLSHost "Type 'help' for a list of available commands." -ForegroundColor Cyan -Log } else { Write-CCLSHost "ALERT, type command 'setup' to set critical values before downloading." -ForegroundColor Red -Log } Start-CommandInterface -username $loginResult.Username } else { Write-CCLSHost "Login failed. Press any key to exit..." -ForegroundColor Red -Log $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } } # Format a file size in bytes to a human-readable string function Format-Size { param ( [long]$Size ) if ($Size -ge 1GB) { return "{0:N2} GB" -f ($Size / 1GB) } elseif ($Size -ge 1MB) { return "{0:N2} MB" -f ($Size / 1MB) } elseif ($Size -ge 1KB) { return "{0:N2} KB" -f ($Size / 1KB) } else { return "$Size B" } } # Start the application Try { Start-CclsCliTool } Finally { # Ensure logging is completed even if script exits unexpectedly End-Logging }