diff --git a/install/bash/install.sh b/install/bash/install.sh index d66b753..35bab15 100755 --- a/install/bash/install.sh +++ b/install/bash/install.sh @@ -5,6 +5,9 @@ set -euo pipefail # Base URL for additional scripts CODEPLAIN_SCRIPTS_BASE_URL="${CODEPLAIN_SCRIPTS_BASE_URL:-https://codeplain.ai}" +# Base URL for the Codeplain API (used to verify the API key) +CODEPLAIN_API_URL="${CODEPLAIN_API_URL:-https://api.codeplain.ai}" + # Brand Colors (True Color / 24-bit) YELLOW='\033[38;2;224;255;110m' # #E0FF6E GREEN='\033[38;2;121;252;150m' # #79FC96 @@ -39,6 +42,30 @@ install_uv() { export PATH="$HOME/.local/bin:$PATH" } +# Trim leading/trailing whitespace (spaces, tabs, newlines, carriage returns). +# Users often copy the API key with surrounding whitespace or newlines. +trim_whitespace() { + local value="$*" + value="${value#"${value%%[![:space:]]*}"}" # strip leading whitespace + value="${value%"${value##*[![:space:]]}"}" # strip trailing whitespace + printf '%s' "$value" +} + +# Verify an API key against the Codeplain API's /status endpoint. +# Sets the global VALIDATION_HTTP_CODE and returns 0 only when the key is valid +# (HTTP 200). This checks only the API key, nothing else about the install. +validate_api_key() { + local key="$1" + local http_code + http_code=$(curl -s -o /dev/null -w "%{http_code}" \ + --max-time 30 \ + -X POST "${CODEPLAIN_API_URL}/status" \ + -H "Content-Type: application/json" \ + --data "{\"api_key\":\"${key}\"}" 2>/dev/null || true) + VALIDATION_HTTP_CODE="${http_code:-000}" + [ "$VALIDATION_HTTP_CODE" = "200" ] +} + # Check if uv is installed if ! command -v uv &> /dev/null; then echo -e "${GRAY}uv is not installed.${NC}" @@ -75,10 +102,11 @@ fi # Check if API key already exists SKIP_API_KEY_SETUP=false +API_KEY_VERIFIED=false if [ -n "${CODEPLAIN_API_KEY:-}" ]; then + USE_EXISTING=false if [ "$NONINTERACTIVE" = "1" ]; then - echo -e "${GREEN}✓${NC} Using existing CODEPLAIN_API_KEY (non-interactive mode)." - SKIP_API_KEY_SETUP=true + USE_EXISTING=true else echo -e " You already have an API key configured." echo "" @@ -88,8 +116,33 @@ if [ -n "${CODEPLAIN_API_KEY:-}" ]; then echo "" if [[ ! "$GET_NEW_KEY" =~ ^[Yy]$ ]]; then + USE_EXISTING=true + fi + fi + + # Verify the existing key before trusting it, so a stale/expired key + # doesn't slip through with a false "Sign in successful". + if [ "$USE_EXISTING" = true ]; then + echo -e "${GRAY}Verifying your existing API key...${NC}" + if validate_api_key "$CODEPLAIN_API_KEY"; then echo -e "${GREEN}✓${NC} Using existing API key." SKIP_API_KEY_SETUP=true + API_KEY_VERIFIED=true + elif [ "$VALIDATION_HTTP_CODE" = "401" ]; then + if [ "$NONINTERACTIVE" = "1" ]; then + echo -e "${RED}Existing CODEPLAIN_API_KEY is invalid.${NC}" + echo -e "${GRAY}Set a valid CODEPLAIN_API_KEY and re-run the installer.${NC}" + SKIP_API_KEY_SETUP=true + else + echo -e "${RED}Your existing API key is invalid. Please enter a new one.${NC}" + echo "" + # SKIP_API_KEY_SETUP stays false: fall through to the paste flow. + fi + else + # Could not reach the API. Keep the existing key rather than + # blocking the user over a transient network issue. + echo -e "${GRAY}Could not verify the existing API key (could not reach ${CODEPLAIN_API_URL}). Keeping it.${NC}" + SKIP_API_KEY_SETUP=true fi fi fi @@ -100,8 +153,34 @@ if [ "$SKIP_API_KEY_SETUP" = false ]; then else echo -e "Go to ${YELLOW}https://platform.codeplain.ai${NC} and sign up to get your API key." echo "" - read -r -p "Paste your API key here: " API_KEY < /dev/tty - echo "" + # Keep prompting until we get a valid API key (or the user submits an + # empty value to skip). The pasted key is trimmed and verified against + # the Codeplain API before we accept it. + while true; do + read -r -p "Paste your API key here: " API_KEY < /dev/tty + echo "" + API_KEY="$(trim_whitespace "$API_KEY")" + + # Empty input: let the user skip key setup for now. + if [ -z "$API_KEY" ]; then + break + fi + + echo -e "${GRAY}Verifying your API key...${NC}" + if validate_api_key "$API_KEY"; then + echo -e "${GREEN}✓${NC} API key verified." + echo "" + API_KEY_VERIFIED=true + break + elif [ "$VALIDATION_HTTP_CODE" = "401" ]; then + echo -e "${RED}Invalid API key. Please make sure the full key was copied.${NC}" + echo "" + else + echo -e "${RED}Could not verify the API key (could not reach ${CODEPLAIN_API_URL}).${NC}" + echo -e "${GRAY}Check your internet connection and try again.${NC}" + echo "" + fi + done fi fi @@ -159,8 +238,11 @@ cat << 'EOF' |_| EOF echo "" -echo -e "${GREEN}✓ Sign in successful.${NC}" -echo "" +# Only claim success when a verified API key is actually configured. +if [ "$API_KEY_VERIFIED" = true ]; then + echo -e "${GREEN}✓ Sign in successful.${NC}" + echo "" +fi echo -e " ${WHITE}Welcome to *codeplain!${NC}" echo "" echo -e " ${GRAY}Spec-driven, production-ready code generation${NC}" @@ -307,6 +389,23 @@ if [[ ! "${DOWNLOAD_EXAMPLES:-}" =~ ^[Nn]$ ]]; then run_script "examples.sh" fi +# Final verification: make sure the installed codeplain can actually run and +# reach the API. Only meaningful when an API key is configured, so users who +# skipped the key are not falsely told something went wrong. +if [ -n "${CODEPLAIN_API_KEY:-}" ]; then + # Ensure the freshly installed tool is findable in this session. + export PATH="$HOME/.local/bin:$PATH" + echo -e "${GRAY}Verifying your installation...${NC}" + if codeplain --status > /dev/null 2>&1; then + echo -e "${GREEN}✓${NC} Installation verified." + else + echo -e "${RED}Something went wrong during installation.${NC}" + echo -e "${GRAY}Please restart your terminal and try again, or reinstall with:${NC}" + echo -e " uv tool install --force codeplain" + exit 1 + fi +fi + # Final message if [ "$NONINTERACTIVE" != "1" ]; then clear diff --git a/install/powershell/install.ps1 b/install/powershell/install.ps1 index 75a4b25..a7418da 100644 --- a/install/powershell/install.ps1 +++ b/install/powershell/install.ps1 @@ -12,6 +12,11 @@ if (-not $env:CODEPLAIN_SCRIPTS_BASE_URL) { $env:CODEPLAIN_SCRIPTS_BASE_URL = "https://codeplain.ai" } +# Base URL for the Codeplain API (used to verify the API key) +if (-not $env:CODEPLAIN_API_URL) { + $env:CODEPLAIN_API_URL = "https://api.codeplain.ai" +} + # Brand Colors (True Color / 24-bit) $ESC = [char]27 $YELLOW = "$ESC[38;2;224;255;110m" # #E0FF6E @@ -56,6 +61,33 @@ function Install-Uv { } } +# Verify an API key against the Codeplain API's /status endpoint. +# Returns a status string: "valid" (HTTP 200), "invalid" (HTTP 401), or +# "error" (could not reach the API). This checks only the API key. +function Test-ApiKey { + param([string]$Key) + + $body = @{ api_key = $Key } | ConvertTo-Json -Compress + try { + $response = Invoke-WebRequest -Uri "$($env:CODEPLAIN_API_URL)/status" ` + -Method Post ` + -ContentType "application/json" ` + -Body $body ` + -TimeoutSec 30 ` + -UseBasicParsing ` + -ErrorAction Stop + if ($response.StatusCode -eq 200) { return "valid" } + return "error" + } catch { + $statusCode = $null + if ($_.Exception.Response) { + $statusCode = [int]$_.Exception.Response.StatusCode + } + if ($statusCode -eq 401) { return "invalid" } + return "error" + } +} + # Check if uv is installed if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { Write-Host "${GRAY}uv is not installed.${NC}" @@ -125,10 +157,11 @@ Write-Host "" # Check if API key already exists $skipApiKeySetup = $false +$apiKeyVerified = $false if ($env:CODEPLAIN_API_KEY) { + $useExisting = $false if ($nonInteractive) { - Write-Host "${GREEN}✓${NC} Using existing CODEPLAIN_API_KEY (non-interactive mode)." - $skipApiKeySetup = $true + $useExisting = $true } else { Write-Host " You already have an API key configured." Write-Host "" @@ -138,8 +171,34 @@ if ($env:CODEPLAIN_API_KEY) { Write-Host "" if ($getNewKey -notmatch '^[Yy]$') { + $useExisting = $true + } + } + + # Verify the existing key before trusting it, so a stale/expired key + # doesn't slip through with a false "Sign in successful". + if ($useExisting) { + Write-Host "${GRAY}Verifying your existing API key...${NC}" + $existingResult = Test-ApiKey $env:CODEPLAIN_API_KEY + if ($existingResult -eq "valid") { Write-Host "${GREEN}✓${NC} Using existing API key." $skipApiKeySetup = $true + $apiKeyVerified = $true + } elseif ($existingResult -eq "invalid") { + if ($nonInteractive) { + Write-Host "${RED}Existing CODEPLAIN_API_KEY is invalid.${NC}" + Write-Host "${GRAY}Set a valid CODEPLAIN_API_KEY and re-run the installer.${NC}" + $skipApiKeySetup = $true + } else { + Write-Host "${RED}Your existing API key is invalid. Please enter a new one.${NC}" + Write-Host "" + # $skipApiKeySetup stays false: fall through to the paste flow. + } + } else { + # Could not reach the API. Keep the existing key rather than + # blocking the user over a transient network issue. + Write-Host "${GRAY}Could not verify the existing API key (could not reach $($env:CODEPLAIN_API_URL)). Keeping it.${NC}" + $skipApiKeySetup = $true } } } @@ -151,8 +210,34 @@ if (-not $skipApiKeySetup) { } else { Write-Host "Go to ${YELLOW}https://platform.codeplain.ai${NC} and sign up to get your API key." Write-Host "" - $apiKey = Read-Host "Paste your API key here" - Write-Host "" + # Keep prompting until we get a valid API key (or the user submits an + # empty value to skip). The pasted key is trimmed and verified against + # the Codeplain API before we accept it. + while ($true) { + $apiKey = (Read-Host "Paste your API key here").Trim() + Write-Host "" + + # Empty input: let the user skip key setup for now. + if (-not $apiKey) { + break + } + + Write-Host "${GRAY}Verifying your API key...${NC}" + $result = Test-ApiKey $apiKey + if ($result -eq "valid") { + Write-Host "${GREEN}✓${NC} API key verified." + Write-Host "" + $apiKeyVerified = $true + break + } elseif ($result -eq "invalid") { + Write-Host "${RED}Invalid API key. Please make sure the full key was copied.${NC}" + Write-Host "" + } else { + Write-Host "${RED}Could not verify the API key (could not reach $($env:CODEPLAIN_API_URL)).${NC}" + Write-Host "${GRAY}Check your internet connection and try again.${NC}" + Write-Host "" + } + } } } @@ -181,8 +266,11 @@ Write-Host @' |_| '@ Write-Host "" -Write-Host "${GREEN}✓ Sign in successful.${NC}" -Write-Host "" +# Only claim success when a verified API key is actually configured. +if ($apiKeyVerified) { + Write-Host "${GREEN}✓ Sign in successful.${NC}" + Write-Host "" +} Write-Host " ${WHITE}Welcome to *codeplain!${NC}" Write-Host "" Write-Host " ${GRAY}Spec-driven, production-ready code generation${NC}" @@ -332,6 +420,28 @@ if ($downloadExamples -notmatch '^[Nn]$') { Invoke-SubScript "examples.ps1" } +# Final verification: make sure the installed codeplain can actually run and +# reach the API. Only meaningful when an API key is configured, so users who +# skipped the key are not falsely told something went wrong. +if ($env:CODEPLAIN_API_KEY) { + Write-Host "${GRAY}Verifying your installation...${NC}" + $verifyOk = $false + try { + & codeplain --status *> $null + $verifyOk = ($LASTEXITCODE -eq 0) + } catch { + $verifyOk = $false + } + if ($verifyOk) { + Write-Host "${GREEN}✓${NC} Installation verified." + } else { + Write-Host "${RED}Something went wrong during installation.${NC}" + Write-Host "${GRAY}Please restart your terminal and try again, or reinstall with:${NC}" + Write-Host " uv tool install --force codeplain" + exit 1 + } +} + # Final message if (-not $nonInteractive) { Clear-Host } Write-Host "" diff --git a/plain2code.py b/plain2code.py index 37a20f3..d0d3d9e 100644 --- a/plain2code.py +++ b/plain2code.py @@ -327,7 +327,7 @@ def main(): # noqa: C901 console.error( "Your API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument.\n" ) - return + sys.exit(1) if not args.api: args.api = "https://api.codeplain.ai" @@ -336,6 +336,7 @@ def main(): # noqa: C901 print_status(args.api_key, args.api, system_config.client_version) except Exception as e: console.error(f"Error fetching status: {str(e)}") + sys.exit(1) return template_dirs = file_utils.get_template_directories(args.filename, args.template_dir, DEFAULT_TEMPLATE_DIRS)