Compare commits

..

11 Commits

Author SHA1 Message Date
Austen Bruhn a7a4ae4cdf Merge branch 'main' into aws-cli-module 2025-11-18 14:34:14 -07:00
Austen Bruhn be4dc937b5 Address PR review comments
- Add set -e for strict error handling
- Exit on version mismatch with clear error message
- Use >> for log appending (was overwriting with >)
- Add error checking after curl, unzip, and install commands
- Add macOS architecture-specific download URLs
- Move autocomplete config after successful installation verification
2025-11-15 18:33:05 -07:00
Austen Bruhn 7a70127f35 Add download_url parameter for airgapped environment support 2025-11-15 18:20:24 -07:00
Austen Bruhn 2039c3908b Update namespace profile with personal info and GitHub avatar 2025-11-15 18:12:47 -07:00
Austen Bruhn eef8697f52 Merge remote-tracking branch 'origin/main' into aws-cli-module 2025-11-15 17:54:32 -07:00
Austen Bruhn 3393a04272 Add AWS CLI autocomplete support for bash, zsh, and fish shells 2025-11-15 17:50:21 -07:00
Austen Bruhn 75fd523215 Add aws-cli module for ausbru87 namespace 2025-11-15 17:23:42 -07:00
Austen Bruhn 5564a552ba Remove trailing whitespace from README.md
Fix formatting issue on line 56 that was causing CI check to fail
2025-11-15 17:01:21 -07:00
Austen Bruhn fca4fdf2ef Fix formatting issues in AWS CLI module
- Format README.md tables with proper column alignment
- Add spaces around redirect operators in run.sh
- Remove trailing whitespace
- Update bun.lock with installed dependencies
2025-11-15 16:57:30 -07:00
Austen Bruhn c22fb8bcdc Restructure AWS CLI module to personal namespace
- Create ausbru87 namespace with avatar and README
- Move aws-cli module from registry/coder to registry/ausbru87
- Update all source paths to registry.coder.com/modules/ausbru87/aws-cli
- Follows contribution guidelines for community modules
2025-11-15 16:54:36 -07:00
Austen Bruhn fdaaba7378 Add AWS CLI module for Coder Registry
- Installs AWS CLI v2 in Coder workspaces
- Supports x86_64 and ARM64 architectures with auto-detection
- Allows version pinning or installs latest version
- Optional GPG signature verification for security
- Idempotent installation (skips if already installed)
- Supports custom installation directories
- Includes comprehensive tests (Terraform + TypeScript)
- Full documentation with multiple usage examples
2025-11-15 16:47:07 -07:00
47 changed files with 396 additions and 1658 deletions
+12 -29
View File
@@ -70,38 +70,21 @@ update_readme_version() {
if grep -q "source.*${module_source}" "$readme_path"; then
echo "Updating version references for $namespace/$module_name in $readme_path"
awk -v module_source="$module_source" -v new_version="$new_version" '
/^[[:space:]]*module[[:space:]]/ {
in_module_block = 1
module_content = $0 "\n"
module_has_target_source = 0
next
}
in_module_block {
module_content = module_content $0 "\n"
if ($0 ~ /source.*=/ && $0 ~ module_source) {
module_has_target_source = 1
/source.*=.*/ {
if ($0 ~ module_source) {
in_target_module = 1
} else {
in_target_module = 0
}
if ($0 ~ /^[[:space:]]*}[[:space:]]*$/) {
in_module_block = 0
if (module_has_target_source) {
num_lines = split(module_content, lines, "\n")
for (i = 1; i <= num_lines; i++) {
line = lines[i]
if (line ~ /^[[:space:]]*version[[:space:]]*=/) {
match(line, /^[[:space:]]*/)
indent = substr(line, 1, RLENGTH)
printf "%sversion = \"%s\"\n", indent, new_version
} else {
print line
}
}
} else {
printf "%s", module_content
}
module_content = ""
}
/^[[:space:]]*version[[:space:]]*=/ {
if (in_target_module) {
match($0, /^[[:space]]*/
indent = substr($0, 1, RLENGTH)
print indent "version = \"" new_version "\""
in_target_module = 0
next
}
next
}
{ print }
' "$readme_path" > "${readme_path}.tmp" && mv "${readme_path}.tmp" "$readme_path"
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Run check.sh
run: |
+4 -4
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Detect changed files
uses: dorny/paths-filter@v3
id: filter
@@ -69,7 +69,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
@@ -93,11 +93,11 @@ jobs:
needs: validate-style
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.24.0"
go-version: "1.23.2"
- name: Validate contributors
run: go build ./cmd/readmevalidation && ./readmevalidation
- name: Remove build file artifact
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Authenticate with Google Cloud
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
with:
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: stable
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
issues: write
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
-1
View File
@@ -1 +0,0 @@
<svg width='240' height='300' viewBox='0 0 240 300' fill='none' xmlns='http://www.w3.org/2000/svg'><g clip-path='url(#clip0_1401_86283)'><mask id='mask0_1401_86283' style='mask-type:luminance' maskUnits='userSpaceOnUse' x='0' y='0' width='240' height='300'><path d='M240 0H0V300H240V0Z' fill='white'/></mask><g mask='url(#mask0_1401_86283)'><path d='M180 240H60V120H180V240Z' fill='#4B4646'/><path d='M180 60H60V240H180V60ZM240 300H0V0H240V300Z' fill='#F1ECEC'/></g></g><defs><clipPath id='clip0_1401_86283'><rect width='240' height='300' fill='white'/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 577 B

-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 440 440">
<g id="Layer_6" data-name="Layer 6">
<rect x="12" y="12" width="416" height="416" rx="103" ry="103" fill="#3a78b1" stroke-width="0"/>
</g>
<g id="Layer_8" data-name="Layer 8">
<path d="M83.6,373.6v-178.6c0-63,52.4-143.7,142.7-143.7s144.1,69.7,144.1,141.1c0,122.6-116.6,143.1-116.6,143.1,0,0,7.2-11.5,7.2-29.5s-8-28-8-28c0,0,60-16,60-87s-53-83-86-83c-57,0-88.3,48.5-88.3,84.3v181.9c0,25.9-17.5,23.9-17.5,23.9h-19.2s-18.4,0-18.4-24.4Z" fill="#fff" stroke-width="0"/>
</g>
<g id="Layer_9" data-name="Layer 9">
<circle cx="204.9" cy="306.6" r="48" fill="#fff" stroke-width="0"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 727 B

+1
View File
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "registry",
+4 -4
View File
@@ -1,6 +1,6 @@
module coder.com/coder-registry
go 1.24.0
go 1.23.2
require (
cdr.dev/slog v1.6.1
@@ -20,7 +20,7 @@ require (
github.com/rivo/uniseg v0.4.4 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.29.0 // indirect
)
+10 -10
View File
@@ -51,17 +51,17 @@ go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiM
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg=
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+32
View File
@@ -0,0 +1,32 @@
---
display_name: Austen Bruhn
bio: Solution Engineer at Coder, OSS enthusiast
github: ausbru87
avatar: ./.images/avatar.png
linkedin: https://www.linkedin.com/in/austen-bruhn-b0a646a1/
status: community
---
# ausbru87
Brief description of what this namespace provides. Include information about:
- What types of templates/modules you offer
- Your focus areas (e.g., specific cloud providers, technologies)
- Any special features or configurations
## Templates
List your available templates here:
- **template-name**: Brief description
## Modules
List your available modules here:
- **module-name**: Brief description
## Contributing
If you'd like to contribute to this namespace, please [open an issue](https://github.com/coder/registry/issues) or submit a pull request.
@@ -0,0 +1,78 @@
---
display_name: AWS CLI
description: Install AWS CLI v2 in your workspace
icon: ../../../../.icons/aws.svg
verified: false
tags: [helper, aws, cli]
---
# AWS CLI
Automatically install the [AWS CLI v2](https://aws.amazon.com/cli/) in your Coder workspace with command autocomplete support for bash, zsh, and fish shells.
```tf
module "aws-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/ausbru87/aws-cli/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
## Features
- Installs AWS CLI v2 for Linux and macOS
- Supports x86_64 and ARM64 architectures
- Optional version pinning
- **Auto-configures command autocomplete** for bash, zsh, and fish shells
## Examples
### Basic Installation
```tf
module "aws-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/ausbru87/aws-cli/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
### Pin to Specific Version
```tf
module "aws-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/ausbru87/aws-cli/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
install_version = "2.15.0"
}
```
### Custom Log Path
```tf
module "aws-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/ausbru87/aws-cli/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
log_path = "/var/log/aws-cli.log"
}
```
### Airgapped Environment
Use a custom download URL for environments without internet access to AWS:
```tf
module "aws-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/ausbru87/aws-cli/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
download_url = "https://internal-mirror.company.com/awscli-exe-linux-x86_64.zip"
}
```
@@ -0,0 +1,34 @@
run "required_vars" {
command = plan
variables {
agent_id = "test-agent-id"
}
}
run "with_custom_version" {
command = plan
variables {
agent_id = "test-agent-id"
install_version = "2.15.0"
}
}
run "with_custom_log_path" {
command = plan
variables {
agent_id = "test-agent-id"
log_path = "/var/log/aws-cli.log"
}
}
run "with_custom_download_url" {
command = plan
variables {
agent_id = "test-agent-id"
download_url = "https://internal-mirror.company.com/awscli-exe-linux-x86_64.zip"
}
}
+46
View File
@@ -0,0 +1,46 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "install_version" {
type = string
description = "The version of AWS CLI to install."
default = ""
}
variable "download_url" {
type = string
description = "Custom download URL for AWS CLI. Useful for airgapped environments. If not set, uses the official AWS download URL."
default = ""
}
variable "log_path" {
type = string
description = "The path to the AWS CLI installation log file."
default = "/tmp/aws-cli-install.log"
}
resource "coder_script" "aws-cli" {
agent_id = var.agent_id
display_name = "AWS CLI"
icon = "/icon/aws.svg"
script = templatefile("${path.module}/run.sh", {
LOG_PATH : var.log_path,
VERSION : var.install_version,
DOWNLOAD_URL : var.download_url,
})
run_on_start = true
run_on_stop = false
}
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env sh
set -e
LOG_PATH=${LOG_PATH}
VERSION=${VERSION}
DOWNLOAD_URL=${DOWNLOAD_URL}
BOLD='\\033[0;1m'
RESET='\\033[0m'
printf "${BOLD}Installing AWS CLI...\\n${RESET}"
# Check if AWS CLI is already installed
if command -v aws > /dev/null 2>&1; then
INSTALLED_VERSION=$(aws --version 2>&1 | cut -d' ' -f1 | cut -d'/' -f2)
if [ -n "$VERSION" ] && [ "$INSTALLED_VERSION" != "$VERSION" ]; then
printf "❌ AWS CLI $INSTALLED_VERSION is installed, but version $VERSION was requested.\\n"
printf "Note: AWS CLI installer does not support version-specific installation.\\n"
exit 1
else
printf "AWS CLI is already installed ($INSTALLED_VERSION). Skipping installation.\\n"
exit 0
fi
fi
# Determine OS and architecture
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH="x86_64" ;;
aarch64 | arm64) ARCH="aarch64" ;;
*)
printf "Unsupported architecture: $ARCH\\n" >> "${LOG_PATH}" 2>&1
exit 1
;;
esac
# Install AWS CLI
if [ "$OS" = "linux" ]; then
# Use custom download URL if provided, otherwise use default AWS URL
if [ -z "$DOWNLOAD_URL" ]; then
DOWNLOAD_URL="https://awscli.amazonaws.com/awscli-exe-linux-${ARCH}.zip"
fi
printf "Downloading AWS CLI from $DOWNLOAD_URL...\\n"
curl -fsSL "$DOWNLOAD_URL" -o /tmp/awscliv2.zip >> "${LOG_PATH}" 2>&1 || exit 1
unzip -q /tmp/awscliv2.zip -d /tmp >> "${LOG_PATH}" 2>&1 || exit 1
sudo /tmp/aws/install >> "${LOG_PATH}" 2>&1 || exit 1
rm -rf /tmp/awscliv2.zip /tmp/aws
elif [ "$OS" = "darwin" ]; then
# Use custom download URL if provided, otherwise use architecture-specific AWS URL
if [ -z "$DOWNLOAD_URL" ]; then
case "$ARCH" in
x86_64)
DOWNLOAD_URL="https://awscli.amazonaws.com/AWSCLIV2-x86_64.pkg"
;;
aarch64)
DOWNLOAD_URL="https://awscli.amazonaws.com/AWSCLIV2-arm64.pkg"
;;
*)
DOWNLOAD_URL="https://awscli.amazonaws.com/AWSCLIV2.pkg"
;;
esac
fi
printf "Downloading AWS CLI from $DOWNLOAD_URL...\\n"
curl -fsSL "$DOWNLOAD_URL" -o /tmp/AWSCLIV2.pkg >> "${LOG_PATH}" 2>&1 || exit 1
sudo installer -pkg /tmp/AWSCLIV2.pkg -target / >> "${LOG_PATH}" 2>&1 || exit 1
rm -f /tmp/AWSCLIV2.pkg
else
printf "Unsupported OS: $OS\\n" >> "${LOG_PATH}" 2>&1
exit 1
fi
# Verify installation was successful
if command -v aws > /dev/null 2>&1; then
printf "🥳 AWS CLI installed successfully!\\n"
aws --version
# Configure autocomplete for common shells
if command -v aws_completer > /dev/null 2>&1; then
AWS_COMPLETER_PATH=$(which aws_completer)
# Bash autocomplete
if [ -f ~/.bashrc ]; then
if ! grep -q "aws_completer.*aws" ~/.bashrc; then
echo "complete -C '$AWS_COMPLETER_PATH' aws" >> ~/.bashrc
printf "✓ Configured AWS CLI autocomplete for bash\\n"
fi
fi
# Zsh autocomplete
if [ -f ~/.zshrc ] || [ -d ~/.oh-my-zsh ]; then
if ! grep -q "aws_completer.*aws" ~/.zshrc 2> /dev/null; then
cat >> ~/.zshrc << ZSHEOF
# AWS CLI autocomplete
autoload bashcompinit && bashcompinit
autoload -Uz compinit && compinit
complete -C '$AWS_COMPLETER_PATH' aws
ZSHEOF
printf "✓ Configured AWS CLI autocomplete for zsh\\n"
fi
fi
# Fish autocomplete
if [ -d ~/.config/fish ] || command -v fish > /dev/null 2>&1; then
mkdir -p ~/.config/fish/completions
FISH_COMPLETION=~/.config/fish/completions/aws.fish
if [ ! -f "$FISH_COMPLETION" ]; then
cat > "$FISH_COMPLETION" << 'FISHEOF'
complete --command aws --no-files --arguments '(begin; set --local --export COMP_SHELL fish; set --local --export COMP_LINE (commandline); aws_completer | sed '"'"'s/ $//'"'"'; end)'
FISHEOF
printf "✓ Configured AWS CLI autocomplete for fish\\n"
fi
fi
fi
else
printf "❌ AWS CLI installation failed. Check logs at ${LOG_PATH}\\n"
exit 1
fi
@@ -1,108 +0,0 @@
---
display_name: OpenCode
icon: ../../../../.icons/opencode.svg
description: Run OpenCode AI coding assistant for AI-powered terminal assistance
verified: false
tags: [agent, opencode, ai, tasks]
---
# OpenCode
Run [OpenCode](https://opencode.ai) AI coding assistant in your workspace for intelligent code generation, analysis, and development assistance. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for seamless task reporting in the Coder UI.
```tf
module "opencode" {
source = "registry.coder.com/coder-labs/opencode/coder"
version = "0.1.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
}
```
## Prerequisites
- **Authentication credentials** - OpenCode auth.json file is required for non-interactive authentication, you can find this file on your system: `$HOME/.local/share/opencode/auth.json`
## Examples
### Basic Usage with Tasks
```tf
resource "coder_ai_task" "task" {
app_id = module.opencode.task_app_id
}
module "opencode" {
source = "registry.coder.com/coder-labs/opencode/coder"
version = "0.1.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
ai_prompt = coder_ai_task.task.prompt
auth_json = <<-EOT
{
"google": {
"type": "api",
"key": "gem-xxx-xxxx"
},
"anthropic": {
"type": "api",
"key": "sk-ant-api03-xxx-xxxxxxx"
}
}
EOT
config_json = jsonencode({
"$schema" = "https://opencode.ai/config.json"
mcp = {
filesystem = {
command = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/home/coder/projects"]
enabled = true
type = "local"
environment = {
SOME_VARIABLE_X = "value"
}
}
playwright = {
command = ["npx", "-y", "@playwright/mcp@latest", "--headless", "--isolated"]
enabled = true
type = "local"
}
}
model = "anthropic/claude-sonnet-4-20250514"
})
pre_install_script = <<-EOT
#!/bin/bash
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
EOT
}
```
### Standalone CLI Mode
Run OpenCode as a command-line tool without web interface or task reporting:
```tf
module "opencode" {
source = "registry.coder.com/coder-labs/opencode/coder"
version = "0.1.0"
agent_id = coder_agent.example.id
workdir = "/home/coder"
report_tasks = false
cli_app = true
}
```
## Troubleshooting
If you encounter any issues, check the log files in the `~/.opencode-module` directory within your workspace for detailed information.
## References
- [Opencode JSON Config](https://opencode.ai/docs/config/)
- [OpenCode Documentation](https://opencode.ai/docs)
- [AgentAPI Documentation](https://github.com/coder/agentapi)
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
@@ -1,362 +0,0 @@
import {
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";
import dedent from "dedent";
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);
}
}
});
interface SetupProps {
skipAgentAPIMock?: boolean;
skipOpencodeMock?: 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_opencode: props?.skipOpencodeMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
workdir: projectDir,
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
if (!props?.skipOpencodeMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/opencode",
content: await loadTestFile(import.meta.dir, "opencode-mock.sh"),
});
}
return { id };
};
setDefaultTimeout(60 * 1000);
describe("opencode", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
test("happy-path", async () => {
const { id } = await setup();
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("install-opencode-version", async () => {
const version_to_install = "0.1.0";
const { id } = await setup({
skipOpencodeMock: true,
moduleVariables: {
install_opencode: "true",
opencode_version: version_to_install,
pre_install_script: dedent`
#!/usr/bin/env bash
set -euo pipefail
# Mock the opencode install for testing
mkdir -p /home/coder/.opencode/bin
echo '#!/bin/bash\necho "opencode mock version ${version_to_install}"' > /home/coder/.opencode/bin/opencode
chmod +x /home/coder/.opencode/bin/opencode
`,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`cat /home/coder/.opencode-module/install.log`,
]);
expect(resp.stdout).toContain(version_to_install);
});
test("check-latest-opencode-version-works", async () => {
const { id } = await setup({
skipOpencodeMock: true,
skipAgentAPIMock: true,
moduleVariables: {
install_opencode: "true",
pre_install_script: dedent`
#!/usr/bin/env bash
set -euo pipefail
# Mock the opencode install for testing
mkdir -p /home/coder/.opencode/bin
echo '#!/bin/bash\necho "opencode mock latest version"' > /home/coder/.opencode/bin/opencode
chmod +x /home/coder/.opencode/bin/opencode
`,
},
});
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("opencode-auth-json", async () => {
const authJson = JSON.stringify({
token: "test-auth-token-123",
user: "test-user",
});
const { id } = await setup({
moduleVariables: {
auth_json: authJson,
},
});
await execModuleScript(id);
const authFile = await readFileContainer(
id,
"/home/coder/.local/share/opencode/auth.json",
);
expect(authFile).toContain("test-auth-token-123");
expect(authFile).toContain("test-user");
});
test("opencode-config-json", async () => {
const configJson = JSON.stringify({
$schema: "https://opencode.ai/config.json",
mcp: {
test: {
command: ["test-cmd"],
type: "local",
},
},
model: "anthropic/claude-sonnet-4-20250514",
});
const { id } = await setup({
moduleVariables: {
config_json: configJson,
},
});
await execModuleScript(id);
const configFile = await readFileContainer(
id,
"/home/coder/.config/opencode/opencode.json",
);
expect(configFile).toContain("test-cmd");
expect(configFile).toContain("anthropic/claude-sonnet-4-20250514");
});
test("opencode-ai-prompt", async () => {
const prompt = "This is a task prompt for OpenCode.";
const { id } = await setup({
moduleVariables: {
ai_prompt: prompt,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`cat /home/coder/.opencode-module/agentapi-start.log`,
]);
expect(resp.stdout).toContain(prompt);
});
test("opencode-continue-flag", async () => {
const { id } = await setup({
moduleVariables: {
continue: "true",
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.opencode-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain("--continue");
});
test("opencode-continue-with-session-id", async () => {
const sessionId = "session-123";
const { id } = await setup({
moduleVariables: {
continue: "true",
session_id: sessionId,
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.opencode-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain("--continue");
expect(startLog.stdout).toContain(`--session ${sessionId}`);
});
test("opencode-session-id", async () => {
const sessionId = "session-123";
const { id } = await setup({
moduleVariables: {
session_id: sessionId,
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.opencode-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain(`--session ${sessionId}`);
});
test("opencode-report-tasks-enabled", async () => {
const { id } = await setup({
moduleVariables: {
report_tasks: "true",
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.opencode-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain(
"report your progress using coder_report_task",
);
});
test("opencode-report-tasks-disabled", async () => {
const { id } = await setup({
moduleVariables: {
report_tasks: "false",
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.opencode-module/agentapi-start.log",
]);
expect(startLog.stdout).not.toContain(
"report your progress using coder_report_task",
);
});
test("cli-app-creation", async () => {
const { id } = await setup({
moduleVariables: {
cli_app: "true",
cli_app_display_name: "OpenCode Terminal",
},
});
await execModuleScript(id);
// CLI app creation is handled by the agentapi module
// We just verify the setup completed successfully
await expectAgentAPIStarted(id);
});
test("pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'opencode-pre-install-script'",
post_install_script: "#!/bin/bash\necho 'opencode-post-install-script'",
},
});
await execModuleScript(id);
const preInstallLog = await readFileContainer(
id,
"/home/coder/.opencode-module/pre_install.log",
);
expect(preInstallLog).toContain("opencode-pre-install-script");
const postInstallLog = await readFileContainer(
id,
"/home/coder/.opencode-module/post_install.log",
);
expect(postInstallLog).toContain("opencode-post-install-script");
});
test("workdir-variable", async () => {
const workdir = "/home/coder/opencode-test-folder";
const { id } = await setup({
skipOpencodeMock: false,
moduleVariables: {
workdir,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.opencode-module/agentapi-start.log",
);
expect(resp).toContain(workdir);
});
test("subdomain-enabled", async () => {
const { id } = await setup({
moduleVariables: {
subdomain: "true",
},
});
await execModuleScript(id);
// Subdomain configuration is handled by the agentapi module
// We just verify the setup completed successfully
await expectAgentAPIStarted(id);
});
test("custom-display-names", async () => {
const { id } = await setup({
moduleVariables: {
web_app_display_name: "Custom OpenCode Web",
cli_app_display_name: "Custom OpenCode CLI",
cli_app: "true",
},
});
await execModuleScript(id);
// Display names are handled by the agentapi module
// We just verify the setup completed successfully
await expectAgentAPIStarted(id);
});
});
@@ -1,203 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.12"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/opencode.svg"
}
variable "workdir" {
type = string
description = "The folder to run OpenCode in."
}
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI via AgentAPI"
default = true
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for OpenCode"
default = false
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app"
default = "OpenCode"
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app"
default = "OpenCode CLI"
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing OpenCode."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing OpenCode."
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.11.2"
}
variable "ai_prompt" {
type = string
description = "Initial task prompt for OpenCode."
default = ""
}
variable "subdomain" {
type = bool
description = "Whether to use a subdomain for AgentAPI."
default = false
}
variable "install_opencode" {
type = bool
description = "Whether to install OpenCode."
default = true
}
variable "opencode_version" {
type = string
description = "The version of OpenCode to install."
default = "latest"
}
variable "continue" {
type = bool
description = "continue the last session. Uses the --continue flag"
default = false
}
variable "session_id" {
type = string
description = "Session id to continue. Passed via --session"
default = ""
}
variable "auth_json" {
type = string
description = "Your auth.json from $HOME/.local/share/opencode/auth.json, Required for non-interactive authentication"
default = ""
}
variable "config_json" {
type = string
description = "OpenCode JSON config. https://opencode.ai/docs/config/"
default = ""
}
locals {
workdir = trimsuffix(var.workdir, "/")
app_slug = "opencode"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".opencode-module"
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "2.0.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
folder = local.workdir
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 -o errexit
set -o pipefail
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
ARG_WORKDIR='${local.workdir}' \
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_SESSION_ID='${var.session_id}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_CONTINUE='${var.continue}' \
/tmp/start.sh
EOT
install_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_OPENCODE_VERSION='${var.opencode_version}' \
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_INSTALL_OPENCODE='${var.install_opencode}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_WORKDIR='${local.workdir}' \
ARG_AUTH_JSON='${var.auth_json != null ? base64encode(replace(var.auth_json, "'", "'\\''")) : ""}' \
ARG_OPENCODE_CONFIG='${var.config_json != null ? base64encode(replace(var.config_json, "'", "'\\''")) : ""}' \
/tmp/install.sh
EOT
}
output "task_app_id" {
value = module.agentapi.task_app_id
}
@@ -1,374 +0,0 @@
run "defaults_are_correct" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
}
assert {
condition = var.install_opencode == true
error_message = "OpenCode installation should be enabled by default"
}
assert {
condition = var.install_agentapi == true
error_message = "AgentAPI installation should be enabled by default"
}
assert {
condition = var.agentapi_version == "v0.11.2"
error_message = "Default AgentAPI version should be 'v0.11.2'"
}
assert {
condition = var.opencode_version == "latest"
error_message = "Default OpenCode version should be 'latest'"
}
assert {
condition = var.report_tasks == true
error_message = "Task reporting should be enabled by default"
}
assert {
condition = var.cli_app == false
error_message = "CLI app should be disabled by default"
}
assert {
condition = var.subdomain == false
error_message = "Subdomain should be disabled by default"
}
assert {
condition = var.web_app_display_name == "OpenCode"
error_message = "Default web app display name should be 'OpenCode'"
}
assert {
condition = var.cli_app_display_name == "OpenCode CLI"
error_message = "Default CLI app display name should be 'OpenCode CLI'"
}
assert {
condition = local.app_slug == "opencode"
error_message = "App slug should be 'opencode'"
}
assert {
condition = local.module_dir_name == ".opencode-module"
error_message = "Module dir name should be '.opencode-module'"
}
assert {
condition = local.workdir == "/home/coder/project"
error_message = "Workdir should be trimmed of trailing slash"
}
assert {
condition = var.continue == false
error_message = "Continue flag should be disabled by default"
}
}
run "workdir_trailing_slash_trimmed" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project/"
}
assert {
condition = local.workdir == "/home/coder/project"
error_message = "Workdir should be trimmed of trailing slash"
}
}
run "opencode_version_configuration" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
opencode_version = "v1.0.0"
}
assert {
condition = var.opencode_version == "v1.0.0"
error_message = "OpenCode version should be set correctly"
}
}
run "agentapi_version_configuration" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
agentapi_version = "v0.9.0"
}
assert {
condition = var.agentapi_version == "v0.9.0"
error_message = "AgentAPI version should be set correctly"
}
}
run "cli_app_configuration" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
cli_app = true
cli_app_display_name = "Custom OpenCode CLI"
}
assert {
condition = var.cli_app == true
error_message = "CLI app should be enabled when specified"
}
assert {
condition = var.cli_app_display_name == "Custom OpenCode CLI"
error_message = "Custom CLI app display name should be set"
}
}
run "web_app_configuration" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
web_app_display_name = "Custom OpenCode Web"
order = 5
group = "AI Tools"
icon = "/custom/icon.svg"
}
assert {
condition = var.web_app_display_name == "Custom OpenCode Web"
error_message = "Custom web app display name should be set"
}
assert {
condition = var.order == 5
error_message = "Custom order should be set"
}
assert {
condition = var.group == "AI Tools"
error_message = "Custom group should be set"
}
assert {
condition = var.icon == "/custom/icon.svg"
error_message = "Custom icon should be set"
}
}
run "ai_configuration_variables" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
ai_prompt = "This is a test prompt"
session_id = "session-123"
continue = true
}
assert {
condition = var.ai_prompt == "This is a test prompt"
error_message = "AI prompt should be set correctly"
}
assert {
condition = var.session_id == "session-123"
error_message = "Session ID should be set correctly"
}
assert {
condition = var.continue == true
error_message = "Continue flag should be set correctly"
}
}
run "auth_json_configuration" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
auth_json = "{\"token\": \"test-token\", \"user\": \"test-user\"}"
}
assert {
condition = var.auth_json != ""
error_message = "Auth JSON should be set"
}
assert {
condition = can(jsondecode(var.auth_json))
error_message = "Auth JSON should be valid JSON"
}
}
run "config_json_configuration" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
config_json = "{\"$schema\": \"https://opencode.ai/config.json\", \"mcp\": {\"test\": {\"command\": [\"test-cmd\"], \"type\": \"local\"}}, \"model\": \"anthropic/claude-sonnet-4-20250514\"}"
}
assert {
condition = var.config_json != ""
error_message = "OpenCode JSON configuration should be set"
}
assert {
condition = can(jsondecode(var.config_json))
error_message = "OpenCode JSON configuration should be valid JSON"
}
}
run "task_reporting_configuration" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
report_tasks = false
}
assert {
condition = var.report_tasks == false
error_message = "Task reporting should be disabled when specified"
}
}
run "subdomain_configuration" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
subdomain = true
}
assert {
condition = var.subdomain == true
error_message = "Subdomain should be enabled when specified"
}
}
run "install_flags_configuration" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
install_opencode = false
install_agentapi = false
}
assert {
condition = var.install_opencode == false
error_message = "OpenCode installation should be disabled when specified"
}
assert {
condition = var.install_agentapi == false
error_message = "AgentAPI installation should be disabled when specified"
}
}
run "custom_scripts_configuration" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
pre_install_script = "#!/bin/bash\necho 'pre-install'"
post_install_script = "#!/bin/bash\necho 'post-install'"
}
assert {
condition = var.pre_install_script != null
error_message = "Pre-install script should be set"
}
assert {
condition = var.post_install_script != null
error_message = "Post-install script should be set"
}
assert {
condition = can(regex("pre-install", var.pre_install_script))
error_message = "Pre-install script should contain expected content"
}
assert {
condition = can(regex("post-install", var.post_install_script))
error_message = "Post-install script should contain expected content"
}
}
run "empty_variables_handled_correctly" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
ai_prompt = ""
session_id = ""
auth_json = ""
config_json = ""
continue = false
}
assert {
condition = var.ai_prompt == ""
error_message = "Empty AI prompt should be handled correctly"
}
assert {
condition = var.session_id == ""
error_message = "Empty session ID should be handled correctly"
}
assert {
condition = var.auth_json == ""
error_message = "Empty auth JSON should be handled correctly"
}
assert {
condition = var.config_json == ""
error_message = "Empty config JSON should be handled correctly"
}
assert {
condition = var.continue == false
error_message = "Continue flag default should be handled correctly"
}
}
run "continue_flag_configuration" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
continue = true
}
assert {
condition = var.continue == true
error_message = "Continue flag should be enabled when specified"
}
}
@@ -1,131 +0,0 @@
#!/bin/bash
set -euo pipefail
command_exists() {
command -v "$1" > /dev/null 2>&1
}
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
ARG_OPENCODE_VERSION=${ARG_OPENCODE_VERSION:-latest}
ARG_INSTALL_OPENCODE=${ARG_INSTALL_OPENCODE:-true}
ARG_AUTH_JSON=$(echo -n "$ARG_AUTH_JSON" | base64 -d 2> /dev/null || echo "")
ARG_OPENCODE_CONFIG=$(echo -n "$ARG_OPENCODE_CONFIG" | base64 -d 2> /dev/null || echo "")
# Print all received environment variables
printf "=== INSTALL CONFIG ===\n"
printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG"
printf "ARG_OPENCODE_VERSION: %s\n" "$ARG_OPENCODE_VERSION"
printf "ARG_INSTALL_OPENCODE: %s\n" "$ARG_INSTALL_OPENCODE"
if [ -n "$ARG_AUTH_JSON" ]; then
printf "ARG_AUTH_JSON: [AUTH DATA RECEIVED]\n"
else
printf "ARG_AUTH_JSON: [NOT PROVIDED]\n"
fi
if [ -n "$ARG_OPENCODE_CONFIG" ]; then
printf "ARG_OPENCODE_CONFIG: [RECEIVED]\n"
else
printf "ARG_OPENCODE_CONFIG: [NOT PROVIDED]\n"
fi
printf "==================================\n"
install_opencode() {
if [ "$ARG_INSTALL_OPENCODE" = "true" ]; then
if ! command_exists opencode; then
echo "Installing OpenCode (version: ${ARG_OPENCODE_VERSION})..."
if [ "$ARG_OPENCODE_VERSION" = "latest" ]; then
curl -fsSL https://opencode.ai/install | bash
else
VERSION=$ARG_OPENCODE_VERSION curl -fsSL https://opencode.ai/install | bash
fi
export PATH=/home/coder/.opencode/bin:$PATH
printf "Opencode location: %s\n" "$(which opencode)"
if ! command_exists opencode; then
echo "ERROR: Failed to install OpenCode"
exit 1
fi
echo "OpenCode installed successfully"
else
echo "OpenCode already installed"
fi
else
echo "OpenCode installation skipped (ARG_INSTALL_OPENCODE=false)"
fi
}
setup_opencode_config() {
local opencode_config_file="$HOME/.config/opencode/opencode.json"
local auth_json_file="$HOME/.local/share/opencode/auth.json"
mkdir -p "$(dirname "$auth_json_file")"
mkdir -p "$(dirname "$opencode_config_file")"
setup_opencode_auth "$auth_json_file"
if [ -n "$ARG_OPENCODE_CONFIG" ]; then
echo "Writing to the config file"
echo "$ARG_OPENCODE_CONFIG" > "$opencode_config_file"
fi
if [ "$ARG_REPORT_TASKS" = "true" ]; then
setup_coder_mcp_server "$opencode_config_file"
fi
echo "MCP configuration completed: $opencode_config_file"
}
setup_opencode_auth() {
local auth_json_file="$1"
if [ -n "$ARG_AUTH_JSON" ]; then
echo "$ARG_AUTH_JSON" > "$auth_json_file"
printf "added auth json to %s" "$auth_json_file"
else
printf "auth json not provided"
fi
}
setup_coder_mcp_server() {
local opencode_config_file="$1"
# Set environment variables based on task reporting setting
echo "Configuring OpenCode task reporting"
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
echo "Coder integration configured for task reporting"
# Add coder MCP server configuration to the JSON file
echo "Adding Coder MCP server configuration"
# Create the coder server configuration JSON
coder_config=$(
cat << EOF
{
"type": "local",
"command": ["coder", "exp", "mcp", "server"],
"enabled": true,
"environment": {
"CODER_MCP_APP_STATUS_SLUG": "${CODER_MCP_APP_STATUS_SLUG:-}",
"CODER_MCP_AI_AGENTAPI_URL": "${CODER_MCP_AI_AGENTAPI_URL:-}",
"CODER_AGENT_URL": "${CODER_AGENT_URL:-}",
"CODER_AGENT_TOKEN": "${CODER_AGENT_TOKEN:-}",
"CODER_MCP_ALLOWED_TOOLS": "coder_report_task"
}
}
EOF
)
temp_file=$(mktemp)
jq --argjson coder_config "$coder_config" '.mcp.coder = $coder_config' "$opencode_config_file" > "$temp_file"
mv "$temp_file" "$opencode_config_file"
echo "Coder MCP server configuration added"
}
install_opencode
setup_opencode_config
echo "OpenCode module setup completed."
@@ -1,71 +0,0 @@
#!/bin/bash
set -euo pipefail
export PATH=/home/coder/.opencode/bin:$PATH
command_exists() {
command -v "$1" > /dev/null 2>&1
}
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
ARG_SESSION_ID=${ARG_SESSION_ID:-}
ARG_CONTINUE=${ARG_CONTINUE:-false}
# Print all received environment variables
printf "=== START CONFIG ===\n"
printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
printf "ARG_CONTINUE: %s\n" "$ARG_CONTINUE"
printf "ARG_SESSION_ID: %s\n" "$ARG_SESSION_ID"
if [ -n "$ARG_AI_PROMPT" ]; then
printf "ARG_AI_PROMPT: [AI PROMPT RECEIVED]\n"
else
printf "ARG_AI_PROMPT: [NOT PROVIDED]\n"
fi
printf "==================================\n"
OPENCODE_ARGS=()
AGENTAPI_ARGS=()
validate_opencode_installation() {
if ! command_exists opencode; then
printf "ERROR: OpenCode not installed. Set install_opencode to true\n"
exit 1
fi
}
build_opencode_args() {
if [ -n "$ARG_SESSION_ID" ]; then
OPENCODE_ARGS+=(--session "$ARG_SESSION_ID")
fi
if [ "$ARG_CONTINUE" = "true" ]; then
OPENCODE_ARGS+=(--continue)
fi
if [ -n "$ARG_AI_PROMPT" ]; then
if [ "$ARG_REPORT_TASKS" = "true" ]; then
PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AI_PROMPT"
else
PROMPT="$ARG_AI_PROMPT"
fi
AGENTAPI_ARGS+=(-I "$PROMPT")
fi
}
start_agentapi() {
printf "Starting in directory: %s\n" "$ARG_WORKDIR"
cd "$ARG_WORKDIR"
build_opencode_args
printf "Running OpenCode with args: %s\n" "${OPENCODE_ARGS[*]}"
echo agentapi server "${AGENTAPI_ARGS[@]}" --type opencode --term-width 67 --term-height 1190 -- opencode "${OPENCODE_ARGS[@]}"
agentapi server "${AGENTAPI_ARGS[@]}" --type opencode --term-width 67 --term-height 1190 -- opencode "${OPENCODE_ARGS[@]}"
}
validate_opencode_installation
start_agentapi
@@ -1,25 +0,0 @@
#!/bin/bash
# Mock OpenCode CLI for testing purposes
# This script simulates the OpenCode command-line interface
echo "OpenCode Mock CLI - Test Version"
echo "Args received: $*"
# Simulate opencode behavior based on arguments
case "$1" in
--version | -v)
echo "opencode mock version 0.1.0-test"
;;
--help | -h)
echo "OpenCode Mock Help"
echo "Usage: opencode [options] [command]"
echo "This is a mock version for testing"
;;
*)
echo "Running OpenCode mock with arguments: $*"
echo "Mock execution completed successfully"
;;
esac
exit 0
+10 -12
View File
@@ -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 = "4.2.2"
version = "4.2.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
@@ -51,7 +51,7 @@ module "claude-code" {
boundary_log_level = "WARN"
boundary_additional_allowed_urls = ["GET *google.com"]
boundary_proxy_port = "8087"
version = "4.2.2"
version = "3.4.3"
}
```
@@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.2"
version = "4.2.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
@@ -89,11 +89,9 @@ module "claude-code" {
mcp = <<-EOF
{
"mcpServers": {
"memory": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-memory"],
"env": {}
"my-custom-tool": {
"command": "my-tool-server"
"args": ["--port", "8080"]
}
}
}
@@ -108,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 = "4.2.2"
version = "4.2.0"
agent_id = coder_agent.example.id
workdir = "/home/coder"
install_claude_code = true
@@ -131,7 +129,7 @@ variable "claude_code_oauth_token" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.2"
version = "4.2.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
@@ -204,7 +202,7 @@ resource "coder_env" "bedrock_api_key" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.2"
version = "4.2.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
@@ -261,7 +259,7 @@ resource "coder_env" "google_application_credentials" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.2"
version = "4.2.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514"
@@ -68,16 +68,13 @@ function setup_claude_configurations() {
mkdir -p "$module_path"
if [ "$ARG_MCP" != "" ]; then
(
cd "$ARG_WORKDIR"
while IFS= read -r server_name && IFS= read -r server_json; do
echo "------------------------"
echo "Executing: claude mcp add-json \"$server_name\" '$server_json' (in $ARG_WORKDIR)"
claude mcp add-json "$server_name" "$server_json"
echo "------------------------"
echo ""
done < <(echo "$ARG_MCP" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)')
)
while IFS= read -r server_name && IFS= read -r server_json; do
echo "------------------------"
echo "Executing: claude mcp add \"$server_name\" '$server_json'"
claude mcp add "$server_name" "$server_json"
echo "------------------------"
echo ""
done < <(echo "$ARG_MCP" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)')
fi
if [ -n "$ARG_ALLOWED_TOOLS" ]; then
@@ -103,13 +103,9 @@ task_session_exists() {
local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
local project_dir="$HOME/.claude/projects/${workdir_normalized}"
printf "PROJECT_DIR: %s, workdir_normalized: %s\n" "$project_dir" "$workdir_normalized"
if [ -d "$project_dir" ] && find "$project_dir" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then
printf "TASK_SESSION_ID: %s file found\n" "$TASK_SESSION_ID"
return 0
else
printf "TASK_SESSION_ID: %s file not found\n" "$TASK_SESSION_ID"
return 1
fi
}
@@ -153,11 +149,7 @@ function start_agentapi() {
else
echo "No existing session found"
if [ "$ARG_REPORT_TASKS" = "true" ]; then
if task_session_exists; then
ARGS+=(--resume "$TASK_SESSION_ID")
else
ARGS+=(--session-id "$TASK_SESSION_ID")
fi
ARGS+=(--session-id "$TASK_SESSION_ID")
fi
if [ -n "$ARG_AI_PROMPT" ]; then
if [ "$ARG_REPORT_TASKS" = "true" ]; then
@@ -179,11 +171,7 @@ function start_agentapi() {
else
echo "Continue disabled, starting fresh session"
if [ "$ARG_REPORT_TASKS" = "true" ]; then
if task_session_exists; then
ARGS+=(--resume "$TASK_SESSION_ID")
else
ARGS+=(--session-id "$TASK_SESSION_ID")
fi
ARGS+=(--session-id "$TASK_SESSION_ID")
fi
if [ -n "$ARG_AI_PROMPT" ]; then
if [ "$ARG_REPORT_TASKS" = "true" ]; then
+6 -6
View File
@@ -14,7 +14,7 @@ Automatically install and run mux in a Coder workspace. By default, the module i
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.0.1"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
@@ -35,7 +35,7 @@ module "mux" {
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.0.1"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
@@ -46,7 +46,7 @@ module "mux" {
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.0.1"
version = "1.0.0"
agent_id = coder_agent.example.id
# Default is "latest"; set to a specific version to pin
install_version = "0.4.0"
@@ -59,7 +59,7 @@ module "mux" {
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.0.1"
version = "1.0.0"
agent_id = coder_agent.example.id
port = 8080
}
@@ -73,7 +73,7 @@ Run an existing copy of mux if found, otherwise install from npm:
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.0.1"
version = "1.0.0"
agent_id = coder_agent.example.id
use_cached = true
}
@@ -87,7 +87,7 @@ Run without installing from the network (requires mux to be pre-installed):
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.0.1"
version = "1.0.0"
agent_id = coder_agent.example.id
install = false
}
+1 -2
View File
@@ -55,7 +55,6 @@ describe("mux", async () => {
expect(output.exitCode).toBe(0);
const expectedLines = [
"📦 Installing mux via npm into /tmp/mux...",
"⏭️ Skipping npm lifecycle scripts with --ignore-scripts",
"🥳 mux has been installed in /tmp/mux",
"🚀 Starting mux server on port 4000...",
"Check logs at /tmp/mux.log!",
@@ -63,5 +62,5 @@ describe("mux", async () => {
for (const line of expectedLines) {
expect(output.stdout).toContain(line);
}
}, 180000);
}, 60000);
});
+1 -2
View File
@@ -50,14 +50,13 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
if [ ! -f package.json ]; then
echo '{}' > package.json
fi
echo "⏭️ Skipping npm lifecycle scripts with --ignore-scripts"
PKG="mux"
if [ -z "${VERSION}" ] || [ "${VERSION}" = "latest" ]; then
PKG_SPEC="$PKG@latest"
else
PKG_SPEC="$PKG@${VERSION}"
fi
if ! npm install --no-audit --no-fund --omit=dev --ignore-scripts "$PKG_SPEC"; then
if ! npm install --no-audit --no-fund --omit=dev "$PKG_SPEC"; then
echo "❌ Failed to install mux via npm"
exit 1
fi
@@ -14,7 +14,7 @@ This module lets you authenticate with [Hashicorp Vault](https://www.vaultprojec
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-github/coder"
version = "1.1.0"
version = "1.0.31"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
}
@@ -46,7 +46,7 @@ To configure the Vault module, you must set up a Vault GitHub auth method. See t
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-github/coder"
version = "1.1.0"
version = "1.0.31"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
coder_github_auth_id = "my-github-auth-id"
@@ -59,7 +59,7 @@ module "vault" {
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-github/coder"
version = "1.1.0"
version = "1.0.31"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
coder_github_auth_id = "my-github-auth-id"
@@ -73,7 +73,7 @@ module "vault" {
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-github/coder"
version = "1.1.0"
version = "1.0.31"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_cli_version = "1.15.0"
@@ -32,12 +32,6 @@ variable "vault_github_auth_path" {
default = "github"
}
variable "vault_namespace" {
type = string
description = "The Vault Enterprise namespace that contains the GitHub auth mount."
default = null
}
variable "vault_cli_version" {
type = string
description = "The version of Vault to install."
@@ -58,7 +52,6 @@ resource "coder_script" "vault" {
AUTH_PATH : var.vault_github_auth_path,
GITHUB_EXTERNAL_AUTH_ID : data.coder_external_auth.github.id,
INSTALL_VERSION : var.vault_cli_version,
VAULT_NAMESPACE : var.vault_namespace != null ? var.vault_namespace : "",
})
run_on_start = true
start_blocks_login = true
@@ -70,13 +63,6 @@ resource "coder_env" "vault_addr" {
value = var.vault_addr
}
resource "coder_env" "vault_namespace" {
count = var.vault_namespace == null ? 0 : 1
agent_id = var.agent_id
name = "VAULT_NAMESPACE"
value = var.vault_namespace
}
data "coder_external_auth" "github" {
id = var.coder_github_auth_id
}
@@ -4,7 +4,6 @@
INSTALL_VERSION=${INSTALL_VERSION}
GITHUB_EXTERNAL_AUTH_ID=${GITHUB_EXTERNAL_AUTH_ID}
AUTH_PATH=${AUTH_PATH}
VAULT_NAMESPACE=${VAULT_NAMESPACE}
fetch() {
dest="$1"
@@ -105,11 +104,6 @@ if ! (
fi
rm -rf "$TMP"
if [ -n "$${VAULT_NAMESPACE}" ]; then
export VAULT_NAMESPACE
printf "📁 Using Vault namespace: %s\n\n" "$${VAULT_NAMESPACE}"
fi
# Authenticate with Vault
printf "🔑 Authenticating with Vault ...\n\n"
GITHUB_TOKEN=$(coder external-auth access-token "$${GITHUB_EXTERNAL_AUTH_ID}")
+5 -5
View File
@@ -14,7 +14,7 @@ This module lets you authenticate with [Hashicorp Vault](https://www.vaultprojec
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-jwt/coder"
version = "1.2.0"
version = "1.1.1"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_jwt_role = "coder" # The Vault role to use for authentication
@@ -42,7 +42,7 @@ curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/d
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-jwt/coder"
version = "1.2.0"
version = "1.1.1"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_jwt_auth_path = "oidc"
@@ -58,7 +58,7 @@ data "coder_workspace_owner" "me" {}
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-jwt/coder"
version = "1.2.0"
version = "1.1.1"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_jwt_role = data.coder_workspace_owner.me.groups[0]
@@ -71,7 +71,7 @@ module "vault" {
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-jwt/coder"
version = "1.2.0"
version = "1.1.1"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_jwt_role = "coder" # The Vault role to use for authentication
@@ -132,7 +132,7 @@ resource "jwt_signed_token" "vault" {
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-jwt/coder"
version = "1.2.0"
version = "1.1.1"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_jwt_role = "coder" # The Vault role to use for authentication
-14
View File
@@ -38,12 +38,6 @@ variable "vault_jwt_role" {
description = "The name of the Vault role to use for authentication."
}
variable "vault_namespace" {
type = string
description = "The Vault Enterprise namespace that contains the JWT auth mount."
default = null
}
variable "vault_cli_version" {
type = string
description = "The version of Vault to install."
@@ -63,7 +57,6 @@ resource "coder_script" "vault" {
VAULT_JWT_AUTH_PATH : var.vault_jwt_auth_path,
VAULT_JWT_ROLE : var.vault_jwt_role,
VAULT_CLI_VERSION : var.vault_cli_version,
VAULT_NAMESPACE : var.vault_namespace != null ? var.vault_namespace : "",
})
run_on_start = true
start_blocks_login = true
@@ -75,11 +68,4 @@ resource "coder_env" "vault_addr" {
value = var.vault_addr
}
resource "coder_env" "vault_namespace" {
count = var.vault_namespace == null ? 0 : 1
agent_id = var.agent_id
name = "VAULT_NAMESPACE"
value = var.vault_namespace
}
data "coder_workspace_owner" "me" {}
-6
View File
@@ -4,7 +4,6 @@
VAULT_CLI_VERSION=${VAULT_CLI_VERSION}
VAULT_JWT_AUTH_PATH=${VAULT_JWT_AUTH_PATH}
VAULT_JWT_ROLE=${VAULT_JWT_ROLE}
VAULT_NAMESPACE=${VAULT_NAMESPACE}
CODER_OIDC_ACCESS_TOKEN=${CODER_OIDC_ACCESS_TOKEN}
fetch() {
@@ -106,11 +105,6 @@ if ! (
fi
rm -rf "$TMP"
if [ -n "$${VAULT_NAMESPACE}" ]; then
export VAULT_NAMESPACE
printf "📁 Using Vault namespace: %s\n\n" "$${VAULT_NAMESPACE}"
fi
# Authenticate with Vault
printf "🔑 Authenticating with Vault ...\n\n"
echo "$${CODER_OIDC_ACCESS_TOKEN}" | vault write -field=token auth/"$${VAULT_JWT_AUTH_PATH}"/login role="$${VAULT_JWT_ROLE}" jwt=- | vault login -
+2 -2
View File
@@ -19,7 +19,7 @@ variable "vault_token" {
module "vault" {
source = "registry.coder.com/coder/vault-token/coder"
version = "1.3.0"
version = "1.2.2"
agent_id = coder_agent.example.id
vault_token = var.token # optional
vault_addr = "https://vault.example.com"
@@ -73,7 +73,7 @@ variable "vault_token" {
module "vault" {
source = "registry.coder.com/coder/vault-token/coder"
version = "1.3.0"
version = "1.2.2"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_token = var.token
+1 -2
View File
@@ -50,7 +50,6 @@ resource "coder_script" "vault" {
icon = "/icon/vault.svg"
script = templatefile("${path.module}/run.sh", {
INSTALL_VERSION : var.vault_cli_version,
VAULT_NAMESPACE : var.vault_namespace != null ? var.vault_namespace : "",
})
run_on_start = true
start_blocks_login = true
@@ -74,4 +73,4 @@ resource "coder_env" "vault_namespace" {
agent_id = var.agent_id
name = "VAULT_NAMESPACE"
value = var.vault_namespace
}
}
@@ -2,7 +2,6 @@
# Convert all templated variables to shell variables
INSTALL_VERSION=${INSTALL_VERSION}
VAULT_NAMESPACE=${VAULT_NAMESPACE}
fetch() {
dest="$1"
@@ -102,8 +101,3 @@ if ! (
exit 1
fi
rm -rf "$TMP"
if [ -n "$${VAULT_NAMESPACE}" ]; then
export VAULT_NAMESPACE
printf "📁 Using Vault namespace: %s\n\n" "$${VAULT_NAMESPACE}"
fi
Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

-12
View File
@@ -1,12 +0,0 @@
---
display_name: "CytoShahar"
bio: "Data engineer by day, maker by night"
avatar: "./.images/avatar.jpeg"
github: "https://github.com/CytoShahar"
linkedin: "https://www.linkedin.com/in/shaharzrihen" # Optional
status: "community"
---
# Shahar Zrihen
Data engineer by day, maker by night
@@ -1,38 +0,0 @@
---
display_name: Positron Desktop
description: Add a one-click button to launch Positron Desktop
icon: ../../../../.icons/positron.svg
verified: true
tags: [ide, positron]
---
# Positron Desktop
Add a button to open any workspace with a single click.
Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder).
```tf
module "positron" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/cytoshahar/positron/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
## Examples
### Open in a specific directory
```tf
module "positron" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/cytoshahar/positron/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
```
Based on the [Coder VS Code Desktop Module](https://github.com/coder/registry/tree/main/registry/coder/modules/vscode-desktop)
@@ -1,88 +0,0 @@
import { describe, expect, it } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
describe("positron-desktop", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
it("default output", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
expect(state.outputs.positron_url.value).toBe(
"positron://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "positron",
);
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBeNull();
});
it("adds folder", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/foo/bar",
});
expect(state.outputs.positron_url.value).toBe(
"positron://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
it("adds folder and open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/foo/bar",
open_recent: "true",
});
expect(state.outputs.positron_url.value).toBe(
"positron://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
it("adds folder but not open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/foo/bar",
openRecent: "false",
});
expect(state.outputs.positron_url.value).toBe(
"positron://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
it("adds open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
open_recent: "true",
});
expect(state.outputs.positron_url.value).toBe(
"positron://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
it("expect order to be set", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
order: "22",
});
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "positron",
);
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBe(22);
});
});
@@ -1,74 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
locals {
icon_url = "/icon/positron.svg"
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "folder" {
type = string
description = "The folder to open in Positron."
default = ""
}
variable "open_recent" {
type = bool
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
default = false
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_app" "positron" {
agent_id = var.agent_id
external = true
icon = local.icon_url
slug = "positron"
display_name = "Positron Desktop"
order = var.order
group = var.group
url = join("", [
"positron://coder.coder-remote/open",
"?owner=",
data.coder_workspace_owner.me.name,
"&workspace=",
data.coder_workspace.me.name,
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
var.open_recent ? "&openRecent" : "",
"&url=",
data.coder_workspace.me.access_url,
"&token=$SESSION_TOKEN",
])
}
output "positron_url" {
value = coder_app.positron.url
description = "Positron Desktop URL."
}
@@ -3,7 +3,7 @@ display_name: airflow
description: A module that adds Apache Airflow in your Coder template
icon: ../../../../.icons/airflow.svg
maintainer_github: nataindata
verified: false
verified: true
tags: [airflow, ide, web]
---
@@ -2,7 +2,7 @@
display_name: DigitalOcean Region
description: A parameter with human region names and icons
icon: ../../../../.icons/digital-ocean.svg
verified: false
verified: true
tags: [helper, parameter, digitalocean, regions]
---