Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a3c9b01b8 | |||
| 05b24daccb | |||
| 7f51b2ffdd | |||
| 8351e91bbe | |||
| b040ad1b1c | |||
| b74290051e | |||
| d7fdc793c7 | |||
| 047a5d654b | |||
| 5554283a2f | |||
| fa939bbd5a | |||
| cde8fe3b30 | |||
| 69996231c8 | |||
| c0f2d945c5 | |||
| 48cb3e58b0 | |||
| 0950466310 |
@@ -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 #
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="none" viewBox="0 0 64 64">
|
||||
<defs>
|
||||
<radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="rotate(49.385 -18.029987 36.649084) scale(49.4299)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".026042" stop-color="#8DFDFD"/>
|
||||
<stop offset=".270833" stop-color="#87FBFB"/>
|
||||
<stop offset=".484416" stop-color="#74D6F4"/>
|
||||
<stop offset=".931964" stop-color="#0038FF"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="b" cx="0" cy="0" r="1" gradientTransform="rotate(132.274 3.919184 20.864728) scale(23.7857)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0500FF" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#0100FF" stop-opacity=".15"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="c" cx="0" cy="0" r="1" gradientTransform="rotate(42.678 -19.143042 44.644478) scale(41.8951)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".520394" stop-color="#FF00E5" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#FF00E5" stop-opacity=".65"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="matrix(30.00005 -22.00001 19.46596 26.54453 32.3943 42.4)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".777466" stop-color="#001AFF"/>
|
||||
<stop offset="1" stop-color="#8ACEFF"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="f" cx="0" cy="0" r="1" gradientTransform="matrix(14.91531 -8.80077 11.61873 19.69112 44.057 27.7156)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".71875" stop-color="#FA00FF" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#FF00D6" stop-opacity=".44"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="h" cx="0" cy="0" r="1" gradientTransform="rotate(63.435 -9.856848 34.706598) scale(30.4105 69.8305)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0D67A9"/>
|
||||
<stop offset="1" stop-color="#AEDDFF"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="j" cx="0" cy="0" r="1" gradientTransform="rotate(73.835 -3.838438 33.695644) scale(28.736 56.1739)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0068C9"/>
|
||||
<stop offset="1" stop-color="#fff"/>
|
||||
</radialGradient>
|
||||
<filter id="g" width="48.3057" height="34.5039" x="8.25781" y="24.2656" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur result="effect1_foregroundBlur_6490_3223" stdDeviation="1"/>
|
||||
</filter>
|
||||
<filter id="i" width="45.7057" height="31.9039" x="9.55781" y="25.5656" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur result="effect1_foregroundBlur_6490_3223" stdDeviation=".35"/>
|
||||
</filter>
|
||||
<linearGradient id="d" x1="63.9941" x2="37.1941" y1="33.6" y2="34.4" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FD3AF5"/>
|
||||
<stop offset="1" stop-color="#FD3AF5" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#a)" d="M63.9941 32c0 17.6731-14.3268 32-32 32C14.3211 64-.00586 49.6731-.00586 32c0-17.6731 14.32696-32 31.99996-32 8.306 3.75956 16.9952 17.7487 20.6338 22.1202 3.6389 4.3715 13.0573 9.8798 8.4414-1.6751 1.9681 3.1232 2.9248 8.3051 2.9248 11.5549Z"/>
|
||||
<path fill="url(#b)" d="M63.9941 32c0 17.6731-14.3268 32-32 32C14.3211 64-.00586 49.6731-.00586 32c0-17.6731 14.32696-32 31.99996-32 8.306 3.75956 16.9952 17.7487 20.6338 22.1202 3.6389 4.3715 13.0573 9.8798 8.4414-1.6751 1.9681 3.1232 2.9248 8.3051 2.9248 11.5549Z"/>
|
||||
<path fill="url(#c)" d="M63.9941 32c0 17.6731-14.3268 32-32 32C14.3211 64-.00586 49.6731-.00586 32c0-17.6731 14.32696-32 31.99996-32 8.306 3.75956 16.9952 17.7487 20.6338 22.1202 3.6389 4.3715 13.0573 9.8798 8.4414-1.6751 1.9681 3.1232 2.9248 8.3051 2.9248 11.5549Z"/>
|
||||
<path fill="url(#d)" fill-opacity=".3" d="M63.9941 32c0 17.6731-14.3268 32-32 32C14.3211 64-.00586 49.6731-.00586 32c0-17.6731 14.32696-32 31.99996-32 8.306 3.75956 16.9952 17.7487 20.6338 22.1202 3.6389 4.3715 13.0573 9.8798 8.4414-1.6751 1.9681 3.1232 2.9248 8.3051 2.9248 11.5549Z"/>
|
||||
<path fill="url(#e)" d="M61.0886 20.4758c-3.1529-5.3391-9.686-9.2821-17.5378-10.2688 2.2608 2.7375 4.3175 5.5453 6.0172 7.8664 1.2242 1.6711 2.2633 3.0899 3.0601 4.0469 3.6389 4.3711 13.0573 9.8797 8.4414-1.675.0063.0102.0127.0203.0191.0305Z"/>
|
||||
<path fill="url(#f)" d="M61.0886 20.4758c-3.1529-5.3391-9.686-9.2821-17.5378-10.2688 2.2608 2.7375 4.3175 5.5453 6.0172 7.8664 1.2242 1.6711 2.2633 3.0899 3.0601 4.0469 3.6389 4.3711 13.0573 9.8797 8.4414-1.675.0063.0102.0127.0203.0191.0305Z"/>
|
||||
<g filter="url(#g)">
|
||||
<path fill="url(#h)" d="M29.3127 27.0066c12.113-2.5862 23.3196 1.8139 25.0306 9.8279 1.711 8.014-6.7214 16.6071-18.8343 19.1933C23.396 58.614 12.1894 54.214 10.4783 46.2c-1.711-8.014 6.7215-16.6072 18.8344-19.1934Z"/>
|
||||
</g>
|
||||
<g filter="url(#i)">
|
||||
<path fill="url(#j)" fill-opacity=".2" fill-rule="evenodd" d="M48.9867 47.3643c3.1734-3.2337 4.5278-6.8744 3.8174-10.2012-.7102-3.3268-3.4332-6.0967-7.6507-7.7527-4.2039-1.6506-9.7148-2.1025-15.5122-.8645-5.7973 1.2377-10.6433 3.9007-13.8065 7.1243-3.1734 3.2339-4.5276 6.8744-3.8173 10.2012.7103 3.3267 3.4332 6.0968 7.6506 7.7527 4.2039 1.6506 9.7148 2.1024 15.5122.8647 5.7974-1.2377 10.6433-3.9009 13.8065-7.1245Zm-13.4778 8.6636c12.1128-2.5862 20.5452-11.1793 18.8343-19.1933-1.7112-8.0141-12.9177-12.4142-25.0305-9.828-12.1131 2.5862-20.54545 11.1795-18.8344 19.1935 1.711 8.0138 12.9176 12.414 25.0306 9.8278Z" clip-rule="evenodd"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 452 B After Width: | Height: | Size: 452 B |
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.18",
|
||||
"bun-types": "^1.2.18",
|
||||
"dedent": "^1.6.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^16.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
|
||||
@@ -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 +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 |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 975 KiB |
@@ -1,8 +1,8 @@
|
||||
---
|
||||
display_name: Tasks on Docker
|
||||
description: Run Coder Tasks on Docker with an example application
|
||||
icon: ./.images/tasks.svg
|
||||
maintainer_github: coder
|
||||
icon: ../../../../.icons/tasks.svg
|
||||
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 |
|
After Width: | Height: | Size: 40 KiB |
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
@@ -14,7 +14,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 = "2.0.1"
|
||||
version = "2.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -85,7 +85,7 @@ resource "coder_agent" "main" {
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -103,7 +103,7 @@ Run Claude Code as a standalone app in your workspace. This will install Claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
|
||||
@@ -19,7 +19,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -32,7 +32,7 @@ module "dotfiles" {
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -43,7 +43,7 @@ module "dotfiles" {
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
user = "root"
|
||||
}
|
||||
@@ -55,14 +55,14 @@ module "dotfiles" {
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
module "dotfiles-root" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
user = "root"
|
||||
dotfiles_uri = module.dotfiles.dotfiles_uri
|
||||
@@ -77,7 +77,7 @@ You can set a default dotfiles repository for all users by setting the `default_
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
default_dotfiles_uri = "https://github.com/coder/dotfiles"
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ variable "agent_id" {
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "description" {
|
||||
type = string
|
||||
description = "A custom description for the dotfiles parameter. This is shown in the UI - and allows you to customize the instructions you give to your users."
|
||||
default = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace"
|
||||
}
|
||||
|
||||
variable "default_dotfiles_uri" {
|
||||
type = string
|
||||
description = "The default dotfiles URI if the workspace user does not provide one"
|
||||
@@ -64,7 +70,7 @@ data "coder_parameter" "dotfiles_uri" {
|
||||
display_name = "Dotfiles URL"
|
||||
order = var.coder_parameter_order
|
||||
default = var.default_dotfiles_uri
|
||||
description = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace"
|
||||
description = var.description
|
||||
mutable = true
|
||||
icon = "/icon/dotfiles.svg"
|
||||
}
|
||||
|
||||
@@ -84,7 +84,6 @@ describe("filebrowser", async () => {
|
||||
"sh",
|
||||
"apk add bash",
|
||||
);
|
||||
|
||||
}, 15000);
|
||||
|
||||
it("runs with subdomain=false", async () => {
|
||||
|
||||
@@ -15,7 +15,7 @@ This module allows you to automatically clone a repository by URL and skip if it
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
}
|
||||
@@ -29,7 +29,7 @@ module "git-clone" {
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
base_dir = "~/projects/coder"
|
||||
@@ -44,7 +44,7 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
}
|
||||
@@ -70,7 +70,7 @@ data "coder_parameter" "git_repo" {
|
||||
module "git_clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = data.coder_parameter.git_repo.value
|
||||
}
|
||||
@@ -104,7 +104,7 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.example.com/coder/coder/tree/feat/example"
|
||||
git_providers = {
|
||||
@@ -123,7 +123,7 @@ To GitLab clone with a specific branch like `feat/example`
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
|
||||
}
|
||||
@@ -135,7 +135,7 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
|
||||
git_providers = {
|
||||
@@ -156,7 +156,7 @@ For example, to clone the `feat/example` branch:
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
branch_name = "feat/example"
|
||||
@@ -165,7 +165,8 @@ module "git-clone" {
|
||||
|
||||
## Git clone with different destination folder
|
||||
|
||||
By default, the repository will be cloned into a folder matching the repository name. You can use the `folder_name` attribute to change the name of the destination folder to something else.
|
||||
By default, the repository will be cloned into a folder matching the repository name.
|
||||
You can use the `folder_name` attribute to change the name of the destination folder to something else.
|
||||
|
||||
For example, this will clone into the `~/projects/coder/coder-dev` folder:
|
||||
|
||||
@@ -173,10 +174,28 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder:
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
folder_name = "coder-dev"
|
||||
base_dir = "~/projects/coder"
|
||||
}
|
||||
```
|
||||
|
||||
## Git shallow clone
|
||||
|
||||
Limit the clone history to speed-up workspace startup by setting `depth`.
|
||||
|
||||
When `depth` is greater than `0` the module runs `git clone --depth <depth>`.
|
||||
If not defined, the default, `0`, performs a full clone.
|
||||
|
||||
```tf
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
depth = 1
|
||||
}
|
||||
```
|
||||
|
||||
@@ -56,6 +56,12 @@ variable "folder_name" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "depth" {
|
||||
description = "If > 0, perform a shallow clone using this depth."
|
||||
type = number
|
||||
default = 0
|
||||
}
|
||||
|
||||
locals {
|
||||
# Remove query parameters and fragments from the URL
|
||||
url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "")
|
||||
@@ -113,6 +119,7 @@ resource "coder_script" "git_clone" {
|
||||
CLONE_PATH = local.clone_path,
|
||||
REPO_URL : local.clone_url,
|
||||
BRANCH_NAME : local.branch_name,
|
||||
DEPTH = var.depth,
|
||||
})
|
||||
display_name = "Git Clone"
|
||||
icon = "/icon/git.svg"
|
||||
|
||||
@@ -5,6 +5,7 @@ CLONE_PATH="${CLONE_PATH}"
|
||||
BRANCH_NAME="${BRANCH_NAME}"
|
||||
# Expand home if it's specified!
|
||||
CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}"
|
||||
DEPTH="${DEPTH}"
|
||||
|
||||
# Check if the variable is empty...
|
||||
if [ -z "$REPO_URL" ]; then
|
||||
@@ -36,10 +37,18 @@ fi
|
||||
if [ -z "$(ls -A "$CLONE_PATH")" ]; then
|
||||
if [ -z "$BRANCH_NAME" ]; then
|
||||
echo "Cloning $REPO_URL to $CLONE_PATH..."
|
||||
git clone "$REPO_URL" "$CLONE_PATH"
|
||||
if [ "$DEPTH" -gt 0 ]; then
|
||||
git clone --depth "$DEPTH" "$REPO_URL" "$CLONE_PATH"
|
||||
else
|
||||
git clone "$REPO_URL" "$CLONE_PATH"
|
||||
fi
|
||||
else
|
||||
echo "Cloning $REPO_URL to $CLONE_PATH on branch $BRANCH_NAME..."
|
||||
git clone "$REPO_URL" -b "$BRANCH_NAME" "$CLONE_PATH"
|
||||
if [ "$DEPTH" -gt 0 ]; then
|
||||
git clone --depth "$DEPTH" -b "$BRANCH_NAME" "$REPO_URL" "$CLONE_PATH"
|
||||
else
|
||||
git clone "$REPO_URL" -b "$BRANCH_NAME" "$CLONE_PATH"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "$CLONE_PATH already exists and isn't empty, skipping clone!"
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Run Goose in your workspace
|
||||
icon: ../../../../.icons/goose.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [agent, goose, ai]
|
||||
tags: [agent, goose, ai, tasks]
|
||||
---
|
||||
|
||||
# Goose
|
||||
@@ -13,36 +13,27 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener
|
||||
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
goose_version = "v1.0.16"
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
goose_version = "v1.0.31"
|
||||
goose_provider = "anthropic"
|
||||
goose_model = "claude-3-5-sonnet-latest"
|
||||
agentapi_version = "latest"
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `screen` or `tmux` must be installed in your workspace to run Goose in the background
|
||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
|
||||
|
||||
The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces.
|
||||
|
||||
## Examples
|
||||
|
||||
Your workspace must have `screen` or `tmux` installed to use the background session functionality.
|
||||
|
||||
### Run in the background and report tasks (Experimental)
|
||||
|
||||
> This functionality is in early access as of Coder v2.21 and is still evolving.
|
||||
> For now, we recommend testing it in a demo or staging environment,
|
||||
> rather than deploying to production
|
||||
>
|
||||
> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents)
|
||||
>
|
||||
> Join our [Discord channel](https://discord.gg/coder) or
|
||||
> [contact us](https://coder.com/contact) to get help or share feedback.
|
||||
### Run in the background and report tasks
|
||||
|
||||
```tf
|
||||
module "coder-login" {
|
||||
@@ -81,37 +72,23 @@ resource "coder_agent" "main" {
|
||||
EOT
|
||||
GOOSE_TASK_PROMPT = data.coder_parameter.ai_prompt.value
|
||||
|
||||
# An API key is required for experiment_auto_configure
|
||||
# See https://block.github.io/goose/docs/getting-started/providers
|
||||
ANTHROPIC_API_KEY = var.anthropic_api_key # or use a coder_parameter
|
||||
}
|
||||
}
|
||||
|
||||
module "goose" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
goose_version = "v1.0.16"
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
goose_version = "v1.0.31"
|
||||
agentapi_version = "latest"
|
||||
|
||||
# Enable experimental features
|
||||
experiment_report_tasks = true
|
||||
|
||||
# Run Goose in the background with screen (pick one: screen or tmux)
|
||||
experiment_use_screen = true
|
||||
# experiment_use_tmux = true # Alternative: use tmux instead of screen
|
||||
|
||||
# Optional: customize the session name (defaults to "goose")
|
||||
# session_name = "goose-session"
|
||||
|
||||
# Avoid configuring Goose manually
|
||||
experiment_auto_configure = true
|
||||
|
||||
# Required for experiment_auto_configure
|
||||
experiment_goose_provider = "anthropic"
|
||||
experiment_goose_model = "claude-3-5-sonnet-latest"
|
||||
goose_provider = "anthropic"
|
||||
goose_model = "claude-3-5-sonnet-latest"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -123,11 +100,11 @@ You can extend Goose's capabilities by adding custom extensions. For example, to
|
||||
module "goose" {
|
||||
# ... other configuration ...
|
||||
|
||||
experiment_pre_install_script = <<-EOT
|
||||
pre_install_script = <<-EOT
|
||||
npm i -g @wonderwhy-er/desktop-commander@latest
|
||||
EOT
|
||||
|
||||
experiment_additional_extensions = <<-EOT
|
||||
additional_extensions = <<-EOT
|
||||
desktop-commander:
|
||||
args: []
|
||||
cmd: desktop-commander
|
||||
@@ -145,20 +122,6 @@ This will add the desktop-commander extension to Goose, allowing it to run comma
|
||||
|
||||
Note: The indentation in the heredoc is preserved, so you can write the YAML naturally.
|
||||
|
||||
## Run standalone
|
||||
## Troubleshooting
|
||||
|
||||
Run Goose as a standalone app in your workspace. This will install Goose and run it directly without using screen or tmux, and without any task reporting to the Coder UI.
|
||||
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
goose_version = "v1.0.16"
|
||||
|
||||
# Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL
|
||||
icon = "https://raw.githubusercontent.com/block/goose/refs/heads/main/ui/desktop/src/images/icon.svg"
|
||||
}
|
||||
```
|
||||
The module will create log files in the workspace's `~/.goose-module` directory. If you run into any issues, look at them for more information.
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
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 "../agentapi/test-util";
|
||||
import dedent from "dedent";
|
||||
|
||||
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;
|
||||
skipGooseMock?: 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_goose: props?.skipGooseMock ? "true" : "false",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
goose_provider: "test-provider",
|
||||
goose_model: "test-model",
|
||||
...props?.moduleVariables,
|
||||
},
|
||||
registerCleanup,
|
||||
projectDir,
|
||||
skipAgentAPIMock: props?.skipAgentAPIMock,
|
||||
agentapiMockScript: props?.agentapiMockScript,
|
||||
});
|
||||
if (!props?.skipGooseMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/goose",
|
||||
content: await loadTestFile(import.meta.dir, "goose-mock.sh"),
|
||||
});
|
||||
}
|
||||
return { id };
|
||||
};
|
||||
|
||||
// increase the default timeout to 60 seconds
|
||||
setDefaultTimeout(60 * 1000);
|
||||
|
||||
describe("goose", async () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
test("happy-path", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("install-version", async () => {
|
||||
const { id } = await setup({
|
||||
skipGooseMock: true,
|
||||
moduleVariables: {
|
||||
install_goose: "true",
|
||||
goose_version: "v1.0.24",
|
||||
},
|
||||
});
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`"$HOME/.local/bin/goose" --version`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
expect(resp.exitCode).toBe(0);
|
||||
expect(resp.stdout).toContain("1.0.24");
|
||||
});
|
||||
|
||||
test("install-stable", async () => {
|
||||
const { id } = await setup({
|
||||
skipGooseMock: true,
|
||||
moduleVariables: {
|
||||
install_goose: "true",
|
||||
goose_version: "stable",
|
||||
},
|
||||
});
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`"$HOME/.local/bin/goose" --version`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
expect(resp.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("config", async () => {
|
||||
const expected =
|
||||
dedent`
|
||||
GOOSE_PROVIDER: anthropic
|
||||
GOOSE_MODEL: claude-3-5-sonnet-latest
|
||||
extensions:
|
||||
coder:
|
||||
args:
|
||||
- exp
|
||||
- mcp
|
||||
- server
|
||||
cmd: coder
|
||||
description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
|
||||
enabled: true
|
||||
envs:
|
||||
CODER_MCP_APP_STATUS_SLUG: goose
|
||||
CODER_MCP_AI_AGENTAPI_URL: http://localhost:3284
|
||||
name: Coder
|
||||
timeout: 3000
|
||||
type: stdio
|
||||
developer:
|
||||
display_name: Developer
|
||||
enabled: true
|
||||
name: developer
|
||||
timeout: 300
|
||||
type: builtin
|
||||
custom-stuff:
|
||||
enabled: true
|
||||
name: custom-stuff
|
||||
timeout: 300
|
||||
type: builtin
|
||||
`.trim() + "\n";
|
||||
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
goose_provider: "anthropic",
|
||||
goose_model: "claude-3-5-sonnet-latest",
|
||||
additional_extensions: dedent`
|
||||
custom-stuff:
|
||||
enabled: true
|
||||
name: custom-stuff
|
||||
timeout: 300
|
||||
type: builtin
|
||||
`.trim(),
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.config/goose/config.yaml",
|
||||
);
|
||||
expect(resp).toEqual(expected);
|
||||
});
|
||||
|
||||
test("pre-post-install-scripts", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
|
||||
post_install_script: "#!/bin/bash\necho 'post-install-script'",
|
||||
},
|
||||
});
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const preInstallLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.goose-module/pre_install.log",
|
||||
);
|
||||
expect(preInstallLog).toContain("pre-install-script");
|
||||
|
||||
const postInstallLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.goose-module/post_install.log",
|
||||
);
|
||||
expect(postInstallLog).toContain("post-install-script");
|
||||
});
|
||||
|
||||
const promptFile = "/home/coder/.goose-module/prompt.txt";
|
||||
const agentapiStartLog = "/home/coder/.goose-module/agentapi-start.log";
|
||||
|
||||
test("start-with-prompt", async () => {
|
||||
const { id } = await setup({
|
||||
agentapiMockScript: await loadTestFile(
|
||||
import.meta.dir,
|
||||
"agentapi-mock-print-args.js",
|
||||
),
|
||||
});
|
||||
await execModuleScript(id, {
|
||||
GOOSE_TASK_PROMPT: "custom-test-prompt",
|
||||
});
|
||||
const prompt = await readFileContainer(id, promptFile);
|
||||
expect(prompt).toContain("custom-test-prompt");
|
||||
|
||||
const agentapiMockOutput = await readFileContainer(id, agentapiStartLog);
|
||||
expect(agentapiMockOutput).toContain(
|
||||
"'goose run --interactive --instructions /home/coder/.goose-module/prompt.txt '",
|
||||
);
|
||||
});
|
||||
|
||||
test("start-without-prompt", async () => {
|
||||
const { id } = await setup({
|
||||
agentapiMockScript: await loadTestFile(
|
||||
import.meta.dir,
|
||||
"agentapi-mock-print-args.js",
|
||||
),
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const agentapiMockOutput = await readFileContainer(id, agentapiStartLog);
|
||||
expect(agentapiMockOutput).toContain("'goose '");
|
||||
|
||||
const prompt = await execContainer(id, ["ls", "-l", promptFile]);
|
||||
expect(prompt.exitCode).not.toBe(0);
|
||||
expect(prompt.stderr).toContain("No such file or directory");
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,67 +54,48 @@ variable "goose_version" {
|
||||
default = "stable"
|
||||
}
|
||||
|
||||
variable "experiment_use_screen" {
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to use screen for running Goose in the background."
|
||||
default = false
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "experiment_use_tmux" {
|
||||
type = bool
|
||||
description = "Whether to use tmux instead of screen for running Goose in the background."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "session_name" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "Name for the persistent session (screen or tmux)"
|
||||
default = "goose"
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.2.3"
|
||||
}
|
||||
|
||||
variable "experiment_report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_auto_configure" {
|
||||
type = bool
|
||||
description = "Whether to automatically configure Goose."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_goose_provider" {
|
||||
variable "goose_provider" {
|
||||
type = string
|
||||
description = "The provider to use for Goose (e.g., anthropic)."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "experiment_goose_model" {
|
||||
variable "goose_model" {
|
||||
type = string
|
||||
description = "The model to use for Goose (e.g., claude-3-5-sonnet-latest)."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Goose."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Goose."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_additional_extensions" {
|
||||
variable "additional_extensions" {
|
||||
type = string
|
||||
description = "Additional extensions configuration in YAML format to append to the config."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
app_slug = "goose"
|
||||
base_extensions = <<-EOT
|
||||
coder:
|
||||
args:
|
||||
@@ -125,7 +106,8 @@ coder:
|
||||
description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
|
||||
enabled: true
|
||||
envs:
|
||||
CODER_MCP_APP_STATUS_SLUG: goose
|
||||
CODER_MCP_APP_STATUS_SLUG: ${local.app_slug}
|
||||
CODER_MCP_AI_AGENTAPI_URL: http://localhost:3284
|
||||
name: Coder
|
||||
timeout: 3000
|
||||
type: stdio
|
||||
@@ -139,204 +121,47 @@ EOT
|
||||
|
||||
# Add two spaces to each line of extensions to match YAML structure
|
||||
formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}"
|
||||
additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : ""
|
||||
|
||||
combined_extensions = <<-EOT
|
||||
additional_extensions = var.additional_extensions != null ? "\n ${replace(trimspace(var.additional_extensions), "\n", "\n ")}" : ""
|
||||
combined_extensions = <<-EOT
|
||||
extensions:
|
||||
${local.formatted_base}${local.additional_extensions}
|
||||
EOT
|
||||
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".goose-module"
|
||||
}
|
||||
|
||||
# Install and Initialize Goose
|
||||
resource "coder_script" "goose" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Goose"
|
||||
icon = var.icon
|
||||
script = <<-EOT
|
||||
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 = "${local.app_slug}-cli"
|
||||
cli_app_display_name = "Goose CLI"
|
||||
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 = local.start_script
|
||||
install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
|
||||
# Run pre-install script if provided
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
|
||||
chmod +x /tmp/pre_install.sh
|
||||
/tmp/pre_install.sh
|
||||
fi
|
||||
|
||||
# Install Goose if enabled
|
||||
if [ "${var.install_goose}" = "true" ]; then
|
||||
if ! command_exists npm; then
|
||||
echo "Error: npm is not installed. Please install Node.js and npm first."
|
||||
exit 1
|
||||
fi
|
||||
echo "Installing Goose..."
|
||||
RELEASE_TAG=v${var.goose_version} curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash
|
||||
fi
|
||||
|
||||
# Run post-install script if provided
|
||||
if [ -n "${local.encoded_post_install_script}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
|
||||
chmod +x /tmp/post_install.sh
|
||||
/tmp/post_install.sh
|
||||
fi
|
||||
|
||||
# Configure Goose if auto-configure is enabled
|
||||
if [ "${var.experiment_auto_configure}" = "true" ]; then
|
||||
echo "Configuring Goose..."
|
||||
mkdir -p "$HOME/.config/goose"
|
||||
cat > "$HOME/.config/goose/config.yaml" << EOL
|
||||
GOOSE_PROVIDER: ${var.experiment_goose_provider}
|
||||
GOOSE_MODEL: ${var.experiment_goose_model}
|
||||
${trimspace(local.combined_extensions)}
|
||||
EOL
|
||||
fi
|
||||
|
||||
# Write system prompt to config
|
||||
mkdir -p "$HOME/.config/goose"
|
||||
echo "$GOOSE_SYSTEM_PROMPT" > "$HOME/.config/goose/.goosehints"
|
||||
|
||||
# Handle terminal multiplexer selection (tmux or screen)
|
||||
if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
|
||||
echo "Please set only one of them to true."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine goose command
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run with tmux if enabled
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
echo "Running Goose in the background with tmux..."
|
||||
|
||||
# Check if tmux is installed
|
||||
if ! command_exists tmux; then
|
||||
echo "Error: tmux is not installed. Please install tmux manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.goose.log"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
# Configure tmux for shared sessions
|
||||
if [ ! -f "$HOME/.tmux.conf" ]; then
|
||||
echo "Creating ~/.tmux.conf with shared session settings..."
|
||||
echo "set -g mouse on" > "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
|
||||
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
|
||||
echo "set -g mouse on" >> "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
# Create a new tmux session in detached mode
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"; exec bash"
|
||||
elif [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Running Goose in the background..."
|
||||
|
||||
# Check if screen is installed
|
||||
if ! command_exists screen; then
|
||||
echo "Error: screen is not installed. Please install screen manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.goose.log"
|
||||
|
||||
# Ensure the screenrc exists
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.goose.log"
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.goose.log"
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.goose.log"
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
screen -U -dmS ${var.session_name} bash -c "
|
||||
cd ${var.folder}
|
||||
\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"
|
||||
/bin/bash
|
||||
"
|
||||
fi
|
||||
EOT
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "goose" {
|
||||
slug = "goose"
|
||||
display_name = "Goose"
|
||||
agent_id = var.agent_id
|
||||
command = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Determine goose command
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t ${var.session_name} 2>/dev/null; then
|
||||
echo "Attaching to existing Goose tmux session." | tee -a "$HOME/.goose.log"
|
||||
tmux attach-session -t ${var.session_name}
|
||||
else
|
||||
echo "Starting a new Goose tmux session." | tee -a "$HOME/.goose.log"
|
||||
tmux new-session -s ${var.session_name} -c ${var.folder} "\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"; exec bash"
|
||||
fi
|
||||
elif [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
# Check if session exists first
|
||||
if ! screen -list | grep -q "${var.session_name}"; then
|
||||
echo "Error: No existing Goose session found. Please wait for the script to start it."
|
||||
exit 1
|
||||
fi
|
||||
# Only attach to existing session
|
||||
screen -xRR ${var.session_name}
|
||||
else
|
||||
cd ${var.folder}
|
||||
"$GOOSE_CMD" run --text "Review goosehints. Your task: $GOOSE_TASK_PROMPT" --interactive
|
||||
fi
|
||||
EOT
|
||||
icon = var.icon
|
||||
order = var.order
|
||||
group = var.group
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
set -o nounset
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "provider: $ARG_PROVIDER"
|
||||
echo "model: $ARG_MODEL"
|
||||
echo "goose_config: $ARG_GOOSE_CONFIG"
|
||||
echo "install: $ARG_INSTALL"
|
||||
echo "goose_version: $ARG_GOOSE_VERSION"
|
||||
echo "--------------------------------"
|
||||
|
||||
set +o nounset
|
||||
|
||||
if [ "${ARG_INSTALL}" = "true" ]; then
|
||||
echo "Installing Goose..."
|
||||
parsed_version="${ARG_GOOSE_VERSION}"
|
||||
if [ "${ARG_GOOSE_VERSION}" = "stable" ]; then
|
||||
parsed_version=""
|
||||
fi
|
||||
curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | GOOSE_VERSION="${parsed_version}" CONFIGURE=false bash
|
||||
echo "Goose installed"
|
||||
else
|
||||
echo "Skipping Goose installation"
|
||||
fi
|
||||
|
||||
if [ "${ARG_GOOSE_CONFIG}" != "" ]; then
|
||||
echo "Configuring Goose..."
|
||||
mkdir -p "$HOME/.config/goose"
|
||||
echo "GOOSE_PROVIDER: $ARG_PROVIDER" >"$HOME/.config/goose/config.yaml"
|
||||
echo "GOOSE_MODEL: $ARG_MODEL" >>"$HOME/.config/goose/config.yaml"
|
||||
echo "$ARG_GOOSE_CONFIG" >>"$HOME/.config/goose/config.yaml"
|
||||
else
|
||||
echo "Skipping Goose configuration"
|
||||
fi
|
||||
|
||||
if [ "${GOOSE_SYSTEM_PROMPT}" != "" ]; then
|
||||
echo "Setting Goose system prompt..."
|
||||
mkdir -p "$HOME/.config/goose"
|
||||
echo "$GOOSE_SYSTEM_PROMPT" >"$HOME/.config/goose/.goosehints"
|
||||
else
|
||||
echo "Goose system prompt not set. use the GOOSE_SYSTEM_PROMPT environment variable to set it."
|
||||
fi
|
||||
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# this must be kept up to date with main.tf
|
||||
MODULE_DIR="$HOME/.goose-module"
|
||||
mkdir -p "$MODULE_DIR"
|
||||
|
||||
if [ ! -z "$GOOSE_TASK_PROMPT" ]; then
|
||||
echo "Starting with a prompt"
|
||||
PROMPT="Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT"
|
||||
PROMPT_FILE="$MODULE_DIR/prompt.txt"
|
||||
echo -n "$PROMPT" >"$PROMPT_FILE"
|
||||
GOOSE_ARGS=(run --interactive --instructions "$PROMPT_FILE")
|
||||
else
|
||||
echo "Starting without a prompt"
|
||||
GOOSE_ARGS=()
|
||||
fi
|
||||
|
||||
agentapi server --term-width 67 --term-height 1190 -- \
|
||||
bash -c "$(printf '%q ' "$GOOSE_CMD" "${GOOSE_ARGS[@]}")"
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const http = require("http");
|
||||
const args = process.argv.slice(2);
|
||||
console.log(args);
|
||||
const port = 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,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
while true; do
|
||||
echo "$(date) - goose-mock"
|
||||
sleep 15
|
||||
done
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
display_name: JetBrains Fleet
|
||||
description: Add a one-click button to launch JetBrains Fleet to connect to your workspace.
|
||||
icon: ../../../../.icons/fleet.svg
|
||||
verified: true
|
||||
tags: [ide, jetbrains, fleet]
|
||||
---
|
||||
|
||||
# Jetbrains Fleet
|
||||
|
||||
This module adds a Jetbrains Fleet button to your Coder workspace that opens the workspace in JetBrains Fleet using SSH remote development.
|
||||
|
||||
JetBrains Fleet is a next-generation IDE that supports collaborative development and distributed architectures. It connects to your Coder workspace via SSH, providing a seamless remote development experience.
|
||||
|
||||
```tf
|
||||
module "jetbrains_fleet" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-fleet/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- JetBrains Fleet must be installed locally on your development machine
|
||||
- Download Fleet from: https://www.jetbrains.com/fleet/
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Fleet needs you to either have Coder CLI installed with `coder config-ssh` run or [Coder Desktop](https://coder.com/docs/user-guides/desktop).
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic usage
|
||||
|
||||
```tf
|
||||
module "jetbrains_fleet" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-fleet/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
### Open a specific folder
|
||||
|
||||
```tf
|
||||
module "jetbrains_fleet" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-fleet/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
### Customize app name and grouping
|
||||
|
||||
```tf
|
||||
module "jetbrains_fleet" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-fleet/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
display_name = "Fleet"
|
||||
group = "JetBrains IDEs"
|
||||
order = 1
|
||||
}
|
||||
```
|
||||
|
||||
### With custom agent name
|
||||
|
||||
```tf
|
||||
module "jetbrains_fleet" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-fleet/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = coder_agent.example.name
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("jetbrains-fleet", 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.fleet_url.value).toBe(
|
||||
"fleet://fleet.ssh/default.coder",
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "fleet",
|
||||
);
|
||||
|
||||
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.fleet_url.value).toBe(
|
||||
"fleet://fleet.ssh/default.coder?pwd=/foo/bar",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds agent_name to hostname", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
agent_name: "myagent",
|
||||
});
|
||||
expect(state.outputs.fleet_url.value).toBe(
|
||||
"fleet://fleet.ssh/myagent.default.default.coder",
|
||||
);
|
||||
});
|
||||
|
||||
it("custom display name and slug", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
display_name: "My Fleet",
|
||||
slug: "my-fleet",
|
||||
});
|
||||
expect(state.outputs.fleet_url.value).toBe(
|
||||
"fleet://fleet.ssh/default.coder",
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "fleet",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances[0].attributes.display_name).toBe("My Fleet");
|
||||
expect(coder_app?.instances[0].attributes.slug).toBe("my-fleet");
|
||||
});
|
||||
|
||||
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 === "fleet",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
|
||||
it("expect group to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
group: "JetBrains IDEs",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "fleet",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.group).toBe("JetBrains IDEs");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
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 "agent_name" {
|
||||
type = string
|
||||
description = "The name of the agent"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in Fleet IDE."
|
||||
default = ""
|
||||
}
|
||||
|
||||
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 "slug" {
|
||||
type = string
|
||||
description = "The slug of the app."
|
||||
default = "fleet"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app."
|
||||
default = "JetBrains Fleet"
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
locals {
|
||||
workspace_name = lower(data.coder_workspace.me.name)
|
||||
owner_name = lower(data.coder_workspace_owner.me.name)
|
||||
agent_name = lower(var.agent_name)
|
||||
hostname = var.agent_name != "" ? "${local.agent_name}.${local.workspace_name}.${local.owner_name}.coder" : "${local.workspace_name}.coder"
|
||||
}
|
||||
|
||||
resource "coder_app" "fleet" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/fleet.svg"
|
||||
slug = var.slug
|
||||
display_name = var.display_name
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = join("", [
|
||||
"fleet://fleet.ssh/",
|
||||
local.hostname,
|
||||
var.folder != "" ? join("", ["?pwd=", var.folder]) : ""
|
||||
])
|
||||
}
|
||||
|
||||
output "fleet_url" {
|
||||
value = coder_app.fleet.url
|
||||
description = "Fleet IDE connection URL."
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
> [!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/)
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||

|
||||

|
||||
|
||||
<!-- TODO: Add screenshot -->
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ tags: [vm, linux, gcp, devcontainer]
|
||||
|
||||
# Remote Development in a Devcontainer on Google Compute Engine
|
||||
|
||||

|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||