#!/bin/bash set -euo pipefail # tinsnip installer - Downloads and sets up tinsnip REPO_URL="${REPO_URL:-https://tangled.sh/dynamicalsystem.com/tinsnip}" BRANCH="main" INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/opt/dynamicalsystem.tinsnip}" INSTALLER_VERSION="e3981a2" # Updated automatically on commit via hooks # Service repository options SERVICE_REPO_URL="${SERVICE_REPO_URL:-https://tangled.org/@dynamicalsystem.com/service}" SERVICE_INSTALL_DIR="${SERVICE_INSTALL_DIR:-$HOME/.local/opt/dynamicalsystem.service}" log() { echo "[Installer] $*" } error() { log "ERROR: $*" >&2 exit 1 } download_file() { local file_path="$1" local dest_path="$2" # Support both custom REPO_URL and default git server if [[ -n "${REPO_URL_OVERRIDE:-}" ]] || [[ "$REPO_URL" != "https://tangled.sh/dynamicalsystem.com/tinsnip" ]]; then local url="${REPO_URL}/${file_path}" else local url="${REPO_URL}/raw/${BRANCH}/${file_path}" fi local filename=$(basename "$file_path") log " $filename (from $url)" if command -v curl &> /dev/null; then if curl -fsSL "$url" -o "$dest_path" 2>/dev/null; then chmod +x "$dest_path" 2>/dev/null || true return 0 fi fi error "Failed to download $file_path" } clone_with_git() { log "Cloning repository with git..." if ! command -v git &> /dev/null; then log "Git not found, installing..." sudo apt-get update -qq && sudo apt-get install -y git fi if ! git clone "git@tangled.sh:dynamicalsystem.com/tinsnip" "$INSTALL_DIR"; then error "Failed to clone repository. Make sure you have SSH access to git@tangled.sh" fi } # Clone or download service repository install_service_catalog() { local service_repo="$SERVICE_REPO_URL" log "Installing service catalog..." log " Repository: $service_repo" if [[ -d "$SERVICE_INSTALL_DIR" ]]; then log " Removing: $SERVICE_INSTALL_DIR" rm -rf "$SERVICE_INSTALL_DIR" fi mkdir -p "$(dirname "$SERVICE_INSTALL_DIR")" # Try git clone first if command -v git &> /dev/null; then # Convert HTTPS URL to git URL for cloning local git_url="${service_repo/https:\/\/tangled.sh\//git@tangled.sh:}" if git clone "$git_url" "$SERVICE_INSTALL_DIR" 2>/dev/null; then log " Service catalog installed: $SERVICE_INSTALL_DIR" return 0 fi fi # Fallback to downloading files if git fails log "Git clone failed, downloading service files..." # For now, skip download fallback as service repo structure may vary log "WARNING: Could not clone service repository. Services will not be available." log "You can manually clone the service repository later:" log " git clone $service_repo $SERVICE_INSTALL_DIR" } # Download all local services from ./service/ directory download_local_services() { log "Discovering and downloading local services..." # For HTTP server, try to get directory listing of /service/ if [[ "$REPO_URL" == http://* ]]; then local service_list_url="${REPO_URL}/service/" log "Checking for services at: $service_list_url" # Try to get directory listing (works with simple HTTP servers) local services=() if command -v curl &> /dev/null; then # Extract directory names from HTTP directory listing local listing if listing=$(curl -fsSL "$service_list_url" 2>/dev/null); then log "Directory listing received, parsing..." log "Raw listing preview: $(echo "$listing" | head -10 | tr '\n' ' ')" # Try multiple parsing approaches for different HTTP server formats # Approach 1: Standard href links for directories local href_services=($(echo "$listing" | grep -oE 'href="[^/"]+/"' | sed 's/href="//g' | sed 's/\/"//g' | grep -v '^\.$\|^\.\.$' | sort)) log "Parsed href services: ${href_services[*]}" # Approach 2: Simple directory names (for plain listings) local plain_services=($(echo "$listing" | grep -oE '^[a-zA-Z][a-zA-Z0-9_-]*/$' | sed 's/\/$//g' | sort)) log "Parsed plain services: ${plain_services[*]}" # Combine and deduplicate results services=() if [[ ${#href_services[@]} -gt 0 ]] || [[ ${#plain_services[@]} -gt 0 ]]; then services=($(printf '%s\n' "${href_services[@]}" "${plain_services[@]}" 2>/dev/null | sort -u)) fi log "Discovered services: ${services[*]}" else log "Failed to get directory listing from $service_list_url" fi fi # If discovery failed, warn but don't fallback to hardcoded list if [[ ${#services[@]} -eq 0 ]]; then log "WARNING: Could not discover any local services" log "This may be normal if no services are in development" else log "Final service list: ${services[*]}" # Download each discovered service for service in "${services[@]}"; do download_service "$service" done fi else # For git repositories, we can't discover dynamically, so skip log "Git repository detected - local services not available via git clone" log "Local services are only available when using HTTP server during development" fi } # Download a single service and all its files download_service() { local service="$1" log " Downloading $service service..." # Create service directory in SERVICE_INSTALL_DIR (not INSTALL_DIR/service/) mkdir -p "$SERVICE_INSTALL_DIR/$service" # Try to download common service files local service_files=( "docker-compose.yml" "setup.sh" "README.md" "Dockerfile" "requirements.txt" ) local downloaded_count=0 for file in "${service_files[@]}"; do local file_path="service/$service/$file" local dest_path="$SERVICE_INSTALL_DIR/$service/$file" # Try to download the file, but don't fail if it doesn't exist if [[ "$REPO_URL" == http://* ]]; then local url="${REPO_URL}/${file_path}" else local url="${REPO_URL}/raw/${BRANCH}/${file_path}" fi log " Downloading: $file from $url" curl -v -L "$url" -o "$dest_path" --connect-timeout 10 --max-time 30 local curl_exit_code=$? if [[ $curl_exit_code -eq 0 ]] && [[ -f "$dest_path" ]]; then # Check if file is HTML (404 error page) and skip if so if file "$dest_path" | grep -q "HTML" || head -1 "$dest_path" 2>/dev/null | grep -q "/dev/null || true downloaded_count=$((downloaded_count + 1)) log " ✓ Downloaded: $file" fi else log " ✗ Failed: $file (exit code: $curl_exit_code)" fi done # Try to download app/ directory (common for Python services) log " Checking for app/ directory..." mkdir -p "$SERVICE_INSTALL_DIR/$service/app" local app_files=("main.py" "__init__.py" "config.py" "routes.py") local app_downloaded=false for app_file in "${app_files[@]}"; do local file_path="service/$service/app/$app_file" local dest_path="$SERVICE_INSTALL_DIR/$service/app/$app_file" if [[ "$REPO_URL" == http://* ]]; then local url="${REPO_URL}/${file_path}" else local url="${REPO_URL}/raw/${BRANCH}/${file_path}" fi if curl -fsSL "$url" -o "$dest_path" --connect-timeout 10 --max-time 30 2>/dev/null; then log " ✓ Downloaded: app/$app_file" downloaded_count=$((downloaded_count + 1)) app_downloaded=true fi done # Clean up empty app directory if nothing was downloaded if [[ "$app_downloaded" == false ]]; then rmdir "$SERVICE_INSTALL_DIR/$service/app" 2>/dev/null || true fi if [[ $downloaded_count -gt 0 ]]; then log " ✅ $service ($downloaded_count files)" else log " ❌ $service (no files found)" # Remove empty directory rmdir "$SERVICE_INSTALL_DIR/$service" 2>/dev/null || true fi } # Bootstrap topsheet sheet infrastructure bootstrap_topsheet_sheet() { log "" log "Bootstrapping topsheet sheet infrastructure..." # Check if topsheet.station-prod already exists if [[ -d "/mnt/topsheet-station-prod" ]] || getent passwd topsheet-station-prod &>/dev/null; then log "topsheet.station-prod already exists - skipping bootstrap" return fi # First-time setup - bootstrap is required log "" log "REQUIRED: Setting up the topsheet (topsheet.station-prod)" log "This creates essential infrastructure:" log " - Global sheet registry (prevents UID collisions)" log " - NAS credential storage" log " - Service catalog management" log "" log "This is required for tinsnip to function properly." log "" # Check if we can read from terminal (not piped) if [[ ! -t 0 ]]; then log "ERROR: Interactive setup required but stdin is not a terminal (piped input detected)" log "" log "For first-time setup, run the installer interactively:" log " curl -fsSL \"${REPO_URL}/raw/${BRANCH}/install.sh?\$(date +%s)\" -o /tmp/tinsnip-install.sh" log " INSTALL_SERVICES=true bash /tmp/tinsnip-install.sh" log "" log "Or install CLI only and run bootstrap manually:" log " export BOOTSTRAP_TOPSHEET=false" log " curl -fsSL \"${REPO_URL}/raw/${BRANCH}/install.sh?\$(date +%s)\" | BOOTSTRAP_TOPSHEET=false INSTALL_SERVICES=true bash" log " ~/.local/opt/dynamicalsystem.tinsnip/bin/tin machine station prod " return 1 fi # Check if topsheet NAS server is already registered local nas_registry="/mnt/station-prod/data/nas-credentials/nas-servers" if [[ -f "$nas_registry" ]] && grep -q "^topsheet=" "$nas_registry" 2>/dev/null; then nas_server=$(grep "^topsheet=" "$nas_registry" | cut -d= -f2) log "Using existing NAS server for topsheet sheet: $nas_server" else # Get NAS server for topsheet sheet echo -e "\033[1;36mEnter NAS server hostname or IP for topsheet sheet: \033[0m\c" read nas_server if [[ -z "$nas_server" ]]; then log "ERROR: NAS server is required to setop the topsheet" log "You can complete setup later with:" log " cd $INSTALL_DIR && TIN_SHEET=topsheet ./bin/tin machine create station prod " return 1 fi fi setup_tinsnip="y" case "$setup_tinsnip" in [Yy]*) log "Setting up topsheet sheet infrastructure..." # Set up topsheet.station-prod log "Creating topsheet.station-prod infrastructure..." cd "$INSTALL_DIR" if TIN_SHEET=topsheet ./bin/tin machine create station prod "$nas_server"; then log "✅ topsheet.station-prod created successfully" log "" # Only offer SSH key generation if we don't already have working SSH if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "$nas_server" exit 2>/dev/null; then log "" log "Would you like to generate an SSH key for passwordless NAS access?" log "This will enable automated operations without password prompts." echo -e "\033[1;36mGenerate SSH key for $nas_server? [Y/n]: \033[0m\c" read generate_key else log "✅ SSH access already configured for $nas_server" generate_key="n" # Skip key generation fi case "${generate_key:-y}" in [Nn]*) log "Skipping SSH key generation" log "You can generate it later with: tin key generate $nas_server" ;; *) log "Generating SSH key for $nas_server..." # Debug: Check if topsheet.station-prod infrastructure is ready if [[ -d "/mnt/station-prod/data" ]]; then log "✅ topsheet.station-prod infrastructure detected" log " Mount point: /mnt/station-prod" log " Data directory: /mnt/station-prod/data" else log "❌ topsheet.station-prod infrastructure not found" log " Expected: /mnt/station-prod/data" log " Mount check:" mount | grep station-prod || echo " No station-prod mount found" log " Directory check:" ls -la /mnt/ | grep station || echo " No station directory in /mnt/" fi if "$INSTALL_DIR/bin/tin" key generate "$nas_server"; then log "✅ SSH key generated and installed" else log "⚠️ SSH key generation failed - you can retry later" log "Manual command: tin key generate $nas_server" fi ;; esac log "" log "Platform infrastructure is ready:" log " - Sheet registry: /mnt/station-prod/data/sheets" log " - Service registry: /mnt/station-prod/data/services" log " - NAS credentials: /mnt/station-prod/data/nas-credentials/" log "" log "You can now create other sheets:" log " ./bin/tin machine create station prod $nas_server # (uses default sheet)" log " TIN_SHEET=infrastructure ./bin/tin machine create station prod $nas_server" else log "WARNING: Failed to create topsheet.station-prod" log "You can retry later with:" log " cd $INSTALL_DIR && TIN_SHEET=topsheet ./bin/tin machine create station prod $nas_server" fi ;; *) log "Skipping topsheet setup" log "You can set this up later with:" log " cd $INSTALL_DIR && TIN_SHEET=topsheet ./bin/tin machine create station prod " ;; esac } main() { log "tinsnip Infrastructure Installer (version: $INSTALLER_VERSION)" log "==============================================================" cd ~ || error "Failed to change to home directory" if [[ ! -f /etc/os-release ]] || ! grep -q "Ubuntu" /etc/os-release; then error "This installer requires Ubuntu" fi if [[ -d "$INSTALL_DIR" ]]; then log "Tinsnip directory already exists. Removing for fresh installation..." log " Removing: $INSTALL_DIR" rm -rf "$INSTALL_DIR" fi log " Creating: $INSTALL_DIR" mkdir -p "$INSTALL_DIR"/{scripts,machine/scripts,config} log "Downloading setup files..." # Download main files download_file "setup.sh" "$INSTALL_DIR/setup.sh" download_file "README.md" "$INSTALL_DIR/README.md" # Download CLI commands mkdir -p "$INSTALL_DIR/bin" download_file "bin/tin" "$INSTALL_DIR/bin/tin" # All CLI functionality now migrated to cmd/ structure # Legacy deployment scripts removed - functionality moved to CLI # Download machine infrastructure (the important stuff!) download_file "machine/teardown.sh" "$INSTALL_DIR/machine/teardown.sh" # Download new library structure mkdir -p "$INSTALL_DIR/lib" download_file "lib/core.sh" "$INSTALL_DIR/lib/core.sh" download_file "lib/uid.sh" "$INSTALL_DIR/lib/uid.sh" download_file "lib/registry.sh" "$INSTALL_DIR/lib/registry.sh" download_file "lib/nas.sh" "$INSTALL_DIR/lib/nas.sh" download_file "lib/nfs.sh" "$INSTALL_DIR/lib/nfs.sh" download_file "lib/docker.sh" "$INSTALL_DIR/lib/docker.sh" download_file "lib/metadata.sh" "$INSTALL_DIR/lib/metadata.sh" # Download validation scripts mkdir -p "$INSTALL_DIR/validation" download_file "validation/validate.sh" "$INSTALL_DIR/validation/validate.sh" download_file "validation/validate_functions.sh" "$INSTALL_DIR/validation/validate_functions.sh" download_file "validation/validate_dry_run.sh" "$INSTALL_DIR/validation/validate_dry_run.sh" download_file "validation/validate_port_edge_cases.sh" "$INSTALL_DIR/validation/validate_port_edge_cases.sh" download_file "validation/validate_docker.sh" "$INSTALL_DIR/validation/validate_docker.sh" # Download remaining machine scripts (still needed for now) download_file "machine/setup_service.sh" "$INSTALL_DIR/machine/setup_service.sh" download_file "machine/setup_station.sh" "$INSTALL_DIR/machine/setup_station.sh" download_file "machine/install_docker.sh" "$INSTALL_DIR/machine/install_docker.sh" # Legacy lib.sh removed - functionality consolidated into ./lib/ modules # Note: Service catalog is maintained in separate repository # Installed via install_service_catalog() to ~/.local/opt/{sheet}.service/ # Download new CMD structure mkdir -p "$INSTALL_DIR/cmd/"{sheet,key,machine,service,nas,registry,status} # Sheet commands download_file "cmd/sheet/create.sh" "$INSTALL_DIR/cmd/sheet/create.sh" download_file "cmd/sheet/list.sh" "$INSTALL_DIR/cmd/sheet/list.sh" download_file "cmd/sheet/show.sh" "$INSTALL_DIR/cmd/sheet/show.sh" download_file "cmd/sheet/rm.sh" "$INSTALL_DIR/cmd/sheet/rm.sh" # Key commands download_file "cmd/key/generate.sh" "$INSTALL_DIR/cmd/key/generate.sh" download_file "cmd/key/install.sh" "$INSTALL_DIR/cmd/key/install.sh" download_file "cmd/key/list.sh" "$INSTALL_DIR/cmd/key/list.sh" download_file "cmd/key/remove.sh" "$INSTALL_DIR/cmd/key/remove.sh" download_file "cmd/key/status.sh" "$INSTALL_DIR/cmd/key/status.sh" download_file "cmd/key/show.sh" "$INSTALL_DIR/cmd/key/show.sh" download_file "cmd/key/test.sh" "$INSTALL_DIR/cmd/key/test.sh" # Machine commands download_file "cmd/machine/create.sh" "$INSTALL_DIR/cmd/machine/create.sh" download_file "cmd/machine/list.sh" "$INSTALL_DIR/cmd/machine/list.sh" download_file "cmd/machine/status.sh" "$INSTALL_DIR/cmd/machine/status.sh" download_file "cmd/machine/rm.sh" "$INSTALL_DIR/cmd/machine/rm.sh" # NAS commands download_file "cmd/nas/list.sh" "$INSTALL_DIR/cmd/nas/list.sh" download_file "cmd/nas/show.sh" "$INSTALL_DIR/cmd/nas/show.sh" download_file "cmd/nas/add.sh" "$INSTALL_DIR/cmd/nas/add.sh" download_file "cmd/nas/discover.sh" "$INSTALL_DIR/cmd/nas/discover.sh" # Service commands download_file "cmd/service/deploy.sh" "$INSTALL_DIR/cmd/service/deploy.sh" download_file "cmd/service/list.sh" "$INSTALL_DIR/cmd/service/list.sh" download_file "cmd/service/status.sh" "$INSTALL_DIR/cmd/service/status.sh" download_file "cmd/service/logs.sh" "$INSTALL_DIR/cmd/service/logs.sh" download_file "cmd/service/stop.sh" "$INSTALL_DIR/cmd/service/stop.sh" download_file "cmd/service/rm.sh" "$INSTALL_DIR/cmd/service/rm.sh" # Registry and Status commands download_file "cmd/registry/show.sh" "$INSTALL_DIR/cmd/registry/show.sh" download_file "cmd/status/show.sh" "$INSTALL_DIR/cmd/status/show.sh" # Station service scripts removed - functionality migrated to tin CLI # Always install service catalog first (git clone if available) install_service_catalog # Download all local services from ./service/ directory (adds to catalog) download_local_services # Copy all local services to service catalog if [[ -d "$INSTALL_DIR/service" ]]; then log "Installing local services to catalog..." mkdir -p "$SERVICE_INSTALL_DIR" # Copy all services found in INSTALL_DIR/service/ local service_count=0 for service_dir in "$INSTALL_DIR/service"/*; do if [[ -d "$service_dir" ]]; then local service_name=$(basename "$service_dir") # Skip station service (it's infrastructure, not a containerised service) if [[ "$service_name" != "station" ]]; then cp -r "$service_dir" "$SERVICE_INSTALL_DIR/" chmod +x "$SERVICE_INSTALL_DIR/$service_name/setup.sh" 2>/dev/null || true log " ✅ $service_name" service_count=$((service_count + 1)) fi fi done if [[ $service_count -eq 0 ]]; then log " No local services found to install" else log " Installed $service_count local service(s)" fi fi log "Installation complete!" log "" log "Adding tinsnip CLI to your PATH..." # Add to PATH in shell profiles if not already present local path_export="export PATH=\"$INSTALL_DIR/bin:\$PATH\"" local added_to_profile=false # Add to ~/.bashrc if it exists and path not already there if [[ -f ~/.bashrc ]] && ! grep -q "$INSTALL_DIR/bin" ~/.bashrc 2>/dev/null; then echo "" >> ~/.bashrc echo "# tinsnip CLI" >> ~/.bashrc echo "$path_export" >> ~/.bashrc log " Added to ~/.bashrc" added_to_profile=true fi # Add to ~/.bash_profile if it exists and path not already there if [[ -f ~/.bash_profile ]] && ! grep -q "$INSTALL_DIR/bin" ~/.bash_profile 2>/dev/null; then echo "" >> ~/.bash_profile echo "# tinsnip CLI" >> ~/.bash_profile echo "$path_export" >> ~/.bash_profile log " Added to ~/.bash_profile" added_to_profile=true fi # Add to ~/.profile as fallback if no other profile was updated if [[ "$added_to_profile" == false ]]; then if [[ ! -f ~/.profile ]] || ! grep -q "$INSTALL_DIR/bin" ~/.profile 2>/dev/null; then echo "" >> ~/.profile echo "# tinsnip CLI" >> ~/.profile echo "$path_export" >> ~/.profile log " Added to ~/.profile" added_to_profile=true fi fi if [[ "$added_to_profile" == true ]]; then log " ✅ PATH updated in shell profile(s)" log " ⚠️ Start a new shell session or run: source ~/.bashrc" else log " ⚠️ PATH already configured or no shell profile found" fi log "" log "Verify installation:" log " $INSTALL_DIR/bin/tin --version" log " $INSTALL_DIR/bin/tin help" log "" # Bootstrap topsheet if requested (after CLI is installed and in PATH) if [[ "${BOOTSTRAP_TOPSHEET:-true}" == "true" ]]; then bootstrap_topsheet_sheet fi log "" log "Next steps:" log "1. Set up topsheet (one-time):" log " TIN_SHEET=topsheet $INSTALL_DIR/bin/tin machine station prod " log "" log "2. Create a sheet and deploy services:" log " $INSTALL_DIR/bin/tin sheet infrastructure" log " $INSTALL_DIR/bin/tin machine gateway prod" log " $INSTALL_DIR/bin/tin service gateway-prod lldap" log "" log "3. Deploy services:" log " $INSTALL_DIR/bin/tin machine marshal prod # External route coordinator" log " $INSTALL_DIR/bin/tin service marshal-prod marshal" log " $INSTALL_DIR/bin/tin machine pds prod # atproto Personal Data Server" log " $INSTALL_DIR/bin/tin service pds-prod pds" log "" log "Or use the legacy script interface:" if [[ -d "$SERVICE_INSTALL_DIR" ]]; then # Get list of available services from the catalog for service in $(ls -1 "$SERVICE_INSTALL_DIR" 2>/dev/null | grep -v README | head -3); do log " cd $INSTALL_DIR && ./bin/tin machine create $service prod DS412plus.local" done else log " cd $INSTALL_DIR && ./bin/tin machine create gateway prod DS412plus.local" fi } main "$@"