mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
feat: prioritize clouds with CLI installed + hcloud CLI integration (#1375)
* fix: auto-run gcloud auth login on expired GCP tokens Instead of telling users to run `gcloud auth login` manually, just run it automatically when auth check fails or instance creation hits a reauthentication error, then retry. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: prioritize clouds with CLI installed + hcloud CLI integration When selecting a cloud provider, clouds are now sorted in 3 tiers: 1. Credentials detected (env vars set) — top priority 2. CLI installed (e.g., gcloud, hcloud, aws) — middle priority 3. Neither — default order Also adds hcloud CLI-first support for Hetzner operations (server create/delete/list, SSH key management, auth) with automatic fallback to the existing REST API when hcloud is not available. Closes #1370 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: rename aws-lightsail to aws across the project Simplifies the cloud key from "aws-lightsail" to "aws" — AWS should have a single entry regardless of the underlying service used. Renames the directory, updates manifest.json matrix keys, CLI map, test fixtures, README, and all agent scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: lab <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d452fdea37
commit
c4eccbd72f
26 changed files with 296 additions and 155 deletions
|
|
@ -48,7 +48,7 @@ We are currently shipping with **9 curated clouds** (sorted by price):
|
|||
3. **hetzner** — ~€3.29/mo (CX22)
|
||||
4. **ovh** — ~€3.50/mo (d2-2)
|
||||
5. **fly** — free tier (3 shared-cpu VMs)
|
||||
6. **aws-lightsail** — $3.50/mo (nano)
|
||||
6. **aws** — $3.50/mo (nano)
|
||||
7. **daytona** — pay-per-second sandboxes
|
||||
8. **digitalocean** — $4/mo (Basic droplet)
|
||||
9. **gcp** — $7.11/mo (e2-micro)
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ If an agent fails to install or launch on a cloud:
|
|||
|
||||
## Matrix
|
||||
|
||||
| | [Local Machine](local/) | [Oracle Cloud Infrastructure](oracle/) | [Hetzner Cloud](hetzner/) | [OVHcloud](ovh/) | [Fly.io](fly/) | [AWS Lightsail](aws-lightsail/) | [Daytona](daytona/) | [DigitalOcean](digitalocean/) | [GCP Compute Engine](gcp/) | [Sprite](sprite/) |
|
||||
| | [Local Machine](local/) | [Oracle Cloud Infrastructure](oracle/) | [Hetzner Cloud](hetzner/) | [OVHcloud](ovh/) | [Fly.io](fly/) | [AWS Lightsail](aws/) | [Daytona](daytona/) | [DigitalOcean](digitalocean/) | [GCP Compute Engine](gcp/) | [Sprite](sprite/) |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| [**Claude Code**](https://claude.ai) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| [**OpenClaw**](https://github.com/OpenRouterTeam/openclaw) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
# AWS Lightsail
|
||||
|
||||
AWS Lightsail instances via AWS CLI. [AWS Lightsail](https://aws.amazon.com/lightsail/)
|
||||
|
||||
> Uses 'ubuntu' user instead of 'root'. Requires AWS CLI installed and configured.
|
||||
|
||||
## Agents
|
||||
|
||||
#### Claude Code
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws-lightsail/claude.sh)
|
||||
```
|
||||
|
||||
#### OpenClaw
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws-lightsail/openclaw.sh)
|
||||
```
|
||||
|
||||
#### NanoClaw
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws-lightsail/nanoclaw.sh)
|
||||
```
|
||||
|
||||
#### Aider
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws-lightsail/aider.sh)
|
||||
```
|
||||
|
||||
#### Goose
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws-lightsail/goose.sh)
|
||||
```
|
||||
|
||||
#### Codex CLI
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws-lightsail/codex.sh)
|
||||
```
|
||||
|
||||
#### Open Interpreter
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws-lightsail/interpreter.sh)
|
||||
```
|
||||
|
||||
#### Gemini CLI
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws-lightsail/gemini.sh)
|
||||
```
|
||||
|
||||
#### Amazon Q CLI
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws-lightsail/amazonq.sh)
|
||||
```
|
||||
|
||||
#### Cline
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws-lightsail/cline.sh)
|
||||
```
|
||||
|
||||
#### gptme
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws-lightsail/gptme.sh)
|
||||
```
|
||||
|
||||
#### OpenCode
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws-lightsail/opencode.sh)
|
||||
```
|
||||
|
||||
#### Plandex
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws-lightsail/plandex.sh)
|
||||
```
|
||||
|
||||
## Non-Interactive Mode
|
||||
|
||||
```bash
|
||||
LIGHTSAIL_SERVER_NAME=dev-mk1 \
|
||||
OPENROUTER_API_KEY=sk-or-v1-xxxxx \
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws-lightsail/claude.sh)
|
||||
```
|
||||
93
aws/README.md
Normal file
93
aws/README.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# AWS Lightsail
|
||||
|
||||
AWS Lightsail instances via AWS CLI. [AWS Lightsail](https://aws.amazon.com/lightsail/)
|
||||
|
||||
> Uses 'ubuntu' user instead of 'root'. Requires AWS CLI installed and configured.
|
||||
|
||||
## Agents
|
||||
|
||||
#### Claude Code
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/claude.sh)
|
||||
```
|
||||
|
||||
#### OpenClaw
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/openclaw.sh)
|
||||
```
|
||||
|
||||
#### NanoClaw
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/nanoclaw.sh)
|
||||
```
|
||||
|
||||
#### Aider
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/aider.sh)
|
||||
```
|
||||
|
||||
#### Goose
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/goose.sh)
|
||||
```
|
||||
|
||||
#### Codex CLI
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/codex.sh)
|
||||
```
|
||||
|
||||
#### Open Interpreter
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/interpreter.sh)
|
||||
```
|
||||
|
||||
#### Gemini CLI
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/gemini.sh)
|
||||
```
|
||||
|
||||
#### Amazon Q CLI
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/amazonq.sh)
|
||||
```
|
||||
|
||||
#### Cline
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/cline.sh)
|
||||
```
|
||||
|
||||
#### gptme
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/gptme.sh)
|
||||
```
|
||||
|
||||
#### OpenCode
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/opencode.sh)
|
||||
```
|
||||
|
||||
#### Plandex
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/plandex.sh)
|
||||
```
|
||||
|
||||
## Non-Interactive Mode
|
||||
|
||||
```bash
|
||||
LIGHTSAIL_SERVER_NAME=dev-mk1 \
|
||||
OPENROUTER_API_KEY=sk-or-v1-xxxxx \
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/claude.sh)
|
||||
```
|
||||
|
|
@ -3,11 +3,11 @@ set -eo pipefail
|
|||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=aws-lightsail/lib/common.sh
|
||||
# shellcheck source=aws/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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Aider on AWS Lightsail"
|
||||
|
|
@ -3,11 +3,11 @@
|
|||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=aws-lightsail/lib/common.sh
|
||||
# shellcheck source=aws/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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Amazon Q on AWS Lightsail"
|
||||
|
|
@ -3,11 +3,11 @@ set -eo pipefail
|
|||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=aws-lightsail/lib/common.sh
|
||||
# shellcheck source=aws/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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Claude Code on AWS Lightsail"
|
||||
|
|
@ -3,11 +3,11 @@
|
|||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=aws-lightsail/lib/common.sh
|
||||
# shellcheck source=aws/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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Cline on AWS Lightsail"
|
||||
|
|
@ -2,11 +2,11 @@
|
|||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=aws-lightsail/lib/common.sh
|
||||
# shellcheck source=aws/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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Codex CLI on AWS Lightsail"
|
||||
|
|
@ -3,11 +3,11 @@
|
|||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=aws-lightsail/lib/common.sh
|
||||
# shellcheck source=aws/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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Continue on AWS Lightsail"
|
||||
|
|
@ -3,11 +3,11 @@
|
|||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=aws-lightsail/lib/common.sh
|
||||
# shellcheck source=aws/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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Gemini CLI on AWS Lightsail"
|
||||
|
|
@ -3,11 +3,11 @@ set -eo pipefail
|
|||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=aws-lightsail/lib/common.sh
|
||||
# shellcheck source=aws/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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Goose on AWS Lightsail"
|
||||
|
|
@ -6,7 +6,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
|||
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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "gptme on AWS Lightsail"
|
||||
|
|
@ -2,11 +2,11 @@
|
|||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=aws-lightsail/lib/common.sh
|
||||
# shellcheck source=aws/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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Open Interpreter on AWS Lightsail"
|
||||
|
|
@ -3,11 +3,11 @@
|
|||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=aws-lightsail/lib/common.sh
|
||||
# shellcheck source=aws/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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Kilo Code on AWS Lightsail"
|
||||
|
|
@ -170,7 +170,7 @@ create_server() {
|
|||
|
||||
_wait_for_lightsail_instance "${name}"
|
||||
|
||||
save_vm_connection "${LIGHTSAIL_SERVER_IP}" "ubuntu" "" "$name" "aws-lightsail"
|
||||
save_vm_connection "${LIGHTSAIL_SERVER_IP}" "ubuntu" "" "$name" "aws"
|
||||
}
|
||||
|
||||
# Lightsail uses 'ubuntu' user, not 'root'
|
||||
|
|
@ -3,11 +3,11 @@ set -eo pipefail
|
|||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=aws-lightsail/lib/common.sh
|
||||
# shellcheck source=aws/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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "NanoClaw on AWS Lightsail"
|
||||
|
|
@ -3,11 +3,11 @@ set -eo pipefail
|
|||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=aws-lightsail/lib/common.sh
|
||||
# shellcheck source=aws/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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "OpenClaw on AWS Lightsail"
|
||||
|
|
@ -2,11 +2,11 @@
|
|||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=aws-lightsail/lib/common.sh
|
||||
# shellcheck source=aws/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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "OpenCode on AWS Lightsail"
|
||||
|
|
@ -3,11 +3,11 @@ set -eo pipefail
|
|||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=aws-lightsail/lib/common.sh
|
||||
# shellcheck source=aws/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/aws-lightsail/lib/common.sh)"
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Plandex on AWS Lightsail"
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ describe("SAFE_PROVIDER_RE - provider name validation", () => {
|
|||
});
|
||||
|
||||
it("should accept name with hyphens", () => {
|
||||
expect(SAFE_PROVIDER_RE.test("aws-lightsail")).toBe(true);
|
||||
expect(SAFE_PROVIDER_RE.test("aws")).toBe(true);
|
||||
});
|
||||
|
||||
it("should accept name with underscores", () => {
|
||||
|
|
|
|||
|
|
@ -330,18 +330,40 @@ function validateImplementation(manifest: Manifest, cloud: string, agent: string
|
|||
|
||||
// ── Interactive ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Sort clouds by credential availability and build hint overrides for the picker */
|
||||
/** Map of cloud keys to their CLI tool names */
|
||||
const CLOUD_CLI_MAP: Record<string, string> = {
|
||||
gcp: "gcloud",
|
||||
aws: "aws",
|
||||
oracle: "oci",
|
||||
fly: "flyctl",
|
||||
sprite: "sprite",
|
||||
hetzner: "hcloud",
|
||||
digitalocean: "doctl",
|
||||
};
|
||||
|
||||
/** Check if the relevant CLI tool for a cloud provider is installed */
|
||||
export function hasCloudCli(cloud: string): boolean {
|
||||
const cli = CLOUD_CLI_MAP[cloud];
|
||||
if (!cli) return false;
|
||||
return Bun.which(cli) !== null;
|
||||
}
|
||||
|
||||
/** Sort clouds by credential/CLI availability and build hint overrides for the picker.
|
||||
* Three tiers: credentials set > CLI installed > neither. */
|
||||
export function prioritizeCloudsByCredentials(
|
||||
clouds: string[],
|
||||
manifest: Manifest
|
||||
): { sortedClouds: string[]; hintOverrides: Record<string, string>; credCount: number } {
|
||||
): { sortedClouds: string[]; hintOverrides: Record<string, string>; credCount: number; cliCount: number } {
|
||||
const withCreds: string[] = [];
|
||||
const withoutCreds: string[] = [];
|
||||
const withCli: string[] = [];
|
||||
const rest: string[] = [];
|
||||
for (const c of clouds) {
|
||||
if (hasCloudCredentials(manifest.clouds[c].auth)) {
|
||||
withCreds.push(c);
|
||||
} else if (hasCloudCli(c)) {
|
||||
withCli.push(c);
|
||||
} else {
|
||||
withoutCreds.push(c);
|
||||
rest.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -349,8 +371,11 @@ export function prioritizeCloudsByCredentials(
|
|||
for (const c of withCreds) {
|
||||
hintOverrides[c] = `credentials detected -- ${manifest.clouds[c].description}`;
|
||||
}
|
||||
for (const c of withCli) {
|
||||
hintOverrides[c] = `CLI installed -- ${manifest.clouds[c].description}`;
|
||||
}
|
||||
|
||||
return { sortedClouds: [...withCreds, ...withoutCreds], hintOverrides, credCount: withCreds.length };
|
||||
return { sortedClouds: [...withCreds, ...withCli, ...rest], hintOverrides, credCount: withCreds.length, cliCount: withCli.length };
|
||||
}
|
||||
|
||||
/** Build hint overrides for the agent picker showing cloud count and credential readiness */
|
||||
|
|
@ -399,10 +424,13 @@ function getAndValidateCloudChoices(
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
const { sortedClouds, hintOverrides, credCount } = prioritizeCloudsByCredentials(clouds, manifest);
|
||||
const { sortedClouds, hintOverrides, credCount, cliCount } = prioritizeCloudsByCredentials(clouds, manifest);
|
||||
if (credCount > 0) {
|
||||
p.log.info(`${credCount} cloud${credCount > 1 ? "s" : ""} with credentials detected (shown first)`);
|
||||
}
|
||||
if (cliCount > 0) {
|
||||
p.log.info(`${cliCount} cloud${cliCount > 1 ? "s" : ""} with CLI installed`);
|
||||
}
|
||||
|
||||
return { clouds: sortedClouds, hintOverrides, credCount };
|
||||
}
|
||||
|
|
@ -1518,7 +1546,7 @@ function buildDeleteScript(cloud: string, connection: VMConnection): string {
|
|||
const project = connection.metadata?.project || "";
|
||||
return `${sourceLib}\nensure_gcloud\nexport GCP_ZONE="${zone}"\nexport GCP_PROJECT="${project}"\ndestroy_server "${id}"`;
|
||||
}
|
||||
case "aws-lightsail":
|
||||
case "aws":
|
||||
return `${sourceLib}\nensure_aws_cli\ndestroy_server "${id}"`;
|
||||
case "oracle":
|
||||
return `${sourceLib}\nensure_oci_cli\ndestroy_server "${id}"`;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,16 @@ fi
|
|||
# Hetzner Cloud specific functions
|
||||
# ============================================================
|
||||
|
||||
# Detect hcloud CLI availability
|
||||
_hcloud_cli_available() {
|
||||
command -v hcloud &>/dev/null
|
||||
}
|
||||
|
||||
# Check if hcloud CLI has an active context (authenticated)
|
||||
_hcloud_cli_authenticated() {
|
||||
_hcloud_cli_available && hcloud context active &>/dev/null 2>&1
|
||||
}
|
||||
|
||||
readonly HETZNER_API_BASE="https://api.hetzner.cloud/v1"
|
||||
SPAWN_DASHBOARD_URL="https://console.hetzner.cloud/"
|
||||
# SSH_OPTS is now defined in shared/common.sh
|
||||
|
|
@ -48,8 +58,27 @@ test_hcloud_token() {
|
|||
return 0
|
||||
}
|
||||
|
||||
# Ensure HCLOUD_TOKEN is available (env var → config file → prompt+save)
|
||||
# Ensure Hetzner auth is available (hcloud CLI → env var → config file → prompt+save)
|
||||
ensure_hcloud_token() {
|
||||
# If hcloud CLI is available, prefer it for authentication
|
||||
if _hcloud_cli_available; then
|
||||
if _hcloud_cli_authenticated; then
|
||||
log_info "Using hcloud CLI (context: $(hcloud context active))"
|
||||
# Export token from CLI context for API fallback compatibility
|
||||
if [[ -z "${HCLOUD_TOKEN:-}" ]]; then
|
||||
HCLOUD_TOKEN=$(hcloud context active 2>/dev/null | xargs -I{} grep -A1 "{}" ~/.config/hcloud/cli.toml 2>/dev/null | grep token | sed 's/.*= *"\(.*\)"/\1/' || true)
|
||||
if [[ -n "${HCLOUD_TOKEN:-}" ]]; then
|
||||
export HCLOUD_TOKEN
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
else
|
||||
log_info "hcloud CLI found but no active context"
|
||||
log_info "Run: hcloud context create myproject"
|
||||
log_info "Falling back to API token authentication..."
|
||||
fi
|
||||
fi
|
||||
|
||||
ensure_api_token_with_provider \
|
||||
"Hetzner Cloud" \
|
||||
"HCLOUD_TOKEN" \
|
||||
|
|
@ -60,6 +89,16 @@ ensure_hcloud_token() {
|
|||
|
||||
# Check if SSH key is registered with Hetzner
|
||||
hetzner_check_ssh_key() {
|
||||
if _hcloud_cli_available && [[ -n "${HCLOUD_TOKEN:-}" || "$(_hcloud_cli_authenticated && echo y)" == "y" ]]; then
|
||||
# Use hcloud CLI to check by fingerprint
|
||||
local fingerprint="$1"
|
||||
local result
|
||||
result=$(hcloud ssh-key list -o json 2>/dev/null || true)
|
||||
if [[ -n "$result" ]] && printf '%s' "$result" | jq -e --arg fp "$fingerprint" '.[] | select(.fingerprint == $fp)' &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
check_ssh_key_by_fingerprint hetzner_api "/ssh_keys" "$1"
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +106,16 @@ hetzner_check_ssh_key() {
|
|||
hetzner_register_ssh_key() {
|
||||
local key_name="$1"
|
||||
local pub_path="$2"
|
||||
|
||||
if _hcloud_cli_available && [[ -n "${HCLOUD_TOKEN:-}" || "$(_hcloud_cli_authenticated && echo y)" == "y" ]]; then
|
||||
# Use hcloud CLI to register
|
||||
if hcloud ssh-key create --name "$key_name" --public-key-from-file "$pub_path" &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
# Fall through to API on CLI failure
|
||||
log_warn "hcloud ssh-key create failed, falling back to API"
|
||||
fi
|
||||
|
||||
local pub_key
|
||||
pub_key=$(cat "$pub_path")
|
||||
local json_pub_key json_name
|
||||
|
|
@ -362,7 +411,7 @@ _hetzner_resolve_server_type() {
|
|||
printf '%s' "$validated_type"
|
||||
}
|
||||
|
||||
# Create a Hetzner server with cloud-init
|
||||
# Create a Hetzner server with cloud-init (hcloud CLI preferred, API fallback)
|
||||
create_server() {
|
||||
local name="$1"
|
||||
|
||||
|
|
@ -384,6 +433,38 @@ create_server() {
|
|||
|
||||
log_step "Creating Hetzner server '$name' (type: $server_type, location: $location)..."
|
||||
|
||||
# Try hcloud CLI first
|
||||
if _hcloud_cli_available && [[ -n "${HCLOUD_TOKEN:-}" || "$(_hcloud_cli_authenticated && echo y)" == "y" ]]; then
|
||||
local userdata_file
|
||||
userdata_file=$(mktemp)
|
||||
get_cloud_init_userdata > "$userdata_file"
|
||||
|
||||
local cli_response
|
||||
if cli_response=$(hcloud server create \
|
||||
--name "$name" \
|
||||
--type "$server_type" \
|
||||
--location "$location" \
|
||||
--image "$image" \
|
||||
--ssh-key all \
|
||||
--user-data-from-file "$userdata_file" \
|
||||
-o json 2>&1); then
|
||||
rm -f "$userdata_file"
|
||||
|
||||
HETZNER_SERVER_ID=$(printf '%s' "$cli_response" | jq -r '.server.id')
|
||||
HETZNER_SERVER_IP=$(printf '%s' "$cli_response" | jq -r '.server.public_net.ipv4.ip')
|
||||
|
||||
if [[ -n "$HETZNER_SERVER_ID" && "$HETZNER_SERVER_ID" != "null" && -n "$HETZNER_SERVER_IP" && "$HETZNER_SERVER_IP" != "null" ]]; then
|
||||
export HETZNER_SERVER_ID HETZNER_SERVER_IP
|
||||
log_info "Server created via hcloud CLI: ID=$HETZNER_SERVER_ID, IP=$HETZNER_SERVER_IP"
|
||||
save_vm_connection "${HETZNER_SERVER_IP}" "root" "${HETZNER_SERVER_ID}" "$name" "hetzner"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
rm -f "$userdata_file"
|
||||
log_warn "hcloud CLI server create failed, falling back to API"
|
||||
fi
|
||||
|
||||
# Fallback: REST API
|
||||
# Get all SSH key IDs
|
||||
local ssh_keys_response
|
||||
ssh_keys_response=$(hetzner_api GET "/ssh_keys")
|
||||
|
|
@ -426,11 +507,21 @@ run_server() { ssh_run_server "$@"; }
|
|||
upload_file() { ssh_upload_file "$@"; }
|
||||
interactive_session() { ssh_interactive_session "$@"; }
|
||||
|
||||
# Destroy a Hetzner server
|
||||
# Destroy a Hetzner server (hcloud CLI preferred, API fallback)
|
||||
destroy_server() {
|
||||
local server_id="$1"
|
||||
|
||||
log_step "Destroying server $server_id..."
|
||||
|
||||
# Try hcloud CLI first
|
||||
if _hcloud_cli_available && [[ -n "${HCLOUD_TOKEN:-}" || "$(_hcloud_cli_authenticated && echo y)" == "y" ]]; then
|
||||
if hcloud server delete "$server_id" 2>/dev/null; then
|
||||
log_info "Server $server_id destroyed via hcloud CLI"
|
||||
return 0
|
||||
fi
|
||||
log_warn "hcloud CLI delete failed, falling back to API"
|
||||
fi
|
||||
|
||||
local response
|
||||
response=$(hetzner_api DELETE "/servers/$server_id")
|
||||
|
||||
|
|
@ -446,8 +537,30 @@ destroy_server() {
|
|||
log_info "Server $server_id destroyed"
|
||||
}
|
||||
|
||||
# List all Hetzner servers
|
||||
# List all Hetzner servers (hcloud CLI preferred, API fallback)
|
||||
list_servers() {
|
||||
# Try hcloud CLI first
|
||||
if _hcloud_cli_available && [[ -n "${HCLOUD_TOKEN:-}" || "$(_hcloud_cli_authenticated && echo y)" == "y" ]]; then
|
||||
local cli_response
|
||||
if cli_response=$(hcloud server list -o json 2>/dev/null); then
|
||||
local count
|
||||
count=$(printf '%s' "$cli_response" | jq 'length')
|
||||
if [[ "$count" -eq 0 ]]; then
|
||||
printf 'No servers found\n'
|
||||
return 0
|
||||
fi
|
||||
printf '%-25s %-12s %-12s %-16s %-10s\n' "NAME" "ID" "STATUS" "IP" "TYPE"
|
||||
printf '%s\n' "---------------------------------------------------------------------------"
|
||||
printf '%s' "$cli_response" | jq -r \
|
||||
'.[] | "\(.name)|\(.id)|\(.status)|\(.public_net.ipv4.ip // "N/A")|\(.server_type.name)"' \
|
||||
| while IFS='|' read -r name sid status ip stype; do
|
||||
printf '%-25s %-12s %-12s %-16s %-10s\n' "$name" "$sid" "$status" "$ip" "$stype"
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback: REST API
|
||||
local response
|
||||
response=$(hetzner_api GET "/servers")
|
||||
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@
|
|||
},
|
||||
"notes": "Uses Machines API for provisioning and flyctl SSH for exec. Docker-based, pay-per-second pricing. Requires flyctl CLI."
|
||||
},
|
||||
"aws-lightsail": {
|
||||
"aws": {
|
||||
"name": "AWS Lightsail",
|
||||
"description": "AWS Lightsail instances via AWS CLI",
|
||||
"url": "https://aws.amazon.com/lightsail/",
|
||||
|
|
@ -412,25 +412,25 @@
|
|||
"sprite/interpreter": "implemented",
|
||||
"hetzner/interpreter": "implemented",
|
||||
"digitalocean/interpreter": "implemented",
|
||||
"aws-lightsail/claude": "implemented",
|
||||
"aws-lightsail/openclaw": "implemented",
|
||||
"aws-lightsail/nanoclaw": "implemented",
|
||||
"aws-lightsail/aider": "implemented",
|
||||
"aws-lightsail/goose": "implemented",
|
||||
"aws-lightsail/codex": "implemented",
|
||||
"aws-lightsail/interpreter": "implemented",
|
||||
"aws/claude": "implemented",
|
||||
"aws/openclaw": "implemented",
|
||||
"aws/nanoclaw": "implemented",
|
||||
"aws/aider": "implemented",
|
||||
"aws/goose": "implemented",
|
||||
"aws/codex": "implemented",
|
||||
"aws/interpreter": "implemented",
|
||||
"sprite/gemini": "implemented",
|
||||
"hetzner/gemini": "implemented",
|
||||
"digitalocean/gemini": "implemented",
|
||||
"aws-lightsail/gemini": "implemented",
|
||||
"aws/gemini": "implemented",
|
||||
"sprite/amazonq": "implemented",
|
||||
"hetzner/amazonq": "implemented",
|
||||
"digitalocean/amazonq": "implemented",
|
||||
"aws-lightsail/amazonq": "implemented",
|
||||
"aws/amazonq": "implemented",
|
||||
"sprite/cline": "implemented",
|
||||
"hetzner/cline": "implemented",
|
||||
"digitalocean/cline": "implemented",
|
||||
"aws-lightsail/cline": "implemented",
|
||||
"aws/cline": "implemented",
|
||||
"gcp/claude": "implemented",
|
||||
"gcp/openclaw": "implemented",
|
||||
"gcp/nanoclaw": "implemented",
|
||||
|
|
@ -444,7 +444,7 @@
|
|||
"sprite/gptme": "implemented",
|
||||
"hetzner/gptme": "implemented",
|
||||
"digitalocean/gptme": "implemented",
|
||||
"aws-lightsail/gptme": "implemented",
|
||||
"aws/gptme": "implemented",
|
||||
"gcp/gptme": "implemented",
|
||||
"fly/claude": "implemented",
|
||||
"fly/aider": "implemented",
|
||||
|
|
@ -460,7 +460,7 @@
|
|||
"sprite/opencode": "implemented",
|
||||
"hetzner/opencode": "implemented",
|
||||
"digitalocean/opencode": "implemented",
|
||||
"aws-lightsail/opencode": "implemented",
|
||||
"aws/opencode": "implemented",
|
||||
"gcp/opencode": "implemented",
|
||||
"fly/opencode": "implemented",
|
||||
"daytona/claude": "implemented",
|
||||
|
|
@ -478,7 +478,7 @@
|
|||
"sprite/plandex": "implemented",
|
||||
"hetzner/plandex": "implemented",
|
||||
"digitalocean/plandex": "implemented",
|
||||
"aws-lightsail/plandex": "implemented",
|
||||
"aws/plandex": "implemented",
|
||||
"gcp/plandex": "implemented",
|
||||
"fly/plandex": "implemented",
|
||||
"daytona/plandex": "implemented",
|
||||
|
|
@ -498,7 +498,7 @@
|
|||
"sprite/kilocode": "implemented",
|
||||
"hetzner/kilocode": "implemented",
|
||||
"digitalocean/kilocode": "implemented",
|
||||
"aws-lightsail/kilocode": "implemented",
|
||||
"aws/kilocode": "implemented",
|
||||
"gcp/kilocode": "implemented",
|
||||
"fly/kilocode": "implemented",
|
||||
"daytona/kilocode": "implemented",
|
||||
|
|
@ -520,7 +520,7 @@
|
|||
"sprite/continue": "implemented",
|
||||
"hetzner/continue": "implemented",
|
||||
"digitalocean/continue": "implemented",
|
||||
"aws-lightsail/continue": "implemented",
|
||||
"aws/continue": "implemented",
|
||||
"gcp/continue": "implemented",
|
||||
"fly/continue": "implemented",
|
||||
"daytona/continue": "implemented",
|
||||
|
|
|
|||
|
|
@ -781,7 +781,7 @@ list_clouds() {
|
|||
total_count=$(echo "$ALL_RECORDABLE_CLOUDS" | wc -w | tr -d ' ')
|
||||
printf '%b\n' " ${ready_count}/${total_count} clouds have credentials set"
|
||||
printf '\n'
|
||||
printf " CLI-based clouds (not recordable): sprite, gcp, fly, daytona, aws-lightsail, oracle, local\n"
|
||||
printf " CLI-based clouds (not recordable): sprite, gcp, fly, daytona, aws, oracle, local\n"
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue