diff --git a/manifest.json b/manifest.json index 48212ab8b..1e11b333b 100644 --- a/manifest.json +++ b/manifest.json @@ -795,6 +795,21 @@ "region": "zrh" }, "notes": "API-centric cloud platform with regions in ZRH (Zurich), WDC (Washington DC), and LVS (Las Vegas). Granular resource control (CPU/RAM/Disk independently configurable). Uses 'cloudsigma' user for SSH." + }, + "webdock": { + "name": "Webdock", + "description": "European VPS provider with affordable plans", + "url": "https://webdock.io/", + "type": "api", + "auth": "WEBDOCK_API_TOKEN", + "provision_method": "POST /v1/servers", + "exec_method": "ssh root@IP", + "interactive_method": "ssh -t root@IP", + "defaults": { + "profile": "webdockmicro", + "location": "fi", + "image": "ubuntu2404" + } } }, "matrix": { @@ -1337,6 +1352,21 @@ "cloudsigma/opencode": "missing", "cloudsigma/plandex": "missing", "cloudsigma/kilocode": "missing", - "cloudsigma/continue": "implemented" + "cloudsigma/continue": "implemented", + "webdock/claude": "implemented", + "webdock/openclaw": "missing", + "webdock/nanoclaw": "missing", + "webdock/aider": "implemented", + "webdock/goose": "missing", + "webdock/codex": "missing", + "webdock/interpreter": "missing", + "webdock/gemini": "missing", + "webdock/amazonq": "missing", + "webdock/cline": "implemented", + "webdock/gptme": "missing", + "webdock/opencode": "missing", + "webdock/plandex": "missing", + "webdock/kilocode": "missing", + "webdock/continue": "missing" } -} \ No newline at end of file +} diff --git a/test/mock.sh b/test/mock.sh index 4e62ca284..7487b73a4 100644 --- a/test/mock.sh +++ b/test/mock.sh @@ -263,6 +263,7 @@ _strip_api_base() { https://cloudapi.atlantic.net/*) ENDPOINT=$(echo "$URL" | sed 's|https://cloudapi.atlantic.net/\?||') ;; https://invapi.hostkey.com*) ENDPOINT="${URL#https://invapi.hostkey.com}" ;; https://*.cloudsigma.com/api/2.0*) ENDPOINT=$(echo "$URL" | sed 's|https://[^/]*.cloudsigma.com/api/2.0||') ;; + https://api.webdock.io/v1*) ENDPOINT="${URL#https://api.webdock.io/v1}" ;; esac EP_CLEAN=$(echo "$ENDPOINT" | sed 's|?.*||') } @@ -288,6 +289,7 @@ _validate_body() { vultr) case "$EP_CLEAN" in /instances) _check_fields "label region plan os_id" ;; esac ;; linode) case "$EP_CLEAN" in /linode/instances) _check_fields "label region type image" ;; esac ;; civo) case "$EP_CLEAN" in /instances) _check_fields "hostname size region" ;; esac ;; + webdock) case "$EP_CLEAN" in /servers) _check_fields "name slug locationId profileSlug imageSlug" ;; esac ;; esac } diff --git a/test/record.sh b/test/record.sh index c33f78ce7..0e51c2adf 100644 --- a/test/record.sh +++ b/test/record.sh @@ -31,7 +31,7 @@ ERRORS=0 PROMPT_FOR_CREDS=true # All clouds with REST APIs that we can record from -ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma" +ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma webdock" # --- Endpoint registry --- # Format: "fixture_name:endpoint" @@ -141,6 +141,14 @@ get_endpoints() { "keypairs:/keypairs/" \ "servers:/servers/" ;; + webdock) + printf '%s\n' \ + "account:/account" \ + "publicKeys:/account/publicKeys" \ + "servers:/servers" \ + "profiles:/profiles" \ + "locations:/locations" + ;; esac } @@ -165,6 +173,7 @@ get_auth_env_var() { atlanticnet) printf "ATLANTICNET_API_KEY" ;; hostkey) printf "HOSTKEY_API_KEY" ;; cloudsigma) printf "CLOUDSIGMA_EMAIL" ;; + webdock) printf "WEBDOCK_API_TOKEN" ;; esac } @@ -399,6 +408,7 @@ call_api() { atlanticnet) atlanticnet_api "$endpoint" ;; hostkey) hostkey_api "$endpoint" ;; cloudsigma) cloudsigma_api GET "$endpoint" ;; + webdock) webdock_api GET "$endpoint" ;; esac } @@ -444,6 +454,9 @@ elif cloud == 'hostkey': elif cloud == 'cloudsigma': # CloudSigma returns error objects with 'error_message', 'error_type', etc sys.exit(0 if 'error_message' in d or 'error_type' in d else 1) +elif cloud == 'webdock': + # Webdock returns error objects with 'message' field + sys.exit(0 if 'message' in d and len(d) <= 3 and not any(k in d for k in ('slug','name','status','ipv4')) else 1) else: sys.exit(1) " "$cloud" 2>/dev/null @@ -822,6 +835,12 @@ _live_cloudsigma() { return 0 } +_live_webdock() { + local fixture_dir="$1" + printf '%b\n' " ${YELLOW}skip${NC} Webdock live test (not implemented yet)" >&2 + return 0 +} + # --- Record one cloud --- # Check credentials and prompt if needed; returns 1 to skip this cloud _record_ensure_credentials() { diff --git a/webdock/README.md b/webdock/README.md new file mode 100644 index 000000000..2383236b5 --- /dev/null +++ b/webdock/README.md @@ -0,0 +1,134 @@ +# Webdock + +Webdock is a European VPS provider offering affordable cloud servers with API access. + +## Features + +- **REST API**: Full-featured API for server management +- **Affordable pricing**: Starting from €2.15/month (Intel Xeon) or €4.30/month (AMD EPYC) +- **Multiple locations**: Data centers in Europe (Finland, Netherlands, UK) +- **Developer-friendly**: Free API, automated backups, free SSL, 1-click deployments +- **SSH access**: Full root SSH access to all servers +- **GDPR compliant**: European data protection standards + +## Authentication + +Set your Webdock API token as an environment variable: + +```bash +export WEBDOCK_API_TOKEN="your-api-token-here" +``` + +To obtain an API token: +1. Log in to [https://my.webdock.io](https://my.webdock.io) +2. Go to Account Area > API & Integrations +3. Generate a new API key + +The token will be automatically saved to `~/.config/spawn/webdock.json` for future use. + +## Usage + +Launch Claude Code on a Webdock server: + +```bash +bash webdock/claude.sh +``` + +Or use the CLI: + +```bash +spawn run webdock claude +``` + +## Available Scripts + +- `claude.sh` — Claude Code (Anthropic's CLI coding agent) +- `cline.sh` — Cline (AI pair programming in your editor) +- `aider.sh` — Aider (AI pair programming in the terminal) + +## Configuration + +### Server Profile + +Set the server profile (hardware tier) via environment variable: + +```bash +export WEBDOCK_PROFILE="webdockmicro" # Default +``` + +Available profiles: +- `webdockmicro` — 1 vCPU, 1GB RAM, 25GB SSD (€2.15-4.30/mo) +- `webdocksmall` — 1 vCPU, 2GB RAM, 50GB SSD +- `webdockmedium` — 2 vCPU, 4GB RAM, 80GB SSD +- `webdocklarge` — 4 vCPU, 8GB RAM, 160GB SSD +- `webdockxl` — 6 vCPU, 16GB RAM, 320GB SSD + +Check [Webdock pricing](https://webdock.io/en/pricing) for the full list and current prices. + +### Location + +Set the server location via environment variable: + +```bash +export WEBDOCK_LOCATION="fi" # Default (Finland) +``` + +Available locations: +- `fi` — Finland (Helsinki) +- `nl` — Netherlands (Amsterdam) +- `uk` — United Kingdom (London) + +### Image + +Set the OS image via environment variable: + +```bash +export WEBDOCK_IMAGE="ubuntu2404" # Default +``` + +Available images include Ubuntu, Debian, and various pre-configured stacks (LEMP, WordPress, etc.). +Check the Webdock control panel for the full list of available images. + +### Server Name + +Set a custom server name: + +```bash +export WEBDOCK_SERVER_NAME="my-ai-server" +``` + +If not set, you'll be prompted to enter one. + +## How It Works + +1. **Authenticate**: Validates your API token with Webdock +2. **SSH Key**: Ensures your SSH key is registered (generates ed25519 key if needed) +3. **Provision**: Creates a new server with the specified profile, location, and image +4. **Wait**: Polls until the server is online and SSH-ready +5. **Setup**: Installs the agent and configures OpenRouter API credentials +6. **Launch**: Drops you into an interactive session with the agent + +## Cleanup + +Servers persist after the script exits. To destroy a server: + +```bash +# Using the Webdock API +curl -X DELETE "https://api.webdock.io/v1/servers/SERVER_SLUG" \ + -H "Authorization: Bearer $WEBDOCK_API_TOKEN" +``` + +Or delete from the Webdock control panel: https://my.webdock.io/servers + +## Limitations + +- Webdock is primarily European-focused (data centers in EU only) +- Profile availability may vary by location +- Server creation can take 1-3 minutes depending on load + +## Links + +- Website: https://webdock.io +- API Documentation: https://api.webdock.io/v1 +- Control Panel: https://my.webdock.io +- Pricing: https://webdock.io/en/pricing diff --git a/webdock/aider.sh b/webdock/aider.sh new file mode 100644 index 000000000..31edbba37 --- /dev/null +++ b/webdock/aider.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=webdock/lib/common.sh +if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then + source "${SCRIPT_DIR}/lib/common.sh" +else + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)" +fi + +log_info "Aider on Webdock" +echo "" + +ensure_webdock_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${WEBDOCK_SERVER_IP}" +wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60 + +log_step "Installing Aider..." +run_server "${WEBDOCK_SERVER_IP}" "pip install aider-chat 2>/dev/null || pip3 install aider-chat" +log_info "Aider installed" + +echo "" +if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then + log_info "Using OpenRouter API key from environment" +else + OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) +fi + +MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1 + +log_step "Setting up environment variables..." +inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" + +echo "" +log_info "Webdock server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})" +echo "" + +log_step "Starting Aider..." +sleep 1 +clear +interactive_session "${WEBDOCK_SERVER_IP}" "source ~/.zshrc && aider --model openrouter/${MODEL_ID}" diff --git a/webdock/claude.sh b/webdock/claude.sh new file mode 100644 index 000000000..1002977c4 --- /dev/null +++ b/webdock/claude.sh @@ -0,0 +1,58 @@ +#!/bin/bash +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=webdock/lib/common.sh +if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then + source "${SCRIPT_DIR}/lib/common.sh" +else + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)" +fi + +log_info "Claude Code on Webdock" +echo "" + +ensure_webdock_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${WEBDOCK_SERVER_IP}" +wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60 + +log_step "Verifying Claude Code installation..." +if ! run_server "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then + log_step "Claude Code not found, installing manually..." + run_server "${WEBDOCK_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" +fi +log_info "Claude Code is installed" + +echo "" +if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then + log_info "Using OpenRouter API key from environment" +else + OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) +fi + +log_step "Setting up environment variables..." +inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ + "ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_API_KEY=" \ + "CLAUDE_CODE_SKIP_ONBOARDING=1" \ + "CLAUDE_CODE_ENABLE_TELEMETRY=0" + +setup_claude_code_config "${OPENROUTER_API_KEY}" \ + "upload_file ${WEBDOCK_SERVER_IP}" \ + "run_server ${WEBDOCK_SERVER_IP}" + +echo "" +log_info "Webdock server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})" +echo "" + +log_step "Starting Claude Code..." +sleep 1 +clear +interactive_session "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/webdock/cline.sh b/webdock/cline.sh new file mode 100644 index 000000000..ce83ae19f --- /dev/null +++ b/webdock/cline.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# shellcheck disable=SC2154 +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=webdock/lib/common.sh + +if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then + source "${SCRIPT_DIR}/lib/common.sh" +else + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)" +fi + +log_info "Cline on Webdock" +echo "" + +ensure_webdock_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${WEBDOCK_SERVER_IP}" +wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60 + +log_step "Installing Cline..." +run_server "${WEBDOCK_SERVER_IP}" "npm install -g cline" +log_info "Cline installed" + +echo "" +if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then + log_info "Using OpenRouter API key from environment" +else + OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) +fi + +log_step "Setting up environment variables..." + +inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ + "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + +echo "" +log_info "Webdock server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})" +echo "" + +log_step "Starting Cline..." +sleep 1 +clear +interactive_session "${WEBDOCK_SERVER_IP}" "source ~/.zshrc && cline" diff --git a/webdock/lib/common.sh b/webdock/lib/common.sh new file mode 100644 index 000000000..26a26376e --- /dev/null +++ b/webdock/lib/common.sh @@ -0,0 +1,224 @@ +#!/bin/bash +# Common bash functions for Webdock spawn scripts + +# Bash safety flags +set -eo pipefail + +# ============================================================ +# Provider-agnostic functions +# ============================================================ + +# Source shared provider-agnostic functions (local or remote fallback) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +if [[ -n "$SCRIPT_DIR" && -f "$SCRIPT_DIR/../../shared/common.sh" ]]; then + source "$SCRIPT_DIR/../../shared/common.sh" +else + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/shared/common.sh)" +fi + +# Note: Provider-agnostic functions (logging, OAuth, browser, nc_listen) are now in shared/common.sh + +# ============================================================ +# Webdock specific functions +# ============================================================ + +readonly WEBDOCK_API_BASE="https://api.webdock.io/v1" +# SSH_OPTS is now defined in shared/common.sh + +# Configurable timeout/delay constants +INSTANCE_STATUS_POLL_DELAY=${INSTANCE_STATUS_POLL_DELAY:-5} # Delay between instance status checks + +webdock_api() { + local method="$1" + local endpoint="$2" + local body="${3:-}" + generic_cloud_api "$WEBDOCK_API_BASE" "$WEBDOCK_API_TOKEN" "$method" "$endpoint" "$body" +} + +test_webdock_token() { + local response + response=$(webdock_api GET "/account") + if echo "$response" | grep -q '"email"'; then + log_info "API token validated" + return 0 + else + log_error "API Error: $(extract_api_error_message "$response" "Unable to parse error")" + log_error "How to fix:" + log_warn " 1. Log in to your Webdock account" + log_warn " 2. Go to Account Area > API & Integrations" + log_warn " 3. Generate a new API key" + log_warn " 4. Set WEBDOCK_API_TOKEN environment variable" + return 1 + fi +} + +ensure_webdock_token() { + ensure_api_token_with_provider \ + "Webdock" \ + "WEBDOCK_API_TOKEN" \ + "$HOME/.config/spawn/webdock.json" \ + "https://my.webdock.io/account" \ + "test_webdock_token" +} + +# Check if SSH key is registered with Webdock +webdock_check_ssh_key() { + check_ssh_key_by_fingerprint webdock_api "/account/publicKeys" "$1" +} + +# Register SSH key with Webdock +webdock_register_ssh_key() { + local key_name="$1" + local pub_path="$2" + local pub_key + pub_key=$(cat "$pub_path") + local json_pub_key json_name + json_pub_key=$(json_escape "$pub_key") + json_name=$(json_escape "$key_name") + local register_body="{\"name\":$json_name,\"publicKey\":$json_pub_key}" + local register_response + register_response=$(webdock_api POST "/account/publicKeys" "$register_body") + + if echo "$register_response" | grep -q '"id"'; then + return 0 + else + log_error "API Error: $(extract_api_error_message "$register_response" "$register_response")" + + log_warn "Common causes:" + log_warn " - SSH key already registered with this name" + log_warn " - Invalid SSH key format (must be valid ed25519 public key)" + log_warn " - API token lacks write permissions" + return 1 + fi +} + +ensure_ssh_key() { + ensure_ssh_key_with_provider webdock_check_ssh_key webdock_register_ssh_key "Webdock" +} + +get_server_name() { + get_validated_server_name "WEBDOCK_SERVER_NAME" "Enter server name: " +} + +# get_cloud_init_userdata is now defined in shared/common.sh + +# Build JSON request body for Webdock server creation +# Usage: _webdock_build_server_body NAME SLUG LOCATION_ID PROFILE_SLUG IMAGE_SLUG PUBLIC_KEY_IDS +_webdock_build_server_body() { + local name="$1" slug="$2" location_id="$3" profile_slug="$4" image_slug="$5" public_key_ids="$6" + python3 -c " +import json, sys +name, slug, location_id, profile_slug, image_slug, public_key_ids = sys.argv[1:7] +body = { + 'name': name, + 'slug': slug, + 'locationId': location_id, + 'profileSlug': profile_slug, + 'imageSlug': image_slug, + 'publicKeys': json.loads(public_key_ids) if public_key_ids != '[]' else [] +} +print(json.dumps(body)) +" "$name" "$slug" "$location_id" "$profile_slug" "$image_slug" "$public_key_ids" +} + +# Wait for Webdock server to become active and get its IP +# Sets: WEBDOCK_SERVER_IP +# Usage: _wait_for_webdock_server SERVER_SLUG [MAX_ATTEMPTS] +_wait_for_webdock_server() { + local server_slug="$1" + local max_attempts=${2:-60} + generic_wait_for_instance webdock_api "/servers/${server_slug}" \ + "online" \ + "d['status']" \ + "d['ipv4']" \ + WEBDOCK_SERVER_IP "Server" "${max_attempts}" +} + +create_server() { + local name="$1" + local slug="${name}" + local location_id="${WEBDOCK_LOCATION:-fi}" + local profile_slug="${WEBDOCK_PROFILE:-webdockmicro}" + local image_slug="${WEBDOCK_IMAGE:-ubuntu2404}" + + # Validate env var inputs to prevent injection into Python code + validate_resource_name "$location_id" || { log_error "Invalid WEBDOCK_LOCATION"; return 1; } + validate_resource_name "$profile_slug" || { log_error "Invalid WEBDOCK_PROFILE"; return 1; } + validate_resource_name "$image_slug" || { log_error "Invalid WEBDOCK_IMAGE"; return 1; } + validate_resource_name "$slug" || { log_error "Invalid server slug"; return 1; } + + log_step "Creating Webdock server '$name' (profile: $profile_slug, location: $location_id)..." + + # Get all SSH public key IDs + local public_keys_response + public_keys_response=$(webdock_api GET "/account/publicKeys") + local public_key_ids + public_key_ids=$(echo "$public_keys_response" | python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +if isinstance(data, list): + ids = [k['id'] for k in data if 'id' in k] +else: + ids = [] +print(json.dumps(ids)) +" 2>/dev/null || echo "[]") + + local body + body=$(_webdock_build_server_body "$name" "$slug" "$location_id" "$profile_slug" "$image_slug" "$public_key_ids") + + local response + response=$(webdock_api POST "/servers" "$body") + + if echo "$response" | grep -q '"slug"'; then + WEBDOCK_SERVER_SLUG=$(echo "$response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['slug'])") + export WEBDOCK_SERVER_SLUG + log_info "Server created: slug=$WEBDOCK_SERVER_SLUG" + else + log_error "Failed to create Webdock server" + log_error "API Error: $(extract_api_error_message "$response" "$response")" + log_warn "Common issues:" + log_warn " - Insufficient account balance" + log_warn " - Profile/location unavailable (try different WEBDOCK_PROFILE or WEBDOCK_LOCATION)" + log_warn " - Server limit reached" + log_warn " - Slug already in use" + log_warn "Check your dashboard: https://my.webdock.io/" + return 1 + fi + + _wait_for_webdock_server "$WEBDOCK_SERVER_SLUG" +} + +# SSH operations — delegates to shared helpers (SSH_USER defaults to root) +verify_server_connectivity() { ssh_verify_connectivity "$@"; } +run_server() { ssh_run_server "$@"; } +upload_file() { ssh_upload_file "$@"; } +interactive_session() { ssh_interactive_session "$@"; } + +destroy_server() { + local server_slug="$1" + log_step "Destroying server $server_slug..." + webdock_api DELETE "/servers/$server_slug" + log_info "Server $server_slug destroyed" +} + +list_servers() { + local response + response=$(webdock_api GET "/servers") + python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +servers = data if isinstance(data, list) else [] +if not servers: + print('No servers found') + sys.exit(0) +print(f\"{'NAME':<25} {'SLUG':<25} {'STATUS':<12} {'IP':<16} {'PROFILE':<20}\") +print('-' * 98) +for s in servers: + name = s.get('name', 'N/A') + slug = s['slug'] + status = s.get('status', 'N/A') + ip = s.get('ipv4', 'N/A') + profile = s.get('profileSlug', 'N/A') + print(f'{name:<25} {slug:<25} {status:<12} {ip:<16} {profile:<20}') +" <<< "$response" +}