diff --git a/registry/toti85/README.md b/registry/toti85/README.md new file mode 100644 index 00000000..e69de29b diff --git a/registry/toti85/modules/auto-dev-server/README.md b/registry/toti85/modules/auto-dev-server/README.md new file mode 100644 index 00000000..2a4a7aac --- /dev/null +++ b/registry/toti85/modules/auto-dev-server/README.md @@ -0,0 +1,258 @@ +--- +display_name: Auto Development Server +description: Automatically detect and start development servers based on project detection +icon: ../../../../.icons/play.svg +verified: false +tags: [development, automation, devserver, nodejs, rails, django, flask, spring] +--- + +# Auto Development Server + +This module automatically detects development projects in your workspace and starts the appropriate development servers in the background. It supports multiple frameworks and integrates with devcontainer.json configuration. + +```tf +module "auto_dev_server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/toti85/auto-dev-server/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +## Features + +🔍 **Multi-Framework Detection**: Supports Node.js, Rails, Django, Flask, FastAPI, Spring Boot, Go, Rust, and PHP projects + +⚙️ **Devcontainer Integration**: Automatically reads and executes `postStartCommand` from `.devcontainer/devcontainer.json` + +🚀 **Auto-Start on Workspace Launch**: Servers start automatically when your workspace boots up + +📊 **Health Monitoring**: Periodic health checks ensure servers stay running + +🎛️ **Highly Configurable**: Customize detection patterns, startup commands, and behavior + +📝 **Comprehensive Logging**: Debug and monitor server startup with detailed logs + +## Basic Usage + +Add this module to your Coder template to enable automatic development server detection: + +```tf +module "auto_dev_server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/toti85/auto-dev-server/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +## Supported Frameworks + +| Framework | Detection Files | Default Start Command | +| --------------- | ------------------------------------------ | --------------------------------------------- | +| **Node.js** | `package.json` | `npm start` | +| **Rails** | `Gemfile`, `config.ru`, `app/controllers` | `rails server` | +| **Django** | `manage.py`, `settings.py` | `python manage.py runserver 0.0.0.0:8000` | +| **Flask** | `app.py`, `application.py`, `wsgi.py` | `flask run --host=0.0.0.0` | +| **FastAPI** | `main.py`, `app.py` | `uvicorn main:app --host 0.0.0.0 --port 8000` | +| **Spring Boot** | `pom.xml`, `build.gradle`, `src/main/java` | `./mvnw spring-boot:run` | +| **Go** | `go.mod`, `main.go` | `go run .` | +| **Rust** | `Cargo.toml`, `src/main.rs` | `cargo run` | +| **PHP** | `index.php`, `composer.json` | `php -S 0.0.0.0:8000` | + +## Configuration Options + +### Basic Configuration + +```tf +module "auto_dev_server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/toti85/auto-dev-server/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + project_dir = "/home/coder/projects" + start_delay = 45 +} +``` + +### Framework Selection + +Enable only specific frameworks: + +```tf +module "auto_dev_server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/toti85/auto-dev-server/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + enabled_frameworks = ["nodejs", "rails", "django"] +} +``` + +### Custom Start Commands + +Override default commands for specific frameworks: + +```tf +module "auto_dev_server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/toti85/auto-dev-server/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + custom_commands = { + nodejs = "npm run dev" + rails = "bundle exec rails server -b 0.0.0.0" + django = "python manage.py runserver 0.0.0.0:3000" + } +} +``` + +### Advanced Configuration + +```tf +module "auto_dev_server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/toti85/auto-dev-server/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + project_dir = "/workspace" + enabled_frameworks = ["nodejs", "rails", "django", "spring"] + start_delay = 60 + log_level = "DEBUG" + use_devcontainer = true + custom_commands = { + nodejs = "npm run dev -- --host 0.0.0.0" + spring = "./gradlew bootRun" + } +} +``` + +## Variables + +| Variable | Type | Default | Description | +| -------------------- | -------------- | ---------------------------------------------------------------------------------- | ---------------------------------------- | +| `agent_id` | `string` | **Required** | The ID of a Coder agent | +| `project_dir` | `string` | `"$HOME"` | Directory to scan for projects | +| `enabled_frameworks` | `list(string)` | `["nodejs", "rails", "django", "flask", "fastapi", "spring", "go", "rust", "php"]` | Frameworks to detect | +| `start_delay` | `number` | `30` | Delay before starting servers (seconds) | +| `log_level` | `string` | `"INFO"` | Logging level (DEBUG, INFO, WARN, ERROR) | +| `use_devcontainer` | `bool` | `true` | Enable devcontainer.json integration | +| `custom_commands` | `map(string)` | `{}` | Custom start commands per framework | + +## Outputs + +| Output | Description | +| -------------------- | ------------------------------------ | +| `log_file` | Path to the auto-dev-server log file | +| `enabled_frameworks` | List of enabled frameworks | +| `project_directory` | Directory being scanned | + +## Devcontainer Integration + +The module automatically detects and respects `.devcontainer/devcontainer.json` configuration: + +```json +{ + "name": "My Dev Environment", + "image": "node:18", + "postStartCommand": "npm install && npm run dev", + "forwardPorts": [3000, 8080] +} +``` + +If a `postStartCommand` or `postCreateCommand` is found, it takes precedence over framework-specific defaults. + +## Monitoring & Logs + +### Log Files + +- **Main log**: `$PROJECT_DIR/auto-dev-server.log` +- **Framework logs**: `$PROJECT_DIR/.auto-dev-server/{framework}.log` +- **PID files**: `$PROJECT_DIR/.auto-dev-server/{framework}.pid` + +### Checking Server Status + +```bash +# View main log +tail -f ~/auto-dev-server.log + +# Check running servers +ls ~/.auto-dev-server/*.pid + +# View specific framework log +tail -f ~/.auto-dev-server/nodejs.log +``` + +## Troubleshooting + +### Servers Not Starting + +1. **Check logs**: `tail -f ~/auto-dev-server.log` +2. **Verify detection**: Ensure your project files match detection patterns +3. **Check permissions**: Ensure the agent can execute startup commands +4. **Increase delay**: Some projects need more time to initialize + +### Framework Not Detected + +1. **Verify files exist**: Check that detection files are present +2. **Check enabled frameworks**: Ensure the framework is in `enabled_frameworks` +3. **Custom patterns**: Use `custom_commands` for non-standard setups + +### Port Conflicts + +If you have port conflicts, customize commands to use different ports: + +```tf +custom_commands = { + nodejs = "npm start -- --port 3001" + django = "python manage.py runserver 0.0.0.0:8001" +} +``` + +## Examples + +### Full-Stack Development Setup + +```tf +module "auto_dev_server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/toti85/auto-dev-server/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + project_dir = "/home/coder/workspace" + enabled_frameworks = ["nodejs", "django", "spring"] + start_delay = 45 + custom_commands = { + nodejs = "npm run dev:frontend" + django = "python manage.py runserver 0.0.0.0:8000" + spring = "./mvnw spring-boot:run -Dspring-boot.run.profiles=dev" + } +} +``` + +### Microservices Setup + +```tf +module "auto_dev_server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/toti85/auto-dev-server/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + project_dir = "/workspace/services" + enabled_frameworks = ["nodejs", "go", "fastapi"] + log_level = "DEBUG" + custom_commands = { + nodejs = "npm run dev:api" + go = "go run cmd/server/main.go" + fastapi = "uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload" + } +} +``` + +## Contributing + +This module is part of the Coder registry. For issues and contributions, please visit the [GitHub repository](https://github.com/coder/registry). + +## License + +Licensed under the MIT License. diff --git a/registry/toti85/modules/auto-dev-server/main.tf b/registry/toti85/modules/auto-dev-server/main.tf new file mode 100644 index 00000000..c27eb07a --- /dev/null +++ b/registry/toti85/modules/auto-dev-server/main.tf @@ -0,0 +1,142 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.12" + } + } +} + +variable "agent_id" { + description = "The ID of a Coder agent." + type = string +} + +variable "project_dir" { + description = "The directory to scan for development projects. Defaults to $HOME." + type = string + default = "$HOME" +} + +variable "enabled_frameworks" { + description = "List of frameworks to detect and auto-start. Available: nodejs, rails, django, flask, fastapi, spring, go, rust, php" + type = list(string) + default = ["nodejs", "rails", "django", "flask", "fastapi", "spring", "go", "rust", "php"] + + validation { + condition = alltrue([ + for framework in var.enabled_frameworks : + contains(["nodejs", "rails", "django", "flask", "fastapi", "spring", "go", "rust", "php"], framework) + ]) + error_message = "Invalid framework. Allowed values: nodejs, rails, django, flask, fastapi, spring, go, rust, php" + } +} + +variable "start_delay" { + description = "Delay in seconds before starting dev servers (to allow workspace to fully initialize)" + type = number + default = 30 +} + +variable "log_level" { + description = "Log level for auto-dev-server. Options: DEBUG, INFO, WARN, ERROR" + type = string + default = "INFO" + + validation { + condition = contains(["DEBUG", "INFO", "WARN", "ERROR"], var.log_level) + error_message = "Log level must be one of: DEBUG, INFO, WARN, ERROR" + } +} + +variable "use_devcontainer" { + description = "Enable devcontainer.json integration for custom startup commands" + type = bool + default = true +} + +variable "custom_commands" { + description = "Custom startup commands per framework type" + type = map(string) + default = {} +} + +locals { + # Default commands for each framework + default_commands = { + nodejs = "npm start" + rails = "rails server" + django = "python manage.py runserver 0.0.0.0:8000" + flask = "flask run --host=0.0.0.0" + fastapi = "uvicorn main:app --host 0.0.0.0 --port 8000" + spring = "./mvnw spring-boot:run" + go = "go run ." + rust = "cargo run" + php = "php -S 0.0.0.0:8000" + } + + # Merge custom commands with defaults + framework_commands = merge(local.default_commands, var.custom_commands) + + # Detection patterns for each framework + detection_patterns = { + nodejs = "package.json" + rails = "Gemfile|config.ru|app/controllers" + django = "manage.py|settings.py" + flask = "app.py|application.py|wsgi.py" + fastapi = "main.py|app.py" + spring = "pom.xml|build.gradle|src/main/java" + go = "go.mod|main.go" + rust = "Cargo.toml|src/main.rs" + php = "index.php|composer.json" + } +} + +resource "coder_script" "auto_dev_server" { + agent_id = var.agent_id + display_name = "Auto Development Server" + icon = "/icon/play.svg" + script = replace( + replace( + replace( + replace( + replace( + replace( + replace( + file("${path.module}/scripts/auto-dev-server.sh"), + "PROJECT_DIR_PLACEHOLDER", var.project_dir + ), + "ENABLED_FRAMEWORKS_PLACEHOLDER", jsonencode(var.enabled_frameworks) + ), + "FRAMEWORK_COMMANDS_PLACEHOLDER", jsonencode(local.framework_commands) + ), + "DETECTION_PATTERNS_PLACEHOLDER", jsonencode(local.detection_patterns) + ), + "START_DELAY_PLACEHOLDER", tostring(var.start_delay) + ), + "LOG_LEVEL_PLACEHOLDER", var.log_level + ), + "USE_DEVCONTAINER_PLACEHOLDER", tostring(var.use_devcontainer) + ) + run_on_start = true + run_on_stop = false + timeout = 300 +} + +# Output useful information +output "log_file" { + description = "Path to the auto-dev-server log file" + value = "${var.project_dir}/auto-dev-server.log" +} + +output "enabled_frameworks" { + description = "List of enabled frameworks for detection" + value = var.enabled_frameworks +} + +output "project_directory" { + description = "Directory being scanned for projects" + value = var.project_dir +} diff --git a/registry/toti85/modules/auto-dev-server/scripts/auto-dev-server.sh b/registry/toti85/modules/auto-dev-server/scripts/auto-dev-server.sh new file mode 100644 index 00000000..3578f40d --- /dev/null +++ b/registry/toti85/modules/auto-dev-server/scripts/auto-dev-server.sh @@ -0,0 +1,312 @@ +#!/bin/bash + +# Auto Development Server Script +# Automatically detects and starts development servers based on project structure + +set -euo pipefail + +# Configuration from Terraform variables +PROJECT_DIR="PROJECT_DIR_PLACEHOLDER" +ENABLED_FRAMEWORKS='ENABLED_FRAMEWORKS_PLACEHOLDER' +FRAMEWORK_COMMANDS='FRAMEWORK_COMMANDS_PLACEHOLDER' +DETECTION_PATTERNS='DETECTION_PATTERNS_PLACEHOLDER' +START_DELAY=START_DELAY_PLACEHOLDER +LOG_LEVEL="LOG_LEVEL_PLACEHOLDER" +USE_DEVCONTAINER=USE_DEVCONTAINER_PLACEHOLDER + +# Expand variables like $HOME +PROJECT_DIR=$(eval echo "$PROJECT_DIR") +LOG_FILE="$PROJECT_DIR/auto-dev-server.log" +PID_DIR="$PROJECT_DIR/.auto-dev-server" + +# Logging function +log() { + local level=$1 + shift + local message="$*" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + + case $LOG_LEVEL in + "DEBUG") [[ "$level" =~ ^(DEBUG|INFO|WARN|ERROR)$ ]] && echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE" ;; + "INFO") [[ "$level" =~ ^(INFO|WARN|ERROR)$ ]] && echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE" ;; + "WARN") [[ "$level" =~ ^(WARN|ERROR)$ ]] && echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE" ;; + "ERROR") [[ "$level" == "ERROR" ]] && echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE" ;; + esac +} + +# Parse JSON arrays from Terraform +parse_json() { + echo "$1" | jq -r '.[]' 2> /dev/null || echo "" +} + +# Parse JSON objects from Terraform +parse_json_object() { + echo "$1" | jq -r 'to_entries[] | "\(.key)=\(.value)"' 2> /dev/null || echo "" +} + +# Create necessary directories +mkdir -p "$PID_DIR" +touch "$LOG_FILE" + +log "INFO" "Auto Development Server starting..." +log "DEBUG" "Project directory: $PROJECT_DIR" +log "DEBUG" "Enabled frameworks: $(echo "$ENABLED_FRAMEWORKS" | jq -c .)" + +# Wait for workspace to initialize +log "INFO" "Waiting $START_DELAY seconds for workspace initialization..." +sleep "$START_DELAY" + +# Check if devcontainer.json exists and extract startup commands +check_devcontainer() { + local dir="$1" + local devcontainer_files=(".devcontainer/devcontainer.json" ".devcontainer.json") + + for file in "${devcontainer_files[@]}"; do + local devcontainer_path="$dir/$file" + if [[ -f "$devcontainer_path" ]]; then + log "INFO" "Found devcontainer config: $devcontainer_path" + + # Extract postStartCommand or postCreateCommand + local post_start=$(jq -r '.postStartCommand // empty' "$devcontainer_path" 2> /dev/null) + local post_create=$(jq -r '.postCreateCommand // empty' "$devcontainer_path" 2> /dev/null) + + if [[ -n "$post_start" ]]; then + log "INFO" "Found postStartCommand in devcontainer: $post_start" + echo "$post_start" + return 0 + elif [[ -n "$post_create" ]]; then + log "INFO" "Found postCreateCommand in devcontainer: $post_create" + echo "$post_create" + return 0 + fi + fi + done + + return 1 +} + +# Detect framework type in a directory +detect_framework() { + local dir="$1" + local detected_frameworks=() + + log "DEBUG" "Scanning directory: $dir" + + # Parse detection patterns + while IFS= read -r pattern_line; do + [[ -z "$pattern_line" ]] && continue + + local framework=$(echo "$pattern_line" | cut -d'=' -f1) + local patterns=$(echo "$pattern_line" | cut -d'=' -f2-) + + # Check if framework is enabled + if echo "$ENABLED_FRAMEWORKS" | jq -e --arg fw "$framework" 'index($fw)' > /dev/null 2>&1; then + log "DEBUG" "Checking $framework patterns: $patterns" + + # Split patterns by | + IFS='|' read -ra PATTERN_ARRAY <<< "$patterns" + for pattern in "${PATTERN_ARRAY[@]}"; do + if [[ -e "$dir/$pattern" ]] || find "$dir" -name "$pattern" -type f -print -quit 2> /dev/null | grep -q .; then + log "INFO" "Detected $framework project in $dir (matched: $pattern)" + detected_frameworks+=("$framework") + break + fi + done + fi + done <<< "$(parse_json_object "$DETECTION_PATTERNS")" + + printf '%s\n' "${detected_frameworks[@]}" +} + +# Start development server for a framework +start_dev_server() { + local framework="$1" + local project_path="$2" + local custom_command="$3" + + # Get the command for this framework + local command="" + if [[ -n "$custom_command" ]]; then + command="$custom_command" + else + command=$(echo "$FRAMEWORK_COMMANDS" | jq -r --arg fw "$framework" '.[$fw] // empty') + fi + + if [[ -z "$command" ]]; then + log "ERROR" "No start command defined for framework: $framework" + return 1 + fi + + local pid_file="$PID_DIR/${framework}.pid" + local log_file="$PID_DIR/${framework}.log" + + # Check if server is already running + if [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" 2> /dev/null; then + log "WARN" "$framework server already running (PID: $(cat "$pid_file"))" + return 0 + fi + + log "INFO" "Starting $framework server in $project_path" + log "INFO" "Command: $command" + + # Start the server in background + cd "$project_path" + ( + # Set environment variables for development + export NODE_ENV=development + export RAILS_ENV=development + export FLASK_ENV=development + export DEBUG=true + + # Execute the command + bash -c "$command" > "$log_file" 2>&1 & + echo $! > "$pid_file" + log "INFO" "$framework server started with PID $! (logs: $log_file)" + ) +} + +# Stop all running servers +stop_all_servers() { + log "INFO" "Stopping all development servers..." + + for pid_file in "$PID_DIR"/*.pid; do + [[ -f "$pid_file" ]] || continue + + local framework=$(basename "$pid_file" .pid) + local pid=$(cat "$pid_file") + + if kill -0 "$pid" 2> /dev/null; then + log "INFO" "Stopping $framework server (PID: $pid)" + kill "$pid" || kill -9 "$pid" 2> /dev/null + fi + + rm -f "$pid_file" + done +} + +# Signal handlers +trap stop_all_servers EXIT INT TERM + +# Main scanning and detection logic +scan_projects() { + local base_dir="$PROJECT_DIR" + + # Ensure base directory exists + if [[ ! -d "$base_dir" ]]; then + log "ERROR" "Project directory does not exist: $base_dir" + return 1 + fi + + log "INFO" "Scanning for projects in: $base_dir" + + # Scan current directory and subdirectories (max depth 3) + local scanned_dirs=() + + # Add current directory + scanned_dirs+=("$base_dir") + + # Add subdirectories + while IFS= read -r -d '' dir; do + scanned_dirs+=("$dir") + done < <(find "$base_dir" -maxdepth 3 -type d -print0 2> /dev/null) + + local servers_started=0 + local processed_frameworks=() + + for dir in "${scanned_dirs[@]}"; do + # Skip hidden directories and common non-project dirs + [[ "$(basename "$dir")" =~ ^\. ]] && continue + [[ "$(basename "$dir")" =~ ^(node_modules|vendor|target|build|dist|__pycache__|\.git)$ ]] && continue + + # Check for devcontainer first if enabled + local devcontainer_command="" + if [[ "$USE_DEVCONTAINER" == "true" ]]; then + devcontainer_command=$(check_devcontainer "$dir") || true + fi + + # Detect frameworks in this directory + local frameworks=($(detect_framework "$dir")) + + for framework in "${frameworks[@]}"; do + # Skip if we already processed this framework + if [[ " ${processed_frameworks[*]} " =~ " $framework " ]]; then + log "DEBUG" "Framework $framework already processed, skipping" + continue + fi + + # Start the server + if start_dev_server "$framework" "$dir" "$devcontainer_command"; then + servers_started=$((servers_started + 1)) + processed_frameworks+=("$framework") + fi + done + done + + if [[ $servers_started -eq 0 ]]; then + log "INFO" "No development projects detected or all servers already running" + else + log "INFO" "Started $servers_started development server(s)" + fi +} + +# Health check function +health_check() { + local healthy_servers=0 + local total_servers=0 + + for pid_file in "$PID_DIR"/*.pid; do + [[ -f "$pid_file" ]] || continue + + total_servers=$((total_servers + 1)) + local framework=$(basename "$pid_file" .pid) + local pid=$(cat "$pid_file") + + if kill -0 "$pid" 2> /dev/null; then + healthy_servers=$((healthy_servers + 1)) + log "DEBUG" "$framework server is healthy (PID: $pid)" + else + log "WARN" "$framework server is not responding (PID: $pid)" + rm -f "$pid_file" + fi + done + + log "INFO" "Health check: $healthy_servers/$total_servers servers healthy" +} + +# Main execution +main() { + log "INFO" "=== Auto Development Server Started ===" + + # Ensure required tools are available + for tool in jq find; do + if ! command -v "$tool" > /dev/null 2>&1; then + log "ERROR" "Required tool not found: $tool" + log "INFO" "Installing $tool..." + # Try to install jq if missing + if [[ "$tool" == "jq" ]]; then + if command -v apt-get > /dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y jq + elif command -v yum > /dev/null 2>&1; then + sudo yum install -y jq + elif command -v brew > /dev/null 2>&1; then + brew install jq + else + log "ERROR" "Cannot install jq automatically. Please install manually." + exit 1 + fi + fi + fi + done + + # Run the main scanning + scan_projects + + # Keep running and do periodic health checks + while true; do + sleep 60 + health_check + done +} + +# Run main function +main "$@" diff --git a/registry/toti85/modules/auto-dev-server/test/main.tf b/registry/toti85/modules/auto-dev-server/test/main.tf new file mode 100644 index 00000000..14e9907a --- /dev/null +++ b/registry/toti85/modules/auto-dev-server/test/main.tf @@ -0,0 +1,43 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + } +} + +# Mock agent for testing +resource "coder_agent" "test" { + os = "linux" + arch = "amd64" +} + +# Test basic functionality +module "auto_dev_server_basic" { + source = "../" + agent_id = coder_agent.test.id +} + +# Test with custom configuration +module "auto_dev_server_custom" { + source = "../" + agent_id = coder_agent.test.id + project_dir = "/workspace" + enabled_frameworks = ["nodejs", "django"] + start_delay = 45 + log_level = "DEBUG" + use_devcontainer = false + custom_commands = { + nodejs = "npm run dev" + django = "python manage.py runserver 0.0.0.0:3000" + } +} + +# Test outputs +output "basic_log_file" { + value = module.auto_dev_server_basic.log_file +} + +output "custom_frameworks" { + value = module.auto_dev_server_custom.enabled_frameworks +}