diff --git a/.gitignore b/.gitignore index 157c642aa..55947fc5b 100644 --- a/.gitignore +++ b/.gitignore @@ -145,3 +145,6 @@ dist # Generated credentials from google-github-actions/auth gha-creds-*.json + +# IDEs +.idea diff --git a/.icons/tmux.svg b/.icons/tmux.svg new file mode 100644 index 000000000..ac0174ed0 --- /dev/null +++ b/.icons/tmux.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/registry/anomaly/.images/avatar.jpeg b/registry/anomaly/.images/avatar.jpeg new file mode 100644 index 000000000..ca1072d8d Binary files /dev/null and b/registry/anomaly/.images/avatar.jpeg differ diff --git a/registry/anomaly/README.md b/registry/anomaly/README.md new file mode 100644 index 000000000..a9f074d2d --- /dev/null +++ b/registry/anomaly/README.md @@ -0,0 +1,13 @@ +--- +display_name: "Jay Kumar" +bio: "I'm a Software Engineer :)" +avatar_url: "./.images/avatar.png" +github: "35C4n0r" +linkedin: "https://www.linkedin.com/in/jaykum4r" +support_email: "work.jaykumar@gmail.com" +status: "community" +--- + +# Your Name + +I'm a Software Engineer :) diff --git a/registry/anomaly/modules/tmux/README.md b/registry/anomaly/modules/tmux/README.md new file mode 100644 index 000000000..e0bdd5f22 --- /dev/null +++ b/registry/anomaly/modules/tmux/README.md @@ -0,0 +1,101 @@ +--- +display_name: "Tmux" +description: "Tmux for coder agent :)" +icon: "../../../../.icons/tmux.svg" +verified: false +tags: ["tmux", "terminal", "persistent"] +--- + +# tmux + +This module provisions and configures [tmux](https://github.com/tmux/tmux) with session persistence and plugin support +for a Coder agent. It automatically installs tmux, the Tmux Plugin Manager (TPM), and a set of useful plugins, and sets +up a default or custom tmux configuration with session save/restore capabilities. + +```tf +module "tmux" { + source = "registry.coder.com/anomaly/tmux/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +## Features + +- Installs tmux if not already present +- Installs TPM (Tmux Plugin Manager) +- Configures tmux with plugins for sensible defaults, session persistence, and automation: + - `tmux-plugins/tpm` + - `tmux-plugins/tmux-sensible` + - `tmux-plugins/tmux-resurrect` + - `tmux-plugins/tmux-continuum` +- Supports custom tmux configuration +- Enables automatic session save +- Configurable save interval +- **Supports multiple named tmux sessions, each as a separate app in the Coder UI** + +## Usage + +```tf +module "tmux" { + source = "registry.coder.com/anomaly/tmux/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + tmux_config = "" # Optional: custom tmux.conf content + save_interval = 1 # Optional: save interval in minutes + sessions = ["default", "dev", "ops"] # Optional: list of tmux sessions + order = 1 # Optional: UI order + group = "Terminal" # Optional: UI group + icon = "/icon/tmux.svg" # Optional: app icon +} +``` + +## Multi-Session Support + +This module can provision multiple tmux sessions, each as a separate app in the Coder UI. Use the `sessions` variable to specify a list of session names. For each session, a `coder_app` is created, allowing you to launch or attach to that session directly from the UI. + +- **sessions**: List of tmux session names (default: `["default"]`). + +## How It Works + +- **tmux Installation:** + - Checks if tmux is installed; if not, installs it using the system's package manager (supports apt, yum, dnf, + zypper, apk, brew). +- **TPM Installation:** + - Installs the Tmux Plugin Manager (TPM) to `~/.tmux/plugins/tpm` if not already present. +- **tmux Configuration:** + - If `tmux_config` is provided, writes it to `~/.tmux.conf`. + - Otherwise, generates a default configuration with plugin support and session persistence (using tmux-resurrect and + tmux-continuum). + - Sets up key bindings for quick session save (`Ctrl+s`) and restore (`Ctrl+r`). +- **Plugin Installation:** + - Installs plugins via TPM. +- **Session Persistence:** + - Enables automatic session save/restore at the configured interval. + +## Example + +```tf +module "tmux" { + source = "registry.coder.com/anomaly/tmux/coder" + version = "1.0.0" + agent_id = var.agent_id + sessions = ["default", "dev", "anomaly"] + tmux_config = <<-EOT + set -g mouse on + set -g history-limit 10000 + EOT + group = "Terminal" + order = 2 +} +``` + +> [!IMPORTANT] +> +> - If you provide a custom `tmux_config`, it will completely replace the default configuration. Ensure you include plugin +> and TPM initialization lines if you want plugin support and session persistence. +> - The script will attempt to install dependencies using `sudo` where required. +> - If `git` is not installed, TPM installation will fail. +> - If you are using custom config, you'll be responsible for setting up persistence and plugins. +> - The `order`, `group`, and `icon` variables allow you to customize how tmux apps appear in the Coder UI. +> - In case of session restart or shh reconnection, the tmux session will be automatically restored :) diff --git a/registry/anomaly/modules/tmux/main.test.ts b/registry/anomaly/modules/tmux/main.test.ts new file mode 100644 index 000000000..802147dbc --- /dev/null +++ b/registry/anomaly/modules/tmux/main.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "bun:test"; +import { + runTerraformApply, + runTerraformInit, + testRequiredVariables, + findResourceInstance, +} from "~test"; +import path from "path"; + +const moduleDir = path.resolve(__dirname); + +const requiredVars = { + agent_id: "dummy-agent-id", +}; + +describe("tmux module", async () => { + await runTerraformInit(moduleDir); + + // 1. Required variables + testRequiredVariables(moduleDir, requiredVars); + + // 2. coder_script resource is created + it("creates coder_script resource", async () => { + const state = await runTerraformApply(moduleDir, requiredVars); + const scriptResource = findResourceInstance(state, "coder_script"); + expect(scriptResource).toBeDefined(); + expect(scriptResource.agent_id).toBe(requiredVars.agent_id); + + // check that the script contains expected lines + expect(scriptResource.script).toContain("Installing tmux"); + expect(scriptResource.script).toContain("Installing Tmux Plugin Manager (TPM)"); + expect(scriptResource.script).toContain("tmux configuration created at"); + expect(scriptResource.script).toContain("✅ tmux setup complete!"); + }); +}); diff --git a/registry/anomaly/modules/tmux/main.tf b/registry/anomaly/modules/tmux/main.tf new file mode 100644 index 000000000..36f8471fa --- /dev/null +++ b/registry/anomaly/modules/tmux/main.tf @@ -0,0 +1,78 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "tmux_config" { + type = string + description = "Custom tmux configuration to apply." + default = "" +} + +variable "save_interval" { + type = number + description = "Save interval (in minutes)." + default = 1 +} + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} + +variable "group" { + type = string + description = "The name of a group that this app belongs to." + default = null +} + +variable "icon" { + type = string + description = "The icon to use for the app." + default = "/icon/tmux.svg" +} + +variable "sessions" { + type = list(string) + description = "List of tmux sessions to create or start." + default = ["default"] +} + +resource "coder_script" "tmux" { + agent_id = var.agent_id + display_name = "tmux" + icon = "/icon/terminal.svg" + script = templatefile("${path.module}/scripts/run.sh", { + TMUX_CONFIG = var.tmux_config + SAVE_INTERVAL = var.save_interval + }) + run_on_start = true + run_on_stop = false +} + +resource "coder_app" "tmux_sessions" { + for_each = toset(var.sessions) + + agent_id = var.agent_id + slug = "tmux-${each.value}" + display_name = "tmux - ${each.value}" + icon = var.icon + order = var.order + group = var.group + + command = templatefile("${path.module}/scripts/start.sh", { + SESSION_NAME = each.value + }) +} diff --git a/registry/anomaly/modules/tmux/scripts/run.sh b/registry/anomaly/modules/tmux/scripts/run.sh new file mode 100755 index 000000000..90c0d84b9 --- /dev/null +++ b/registry/anomaly/modules/tmux/scripts/run.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash + +BOLD='\033[0;1m' + +# Convert templated variables to shell variables +SAVE_INTERVAL="${SAVE_INTERVAL}" +TMUX_CONFIG="${TMUX_CONFIG}" + +# Function to install tmux +install_tmux() { + printf "Checking for tmux installation\n" + + if command -v tmux &> /dev/null; then + printf "tmux is already installed \n\n" + return 0 + fi + + printf "Installing tmux \n\n" + + # Detect package manager and install tmux + if command -v apt-get &> /dev/null; then + sudo apt-get update + sudo apt-get install -y tmux + elif command -v yum &> /dev/null; then + sudo yum install -y tmux + elif command -v dnf &> /dev/null; then + sudo dnf install -y tmux + elif command -v zypper &> /dev/null; then + sudo zypper install -y tmux + elif command -v apk &> /dev/null; then + sudo apk add tmux + elif command -v brew &> /dev/null; then + brew install tmux + else + printf "No supported package manager found. Please install tmux manually. \n" + exit 1 + fi + + printf "tmux installed successfully \n" +} + +# Function to install Tmux Plugin Manager (TPM) +install_tpm() { + local tpm_dir="$HOME/.tmux/plugins/tpm" + + if [ -d "$tpm_dir" ]; then + printf "TPM is already installed" + return 0 + fi + + printf "Installing Tmux Plugin Manager (TPM) \n" + + # Create plugins directory + mkdir -p "$HOME/.tmux/plugins" + + # Clone TPM repository + if command -v git &> /dev/null; then + git clone https://github.com/tmux-plugins/tpm "$tpm_dir" + printf "TPM installed successfully" + else + printf "Git is not installed. Please install git to use tmux plugins. \n" + exit 1 + fi +} + +# Function to create tmux configuration +setup_tmux_config() { + printf "Setting up tmux configuration \n" + + local config_dir="$HOME/.tmux" + local config_file="$HOME/.tmux.conf" + + mkdir -p "$config_dir" + + if [ -n "$TMUX_CONFIG" ]; then + printf "$TMUX_CONFIG" > "$config_file" + printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n" + else + cat > "$config_file" << EOF +# Tmux Configuration File + +# ============================================================================= +# PLUGIN CONFIGURATION +# ============================================================================= + +# List of plugins +set -g @plugin 'tmux-plugins/tpm' +set -g @plugin 'tmux-plugins/tmux-sensible' +set -g @plugin 'tmux-plugins/tmux-resurrect' +set -g @plugin 'tmux-plugins/tmux-continuum' + +# tmux-continuum configuration +set -g @continuum-restore 'on' +set -g @continuum-save-interval '$${SAVE_INTERVAL}' +set -g @continuum-boot 'on' +set -g status-right 'Continuum status: #{continuum_status}' + +# ============================================================================= +# KEY BINDINGS FOR SESSION MANAGEMENT +# ============================================================================= + +# Quick session save and restore +bind C-s run-shell "~/.tmux/plugins/tmux-resurrect/scripts/save.sh" +bind C-r run-shell "~/.tmux/plugins/tmux-resurrect/scripts/restore.sh" + +# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf) +run '~/.tmux/plugins/tpm/tpm' +EOF + printf "tmux configuration created at {$config_file} \n\n" + fi +} + +# Function to install tmux plugins +install_plugins() { + printf "Installing tmux plugins" + + # Check if TPM is installed + if [ ! -d "$HOME/.tmux/plugins/tpm" ]; then + printf "TPM is not installed. Cannot install plugins. \n" + return 1 + fi + + # Install plugins using TPM + "$HOME/.tmux/plugins/tpm/bin/install_plugins" + + printf "tmux plugins installed successfully \n" +} + +# Main execution +main() { + printf "$${BOLD} 🛠️Setting up tmux with session persistence! \n\n" + printf "" + + # Install dependencies + install_tmux + install_tpm + + # Setup tmux configuration + setup_tmux_config + + # Install plugins + install_plugins + + printf "$${BOLD}✅ tmux setup complete! \n\n" + + printf "$${BOLD} Attempting to restore sessions\n" + tmux new-session -d \; source-file ~/.tmux.conf \; run-shell '~/.tmux/plugins/tmux-resurrect/scripts/restore.sh' + printf "$${BOLD} Sessions restored: -> %s\n" "$(tmux ls)" + +} + +# Run main function +main \ No newline at end of file diff --git a/registry/anomaly/modules/tmux/scripts/start.sh b/registry/anomaly/modules/tmux/scripts/start.sh new file mode 100755 index 000000000..4638c8f7d --- /dev/null +++ b/registry/anomaly/modules/tmux/scripts/start.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Convert templated variables to shell variables +SESSION_NAME='${SESSION_NAME}' + +# Function to check if tmux is installed +check_tmux() { + if ! command -v tmux &> /dev/null; then + echo "tmux is not installed. Please run the tmux setup script first." + exit 1 + fi +} + +# Function to handle a single session +handle_session() { + local session_name="$1" + + # Check if the session exists + if tmux has-session -t "$session_name" 2>/dev/null; then + echo "Session '$session_name' exists, attaching to it..." + tmux attach-session -t "$session_name" + else + echo "Session '$session_name' does not exist, creating it..." + tmux new-session -d -s "$session_name" + tmux attach-session -t "$session_name" + fi +} + +# Main function +main() { + # Check if tmux is installed + check_tmux + handle_session "${SESSION_NAME}" +} + +# Run the main function +main