mirror of
https://github.com/coder/registry.git
synced 2026-06-03 04:58:15 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e42a145fa | |||
| 0ff3dbcc48 | |||
| a327e79bc4 | |||
| bc39c2ee29 | |||
| e3ff43c0a6 | |||
| 30123e7ea3 | |||
| f7c1be71f7 | |||
| 19519a0a13 | |||
| 63e42283ce | |||
| 0c5a8a2354 | |||
| 51ec6e3212 |
@@ -6,6 +6,7 @@ HashiCorp = "HashiCorp"
|
||||
mavrickrishi = "mavrickrishi" # Username
|
||||
mavrick = "mavrick" # Username
|
||||
inh = "inh" # Option in setpriv command
|
||||
exportfs = "exportfs" # nfs related binary
|
||||
|
||||
[files]
|
||||
extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive
|
||||
@@ -0,0 +1,210 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="300mm"
|
||||
height="207mm"
|
||||
viewBox="0 0 300 207"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<title
|
||||
id="title1">copyparty_logo</title>
|
||||
<defs
|
||||
id="defs1">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient1">
|
||||
<stop
|
||||
style="stop-color:#ffcc55;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop1" />
|
||||
<stop
|
||||
style="stop-color:#ffcc00;stop-opacity:1"
|
||||
offset="0.2"
|
||||
id="stop2" />
|
||||
<stop
|
||||
style="stop-color:#ff8800;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1"
|
||||
id="linearGradient2"
|
||||
x1="15"
|
||||
y1="15"
|
||||
x2="15"
|
||||
y2="143"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>copyparty_logo</dc:title>
|
||||
<dc:source>github.com/9001/copyparty</dc:source>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
inkscape:label="kassett">
|
||||
<rect
|
||||
style="fill:#333333"
|
||||
id="rect1"
|
||||
width="300"
|
||||
height="205"
|
||||
x="0"
|
||||
y="0"
|
||||
rx="12"
|
||||
ry="12" />
|
||||
<rect
|
||||
style="fill:url(#linearGradient2)"
|
||||
id="rect2"
|
||||
width="270"
|
||||
height="128"
|
||||
x="15"
|
||||
y="15"
|
||||
rx="8"
|
||||
ry="8" />
|
||||
<rect
|
||||
style="fill:#333333"
|
||||
id="rect3"
|
||||
width="172"
|
||||
height="52"
|
||||
x="64"
|
||||
y="72"
|
||||
rx="26"
|
||||
ry="26" />
|
||||
<circle
|
||||
style="fill:#cccccc"
|
||||
id="circle1"
|
||||
cx="91"
|
||||
cy="98"
|
||||
r="18" />
|
||||
<circle
|
||||
style="fill:#cccccc"
|
||||
id="circle2"
|
||||
cx="209"
|
||||
cy="98"
|
||||
r="18" />
|
||||
<path
|
||||
style="fill:#737373;stroke-width:1px"
|
||||
d="m 48,207 10,-39 c 1.79,-6.2 5.6,-7.8 12,-8 60,-1 100,-1 160,0 6.4,0.2 10,1.8 12,8 l 10,39 z"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="ccccccc" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="tekst"
|
||||
style="display:none">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:38.8056px;line-height:1.25;font-family:Akbar;-inkscape-font-specification:Akbar;letter-spacing:3.70417px;word-spacing:0px;fill:#333333"
|
||||
x="47.153069"
|
||||
y="55.548954"
|
||||
id="text1"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan1"
|
||||
x="47.153069"
|
||||
y="55.548954"
|
||||
style="-inkscape-font-specification:Akbar"
|
||||
rotate="0 0">copyparty</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="stensatt">
|
||||
<path
|
||||
d="m 63.5,50.9 q -0.85,0.93 -4.73,2.3 -3.6,1.3 -4.4,1.3 -3.3,0 -5.1,-2.1 -1.75,-2 -1.75,-5.36 0,-4.6 3.76,-7.64 3.3,-2.7 7.3,-2.7 0.4,0 0.93,0.74 0.54,0.7 0.54,1.16 0,2.06 -2.2,2.7 -1.36,0.4 -4.04,1.16 -2.2,1.16 -2.2,4.4 0,3.2 2.9,3.2 0.85,0 0.85,0 0.54,0 1.44,-0.16 1.1,-0.23 2.9,-0.74 1.8,-0.54 2.13,-0.54 0.4,0 1.75,0.6 z"
|
||||
style="fill:#333333"
|
||||
id="path11" />
|
||||
<path
|
||||
d="m 87.6,45 q 0,4.2 -3.7,6.95 -3.2,2.3 -6.87,2.3 -3.4,0 -6,-2.6 -2.5,-2.6 -2.5,-6 0,-3.6 3.14,-6.64 3.2,-3 6.8,-3 3.5,0 6.3,2.76 2.83,2.76 2.83,6.25 z m -3.4,0.16 q 0,-2.25 -1.75,-3.7 -1.7,-1.5 -4,-1.5 -0.1,0 -1.6,1.6 -1.44,1.55 -2.44,1.55 -0.6,0 -0.8,-0.3 -1.16,2.3 -1.16,3 0,2.25 2.13,3.4 1.6,0.9 3.6,0.9 2,0 3.76,-1.1 2.25,-1.4 2.25,-3.84 z"
|
||||
style="fill:#333333"
|
||||
id="path12" />
|
||||
<path
|
||||
d="m 112.8,46.8 q 0,2.8 -1.9,4.4 -1.8,1.5 -4.7,1.5 -0.7,0 -2.7,-0.4 -1.9,-0.4 -2.6,-0.4 -2.1,0 -2.1,2.64 0,0.85 0.23,2.6 0.2,1.75 0.2,2.6 0,1.9 -0.77,2.83 -1.44,0 -3,-0.85 -1.46,-9.5 -1.46,-12 0,-3.65 1.75,-8.1 2.37,-6.05 6.45,-6.05 3.7,0 7.3,4.1 3.3,3.84 3.3,7.14 z m -3.8,0.2 q -0.6,-2.2 -2.6,-4.4 -2.3,-2.5 -4.3,-2.5 -1.3,0 -2.33,2.2 -0.9,1.8 -0.9,3.26 0,0.47 0.38,1.24 0.43,0.8 0.85,0.8 1.1,0 3.2,0.3 2.1,0.3 3.2,0.3 0.3,0 1.3,-0.4 1,-0.47 1.3,-0.74 z"
|
||||
style="fill:#333333"
|
||||
id="path13" />
|
||||
<path
|
||||
d="m 133,40 q -2.1,4.1 -3.2,7 -0.1,0.3 -1.6,4.5 -0.4,1.36 -1,4.2 -0.5,2.83 -1,4.2 -1,2.83 -2.3,2.64 -1.4,-0.2 -1.6,-1.6 0,-0.2 0,-0.5 0,-0.16 0.3,-1.5 1,-5.04 1,-6.44 0,-0.54 -0.1,-0.74 -1.4,-2.44 -4.1,-7.4 -2.7,-4.97 -2.4,-7.7 1.5,-1.36 2.1,-1.36 0.4,0 1.1,0.6 0.6,0.6 0.7,1.1 0.8,6.2 4.9,11.1 1,-1.8 1.8,-4.04 0.5,-1.4 1.6,-4.15 1.9,-4.46 3.4,-4.46 0.2,0 0.4,0.1 0.9,0.3 1.3,2.8 z"
|
||||
style="fill:#333333"
|
||||
id="path14" />
|
||||
<path
|
||||
d="m 157.5,48 q 0,2.8 -1.9,4.4 -1.8,1.5 -4.7,1.5 -0.7,0 -2.7,-0.4 -1.9,-0.4 -2.6,-0.4 -2,0 -2,2.64 0,0.85 0.2,2.6 0.2,1.75 0.2,2.6 0,1.9 -0.7,2.83 -1.5,0 -3,-0.85 -1.5,-9.5 -1.5,-11.95 0,-3.65 1.8,-8.1 2.3,-6.05 6.4,-6.05 3.7,0 7.2,4.1 3.3,3.84 3.3,7.14 z m -3.8,0.2 q -0.6,-2.2 -2.6,-4.4 -2.3,-2.5 -4.3,-2.5 -1.3,0 -2.3,2.2 -0.9,1.8 -0.9,3.26 0,0.47 0.4,1.24 0.4,0.8 0.8,0.8 1.1,0 3.2,0.3 2.1,0.3 3.2,0.3 0.3,0 1.3,-0.4 1,-0.47 1.3,-0.74 z"
|
||||
style="fill:#333333"
|
||||
id="path15" />
|
||||
<path
|
||||
d="m 182,53.3 q 0,0.9 -0.6,1.5 -0.6,0.6 -1.4,0.6 -1.6,0 -3,-0.9 -1.4,-0.93 -2.1,-2.3 -0.7,-0.1 -1.5,0.85 -0.9,1.16 -1.1,1.24 -1.2,0.54 -3.9,0.54 -2.2,0 -3.9,-2.44 -1.5,-2.13 -1.5,-4 0,-3.4 3.4,-6.4 3.2,-2.9 6.7,-2.9 0.9,0 1.7,0.6 0.8,0.6 0.8,1.44 0,0.54 -0.4,1.1 2.4,0.9 2.4,2.83 0,0.35 -0.1,1.05 -0.1,0.7 -0.1,1.05 0,0.4 0.1,0.6 0.5,1.3 2.5,3.4 1.9,1.9 1.9,2.2 z m -8.1,-10.1 q -0.4,0 -1.1,-0.1 -0.8,-0.16 -1.1,-0.16 -1.3,0 -3.2,1.94 -1.9,1.94 -1.9,3.3 0,0.8 0.7,1.8 0.9,1.3 2.2,1.3 2.6,0 3.5,-2.9 0.5,-2.6 1,-5.16 z"
|
||||
style="fill:#333333"
|
||||
id="path16" />
|
||||
<path
|
||||
d="m 203.8,42.4 q -0.4,0.4 -1.5,0.4 -0.9,0 -2.5,-0.3 -1.7,-0.3 -2.5,-0.3 -4.7,0 -5.5,6.9 -0.3,3.1 -0.4,3.3 -0.4,1 -1.7,2.3 h -1.1 q -0.7,-1.2 -1.3,-4.1 -0.6,-2.76 -0.6,-4.27 0,-1.16 0.1,-1.5 0.2,-0.54 1,-0.54 0.3,0 0.6,0.3 0.4,0.3 0.4,0.3 1.9,-3.53 3.1,-4.6 1.8,-1.7 5.1,-1.7 1.4,0 3.6,0.9 2.8,1.16 3.3,2.8 z"
|
||||
style="fill:#333333"
|
||||
id="path17" />
|
||||
<path
|
||||
d="m 229.5,37.16 q 0.3,0.8 0.3,1.44 0,1.86 -2.4,1.86 -1,0 -3.5,-0.5 -2.5,-0.54 -3.4,-0.54 -1.3,0 -1.5,0.1 -0.4,0.2 -0.4,1.2 0,2.2 0.6,6.9 0.7,5.86 1.6,6.13 -0.4,0.35 -0.4,1.1 -1.2,0.7 -2.6,0.7 -1.4,0 -2,-3.9 -0.2,-1.36 -0.5,-7.76 -0.2,-4.6 -0.8,-5.5 -0.3,-0.47 -4.3,-0.35 -1,0 -1.6,0.1 -0.5,0 -0.3,0 -0.8,0 -1.2,-0.7 -0.5,-1.3 -0.5,-1.4 0,-1.44 4.1,-2 1.6,-0.16 4.7,-0.5 0,-0.85 -0.1,-2.56 0,-1.75 0,-2.6 0,-4.35 2.1,-4.35 0.5,0 1.1,0.6 0.6,0.6 0.6,1.1 v 7.9 q 1.1,1.2 5,1.7 3.9,0.5 5.3,1.86 z"
|
||||
style="fill:#333333"
|
||||
id="path18" />
|
||||
<path
|
||||
d="m 251.2,40.2 q -2,4.1 -3.2,7 -0.1,0.3 -1.5,4.5 -0.5,1.36 -1,4.2 -0.5,2.83 -1,4.2 -1,2.83 -2.4,2.64 -1.4,-0.2 -1.5,-1.6 -0.1,-0.2 -0.1,-0.5 0,-0.16 0.3,-1.5 1.1,-5.04 1.1,-6.44 0,-0.54 -0.1,-0.74 -1.4,-2.44 -4.1,-7.4 -2.7,-4.97 -2.4,-7.7 1.4,-1.36 2.1,-1.36 0.4,0 1,0.6 0.6,0.6 0.7,1.1 0.9,6.2 4.9,11.1 1,-1.8 1.9,-4.04 0.5,-1.4 1.6,-4.15 1.8,-4.46 3.4,-4.46 0.2,0 0.4,0.1 0.8,0.3 1.2,2.8 z"
|
||||
style="fill:#333333"
|
||||
id="path19" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer5"
|
||||
inkscape:label="tagger">
|
||||
<g
|
||||
id="g1">
|
||||
<path
|
||||
id="path4"
|
||||
style="fill:#333333"
|
||||
d="m 111.4,83.335 -9.526,5.5 2.5,4.33 9.526,-5.5 z m -33.775,19.5 -9.526,5.5 2.5,4.33 9.526,-5.5 z"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
<path
|
||||
id="path5"
|
||||
style="fill:#333333"
|
||||
d="M 88.5,73 V 84 h 5 V 73 Z m 0,39 v 11 h 5 V 112 Z"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
<path
|
||||
id="path6"
|
||||
style="fill:#333333"
|
||||
d="m 68.1,87.665 9.526,5.5 2.5,-4.33 -9.526,-5.5 z m 33.775,19.5 9.527,5.5 2.5,-4.33 -9.527,-5.5 z"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
</g>
|
||||
<g
|
||||
id="g2"
|
||||
transform="rotate(30,150,318.19)">
|
||||
<path
|
||||
id="path7"
|
||||
style="fill:#333333"
|
||||
d="m 111.4,83.335 -9.526,5.5 2.5,4.33 9.526,-5.5 z m -33.775,19.5 -9.526,5.5 2.5,4.33 9.526,-5.5 z"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
<path
|
||||
id="path8"
|
||||
style="fill:#333333"
|
||||
d="M 88.5,73 V 84 h 5 V 73 Z m 0,39 v 11 h 5 V 112 Z"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
<path
|
||||
id="path9"
|
||||
style="fill:#333333"
|
||||
d="m 68.1,87.665 9.526,5.5 2.5,-4.33 -9.526,-5.5 z m 33.775,19.5 9.527,5.5 2.5,-4.33 -9.527,-5.5 z"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
|
||||
```tf
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,8 +239,6 @@ resource "coder_app" "agentapi_cli" {
|
||||
group = var.cli_app_group
|
||||
}
|
||||
|
||||
resource "coder_ai_task" "agentapi" {
|
||||
sidebar_app {
|
||||
id = coder_app.agentapi_web.id
|
||||
}
|
||||
output "task_app_id" {
|
||||
value = coder_app.agentapi_web.id
|
||||
}
|
||||
|
||||
@@ -8,76 +8,58 @@ tags: [agent, ai, aider]
|
||||
|
||||
# Aider
|
||||
|
||||
Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider and provides a persistent session using screen or tmux.
|
||||
Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider with AgentAPI for seamless Coder Tasks Support.
|
||||
|
||||
```tf
|
||||
module "aider" {
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Interactive Parameter Selection**: Choose your AI provider, model, and configuration options when creating the workspace
|
||||
- **Multiple AI Providers**: Supports Anthropic (Claude), OpenAI, DeepSeek, GROQ, and OpenRouter
|
||||
- **Persistent Sessions**: Uses screen (default) or tmux to keep Aider running in the background
|
||||
- **Optional Dependencies**: Install Playwright for web page scraping and PortAudio for voice coding
|
||||
- **Project Integration**: Works with any project directory, including Git repositories
|
||||
- **Browser UI**: Use Aider in your browser with a modern web interface instead of the terminal
|
||||
- **Non-Interactive Mode**: Automatically processes tasks when provided via the `task_prompt` variable
|
||||
|
||||
## Module Parameters
|
||||
|
||||
> [!NOTE]
|
||||
> The `use_screen` and `use_tmux` parameters cannot both be enabled at the same time. By default, `use_screen` is set to `true` and `use_tmux` is set to `false`.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic setup with API key
|
||||
|
||||
```tf
|
||||
variable "anthropic_api_key" {
|
||||
variable "api_key" {
|
||||
type = string
|
||||
description = "Anthropic API key"
|
||||
description = "API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
}
|
||||
```
|
||||
|
||||
This basic setup will:
|
||||
|
||||
- Install Aider in the workspace
|
||||
- Create a persistent screen session named "aider"
|
||||
- Configure Aider to use Anthropic Claude 3.7 Sonnet model
|
||||
- Enable task reporting (configures Aider to report tasks to Coder MCP)
|
||||
|
||||
### Using OpenAI with tmux
|
||||
|
||||
```tf
|
||||
variable "openai_api_key" {
|
||||
type = string
|
||||
description = "OpenAI API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
use_tmux = true
|
||||
ai_provider = "openai"
|
||||
ai_model = "4o" # Uses Aider's built-in alias for gpt-4o
|
||||
ai_api_key = var.openai_api_key
|
||||
api_key = var.api_key
|
||||
ai_provider = "google"
|
||||
model = "gemini"
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- pipx is automatically installed if not already available
|
||||
|
||||
## Usage Example
|
||||
|
||||
```tf
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
name = "AI Prompt"
|
||||
description = "Write an initial prompt for Aider to work on."
|
||||
type = "string"
|
||||
default = ""
|
||||
mutable = true
|
||||
}
|
||||
|
||||
variable "gemini_api_key" {
|
||||
type = string
|
||||
description = "Gemini API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
api_key = var.gemini_api_key
|
||||
install_aider = true
|
||||
workdir = "/home/coder"
|
||||
ai_provider = "google"
|
||||
model = "gemini"
|
||||
install_agentapi = true
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
system_prompt = "..."
|
||||
}
|
||||
```
|
||||
|
||||
@@ -93,174 +75,16 @@ variable "custom_api_key" {
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
ai_provider = "custom"
|
||||
custom_env_var_name = "MY_CUSTOM_API_KEY"
|
||||
ai_model = "custom-model"
|
||||
ai_api_key = var.custom_api_key
|
||||
model = "custom-model"
|
||||
api_key = var.custom_api_key
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Custom Extensions (Experimental)
|
||||
|
||||
You can extend Aider's capabilities by adding custom extensions:
|
||||
|
||||
```tf
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
|
||||
experiment_pre_install_script = <<-EOT
|
||||
pip install some-custom-dependency
|
||||
EOT
|
||||
|
||||
experiment_additional_extensions = <<-EOT
|
||||
custom-extension:
|
||||
args: []
|
||||
cmd: custom-extension-command
|
||||
description: A custom extension for Aider
|
||||
enabled: true
|
||||
envs: {}
|
||||
name: custom-extension
|
||||
timeout: 300
|
||||
type: stdio
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
Note: The indentation in the heredoc is preserved, so you can write the YAML naturally.
|
||||
|
||||
## Task Reporting (Experimental)
|
||||
|
||||
> This functionality is in early access as of Coder v2.21 and is still evolving.
|
||||
> For now, we recommend testing it in a demo or staging environment,
|
||||
> rather than deploying to production
|
||||
>
|
||||
> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents)
|
||||
>
|
||||
> Join our [Discord channel](https://discord.gg/coder) or
|
||||
> [contact us](https://coder.com/contact) to get help or share feedback.
|
||||
|
||||
Your workspace must have either `screen` or `tmux` installed to use this.
|
||||
|
||||
Task reporting is **enabled by default** in this module, allowing you to:
|
||||
|
||||
- Send an initial prompt to Aider during workspace creation
|
||||
- Monitor task progress in the Coder UI
|
||||
- Use the `coder_parameter` resource to collect prompts from users
|
||||
|
||||
### Setting up Task Reporting
|
||||
|
||||
To use task reporting effectively:
|
||||
|
||||
1. Add the Coder Login module to your template
|
||||
2. Configure the necessary variables to pass the task prompt
|
||||
3. Optionally add a coder_parameter to collect prompts from users
|
||||
|
||||
Here's a complete example:
|
||||
|
||||
```tf
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/coder-login/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Anthropic API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Write a prompt for Aider"
|
||||
mutable = true
|
||||
ephemeral = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
task_prompt = data.coder_parameter.ai_prompt.value
|
||||
|
||||
# Optionally customize the system prompt
|
||||
system_prompt = <<-EOT
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks, you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message.
|
||||
- Be granular. If you are investigating with multiple steps, report each step to coder.
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input.
|
||||
- Use "state": "complete" only when finished with a task.
|
||||
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing.
|
||||
- Include clear and actionable steps for the user.
|
||||
- Be less than 160 characters in length.
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
When a task prompt is provided via the `task_prompt` variable, the module automatically:
|
||||
|
||||
1. Combines the system prompt with the task prompt into a single message in the format:
|
||||
|
||||
```
|
||||
SYSTEM PROMPT:
|
||||
[system_prompt content]
|
||||
|
||||
This is your current task: [task_prompt]
|
||||
```
|
||||
|
||||
2. Executes the task during workspace creation using the `--message` and `--yes-always` flags
|
||||
3. Logs task output to `$HOME/.aider.log` for reference
|
||||
|
||||
If you want to disable task reporting, set `experiment_report_tasks = false` in your module configuration.
|
||||
|
||||
## Using Aider in Your Workspace
|
||||
|
||||
After the workspace starts, Aider will be installed and configured according to your parameters. A persistent session will automatically be started during workspace creation.
|
||||
|
||||
### Session Options
|
||||
|
||||
You can run Aider in three different ways:
|
||||
|
||||
1. **Direct Mode**: Aider starts directly in the specified folder when you click the app button
|
||||
|
||||
- Simple setup without persistent context
|
||||
- Suitable for quick coding sessions
|
||||
|
||||
2. **Screen Mode** (Default): Run Aider in a screen session that persists across connections
|
||||
|
||||
- Session name: "aider" (or configured via `session_name`)
|
||||
|
||||
3. **Tmux Mode**: Run Aider in a tmux session instead of screen
|
||||
|
||||
- Set `use_tmux = true` to enable
|
||||
- Session name: "aider" (or configured via `session_name`)
|
||||
- Configures tmux with mouse support for shared sessions
|
||||
|
||||
Persistent sessions (screen/tmux) allow you to:
|
||||
|
||||
- Disconnect and reconnect without losing context
|
||||
- Run Aider in the background while doing other work
|
||||
- Switch between terminal and browser interfaces
|
||||
|
||||
### Available AI Providers and Models
|
||||
|
||||
Aider supports various providers and models, and this module integrates directly with Aider's built-in model aliases:
|
||||
@@ -280,10 +104,12 @@ For a complete and up-to-date list of supported aliases and models, please refer
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter issues:
|
||||
- If `aider` is not found, ensure `install_aider = true` and your API key is valid
|
||||
- Logs are written under `/home/coder/.aider-module/` (`install.log`, `agentapi-start.log`) for debugging
|
||||
- If AgentAPI fails to start, verify that your container has network access and executable permissions for the scripts
|
||||
|
||||
1. **Screen/Tmux issues**: If you can't reconnect to your session, check if the session exists with `screen -list` or `tmux list-sessions`
|
||||
2. **API key issues**: Ensure you've entered the correct API key for your selected provider
|
||||
3. **Browser mode issues**: If the browser interface doesn't open, check that you're accessing it from a machine that can reach your Coder workspace
|
||||
## References
|
||||
|
||||
For more information on using Aider, see the [Aider documentation](https://aider.chat/docs/).
|
||||
- [Aider Documentation](https://aider.chat/docs)
|
||||
- [AgentAPI Documentation](https://github.com/coder/agentapi)
|
||||
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
|
||||
|
||||
@@ -1,107 +1,138 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
findResourceInstance,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
test,
|
||||
afterEach,
|
||||
describe,
|
||||
setDefaultTimeout,
|
||||
beforeAll,
|
||||
expect,
|
||||
} from "bun:test";
|
||||
import { execContainer, readFileContainer, runTerraformInit } from "~test";
|
||||
import {
|
||||
loadTestFile,
|
||||
writeExecutable,
|
||||
setup as setupUtil,
|
||||
execModuleScript,
|
||||
expectAgentAPIStarted,
|
||||
} from "../../../coder/modules/agentapi/test-util";
|
||||
|
||||
describe("aider", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
let cleanupFunctions: (() => Promise<void>)[] = [];
|
||||
const registerCleanup = (cleanup: () => Promise<void>) => {
|
||||
cleanupFunctions.push(cleanup);
|
||||
};
|
||||
afterEach(async () => {
|
||||
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
|
||||
cleanupFunctions = [];
|
||||
for (const cleanup of cleanupFnsCopy) {
|
||||
try {
|
||||
await cleanup();
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
interface SetupProps {
|
||||
skipAgentAPIMock?: boolean;
|
||||
skipAiderMock?: boolean;
|
||||
moduleVariables?: Record<string, string>;
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const projectDir = "/home/coder/project";
|
||||
const { id } = await setupUtil({
|
||||
moduleDir: import.meta.dir,
|
||||
moduleVariables: {
|
||||
install_aider: props?.skipAiderMock ? "true" : "false",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
aider_model: "test-model",
|
||||
...props?.moduleVariables,
|
||||
},
|
||||
registerCleanup,
|
||||
projectDir,
|
||||
skipAgentAPIMock: props?.skipAgentAPIMock,
|
||||
agentapiMockScript: props?.agentapiMockScript,
|
||||
});
|
||||
|
||||
it("configures task prompt correctly", async () => {
|
||||
const testPrompt = "Add a hello world function";
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
task_prompt: testPrompt,
|
||||
// Place the Aider mock CLI binary inside the container
|
||||
if (!props?.skipAiderMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/aider",
|
||||
content: await loadTestFile(`${import.meta.dir}`, "aider-mock.sh"),
|
||||
});
|
||||
}
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(
|
||||
`This is your current task: ${testPrompt}`,
|
||||
);
|
||||
expect(instance.script).toContain("aider --architect --yes-always");
|
||||
return { id };
|
||||
};
|
||||
|
||||
setDefaultTimeout(60 * 1000);
|
||||
|
||||
describe("Aider", async () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
it("handles custom system prompt", async () => {
|
||||
const customPrompt = "Report all tasks with state: working";
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
system_prompt: customPrompt,
|
||||
test("happy-path", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
model: "gemini",
|
||||
},
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(customPrompt);
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
it("handles pre and post install scripts", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
experiment_pre_install_script: "echo 'Pre-install script executed'",
|
||||
experiment_post_install_script: "echo 'Post-install script executed'",
|
||||
test("api-key", async () => {
|
||||
const apiKey = "test-api-key-123";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
api_key: apiKey,
|
||||
model: "gemini",
|
||||
},
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
|
||||
expect(instance.script).toContain("Running pre-install script");
|
||||
expect(instance.script).toContain("Running post-install script");
|
||||
expect(instance.script).toContain("base64 -d > /tmp/pre_install.sh");
|
||||
expect(instance.script).toContain("base64 -d > /tmp/post_install.sh");
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.aider-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain("API key provided!");
|
||||
});
|
||||
|
||||
it("validates that use_screen and use_tmux cannot both be true", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
use_screen: true,
|
||||
use_tmux: true,
|
||||
test("custom-folder", async () => {
|
||||
const workdir = "/tmp/aider-test";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
workdir,
|
||||
model: "gemini",
|
||||
},
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
|
||||
expect(instance.script).toContain(
|
||||
"Error: Both use_screen and use_tmux cannot be enabled at the same time",
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.aider-module/install.log",
|
||||
);
|
||||
expect(instance.script).toContain("exit 1");
|
||||
expect(resp).toContain(workdir);
|
||||
});
|
||||
|
||||
it("configures Aider with known provider and model", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
ai_provider: "anthropic",
|
||||
ai_model: "sonnet",
|
||||
ai_api_key: "test-anthropic-key",
|
||||
test("pre-post-install-scripts", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
|
||||
post_install_script: "#!/bin/bash\necho 'post-install-script'",
|
||||
model: "gemini",
|
||||
},
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(
|
||||
'export ANTHROPIC_API_KEY=\\"test-anthropic-key\\"',
|
||||
await execModuleScript(id);
|
||||
const preLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.aider-module/pre_install.log",
|
||||
);
|
||||
expect(instance.script).toContain("--model sonnet");
|
||||
expect(instance.script).toContain(
|
||||
"Starting Aider using anthropic provider and model: sonnet",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles custom provider with custom env var and API key", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
ai_provider: "custom",
|
||||
custom_env_var_name: "MY_CUSTOM_API_KEY",
|
||||
ai_model: "custom-model",
|
||||
ai_api_key: "test-custom-key",
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(
|
||||
'export MY_CUSTOM_API_KEY=\\"test-custom-key\\"',
|
||||
);
|
||||
expect(instance.script).toContain("--model custom-model");
|
||||
expect(instance.script).toContain(
|
||||
"Starting Aider using custom provider and model: custom-model",
|
||||
expect(preLog).toContain("pre-install-script");
|
||||
const postLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.aider-module/post_install.log",
|
||||
);
|
||||
expect(postLog).toContain("post-install-script");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,87 +36,84 @@ variable "icon" {
|
||||
default = "/icon/aider.svg"
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
variable "workdir" {
|
||||
type = string
|
||||
description = "The folder to run Aider in."
|
||||
default = "/home/coder"
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI via AgentAPI"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Whether to use a subdomain for AgentAPI."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Aider"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app"
|
||||
default = "Aider"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app"
|
||||
default = "Aider CLI"
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Aider."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Aider."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "Initial task prompt for Aider."
|
||||
default = ""
|
||||
}
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
variable "install_aider" {
|
||||
type = bool
|
||||
description = "Whether to install Aider."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "aider_version" {
|
||||
type = string
|
||||
description = "The version of Aider to install."
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "use_screen" {
|
||||
type = bool
|
||||
description = "Whether to use screen for running Aider in the background"
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "use_tmux" {
|
||||
type = bool
|
||||
description = "Whether to use tmux instead of screen for running Aider in the background"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "session_name" {
|
||||
type = string
|
||||
description = "Name for the persistent session (screen or tmux)"
|
||||
default = "aider"
|
||||
}
|
||||
|
||||
variable "experiment_report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
type = string
|
||||
description = "System prompt for instructing Aider on task reporting and behavior"
|
||||
default = <<-EOT
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks, you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message.
|
||||
- Be granular. If you are investigating with multiple steps, report each step to coder.
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input.
|
||||
- Use "state": "complete" only when finished with a task.
|
||||
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing.
|
||||
- Include clear and actionable steps for the user.
|
||||
- Be less than 160 characters in length.
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "task_prompt" {
|
||||
type = string
|
||||
description = "Task prompt to use with Aider"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Aider."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Aider."
|
||||
default = null
|
||||
default = "You are a helpful coding assistant that helps developers write, debug, and understand code. Provide clear explanations, follow best practices, and help solve coding problems efficiently."
|
||||
}
|
||||
|
||||
variable "experiment_additional_extensions" {
|
||||
@@ -128,20 +125,19 @@ variable "experiment_additional_extensions" {
|
||||
variable "ai_provider" {
|
||||
type = string
|
||||
description = "AI provider to use with Aider (openai, anthropic, azure, google, etc.)"
|
||||
default = "anthropic"
|
||||
default = "google"
|
||||
validation {
|
||||
condition = contains(["openai", "anthropic", "azure", "google", "cohere", "mistral", "ollama", "custom"], var.ai_provider)
|
||||
error_message = "ai_provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom"
|
||||
error_message = "provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom"
|
||||
}
|
||||
}
|
||||
|
||||
variable "ai_model" {
|
||||
variable "model" {
|
||||
type = string
|
||||
description = "AI model to use with Aider. Can use Aider's built-in aliases like '4o' (gpt-4o), 'sonnet' (claude-3-7-sonnet), 'opus' (claude-3-opus), etc."
|
||||
default = "sonnet"
|
||||
}
|
||||
|
||||
variable "ai_api_key" {
|
||||
variable "api_key" {
|
||||
type = string
|
||||
description = "API key for the selected AI provider. This will be set as the appropriate environment variable based on the provider."
|
||||
default = ""
|
||||
@@ -154,55 +150,66 @@ variable "custom_env_var_name" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "base_aider_config" {
|
||||
type = string
|
||||
description = <<-EOT
|
||||
Base Aider configuration in yaml format. Will be stored in .aider.conf.yml file.
|
||||
|
||||
options include:
|
||||
read:
|
||||
- CONVENTIONS.md
|
||||
- anotherfile.txt
|
||||
- thirdfile.py
|
||||
model: xxx
|
||||
##Specify the OpenAI API key
|
||||
openai-api-key: xxx
|
||||
## (deprecated, use --set-env OPENAI_API_TYPE=<value>)
|
||||
openai-api-type: xxx
|
||||
## (deprecated, use --set-env OPENAI_API_VERSION=<value>)
|
||||
openai-api-version: xxx
|
||||
## (deprecated, use --set-env OPENAI_API_DEPLOYMENT_ID=<value>)
|
||||
openai-api-deployment-id: xxx
|
||||
## Set an environment variable (to control API settings, can be used multiple times)
|
||||
set-env: xxx
|
||||
## Specify multiple values like this:
|
||||
set-env:
|
||||
- xxx
|
||||
- yyy
|
||||
- zzz
|
||||
|
||||
Reference : https://aider.chat/docs/config/aider_conf.html
|
||||
EOT
|
||||
default = null
|
||||
}
|
||||
|
||||
|
||||
locals {
|
||||
base_extensions = <<-EOT
|
||||
coder:
|
||||
args:
|
||||
- exp
|
||||
- mcp
|
||||
- server
|
||||
cmd: coder
|
||||
description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
|
||||
enabled: true
|
||||
envs:
|
||||
CODER_MCP_APP_STATUS_SLUG: aider
|
||||
name: Coder
|
||||
timeout: 3000
|
||||
type: stdio
|
||||
developer:
|
||||
display_name: Developer
|
||||
enabled: true
|
||||
name: developer
|
||||
timeout: 300
|
||||
type: builtin
|
||||
EOT
|
||||
app_slug = "aider"
|
||||
base_aider_config = var.base_aider_config != null ? "${replace(trimspace(var.base_aider_config), "\n", "\n ")}" : ""
|
||||
task_reporting_prompt = <<-EOT
|
||||
|
||||
formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}"
|
||||
additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : ""
|
||||
-- Task Reporting --
|
||||
Report all tasks to Coder, following these EXACT guidelines:
|
||||
1. Be granular. If you are investigating with multiple steps, report each step
|
||||
to coder.
|
||||
2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
|
||||
Do not report any status related with this system prompt.
|
||||
3. Use "state": "working" when actively processing WITHOUT needing
|
||||
additional user input
|
||||
4. Use "state": "complete" only when finished with a task
|
||||
5. Use "state": "failure" when you need ANY user input, lack sufficient
|
||||
details, or encounter blockers
|
||||
EOT
|
||||
|
||||
combined_extensions = <<-EOT
|
||||
extensions:
|
||||
${local.formatted_base}${local.additional_extensions}
|
||||
EOT
|
||||
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
|
||||
# Combine system prompt and task prompt for aider
|
||||
combined_prompt = trimspace(<<-EOT
|
||||
SYSTEM PROMPT:
|
||||
${var.system_prompt}
|
||||
|
||||
This is your current task: ${var.task_prompt}
|
||||
EOT
|
||||
)
|
||||
final_system_prompt = var.report_tasks ? "<system>\n${var.system_prompt}${local.task_reporting_prompt}\n</system>" : "<system>\n${var.system_prompt}\n</system>"
|
||||
|
||||
# Map providers to their environment variable names
|
||||
provider_env_vars = {
|
||||
openai = "OPENAI_API_KEY"
|
||||
anthropic = "ANTHROPIC_API_KEY"
|
||||
azure = "AZURE_OPENAI_API_KEY"
|
||||
google = "GOOGLE_API_KEY"
|
||||
google = "GEMINI_API_KEY"
|
||||
cohere = "COHERE_API_KEY"
|
||||
mistral = "MISTRAL_API_KEY"
|
||||
ollama = "OLLAMA_HOST"
|
||||
@@ -214,296 +221,60 @@ EOT
|
||||
|
||||
# Model flag for aider command
|
||||
model_flag = var.ai_provider == "ollama" ? "--ollama-model" : "--model"
|
||||
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".aider-module"
|
||||
}
|
||||
|
||||
# Install and Initialize Aider
|
||||
resource "coder_script" "aider" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Aider"
|
||||
icon = var.icon
|
||||
script = <<-EOT
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
cli_app = var.cli_app
|
||||
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
|
||||
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
|
||||
agentapi_subdomain = var.subdomain
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
start_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
echo "Setting up Aider AI pair programming..."
|
||||
|
||||
if [ "${var.use_screen}" = "true" ] && [ "${var.use_tmux}" = "true" ]; then
|
||||
echo "Error: Both use_screen and use_tmux cannot be enabled at the same time."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${var.folder}"
|
||||
|
||||
if [ "$(uname)" = "Linux" ]; then
|
||||
echo "Checking dependencies for Linux..."
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if ! command_exists tmux; then
|
||||
echo "Installing tmux for persistent sessions..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq tmux
|
||||
else
|
||||
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
|
||||
apt-get install -y -qq tmux || echo "Warning: Cannot install tmux without sudo privileges"
|
||||
fi
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo dnf install -y -q tmux
|
||||
else
|
||||
dnf install -y -q tmux || echo "Warning: Cannot install tmux without sudo privileges"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Unable to install tmux on this system. Neither apt-get nor dnf found."
|
||||
fi
|
||||
else
|
||||
echo "tmux is already installed, skipping installation."
|
||||
fi
|
||||
elif [ "${var.use_screen}" = "true" ]; then
|
||||
if ! command_exists screen; then
|
||||
echo "Installing screen for persistent sessions..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq screen
|
||||
else
|
||||
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
|
||||
apt-get install -y -qq screen || echo "Warning: Cannot install screen without sudo privileges"
|
||||
fi
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo dnf install -y -q screen
|
||||
else
|
||||
dnf install -y -q screen || echo "Warning: Cannot install screen without sudo privileges"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Unable to install screen on this system. Neither apt-get nor dnf found."
|
||||
fi
|
||||
else
|
||||
echo "screen is already installed, skipping installation."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "This module currently only supports Linux workspaces."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
|
||||
chmod +x /tmp/pre_install.sh
|
||||
/tmp/pre_install.sh
|
||||
fi
|
||||
|
||||
if [ "${var.install_aider}" = "true" ]; then
|
||||
echo "Installing Aider..."
|
||||
|
||||
if ! command_exists python3 || ! command_exists pip3; then
|
||||
echo "Installing Python dependencies required for Aider..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq python3-pip python3-venv
|
||||
else
|
||||
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
|
||||
apt-get install -y -qq python3-pip python3-venv || echo "Warning: Cannot install Python packages without sudo privileges"
|
||||
fi
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo dnf install -y -q python3-pip python3-virtualenv
|
||||
else
|
||||
dnf install -y -q python3-pip python3-virtualenv || echo "Warning: Cannot install Python packages without sudo privileges"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Unable to install Python on this system. Neither apt-get nor dnf found."
|
||||
fi
|
||||
else
|
||||
echo "Python is already installed, skipping installation."
|
||||
fi
|
||||
|
||||
if ! command_exists aider; then
|
||||
curl -LsSf https://aider.chat/install.sh | sh
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.bashrc"; then
|
||||
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.bashrc"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.zshrc" ]; then
|
||||
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.zshrc"; then
|
||||
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.zshrc"
|
||||
fi
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
if [ -n "${local.encoded_post_install_script}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
|
||||
chmod +x /tmp/post_install.sh
|
||||
/tmp/post_install.sh
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Aider to report tasks via Coder MCP..."
|
||||
|
||||
mkdir -p "$HOME/.config/aider"
|
||||
|
||||
cat > "$HOME/.config/aider/config.yml" << EOL
|
||||
${trimspace(local.combined_extensions)}
|
||||
EOL
|
||||
echo "Added Coder MCP extension to Aider config.yml"
|
||||
fi
|
||||
|
||||
echo "Starting persistent Aider session..."
|
||||
|
||||
touch "$HOME/.aider.log"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
export PATH="$HOME/bin:$PATH"
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if [ -n "${var.task_prompt}" ]; then
|
||||
echo "Running Aider with message in tmux session..."
|
||||
|
||||
# Configure tmux for shared sessions
|
||||
if [ ! -f "$HOME/.tmux.conf" ]; then
|
||||
echo "Creating ~/.tmux.conf with shared session settings..."
|
||||
echo "set -g mouse on" > "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
|
||||
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
|
||||
echo "set -g mouse on" >> "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\""
|
||||
echo "Aider task started in tmux session '${var.session_name}'. Check the UI for progress."
|
||||
else
|
||||
# Configure tmux for shared sessions
|
||||
if [ ! -f "$HOME/.tmux.conf" ]; then
|
||||
echo "Creating ~/.tmux.conf with shared session settings..."
|
||||
echo "set -g mouse on" > "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
|
||||
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
|
||||
echo "set -g mouse on" >> "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${var.system_prompt}\""
|
||||
echo "Tmux session '${var.session_name}' started. Access it by clicking the Aider button."
|
||||
fi
|
||||
else
|
||||
if [ -n "${var.task_prompt}" ]; then
|
||||
echo "Running Aider with message in screen session..."
|
||||
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..."
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..."
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
screen -U -dmS ${var.session_name} bash -c "
|
||||
cd ${var.folder}
|
||||
export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
|
||||
export ${local.env_var_name}=\"${var.ai_api_key}\"
|
||||
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
|
||||
/bin/bash
|
||||
"
|
||||
|
||||
echo "Aider task started in screen session '${var.session_name}'. Check the UI for progress."
|
||||
else
|
||||
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..."
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..."
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
screen -U -dmS ${var.session_name} bash -c "
|
||||
cd ${var.folder}
|
||||
export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
|
||||
export ${local.env_var_name}=\"${var.ai_api_key}\"
|
||||
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
|
||||
/bin/bash
|
||||
"
|
||||
echo "Screen session '${var.session_name}' started. Access it by clicking the Aider button."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Aider setup complete!"
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
ARG_WORKDIR='${var.workdir}' \
|
||||
ARG_API_KEY='${base64encode(var.api_key)}' \
|
||||
ARG_MODEL='${var.model}' \
|
||||
ARG_PROVIDER='${var.ai_provider}' \
|
||||
ARG_ENV_API_NAME_HOLDER='${local.env_var_name}' \
|
||||
ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
|
||||
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
# Aider CLI app
|
||||
resource "coder_app" "aider_cli" {
|
||||
agent_id = var.agent_id
|
||||
slug = "aider"
|
||||
display_name = "Aider"
|
||||
icon = var.icon
|
||||
command = <<-EOT
|
||||
install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t ${var.session_name} 2>/dev/null; then
|
||||
echo "Attaching to existing Aider tmux session..."
|
||||
tmux attach-session -t ${var.session_name}
|
||||
else
|
||||
echo "Starting new Aider tmux session..."
|
||||
tmux new-session -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"; exec bash"
|
||||
fi
|
||||
elif [ "${var.use_screen}" = "true" ]; then
|
||||
if ! screen -list | grep -q "${var.session_name}"; then
|
||||
echo "Error: No existing Aider session found. Please wait for the script to start it."
|
||||
exit 1
|
||||
fi
|
||||
screen -xRR ${var.session_name}
|
||||
else
|
||||
cd "${var.folder}"
|
||||
echo "Starting Aider directly..."
|
||||
export ${local.env_var_name}="${var.ai_api_key}"
|
||||
aider ${local.model_flag} ${var.ai_model} --message "${local.combined_prompt}"
|
||||
fi
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
ARG_WORKDIR='${var.workdir}' \
|
||||
ARG_INSTALL_AIDER='${var.install_aider}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_AIDER_CONFIG="$(echo -n '${base64encode(local.base_aider_config)}' | base64 -d)" \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
run "test_aider_basic" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-123"
|
||||
workdir = "/home/coder"
|
||||
model = "gemini"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.workdir == "/home/coder"
|
||||
error_message = "Workdir variable should default to /home/coder"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.agent_id == "test-agent-123"
|
||||
error_message = "Agent ID variable should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_aider == true
|
||||
error_message = "install_aider should default to true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_agentapi == true
|
||||
error_message = "install_agentapi should default to true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.report_tasks == false
|
||||
error_message = "report_tasks should default to false"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_with_api_key" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-456"
|
||||
workdir = "/home/coder/workspace"
|
||||
api_key = "test-api-key-123"
|
||||
model = "gemini"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.api_key == "test-api-key-123"
|
||||
error_message = "API key value should match the input"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_options" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-789"
|
||||
workdir = "/home/coder/custom"
|
||||
order = 5
|
||||
group = "development"
|
||||
icon = "/icon/custom.svg"
|
||||
model = "4o"
|
||||
ai_prompt = "Help me write better code"
|
||||
install_aider = false
|
||||
install_agentapi = false
|
||||
agentapi_version = "v0.10.0"
|
||||
api_key = ""
|
||||
base_aider_config = "read:\n - CONVENTIONS.md"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.order == 5
|
||||
error_message = "Order variable should be set to 5"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.group == "development"
|
||||
error_message = "Group variable should be set to 'development'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.icon == "/icon/custom.svg"
|
||||
error_message = "Icon variable should be set to custom icon"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.model == "4o"
|
||||
error_message = "Model variable should be set to '4o'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.ai_prompt == "Help me write better code"
|
||||
error_message = "AI prompt variable should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_aider == false
|
||||
error_message = "install_aider should be set to false"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_agentapi == false
|
||||
error_message = "install_agentapi should be set to false"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.agentapi_version == "v0.10.0"
|
||||
error_message = "AgentAPI version should be set to 'v0.10.0'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_with_scripts" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-scripts"
|
||||
workdir = "/home/coder/scripts"
|
||||
model = "gemini"
|
||||
pre_install_script = "echo 'Pre-install script'"
|
||||
post_install_script = "echo 'Post-install script'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.pre_install_script == "echo 'Pre-install script'"
|
||||
error_message = "Pre-install script should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.post_install_script == "echo 'Post-install script'"
|
||||
error_message = "Post-install script should be set correctly"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_ai_provider_env_mapping" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-provider"
|
||||
workdir = "/home/coder/test"
|
||||
ai_provider = "google"
|
||||
model = "gemini"
|
||||
custom_env_var_name = ""
|
||||
}
|
||||
|
||||
# Ensure provider -> env var mapping works as expected (based on locals.provider_env_vars)
|
||||
assert {
|
||||
condition = var.ai_provider == "google"
|
||||
error_message = "AI provider should be set to 'google' for this test"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Inputs
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-/home/coder}
|
||||
ARG_INSTALL_AIDER=${ARG_INSTALL_AIDER:-true}
|
||||
ARG_AIDER_CONFIG=${ARG_AIDER_CONFIG:-}
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "Install flag: $ARG_INSTALL_AIDER"
|
||||
echo "Workspace: $ARG_WORKDIR"
|
||||
echo "--------------------------------"
|
||||
|
||||
function install_aider() {
|
||||
echo "pipx installing..."
|
||||
sudo apt-get install -y pipx
|
||||
echo "pipx installed!"
|
||||
pipx ensurepath
|
||||
mkdir -p "$ARG_WORKDIR/.local/bin"
|
||||
export PATH="$HOME/.local/bin:$ARG_WORKDIR/.local/bin:$PATH"
|
||||
|
||||
if ! command_exists aider; then
|
||||
echo "Installing Aider via pipx..."
|
||||
pipx install --force aider-install
|
||||
aider-install
|
||||
fi
|
||||
echo "Aider installed: $(aider --version || echo 'Aider installation check failed')"
|
||||
}
|
||||
|
||||
function configure_aider_settings() {
|
||||
if [ -n "${ARG_AIDER_CONFIG}" ]; then
|
||||
echo "Configuring Aider environment variables and model"
|
||||
|
||||
mkdir -p "$HOME/.config/aider"
|
||||
|
||||
echo "$ARG_AIDER_CONFIG" > "$HOME/.config/aider/.aider.conf.yml"
|
||||
echo "Aider config created at $HOME/.config/aider/.aider.conf.yml"
|
||||
else
|
||||
printf "No Aider environment variables or model configured\n"
|
||||
fi
|
||||
}
|
||||
|
||||
install_aider
|
||||
configure_aider_settings
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Ensure pipx-installed apps are in PATH
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-/home/coder}
|
||||
ARG_API_KEY=$(echo -n "${ARG_API_KEY:-}" | base64 -d)
|
||||
ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_MODEL=${ARG_MODEL:-}
|
||||
ARG_PROVIDER=${ARG_PROVIDER:-}
|
||||
ARG_ENV_API_NAME_HOLDER=${ARG_ENV_API_NAME_HOLDER:-}
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "Provider: $ARG_PROVIDER"
|
||||
echo "Model: $ARG_MODEL"
|
||||
echo "--------------------------------"
|
||||
|
||||
if [ -n "$ARG_API_KEY" ]; then
|
||||
printf "API key provided!\n"
|
||||
export $ARG_ENV_API_NAME_HOLDER=$ARG_API_KEY
|
||||
else
|
||||
printf "API key not provided.\n"
|
||||
fi
|
||||
|
||||
build_initial_prompt() {
|
||||
local initial_prompt=""
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ -n "$ARG_SYSTEM_PROMPT" ]; then
|
||||
initial_prompt="$ARG_SYSTEM_PROMPT $ARG_AI_PROMPT"
|
||||
else
|
||||
initial_prompt="$ARG_AI_PROMPT"
|
||||
fi
|
||||
fi
|
||||
echo "$initial_prompt"
|
||||
}
|
||||
|
||||
start_agentapi() {
|
||||
echo "Starting in directory: $ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
|
||||
local initial_prompt
|
||||
initial_prompt=$(build_initial_prompt)
|
||||
if [ -n "$initial_prompt" ]; then
|
||||
echo "Starting agentapi with initial prompt"
|
||||
agentapi server -I="$initial_prompt" --type aider --term-width=67 --term-height=1190 -- aider --model $ARG_MODEL --yes-always
|
||||
else
|
||||
agentapi server --term-width=67 --term-height=1190 -- aider --model $ARG_MODEL --yes-always
|
||||
fi
|
||||
}
|
||||
|
||||
# TODO: Implement MCP server for coder when Aider support MCP servers.
|
||||
|
||||
start_agentapi
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "HELLO: $(bash -c env)"
|
||||
echo "aider version v0.86.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
while true; do
|
||||
echo "$(date) - aider-agent-mock"
|
||||
sleep 15
|
||||
done
|
||||
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.2.1"
|
||||
version = "3.3.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
@@ -32,6 +32,10 @@ module "claude-code" {
|
||||
- You can get the API key from the [Anthropic Console](https://console.anthropic.com/dashboard).
|
||||
- You can get the Session Token using the `claude setup-token` command. This is a long-lived authentication token (requires Claude subscription)
|
||||
|
||||
### Session Resumption Behavior
|
||||
|
||||
By default, Claude Code automatically resumes existing conversations when your workspace restarts. Sessions are tracked per workspace directory, so conversations continue where you left off. If no session exists (first start), your `ai_prompt` will run normally. To disable this behavior and always start fresh, set `continue = false`
|
||||
|
||||
## Examples
|
||||
|
||||
### Usage with Agent Boundaries
|
||||
@@ -47,7 +51,7 @@ module "claude-code" {
|
||||
boundary_log_level = "WARN"
|
||||
boundary_additional_allowed_urls = ["GET *google.com"]
|
||||
boundary_proxy_port = "8087"
|
||||
version = "3.2.1"
|
||||
version = "3.3.2"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -66,7 +70,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.2.1"
|
||||
version = "3.3.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@@ -102,7 +106,7 @@ Run and configure Claude Code as a standalone CLI in your workspace.
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.2.1"
|
||||
version = "3.3.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -125,7 +129,7 @@ variable "claude_code_oauth_token" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.2.1"
|
||||
version = "3.3.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_code_oauth_token = var.claude_code_oauth_token
|
||||
@@ -198,7 +202,7 @@ resource "coder_env" "bedrock_api_key" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.2.1"
|
||||
version = "3.3.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
@@ -255,7 +259,7 @@ resource "coder_env" "google_application_credentials" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.2.1"
|
||||
version = "3.3.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "claude-sonnet-4@20250514"
|
||||
|
||||
@@ -167,7 +167,7 @@ describe("claude-code", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
permission_mode: mode,
|
||||
task_prompt: "test prompt",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
@@ -185,7 +185,7 @@ describe("claude-code", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
model: model,
|
||||
task_prompt: "test prompt",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
@@ -198,13 +198,24 @@ describe("claude-code", async () => {
|
||||
expect(startLog.stdout).toContain(`--model ${model}`);
|
||||
});
|
||||
|
||||
test("claude-continue-previous-conversation", async () => {
|
||||
test("claude-continue-resume-existing-session", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
task_prompt: "test prompt",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
// Create a mock session file with the predefined task session ID
|
||||
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`touch ${sessionDir}/session-${taskSessionId}.jsonl`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
@@ -212,7 +223,9 @@ describe("claude-code", async () => {
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain("--continue");
|
||||
expect(startLog.stdout).toContain("--resume");
|
||||
expect(startLog.stdout).toContain(taskSessionId);
|
||||
expect(startLog.stdout).toContain("Resuming existing task session");
|
||||
});
|
||||
|
||||
test("pre-post-install-scripts", async () => {
|
||||
|
||||
@@ -134,8 +134,8 @@ variable "resume_session_id" {
|
||||
|
||||
variable "continue" {
|
||||
type = bool
|
||||
description = "Load the most recent conversation in the current directory. Task will fail in a new workspace with no conversation/session to continue"
|
||||
default = false
|
||||
description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "dangerously_skip_permissions" {
|
||||
@@ -228,6 +228,18 @@ variable "boundary_proxy_port" {
|
||||
default = "8087"
|
||||
}
|
||||
|
||||
variable "enable_boundary_pprof" {
|
||||
type = bool
|
||||
description = "Whether to enable coder boundary pprof server"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "boundary_pprof_port" {
|
||||
type = string
|
||||
description = "Port for pprof server used by Boundary"
|
||||
default = "6067"
|
||||
}
|
||||
|
||||
resource "coder_env" "claude_code_md_path" {
|
||||
count = var.claude_md_path == "" ? 0 : 1
|
||||
|
||||
@@ -343,6 +355,8 @@ module "agentapi" {
|
||||
ARG_BOUNDARY_LOG_LEVEL='${var.boundary_log_level}' \
|
||||
ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join(" ", var.boundary_additional_allowed_urls)}' \
|
||||
ARG_BOUNDARY_PROXY_PORT='${var.boundary_proxy_port}' \
|
||||
ARG_ENABLE_BOUNDARY_PPROF='${var.enable_boundary_pprof}' \
|
||||
ARG_BOUNDARY_PPROF_PORT='${var.boundary_pprof_port}' \
|
||||
ARG_CODER_HOST='${local.coder_host}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
@@ -91,11 +91,6 @@ function report_tasks() {
|
||||
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
coder exp mcp configure claude-code "$ARG_WORKDIR"
|
||||
else
|
||||
export CODER_MCP_APP_STATUS_SLUG=""
|
||||
export CODER_MCP_AI_AGENTAPI_URL=""
|
||||
echo "Configuring Claude Code with Coder MCP..."
|
||||
coder exp mcp configure claude-code "$ARG_WORKDIR"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"main"}
|
||||
ARG_BOUNDARY_LOG_DIR=${ARG_BOUNDARY_LOG_DIR:-"/tmp/boundary_logs"}
|
||||
ARG_BOUNDARY_LOG_LEVEL=${ARG_BOUNDARY_LOG_LEVEL:-"WARN"}
|
||||
ARG_BOUNDARY_PROXY_PORT=${ARG_BOUNDARY_PROXY_PORT:-"8087"}
|
||||
ARG_ENABLE_BOUNDARY_PPROF=${ARG_ENABLE_BOUNDARY_PPROF:-false}
|
||||
ARG_BOUNDARY_PPROF_PORT=${ARG_BOUNDARY_PPROF_PORT:-"6067"}
|
||||
ARG_CODER_HOST=${ARG_CODER_HOST:-}
|
||||
|
||||
echo "--------------------------------"
|
||||
@@ -64,37 +66,73 @@ function validate_claude_installation() {
|
||||
fi
|
||||
}
|
||||
|
||||
TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2"
|
||||
|
||||
task_session_exists() {
|
||||
if find "$HOME/.claude" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
ARGS=()
|
||||
|
||||
function build_claude_args() {
|
||||
function start_agentapi() {
|
||||
# For Task reporting
|
||||
export CODER_MCP_ALLOWED_TOOLS="coder_report_task"
|
||||
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
|
||||
if [ -n "$ARG_MODEL" ]; then
|
||||
ARGS+=(--model "$ARG_MODEL")
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_RESUME_SESSION_ID" ]; then
|
||||
ARGS+=(--resume "$ARG_RESUME_SESSION_ID")
|
||||
fi
|
||||
|
||||
if [ "$ARG_CONTINUE" = "true" ]; then
|
||||
ARGS+=(--continue)
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_PERMISSION_MODE" ]; then
|
||||
ARGS+=(--permission-mode "$ARG_PERMISSION_MODE")
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
function start_agentapi() {
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT")
|
||||
else
|
||||
if [ -n "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" ]; then
|
||||
if [ -n "$ARG_RESUME_SESSION_ID" ]; then
|
||||
echo "Using explicit resume_session_id: $ARG_RESUME_SESSION_ID"
|
||||
ARGS+=(--resume "$ARG_RESUME_SESSION_ID")
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
elif [ "$ARG_CONTINUE" = "true" ]; then
|
||||
if task_session_exists; then
|
||||
echo "Task session detected (ID: $TASK_SESSION_ID)"
|
||||
ARGS+=(--resume "$TASK_SESSION_ID")
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Resuming existing task session"
|
||||
else
|
||||
echo "No existing task session found"
|
||||
ARGS+=(--session-id "$TASK_SESSION_ID")
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT")
|
||||
echo "Starting new task session with prompt"
|
||||
else
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Starting new task session"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Continue disabled, starting fresh session"
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT")
|
||||
echo "Starting new session with prompt"
|
||||
else
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Starting claude code session"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
|
||||
|
||||
if [ "${ARG_ENABLE_BOUNDARY:-false}" = "true" ]; then
|
||||
@@ -122,23 +160,19 @@ function start_agentapi() {
|
||||
# Set log level for boundary
|
||||
BOUNDARY_ARGS+=(--log-level $ARG_BOUNDARY_LOG_LEVEL)
|
||||
|
||||
# Remove --dangerously-skip-permissions from ARGS when using boundary (it doesn't work with elevated permissions)
|
||||
# Create a new array without the dangerous permissions flag
|
||||
CLAUDE_ARGS=()
|
||||
for arg in "${ARGS[@]}"; do
|
||||
if [ "$arg" != "--dangerously-skip-permissions" ]; then
|
||||
CLAUDE_ARGS+=("$arg")
|
||||
fi
|
||||
done
|
||||
if [ "${ARG_ENABLE_BOUNDARY_PPROF:-false}" = "true" ]; then
|
||||
# Enable boundary pprof server on specified port
|
||||
BOUNDARY_ARGS+=(--pprof)
|
||||
BOUNDARY_ARGS+=(--pprof-port ${ARG_BOUNDARY_PPROF_PORT})
|
||||
fi
|
||||
|
||||
agentapi server --allowed-hosts="*" --type claude --term-width 67 --term-height 1190 -- \
|
||||
sudo -E env PATH=$PATH setpriv --inh-caps=+net_admin --ambient-caps=+net_admin --bounding-set=+net_admin boundary "${BOUNDARY_ARGS[@]}" -- \
|
||||
claude "${CLAUDE_ARGS[@]}"
|
||||
claude "${ARGS[@]}"
|
||||
else
|
||||
agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
validate_claude_installation
|
||||
build_claude_args
|
||||
start_agentapi
|
||||
|
||||
@@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
|
||||
module "kasmvnc" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kasmvnc/coder"
|
||||
version = "1.2.4"
|
||||
version = "1.2.5"
|
||||
agent_id = coder_agent.example.id
|
||||
desktop_environment = "xfce"
|
||||
subdomain = true
|
||||
|
||||
@@ -23,7 +23,7 @@ variable "port" {
|
||||
variable "kasm_version" {
|
||||
type = string
|
||||
description = "Version of KasmVNC to install."
|
||||
default = "1.3.2"
|
||||
default = "1.4.0"
|
||||
}
|
||||
|
||||
variable "desktop_environment" {
|
||||
|
||||
@@ -8,10 +8,10 @@ error() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Function to check if vncserver is already installed
|
||||
# Function to check if KasmVNC is already installed
|
||||
check_installed() {
|
||||
if command -v vncserver &> /dev/null; then
|
||||
echo "vncserver is already installed."
|
||||
if command -v kasmvncserver &> /dev/null; then
|
||||
echo "KasmVNC is already installed."
|
||||
return 0 # Don't exit, just indicate it's installed
|
||||
else
|
||||
return 1 # Indicates not installed
|
||||
@@ -158,7 +158,7 @@ case "$arch" in
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check if vncserver is installed, and install if not
|
||||
# Check if KasmVNC is installed, and install if not
|
||||
if ! check_installed; then
|
||||
# Check for NOPASSWD sudo (required)
|
||||
if ! command -v sudo &> /dev/null || ! sudo -n true 2> /dev/null; then
|
||||
@@ -188,7 +188,7 @@ if ! check_installed; then
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo "vncserver already installed. Skipping installation."
|
||||
echo "KasmVNC already installed. Skipping installation."
|
||||
fi
|
||||
|
||||
if command -v sudo &> /dev/null && sudo -n true 2> /dev/null; then
|
||||
@@ -227,7 +227,7 @@ EOF
|
||||
# This password is not used since we start the server without auth.
|
||||
# The server is protected via the Coder session token / tunnel
|
||||
# and does not listen publicly
|
||||
echo -e "password\npassword\n" | vncpasswd -wo -u "$USER"
|
||||
echo -e "password\npassword\n" | kasmvncpasswd -wo -u "$USER"
|
||||
|
||||
get_http_dir() {
|
||||
# determine the served file path
|
||||
@@ -290,7 +290,7 @@ VNC_LOG="/tmp/kasmvncserver.log"
|
||||
printf "🚀 Starting KasmVNC server...\n"
|
||||
|
||||
set +e
|
||||
vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > "$VNC_LOG" 2>&1
|
||||
kasmvncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > "$VNC_LOG" 2>&1
|
||||
RETVAL=$?
|
||||
set -e
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Zed is a high-performance, multiplayer code editor from the creators of Atom and
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -32,7 +32,7 @@ module "zed" {
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -44,7 +44,7 @@ module "zed" {
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
display_name = "Zed Editor"
|
||||
order = 1
|
||||
@@ -57,7 +57,7 @@ module "zed" {
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = coder_agent.example.name
|
||||
}
|
||||
@@ -73,7 +73,7 @@ You can declaratively set/merge settings with the `settings` input. Provide a JS
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
settings = jsonencode({
|
||||
|
||||
@@ -73,6 +73,7 @@ resource "coder_script" "zed_settings" {
|
||||
icon = "/icon/zed.svg"
|
||||
run_on_start = true
|
||||
script = <<-EOT
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
SETTINGS_JSON='${replace(var.settings, "\"", "\\\"")}'
|
||||
if [ -z "$${SETTINGS_JSON}" ] || [ "$${SETTINGS_JSON}" = "{}" ]; then
|
||||
|
||||
@@ -264,7 +264,7 @@ resource "kubernetes_deployment" "main" {
|
||||
container {
|
||||
name = "dev"
|
||||
image = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image
|
||||
image_pull_policy = "Always"
|
||||
image_pull_policy = "IfNotPresent"
|
||||
security_context {
|
||||
privileged = true
|
||||
}
|
||||
@@ -455,4 +455,4 @@ resource "coder_metadata" "container_info" {
|
||||
key = "cache repo"
|
||||
value = var.cache_repo == "" ? "not enabled" : var.cache_repo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ resource "kubernetes_pod" "main" {
|
||||
name = "dev"
|
||||
# We highly recommend pinning this to a specific release of envbox, as the latest tag may change.
|
||||
image = "ghcr.io/coder/envbox:latest"
|
||||
image_pull_policy = "Always"
|
||||
image_pull_policy = "IfNotPresent"
|
||||
command = ["/envbox", "docker"]
|
||||
|
||||
security_context {
|
||||
@@ -310,4 +310,4 @@ resource "kubernetes_pod" "main" {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ resource "kubernetes_deployment" "main" {
|
||||
container {
|
||||
name = "dev"
|
||||
image = "codercom/enterprise-base:ubuntu"
|
||||
image_pull_policy = "Always"
|
||||
image_pull_policy = "IfNotPresent"
|
||||
command = ["sh", "-c", coder_agent.main.init_script]
|
||||
security_context {
|
||||
run_as_user = "1000"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,11 @@
|
||||
---
|
||||
display_name: "Austin"
|
||||
bio: "IT Pro by day, script kiddie at night."
|
||||
avatar: "./.images/avatar.png"
|
||||
github: "djarbz"
|
||||
status: "community"
|
||||
---
|
||||
|
||||
# Austin
|
||||
|
||||
I like to program as a hobby.
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
display_name: copyparty
|
||||
description: A web based file explorer alternative to Filebrowser.
|
||||
icon: ../../../../.icons/copyparty.svg
|
||||
verified: false
|
||||
tags: [files, filebrowser, web, copyparty]
|
||||
---
|
||||
|
||||
# copyparty
|
||||
|
||||
<!-- Describes what this module does -->
|
||||
|
||||
This module installs Copyparty, an alternative to Filebrowser.
|
||||
[Copyparty](https://github.com/9001/copyparty) is a portable file server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps
|
||||
|
||||
```tf
|
||||
module "copyparty" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/djarbz/copyparty/coder"
|
||||
version = "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
<!-- Add a screencast or screenshot here put them in .images directory -->
|
||||
|
||||

|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1
|
||||
|
||||
Some basic command line options:
|
||||
|
||||
```tf
|
||||
module "copyparty" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/djarbz/copyparty/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
arguments = [
|
||||
"-v", "/home/coder/:/home:r", # Share home directory (read-only)
|
||||
"-v", "${local.repo_dir}:/repo:rw", # Share project directory (read-write)
|
||||
"-e2dsa", # Enables general file indexing"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2
|
||||
|
||||
```tf
|
||||
module "copyparty" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/djarbz/copyparty/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
subdomain = true
|
||||
arguments = [
|
||||
"-v", "/tmp:/tmp:r", # Share tmp directory (read-only)
|
||||
"-v", "/home/coder/:/home:rw", # Share home directory (read-write)
|
||||
"-v", "${local.root_dir}:/work:A:c,dotsrch", # Share work directory (All Perms)
|
||||
"-e2dsa", # Enables general file indexing"
|
||||
"--re-maxage", "900", # Rescan filesystem for changes every SEC
|
||||
"--see-dots", # Show dotfiles by default if user has correct permissions on volume
|
||||
"--xff-src=lan", # List of trusted reverse-proxy CIDRs (comma-separated) or `lan` for private IPs.
|
||||
"--rproxy", "1", # Which ip to associate clients with, index of X-FWD IP.
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,181 @@
|
||||
# --- Test Case 1: Required Variables ---
|
||||
run "plan_with_required_vars" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "example-agent-id"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Test Case 2: Coder App URL uses custom port ---
|
||||
run "app_url_uses_port" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "example-agent-id"
|
||||
port = 19999
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.copyparty.url == "http://localhost:19999"
|
||||
error_message = "Expected copyparty app URL to include configured port"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Test Case 3: Default Values ---
|
||||
run "test_defaults" {
|
||||
# This run block applies the module with default values
|
||||
# (except for the required 'agent_id' provided above).
|
||||
|
||||
variables {
|
||||
agent_id = "example-agent-id"
|
||||
}
|
||||
|
||||
# --- Asserts for coder_app "copyparty" ---
|
||||
assert {
|
||||
condition = resource.coder_app.copyparty.display_name == "copyparty"
|
||||
error_message = "Default display_name is incorrect"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.copyparty.slug == "copyparty"
|
||||
error_message = "Default slug is incorrect"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.copyparty.url == "http://localhost:3923"
|
||||
error_message = "Default URL is incorrect, expected port 3923"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.copyparty.subdomain == false
|
||||
error_message = "Default subdomain should be false"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.copyparty.share == "owner"
|
||||
error_message = "Default share value should be 'owner'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.copyparty.open_in == "slim-window"
|
||||
error_message = "Default open_in value should be 'slim-window'"
|
||||
}
|
||||
|
||||
# --- Asserts for coder_script "copyparty" ---
|
||||
assert {
|
||||
condition = coder_script.copyparty.display_name == "copyparty"
|
||||
error_message = "Script display_name is incorrect"
|
||||
}
|
||||
|
||||
# Check rendered script content (this assumes your run.sh uses the variables)
|
||||
assert {
|
||||
condition = strcontains(coder_script.copyparty.script, "PORT=\"3923\"")
|
||||
error_message = "Script content does not reflect default port"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(coder_script.copyparty.script, "LOG_PATH=\"/tmp/copyparty.log\"")
|
||||
error_message = "Script content does not reflect default log_path"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(coder_script.copyparty.script, "IFS=',' read -r -a ARGUMENTS \u003c\u003c\u003c \"\"")
|
||||
error_message = "Script content does not reflect default empty arguments"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Test Case 4: Custom Values ---
|
||||
run "test_custom_values" {
|
||||
# Override default variables for this specific run
|
||||
variables {
|
||||
agent_id = "example-agent-id"
|
||||
port = 8080
|
||||
slug = "my-custom-app"
|
||||
display_name = "My Custom App"
|
||||
share = "authenticated"
|
||||
open_in = "tab"
|
||||
pinned_version = "v1.2.3"
|
||||
arguments = ["--verbose", "-v"]
|
||||
log_path = "/var/log/custom.log"
|
||||
}
|
||||
|
||||
# --- Asserts for coder_app "copyparty" ---
|
||||
assert {
|
||||
condition = resource.coder_app.copyparty.display_name == "My Custom App"
|
||||
error_message = "Custom display_name was not applied"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.copyparty.slug == "my-custom-app"
|
||||
error_message = "Custom slug was not applied"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.copyparty.url == "http://localhost:8080"
|
||||
error_message = "Custom port was not applied to URL"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.copyparty.share == "authenticated"
|
||||
error_message = "Custom share value was not applied"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.copyparty.open_in == "tab"
|
||||
error_message = "Custom open_in value was not applied"
|
||||
}
|
||||
|
||||
# --- Asserts for coder_script "copyparty" ---
|
||||
assert {
|
||||
condition = strcontains(coder_script.copyparty.script, "PORT=\"8080\"")
|
||||
error_message = "Script content does not reflect custom port"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(coder_script.copyparty.script, "PINNED_VERSION=\"v1.2.3\"")
|
||||
error_message = "Script content does not reflect custom pinned_version"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(coder_script.copyparty.script, "IFS=',' read -r -a ARGUMENTS \u003c\u003c\u003c \"--verbose,-v\"")
|
||||
error_message = "Script content does not reflect custom arguments"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(coder_script.copyparty.script, "LOG_PATH=\"/var/log/custom.log\"")
|
||||
error_message = "Script content does not reflect custom log_path"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Test Case 5: Validation Failure (open_in) ---
|
||||
run "test_invalid_open_in" {
|
||||
# This is a 'plan' test that expects a failure
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "example-agent-id"
|
||||
open_in = "invalid-value"
|
||||
}
|
||||
|
||||
# Expect this plan to fail due to the validation rule in 'var.open_in'
|
||||
expect_failures = [
|
||||
var.open_in,
|
||||
]
|
||||
}
|
||||
|
||||
# --- Test Case 6: Validation Failure (share) ---
|
||||
run "test_invalid_share" {
|
||||
# This is a 'plan' test that expects a failure
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "example-agent-id"
|
||||
share = "everyone" # This is not 'owner', 'authenticated', or 'public'
|
||||
}
|
||||
|
||||
# Expect this plan to fail due to the validation rule in 'var.share'
|
||||
expect_failures = [
|
||||
var.share,
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
# A built-in icon like "/icon/code.svg" or a full URL of icon
|
||||
icon_url = "/icon/copyparty.svg"
|
||||
# a map of all possible values
|
||||
# options = {
|
||||
# "Option 1" = {
|
||||
# "name" = "Option 1",
|
||||
# "value" = "1"
|
||||
# "icon" = "/emojis/1.png"
|
||||
# }
|
||||
# "Option 2" = {
|
||||
# "name" = "Option 2",
|
||||
# "value" = "2"
|
||||
# "icon" = "/emojis/2.png"
|
||||
# }
|
||||
# }
|
||||
}
|
||||
|
||||
# Add required variables for your modules and remove any unneeded variables
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "log_path" {
|
||||
type = string
|
||||
description = "The path to log copyparty to."
|
||||
default = "/tmp/copyparty.log"
|
||||
}
|
||||
|
||||
variable "port" {
|
||||
type = number
|
||||
description = "ports to listen on (comma/range); ignored for unix-sockets (default: 3923)"
|
||||
default = 3923
|
||||
}
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug for the copyparty application."
|
||||
default = "copyparty"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name for the copyparty application."
|
||||
default = "copyparty"
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "open_in" {
|
||||
type = string
|
||||
description = <<-EOT
|
||||
Determines where the app will be opened. Valid values are `"tab"` and `"slim-window" (default)`.
|
||||
`"tab"` opens in a new tab in the same browser window.
|
||||
`"slim-window"` opens a new browser window without navigation controls.
|
||||
EOT
|
||||
default = "slim-window"
|
||||
validation {
|
||||
condition = contains(["tab", "slim-window"], var.open_in)
|
||||
error_message = "The 'open_in' variable must be one of: 'tab', 'slim-window'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = <<-EOT
|
||||
Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder.
|
||||
If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible.
|
||||
EOT
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "share" {
|
||||
type = string
|
||||
default = "owner"
|
||||
validation {
|
||||
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
|
||||
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
|
||||
}
|
||||
}
|
||||
|
||||
# variable "mutable" {
|
||||
# type = bool
|
||||
# description = "Whether the parameter is mutable."
|
||||
# default = true
|
||||
# }
|
||||
|
||||
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
|
||||
}
|
||||
# Add other variables here
|
||||
|
||||
variable "pinned_version" {
|
||||
type = string
|
||||
description = "Install a specific version in semver format (v1.19.16)."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "arguments" {
|
||||
type = list(string)
|
||||
description = "A list of arguments to pass to the application."
|
||||
default = []
|
||||
}
|
||||
|
||||
|
||||
resource "coder_script" "copyparty" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "copyparty"
|
||||
icon = local.icon_url
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
LOG_PATH : var.log_path,
|
||||
PORT : var.port,
|
||||
PINNED_VERSION : var.pinned_version,
|
||||
ARGUMENTS : join(",", var.arguments),
|
||||
})
|
||||
run_on_start = true
|
||||
run_on_stop = false
|
||||
}
|
||||
|
||||
resource "coder_app" "copyparty" {
|
||||
agent_id = var.agent_id
|
||||
slug = var.slug
|
||||
display_name = var.display_name
|
||||
url = "http://localhost:${var.port}"
|
||||
icon = local.icon_url
|
||||
subdomain = var.subdomain
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
open_in = var.open_in
|
||||
|
||||
# Remove if the app does not have a healthcheck endpoint
|
||||
healthcheck {
|
||||
url = "http://localhost:${var.port}"
|
||||
interval = 5
|
||||
threshold = 6
|
||||
}
|
||||
}
|
||||
|
||||
# data "coder_parameter" "copyparty" {
|
||||
# type = "list(string)"
|
||||
# name = "copyparty"
|
||||
# display_name = "copyparty"
|
||||
# icon = local.icon_url
|
||||
# mutable = var.mutable
|
||||
# default = local.options["Option 1"]["value"]
|
||||
|
||||
# dynamic "option" {
|
||||
# for_each = local.options
|
||||
# content {
|
||||
# icon = option.value.icon
|
||||
# name = option.value.name
|
||||
# value = option.value.value
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Convert templated variables to shell variables
|
||||
# This variable is assigned to itself, so the assignment does nothing.
|
||||
# shellcheck disable=SC2269
|
||||
LOG_PATH="${LOG_PATH}"
|
||||
|
||||
# Ports to listen on (comma/range); ignored for unix-sockets (default: 3923)
|
||||
PORT="${PORT}"
|
||||
# Pinned version (e.g., v1.19.16); overrides latest release discovery if set
|
||||
PINNED_VERSION="${PINNED_VERSION}"
|
||||
# Custom CLI Arguments# The variable from Terraform is a single, comma-separated string.
|
||||
# We need to split it into a proper bash array using the comma (,) as the delimiter.
|
||||
IFS=',' read -r -a ARGUMENTS <<< "${ARGUMENTS}"
|
||||
|
||||
# VARIABLE appears unused. Verify use (or export if used externally).
|
||||
# shellcheck disable=SC2034
|
||||
MODULE_NAME="Copyparty"
|
||||
|
||||
# VARIABLE appears unused. Verify use (or export if used externally).
|
||||
# shellcheck disable=SC2034
|
||||
BOLD='\033[0;1m'
|
||||
|
||||
printf '%sInstalling %s ...\n\n' "$${BOLD}" "$${MODULE_NAME}"
|
||||
|
||||
# Add code here
|
||||
# Use variables from the templatefile function in main.tf
|
||||
# e.g. LOG_PATH, PORT, etc.
|
||||
|
||||
printf "🐍 Verifying Python 3 installation...\n"
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
printf "❌ Python3 could not be found. Please install it to continue.\n"
|
||||
exit 1
|
||||
fi
|
||||
printf "✅ Python3 is installed.\n\n"
|
||||
|
||||
RELEASE_TO_INSTALL=""
|
||||
# Install provided version to pin, otherwise discover latest github release from `https://github.com/9001/copyparty`.
|
||||
if [[ -n "$${PINNED_VERSION}" ]]; then
|
||||
printf "📌 Pinned version specified: %s\n" "$${PINNED_VERSION}"
|
||||
# Verify that it is in v#.#.# format
|
||||
if [[ ! "$${PINNED_VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
printf "❌ Invalid format for PINNED_VERSION. Expected 'v#.#.#' (e.g., v1.19.16).\n"
|
||||
exit 1
|
||||
fi
|
||||
RELEASE_TO_INSTALL="$${PINNED_VERSION}"
|
||||
printf "✅ Using pinned version %s.\n\n" "$${RELEASE_TO_INSTALL}"
|
||||
else
|
||||
printf "🔎 Discovering latest release from GitHub...\n"
|
||||
# Use curl to get the latest release tag from the GitHub API and sed to parse it
|
||||
LATEST_RELEASE=$(curl -fsSL https://api.github.com/repos/9001/copyparty/releases/latest | grep '"tag_name":' | sed -E 's/.*"(v[^"]+)".*/\1/')
|
||||
if [[ -z "$${LATEST_RELEASE}" ]]; then
|
||||
printf "❌ Could not determine the latest release. Please check your internet connection.\n"
|
||||
exit 1
|
||||
fi
|
||||
RELEASE_TO_INSTALL="$${LATEST_RELEASE}"
|
||||
printf "🏷️ Latest release is %s.\n\n" "$${RELEASE_TO_INSTALL}"
|
||||
fi
|
||||
|
||||
# Download appropriate release version assets: `copyparty-sfx.py` and `helptext.html`.
|
||||
printf "🚀 Downloading copyparty v%s...\n" "$${RELEASE_TO_INSTALL}"
|
||||
DOWNLOAD_URL="https://github.com/9001/copyparty/releases/download/$${RELEASE_TO_INSTALL}"
|
||||
|
||||
printf "⏬ Downloading copyparty-sfx.py...\n"
|
||||
if ! curl -fsSL -o /tmp/copyparty-sfx.py "$${DOWNLOAD_URL}/copyparty-sfx.py"; then
|
||||
printf "❌ Failed to download copyparty-sfx.py.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "⏬ Downloading helptext.html...\n"
|
||||
if ! curl -fsSL -o /tmp/helptext.html "$${DOWNLOAD_URL}/helptext.html"; then
|
||||
# This is not a fatal error, just a warning.
|
||||
printf "⚠️ Could not download helptext.html. The application will still work.\n"
|
||||
fi
|
||||
|
||||
chmod +x /tmp/copyparty-sfx.py
|
||||
printf "✅ Download complete.\n\n"
|
||||
|
||||
printf "🥳 Installation complete!\n\n"
|
||||
|
||||
# Build a clean, quoted string of the command for logging purposes only.
|
||||
log_command="python3 /tmp/copyparty-sfx.py -p '$${PORT}'"
|
||||
for arg in "$${ARGUMENTS[@]}"; do
|
||||
# printf "DEBUG: ARG [$${arg}]\n"
|
||||
log_command+=" '$${arg}'"
|
||||
done
|
||||
|
||||
# Clear the log file and write the header and command string using printf.
|
||||
{
|
||||
printf "=== Starting copyparty at %s ===\n" "$(date)"
|
||||
printf "EXECUTING: %s\n" "$${log_command}"
|
||||
} > "$${LOG_PATH}"
|
||||
|
||||
printf "👷 Starting %s in background...\n\n" "$${MODULE_NAME}"
|
||||
|
||||
# Execute the actual command using the robust array expansion.
|
||||
# Then, append its output (stdout and stderr) to the log file.
|
||||
python3 /tmp/copyparty-sfx.py -p "$${PORT}" "$${ARGUMENTS[@]}" >> "$${LOG_PATH}" 2>&1 &
|
||||
|
||||
printf "✅ Service started. Check logs at %s\n\n" "$${LOG_PATH}"
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
display_name: "NFS K8s Deployment"
|
||||
description: "Mount an NFS share to a Coder K8s workspace"
|
||||
icon: "../../../../.icons/folder.svg"
|
||||
verified: false
|
||||
tags: ["kubernetes", "shared-dir", "nfs"]
|
||||
---
|
||||
|
||||
# NFS K8s Deployment
|
||||
|
||||
This template provisions a Coder workspace as a Kubernetes Deployment, with an NFS share mounted
|
||||
as a volume. The NFS share will synchronize the server-side files onto the client (Coder workspace)
|
||||
When you stop the Coder workspace and rebuild, the NFS share will be re-mounted, and the changes persisted.
|
||||
|
||||
Note the `volume` and `volume_mount` blocks in the deployment and container spec,
|
||||
respectively:
|
||||
|
||||
```terraform
|
||||
resource "kubernetes_deployment" "main" {
|
||||
spec {
|
||||
template {
|
||||
spec {
|
||||
container {
|
||||
volume_mount {
|
||||
mount_path = data.coder_parameter.nfs_mount_path.value # mount path in the container
|
||||
name = "nfs-share"
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "nfs-share"
|
||||
nfs {
|
||||
path = data.coder_parameter.nfs_mount_path.value # path to be exported from the server
|
||||
server = data.coder_parameter.nfs_server.value # server IP address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## server-side configuration
|
||||
|
||||
1. Create an NFS mount on the server for the clients to access:
|
||||
|
||||
```console
|
||||
export NFS_MNT_PATH=/mnt/nfs_share
|
||||
# Create directory to shaare
|
||||
sudo mkdir -p $NFS_MNT_PATH
|
||||
# Assign UID & GIDs access
|
||||
sudo chown -R uid:gid $NFS_MNT_PATH
|
||||
sudo chmod 777 $NFS_MNT_PATH
|
||||
```
|
||||
|
||||
1. Grant access to the client by updating the `/etc/exports` file, which
|
||||
controls the directories shared with remote clients. See
|
||||
[Red Hat's docs for more information about the configuration options](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/5/html/deployment_guide/s1-nfs-server-config-exports).
|
||||
|
||||
```console
|
||||
# Provides read/write access to clients accessing the NFS from any IP address.
|
||||
/mnt/nfs_share *(rw,sync,no_subtree_check)
|
||||
```
|
||||
|
||||
1. Export the NFS file share directory. You must do this every time you change
|
||||
`/etc/exports`.
|
||||
|
||||
```console
|
||||
sudo exportfs -a
|
||||
sudo systemctl restart <nfs-package>
|
||||
```
|
||||
@@ -0,0 +1,348 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "coder" {
|
||||
}
|
||||
|
||||
provider "kubernetes" {
|
||||
config_path = var.use_kubeconfig == true ? "~/.kube/config" : null
|
||||
}
|
||||
|
||||
variable "use_kubeconfig" {
|
||||
type = bool
|
||||
description = <<-EOF
|
||||
Use host kubeconfig? (true/false)
|
||||
|
||||
Set this to false if the Coder host is itself running as a Pod on the same
|
||||
Kubernetes cluster as you are deploying workspaces to.
|
||||
|
||||
Set this to true if the Coder host is running outside the Kubernetes cluster
|
||||
for workspaces. A valid "~/.kube/config" must be present on the Coder host.
|
||||
EOF
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "namespace" {
|
||||
type = string
|
||||
description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces). If the Coder host is itself running as a Pod on the same Kubernetes cluster as you are deploying workspaces to, set this to the same namespace."
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
data "coder_parameter" "cpu" {
|
||||
name = "cpu"
|
||||
display_name = "CPU"
|
||||
description = "The number of CPU cores"
|
||||
default = "2"
|
||||
icon = "/icon/memory.svg"
|
||||
mutable = true
|
||||
option {
|
||||
name = "2 Cores"
|
||||
value = "2"
|
||||
}
|
||||
option {
|
||||
name = "4 Cores"
|
||||
value = "4"
|
||||
}
|
||||
option {
|
||||
name = "6 Cores"
|
||||
value = "6"
|
||||
}
|
||||
option {
|
||||
name = "8 Cores"
|
||||
value = "8"
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "memory" {
|
||||
name = "memory"
|
||||
display_name = "Memory"
|
||||
description = "The amount of memory in GB"
|
||||
default = "2"
|
||||
icon = "/icon/memory.svg"
|
||||
mutable = true
|
||||
option {
|
||||
name = "2 GB"
|
||||
value = "2"
|
||||
}
|
||||
option {
|
||||
name = "4 GB"
|
||||
value = "4"
|
||||
}
|
||||
option {
|
||||
name = "6 GB"
|
||||
value = "6"
|
||||
}
|
||||
option {
|
||||
name = "8 GB"
|
||||
value = "8"
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "home_disk_size" {
|
||||
name = "home_disk_size"
|
||||
display_name = "Home disk size"
|
||||
description = "The size of the home disk in GB"
|
||||
default = "10"
|
||||
type = "number"
|
||||
icon = "/emojis/1f4be.png"
|
||||
mutable = false
|
||||
validation {
|
||||
min = 1
|
||||
max = 99999
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "nfs_server" {
|
||||
name = "nfs_server"
|
||||
type = "string"
|
||||
display_name = "NFS Server IP"
|
||||
description = "The NFS server IP address to use for the workspace"
|
||||
}
|
||||
|
||||
data "coder_parameter" "nfs_mount_path" {
|
||||
name = "nfs_mount_path"
|
||||
type = "string"
|
||||
display_name = "NFS Mount Path"
|
||||
description = "The path in your workspace container to mount the NFS share to"
|
||||
default = "/mnt/nfs-share"
|
||||
validation {
|
||||
regex = "^/[a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)*$"
|
||||
error = "NFS mount path must be a valid path in your workspace container"
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_agent" "coder" {
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
|
||||
# The following metadata blocks are optional. They are used to display
|
||||
# information about your workspace in the dashboard. You can remove them
|
||||
# if you don't want to display any information.
|
||||
# For basic resources, you can use the `coder stat` command.
|
||||
# If you need more control, you can write your own script.
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Home Disk"
|
||||
key = "3_home_disk"
|
||||
script = "coder stat disk --path $${HOME}"
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage (Host)"
|
||||
key = "4_cpu_usage_host"
|
||||
script = "coder stat cpu --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory Usage (Host)"
|
||||
key = "5_mem_usage_host"
|
||||
script = "coder stat mem --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Load Average (Host)"
|
||||
key = "6_load_host"
|
||||
# get load avg scaled by number of cores
|
||||
script = <<EOT
|
||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||
EOT
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.3.1"
|
||||
agent_id = coder_agent.coder.id
|
||||
accept_license = true
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "main" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
depends_on = [
|
||||
kubernetes_persistent_volume_claim.home
|
||||
]
|
||||
wait_for_rollout = false
|
||||
metadata {
|
||||
name = "coder-${data.coder_workspace.me.id}"
|
||||
namespace = var.namespace
|
||||
labels = {
|
||||
"app.kubernetes.io/name" = "coder-workspace"
|
||||
"app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}"
|
||||
"app.kubernetes.io/part-of" = "coder"
|
||||
"com.coder.resource" = "true"
|
||||
"com.coder.workspace.id" = data.coder_workspace.me.id
|
||||
"com.coder.workspace.name" = data.coder_workspace.me.name
|
||||
"com.coder.user.id" = data.coder_workspace_owner.me.id
|
||||
"com.coder.user.username" = data.coder_workspace_owner.me.name
|
||||
}
|
||||
annotations = {
|
||||
"com.coder.user.email" = data.coder_workspace_owner.me.email
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
replicas = 1
|
||||
selector {
|
||||
match_labels = {
|
||||
"app.kubernetes.io/name" = "coder-workspace"
|
||||
"app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}"
|
||||
"app.kubernetes.io/part-of" = "coder"
|
||||
"com.coder.resource" = "true"
|
||||
"com.coder.workspace.id" = data.coder_workspace.me.id
|
||||
"com.coder.workspace.name" = data.coder_workspace.me.name
|
||||
"com.coder.user.id" = data.coder_workspace_owner.me.id
|
||||
"com.coder.user.username" = data.coder_workspace_owner.me.name
|
||||
}
|
||||
}
|
||||
strategy {
|
||||
type = "Recreate"
|
||||
}
|
||||
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
"app.kubernetes.io/name" = "coder-workspace"
|
||||
"app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}"
|
||||
"app.kubernetes.io/part-of" = "coder"
|
||||
"com.coder.resource" = "true"
|
||||
"com.coder.workspace.id" = data.coder_workspace.me.id
|
||||
"com.coder.workspace.name" = data.coder_workspace.me.name
|
||||
"com.coder.user.id" = data.coder_workspace_owner.me.id
|
||||
"com.coder.user.username" = data.coder_workspace_owner.me.name
|
||||
}
|
||||
}
|
||||
spec {
|
||||
|
||||
container {
|
||||
name = "dev"
|
||||
image = "codercom/enterprise-base:ubuntu"
|
||||
image_pull_policy = "Always"
|
||||
command = ["sh", "-c", coder_agent.coder.init_script]
|
||||
env {
|
||||
name = "CODER_AGENT_TOKEN"
|
||||
value = coder_agent.coder.token
|
||||
}
|
||||
resources {
|
||||
requests = {
|
||||
"cpu" = "250m"
|
||||
"memory" = "512Mi"
|
||||
}
|
||||
limits = {
|
||||
"cpu" = "${data.coder_parameter.cpu.value}"
|
||||
"memory" = "${data.coder_parameter.memory.value}Gi"
|
||||
}
|
||||
}
|
||||
volume_mount {
|
||||
mount_path = "/home/${lower(data.coder_workspace_owner.me.name)}"
|
||||
name = "home"
|
||||
read_only = false
|
||||
}
|
||||
volume_mount {
|
||||
mount_path = data.coder_parameter.nfs_mount_path.value
|
||||
name = "nfs-share"
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "home"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.home.metadata.0.name
|
||||
read_only = false
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "nfs-share"
|
||||
nfs {
|
||||
path = data.coder_parameter.nfs_mount_path.value
|
||||
server = data.coder_parameter.nfs_server.value
|
||||
}
|
||||
}
|
||||
|
||||
affinity {
|
||||
// This affinity attempts to spread out all workspace pods evenly across
|
||||
// nodes.
|
||||
pod_anti_affinity {
|
||||
preferred_during_scheduling_ignored_during_execution {
|
||||
weight = 1
|
||||
pod_affinity_term {
|
||||
topology_key = "kubernetes.io/hostname"
|
||||
label_selector {
|
||||
match_expressions {
|
||||
key = "app.kubernetes.io/name"
|
||||
operator = "In"
|
||||
values = ["coder-workspace"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "home" {
|
||||
metadata {
|
||||
name = "${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace_owner.me.name)}-home"
|
||||
namespace = var.namespace
|
||||
labels = {
|
||||
"app.kubernetes.io/name" = "coder-pvc"
|
||||
"app.kubernetes.io/instance" = "coder-pvc-${data.coder_workspace.me.id}"
|
||||
"app.kubernetes.io/part-of" = "coder"
|
||||
//Coder-specific labels.
|
||||
"com.coder.resource" = "true"
|
||||
"com.coder.workspace.id" = data.coder_workspace.me.id
|
||||
"com.coder.workspace.name" = data.coder_workspace.me.name
|
||||
"com.coder.user.id" = data.coder_workspace_owner.me.id
|
||||
"com.coder.user.username" = data.coder_workspace_owner.me.name
|
||||
}
|
||||
annotations = {
|
||||
"com.coder.user.email" = data.coder_workspace_owner.me.email
|
||||
}
|
||||
}
|
||||
wait_until_bound = false
|
||||
spec {
|
||||
access_modes = ["ReadWriteOnce"]
|
||||
resources {
|
||||
requests = {
|
||||
storage = "${data.coder_parameter.home_disk_size.value}Gi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user