diff --git a/scaleway/README.md b/scaleway/README.md new file mode 100644 index 00000000..cfc4ec56 --- /dev/null +++ b/scaleway/README.md @@ -0,0 +1,97 @@ +# Scaleway + +Scaleway Cloud instances via REST API. [Scaleway](https://www.scaleway.com/) + +## Agents + +#### Claude Code + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/scaleway/claude.sh) +``` + +#### OpenClaw + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/scaleway/openclaw.sh) +``` + +#### NanoClaw + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/scaleway/nanoclaw.sh) +``` + +#### Aider + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/scaleway/aider.sh) +``` + +#### Goose + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/scaleway/goose.sh) +``` + +#### Codex CLI + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/scaleway/codex.sh) +``` + +#### Open Interpreter + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/scaleway/interpreter.sh) +``` + +#### Gemini CLI + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/scaleway/gemini.sh) +``` + +#### Amazon Q CLI + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/scaleway/amazonq.sh) +``` + +#### Cline + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/scaleway/cline.sh) +``` + +#### gptme + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/scaleway/gptme.sh) +``` + +#### OpenCode + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/scaleway/opencode.sh) +``` + +## Non-Interactive Mode + +```bash +SCALEWAY_SERVER_NAME=dev-mk1 \ +SCW_SECRET_KEY=your-secret-key \ +OPENROUTER_API_KEY=sk-or-v1-xxxxx \ + bash <(curl -fsSL https://openrouter.ai/lab/spawn/scaleway/claude.sh) +``` + +## Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `SCW_SECRET_KEY` | Scaleway API secret key (required) | - | +| `SCW_DEFAULT_PROJECT_ID` | Scaleway project ID (auto-detected if not set) | - | +| `SCALEWAY_SERVER_NAME` | Server name | prompted | +| `SCALEWAY_ZONE` | Availability zone | `fr-par-1` | +| `SCALEWAY_TYPE` | Commercial type (instance size) | `DEV1-S` | +| `OPENROUTER_API_KEY` | OpenRouter API key | prompted via OAuth | diff --git a/scaleway/aider.sh b/scaleway/aider.sh new file mode 100644 index 00000000..71a6a0ab --- /dev/null +++ b/scaleway/aider.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=scaleway/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/scaleway/lib/common.sh)" +fi + +log_info "Aider on Scaleway" +echo "" + +ensure_scaleway_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${SCALEWAY_SERVER_IP}" +install_base_packages "${SCALEWAY_SERVER_IP}" + +log_warn "Installing Aider..." +run_server "${SCALEWAY_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_warn "Setting up environment variables..." +inject_env_vars_ssh "${SCALEWAY_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" + +echo "" +log_info "Scaleway instance setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${SCALEWAY_SERVER_ID}, IP: ${SCALEWAY_SERVER_IP})" +echo "" + +log_warn "Starting Aider..." +sleep 1 +clear +interactive_session "${SCALEWAY_SERVER_IP}" "source ~/.zshrc && aider --model openrouter/${MODEL_ID}" diff --git a/scaleway/amazonq.sh b/scaleway/amazonq.sh new file mode 100644 index 00000000..a0ec0722 --- /dev/null +++ b/scaleway/amazonq.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# shellcheck disable=SC2154 +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=scaleway/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/scaleway/lib/common.sh)" +fi + +log_info "Amazon Q on Scaleway" +echo "" + +ensure_scaleway_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${SCALEWAY_SERVER_IP}" +install_base_packages "${SCALEWAY_SERVER_IP}" + +log_warn "Installing Amazon Q CLI..." +run_server "${SCALEWAY_SERVER_IP}" "curl -fsSL https://desktop-release.q.us-east-1.amazonaws.com/latest/amazon-q-cli-install.sh | bash" +log_info "Amazon Q CLI 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_warn "Setting up environment variables..." +inject_env_vars_ssh "${SCALEWAY_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 "Scaleway instance setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${SCALEWAY_SERVER_ID}, IP: ${SCALEWAY_SERVER_IP})" +echo "" + +log_warn "Starting Amazon Q..." +sleep 1 +clear +interactive_session "${SCALEWAY_SERVER_IP}" "source ~/.zshrc && q chat" diff --git a/scaleway/claude.sh b/scaleway/claude.sh new file mode 100644 index 00000000..1599e0b7 --- /dev/null +++ b/scaleway/claude.sh @@ -0,0 +1,55 @@ +#!/bin/bash +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=scaleway/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/scaleway/lib/common.sh)" +fi + +log_info "Claude Code on Scaleway" +echo "" + +ensure_scaleway_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${SCALEWAY_SERVER_IP}" +install_base_packages "${SCALEWAY_SERVER_IP}" + +log_warn "Installing Claude Code..." +run_server "${SCALEWAY_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" +log_info "Claude Code 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_warn "Setting up environment variables..." +inject_env_vars_ssh "${SCALEWAY_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 ${SCALEWAY_SERVER_IP}" \ + "run_server ${SCALEWAY_SERVER_IP}" + +echo "" +log_info "Scaleway instance setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${SCALEWAY_SERVER_ID}, IP: ${SCALEWAY_SERVER_IP})" +echo "" + +log_warn "Starting Claude Code..." +sleep 1 +clear +interactive_session "${SCALEWAY_SERVER_IP}" "source ~/.zshrc && claude" diff --git a/scaleway/cline.sh b/scaleway/cline.sh new file mode 100644 index 00000000..170f86cc --- /dev/null +++ b/scaleway/cline.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# shellcheck disable=SC2154 +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=scaleway/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/scaleway/lib/common.sh)" +fi + +log_info "Cline on Scaleway" +echo "" + +ensure_scaleway_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${SCALEWAY_SERVER_IP}" +install_base_packages "${SCALEWAY_SERVER_IP}" + +log_warn "Installing Cline..." +run_server "${SCALEWAY_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_warn "Setting up environment variables..." +inject_env_vars_ssh "${SCALEWAY_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 "Scaleway instance setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${SCALEWAY_SERVER_ID}, IP: ${SCALEWAY_SERVER_IP})" +echo "" + +log_warn "Starting Cline..." +sleep 1 +clear +interactive_session "${SCALEWAY_SERVER_IP}" "source ~/.zshrc && cline" diff --git a/scaleway/codex.sh b/scaleway/codex.sh new file mode 100644 index 00000000..03672dcf --- /dev/null +++ b/scaleway/codex.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=scaleway/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/scaleway/lib/common.sh)" +fi + +log_info "Codex CLI on Scaleway" +echo "" + +ensure_scaleway_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${SCALEWAY_SERVER_IP}" +install_base_packages "${SCALEWAY_SERVER_IP}" + +log_warn "Installing Codex CLI..." +run_server "${SCALEWAY_SERVER_IP}" "npm install -g @openai/codex" +log_info "Codex CLI 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_warn "Setting up environment variables..." +inject_env_vars_ssh "${SCALEWAY_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 "Scaleway instance setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${SCALEWAY_SERVER_ID}, IP: ${SCALEWAY_SERVER_IP})" +echo "" + +log_warn "Starting Codex..." +sleep 1 +clear +interactive_session "${SCALEWAY_SERVER_IP}" "source ~/.zshrc && codex" diff --git a/scaleway/gemini.sh b/scaleway/gemini.sh new file mode 100644 index 00000000..bfdcdec4 --- /dev/null +++ b/scaleway/gemini.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# shellcheck disable=SC2154 +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=scaleway/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/scaleway/lib/common.sh)" +fi + +log_info "Gemini CLI on Scaleway" +echo "" + +ensure_scaleway_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${SCALEWAY_SERVER_IP}" +install_base_packages "${SCALEWAY_SERVER_IP}" + +log_warn "Installing Gemini CLI..." +run_server "${SCALEWAY_SERVER_IP}" "npm install -g @google/gemini-cli" +log_info "Gemini CLI 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_warn "Setting up environment variables..." +inject_env_vars_ssh "${SCALEWAY_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "GEMINI_API_KEY=${OPENROUTER_API_KEY}" \ + "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ + "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + +echo "" +log_info "Scaleway instance setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${SCALEWAY_SERVER_ID}, IP: ${SCALEWAY_SERVER_IP})" +echo "" + +log_warn "Starting Gemini..." +sleep 1 +clear +interactive_session "${SCALEWAY_SERVER_IP}" "source ~/.zshrc && gemini" diff --git a/scaleway/goose.sh b/scaleway/goose.sh new file mode 100644 index 00000000..2e398454 --- /dev/null +++ b/scaleway/goose.sh @@ -0,0 +1,47 @@ +#!/bin/bash +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=scaleway/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/scaleway/lib/common.sh)" +fi + +log_info "Goose on Scaleway" +echo "" + +ensure_scaleway_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${SCALEWAY_SERVER_IP}" +install_base_packages "${SCALEWAY_SERVER_IP}" + +log_warn "Installing Goose..." +run_server "${SCALEWAY_SERVER_IP}" "CONFIGURE=false curl -fsSL https://github.com/block/goose/releases/latest/download/download_cli.sh | bash" +log_info "Goose 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_warn "Setting up environment variables..." +inject_env_vars_ssh "${SCALEWAY_SERVER_IP}" upload_file run_server \ + "GOOSE_PROVIDER=openrouter" \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" + +echo "" +log_info "Scaleway instance setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${SCALEWAY_SERVER_ID}, IP: ${SCALEWAY_SERVER_IP})" +echo "" + +log_warn "Starting Goose..." +sleep 1 +clear +interactive_session "${SCALEWAY_SERVER_IP}" "source ~/.zshrc && goose" diff --git a/scaleway/gptme.sh b/scaleway/gptme.sh new file mode 100644 index 00000000..51553a9b --- /dev/null +++ b/scaleway/gptme.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=scaleway/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/scaleway/lib/common.sh)" +fi + +log_info "gptme on Scaleway" +echo "" + +ensure_scaleway_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${SCALEWAY_SERVER_IP}" +install_base_packages "${SCALEWAY_SERVER_IP}" + +log_warn "Installing gptme..." +run_server "${SCALEWAY_SERVER_IP}" "pip install gptme 2>/dev/null || pip3 install gptme" +log_info "gptme 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" "gptme") || exit 1 + +log_warn "Setting up environment variables..." +inject_env_vars_ssh "${SCALEWAY_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" + +echo "" +log_info "Scaleway instance setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${SCALEWAY_SERVER_ID}, IP: ${SCALEWAY_SERVER_IP})" +echo "" + +log_warn "Starting gptme..." +sleep 1 +clear +interactive_session "${SCALEWAY_SERVER_IP}" "source ~/.zshrc && gptme -m openrouter/${MODEL_ID}" diff --git a/scaleway/interpreter.sh b/scaleway/interpreter.sh new file mode 100644 index 00000000..6d8383eb --- /dev/null +++ b/scaleway/interpreter.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=scaleway/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/scaleway/lib/common.sh)" +fi + +log_info "Open Interpreter on Scaleway" +echo "" + +ensure_scaleway_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${SCALEWAY_SERVER_IP}" +install_base_packages "${SCALEWAY_SERVER_IP}" + +log_warn "Installing Open Interpreter..." +run_server "${SCALEWAY_SERVER_IP}" "pip install open-interpreter 2>/dev/null || pip3 install open-interpreter" +log_info "Open Interpreter 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_warn "Setting up environment variables..." +inject_env_vars_ssh "${SCALEWAY_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 "Scaleway instance setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${SCALEWAY_SERVER_ID}, IP: ${SCALEWAY_SERVER_IP})" +echo "" + +log_warn "Starting Open Interpreter..." +sleep 1 +clear +interactive_session "${SCALEWAY_SERVER_IP}" "source ~/.zshrc && interpreter" diff --git a/scaleway/lib/common.sh b/scaleway/lib/common.sh new file mode 100644 index 00000000..92c36180 --- /dev/null +++ b/scaleway/lib/common.sh @@ -0,0 +1,460 @@ +#!/bin/bash +# Common bash functions for Scaleway 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, etc.) are now in shared/common.sh + +# ============================================================ +# Scaleway specific functions +# ============================================================ + +SCALEWAY_ZONE="${SCALEWAY_ZONE:-fr-par-1}" +readonly SCALEWAY_API_BASE="https://api.scaleway.com/instance/v1/zones/${SCALEWAY_ZONE}" +readonly SCALEWAY_ACCOUNT_API="https://api.scaleway.com/account/v3" +# SSH_OPTS is defined in shared/common.sh + +# Configurable timeout/delay constants +INSTANCE_STATUS_POLL_DELAY=${INSTANCE_STATUS_POLL_DELAY:-5} + +# Scaleway API wrapper (uses X-Auth-Token header instead of Bearer) +scaleway_api() { + local method="$1" + local url="$2" + local body="${3:-}" + local max_retries="${4:-3}" + + local attempt=1 + local interval=2 + local max_interval=30 + + while [[ "${attempt}" -le "${max_retries}" ]]; do + local args=( + -s + -w "\n%{http_code}" + -X "${method}" + -H "X-Auth-Token: ${SCW_SECRET_KEY}" + -H "Content-Type: application/json" + ) + + if [[ -n "${body}" ]]; then + args+=(-d "${body}") + fi + + local response + response=$(curl "${args[@]}" "${url}" 2>&1) + local curl_exit_code=$? + + local http_code + http_code=$(echo "${response}" | tail -1) + local response_body + response_body=$(echo "${response}" | head -n -1) + + if [[ ${curl_exit_code} -ne 0 ]]; then + if [[ "${attempt}" -ge "${max_retries}" ]]; then + log_error "Scaleway API network error after ${max_retries} attempts" + return 1 + fi + local next_interval=$((interval * 2)) + if [[ "${next_interval}" -gt "${max_interval}" ]]; then + next_interval="${max_interval}" + fi + local jitter + jitter=$(python3 -c "import random; print(int(${interval} * (0.8 + random.random() * 0.4)))" 2>/dev/null || echo "${interval}") + log_warn "Scaleway API network error (attempt ${attempt}/${max_retries}), retrying in ${jitter}s..." + sleep "${jitter}" + interval="${next_interval}" + attempt=$((attempt + 1)) + continue + fi + + if [[ "${http_code}" == "429" ]] || [[ "${http_code}" == "503" ]]; then + if [[ "${attempt}" -ge "${max_retries}" ]]; then + log_error "Scaleway API returned HTTP ${http_code} after ${max_retries} attempts" + echo "${response_body}" + return 1 + fi + local next_interval=$((interval * 2)) + if [[ "${next_interval}" -gt "${max_interval}" ]]; then + next_interval="${max_interval}" + fi + local jitter + jitter=$(python3 -c "import random; print(int(${interval} * (0.8 + random.random() * 0.4)))" 2>/dev/null || echo "${interval}") + log_warn "Scaleway API rate limited (HTTP ${http_code}, attempt ${attempt}/${max_retries}), retrying in ${jitter}s..." + sleep "${jitter}" + interval="${next_interval}" + attempt=$((attempt + 1)) + continue + fi + + echo "${response_body}" + return 0 + done + + log_error "Scaleway API retry logic exhausted" + return 1 +} + +# Convenience wrapper for instance API +scaleway_instance_api() { + local method="$1" + local endpoint="$2" + local body="${3:-}" + scaleway_api "$method" "${SCALEWAY_API_BASE}${endpoint}" "$body" +} + +test_scaleway_token() { + local response + response=$(scaleway_instance_api GET "/servers?per_page=1") + if echo "$response" | grep -q '"message"'; then + local error_msg + error_msg=$(echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('message','No details available'))" 2>/dev/null || echo "Unable to parse error") + log_error "API Error: $error_msg" + log_warn "Remediation steps:" + log_warn " 1. Verify secret key at: https://console.scaleway.com/iam/api-keys" + log_warn " 2. Ensure the key has appropriate permissions" + log_warn " 3. Check key hasn't been revoked" + return 1 + fi + return 0 +} + +ensure_scaleway_token() { + ensure_api_token_with_provider \ + "Scaleway" \ + "SCW_SECRET_KEY" \ + "$HOME/.config/spawn/scaleway.json" \ + "https://console.scaleway.com/iam/api-keys" \ + "test_scaleway_token" +} + +# Get Scaleway project ID (required for creating resources) +get_scaleway_project_id() { + if [[ -n "${SCW_DEFAULT_PROJECT_ID:-}" ]]; then + echo "${SCW_DEFAULT_PROJECT_ID}" + return 0 + fi + + # Try to get the default project from the API + local response + response=$(scaleway_api GET "${SCALEWAY_ACCOUNT_API}/projects?page_size=1&order_by=created_at_asc") + local project_id + project_id=$(echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('projects',[{}])[0].get('id',''))" 2>/dev/null) + + if [[ -z "$project_id" ]]; then + log_error "Failed to get Scaleway project ID" + log_warn "Set SCW_DEFAULT_PROJECT_ID environment variable or check API permissions" + return 1 + fi + + export SCW_DEFAULT_PROJECT_ID="$project_id" + echo "$project_id" +} + +# Get Ubuntu image ID for the current zone +get_ubuntu_image_id() { + log_warn "Looking up Ubuntu image for zone ${SCALEWAY_ZONE}..." + local response + response=$(scaleway_instance_api GET "/images?name=Ubuntu+24.04+Jammy+Jellyfish&arch=x86_64&per_page=50") + + local image_id + image_id=$(echo "$response" | python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +images = data.get('images', []) +# Find Ubuntu 24.04 or the latest Ubuntu +for img in images: + name = img.get('name', '').lower() + if '24.04' in name or 'noble' in name: + print(img['id']) + sys.exit(0) +# Fallback: try broader search +" 2>/dev/null) + + if [[ -z "$image_id" ]]; then + # Try a broader search + response=$(scaleway_instance_api GET "/images?name=Ubuntu&arch=x86_64&per_page=50") + image_id=$(echo "$response" | python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +images = data.get('images', []) +for img in images: + name = img.get('name', '').lower() + if '24.04' in name or 'noble' in name: + print(img['id']) + sys.exit(0) +for img in images: + name = img.get('name', '').lower() + if '22.04' in name or 'jammy' in name: + print(img['id']) + sys.exit(0) +if images: + print(images[0]['id']) +" 2>/dev/null) + fi + + if [[ -z "$image_id" ]]; then + log_error "Could not find Ubuntu image for zone ${SCALEWAY_ZONE}" + return 1 + fi + + echo "$image_id" +} + +# Check if SSH key is registered with Scaleway +scaleway_check_ssh_key() { + local fingerprint="$1" + local existing_keys + existing_keys=$(scaleway_api GET "${SCALEWAY_ACCOUNT_API}/ssh-keys?per_page=50") + echo "$existing_keys" | grep -q "$fingerprint" +} + +# Register SSH key with Scaleway +scaleway_register_ssh_key() { + local key_name="$1" + local pub_path="$2" + local pub_key + pub_key=$(cat "$pub_path") + + local project_id + project_id=$(get_scaleway_project_id) || return 1 + + local register_body + register_body=$(python3 -c " +import json +body = { + 'name': '$key_name', + 'public_key': '''$pub_key''', + 'project_id': '$project_id' +} +print(json.dumps(body)) +") + + local register_response + register_response=$(scaleway_api POST "${SCALEWAY_ACCOUNT_API}/ssh-keys" "$register_body") + + if echo "$register_response" | grep -q '"id"'; then + return 0 + else + local error_msg + error_msg=$(echo "$register_response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('message','Unknown error'))" 2>/dev/null || echo "$register_response") + log_error "API Error: $error_msg" + log_warn "Common causes:" + log_warn " - SSH key already registered with this name" + log_warn " - Invalid SSH key format" + log_warn " - API key lacks write permissions" + return 1 + fi +} + +ensure_ssh_key() { + ensure_ssh_key_with_provider scaleway_check_ssh_key scaleway_register_ssh_key "Scaleway" +} + +get_server_name() { + local server_name + server_name=$(get_resource_name "SCALEWAY_SERVER_NAME" "Enter server name: ") || return 1 + + if ! validate_server_name "$server_name"; then + return 1 + fi + + echo "$server_name" +} + +create_server() { + local name="$1" + local commercial_type="${SCALEWAY_TYPE:-DEV1-S}" + local zone="${SCALEWAY_ZONE:-fr-par-1}" + + log_warn "Creating Scaleway instance '$name' (type: $commercial_type, zone: $zone)..." + + local project_id + project_id=$(get_scaleway_project_id) || return 1 + + # Get Ubuntu image ID + local image_id + image_id=$(get_ubuntu_image_id) || return 1 + log_info "Using image: $image_id" + + local body + body=$(python3 -c " +import json +body = { + 'name': '$name', + 'commercial_type': '$commercial_type', + 'image': '$image_id', + 'project': '$project_id', + 'dynamic_ip_required': True +} +print(json.dumps(body)) +") + + local response + response=$(scaleway_instance_api POST "/servers" "$body") + + if echo "$response" | grep -q '"server"'; then + SCALEWAY_SERVER_ID=$(echo "$response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['server']['id'])") + export SCALEWAY_SERVER_ID + log_info "Instance created: ID=$SCALEWAY_SERVER_ID" + else + log_error "Failed to create Scaleway instance" + local error_msg + error_msg=$(echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('message','Unknown error'))" 2>/dev/null || echo "$response") + log_error "API Error: $error_msg" + log_warn "Common issues:" + log_warn " - Insufficient account balance" + log_warn " - Commercial type unavailable in zone (try different SCALEWAY_TYPE or SCALEWAY_ZONE)" + log_warn " - Instance limit reached" + log_warn "Remediation: Check https://console.scaleway.com/" + return 1 + fi + + # Power on the server (Scaleway requires explicit poweron after create) + log_warn "Powering on instance..." + local action_response + action_response=$(scaleway_instance_api POST "/servers/${SCALEWAY_SERVER_ID}/action" '{"action":"poweron"}') + + if echo "$action_response" | grep -q '"task"'; then + log_info "Power on initiated" + else + log_warn "Power on may have failed, checking status..." + fi + + # Wait for instance to get an IP and become running + log_warn "Waiting for instance to become active..." + local max_attempts=60 + local attempt=1 + while [[ "$attempt" -le "$max_attempts" ]]; do + local status_response + status_response=$(scaleway_instance_api GET "/servers/$SCALEWAY_SERVER_ID") + local state + state=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['server']['state'])") + + if [[ "$state" == "running" ]]; then + SCALEWAY_SERVER_IP=$(echo "$status_response" | python3 -c " +import json, sys +server = json.loads(sys.stdin.read())['server'] +ip = server.get('public_ip', {}) +if ip: + print(ip.get('address', '')) +else: + # Check public_ips array + ips = server.get('public_ips', []) + for pip in ips: + if pip.get('address'): + print(pip['address']) + sys.exit(0) + print('') +") + if [[ -n "$SCALEWAY_SERVER_IP" ]]; then + export SCALEWAY_SERVER_IP + log_info "Instance active: IP=$SCALEWAY_SERVER_IP" + return 0 + fi + fi + + log_warn "Instance state: $state ($attempt/$max_attempts)" + sleep "${INSTANCE_STATUS_POLL_DELAY}" + attempt=$((attempt + 1)) + done + + log_error "Instance did not become active in time" + return 1 +} + +verify_server_connectivity() { + local ip="$1" + local max_attempts=${2:-30} + # shellcheck disable=SC2154 + generic_ssh_wait "root" "$ip" "$SSH_OPTS -o ConnectTimeout=5" "echo ok" "SSH connectivity" "$max_attempts" 5 +} + +wait_for_server_ready() { + local ip="$1" + local max_attempts=${2:-60} + # Scaleway doesn't use cloud-init by default, so we wait for basic tools + log_warn "Waiting for server to be ready..." + generic_ssh_wait "root" "$ip" "$SSH_OPTS -o ConnectTimeout=5" "command -v curl" "server readiness" "$max_attempts" 5 +} + +install_base_packages() { + local ip="$1" + log_warn "Installing base packages..." + run_server "$ip" "apt-get update -qq && apt-get install -y -qq curl unzip git zsh >/dev/null 2>&1" + log_warn "Installing Bun..." + run_server "$ip" "curl -fsSL https://bun.sh/install | bash" + log_warn "Installing Node.js..." + run_server "$ip" "curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y -qq nodejs >/dev/null 2>&1" + # Set up PATH in shell configs + run_server "$ip" "printf 'export PATH=\"\${HOME}/.claude/local/bin:\${HOME}/.bun/bin:\${PATH}\"\n' >> /root/.bashrc" + run_server "$ip" "printf 'export PATH=\"\${HOME}/.claude/local/bin:\${HOME}/.bun/bin:\${PATH}\"\n' >> /root/.zshrc" + log_info "Base packages installed" +} + +run_server() { + local ip="$1"; local cmd="$2" + # shellcheck disable=SC2086 + ssh $SSH_OPTS "root@$ip" "$cmd" +} + +upload_file() { + local ip="$1"; local local_path="$2"; local remote_path="$3" + # shellcheck disable=SC2086 + scp $SSH_OPTS "$local_path" "root@$ip:$remote_path" +} + +interactive_session() { + local ip="$1"; local cmd="$2" + # shellcheck disable=SC2086 + ssh -t $SSH_OPTS "root@$ip" "$cmd" +} + +destroy_server() { + local server_id="$1" + log_warn "Destroying instance $server_id..." + + # Scaleway requires powering off before deleting + scaleway_instance_api POST "/servers/$server_id/action" '{"action":"poweroff"}' >/dev/null 2>&1 || true + sleep 5 + + # Delete the server (terminate also deletes attached volumes/IPs) + scaleway_instance_api POST "/servers/$server_id/action" '{"action":"terminate"}' >/dev/null 2>&1 || true + log_info "Instance $server_id destroyed" +} + +list_servers() { + local response + response=$(scaleway_instance_api GET "/servers") + python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +servers = data.get('servers', []) +if not servers: + print('No instances found') + sys.exit(0) +print(f\"{'NAME':<25} {'ID':<40} {'STATE':<12} {'IP':<16} {'TYPE':<10}\") +print('-' * 103) +for s in servers: + name = s['name'] + sid = s['id'] + state = s['state'] + ip_data = s.get('public_ip') or {} + ip = ip_data.get('address', 'N/A') if ip_data else 'N/A' + stype = s.get('commercial_type', 'N/A') + print(f'{name:<25} {sid:<40} {state:<12} {ip:<16} {stype:<10}') +" <<< "$response" +} diff --git a/scaleway/nanoclaw.sh b/scaleway/nanoclaw.sh new file mode 100644 index 00000000..bf67fb02 --- /dev/null +++ b/scaleway/nanoclaw.sh @@ -0,0 +1,59 @@ +#!/bin/bash +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=scaleway/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/scaleway/lib/common.sh)" +fi + +log_info "NanoClaw on Scaleway" +echo "" + +ensure_scaleway_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${SCALEWAY_SERVER_IP}" +install_base_packages "${SCALEWAY_SERVER_IP}" + +log_warn "Installing tsx..." +run_server "${SCALEWAY_SERVER_IP}" "source ~/.bashrc && bun install -g tsx" +log_warn "Cloning and building nanoclaw..." +run_server "${SCALEWAY_SERVER_IP}" "git clone https://github.com/gavrielc/nanoclaw.git ~/nanoclaw && cd ~/nanoclaw && npm install && npm run build" +log_info "NanoClaw 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_warn "Setting up environment variables..." +inject_env_vars_ssh "${SCALEWAY_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_BASE_URL=https://openrouter.ai/api" + +log_warn "Configuring nanoclaw..." +DOTENV_TEMP=$(mktemp) +trap 'rm -f "${DOTENV_TEMP}"' EXIT +chmod 600 "${DOTENV_TEMP}" +cat > "${DOTENV_TEMP}" << EOF +ANTHROPIC_API_KEY=${OPENROUTER_API_KEY} +EOF +upload_file "${SCALEWAY_SERVER_IP}" "${DOTENV_TEMP}" "/root/nanoclaw/.env" + +echo "" +log_info "Scaleway instance setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${SCALEWAY_SERVER_ID}, IP: ${SCALEWAY_SERVER_IP})" +echo "" + +log_warn "Starting nanoclaw..." +log_warn "You will need to scan a WhatsApp QR code to authenticate." +echo "" +interactive_session "${SCALEWAY_SERVER_IP}" "cd ~/nanoclaw && source ~/.zshrc && npm run dev" diff --git a/scaleway/openclaw.sh b/scaleway/openclaw.sh new file mode 100644 index 00000000..45766556 --- /dev/null +++ b/scaleway/openclaw.sh @@ -0,0 +1,54 @@ +#!/bin/bash +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=scaleway/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/scaleway/lib/common.sh)" +fi + +log_info "OpenClaw on Scaleway" +echo "" + +ensure_scaleway_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${SCALEWAY_SERVER_IP}" +install_base_packages "${SCALEWAY_SERVER_IP}" + +log_warn "Installing openclaw..." +run_server "${SCALEWAY_SERVER_IP}" "source ~/.bashrc && bun install -g openclaw" +log_info "OpenClaw 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" "Openclaw") || exit 1 + +log_warn "Setting up environment variables..." +inject_env_vars_ssh "${SCALEWAY_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_BASE_URL=https://openrouter.ai/api" + +setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" \ + "upload_file ${SCALEWAY_SERVER_IP}" \ + "run_server ${SCALEWAY_SERVER_IP}" + +echo "" +log_info "Scaleway instance setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${SCALEWAY_SERVER_ID}, IP: ${SCALEWAY_SERVER_IP})" +echo "" + +log_warn "Starting openclaw..." +run_server "${SCALEWAY_SERVER_IP}" "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &" +sleep 2 +interactive_session "${SCALEWAY_SERVER_IP}" "source ~/.zshrc && openclaw tui" diff --git a/scaleway/opencode.sh b/scaleway/opencode.sh new file mode 100644 index 00000000..27940d15 --- /dev/null +++ b/scaleway/opencode.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=scaleway/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/scaleway/lib/common.sh)" +fi + +log_info "OpenCode on Scaleway" +echo "" + +ensure_scaleway_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +verify_server_connectivity "${SCALEWAY_SERVER_IP}" +install_base_packages "${SCALEWAY_SERVER_IP}" + +log_warn "Installing OpenCode..." +run_server "${SCALEWAY_SERVER_IP}" "curl -fsSL https://raw.githubusercontent.com/opencode-ai/opencode/refs/heads/main/install | bash" +log_info "OpenCode 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_warn "Setting up environment variables..." +inject_env_vars_ssh "${SCALEWAY_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" + +echo "" +log_info "Scaleway instance setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${SCALEWAY_SERVER_ID}, IP: ${SCALEWAY_SERVER_IP})" +echo "" + +log_warn "Starting OpenCode..." +sleep 1 +clear +interactive_session "${SCALEWAY_SERVER_IP}" "source ~/.zshrc && opencode"