Initial fleet-api skill

This commit is contained in:
Azat
2026-02-03 00:46:23 +01:00
commit d53c1f4917
5 changed files with 358 additions and 0 deletions

51
SKILL.md Normal file
View File

@@ -0,0 +1,51 @@
---
name: fleet-api
description: Machine registry API for fleet management
metadata:
version: "1.0.0"
vibestack:
main: false
requires: []
---
# Fleet API
Minimal REST API for machine registration and discovery.
## Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/machines/register` | Register/heartbeat a machine |
| GET | `/api/machines` | List all registered machines |
| DELETE | `/api/machines/:id` | Unregister a machine |
## Registration Payload
```json
{
"id": "machine-uuid",
"name": "dev-machine-1",
"url": "http://192.168.1.10:3000",
"version": "1.0.0",
"skills": ["caddy", "ttyd", "dashboard"],
"status": "running"
}
```
## Dependencies
Requires `socat` and `jq` to be installed:
```bash
apt-get install -y socat jq
```
## Configuration
- `FLEET_API_PORT` - API server port (default: 3001)
- `FLEET_DATA_DIR` - Data directory (default: /data/fleet)
## Storage
Machines stored in `machines.json` with `lastSeen` timestamps.
Machines with `lastSeen` > 60 seconds are marked as `status: unknown`.

137
lib/http.sh Normal file
View File

