From d836184e5f64213dc61862be21761fd0550d4229 Mon Sep 17 00:00:00 2001 From: kunstewi Date: Sat, 12 Jul 2025 02:57:20 +0530 Subject: [PATCH 1/2] added auto-dev-server --- registry/kunstewi/.images/avatar.png | Bin 0 -> 43318 bytes registry/kunstewi/README.md | 14 + .../modules/auto-dev-server/README.md | 59 ++++ .../kunstewi/modules/auto-dev-server/main.tf | 71 +++++ .../kunstewi/modules/auto-dev-server/run.sh | 26 ++ .../scripts/auto-dev-servers.sh | 289 ++++++++++++++++++ .../modules/auto-dev-server/variables.tf | 34 +++ 7 files changed, 493 insertions(+) create mode 100644 registry/kunstewi/.images/avatar.png create mode 100644 registry/kunstewi/README.md create mode 100644 registry/kunstewi/modules/auto-dev-server/README.md create mode 100644 registry/kunstewi/modules/auto-dev-server/main.tf create mode 100755 registry/kunstewi/modules/auto-dev-server/run.sh create mode 100644 registry/kunstewi/modules/auto-dev-server/scripts/auto-dev-servers.sh create mode 100644 registry/kunstewi/modules/auto-dev-server/variables.tf diff --git a/registry/kunstewi/.images/avatar.png b/registry/kunstewi/.images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..e918aea257aaad01dbe89738e9f6e0e502d96da2 GIT binary patch literal 43318 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 Y9{%RDBT(IH*hL&-sY$Y2BDx>{+0^(dO#lD@ literal 0 HcmV?d00001 diff --git a/registry/kunstewi/README.md b/registry/kunstewi/README.md new file mode 100644 index 00000000..88588d2b --- /dev/null +++ b/registry/kunstewi/README.md @@ -0,0 +1,14 @@ +--- +display_name: Stewi +bio: I build and break things, probably i break things more. +avatar_url: ./.images/avatar.png +github: kunstewi +linkedin: "https://www.linkedin.com/in/kunstewi" +website: "https://kunstewi.tech" +support_email: kunstewi@gmail.com +status: "community" +--- + +# Stewi + +I build and break things, probably i break things more. \ No newline at end of file diff --git a/registry/kunstewi/modules/auto-dev-server/README.md b/registry/kunstewi/modules/auto-dev-server/README.md new file mode 100644 index 00000000..3b8d264f --- /dev/null +++ b/registry/kunstewi/modules/auto-dev-server/README.md @@ -0,0 +1,59 @@ +--- +display_name: Auto Development Server +description: Automatically detect and start development servers based on project detection +icon: ../../../../.icons/play.svg +verified: false +maintainer_github: kunstewi +tags: [development, automation, devcontainer] +--- + +# Auto Development Server + +Automatically detects and starts development servers for various project types when the workspace starts. Supports Node.js, Python, Ruby, Go, Rust, PHP projects, and integrates with devcontainer.json configuration. + +```tf +module "auto_dev_server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/kunstewi/auto-dev-server/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +## Supported Project Types + +- **Node.js**: Detects `package.json` and runs `npm start`, `npm run dev`, or `yarn start` +- **Python**: Detects Django (`manage.py`), Flask, or FastAPI projects +- **Ruby**: Detects Rails applications and Rack applications +- **Go**: Detects `go.mod` or `main.go` files +- **Rust**: Detects `Cargo.toml` files +- **PHP**: Detects `composer.json` or `index.php` files +- **Devcontainer**: Uses `postStartCommand` from `.devcontainer/devcontainer.json` + +## Examples + +### Basic Usage + +```tf +module "auto_dev_server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/kunstewi/auto-dev-server/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +### Custom Configuration + +```tf +module "auto_dev_server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/kunstewi/auto-dev-server/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + project_dir = "/workspace/projects" + port_range_start = 4000 + port_range_end = 8000 + log_level = "DEBUG" +} +``` \ No newline at end of file diff --git a/registry/kunstewi/modules/auto-dev-server/main.tf b/registry/kunstewi/modules/auto-dev-server/main.tf new file mode 100644 index 00000000..530176d8 --- /dev/null +++ b/registry/kunstewi/modules/auto-dev-server/main.tf @@ -0,0 +1,71 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + } +} + +variable "agent_id" { + description = "The ID of a Coder agent." + type = string +} + +variable "project_dir" { + description = "The directory to scan for projects" + type = string + default = "/home/coder" +} + +variable "auto_start" { + description = "Whether to automatically start development servers" + type = bool + default = true +} + +variable "port_range_start" { + description = "Starting port for development servers" + type = number + default = 3000 +} + +variable "port_range_end" { + description = "Ending port for development servers" + type = number + default = 9000 +} + +variable "log_level" { + description = "Log level for the auto-dev-server script" + 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" + } +} + +locals { + script_content = templatefile("${path.module}/scripts/auto-dev-server.sh", { + project_dir = var.project_dir + auto_start = var.auto_start + port_range_start = var.port_range_start + port_range_end = var.port_range_end + log_level = var.log_level + }) +} + +resource "coder_script" "auto_dev_server" { + agent_id = var.agent_id + display_name = "Auto Development Server" + icon = "/icon/play.svg" + script = local.script_content + run_on_start = var.auto_start + run_on_stop = false + timeout = 300 +} + +output "script_id" { + description = "The ID of the auto-dev-server script" + value = coder_script.auto_dev_server.id +} \ No newline at end of file diff --git a/registry/kunstewi/modules/auto-dev-server/run.sh b/registry/kunstewi/modules/auto-dev-server/run.sh new file mode 100755 index 00000000..a15fcf6c --- /dev/null +++ b/registry/kunstewi/modules/auto-dev-server/run.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env sh + +# Convert templated variables to shell variables +# shellcheck disable=SC2269 +LOG_PATH=${LOG_PATH} + +# shellcheck disable=SC2034 +BOLD='\033[0;1m' + +# shellcheck disable=SC2059 +printf "$${BOLD}Installing MODULE_NAME ...\n\n" + +# Add code here +# Use variables from the templatefile function in main.tf +# e.g. LOG_PATH, PORT, etc. + +printf "🥳 Installation complete!\n\n" + +printf "👷 Starting MODULE_NAME in background...\n\n" +# Start the app in here +# 1. Use & to run it in background +# 2. redirct stdout and stderr to log files + +./app > "$${LOG_PATH}" 2>&1 & + +printf "check logs at %s\n\n" "$${LOG_PATH}" diff --git a/registry/kunstewi/modules/auto-dev-server/scripts/auto-dev-servers.sh b/registry/kunstewi/modules/auto-dev-server/scripts/auto-dev-servers.sh new file mode 100644 index 00000000..c17f15bd --- /dev/null +++ b/registry/kunstewi/modules/auto-dev-server/scripts/auto-dev-servers.sh @@ -0,0 +1,289 @@ +#!/bin/bash + +set -euo pipefail + +# Configuration variables +PROJECT_DIR="${project_dir}" +AUTO_START="${auto_start}" +PORT_RANGE_START="${port_range_start}" +PORT_RANGE_END="${port_range_end}" +LOG_LEVEL="${log_level}" +LOG_FILE="$HOME/.auto-dev-server.log" +PID_DIR="$HOME/.auto-dev-server-pids" + +# Logging function +log() { + local level=$1 + shift + local message="$*" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE" +} + +# Create PID directory +mkdir -p "$PID_DIR" + +# Project detection functions +detect_nodejs() { + local dir=$1 + if [[ -f "$dir/package.json" ]]; then + local start_script=$(jq -r '.scripts.start // empty' "$dir/package.json" 2>/dev/null || echo "") + local dev_script=$(jq -r '.scripts.dev // empty' "$dir/package.json" 2>/dev/null || echo "") + + if [[ -n "$dev_script" ]]; then + echo "npm run dev" + elif [[ -n "$start_script" ]]; then + echo "npm start" + elif [[ -f "$dir/yarn.lock" ]]; then + echo "yarn start" + else + echo "npm start" + fi + fi +} + +detect_python() { + local dir=$1 + if [[ -f "$dir/requirements.txt" ]] || [[ -f "$dir/pyproject.toml" ]] || [[ -f "$dir/setup.py" ]]; then + if [[ -f "$dir/manage.py" ]]; then + echo "python manage.py runserver" + elif [[ -f "$dir/app.py" ]] || [[ -f "$dir/main.py" ]]; then + if command -v flask >/dev/null 2>&1; then + echo "flask run" + else + echo "python app.py" + fi + elif [[ -f "$dir/pyproject.toml" ]] && grep -q "fastapi" "$dir/pyproject.toml" 2>/dev/null; then + echo "uvicorn main:app --reload" + fi + fi +} + +detect_ruby() { + local dir=$1 + if [[ -f "$dir/Gemfile" ]]; then + if [[ -f "$dir/config.ru" ]]; then + echo "bundle exec rackup" + elif [[ -f "$dir/config/application.rb" ]]; then + echo "bundle exec rails server" + fi + fi +} + +detect_go() { + local dir=$1 + if [[ -f "$dir/go.mod" ]] || [[ -f "$dir/main.go" ]]; then + echo "go run ." + fi +} + +detect_rust() { + local dir=$1 + if [[ -f "$dir/Cargo.toml" ]]; then + echo "cargo run" + fi +} + +detect_php() { + local dir=$1 + if [[ -f "$dir/composer.json" ]] || [[ -f "$dir/index.php" ]]; then + echo "php -S localhost:8000" + fi +} + +detect_devcontainer() { + local dir=$1 + local devcontainer_file="" + + if [[ -f "$dir/.devcontainer/devcontainer.json" ]]; then + devcontainer_file="$dir/.devcontainer/devcontainer.json" + elif [[ -f "$dir/.devcontainer.json" ]]; then + devcontainer_file="$dir/.devcontainer.json" + fi + + if [[ -n "$devcontainer_file" ]]; then + # Extract postStartCommand from devcontainer.json + local post_start_cmd=$(jq -r '.postStartCommand // empty' "$devcontainer_file" 2>/dev/null || echo "") + if [[ -n "$post_start_cmd" ]]; then + echo "$post_start_cmd" + fi + fi +} + +# Find available port +find_available_port() { + local start_port=$PORT_RANGE_START + local end_port=$PORT_RANGE_END + + for ((port=start_port; port<=end_port; port++)); do + if ! ss -tuln | grep -q ":$port "; then + echo $port + return 0 + fi + done + + log "ERROR" "No available ports in range $start_port-$end_port" + return 1 +} + +# Start development server +start_dev_server() { + local dir=$1 + local command=$2 + local project_name=$(basename "$dir") + local port=$(find_available_port) + + if [[ -z "$port" ]]; then + log "ERROR" "Could not find available port for $project_name" + return 1 + fi + + log "INFO" "Starting development server for $project_name in $dir" + log "INFO" "Command: $command" + log "INFO" "Port: $port" + + cd "$dir" + + # Modify command to use specific port if possible + if [[ "$command" == *"npm"* ]] || [[ "$command" == *"yarn"* ]]; then + command="PORT=$port $command" + elif [[ "$command" == *"flask"* ]]; then + command="$command --port $port" + elif [[ "$command" == *"rails"* ]]; then + command="$command -p $port" + elif [[ "$command" == *"uvicorn"* ]]; then + command="$command --port $port" + fi + + # Start the server in background + nohup bash -c "$command" > "$HOME/.auto-dev-server-$project_name.log" 2>&1 & + local pid=$! + + # Save PID for cleanup + echo $pid > "$PID_DIR/$project_name.pid" + + log "INFO" "Started $project_name with PID $pid on port $port" + + # Create Coder app for the development server + if command -v coder >/dev/null 2>&1; then + coder apps create "$project_name-dev" \ + --url "http://localhost:$port" \ + --icon "/icon/code.svg" \ + --display-name "$project_name Development Server" || true + fi +} + +# Main detection and startup logic +scan_and_start_projects() { + log "INFO" "Scanning for projects in $PROJECT_DIR" + + # Find all potential project directories + find "$PROJECT_DIR" -maxdepth 3 -type f \( \ + -name "package.json" -o \ + -name "requirements.txt" -o \ + -name "pyproject.toml" -o \ + -name "Gemfile" -o \ + -name "go.mod" -o \ + -name "Cargo.toml" -o \ + -name "composer.json" -o \ + -name "devcontainer.json" -o \ + -name ".devcontainer.json" \ + \) | while read -r file; do + local project_dir=$(dirname "$file") + local project_name=$(basename "$project_dir") + + # Skip if already running + if [[ -f "$PID_DIR/$project_name.pid" ]] && kill -0 "$(cat "$PID_DIR/$project_name.pid")" 2>/dev/null; then + log "INFO" "Project $project_name is already running" + continue + fi + + log "INFO" "Found project: $project_name in $project_dir" + + # Try different detection methods + local command="" + + # Check devcontainer first + command=$(detect_devcontainer "$project_dir") + if [[ -z "$command" ]]; then + command=$(detect_nodejs "$project_dir") + fi + if [[ -z "$command" ]]; then + command=$(detect_python "$project_dir") + fi + if [[ -z "$command" ]]; then + command=$(detect_ruby "$project_dir") + fi + if [[ -z "$command" ]]; then + command=$(detect_go "$project_dir") + fi + if [[ -z "$command" ]]; then + command=$(detect_rust "$project_dir") + fi + if [[ -z "$command" ]]; then + command=$(detect_php "$project_dir") + fi + + if [[ -n "$command" ]]; then + start_dev_server "$project_dir" "$command" + else + log "WARN" "No suitable development server command found for $project_name" + fi + done +} + +# Cleanup function +cleanup_dead_processes() { + log "INFO" "Cleaning up dead processes" + + for pid_file in "$PID_DIR"/*.pid; do + if [[ -f "$pid_file" ]]; then + local pid=$(cat "$pid_file") + local project_name=$(basename "$pid_file" .pid) + + if ! kill -0 "$pid" 2>/dev/null; then + log "INFO" "Cleaning up dead process for $project_name (PID: $pid)" + rm -f "$pid_file" + fi + fi + done +} + +# Install required dependencies +install_dependencies() { + log "INFO" "Checking and installing dependencies" + + # Install jq if not available + if ! command -v jq >/dev/null 2>&1; then + log "INFO" "Installing jq" + 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 apk >/dev/null 2>&1; then + sudo apk add jq + fi + fi +} + +# Main execution +main() { + log "INFO" "Auto Development Server starting..." + log "INFO" "Project directory: $PROJECT_DIR" + log "INFO" "Auto start: $AUTO_START" + log "INFO" "Port range: $PORT_RANGE_START-$PORT_RANGE_END" + + install_dependencies + cleanup_dead_processes + + if [[ "$AUTO_START" == "true" ]]; then + scan_and_start_projects + else + log "INFO" "Auto start is disabled" + fi + + log "INFO" "Auto Development Server completed" +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/registry/kunstewi/modules/auto-dev-server/variables.tf b/registry/kunstewi/modules/auto-dev-server/variables.tf new file mode 100644 index 00000000..4b77db9f --- /dev/null +++ b/registry/kunstewi/modules/auto-dev-server/variables.tf @@ -0,0 +1,34 @@ +variable "agent_id" { + description = "The ID of a Coder agent." + type = string +} + +variable "project_dir" { + description = "The directory to scan for projects" + type = string + default = "/home/coder" +} + +variable "auto_start" { + description = "Whether to automatically start development servers" + type = bool + default = true +} + +variable "port_range_start" { + description = "Starting port for development servers" + type = number + default = 3000 +} + +variable "port_range_end" { + description = "Ending port for development servers" + type = number + default = 9000 +} + +variable "log_level" { + description = "Log level for the auto-dev-server script" + type = string + default = "INFO" +} \ No newline at end of file From 43942d1f36966f4c6ae66ee45ca3d34196be2c9b Mon Sep 17 00:00:00 2001 From: kunstewi Date: Sat, 12 Jul 2025 12:38:45 +0530 Subject: [PATCH 2/2] added the test --- .../modules/auto-dev-server/main.test.ts | 350 ++++++++++++++++++ .../kunstewi/modules/auto-dev-server/run.sh | 26 -- ...auto-dev-servers.sh => auto-dev-server.sh} | 0 3 files changed, 350 insertions(+), 26 deletions(-) create mode 100644 registry/kunstewi/modules/auto-dev-server/main.test.ts delete mode 100755 registry/kunstewi/modules/auto-dev-server/run.sh rename registry/kunstewi/modules/auto-dev-server/scripts/{auto-dev-servers.sh => auto-dev-server.sh} (100%) diff --git a/registry/kunstewi/modules/auto-dev-server/main.test.ts b/registry/kunstewi/modules/auto-dev-server/main.test.ts new file mode 100644 index 00000000..fd73889c --- /dev/null +++ b/registry/kunstewi/modules/auto-dev-server/main.test.ts @@ -0,0 +1,350 @@ +import { expect, it, describe } from "bun:test"; +import { + runTerraformApply, + runTerraformInit, + testRequiredVariables, + runContainer, + removeContainer, + execContainer, + writeFileContainer, + readFileContainer, + TerraformState, +} from "../../../../test/test"; + +const moduleDir = import.meta.dir; + +describe("auto-dev-server", () => { + // Test required variables + testRequiredVariables(moduleDir, { + agent_id: "test-agent-id", + }); + + // Test basic module functionality + it("should apply successfully with default values", async () => { + const state = await runTerraformApply(moduleDir, { + agent_id: "test-agent-id", + }); + + // Verify the script resource was created + const scriptResource = state.resources.find( + (r) => r.type === "coder_script" && r.name === "auto_dev_server" + ); + expect(scriptResource).toBeDefined(); + expect(scriptResource?.instances[0].attributes.agent_id).toBe("test-agent-id"); + expect(scriptResource?.instances[0].attributes.display_name).toBe("Auto Development Server"); + expect(scriptResource?.instances[0].attributes.run_on_start).toBe(true); + }); + + // Test custom configuration + it("should apply successfully with custom values", async () => { + const state = await runTerraformApply(moduleDir, { + agent_id: "test-agent-id", + project_dir: "/workspace/projects", + auto_start: false, + port_range_start: 4000, + port_range_end: 8000, + log_level: "DEBUG", + }); + + const scriptResource = state.resources.find( + (r) => r.type === "coder_script" && r.name === "auto_dev_server" + ); + expect(scriptResource).toBeDefined(); + + // Check that the script contains our custom values + const script = scriptResource?.instances[0].attributes.script as string; + expect(script).toContain('PROJECT_DIR="/workspace/projects"'); + expect(script).toContain('AUTO_START="false"'); + expect(script).toContain('PORT_RANGE_START="4000"'); + expect(script).toContain('PORT_RANGE_END="8000"'); + expect(script).toContain('LOG_LEVEL="DEBUG"'); + }); + + // Test script execution in container + it("should execute script and detect Node.js project", async () => { + const state = await runTerraformApply(moduleDir, { + agent_id: "test-agent-id", + project_dir: "/workspace", + auto_start: true, + }); + + const containerId = await runContainer("ubuntu:22.04"); + + try { + // Install dependencies + await execContainer(containerId, [ + "bash", "-c", + "apt-get update && apt-get install -y jq curl nodejs npm" + ]); + + // Create a test Node.js project + await writeFileContainer(containerId, "/workspace/package.json", JSON.stringify({ + name: "test-project", + scripts: { + start: "echo 'Server started' && sleep 10", + dev: "echo 'Dev server started' && sleep 10" + } + })); + + // Execute the auto-dev-server script + const scriptResource = state.resources.find( + (r) => r.type === "coder_script" && r.name === "auto_dev_server" + ); + const script = scriptResource?.instances[0].attributes.script as string; + + // Run the script + const result = await execContainer(containerId, ["bash", "-c", script]); + + // Check that the script executed without errors + expect(result.exitCode).toBe(0); + + // Wait a moment for processes to start + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Check if PID file was created + const pidFile = await readFileContainer(containerId, "/root/.auto-dev-server-pids/test-project.pid"); + expect(pidFile).toBeDefined(); + + // Check if log file was created + const logFile = await readFileContainer(containerId, "/root/.auto-dev-server.log"); + expect(logFile).toContain("Found project: test-project"); + expect(logFile).toContain("Starting development server for test-project"); + + } finally { + await removeContainer(containerId); + } + }); + + // Test Python project detection + it("should detect and start Python Flask project", async () => { + const state = await runTerraformApply(moduleDir, { + agent_id: "test-agent-id", + project_dir: "/workspace", + auto_start: true, + }); + + const containerId = await runContainer("python:3.9-slim"); + + try { + // Install dependencies + await execContainer(containerId, [ + "bash", "-c", + "apt-get update && apt-get install -y jq curl" + ]); + + // Create a test Flask project + await writeFileContainer(containerId, "/workspace/app.py", ` +from flask import Flask +app = Flask(__name__) + +@app.route('/') +def hello(): + return 'Hello, World!' + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) + `); + + await writeFileContainer(containerId, "/workspace/requirements.txt", "flask==2.0.1"); + + // Execute the auto-dev-server script + const scriptResource = state.resources.find( + (r) => r.type === "coder_script" && r.name === "auto_dev_server" + ); + const script = scriptResource?.instances[0].attributes.script as string; + + const result = await execContainer(containerId, ["bash", "-c", script]); + expect(result.exitCode).toBe(0); + + // Wait for processes to start + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Check if Flask process is running + const processes = await execContainer(containerId, ["ps", "aux"]); + expect(processes.stdout).toContain("flask"); + + } finally { + await removeContainer(containerId); + } + }); + + // Test devcontainer integration + it("should use devcontainer postStartCommand", async () => { + const state = await runTerraformApply(moduleDir, { + agent_id: "test-agent-id", + project_dir: "/workspace", + auto_start: true, + }); + + const containerId = await runContainer("ubuntu:22.04"); + + try { + // Install dependencies + await execContainer(containerId, [ + "bash", "-c", + "apt-get update && apt-get install -y jq" + ]); + + // Create devcontainer.json with postStartCommand + await writeFileContainer(containerId, "/workspace/.devcontainer/devcontainer.json", JSON.stringify({ + name: "test-devcontainer", + postStartCommand: "echo 'Custom post-start command executed' && sleep 5" + })); + + // Execute the auto-dev-server script + const scriptResource = state.resources.find( + (r) => r.type === "coder_script" && r.name === "auto_dev_server" + ); + const script = scriptResource?.instances[0].attributes.script as string; + + const result = await execContainer(containerId, ["bash", "-c", script]); + expect(result.exitCode).toBe(0); + + // Check if the custom command was executed + const logFile = await readFileContainer(containerId, "/root/.auto-dev-server.log"); + expect(logFile).toContain("Custom post-start command executed"); + + } finally { + await removeContainer(containerId); + } + }); + + // Test port allocation + it("should allocate different ports for multiple projects", async () => { + const state = await runTerraformApply(moduleDir, { + agent_id: "test-agent-id", + project_dir: "/workspace", + auto_start: true, + port_range_start: 3000, + port_range_end: 3010, + }); + + const containerId = await runContainer("node:16"); + + try { + // Create multiple Node.js projects + await writeFileContainer(containerId, "/workspace/project1/package.json", JSON.stringify({ + name: "project1", + scripts: { start: "echo 'Project 1 started' && sleep 10" } + })); + + await writeFileContainer(containerId, "/workspace/project2/package.json", JSON.stringify({ + name: "project2", + scripts: { start: "echo 'Project 2 started' && sleep 10" } + })); + + // Execute the auto-dev-server script + const scriptResource = state.resources.find( + (r) => r.type === "coder_script" && r.name === "auto_dev_server" + ); + const script = scriptResource?.instances[0].attributes.script as string; + + const result = await execContainer(containerId, ["bash", "-c", script]); + expect(result.exitCode).toBe(0); + + // Wait for processes to start + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Check that both projects got different ports + const logFile = await readFileContainer(containerId, "/root/.auto-dev-server.log"); + expect(logFile).toContain("project1"); + expect(logFile).toContain("project2"); + + // Check that different ports were allocated + const portMatches = logFile.match(/Port: (\d+)/g); + expect(portMatches).toBeDefined(); + expect(portMatches!.length).toBeGreaterThan(1); + + // Verify ports are different + const ports = portMatches!.map(match => parseInt(match.split(": ")[1])); + const uniquePorts = new Set(ports); + expect(uniquePorts.size).toBeGreaterThan(1); + + } finally { + await removeContainer(containerId); + } + }); + + // Test cleanup functionality + it("should cleanup dead processes", async () => { + const state = await runTerraformApply(moduleDir, { + agent_id: "test-agent-id", + project_dir: "/workspace", + auto_start: true, + }); + + const containerId = await runContainer("ubuntu:22.04"); + + try { + // Install dependencies + await execContainer(containerId, [ + "bash", "-c", + "apt-get update && apt-get install -y jq" + ]); + + // Create a PID file for a non-existent process + await execContainer(containerId, [ + "bash", "-c", + "mkdir -p /root/.auto-dev-server-pids && echo 99999 > /root/.auto-dev-server-pids/test-project.pid" + ]); + + // Execute the auto-dev-server script + const scriptResource = state.resources.find( + (r) => r.type === "coder_script" && r.name === "auto_dev_server" + ); + const script = scriptResource?.instances[0].attributes.script as string; + + const result = await execContainer(containerId, ["bash", "-c", script]); + expect(result.exitCode).toBe(0); + + // Check that the dead process PID file was cleaned up + const logFile = await readFileContainer(containerId, "/root/.auto-dev-server.log"); + expect(logFile).toContain("Cleaning up dead process for test-project"); + + // Verify PID file was removed + const pidFileExists = await execContainer(containerId, [ + "test", "-f", "/root/.auto-dev-server-pids/test-project.pid" + ]); + expect(pidFileExists.exitCode).toBe(1); // File should not exist + + } finally { + await removeContainer(containerId); + } + }); + + // Test log level configuration + it("should respect log level configuration", async () => { + const state = await runTerraformApply(moduleDir, { + agent_id: "test-agent-id", + project_dir: "/workspace", + auto_start: false, // Don't start servers, just test logging + log_level: "DEBUG", + }); + + const containerId = await runContainer("ubuntu:22.04"); + + try { + // Install dependencies + await execContainer(containerId, [ + "bash", "-c", + "apt-get update && apt-get install -y jq" + ]); + + // Execute the auto-dev-server script + const scriptResource = state.resources.find( + (r) => r.type === "coder_script" && r.name === "auto_dev_server" + ); + const script = scriptResource?.instances[0].attributes.script as string; + + const result = await execContainer(containerId, ["bash", "-c", script]); + expect(result.exitCode).toBe(0); + + // Check that DEBUG level logging is working + const logFile = await readFileContainer(containerId, "/root/.auto-dev-server.log"); + expect(logFile).toContain("[DEBUG]"); + + } finally { + await removeContainer(containerId); + } + }); +}); diff --git a/registry/kunstewi/modules/auto-dev-server/run.sh b/registry/kunstewi/modules/auto-dev-server/run.sh deleted file mode 100755 index a15fcf6c..00000000 --- a/registry/kunstewi/modules/auto-dev-server/run.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env sh - -# Convert templated variables to shell variables -# shellcheck disable=SC2269 -LOG_PATH=${LOG_PATH} - -# shellcheck disable=SC2034 -BOLD='\033[0;1m' - -# shellcheck disable=SC2059 -printf "$${BOLD}Installing MODULE_NAME ...\n\n" - -# Add code here -# Use variables from the templatefile function in main.tf -# e.g. LOG_PATH, PORT, etc. - -printf "🥳 Installation complete!\n\n" - -printf "👷 Starting MODULE_NAME in background...\n\n" -# Start the app in here -# 1. Use & to run it in background -# 2. redirct stdout and stderr to log files - -./app > "$${LOG_PATH}" 2>&1 & - -printf "check logs at %s\n\n" "$${LOG_PATH}" diff --git a/registry/kunstewi/modules/auto-dev-server/scripts/auto-dev-servers.sh b/registry/kunstewi/modules/auto-dev-server/scripts/auto-dev-server.sh similarity index 100% rename from registry/kunstewi/modules/auto-dev-server/scripts/auto-dev-servers.sh rename to registry/kunstewi/modules/auto-dev-server/scripts/auto-dev-server.sh