Compare commits

...

25 Commits

Author SHA1 Message Date
Atif Ali c977142180 Merge branch 'main' into jetbrains 2025-07-08 20:39:18 +05:00
Muhammad Atif Ali 56a01f2f76 Fix redundant entry in JetBrains README 2025-07-08 20:18:36 +05:00
Muhammad Atif Ali d9cc3fc1a3 Add JetBrains IDE list image to README 2025-07-08 18:40:57 +05:00
Muhammad Atif Ali 34346e7d68 Update JetBrains module readme for clarity 2025-07-08 17:57:16 +05:00
Hugo Dutka 047a5d654b feat: add the agentapi module (#177)
The agentapi module is a building block for modules that need to run an
agentapi server. It's not meant for end users of Coder.

The agentapi-specific logic is mostly extracted from [the claude-code
module](https://github.com/coder/registry/tree/c0f2d945c5808c1962b15b5c2e2a5269ea0f4339/registry/coder/modules/claude-code).
You can see this module in action in [the goose 2.0
PR](https://github.com/coder/registry/pull/178).
2025-07-08 13:01:09 +02:00
Muhammad Atif Ali 15fcf0e66a Update JetBrains version requirements in README 2025-07-08 15:50:34 +05:00
DevCats 5554283a2f docs(CONTRIBUTING): add template contributing guide for new and existing templates (#181)
## Description

Add contributing guide for new and existing templates

---

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [X] Documentation
- [ ] Other

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-07-07 22:30:29 -05:00
Muhammad Atif Ali ef8ad092e1 Add agent_name to JetBrains module URLs
Add agent_name parameter to URLs for workspaces with
multiple agents. Updated tests to ensure correct URL
generation, with and without agent_name specified.
2025-07-08 01:34:04 +05:00
dependabot[bot] fa939bbd5a chore(deps): bump golang.org/x/crypto from 0.11.0 to 0.35.0 in the go_modules group across 1 directory (#183)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 00:02:47 +05:00
Muhammad Atif Ali c251fbfa9c Add JetBrains icon and update README 2025-07-08 00:02:01 +05:00
Muhammad Atif Ali 2a48544fd5 Fix JetBrains icon path in README 2025-07-08 00:00:07 +05:00
Atif Ali b230b2a3ce Merge branch 'main' into jetbrains 2025-07-07 21:53:57 +05:00
Michael Smith cde8fe3b30 fix: consolidate template README images into single directory (#193)
## Description

This PR moves all the existing template README images for the Coder
namespace from being defined inline to its `.images` directory. This
makes the image-processing logic in the Registry build step easier to
maintain.

## Type of Change

- [ ] New module
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

---

## Related Issues

Related to https://github.com/coder/registry/issues/132
2025-07-07 12:50:34 -04:00
imgbot[bot] 69996231c8 [ImgBot] Optimize images (#189)
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2025-07-07 21:42:01 +05:00
Michael Smith c0f2d945c5 fix: update namespace for coder-labs template (#191)
## Description

This PR updates the `maintainer_github` field for the new Coder Labs
template to use the value of `coder-labs`. This shouldn't be necessary
(`maintainer_github` should be deprecated), but something is off with
our build step.

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [x] Other

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

---

## Related Issues

None
2025-07-07 09:32:24 -04:00
Muhammad Atif Ali ec5aa854af Update JetBrains IDE build numbers 2025-07-07 17:03:28 +05:00
Muhammad Atif Ali b7cc89cdfd Fix JetBrains URL to include agent_id parameter
- Updates URL generation logic to include the agent_id.
- Adjusts tests to validate the presence of agent_id in URLs.
2025-07-07 16:15:43 +05:00
Muhammad Atif Ali 05311159e1 Fix JetBrains URL for Toolbox 2.6.3+ 2025-07-06 18:13:43 +05:00
Muhammad Atif Ali d99c7704a5 Refactor JetBrains module for air-gapped support
- Improved logic for handling air-gapped environments by utilizing
  fallback mechanisms in JetBrains integrations.
- Updated parameters and default settings to align with the new
  connectivity conditions, ensuring robustness in varied network
  scenarios.
- Expanded tests to validate custom configurations even when API access
  is restricted, confirming consistent behavior across setups.
2025-07-06 18:10:42 +05:00
Atif Ali e54ca31402 Merge branch 'main' into jetbrains 2025-07-06 17:37:42 +05:00
Atif Ali 283bdc3683 Merge branch 'main' into jetbrains 2025-07-04 01:47:11 +05:00
Muhammad Atif Ali de29c2aa92 Renamed the data source from jetbrains_ide to jetbrains_ides for consistency 2025-05-29 23:01:26 +05:00
Muhammad Atif Ali c96782e124 Renamed the data source from jetbrains_ide to jetbrains_ides for consistency 2025-05-29 22:57:20 +05:00
Muhammad Atif Ali d41870120e Enhance JetBrains module config and tests
- Refine README for improved clarity and structure.
- Expand automated test coverage across multiple scenarios.
- Ensure custom IDE configurations and URL generation are validated.
- Simplify handling of parameter defaults and custom builds.
2025-05-23 20:53:07 +05:00
Muhammad Atif Ali 09873f9d79 wip 2025-05-23 15:29:48 +05:00
30 changed files with 2584 additions and 68 deletions
+2 -10
View File
@@ -1,9 +1,9 @@
Closes #
## Description
<!-- Briefly describe what this PR does and why -->
---
## Type of Change
- [ ] New module
@@ -12,8 +12,6 @@
- [ ] Documentation
- [ ] Other
---
## Module Information
<!-- Delete this section if not applicable -->
@@ -22,18 +20,12 @@
**New version:** `v1.0.0`
**Breaking change:** [ ] Yes [ ] No
---
## Testing & Validation
- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] Changes tested locally
---
## Related Issues
<!-- Link related issues or write "None" if not applicable -->
Closes #
+1
View File
@@ -0,0 +1 @@
<svg fill="none" height="64" viewBox="0 0 64 64" width="64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1=".850001" x2="62.62" y1="62.72" y2="1.81"><stop offset="0" stop-color="#ff9419"/><stop offset=".43" stop-color="#ff021d"/><stop offset=".99" stop-color="#e600ff"/></linearGradient><clipPath id="b"><path d="m0 0h64v64h-64z"/></clipPath><g clip-path="url(#b)"><path d="m20.34 3.66-16.68 16.68c-2.34 2.34-3.66 5.52-3.66 8.84v29.82c0 2.76 2.24 5 5 5h29.82c3.32 0 6.49-1.32 8.84-3.66l16.68-16.68c2.34-2.34 3.66-5.52 3.66-8.84v-29.82c0-2.76-2.24-5-5-5h-29.82c-3.32 0-6.49 1.32-8.84 3.66z" fill="url(#a)"/><path d="m48 16h-40v40h40z" fill="#000"/><path d="m30 47h-17v4h17z" fill="#fff"/></g></svg>

After

Width:  |  Height:  |  Size: 785 B

+184 -10
View File
@@ -4,12 +4,14 @@ Welcome! This guide covers how to contribute to the Coder Registry, whether you'
## What is the Coder Registry?
The Coder Registry is a collection of Terraform modules that extend Coder workspaces with development tools like VS Code, Cursor, JetBrains IDEs, and more.
The Coder Registry is a collection of Terraform modules and templates for Coder workspaces. Modules provide IDEs, authentication integrations, development tools, and other workspace functionality. Templates provide complete workspace configurations for different platforms and use cases that appear as community templates on the registry website.
## Types of Contributions
- **[New Modules](#creating-a-new-module)** - Add support for a new tool or functionality
- **[New Templates](#creating-a-new-template)** - Create complete workspace configurations
- **[Existing Modules](#contributing-to-existing-modules)** - Fix bugs, add features, or improve documentation
- **[Existing Templates](#contributing-to-existing-templates)** - Improve workspace templates
- **[Bug Reports](#reporting-issues)** - Report problems or request features
## Setup
@@ -36,7 +38,15 @@ bun install
### Understanding Namespaces
All modules are organized under `/registry/[namespace]/modules/`. Each contributor gets their own namespace (e.g., `/registry/your-username/modules/`). If a namespace is taken, choose a different unique namespace, but you can still use any display name on the Registry website.
All modules and templates are organized under `/registry/[namespace]/`. Each contributor gets their own namespace with both modules and templates directories:
```
registry/[namespace]/
├── modules/ # Individual components and tools
└── templates/ # Complete workspace configurations
```
For example: `/registry/your-username/modules/` and `/registry/your-username/templates/`. If a namespace is taken, choose a different unique namespace, but you can still use any display name on the Registry website.
### Images and Icons
@@ -136,15 +146,171 @@ git push origin your-branch
---
## Contributing to Existing Modules
## Creating a New Template
### 1. Find the Module
Templates are complete Coder workspace configurations that users can deploy directly. Unlike modules (which are components), templates provide full infrastructure definitions for specific platforms or use cases.
```bash
find registry -name "*[module-name]*" -type d
### Template Structure
Templates follow the same namespace structure as modules but are located in the `templates` directory:
```
registry/[your-username]/templates/[template-name]/
├── main.tf # Complete Terraform configuration
├── README.md # Documentation with frontmatter
├── [additional files] # Scripts, configs, etc.
```
### 2. Make Your Changes
### 1. Create Your Template Directory
```bash
mkdir -p registry/[your-username]/templates/[template-name]
cd registry/[your-username]/templates/[template-name]
```
### 2. Create Template Files
#### main.tf
Your `main.tf` should be a complete Coder template configuration including:
- Required providers (coder, and your infrastructure provider)
- Coder agent configuration
- Infrastructure resources (containers, VMs, etc.)
- Registry modules for IDEs, tools, and integrations
Example structure:
```terraform
terraform {
required_providers {
coder = {
source = "coder/coder"
}
# Add your infrastructure provider (docker, aws, etc.)
}
}
# Coder data sources
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
# Coder agent
resource "coder_agent" "main" {
arch = "amd64"
os = "linux"
startup_script = <<-EOT
# Startup commands here
EOT
}
# Registry modules for IDEs, tools, and integrations
module "code-server" {
source = "registry.coder.com/coder/code-server/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
}
# Your infrastructure resources
# (Docker containers, AWS instances, etc.)
```
#### README.md
Create documentation with proper frontmatter:
```markdown
---
display_name: "Template Name"
description: "Brief description of what this template provides"
icon: "../../../../.icons/platform.svg"
verified: false
tags: ["platform", "use-case", "tools"]
---
# Template Name
Describe what the template provides and how to use it.
Include any setup requirements, resource information, or usage notes that users need to know.
```
### 3. Test Your Template
Templates should be tested to ensure they work correctly. Test with Coder:
```bash
cd registry/[your-username]/templates/[template-name]
coder templates push [template-name] -d .
```
### 4. Template Best Practices
- **Use registry modules**: Leverage existing modules for IDEs, tools, and integrations
- **Provide sensible defaults**: Make the template work out-of-the-box
- **Include metadata**: Add useful workspace metadata (CPU, memory, disk usage)
- **Document prerequisites**: Clearly explain infrastructure requirements
- **Use variables**: Allow customization of common settings
- **Follow naming conventions**: Use descriptive, consistent naming
### 5. Template Guidelines
- Templates appear as "Community Templates" on the registry website
- Include proper error handling and validation
- Test with Coder before submitting
- Document any required permissions or setup steps
- Use semantic versioning in your README frontmatter
---
## Contributing to Existing Templates
### 1. Types of Template Improvements
**Bug fixes:**
- Fix infrastructure provisioning issues
- Resolve agent connectivity problems
- Correct resource naming or tagging
**Feature additions:**
- Add new registry modules for additional functionality
- Include additional infrastructure options
- Improve startup scripts or automation
**Platform updates:**
- Update base images or AMIs
- Adapt to new platform features
- Improve security configurations
**Documentation:**
- Clarify prerequisites and setup steps
- Add troubleshooting guides
- Improve usage examples
### 2. Testing Template Changes
Testing template modifications thoroughly is necessary. Test with Coder:
```bash
coder templates push test-[template-name] -d .
```
### 3. Maintain Compatibility
- Don't remove existing variables without clear migration path
- Preserve backward compatibility when possible
- Test that existing workspaces still function
- Document any breaking changes clearly
---
## Contributing to Existing Modules
### 1. Make Your Changes
**For bug fixes:**
@@ -166,7 +332,7 @@ find registry -name "*[module-name]*" -type d
- Add missing variable documentation
- Improve usage examples
### 3. Test Your Changes
### 2. Test Your Changes
```bash
# Test a specific module
@@ -176,7 +342,7 @@ bun test -t 'module-name'
bun test
```
### 4. Maintain Backward Compatibility
### 3. Maintain Backward Compatibility
- New variables should have default values
- Don't break existing functionality
@@ -208,6 +374,7 @@ bun test
We have different PR templates for different types of contributions. GitHub will show you options to choose from, or you can manually select:
- **New Module**: Use `?template=new_module.md`
- **New Template**: Use `?template=new_template.md`
- **Bug Fix**: Use `?template=bug_fix.md`
- **Feature**: Use `?template=feature.md`
- **Documentation**: Use `?template=documentation.md`
@@ -224,6 +391,13 @@ Example: `https://github.com/coder/registry/compare/main...your-branch?template=
- `main.test.ts` - Working tests
- `README.md` - Documentation with frontmatter
### Every Template Must Have
- `main.tf` - Complete Terraform configuration
- `README.md` - Documentation with frontmatter
Templates don't require test files like modules do, but should be manually tested before submission.
### README Frontmatter
Module README frontmatter must include:
@@ -304,7 +478,7 @@ When reporting bugs, include:
## Getting Help
- **Examples**: Check `/registry/coder/modules/` for well-structured modules
- **Examples**: Check `/registry/coder/modules/` for well-structured modules and `/registry/coder/templates/` for complete templates
- **Issues**: Open an issue for technical problems
- **Community**: Reach out to the Coder community for questions
+3 -3
View File
@@ -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.11.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/term v0.10.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.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
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.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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=
+220
View File
@@ -0,0 +1,220 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
typescript:
specifier: ^5.8.3
version: 5.8.3
devDependencies:
'@types/bun':
specifier: ^1.2.18
version: 1.2.18(@types/react@19.1.8)
bun-types:
specifier: ^1.2.18
version: 1.2.18(@types/react@19.1.8)
gray-matter:
specifier: ^4.0.3
version: 4.0.3
marked:
specifier: ^16.0.0
version: 16.0.0
prettier:
specifier: ^3.6.2
version: 3.6.2
prettier-plugin-sh:
specifier: ^0.18.0
version: 0.18.0(prettier@3.6.2)
prettier-plugin-terraform-formatter:
specifier: ^1.2.1
version: 1.2.1(prettier@3.6.2)
packages:
'@reteps/dockerfmt@0.3.6':
resolution: {integrity: sha512-Tb5wIMvBf/nLejTQ61krK644/CEMB/cpiaIFXqGApfGqO3GwcR3qnI0DbmkFVCl2OyEp8LnLX3EkucoL0+tbFg==}
engines: {node: ^v12.20.0 || ^14.13.0 || >=16.0.0}
'@types/bun@1.2.18':
resolution: {integrity: sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ==}
'@types/node@24.0.10':
resolution: {integrity: sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==}
'@types/react@19.1.8':
resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==}
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
bun-types@1.2.18:
resolution: {integrity: sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw==}
peerDependencies:
'@types/react': ^19
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
extend-shallow@2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
engines: {node: '>=0.10.0'}
gray-matter@4.0.3:
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
engines: {node: '>=6.0'}
is-extendable@0.1.1:
resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
engines: {node: '>=0.10.0'}
js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
hasBin: true
kind-of@6.0.3:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
marked@16.0.0:
resolution: {integrity: sha512-MUKMXDjsD/eptB7GPzxo4xcnLS6oo7/RHimUMHEDRhUooPwmN9BEpMl7AEOJv3bmso169wHI2wUF9VQgL7zfmA==}
engines: {node: '>= 20'}
hasBin: true
prettier-plugin-sh@0.18.0:
resolution: {integrity: sha512-cW1XL27FOJQ/qGHOW6IHwdCiNWQsAgK+feA8V6+xUTaH0cD3Mh+tFAtBvEEWvuY6hTDzRV943Fzeii+qMOh7nQ==}
engines: {node: '>=16.0.0'}
peerDependencies:
prettier: ^3.6.0
prettier-plugin-terraform-formatter@1.2.1:
resolution: {integrity: sha512-rdzV61Bs/Ecnn7uAS/vL5usTX8xUWM+nQejNLZxt3I1kJH5WSeLEmq7LYu1wCoEQF+y7Uv1xGvPRfl3lIe6+tA==}
peerDependencies:
prettier: '>= 1.16.0'
peerDependenciesMeta:
prettier:
optional: true
prettier@3.6.2:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
engines: {node: '>=14'}
hasBin: true
section-matter@1.0.0:
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
engines: {node: '>=4'}
sh-syntax@0.5.8:
resolution: {integrity: sha512-JfVoxf4FxQI5qpsPbkHhZo+n6N9YMJobyl4oGEUBb/31oQYlgTjkXQD8PBiafS2UbWoxrTO0Z5PJUBXEPAG1Zw==}
engines: {node: '>=16.0.0'}
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
strip-bom-string@1.0.0:
resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
engines: {node: '>=0.10.0'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typescript@5.8.3:
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@7.8.0:
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
snapshots:
'@reteps/dockerfmt@0.3.6': {}
'@types/bun@1.2.18(@types/react@19.1.8)':
dependencies:
bun-types: 1.2.18(@types/react@19.1.8)
transitivePeerDependencies:
- '@types/react'
'@types/node@24.0.10':
dependencies:
undici-types: 7.8.0
'@types/react@19.1.8':
dependencies:
csstype: 3.1.3
argparse@1.0.10:
dependencies:
sprintf-js: 1.0.3
bun-types@1.2.18(@types/react@19.1.8):
dependencies:
'@types/node': 24.0.10
'@types/react': 19.1.8
csstype@3.1.3: {}
esprima@4.0.1: {}
extend-shallow@2.0.1:
dependencies:
is-extendable: 0.1.1
gray-matter@4.0.3:
dependencies:
js-yaml: 3.14.1
kind-of: 6.0.3
section-matter: 1.0.0
strip-bom-string: 1.0.0
is-extendable@0.1.1: {}
js-yaml@3.14.1:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
kind-of@6.0.3: {}
marked@16.0.0: {}
prettier-plugin-sh@0.18.0(prettier@3.6.2):
dependencies:
'@reteps/dockerfmt': 0.3.6
prettier: 3.6.2
sh-syntax: 0.5.8
prettier-plugin-terraform-formatter@1.2.1(prettier@3.6.2):
optionalDependencies:
prettier: 3.6.2
prettier@3.6.2: {}
section-matter@1.0.0:
dependencies:
extend-shallow: 2.0.1
kind-of: 6.0.3
sh-syntax@0.5.8:
dependencies:
tslib: 2.8.1
sprintf-js@1.0.3: {}
strip-bom-string@1.0.0: {}
tslib@2.8.1: {}
typescript@5.8.3: {}
undici-types@7.8.0: {}
+1 -25
View File
@@ -1,25 +1 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="512" height="512" fill="#090B0B"/>
<path d="M427.787 172H296.889V188.277H427.787V172Z" fill="white"/>
<path d="M427.787 190.878H296.889V207.154H427.787V190.878Z" fill="white"/>
<path d="M427.787 209.74H296.889V226.017H427.787V209.74Z" fill="white"/>
<path d="M427.787 228.618H296.889V244.895H427.787V228.618Z" fill="white"/>
<path d="M427.787 247.496H296.889V263.773H427.787V247.496Z" fill="white"/>
<path d="M428 266.359H297.102V282.636H428V266.359Z" fill="white"/>
<path d="M427.68 285.237H296.783V301.513H427.68V285.237Z" fill="white"/>
<path d="M427.68 304.115H296.783V320.391H427.68V304.115Z" fill="white"/>
<path d="M427.893 322.993H296.996V339.269H427.893V322.993Z" fill="white"/>
<path d="M245.778 172H116.325V188.277H245.778V172Z" fill="white"/>
<path d="M262.024 190.878H100.17V207.154H262.024V190.878Z" fill="white"/>
<path d="M262.024 304.115H100.17V320.391H262.024V304.115Z" fill="white"/>
<path d="M245.747 322.993H116.325V339.269H245.747V322.993Z" fill="white"/>
<path d="M138.839 247.496H84V263.773H138.839V247.496Z" fill="white"/>
<path d="M148.665 266.374H84V282.651H148.665V266.374Z" fill="white"/>
<path d="M278.088 266.374H213.422V282.651H278.088V266.374Z" fill="white"/>
<path d="M278.087 285.237H207.17V301.513H278.087V285.237Z" fill="white"/>
<path d="M156.652 285.237H84V301.513H156.652V285.237Z" fill="white"/>
<path d="M156.652 209.74H84V226.017H156.652V209.74Z" fill="white"/>
<path d="M278.118 209.74H207.17V226.017H278.118V209.74Z" fill="white"/>
<path d="M148.665 228.618H84V244.895H148.665V228.618Z" fill="white"/>
<path d="M278.118 228.618H213.392V244.895H278.118V228.618Z" fill="white"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none" viewBox="0 0 512 512"><rect width="512" height="512" fill="#090B0B"/><path fill="#fff" d="M427.787 172H296.889V188.277H427.787V172Z"/><path fill="#fff" d="M427.787 190.878H296.889V207.154H427.787V190.878Z"/><path fill="#fff" d="M427.787 209.74H296.889V226.017H427.787V209.74Z"/><path fill="#fff" d="M427.787 228.618H296.889V244.895H427.787V228.618Z"/><path fill="#fff" d="M427.787 247.496H296.889V263.773H427.787V247.496Z"/><path fill="#fff" d="M428 266.359H297.102V282.636H428V266.359Z"/><path fill="#fff" d="M427.68 285.237H296.783V301.513H427.68V285.237Z"/><path fill="#fff" d="M427.68 304.115H296.783V320.391H427.68V304.115Z"/><path fill="#fff" d="M427.893 322.993H296.996V339.269H427.893V322.993Z"/><path fill="#fff" d="M245.778 172H116.325V188.277H245.778V172Z"/><path fill="#fff" d="M262.024 190.878H100.17V207.154H262.024V190.878Z"/><path fill="#fff" d="M262.024 304.115H100.17V320.391H262.024V304.115Z"/><path fill="#fff" d="M245.747 322.993H116.325V339.269H245.747V322.993Z"/><path fill="#fff" d="M138.839 247.496H84V263.773H138.839V247.496Z"/><path fill="#fff" d="M148.665 266.374H84V282.651H148.665V266.374Z"/><path fill="#fff" d="M278.088 266.374H213.422V282.651H278.088V266.374Z"/><path fill="#fff" d="M278.087 285.237H207.17V301.513H278.087V285.237Z"/><path fill="#fff" d="M156.652 285.237H84V301.513H156.652V285.237Z"/><path fill="#fff" d="M156.652 209.74H84V226.017H156.652V209.74Z"/><path fill="#fff" d="M278.118 209.74H207.17V226.017H278.118V209.74Z"/><path fill="#fff" d="M148.665 228.618H84V244.895H148.665V228.618Z"/><path fill="#fff" d="M278.118 228.618H213.392V244.895H278.118V228.618Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 975 KiB

@@ -2,7 +2,7 @@
display_name: Tasks on Docker
description: Run Coder Tasks on Docker with an example application
icon: ../../../../.icons/tasks.svg
maintainer_github: coder
maintainer_github: coder-labs
verified: false
tags: [docker, container, ai, tasks]
---

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

+54
View File
@@ -0,0 +1,54 @@
---
display_name: AgentAPI
description: Building block for modules that need to run an agentapi server
icon: ../../../../.icons/coder.svg
maintainer_github: coder
verified: true
tags: [internal]
---
# AgentAPI
The AgentAPI module is a building block for modules that need to run an agentapi server. It is intended primarily for internal use by Coder to create modules compatible with Tasks.
We do not recommend using this module directly. Instead, please consider using one of our [Tasks-compatible AI agent modules](https://registry.coder.com/modules?search=tag%3Atasks).
```tf
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.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 = "Goose"
cli_app_slug = "goose-cli"
cli_app_display_name = "Goose CLI"
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = local.start_script
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_PROVIDER='${var.goose_provider}' \
ARG_MODEL='${var.goose_model}' \
ARG_GOOSE_CONFIG="$(echo -n '${base64encode(local.combined_extensions)}' | base64 -d)" \
ARG_INSTALL='${var.install_goose}' \
ARG_GOOSE_VERSION='${var.goose_version}' \
/tmp/install.sh
EOT
}
```
## For module developers
For a complete example of how to use this module, see the [goose module](https://github.com/coder/registry/blob/main/registry/coder/modules/goose/main.tf).
@@ -0,0 +1,151 @@
import {
test,
afterEach,
expect,
describe,
setDefaultTimeout,
beforeAll,
} from "bun:test";
import { execContainer, readFileContainer, runTerraformInit } from "~test";
import {
loadTestFile,
writeExecutable,
setup as setupUtil,
execModuleScript,
expectAgentAPIStarted,
} from "./test-util";
let cleanupFunctions: (() => Promise<void>)[] = [];
const registerCleanup = (cleanup: () => Promise<void>) => {
cleanupFunctions.push(cleanup);
};
// Cleanup logic depends on the fact that bun's built-in test runner
// runs tests sequentially.
// https://bun.sh/docs/test/discovery#execution-order
// Weird things would happen if tried to run tests in parallel.
// One test could clean up resources that another test was still using.
afterEach(async () => {
// reverse the cleanup functions so that they are run in the correct order
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;
moduleVariables?: Record<string, string>;
}
const moduleDirName = ".agentapi-module";
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const projectDir = "/home/coder/project";
const { id } = await setupUtil({
moduleVariables: {
experiment_report_tasks: "true",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
web_app_display_name: "AgentAPI Web",
web_app_slug: "agentapi-web",
web_app_icon: "/icon/coder.svg",
cli_app_display_name: "AgentAPI CLI",
cli_app_slug: "agentapi-cli",
agentapi_version: "latest",
module_dir_name: moduleDirName,
start_script: await loadTestFile(import.meta.dir, "agentapi-start.sh"),
folder: projectDir,
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
moduleDir: import.meta.dir,
});
await writeExecutable({
containerId: id,
filePath: "/usr/bin/aiagent",
content: await loadTestFile(import.meta.dir, "ai-agent-mock.js"),
});
return { id };
};
// increase the default timeout to 60 seconds
setDefaultTimeout(60 * 1000);
// we don't run these tests in CI because they take too long and make network
// calls. they are dedicated for local development.
describe("agentapi", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
test("happy-path", async () => {
const { id } = await setup();
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("custom-port", async () => {
const { id } = await setup({
moduleVariables: {
agentapi_port: "3827",
},
});
await execModuleScript(id);
await expectAgentAPIStarted(id, 3827);
});
test("pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
pre_install_script: `#!/bin/bash\necho "pre-install"`,
install_script: `#!/bin/bash\necho "install"`,
post_install_script: `#!/bin/bash\necho "post-install"`,
},
});
await execModuleScript(id);
await expectAgentAPIStarted(id);
const preInstallLog = await readFileContainer(
id,
`/home/coder/${moduleDirName}/pre_install.log`,
);
const installLog = await readFileContainer(
id,
`/home/coder/${moduleDirName}/install.log`,
);
const postInstallLog = await readFileContainer(
id,
`/home/coder/${moduleDirName}/post_install.log`,
);
expect(preInstallLog).toContain("pre-install");
expect(installLog).toContain("install");
expect(postInstallLog).toContain("post-install");
});
test("install-agentapi", async () => {
const { id } = await setup({ skipAgentAPIMock: true });
const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
await expectAgentAPIStarted(id);
const respAgentAPI = await execContainer(id, [
"bash",
"-c",
"agentapi --version",
]);
expect(respAgentAPI.exitCode).toBe(0);
});
});
+213
View File
@@ -0,0 +1,213 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "web_app_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 "web_app_group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "web_app_icon" {
type = string
description = "The icon to use for the app."
}
variable "web_app_display_name" {
type = string
description = "The display name of the web app."
}
variable "web_app_slug" {
type = string
description = "The slug of the web app."
}
variable "folder" {
type = string
description = "The folder to run AgentAPI in."
default = "/home/coder"
}
variable "cli_app" {
type = bool
description = "Whether to create the CLI workspace app."
default = false
}
variable "cli_app_order" {
type = number
description = "The order of the CLI workspace app."
default = null
}
variable "cli_app_group" {
type = string
description = "The group of the CLI workspace app."
default = null
}
variable "cli_app_icon" {
type = string
description = "The icon to use for the app."
default = "/icon/claude.svg"
}
variable "cli_app_display_name" {
type = string
description = "The display name of the CLI workspace app."
}
variable "cli_app_slug" {
type = string
description = "The slug of the CLI workspace app."
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing the agent used by AgentAPI."
default = null
}
variable "install_script" {
type = string
description = "Script to install the agent used by AgentAPI."
default = ""
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing the agent used by AgentAPI."
default = null
}
variable "start_script" {
type = string
description = "Script that starts AgentAPI."
}
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.2.3"
}
variable "agentapi_port" {
type = number
description = "The port used by AgentAPI."
default = 3284
}
variable "module_dir_name" {
type = string
description = "Name of the subdirectory in the home directory for module files."
}
locals {
# we always trim the slash for consistency
workdir = trimsuffix(var.folder, "/")
encoded_pre_install_script = var.pre_install_script != null ? base64encode(var.pre_install_script) : ""
encoded_install_script = var.install_script != null ? base64encode(var.install_script) : ""
encoded_post_install_script = var.post_install_script != null ? base64encode(var.post_install_script) : ""
agentapi_start_script_b64 = base64encode(var.start_script)
agentapi_wait_for_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh"))
main_script = file("${path.module}/scripts/main.sh")
}
resource "coder_script" "agentapi" {
agent_id = var.agent_id
display_name = "Install and start AgentAPI"
icon = var.web_app_icon
script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.main_script)}' | base64 -d > /tmp/main.sh
chmod +x /tmp/main.sh
ARG_MODULE_DIR_NAME='${var.module_dir_name}' \
ARG_WORKDIR="$(echo -n '${base64encode(local.workdir)}' | base64 -d)" \
ARG_PRE_INSTALL_SCRIPT="$(echo -n '${local.encoded_pre_install_script}' | base64 -d)" \
ARG_INSTALL_SCRIPT="$(echo -n '${local.encoded_install_script}' | base64 -d)" \
ARG_INSTALL_AGENTAPI='${var.install_agentapi}' \
ARG_AGENTAPI_VERSION='${var.agentapi_version}' \
ARG_START_SCRIPT="$(echo -n '${local.agentapi_start_script_b64}' | base64 -d)" \
ARG_WAIT_FOR_START_SCRIPT="$(echo -n '${local.agentapi_wait_for_start_script_b64}' | base64 -d)" \
ARG_POST_INSTALL_SCRIPT="$(echo -n '${local.encoded_post_install_script}' | base64 -d)" \
ARG_AGENTAPI_PORT='${var.agentapi_port}' \
/tmp/main.sh
EOT
run_on_start = true
}
resource "coder_app" "agentapi_web" {
slug = var.web_app_slug
display_name = var.web_app_display_name
agent_id = var.agent_id
url = "http://localhost:${var.agentapi_port}/"
icon = var.web_app_icon
order = var.web_app_order
group = var.web_app_group
subdomain = true
healthcheck {
url = "http://localhost:${var.agentapi_port}/status"
interval = 3
threshold = 20
}
}
resource "coder_app" "agentapi_cli" {
count = var.cli_app ? 1 : 0
slug = var.cli_app_slug
display_name = var.cli_app_display_name
agent_id = var.agent_id
command = <<-EOT
#!/bin/bash
set -e
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
agentapi attach
EOT
icon = var.cli_app_icon
order = var.cli_app_order
group = var.cli_app_group
}
resource "coder_ai_task" "agentapi" {
sidebar_app {
id = coder_app.agentapi_web.id
}
}
@@ -0,0 +1,32 @@
#!/bin/bash
set -o errexit
set -o pipefail
port=${1:-3284}
# This script waits for the agentapi server to start on port 3284.
# It considers the server started after 3 consecutive successful responses.
agentapi_started=false
echo "Waiting for agentapi server to start on port $port..."
for i in $(seq 1 150); do
for j in $(seq 1 3); do
sleep 0.1
if curl -fs -o /dev/null "http://localhost:$port/status"; then
echo "agentapi response received ($j/3)"
else
echo "agentapi server not responding ($i/15)"
continue 2
fi
done
agentapi_started=true
break
done
if [ "$agentapi_started" != "true" ]; then
echo "Error: agentapi server did not start on port $port after 15 seconds."
exit 1
fi
echo "agentapi server started on port $port."
@@ -0,0 +1,96 @@
#!/bin/bash
set -e
set -x
set -o nounset
MODULE_DIR_NAME="$ARG_MODULE_DIR_NAME"
WORKDIR="$ARG_WORKDIR"
PRE_INSTALL_SCRIPT="$ARG_PRE_INSTALL_SCRIPT"
INSTALL_SCRIPT="$ARG_INSTALL_SCRIPT"
INSTALL_AGENTAPI="$ARG_INSTALL_AGENTAPI"
AGENTAPI_VERSION="$ARG_AGENTAPI_VERSION"
START_SCRIPT="$ARG_START_SCRIPT"
WAIT_FOR_START_SCRIPT="$ARG_WAIT_FOR_START_SCRIPT"
POST_INSTALL_SCRIPT="$ARG_POST_INSTALL_SCRIPT"
AGENTAPI_PORT="$ARG_AGENTAPI_PORT"
set +o nounset
command_exists() {
command -v "$1" >/dev/null 2>&1
}
module_path="$HOME/${MODULE_DIR_NAME}"
mkdir -p "$module_path/scripts"
if [ ! -d "${WORKDIR}" ]; then
echo "Warning: The specified folder '${WORKDIR}' does not exist."
echo "Creating the folder..."
mkdir -p "${WORKDIR}"
echo "Folder created successfully."
fi
if [ -n "${PRE_INSTALL_SCRIPT}" ]; then
echo "Running pre-install script..."
echo -n "${PRE_INSTALL_SCRIPT}" >"$module_path/pre_install.sh"
chmod +x "$module_path/pre_install.sh"
"$module_path/pre_install.sh" 2>&1 | tee "$module_path/pre_install.log"
fi
echo "Running install script..."
echo -n "${INSTALL_SCRIPT}" >"$module_path/install.sh"
chmod +x "$module_path/install.sh"
"$module_path/install.sh" 2>&1 | tee "$module_path/install.log"
# Install AgentAPI if enabled
if [ "${INSTALL_AGENTAPI}" = "true" ]; then
echo "Installing AgentAPI..."
arch=$(uname -m)
if [ "$arch" = "x86_64" ]; then
binary_name="agentapi-linux-amd64"
elif [ "$arch" = "aarch64" ]; then
binary_name="agentapi-linux-arm64"
else
echo "Error: Unsupported architecture: $arch"
exit 1
fi
if [ "${AGENTAPI_VERSION}" = "latest" ]; then
# for the latest release the download URL pattern is different than for tagged releases
# https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases
download_url="https://github.com/coder/agentapi/releases/latest/download/$binary_name"
else
download_url="https://github.com/coder/agentapi/releases/download/${AGENTAPI_VERSION}/$binary_name"
fi
curl \
--retry 5 \
--retry-delay 5 \
--fail \
--retry-all-errors \
-L \
-C - \
-o agentapi \
"$download_url"
chmod +x agentapi
sudo mv agentapi /usr/local/bin/agentapi
fi
if ! command_exists agentapi; then
echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually."
exit 1
fi
echo -n "${START_SCRIPT}" >"$module_path/scripts/agentapi-start.sh"
echo -n "${WAIT_FOR_START_SCRIPT}" >"$module_path/scripts/agentapi-wait-for-start.sh"
chmod +x "$module_path/scripts/agentapi-start.sh"
chmod +x "$module_path/scripts/agentapi-wait-for-start.sh"
if [ -n "${POST_INSTALL_SCRIPT}" ]; then
echo "Running post-install script..."
echo -n "${POST_INSTALL_SCRIPT}" >"$module_path/post_install.sh"
chmod +x "$module_path/post_install.sh"
"$module_path/post_install.sh" 2>&1 | tee "$module_path/post_install.log"
fi
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
cd "${WORKDIR}"
nohup "$module_path/scripts/agentapi-start.sh" true "${AGENTAPI_PORT}" &>"$module_path/agentapi-start.log" &
"$module_path/scripts/agentapi-wait-for-start.sh" "${AGENTAPI_PORT}"
@@ -0,0 +1,130 @@
import {
execContainer,
findResourceInstance,
removeContainer,
runContainer,
runTerraformApply,
writeFileContainer,
} from "~test";
import path from "path";
import { expect } from "bun:test";
export const setupContainer = async ({
moduleDir,
image,
vars,
}: {
moduleDir: string;
image?: string;
vars?: Record<string, string>;
}) => {
const state = await runTerraformApply(moduleDir, {
agent_id: "foo",
...vars,
});
const coderScript = findResourceInstance(state, "coder_script");
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
return { id, coderScript, cleanup: () => removeContainer(id) };
};
export const loadTestFile = async (
moduleDir: string,
...relativePath: [string, ...string[]]
) => {
return await Bun.file(
path.join(moduleDir, "testdata", ...relativePath),
).text();
};
export const writeExecutable = async ({
containerId,
filePath,
content,
}: {
containerId: string;
filePath: string;
content: string;
}) => {
await writeFileContainer(containerId, filePath, content, {
user: "root",
});
await execContainer(
containerId,
["bash", "-c", `chmod 755 ${filePath}`],
["--user", "root"],
);
};
interface SetupProps {
skipAgentAPIMock?: boolean;
moduleDir: string;
moduleVariables: Record<string, string>;
projectDir?: string;
registerCleanup: (cleanup: () => Promise<void>) => void;
agentapiMockScript?: string;
}
export const setup = async (props: SetupProps): Promise<{ id: string }> => {
const projectDir = props.projectDir ?? "/home/coder/project";
const { id, coderScript, cleanup } = await setupContainer({
moduleDir: props.moduleDir,
vars: props.moduleVariables,
});
props.registerCleanup(cleanup);
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
if (!props?.skipAgentAPIMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/agentapi",
content:
props.agentapiMockScript ??
(await loadTestFile(import.meta.dir, "agentapi-mock.js")),
});
}
await writeExecutable({
containerId: id,
filePath: "/home/coder/script.sh",
content: coderScript.script,
});
return { id };
};
export const expectAgentAPIStarted = async (
id: string,
port: number = 3284,
) => {
const resp = await execContainer(id, [
"bash",
"-c",
`curl -fs -o /dev/null "http://localhost:${port}/status"`,
]);
if (resp.exitCode !== 0) {
console.log("agentapi not started");
console.log(resp.stdout);
console.log(resp.stderr);
}
expect(resp.exitCode).toBe(0);
};
export const execModuleScript = async (
id: string,
env?: Record<string, string>,
) => {
const envArgs = Object.entries(env ?? {})
.map(([key, value]) => ["--env", `${key}=${value}`])
.flat();
const resp = await execContainer(
id,
[
"bash",
"-c",
`set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`,
],
envArgs,
);
if (resp.exitCode !== 0) {
console.log(resp.stdout);
console.log(resp.stderr);
}
return resp;
};
@@ -0,0 +1,19 @@
#!/usr/bin/env node
const http = require("http");
const args = process.argv.slice(2);
const portIdx = args.findIndex((arg) => arg === "--port") + 1;
const port = portIdx ? args[portIdx] : 3284;
console.log(`starting server on port ${port}`);
http
.createServer(function (_request, response) {
response.writeHead(200);
response.end(
JSON.stringify({
status: "stable",
}),
);
})
.listen(port);
@@ -0,0 +1,16 @@
#!/bin/bash
set -o errexit
set -o pipefail
use_prompt=${1:-false}
port=${2:-3284}
module_path="$HOME/.agentapi-module"
log_file_path="$module_path/agentapi.log"
echo "using prompt: $use_prompt" >>/home/coder/test-agentapi-start.log
echo "using port: $port" >>/home/coder/test-agentapi-start.log
agentapi server --port "$port" --term-width 67 --term-height 1190 -- \
bash -c aiagent \
>"$log_file_path" 2>&1
@@ -0,0 +1,9 @@
#!/usr/bin/env node
const main = async () => {
console.log("mocking an ai agent");
// sleep for 30 minutes
await new Promise((resolve) => setTimeout(resolve, 30 * 60 * 1000));
};
main();
@@ -84,7 +84,6 @@ describe("filebrowser", async () => {
"sh",
"apk add bash",
);
}, 15000);
it("runs with subdomain=false", async () => {
+148
View File
@@ -0,0 +1,148 @@
---
display_name: JetBrains Toolbox
description: Add JetBrains IDE integrations to your Coder workspaces with configurable options.
icon: ../../../../.icons/jetbrains.svg
maintainer_github: coder
verified: true
tags: [ide, jetbrains, parameter]
---
# JetBrains IDEs
This module adds JetBrains IDE buttons to launch IDEs directly from the dashboard by integrating with the JetBrains Toolbox.
```tf
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
```
![JetBrains IDEs list](../../.images/jetbrains-dropdown.png)
> [!IMPORTANT]
> This module requires Coder version 2.24+ and [JetBrains Toolbox](https://www.jetbrains.com/toolbox-app/) version 2.7 or higher.
> [!WARNING]
> JetBrains recommends a minimum of 4 CPU cores and 8GB of RAM.
> Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prerequisites.html#min_requirements) to confirm other system requirements.
## Examples
### Pre-configured Mode (Direct App Creation)
When `default` contains IDE codes, those IDEs are created directly without user selection:
```tf
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA
}
```
### User Choice with Limited Options
```tf
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
# Show parameter with limited options
options = ["IU", "PY"] # Only these IDEs are available for selection
}
```
### Early Access Preview (EAP) Versions
```tf
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
default = ["IU", "PY"]
channel = "eap" # Use Early Access Preview versions
major_version = "2025.2" # Specific major version
}
```
### Custom IDE Configuration
```tf
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
folder = "/workspace/project"
# Custom IDE metadata (display names and icons)
ide_config = {
"IU" = {
name = "IntelliJ IDEA"
icon = "/custom/icons/intellij.svg"
build = "251.26927.53"
}
"PY" = {
name = "PyCharm"
icon = "/custom/icons/pycharm.svg"
build = "251.23774.211"
}
}
}
```
### Single IDE for Specific Use Case
```tf
module "jetbrains_pycharm" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
folder = "/workspace/project"
default = ["PY"] # Only PyCharm
# Specific version for consistency
major_version = "2025.1"
channel = "release"
}
```
## Behavior
### Parameter vs Direct Apps
- **`default = []` (empty)**: Creates a `coder_parameter` allowing users to select IDEs from `options`
- **`default` with values**: Skips parameter and directly creates `coder_app` resources for the specified IDEs
### Version Resolution
- Build numbers are fetched from the JetBrains API for the latest compatible versions when internet access is available
- If the API is unreachable (air-gapped environments), the module automatically falls back to build numbers from `ide_config`
- `major_version` and `channel` control which API endpoint is queried (when API access is available)
## Supported IDEs
All JetBrains IDEs with remote development capabilities:
- [CLion (`CL`)](https://www.jetbrains.com/clion/)
- [GoLand (`GO`)](https://www.jetbrains.com/go/)
- [IntelliJ IDEA Ultimate (`IU`)](https://www.jetbrains.com/idea/)
- [PhpStorm (`PS`)](https://www.jetbrains.com/phpstorm/)
- [PyCharm Professional (`PY`)](https://www.jetbrains.com/pycharm/)
- [Rider (`RD`)](https://www.jetbrains.com/rider/)
- [RubyMine (`RM`)](https://www.jetbrains.com/ruby/)
- [RustRover (`RR`)](https://www.jetbrains.com/rust/)
- [WebStorm (`WS`)](https://www.jetbrains.com/webstorm/)
File diff suppressed because it is too large Load Diff
+250
View File
@@ -0,0 +1,250 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
http = {
source = "hashicorp/http"
version = ">= 3.0"
}
}
}
variable "agent_id" {
type = string
description = "The resource ID of a Coder agent."
}
variable "agent_name" {
type = string
description = "The name of a Coder agent. Needed for workspaces with multiple agents."
default = null
}
variable "folder" {
type = string
description = "The directory to open in the IDE. e.g. /home/coder/project"
validation {
condition = can(regex("^(?:/[^/]+)+/?$", var.folder))
error_message = "The folder must be a full path and must not start with a ~."
}
}
variable "default" {
default = []
type = set(string)
description = <<-EOT
The default IDE selection. Removes the selection from the UI. e.g. ["CL", "GO", "IU"]
EOT
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "coder_app_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 "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
variable "major_version" {
type = string
description = "The major version of the IDE. i.e. 2025.1"
default = "latest"
validation {
condition = can(regex("^[0-9]{4}\\.[0-2]{1}$", var.major_version)) || var.major_version == "latest"
error_message = "The major_version must be a valid version number. i.e. 2025.1 or latest"
}
}
variable "channel" {
type = string
description = "JetBrains IDE release channel. Valid values are release and eap."
default = "release"
validation {
condition = can(regex("^(release|eap)$", var.channel))
error_message = "The channel must be either release or eap."
}
}
variable "options" {
type = set(string)
description = "The list of IDE product codes."
default = ["CL", "GO", "IU", "PS", "PY", "RD", "RM", "RR", "WS"]
validation {
condition = (
alltrue([
for code in var.options : contains(["CL", "GO", "IU", "PS", "PY", "RD", "RM", "RR", "WS"], code)
])
)
error_message = "The options must be a set of valid product codes. Valid product codes are ${join(",", ["CL", "GO", "IU", "PS", "PY", "RD", "RM", "RR", "WS"])}."
}
# check if the set is empty
validation {
condition = length(var.options) > 0
error_message = "The options must not be empty."
}
}
variable "releases_base_link" {
type = string
description = "URL of the JetBrains releases base link."
default = "https://data.services.jetbrains.com"
validation {
condition = can(regex("^https?://.+$", var.releases_base_link))
error_message = "The releases_base_link must be a valid HTTP/S address."
}
}
variable "download_base_link" {
type = string
description = "URL of the JetBrains download base link."
default = "https://download.jetbrains.com"
validation {
condition = can(regex("^https?://.+$", var.download_base_link))
error_message = "The download_base_link must be a valid HTTP/S address."
}
}
data "http" "jetbrains_ide_versions" {
for_each = length(var.default) == 0 ? var.options : var.default
url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}&latest=true${var.major_version == "latest" ? "" : "&major_version=${var.major_version}"}"
}
variable "ide_config" {
description = <<-EOT
A map of JetBrains IDE configurations.
The key is the product code and the value is an object with the following properties:
- name: The name of the IDE.
- icon: The icon of the IDE.
- build: The build number of the IDE.
Example:
{
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
}
EOT
type = map(object({
name = string
icon = string
build = string
}))
default = {
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "251.26927.60" },
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "251.26927.74" },
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "251.26927.67" },
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "251.26927.47" },
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "251.26927.79" },
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "251.26927.40" }
}
validation {
condition = length(var.ide_config) > 0
error_message = "The ide_config must not be empty."
}
# ide_config must be a superset of var.. options
validation {
condition = alltrue([
for code in var.options : contains(keys(var.ide_config), code)
])
error_message = "The ide_config must be a superset of var.options."
}
}
locals {
# Parse HTTP responses once with error handling for air-gapped environments
parsed_responses = {
for code in length(var.default) == 0 ? var.options : var.default : code => try(
jsondecode(data.http.jetbrains_ide_versions[code].response_body),
{} # Return empty object if API call fails
)
}
# Dynamically generate IDE configurations based on options with fallback to ide_config
options_metadata = {
for code in length(var.default) == 0 ? var.options : var.default : code => {
icon = var.ide_config[code].icon
name = var.ide_config[code].name
identifier = code
key = code
# Use API build number if available, otherwise fall back to ide_config build number
build = length(keys(local.parsed_responses[code])) > 0 ? (
local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0].build
) : var.ide_config[code].build
# Store API data for potential future use (only if API is available)
json_data = length(keys(local.parsed_responses[code])) > 0 ? local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0] : null
response_key = length(keys(local.parsed_responses[code])) > 0 ? keys(local.parsed_responses[code])[0] : null
}
}
# Convert the parameter value to a set for for_each
selected_ides = length(var.default) == 0 ? toset(jsondecode(coalesce(data.coder_parameter.jetbrains_ides[0].value, "[]"))) : toset(var.default)
}
data "coder_parameter" "jetbrains_ides" {
count = length(var.default) == 0 ? 1 : 0
type = "list(string)"
name = "jetbrains_ides"
display_name = "JetBrains IDEs"
icon = "/icon/jetbrains-toolbox.svg"
mutable = true
default = jsonencode([])
order = var.coder_parameter_order
form_type = "multi-select" # requires Coder version 2.24+
dynamic "option" {
for_each = var.options
content {
icon = var.ide_config[option.value].icon
name = var.ide_config[option.value].name
value = option.value
}
}
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_app" "jetbrains" {
for_each = local.selected_ides
agent_id = var.agent_id
slug = "jetbrains-${lower(each.key)}"
display_name = local.options_metadata[each.key].name
icon = local.options_metadata[each.key].icon
external = true
order = var.coder_app_order
url = join("", [
"jetbrains://gateway/coder?&workspace=", # requires 2.6.3+ version of Toolbox
data.coder_workspace.me.name,
"&owner=",
data.coder_workspace_owner.me.name,
"&folder=",
var.folder,
"&url=",
data.coder_workspace.me.access_url,
"&token=",
"$SESSION_TOKEN",
"&ide_product_code=",
each.key,
"&ide_build_number=",
local.options_metadata[each.key].build,
var.agent_name != null ? "&agent_name=${var.agent_name}" : "",
])
}
@@ -12,6 +12,12 @@ tags: [rdp, windows, desktop, local]
This module enables Remote Desktop Protocol (RDP) on Windows workspaces and adds a one-click button to launch RDP sessions directly through [Coder Desktop](https://coder.com/docs/user-guides/desktop). It provides a complete, standalone solution for RDP access, eliminating the need for manual configuration or port forwarding through the Coder CLI.
<!--
2025-07-07 - Prettier isn't formatting GFM comments properly if they don't
start with a letter.
See https://github.com/prettier/prettier/issues/15479
-->
<!-- prettier-ignore -->
> [!NOTE]
> [Coder Desktop](https://coder.com/docs/user-guides/desktop) is required on client devices to use the Local Windows RDP access feature.
+2 -6
View File
@@ -16,9 +16,7 @@ describe("zed", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
expect(state.outputs.zed_url.value).toBe(
"zed://ssh/default.coder",
);
expect(state.outputs.zed_url.value).toBe("zed://ssh/default.coder");
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "zed",
@@ -34,9 +32,7 @@ describe("zed", async () => {
agent_id: "foo",
folder: "/foo/bar",
});
expect(state.outputs.zed_url.value).toBe(
"zed://ssh/default.coder/foo/bar",
);
expect(state.outputs.zed_url.value).toBe("zed://ssh/default.coder/foo/bar");
});
it("expect order to be set", async () => {
@@ -10,7 +10,7 @@ tags: [vm, linux, aws, persistent, devcontainer]
# Remote Development on AWS EC2 VMs using a Devcontainer
Provision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs) with this example template.
![Architecture Diagram](./architecture.svg)
![Architecture Diagram](../../.images/aws-devcontainer-architecture.svg)
<!-- TODO: Add screenshot -->
@@ -9,7 +9,7 @@ tags: [vm, linux, gcp, devcontainer]
# Remote Development in a Devcontainer on Google Compute Engine
![Architecture Diagram](./architecture.svg)
![Architecture Diagram](../../.images/gcp-devcontainer-architecture.svg)
## Prerequisites
+10
View File
@@ -324,3 +324,13 @@ export const writeFileContainer = async (
}
expect(proc.exitCode).toBe(0);
};
export const readFileContainer = async (id: string, path: string) => {
const proc = await execContainer(id, ["cat", path], ["--user", "root"]);
if (proc.exitCode !== 0) {
console.log(proc.stderr);
console.log(proc.stdout);
}
expect(proc.exitCode).toBe(0);
return proc.stdout;
};