@@ -0,0 +1,137 @@
#!/bin/bash
# Minimal HTTP server using bash and socat/nc
# Send HTTP response
send_response() {
local status="$1"
local content_type="${2:-application/json}"
local body="$3"
local length=${#body}
printf "HTTP/1.1 %s\r\n" "$status"
printf "Content-Type: %s\r\n" "$content_type"
printf "Content-Length: %d\r\n" "$length"
printf "Access-Control-Allow-Origin: *\r\n"
printf "Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS\r\n"
printf "Access-Control-Allow-Headers: Content-Type\r\n"
printf "Connection: close\r\n"
printf "\r\n"
printf "%s" "$body"
}
send_json() {
send_response "$1" "application/json" "$2"
}
send_error() {
local status="$1"
local message="$2"
send_json "$status" "{\"error\":\"$message\"}"
}
# Handle a single HTTP request
handle_request() {
local request_line=""
local content_length=0
local body=""
# Read request line
read -r request_line
request_line=$(echo "$request_line" | tr -d '\r')
# Parse method and path
local method=$(echo "$request_line" | cut -d' ' -f1)
local path=$(echo "$request_line" | cut -d' ' -f2)
# Read headers
while read -r header; do
header=$(echo "$header" | tr -d '\r')
[ -z "$header" ] && break
# Extract content-length
if echo "$header" | grep -qi "^content-length:"; then
content_length=$(echo "$header" | cut -d':' -f2 | tr -d ' ')
fi
done
# Read body if present
if [ "$content_length" -gt 0 ] 2>/dev/null; then
body=$(head -c "$content_length")
fi
# Route request
route_request "$method" "$path" "$body"
}
# Route requests to handlers
route_request() {
local method="$1"
local path="$2"
local body="$3"
# Handle CORS preflight
if [ "$method" = "OPTIONS" ]; then
send_response "204 No Content" "text/plain" ""
return
fi
# API routes
case "$path" in
/api/machines/register)
if [ "$method" = "POST" ]; then
handle_register "$body"
else
send_error "405 Method Not Allowed" "Use POST"
fi
;;
/api/machines)
if [ "$method" = "GET" ]; then
handle_list_machines
else
send_error "405 Method Not Allowed" "Use GET"
fi
;;
/api/machines/*)
local machine_id="${path#/api/machines/}"
if [ "$method" = "DELETE" ]; then
handle_delete_machine "$machine_id"
elif [ "$method" = "GET" ]; then
handle_get_machine "$machine_id"
else
send_error "405 Method Not Allowed" "Use GET or DELETE"
fi
;;
/health)
send_json "200 OK" '{"status":"healthy"}'
;;
*)
send_error "404 Not Found" "Endpoint not found"
;;
esac
}
# Start HTTP server
http_server() {
local port="$1"
# Check for socat
if command -v socat &>/dev/null; then
echo "Using socat..."
while true; do
socat TCP-LISTEN:"$port",reuseaddr,fork EXEC:"$0 --handle-request"
done
else
echo "Error: socat not found. Install with: apt-get install socat"
exit 1
fi
}
# Handle --handle-request flag (called by socat)
if [ "$1" = "--handle-request" ]; then
SKILL_DIR="$(dirname "$(dirname "$0")")"
source "$SKILL_DIR/lib/machines.sh"
DATA_DIR="${FLEET_DATA_DIR:-/data/fleet}"
export DATA_DIR
handle_request
fi

131
lib/machines.sh Normal file
View File

@@ -0,0 +1,131 @@
#!/bin/bash
# Machine registry logic
MACHINES_FILE="${DATA_DIR:-/data/fleet}/machines.json"
STALE_THRESHOLD=60 # seconds
# Get current timestamp
now() {
date +%s
}
# Read machines file with lock
read_machines() {
if [ -f "$MACHINES_FILE" ]; then
cat "$MACHINES_FILE"
else
echo '{}'
fi
}
# Write machines file with lock
write_machines() {
local data="$1"
echo "$data" > "$MACHINES_FILE.tmp"
mv "$MACHINES_FILE.tmp" "$MACHINES_FILE"
}
# Update machine status based on lastSeen
update_status() {
local machines="$1"
local current_time=$(now)
echo "$machines" | jq --arg now "$current_time" --arg threshold "$STALE_THRESHOLD" '
to_entries | map(
.value.status = (
if (.value.lastSeen | tonumber) < (($now | tonumber) - ($threshold | tonumber))
then "unknown"
else .value.status
end
)
) | from_entries
'
}
# Handle POST /api/machines/register
handle_register() {
local body="$1"
# Validate JSON
if ! echo "$body" | jq -e . >/dev/null 2>&1; then
send_error "400 Bad Request" "Invalid JSON"
return
fi
# Extract required fields
local id=$(echo "$body" | jq -r '.id // empty')
local name=$(echo "$body" | jq -r '.name // empty')
local url=$(echo "$body" | jq -r '.url // empty')
if [ -z "$id" ] || [ -z "$name" ] || [ -z "$url" ]; then
send_error "400 Bad Request" "Missing required fields: id, name, url"
return
fi
# Read current machines
local machines=$(read_machines)
local current_time=$(now)
# Create/update machine entry
local machine=$(echo "$body" | jq --arg ts "$current_time" '. + {lastSeen: ($ts | tonumber)}')
# Add to machines
machines=$(echo "$machines" | jq --arg id "$id" --argjson machine "$machine" '.[$id] = $machine')
# Write back
write_machines "$machines"
send_json "200 OK" "$machine"
}
# Handle GET /api/machines
handle_list_machines() {
local machines=$(read_machines)
# Update status based on lastSeen
machines=$(update_status "$machines")
# Convert to array
local result=$(echo "$machines" | jq '[to_entries[] | .value]')
send_json "200 OK" "$result"
}
# Handle GET /api/machines/:id
handle_get_machine() {
local id="$1"
local machines=$(read_machines)
# Update status
machines=$(update_status "$machines")
# Get machine
local machine=$(echo "$machines" | jq --arg id "$id" '.[$id] // null')
if [ "$machine" = "null" ]; then
send_error "404 Not Found" "Machine not found"
return
fi
send_json "200 OK" "$machine"
}
# Handle DELETE /api/machines/:id
handle_delete_machine() {
local id="$1"
local machines=$(read_machines)
# Check if exists
local exists=$(echo "$machines" | jq --arg id "$id" 'has($id)')
if [ "$exists" = "false" ]; then
send_error "404 Not Found" "Machine not found"
return
fi
# Remove machine
machines=$(echo "$machines" | jq --arg id "$id" 'del(.[$id])')
write_machines "$machines"
send_json "200 OK" '{"deleted":true}'
}

22
scripts/autorun.sh Normal file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
set -e
SKILL_DIR="$(dirname "$(dirname "$0")")"
echo "=== Fleet API Setup ==="
# Create data directory
DATA_DIR="${FLEET_DATA_DIR:-/data/fleet}"
mkdir -p "$DATA_DIR"
# Initialize empty machines file if not exists
if [ ! -f "$DATA_DIR/machines.json" ]; then
echo '{}' > "$DATA_DIR/machines.json"
echo "Initialized machines.json"
fi
# Ensure scripts are executable
chmod +x "$SKILL_DIR/scripts/"*.sh
chmod +x "$SKILL_DIR/lib/"*.sh
echo "Fleet API setup complete"

17
scripts/run.sh Normal file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
set -e
SKILL_DIR="$(dirname "$(dirname "$0")")"
source "$SKILL_DIR/lib/http.sh"
source "$SKILL_DIR/lib/machines.sh"
PORT="${FLEET_API_PORT:-3001}"
DATA_DIR="${FLEET_DATA_DIR:-/data/fleet}"
export DATA_DIR
echo "Starting Fleet API on port $PORT..."
echo "Data directory: $DATA_DIR"
# Start HTTP server
http_server "$PORT"