CLI tool for app developers: - create: Scaffold manifest.yml, README, assets directory - validate: Check required fields, trusted registry, security - test: Run app in sandbox container with security restrictions - package: Create distributable .archy-app.tar.gz Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
290 lines
7.8 KiB
Bash
Executable File
290 lines
7.8 KiB
Bash
Executable File
#!/bin/bash
|
|
# archy-dev — App developer SDK for Archipelago
|
|
# Usage:
|
|
# archy-dev create <app-id> Create a new app scaffold
|
|
# archy-dev validate <app-dir> Validate an app manifest
|
|
# archy-dev test <app-dir> Test app in sandbox container
|
|
# archy-dev package <app-dir> 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 <app-id> Create a new app scaffold"
|
|
echo " ${SCRIPT_NAME} validate <app-dir> Validate app manifest"
|
|
echo " ${SCRIPT_NAME} test <app-dir> Test app in sandbox"
|
|
echo " ${SCRIPT_NAME} package <app-dir> 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 <app-id>"; exit 1; }
|
|
cmd_create "$2"
|
|
;;
|
|
validate)
|
|
[ -z "${2:-}" ] && { echo "Usage: $SCRIPT_NAME validate <app-dir>"; exit 1; }
|
|
cmd_validate "$2"
|
|
;;
|
|
test)
|
|
[ -z "${2:-}" ] && { echo "Usage: $SCRIPT_NAME test <app-dir>"; exit 1; }
|
|
cmd_test "$2"
|
|
;;
|
|
package)
|
|
[ -z "${2:-}" ] && { echo "Usage: $SCRIPT_NAME package <app-dir>"; exit 1; }
|
|
cmd_package "$2"
|
|
;;
|
|
--version|-v)
|
|
echo "$SCRIPT_NAME v$VERSION"
|
|
;;
|
|
*)
|
|
usage
|
|
;;
|
|
esac
|