#!/bin/bash # archy-dev — App developer SDK for Archipelago # Usage: # archy-dev create Create a new app scaffold # archy-dev validate Validate an app manifest # archy-dev test Test app in sandbox container # archy-dev package Package app for distribution # # Creates apps compatible with the Archipelago marketplace. set -euo pipefail SCRIPT_NAME="archy-dev" VERSION="0.1.0" usage() { echo "${SCRIPT_NAME} v${VERSION} — Archipelago App Developer SDK" echo "" echo "Usage:" echo " ${SCRIPT_NAME} create Create a new app scaffold" echo " ${SCRIPT_NAME} validate Validate app manifest" echo " ${SCRIPT_NAME} test Test app in sandbox" echo " ${SCRIPT_NAME} package Package for distribution" echo "" echo "Examples:" echo " ${SCRIPT_NAME} create my-cool-app" echo " ${SCRIPT_NAME} validate ./my-cool-app" echo " ${SCRIPT_NAME} test ./my-cool-app" } cmd_create() { local APP_ID="$1" # Validate app ID if ! echo "$APP_ID" | grep -qE '^[a-z][a-z0-9-]{0,63}$'; then echo "Error: App ID must be lowercase alphanumeric with hyphens, 1-64 chars" exit 1 fi if [ -d "$APP_ID" ]; then echo "Error: Directory '$APP_ID' already exists" exit 1 fi echo "Creating app scaffold: $APP_ID" mkdir -p "$APP_ID" # Create manifest cat > "$APP_ID/manifest.yml" << EOF # Archipelago App Manifest id: ${APP_ID} title: $(echo "$APP_ID" | sed 's/-/ /g' | sed 's/\b\(.\)/\u\1/g') version: 0.1.0 description: A custom Archipelago app author: Your Name license: MIT # Docker image (must be from trusted registry) image: docker.io/your-org/${APP_ID}:0.1.0 # Resource requirements resources: memory: 256m cpus: 1 # Port mappings (host:container) ports: - "8080:8080" # Persistent data volumes volumes: - "/var/lib/archipelago/${APP_ID}:/data" # Environment variables environment: - "TZ=UTC" # Security (recommended defaults) security: readonly_root: false capabilities: [] # Add only what's needed: CHOWN, SETUID, etc. no_new_privileges: true # Health check health: cmd: "curl -sf http://localhost:8080/health || exit 1" interval: 30s retries: 3 # App tier: core, recommended, or optional tier: optional # Dependencies (other Archipelago apps that must be running) dependencies: [] # Category for marketplace category: other EOF # Create README cat > "$APP_ID/README.md" << EOF # ${APP_ID} An Archipelago app. ## Development 1. Edit \`manifest.yml\` with your app's configuration 2. Build your Docker image: \`docker build -t ${APP_ID}:0.1.0 .\` 3. Validate: \`archy-dev validate .\` 4. Test: \`archy-dev test .\` 5. Package: \`archy-dev package .\` ## Manifest Reference See \`docs/app-manifest-spec.md\` in the Archipelago repository. EOF # Create icon placeholder mkdir -p "$APP_ID/assets" echo "Place your app icon here (PNG, 256x256 recommended)" > "$APP_ID/assets/icon-placeholder.txt" echo "✅ App scaffold created at ./$APP_ID" echo "" echo "Next steps:" echo " 1. Edit $APP_ID/manifest.yml" echo " 2. Build your Docker image" echo " 3. Run: ${SCRIPT_NAME} validate ./$APP_ID" } cmd_validate() { local APP_DIR="$1" local MANIFEST="$APP_DIR/manifest.yml" if [ ! -f "$MANIFEST" ]; then echo "Error: No manifest.yml found in $APP_DIR" exit 1 fi echo "Validating $MANIFEST..." PASS=0; FAIL=0 check() { if eval "$2" 2>/dev/null; then PASS=$((PASS + 1)) echo " ✅ $1" else FAIL=$((FAIL + 1)) echo " ❌ $1" fi } # Check required fields check "id field present" "grep -q '^id:' $MANIFEST" check "title field present" "grep -q '^title:' $MANIFEST" check "version field present" "grep -q '^version:' $MANIFEST" check "description field present" "grep -q '^description:' $MANIFEST" check "image field present" "grep -q '^image:' $MANIFEST" # Check image from trusted registry IMAGE=$(grep '^image:' "$MANIFEST" | awk '{print $2}') check "image from trusted registry" "echo '$IMAGE' | grep -qE '^(docker.io|ghcr.io|quay.io)/'" check "image not using :latest" "echo '$IMAGE' | grep -qvE ':latest$'" # Check security check "no privileged mode" "! grep -q 'privileged: true' $MANIFEST" check "no host networking" "! grep -q 'network: host' $MANIFEST" # Check memory limit check "memory limit set" "grep -q 'memory:' $MANIFEST" echo "" echo "Results: $PASS passed, $FAIL failed" [ "$FAIL" -gt 0 ] && exit 1 echo "✅ Manifest is valid" } cmd_test() { local APP_DIR="$1" local MANIFEST="$APP_DIR/manifest.yml" if [ ! -f "$MANIFEST" ]; then echo "Error: No manifest.yml found in $APP_DIR" exit 1 fi IMAGE=$(grep '^image:' "$MANIFEST" | awk '{print $2}') APP_ID=$(grep '^id:' "$MANIFEST" | awk '{print $2}') echo "Testing $APP_ID ($IMAGE) in sandbox..." echo "" # Check if image exists locally if ! podman image exists "$IMAGE" 2>/dev/null && ! docker image inspect "$IMAGE" >/dev/null 2>&1; then echo "Image not found locally. Pull or build first:" echo " docker pull $IMAGE" echo " OR" echo " docker build -t $IMAGE $APP_DIR" exit 1 fi DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker echo "Starting sandbox container..." $DOCKER run -d --name "archy-test-${APP_ID}" \ --cap-drop=ALL \ --security-opt=no-new-privileges:true \ --memory=512m \ --cpus=1 \ "$IMAGE" 2>/dev/null echo "Waiting 10s for startup..." sleep 10 STATE=$($DOCKER inspect "archy-test-${APP_ID}" --format '{{.State.Status}}' 2>/dev/null || echo "unknown") echo "Container state: $STATE" if [ "$STATE" = "running" ]; then echo "✅ App starts successfully in sandbox" else echo "❌ App failed to start" echo "Logs:" $DOCKER logs "archy-test-${APP_ID}" 2>&1 | tail -20 fi # Cleanup $DOCKER stop "archy-test-${APP_ID}" 2>/dev/null || true $DOCKER rm "archy-test-${APP_ID}" 2>/dev/null || true } cmd_package() { local APP_DIR="$1" local MANIFEST="$APP_DIR/manifest.yml" if [ ! -f "$MANIFEST" ]; then echo "Error: No manifest.yml found in $APP_DIR" exit 1 fi APP_ID=$(grep '^id:' "$MANIFEST" | awk '{print $2}') VERSION=$(grep '^version:' "$MANIFEST" | awk '{print $2}') PACKAGE="${APP_ID}-${VERSION}.archy-app.tar.gz" echo "Packaging $APP_ID v$VERSION..." # Validate first cmd_validate "$APP_DIR" || exit 1 # Create package tar -czf "$PACKAGE" -C "$APP_DIR" manifest.yml README.md assets/ 2>/dev/null || \ tar -czf "$PACKAGE" -C "$APP_DIR" manifest.yml 2>/dev/null echo "" echo "✅ Package created: $PACKAGE" echo " Size: $(ls -lh "$PACKAGE" | awk '{print $5}')" echo "" echo "To submit to the Archipelago marketplace:" echo " 1. Push your Docker image to a trusted registry" echo " 2. Submit a PR to the Archipelago apps/ directory" echo " 3. Include this package for review" } # Main dispatch case "${1:-}" in create) [ -z "${2:-}" ] && { echo "Usage: $SCRIPT_NAME create "; exit 1; } cmd_create "$2" ;; validate) [ -z "${2:-}" ] && { echo "Usage: $SCRIPT_NAME validate "; exit 1; } cmd_validate "$2" ;; test) [ -z "${2:-}" ] && { echo "Usage: $SCRIPT_NAME test "; exit 1; } cmd_test "$2" ;; package) [ -z "${2:-}" ] && { echo "Usage: $SCRIPT_NAME package "; exit 1; } cmd_package "$2" ;; --version|-v) echo "$SCRIPT_NAME v$VERSION" ;; *) usage ;; esac