Compare commits

..

69 Commits

Author SHA1 Message Date
Riajul Islam da5a2ba6a8 feat(git-clone module): added post_clone_script. (#357)
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-10-15 12:53:17 +00:00
Anas 63cad25954 fix(amazon-q): pass workdir variable into agentapi folder variable (#478)
Co-authored-by: DevCats <christofer@coder.com>
2025-10-15 17:44:58 +05:00
Hulto cd759bd9a1 goose module: pass folder along to agentapi (#412)
Co-authored-by: DevCats <christofer@coder.com>
2025-10-15 17:44:18 +05:00
DevCats 54a7bb0001 docs: add usage examples for bedrock and vertex (#431)
Closes #

## Description

Adds Usage Examples for Vertex and Bedrock as described in the linked
documentation.

## Type of Change

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

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v3.0.1`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

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

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-10-14 12:06:32 -05:00
Matt Hazinski 50f4d5388b fix(codex): pass folder variable to agentapi module (#477)
## Description

The folder variable was not being passed from the codex module to the
agentapi module, causing agentapi to use its default value of
`/home/coder` instead of the user-specified folder path.

This resulted in permission errors when the codex module tried to create
directories in `/home/coder` when users specified a different folder
like `/home/matt/foo`.

Fix by adding `folder = var.folder` to the agentapi module invocation.

## Type of Change

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

## Module Information

Path: registry/coder-labs/modules/codex
New version: v2.1.1
Breaking change: [ ] Yes [X] No

## Testing & Validation

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

## Related Issues
Fixes https://github.com/coder/registry/issues/476

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DevelopmentCats <christofer@coder.com>
2025-10-14 16:49:52 +00:00
dependabot[bot] 36943d1dfb chore(deps): bump crate-ci/typos from 1.37.2 to 1.38.1 in the github-actions group (#475)
Bumps the github-actions group with 1 update:
[crate-ci/typos](https://github.com/crate-ci/typos).

Updates `crate-ci/typos` from 1.37.2 to 1.38.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/crate-ci/typos/releases">crate-ci/typos's
releases</a>.</em></p>
<blockquote>
<h2>v1.38.1</h2>
<h2>[1.38.1] - 2025-10-07</h2>
<h3>Fixes</h3>
<ul>
<li>Ignore common golang identifiers</li>
</ul>
<h2>v1.38.0</h2>
<h2>[1.38.0] - 2025-10-06</h2>
<h3>Features</h3>
<ul>
<li>Update type list</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Don't correct <code>typ</code></li>
<li>Consistently error on unused config fields</li>
</ul>
<h2>v1.37.3</h2>
<h2>[1.37.3] - 2025-10-06</h2>
<h3>Fixes</h3>
<ul>
<li>Don't correct <code>PN</code> for <code>bitbake</code> file
types</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/crate-ci/typos/blob/master/CHANGELOG.md">crate-ci/typos's
changelog</a>.</em></p>
<blockquote>
<h2>[1.38.1] - 2025-10-07</h2>
<h3>Fixes</h3>
<ul>
<li>Ignore common golang identifiers</li>
</ul>
<h2>[1.38.0] - 2025-10-06</h2>
<h3>Features</h3>
<ul>
<li>Update type list</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Don't correct <code>typ</code></li>
<li>Consistently error on unused config fields</li>
</ul>
<h2>[1.37.3] - 2025-10-06</h2>
<h3>Fixes</h3>
<ul>
<li>Don't correct <code>PN</code> for <code>bitbake</code> file
types</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/crate-ci/typos/commit/80c8a4945eec0f6d464eaf9e65ed98ef085283d1"><code>80c8a49</code></a>
chore: Release</li>
<li><a
href="https://github.com/crate-ci/typos/commit/c1008ce1b695c69fa611c3a79c32852be029709a"><code>c1008ce</code></a>
docs: Update changelog</li>
<li><a
href="https://github.com/crate-ci/typos/commit/62a3b5083afa59e8054f76ff5dbb94bb676ce5e4"><code>62a3b50</code></a>
Merge pull request <a
href="https://redirect.github.com/crate-ci/typos/issues/1398">#1398</a>
from ccoveille-forks/go-exclusions</li>
<li><a
href="https://github.com/crate-ci/typos/commit/e6bedbde77058052de3f00d82a67284618385615"><code>e6bedbd</code></a>
fix(config): Add some Go exclusions</li>
<li><a
href="https://github.com/crate-ci/typos/commit/90cacd60e824aaf9adff4afa0d6582f52631bc6d"><code>90cacd6</code></a>
docs(ref): Speak to glob ambiguity</li>
<li><a
href="https://github.com/crate-ci/typos/commit/b81b12ea1b8702b57e1a917e5a7bfc26d46c21e9"><code>b81b12e</code></a>
docs(ref): Clarify directories are not spell checked</li>
<li><a
href="https://github.com/crate-ci/typos/commit/eaf25df9941e5b6a2f145729a76b06af9eab44ca"><code>eaf25df</code></a>
docs(ref): Speak to locale's behavior</li>
<li><a
href="https://github.com/crate-ci/typos/commit/a9735e2e141b9a8f08340e41aac57b52805ae185"><code>a9735e2</code></a>
docs(ref): Provide identifier/word config examples</li>
<li><a
href="https://github.com/crate-ci/typos/commit/3c14191fcc71bad3e87e231c86a6d3a7876ae8a2"><code>3c14191</code></a>
docs(ref): Talk about include lists</li>
<li><a
href="https://github.com/crate-ci/typos/commit/d0f81dc972d4fc9862590daafb26ee03a2dbfda5"><code>d0f81dc</code></a>
docs(ref): Re-organize help more like cargo</li>
<li>Additional commits viewable in <a
href="https://github.com/crate-ci/typos/compare/v1.37.2...v1.38.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=crate-ci/typos&package-manager=github_actions&previous-version=1.37.2&new-version=1.38.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 07:40:01 -05:00
greg-the-coder e7d705bf98 Fixes from AWS Workshop testing (#428)
Closes #

## Description

Changes to code-server and jetbrains modules that were not caught during
initial unit-testing, that appear to be related to older versions of the
modules or recent changes.

## Type of Change

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

## Testing & Validation

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

## Related Issues

None

---------

Co-authored-by: DevelopmentCats <christofer@coder.com>
2025-10-10 06:27:18 +05:00
Atif Ali 898219b16b Enhance PR template with template information section (#474) 2025-10-09 15:50:38 +00:00
chgl fc071e0930 refactor: refactored get_http_dir (#360)
Closes #

## Description

<!-- Briefly describe what this PR does and why -->

I just couldn't get the script to execute properly in its current form.
I saw e.g.

```console
[[: 1989{#d[@]}: syntax error: invalid arithmetic operator (error token is "{#d[@]}")
```

when trying to run the script locally. (GNU bash, version
5.2.21(1)-release (x86_64-pc-linux-gnu)).

This uses a likely simpler bash script, but requires both grep and awk.

## Type of Change

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

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/kasmvnc`  
**New version:** `v1.2.3`  
**Breaking change:** [ ] Yes [x] 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 -->

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Atif Ali <atif@coder.com>
Co-authored-by: DevCats <christofer@coder.com>
2025-10-09 09:36:14 -05:00
DevCats d516aff908 chore: set verified to false and bump to 1.0.1 (#473)
## Description

Removes verified status from nexus module.
<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/mavrickrishi/modules/nexus-repository`  
**New version:** `v1.0.1`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally
2025-10-09 08:13:06 -05:00
DevCats ccdca6daf5 chore: update CONTRIBUTION docs to explain both tests, and update CI for both tests (#384)
Closes #383 

## Description

- Update CONTRIBUTION.md to elaborate on ts and tf tests
- Add ./scripts/terraform_test_all.sh to CI for ts tests

<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Testing & Validation

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

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-10-09 12:42:07 +00:00
Rishi Mondal ce039f64df Add Sonatype Nexus repository integration module (#262)
# Add Sonatype Nexus Repository Integration Module

## Summary
Implements a Coder module for Sonatype Nexus Repository Manager
integration that automatically configures Maven, npm, PyPI, and Docker
registries for development workspaces.

## Demo Video & Screenshots

https://github.com/user-attachments/assets/2c51f229-d34d-483b-a0e9-f4e0d79332c2

![Nexus Repository
Integration](https://github.com/user-attachments/assets/1a778a8f-0e48-40f2-ae0f-5b8d5d5ce849)

## Features
-  **Maven Support**: Automatic `settings.xml` configuration
-  **npm Support**: Automatic `.npmrc` configuration with scoped
packages
-  **PyPI Support**: Automatic `pip.conf` configuration
-  **Docker Support**: Registry authentication setup
-  **Flexible Configuration**: Support for multiple repositories per
package manager
-  **Secure Credentials**: API token and password support
-  **Username Options**: Configurable username field (username or
email)

## Nexus Repository Manager Requirements

### Version Requirements
**Yes, this module requires Nexus Repository Manager Pro version** for
full functionality, though basic features work with the Community
Edition (OSS).

### Supported Authentication Methods
This module supports **4 authentication methods**:

1. **User Token Authentication** (Recommended - Pro only)
   - Enhanced security with two-part tokens
   - Ideal for CI/CD and automated environments
   - Requires `nx-usertoken-current` privilege

2. **API Token Authentication** (Pro only)
   - Single-use access tokens via REST API
   - Programmatic token generation and management

3. **Basic Authentication** (OSS & Pro)
   - Standard HTTP Basic Auth with username/password
   - Works with both OSS and Pro versions

4. **Base64 Encoded Credentials** (OSS & Pro)  
   - Base64 encoded `username:password` format
   - Compatible with npm and other package managers

### Testing Instructions

#### Prerequisites
- Nexus Repository Manager instance (OSS or Pro)
- Admin access to configure repositories
- Test repositories for each package manager you want to test

#### Setup Test Environment
1. **Create Test Repositories** in your Nexus instance:
   - Maven: `maven-public`, `maven-releases` 
   - npm: `npm-public`, `@company:npm-private`
   - PyPI: `pypi-public`, `pypi-private`
   - Docker: `docker-public`, `docker-private`

2. **Configure Authentication**:
   - For Pro: Generate user tokens via UI (User menu → User Token)
   - For OSS: Use username/password or base64 encoded credentials
   - Set up appropriate permissions for test repositories

3. **Test the Module**:
   ```hcl
   module "nexus" {
     source         = "registry.coder.com/mavrickrishi/nexus/coder"
     version        = "1.0.0"
     agent_id       = coder_agent.main.id
     nexus_url      = "https://your-nexus-instance.com"
     nexus_password = var.nexus_api_token  # or password
     package_managers = {
       maven  = ["maven-public", "maven-releases"]
       npm    = ["npm-public", "@company:npm-private"]
       pypi   = ["pypi-public", "pypi-private"]
       docker = ["docker-public", "docker-private"]
     }
   }
   ```

4. **Verify Configuration**:
   - Check generated config files in workspace
   - Test package installation from configured repositories
   - Verify authentication works for each package manager

#### EC2 Deployment Testing
Tested by deploying on EC2 instance with:
- Ubuntu 22.04 LTS
- Nexus Repository Manager Pro
- All package managers (Maven, npm, PyPI, Docker)
- Both token and basic authentication methods

## Usage Example
```hcl
module "nexus" {
  source         = "registry.coder.com/mavrickrishi/nexus/coder"
  version        = "1.0.0"
  agent_id       = coder_agent.main.id
  nexus_url      = "https://nexus.company.com"
  nexus_password = var.nexus_api_token
  package_managers = {
    maven  = ["maven-public", "maven-releases"]
    npm    = ["npm-public", "@company:npm-private"]
    pypi   = ["pypi-public", "pypi-private"]
    docker = ["docker-public", "docker-private"]
  }
}
```

## Testing
-  11 comprehensive tests covering all functionality
-  Variable validation tests
-  Package manager configuration tests
-  Error handling tests
-  All tests passing
-  EC2 deployment tested

## Files Added
- `registry/mavrickrishi/modules/nexus/main.tf` - Main module
configuration
- `registry/mavrickrishi/modules/nexus/README.md` - Complete
documentation
- `registry/mavrickrishi/modules/nexus/main.test.ts` - Test suite

## Checklist
- [x] Module follows existing patterns and conventions
- [x] Comprehensive test coverage (11 tests)
- [x] Complete documentation with examples
- [x] Input validation and error handling
- [x] Secure credential handling
- [x] All tests passing
- [x] Demo video included
- [x] Screenshots added
- [x] Testing instructions provided
- [x] Authentication methods documented
- [x] EC2 deployment tested

Closes #202
/claim #202

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Atif Ali <me@matifali.dev>
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: DevCats <chris@dualriver.com>
2025-10-09 07:31:43 -05:00
DevCats 8acda84dd7 chore: update icons for auto-start-dev-server module (#471)
## Description

Adds icons for module, and update all refrences.

PR for Site Icon's Addition: https://github.com/coder/coder/pull/20219

## Type of Change

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

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/mavrickrishi/modules/auto-start-dev-server`  
**New version:** `v1.0.1`  
**Breaking change:** [ ] Yes [ ] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally
2025-10-08 13:20:45 -05:00
Atif Ali 76c1299968 docs: upgrade alert style to a GFM style tip for JetBrains Gateway (#468) 2025-10-08 06:39:54 +00:00
Jullian Pepito 60372ff797 fix(git-clone): Update README.md (#448)
Changes `coder_git_auth` to `coder_external_auth` in README

## Description

<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/git-clone` 
**New version:** `v1.1.2`  
**Breaking change:** [ ] Yes [X] 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 -->

---------

Co-authored-by: Jullian Pepito <jullian@MacBook-Pro.local>
Co-authored-by: DevCats <christofer@coder.com>
2025-10-07 15:35:02 -05:00
Rishi Mondal f28bcdb713 Auto-Start Development Servers Module (#316)
# Auto-Start Development Servers Module

## Summary

/claim #204

Implements automatic detection and startup of development servers based
on project detection as requested in #204.

-  **Multi-language support**: Node.js, Rails, Django, Flask, Spring
Boot, Go, PHP, Rust, .NET
-  **Background execution**: Servers start automatically without user
intervention
-  **Devcontainer.json integration**: Uses custom start commands when
available
-  **Smart fallback**: Creates sample project when no existing projects
found
-  **Comprehensive logging**: Full activity logs for troubleshooting



https://github.com/user-attachments/assets/2eddf67c-3ac1-4e55-a5ba-79292d61e918



## Addresses GitHub Issue

Closes #204 - "Auto-start development servers based on project
detection"

---------

Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: DevCats <chris@dualriver.com>
2025-10-07 14:44:00 -05:00
romracer cb553209a5 fix: update CLI icon for copilot module to same icon as web app (#469)
## Description

Sets `cli_app_icon` in agentapi to the same icon used for
`web_app_icon`. Its currently using the default of Claude.

## Type of Change

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

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder-labs/modules/copilot`  
**New version:** `v0.2.1`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

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

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-10-07 13:54:14 -05:00
35C4n0r 5d0504aef9 feat: update agentapi_version to 0.10.0 (#456)
Closes #

## Description

<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/[namespace]/modules/[module-name]`  
**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 -->
2025-10-07 13:27:12 -05:00
35C4n0r c1c0dec90f chore: bump agentapi module version (#465) 2025-10-07 18:09:44 +00:00
DevCats 59b67c2c98 chore: update display name for copilot module to Copilot CLI (#467)
## Description

update display name for copilot module to Copilot CLI

## Type of Change

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

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder-labs/modules/copilot`  
**New version:** `v0.1.2`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally
2025-10-07 17:40:23 +00:00
DevCats 7abe422e0a fix: Add COPILOT_MODEL to install script args (#464)
Closes #462

## Description

<!-- Briefly describe what this PR does and why -->
Fixes missing COPILOT_MODEL arg from install script

## Type of Change

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

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder-labs/modules/copilot`  
**New version:** `v0.1.1`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally
2025-10-07 12:05:50 -05:00
Susana Ferreira db8217e4e5 fix(claude-code): update inner system prompt to include summary rules (#461)
## Description

Update `report_tasks_system_prompt` to include `coder_report_task`
summary rules.

## Type of Change

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

## Module Information

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v3.0.3`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

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

## Related Issues

Follow-up from: https://github.com/coder/registry/pull/443
Related to: https://github.com/coder/coder/pull/20191/files#r2410441026
2025-10-07 15:26:09 +01:00
DevCats f75afeb0c8 feat: New Copilot-CLI Module (#441)
## Description

New Copilot-CLI Module using AgentAPI

Need to test once AgentAPI Changes are pushed.

## Type of Change

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

## Module Information

**Path:** `registry/coder-labs/modules/copilot-cli`  
**New version:** `v0.1.0`  
**Breaking change:** [ ] Yes [ ] No

## Testing & Validation

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

## Related Issues

<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-10-07 07:47:02 -05:00
Susana Ferreira 182e5548e2 chore: update MAINTAINER.md to check PR version label (#460)
## Description

Update MAINTAINER.md to include a check of the version label on the PRs

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [x] Documentation
- [ ] Other
2025-10-07 10:47:41 +01:00
Susana Ferreira d057a820c1 feat(claude-code): add coder-specific prompt to system_prompt (#443)
## Description

This PR updates the `claude-code` module to automatically include the
Coder task-reporting system prompt whenever `report_tasks = true`, and
to wrap the final system prompt in `<system>…</system>` when non-empty.

Previously, users needed to manually include this content in their
system prompts to enable proper task reporting. When `report_tasks =
true`, the system prompt is prepended with the Coder task-reporting, and
any user `system_prompt` (if provided) is appended after it, ensuring
consistent integration without manual copy/paste.

When `report_tasks = false`, the module includes only the user
`system_prompt` (if any). If both `report_tasks = false` and
`system_prompt` is empty, the system prompt sent to Claude is empty.

## Type of Change

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

## Module Information

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v3.0.2` 
**Breaking change:** [] Yes [x]  No

## Testing & Validation

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

Related to internal slack thread:
https://codercom.slack.com/archives/C0992H8HGCS/p1759317555713269

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-10-07 10:09:49 +01:00
Satbir Chahal b4e9545c35 fix(claude-code): source bashrc file only if it exists (#459) 2025-10-07 07:33:17 +00:00
DevCats 50ac3b31f6 docs: add MAINTAINER.md link to CONTRIBUTING.md and README.md (#453)
## Description

<!-- Briefly describe what this PR does and why -->
Add links to `MAINTAINER.md` in `README.md` and `CONTRIBUTING.md` to
help guide internal contributors.

## Type of Change

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

## Testing & Validation

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

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-10-06 12:52:58 -05:00
dependabot[bot] 056937a758 chore(deps): bump crate-ci/typos from 1.36.3 to 1.37.2 in the github-actions group (#451)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: M Atif Ali <atif@coder.com>
2025-10-06 07:59:23 -05:00
Rowan Smith af8b4f02fd chore: fix for jetbrains gateway agent_id issue (#437)
## Description

Fixes a regression added in #167 which implemented support for multiple
agents by appending the agent id to the URI, however in a single agent
environment it results in the agent id from the template apply (on
upload to Coder from client) being injected, and when a workspace is
later built using the template the agent id is no longer correct.

Resolves the error `The workspace “<name>” does not have an agent with
ID “<id>”` being thrown by Jetbrains Gateway app upon attempting to open
a Jetbrains app from within a Coder workspace.

When wishing to target a specific Coder Agent with the Jetbrains Gateway
module one should use the `agent_name` variable in the module
configuration to specify the desired agent name. This will append the
agent name to the URI.

## Type of Change

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

## Module Information

**Path:** `registry/coder/modules/jetbrains-gateway`  
**New version:** `v1.2.4`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

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

## Related Issues

Reported by customer on Zendesk ticket 4391
2025-10-06 08:29:33 +11:00
Susana Ferreira 2de6a57a3f fix: claude-code api_key terraform test (#444)
## Description

Fix claude-code module `test_claude_code_with_api_key` terraform test.
2025-10-01 18:21:54 -05:00
Jiachen Jiang 60fec19d7d Update README.md (#440)
Added recommendation to the Gateway README, pointing to the Toolbox
module.

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-09-30 09:14:16 -07:00
Atif Ali 44354b202d Fix claude-code module not passing workdir to agentapi (#439)
## Summary

Fixes #436 - The claude-code 3.0.0 module was not passing the custom
`workdir` variable to the agentapi module, causing it to default to
`/home/coder` instead of using the specified working directory.

## Changes

- Added missing `folder = local.workdir` parameter to the agentapi
module call in `main.tf:247`
- This ensures that custom working directories are properly propagated
to the agentapi module

## Test Plan

- [x] Terraform validation passes
- [x] Code formatting applied with `bun run fmt`
- [x] Basic terraform test passes (one pre-existing test failure
unrelated to this change)

## Verification

The fix adds the missing parameter that was identified in the issue:
```terraform
module "agentapi" {
  # ... other parameters
  folder = local.workdir  # <- Added this line
  # ... rest of configuration
}
```

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-09-30 08:02:35 -05:00
dependabot[bot] 80acbd7e3a chore(deps): bump crate-ci/typos from 1.36.2 to 1.36.3 in the github-actions group (#438)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 12:53:03 +00:00
DevCats 80f429faf1 chore: remove it wrappers from required variables tests (#442)
## Description

<!-- Briefly describe what this PR does and why -->
Remove it wrappers from required variables tf test in jfrog-oauth and
jfrog-token modules. This solves the failing tf tests that we were
encountering in all PR's across the board.

## Type of Change

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

## Testing & Validation

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

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-09-30 07:44:41 -05:00
Benraouane Soufiane e516446d03 Add Rustdesk module (#266)
Closes #79

## Description
This PR add new module, install minimal desktop environment (xfce),
virtual display, ,rustdesk package from deb file, init new screen,
export DISPLAY environment variable with last created virtual screen,
start new xfce session & execute the rustdesk cli, generate new
password, change the default password, then log the ID & password to be
used within rustdesk client to connect to the host

## Type of Change

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

## Module Information
Overview/test video: live demo that launch rustdesk with GUI in a docker
container https://youtu.be/_rR-l7nARN4
Screenshots: 
<img width="1920" height="1080" alt="image"
src="https://github.com/user-attachments/assets/ba67a864-4295-471e-8b6a-976c23cb8f55"
/>
<img width="1920" height="1080" alt="image"
src="https://github.com/user-attachments/assets/24686339-aba7-47fe-92b4-5700ef5b154a"
/>
<img width="1920" height="1080" alt="image"
src="https://github.com/user-attachments/assets/21884c31-9eed-45ef-b3de-c12c99f2aa96"
/>
<img width="1920" height="1080" alt="image"
src="https://github.com/user-attachments/assets/ec0c65fe-61be-404c-ba36-8cc2882e85a2"
/>







**Path:** `registry/BenraouaneSoufiane/modules/rustdesk`  
**New version:** `v1.0.0`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

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

## Related Issues

/claim #79 (remain asset 150$)

---------

Co-authored-by: root <root@DESKTOP-6QN3GRE.localdomain>
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-22 20:04:24 -05:00
Rafael Rodriguez f0045397d4 feat: add tooltip support to jetbrains module (#421)
## Description

In this pull request we're updating the JetBrains module to support the
tooltip field added as requested in
https://github.com/coder/coder/pull/19781#pullrequestreview-3214217375

## Type of Change

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

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/jetbrains`  
**New version:** `v1.1.0`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

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

## Related Issues

https://github.com/coder/coder/issues/18431

---------

Co-authored-by: Benjamin Peinhardt <61021968+bcpeinhardt@users.noreply.github.com>
2025-09-22 13:29:12 -05:00
DevCats 6af8508bc0 chore: update tasks template for claude-code update (#423)
## Description

Refactor template for claude-code module update for tasks

## Type of Change

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

## Testing & Validation

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

## Related Issues

https://github.com/coder/registry/pull/402

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-09-19 14:51:37 -05:00
35C4n0r d212de47ed feat: refactor claude code to use agentapi module (#402)
Closes #302

## Description

<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/[namespace]/modules/[module-name]`  
**New version:** `v3.0.0`  
**Breaking change:** [ ] Yes [ ] No

## Testing & Validation

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

## Related Issues

<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Atif Ali <me@matifali.dev>
2025-09-18 10:04:52 -05:00
Marcin Tojek 54b9bf3038 add: nextflow module (#416) 2025-09-18 09:30:48 +00:00
Cian Johnston cb990bbee0 fix(registry/modules/claude-code): default subdomain to false (#419)
Relates to https://github.com/coder/coder/issues/18779

By default, we set `subdomain = true`. Most folks testing this out don't
have a wildcard subdomain setup. This switches to path-based behaviour
by default and adds a note to the troubleshooting section.
2025-09-15 13:09:12 +01:00
Cian Johnston 213aabb3b0 fix(registry/modules/goose): default subdomain to false (#420)
Relates to https://github.com/coder/coder/issues/18779

See also https://github.com/coder/registry/pull/419

By default, we set subdomain = true. Most folks testing this out don't
have a wildcard subdomain setup. This switches to path-based behaviour
by default and adds a note to the troubleshooting section.
2025-09-15 09:00:07 +01:00
david-fraley 2937286712 feat: update Tasks on Docker Template (#418) 2025-09-11 21:53:22 +00:00
Atif Ali 8d556a8ab7 chore: update KasmVNC README note (#417) 2025-09-11 15:46:44 +05:00
Michael Orlov 16015559e2 feat: refactor Amazon Q module to use agentAPI (#362)
### **Title:**
feat: complete amazon-q module v2.0.0 with comprehensive enhancements


### **Description:**
Closes #240

This PR introduces a complete rewrite and enhancement of the amazon-q
module, bringing it to version 2.0.0. The module now provides AgentAPI
support.

## Type of Change

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

## Module Information

**Path:** `registry/coder/modules/amazon-q`
**New version:** `v2.0.0`
**Breaking change:** [x] Yes [ ] No

## Key Features & Enhancements

### 🚀 Core Functionality
- **AgentAPI Support**: Web and CLI app integration with health checks
- **Amazon Q CLI Integration**: Automatic installation and configuration
of Amazon Q CLI
- **MCP Integration**: Model Context Protocol support for task reporting
to Coder
- **Authentication System**: Tarball-based authentication with
environment variable management

### 🛠️ Customization & Configuration
- **Pre/Post Install Scripts**: Support for custom setup and
finalization scripts
- **Agent Configuration**: Templated agent config with tool and resource
management
- **Custom System Prompts**: Configurable AI behavior and task reporting
instructions
- **Version Pinning**: Support for specific Amazon Q CLI and AgentAPI
versions

### 📚 Documentation & Testing
- **Comprehensive README**: Complete user guide with examples,
configuration details, and troubleshooting
- **Visual Documentation**: Updated screenshots and interface examples
- **Terraform Testing**: Complete .tftest.hcl with 8 test cases (all
passing)
- **Registry Compliance**: Full adherence to Coder Registry contributing
guidelines

d## Breaking Changes

This is a major version update (v2.0.0) with breaking changes:
- Renamed variables names (Removed experimantal_ prefix)
- Updated AgentAPI integration method
- Modified default configuration structure

## Testing & Validation

- [x] Tests pass (`terraform test` - 8/8 tests passing)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally
- [x] Registry compliance verified
- [x] Documentation reviewed and updated

## Related Issues

Closes #240 - Amazon Q module enhancement request

## Additional Notes

- Module is now production-ready with professional quality code and
documentation
- Full compliance with Coder Registry contributing guidelines
- Comprehensive test coverage ensures reliability
- Ready for registry submission and community use

## Screenshots:
<img width="3001" height="1068" alt="image"
src="https://github.com/user-attachments/assets/24453cb3-d4dc-4a45-bb62-7a834940ebae"
/>
<img width="1209" height="600" alt="image"
src="https://github.com/user-attachments/assets/f2b18c42-ba7f-4e16-a9e7-d51ad1095712"
/>
<img width="1505" height="1251" alt="image"
src="https://github.com/user-attachments/assets/3e6e49b1-808d-482e-a237-b606e50262f5"
/>


https://github.com/user-attachments/assets/6533dead-35f1-47f5-875a-3cebb81453c9



https://github.com/user-attachments/assets/da8047f6-7023-4e6c-af90-138541298089

/claim #240

Co-authored-by: Michael Orlov <michaelo@amdocs.com>
2025-09-10 18:50:22 -05:00
हिमांशु f1010ee7a6 Add maven package manager support to JFrog modules (#414)
Closes #33 
/claim #33 
## Description
Jfrog Modules doesn't support conda package manager, This PR adds
support of that



## Type of Change

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

## Testing & Validation

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

## Related Issues

#33 
## video


https://github.com/user-attachments/assets/61c33963-e1a7-43e2-b1cc-fdb747405cf5
2025-09-09 10:36:31 +05:00
dependabot[bot] 17734c073a chore(deps): bump the github-actions group across 1 directory with 5 updates (#415)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 16:54:57 +05:00
greg-the-coder 6813e0b5b8 Fix/kubernetes devcontainer template to require fewer updates on initial deployment (#386)
Co-authored-by: greg-the-coder <greg@coder.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-09-07 21:48:49 +05:30
Atif Ali 9e47369905 chore: mark AMP CLI agent as verified (#408) 2025-09-03 04:37:22 +00:00
हिमांशु d9d44ca338 fix: bump versions of jfrog-oauth and jfrog-token (#407)
Closes #

## Description

## Type of Change

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

## Testing & Validation

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

## Related Issues
Follow up of #375
2025-09-01 22:31:43 +05:00
हिमांशु 7152b85246 feat: add conda support to JFrog modules (#375) 2025-09-01 21:54:23 +05:00
Atif Ali 41c6bece3e fix: use correct source url (#404) 2025-09-01 12:56:04 +00:00
Marcin Tojek 9452763f7d add: rstudio module (#327) 2025-09-01 14:25:28 +02:00
m4rrypro 77328656ff feat: add linode vm template (#367)
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-31 17:07:54 +05:00
m4rrypro c4c484089f feat: add digitalocean region module (#355)
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-31 17:06:08 +05:00
blink-so[bot] 7e53098bea Update jetbrains-gateway module references to coder/jetbrains (#396)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-29 17:46:25 +05:00
Lucas Kaplan 901043bb01 chore: Fixed path to avatar (#400)
This PR fixes a path issue in the README of the AJ0070 profile. The
original avatar path had the wrong extension.

---------

Co-authored-by: Benjamin <benjaminpeinhardt@gmail.com>
2025-08-28 18:56:11 -04:00
Jash Ambaliya 35e64f2e4a feat(pgadmin): add new module for pgAdmin (#228)
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-28 22:32:27 +05:30
blink-so[bot] 65edb54e88 Add template scaffolding script and enhance module script (#395)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: matifali <104543375+matifali@users.noreply.github.com>
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-28 20:42:05 +05:00
blink-so[bot] c270edfdab docs: add Premium license warning for externally-managed-workspace template (#397)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: M Atif Ali <U04T3LN8ASU+AtifCoder@users.noreply.github.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-28 11:14:57 +00:00
Kacper Sawicki f712d1c55b feat: add template for externally managed workspaces to coder-labs (#343)
## Description

Add externally-managed-workspace template for connecting Coder
workspaces to externally provisioned compute resources

## Type of Change

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

## Testing & Validation

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

## Related Issues

https://github.com/coder/coder/issues/19091
2025-08-28 10:33:29 +00:00
Atif Ali bc383a32f3 chore: add AGENTS.md (#393)
## Summary
- Adds comprehensive AGENTS.md documentation for AI coding assistants
- Provides guidance on project structure, development commands, and
testing workflows
- Includes specific instructions for Terraform module development and
validation

## Test plan
- [ ] Validate document formatting and structure
- [ ] Verify all referenced commands work correctly
- [ ] Test that instructions align with existing project workflows

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DevelopmentCats <christofer@coder.com>
2025-08-27 21:02:24 -05:00
blink-so[bot] a9b015044f Update coder-login module to use coder_env resources (#389)
This PR updates the `coder-login` module to use `coder_env` resources
instead of shell scripts for better security, maintainability, and
native Terraform integration.

## Changes
- **Replaced `coder_script` with `coder_env` resources**: Uses native
Terraform provider resources instead of shell scripts
- **Removed `run.sh` script**: Eliminated the need for external shell
scripts
- **Environment variables**: Sets `CODER_SESSION_TOKEN` and `CODER_URL`
using `coder_env` resources
- **Added comprehensive tests**: Includes Terraform tests with mocked
data validation
- **Version bump**: Updated module version from `v1.0.31` to `v1.1.0`
(minor bump)

## Benefits
- **Native Terraform approach**: Uses the provider's built-in resources
instead of external scripts
- **Better security**: Environment variables are set directly by
Terraform without shell script interpolation
- **Improved maintainability**: Cleaner, more declarative configuration
- **Proper testing**: Comprehensive test coverage with mocked data
sources
- **Correct environment variables**: Uses `CODER_SESSION_TOKEN` and
`CODER_URL` as per coder CLI documentation

## Testing
- All Terraform tests pass successfully
- Module validates correctly with `terraform validate`
- Proper formatting verified with `terraform fmt`

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: Atif Ali <atif@coder.com>
Co-authored-by: Cian Johnston <public@cianjohnston.ie>
2025-08-26 21:33:00 -05:00
Michael Smith e94dfd2df6 fix: add validation for Github-Flavored Markdown Alerts (#394)
No issue to link – this was a problem we discovered while updating the
Registry website

## Description

This PR adds (very) basic validation for the GitHub Flavored Markdown
alerts that we allow contributors to add to their README files. The
errors that get generated should be correct, but the error messages
themselves aren't as helpful as they could be. I'm going to be handling
that in a separate PR, just so we can get this one in sooner.

### Changes made
- Added function for validating the core structure of all GFM alerts
- Updated existing README files that were failing the new validation
requirements

## Type of Change

- [ ] New module
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other
2025-08-26 20:52:22 -05:00
DevCats 9125a52f57 feat: add mcp config for kiro and windsurf modules (#391)
## Description

- Introduces mcp file creation via `coder_script` in kiro and windsurf
modules
- Add mcp variable to both modules
- Add slug and display_name variables to windsurf to match up with other
modules
- Add tests for testing mcp file creation for both kiro and windsurf

<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/kiro`  
**New version:** `v1.1.0`  
**Breaking change:** [ ] Yes [X] No

**Path:** `registry/coder/modules/windsurf`  
**New version:** `v1.2.0`  
**Breaking change:** [ ] Yes [X] No

**Path:** `registry/coder/modules/cursor`  
**New version:** `v1.3.2`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally
2025-08-26 20:48:15 -05:00
DevCats c8441fc593 feat(claude-code): add subdomain variable and logic (#387)
Closes #

## Description

- Introduces `subdomain` variable
- Logic for subdomain and base path

Tested with and without subdomain to ensure no breaking changes
<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v2.2.0`  
**Breaking change:** [X] Yes [ ] No

## Testing & Validation

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

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-08-26 20:36:27 -05:00
Atif Ali 62951f1fca chore: improve Prettier configuration (#392) 2025-08-27 01:57:43 +05:00
DevCats 6bebc02122 fix(ci): add fallback for GitHub API failures in release workflow (#388)
## Description

CI was failing on new module releases because there was no fallback to
gh api failures when there was no previous tag for the module was found.


https://github.com/coder/registry/actions/runs/17225186737/job/48868318539

<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [X] Other
2025-08-26 08:19:49 -05:00
Rishi Mondal 97b036e7d4 AWS AMI Snapshot Module for Persistent Workspace State (#219)
## Description

This PR implements AMI-based snapshots for Coder workspaces on AWS,
enabling persistent state across workspace stop/start cycles. Users can
now create snapshots of their workspace state when stopping and restore
from selected snapshots when starting workspaces.

**Solves GitHub Issue #26** - AWS Snapshot functionality for persistent
workspace state.

## Type of Change

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

## Module Information

**Path:** `registry/mavrickrishi/modules/aws-ami-snapshot`
**New version:** `v1.0.0`
**Breaking change:** [ ] Yes [x] No

## Implementation Details

### All Requirements from Issue #26 Implemented:

 **Requirement 1: Create AMI snapshots on workspace stop**
- Uses `aws_ami_from_instance` resource triggered by
`coder_workspace.me.transition == "stop"`
- Snapshots created without reboot for graceful handling

 **Requirement 2: Tag AMIs with workspace metadata**
- Tags include: workspace owner, name, template, creation timestamp
- Comprehensive tagging for organization and filtering

 **Requirement 3: User parameters for snapshot control**
- `enable_snapshots` - Toggle snapshot creation (default: true)
- `snapshot_label` - Custom label for snapshots (optional)
- `use_previous_snapshot` - Dropdown to select from available snapshots

 **Requirement 4: Retrieve available snapshots**
- Uses `aws_ami_ids` data source with Coder-specific tag filters
- Formats snapshot metadata for selection dropdown

 **Requirement 5: Modify instance creation**
- `local.ami_id` variable selects user snapshot or default AMI
- Dynamic AMI selection logic implemented
- `lifecycle { ignore_changes = [ami] }` prevents Terraform conflicts

 **Requirement 6: Optional cleanup**
- `aws_dlm_lifecycle_policy` for snapshot retention management
- Configurable retention periods and counts
- Cost control through deprecation time

 **Requirement 7: Key considerations**
- IAM permissions documented
- Graceful workspace stop handling
- Cost control implementation
- Proper tagging for organization

## Testing & Validation

### Comprehensive Test Suite

Created comprehensive test script that validates **ALL** requirements
from issue #26:

<details>
<summary>🔧 Comprehensive Test Script (Click to expand)</summary>

```bash
#!/bin/bash

# Comprehensive test for AWS AMI Snapshot module
# Tests EVERY requirement from GitHub issue #26

set -e

echo "🎯 COMPREHENSIVE TEST: AWS AMI Snapshot Module"
echo "Testing ALL requirements from issue #26"
echo "=============================================="
echo ""

# Test variables
TEST_WORKSPACE="test-workspace-$(date +%s)"
TEST_OWNER="test-owner"
TEST_TEMPLATE="comprehensive-test"
REGION="${AWS_DEFAULT_REGION:-us-east-1}"

echo "📋 Test Configuration:"
echo "  Account: $(aws sts get-caller-identity --query Account --output text)"
echo "  Region: $REGION"
echo "  Workspace: $TEST_WORKSPACE"
echo "  Owner: $TEST_OWNER"
echo "  Template: $TEST_TEMPLATE"
echo ""

# ===== REQUIREMENT 1: Create AMI snapshots on workspace stop =====
echo "🔍 REQUIREMENT 1: AMI Snapshots on Workspace Stop"
echo "=================================================="

# Create test infrastructure
cat > test-comprehensive.tf << EOF
terraform {
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
    coder = { source = "coder/coder", version = ">= 0.17" }
  }
}

provider "aws" { region = "$REGION" }
provider "coder" {}

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

resource "aws_instance" "test" {
  ami           = module.ami_snapshot.ami_id
  instance_type = "t3.micro"
  tags = { Name = "comprehensive-test" }
  lifecycle { ignore_changes = [ami] }
}

module "ami_snapshot" {
  source = "./registry/mavrickrishi/modules/aws-ami-snapshot"
  instance_id     = aws_instance.test.id
  default_ami_id  = data.aws_ami.ubuntu.id
  template_name   = "$TEST_TEMPLATE"
  
  # Test optional cleanup features
  enable_dlm_cleanup = false
  snapshot_retention_count = 5
  
  tags = {
    Environment = "test"
    TestType = "comprehensive"
  }
}

output "instance_id" { value = aws_instance.test.id }
output "ami_id" { value = module.ami_snapshot.ami_id }
output "is_using_snapshot" { value = module.ami_snapshot.is_using_snapshot }
output "available_snapshots" { value = module.ami_snapshot.available_snapshots }
output "snapshot_info" { value = module.ami_snapshot.snapshot_info }
EOF

echo " Test 1.1: aws_ami_from_instance resource exists in module"
echo "  💻 Running: grep aws_ami_from_instance registry/mavrickrishi/modules/aws-ami-snapshot/main.tf"
grep -q "aws_ami_from_instance" registry/mavrickrishi/modules/aws-ami-snapshot/main.tf && echo "   Found aws_ami_from_instance resource"

echo " Test 1.2: Triggered by coder_workspace.me.transition == 'stop'"
echo "  💻 Running: grep 'coder_workspace.me.transition == \"stop\"' main.tf"
grep -q 'coder_workspace.me.transition == "stop"' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf && echo "   Found stop transition trigger"

echo " Test 1.3: Deploy test infrastructure"
echo "  🔧 Initializing Terraform..."
echo "  💻 Running: terraform init"
terraform init
echo ""
echo "  🚀 Applying Terraform configuration..."
echo "  💻 Running: terraform apply -auto-approve"
terraform apply -auto-approve
echo ""
INSTANCE_ID=$(terraform output -raw instance_id)
echo "   Created test instance: $INSTANCE_ID"
echo ""
echo "  📊 Initial module outputs:"
echo "  💻 Running: terraform output"
terraform output

# ===== REQUIREMENT 2: Tag AMIs with workspace metadata =====
echo ""
echo "🔍 REQUIREMENT 2: AMI Tagging with Workspace Metadata"
echo "====================================================="

echo " Test 2.1: Create AMI with proper tags (simulating workspace stop)"
echo "  💻 Running: aws ec2 create-image --instance-id $INSTANCE_ID ..."
AMI_ID=$(aws ec2 create-image \
  --instance-id $INSTANCE_ID \
  --name "$TEST_OWNER-$TEST_WORKSPACE-$(date +%Y-%m-%d-%H%M)" \
  --description "Comprehensive test snapshot" \
  --no-reboot \
  --tag-specifications "ResourceType=image,Tags=[
    {Key=Name,Value=$TEST_OWNER-$TEST_WORKSPACE-snapshot},
    {Key=CoderWorkspace,Value=$TEST_WORKSPACE},
    {Key=CoderOwner,Value=$TEST_OWNER},
    {Key=CoderTemplate,Value=$TEST_TEMPLATE},
    {Key=SnapshotLabel,Value=comprehensive-test},
    {Key=CreatedAt,Value=$(date -Iseconds)},
    {Key=SnapshotType,Value=workspace},
    {Key=WorkspaceId,Value=test-workspace-id}
  ]" \
  --query ImageId --output text)

echo "   Created AMI: $AMI_ID"

echo " Test 2.2: Verify AMI tags include workspace owner"
aws ec2 describe-images --image-ids $AMI_ID --query 'Images[0].Tags[?Key==`CoderOwner`].Value' --output text | grep -q "$TEST_OWNER" && echo "   CoderOwner tag correct"

echo " Test 2.3: Verify AMI tags include workspace name"
aws ec2 describe-images --image-ids $AMI_ID --query 'Images[0].Tags[?Key==`CoderWorkspace`].Value' --output text | grep -q "$TEST_WORKSPACE" && echo "   CoderWorkspace tag correct"

echo " Test 2.4: Verify AMI tags include template name"
aws ec2 describe-images --image-ids $AMI_ID --query 'Images[0].Tags[?Key==`CoderTemplate`].Value' --output text | grep -q "$TEST_TEMPLATE" && echo "   CoderTemplate tag correct"

echo " Test 2.5: Verify AMI tags include creation timestamp"
aws ec2 describe-images --image-ids $AMI_ID --query 'Images[0].Tags[?Key==`CreatedAt`].Value' --output text | grep -q "$(date +%Y-%m-%d)" && echo "   CreatedAt tag correct"

# ===== REQUIREMENT 3: User parameters for snapshot control =====
echo ""
echo "🔍 REQUIREMENT 3: User Parameters for Snapshot Control"
echo "======================================================"

echo " Test 3.1: Enable/disable snapshot functionality parameter"
grep -q 'data "coder_parameter" "enable_snapshots"' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf && echo "   Found enable_snapshots parameter"

echo " Test 3.2: Custom snapshot labels parameter"
grep -q 'data "coder_parameter" "snapshot_label"' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf && echo "   Found snapshot_label parameter"

echo " Test 3.3: Previous snapshots selection parameter"
grep -q 'data "coder_parameter" "use_previous_snapshot"' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf && echo "   Found use_previous_snapshot parameter"

echo " Test 3.4: Parameter has dropdown options"
grep -q 'dynamic "option"' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf && echo "   Found dynamic options for snapshot selection"

# ===== REQUIREMENT 4: Retrieve available snapshots =====
echo ""
echo "🔍 REQUIREMENT 4: Retrieve Available Snapshots"
echo "=============================================="

echo " Test 4.1: aws_ami data source with filters"
grep -q 'data "aws_ami_ids" "workspace_snapshots"' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf && echo "   Found aws_ami_ids data source"

echo " Test 4.2: Filter by Coder-specific tags"
grep -A 10 'data "aws_ami_ids" "workspace_snapshots"' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf | grep -q "CoderWorkspace" && echo "   Found CoderWorkspace filter"
grep -A 10 'data "aws_ami_ids" "workspace_snapshots"' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf | grep -q "CoderOwner" && echo "   Found CoderOwner filter"
grep -A 10 'data "aws_ami_ids" "workspace_snapshots"' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf | grep -q "CoderTemplate" && echo "   Found CoderTemplate filter"

echo " Test 4.3: Wait for AMI to be available"
echo "   Waiting for AMI $AMI_ID to become available (this may take a few minutes)..."
aws ec2 wait image-available --image-ids $AMI_ID
echo "   AMI is now available"

echo " Test 4.4: Test snapshot retrieval functionality"
echo "  🏷️  Updating tags to match Coder provider values..."
aws ec2 create-tags --resources $AMI_ID --tags \
  Key=CoderWorkspace,Value=default \
  Key=CoderOwner,Value=default \
  Key=CoderTemplate,Value=$TEST_TEMPLATE

echo "  🔄 Refreshing Terraform state to detect snapshots..."
echo "  💻 Running: terraform refresh"
terraform refresh
echo ""
echo "  📊 Updated module outputs:"
echo "  💻 Running: terraform output"
terraform output
echo ""
FOUND_SNAPSHOTS=$(terraform output -json available_snapshots | jq -r '.[]' | wc -l)
if [ "$FOUND_SNAPSHOTS" -gt 0 ]; then
  echo "   Module detected $FOUND_SNAPSHOTS snapshot(s)!"
  echo "  📸 Available snapshots:"
  terraform output -json available_snapshots | jq -r '.[]'
else
  echo "   Module did not detect snapshots"
fi

# ===== REQUIREMENT 5: Modify instance creation =====
echo ""
echo "🔍 REQUIREMENT 5: Dynamic AMI Selection"
echo "======================================="

echo " Test 5.1: local.ami_id variable exists"
grep -q 'local.ami_id' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf && echo "   Found local.ami_id variable"

echo " Test 5.2: Dynamic AMI selection logic"
grep -A 5 'locals {' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf | grep -q 'use_snapshot.*=.*' && echo "   Found snapshot selection logic"

echo " Test 5.3: Test AMI ID output"
CURRENT_AMI=$(terraform output -raw ami_id)
echo "   Module returns AMI ID: $CURRENT_AMI"

echo " Test 5.4: Test snapshot usage flag"
IS_USING_SNAPSHOT=$(terraform output -raw is_using_snapshot)
echo "   Using snapshot: $IS_USING_SNAPSHOT"

echo " Test 5.5: Test instance creation from snapshot"
echo "  🚀 Creating new instance from snapshot AMI..."
echo "  💻 Running: aws ec2 run-instances --image-id $AMI_ID ..."
NEW_INSTANCE_ID=$(aws ec2 run-instances \
  --image-id $AMI_ID \
  --instance-type t3.micro \
  --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=test-from-snapshot}]" \
  --query 'Instances[0].InstanceId' --output text)
echo "   Waiting for new instance to be running..."
echo "  💻 Running: aws ec2 wait instance-running --instance-ids $NEW_INSTANCE_ID"
aws ec2 wait instance-running --instance-ids $NEW_INSTANCE_ID
echo "   Created instance from snapshot: $NEW_INSTANCE_ID"

# ===== REQUIREMENT 6: Optional cleanup (DLM) =====
echo ""
echo "🔍 REQUIREMENT 6: Optional Cleanup Implementation"
echo "==============================================="

echo " Test 6.1: DLM lifecycle policy resource exists"
grep -q 'aws_dlm_lifecycle_policy' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf && echo "   Found DLM lifecycle policy resource"

echo " Test 6.2: DLM configuration options exist"
grep -q 'variable "enable_dlm_cleanup"' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf && echo "   Found enable_dlm_cleanup variable"
grep -q 'variable "dlm_role_arn"' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf && echo "   Found dlm_role_arn variable"
grep -q 'variable "snapshot_retention_count"' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf && echo "   Found snapshot_retention_count variable"

echo " Test 6.3: DLM targets correct resources"
grep -A 10 'aws_dlm_lifecycle_policy' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf | grep -q 'resource_types.*=.*\["INSTANCE"\]' && echo "   DLM targets instances"

# ===== REQUIREMENT 7: Key Considerations =====
echo ""
echo "🔍 REQUIREMENT 7: Key Considerations"
echo "==================================="

echo " Test 7.1: IAM permissions documented"
grep -q "ec2:CreateImage" registry/mavrickrishi/modules/aws-ami-snapshot/README.md && echo "   Required IAM permissions documented"

echo " Test 7.2: Graceful workspace stop handling"
grep -q "snapshot_without_reboot.*=.*true" registry/mavrickrishi/modules/aws-ami-snapshot/main.tf && echo "   Uses snapshot_without_reboot for graceful handling"

echo " Test 7.3: Cost control through cleanup"
grep -q "deprecation_time" registry/mavrickrishi/modules/aws-ami-snapshot/main.tf && echo "   Sets deprecation_time for cost control"

echo " Test 7.4: Proper tagging for organization"
grep -A 20 'tags = merge' registry/mavrickrishi/modules/aws-ami-snapshot/main.tf | grep -q "SnapshotType" && echo "   Comprehensive tagging implemented"

echo " Test 7.5: Lifecycle ignore_changes prevention"
grep -q "ignore_changes.*=.*\[.*ami.*\]" test-comprehensive.tf && echo "   Terraform conflicts prevented"

# ===== FINAL VALIDATION =====
echo ""
echo "🔍 FINAL VALIDATION: End-to-End Test"
echo "===================================="

echo " Test: Show all created resources"
echo "  Original instance: $INSTANCE_ID (using default AMI)"
echo "  Snapshot AMI: $AMI_ID (with Coder metadata)"  
echo "  New instance: $NEW_INSTANCE_ID (from snapshot)"

echo " Test: Verify snapshot metadata"
echo "  💻 Running: aws ec2 describe-images --image-ids $AMI_ID ..."
aws ec2 describe-images --image-ids $AMI_ID --query 'Images[0].{Name:Name,State:State,Tags:Tags}' --output table

echo ""
echo " Test: Show both instances (original vs from snapshot)"
echo "  💻 Running: aws ec2 describe-instances --instance-ids $INSTANCE_ID $NEW_INSTANCE_ID ..."
aws ec2 describe-instances \
  --instance-ids $INSTANCE_ID $NEW_INSTANCE_ID \
  --query 'Reservations[*].Instances[*].{InstanceId:InstanceId,State:State.Name,ImageId:ImageId,Name:Tags[?Key==`Name`].Value|[0]}' \
  --output table

echo ""
echo " Test: Final module outputs"
echo "  💻 Running: terraform output"
terraform output

echo ""
echo "🎉 COMPREHENSIVE TEST RESULTS"
echo "============================="
echo " ALL REQUIREMENTS FROM ISSUE #26 IMPLEMENTED AND TESTED!"
echo ""
echo "📋 Validated Implementation:"
echo "   AMI snapshots on workspace stop (aws_ami_from_instance)"
echo "   Proper tagging with workspace metadata"
echo "   User parameters (enable, labels, selection)"
echo "   Snapshot retrieval with Coder-specific filters"
echo "   Dynamic AMI selection (local.ami_id)"
echo "   Optional DLM cleanup policies"
echo "   All key considerations addressed"
echo ""
echo "🎯 Module successfully provides persistent workspace state!"

# Cleanup prompt
echo ""
read -p "🧹 Clean up test resources? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
  echo "Cleaning up..."
  echo "  💻 Running: aws ec2 terminate-instances --instance-ids $INSTANCE_ID $NEW_INSTANCE_ID"
  aws ec2 terminate-instances --instance-ids $INSTANCE_ID $NEW_INSTANCE_ID > /dev/null
  echo "  💻 Running: aws ec2 deregister-image --image-id $AMI_ID"
  aws ec2 deregister-image --image-id $AMI_ID > /dev/null
  echo "  💻 Running: terraform destroy -auto-approve"
  terraform destroy -auto-approve > /dev/null
  echo "  💻 Running: rm -f test-comprehensive.tf terraform.tfstate* .terraform.lock.hcl"
  rm -f test-comprehensive.tf terraform.tfstate* .terraform.lock.hcl
  echo "  💻 Running: rm -rf .terraform/"
  rm -rf .terraform/
  echo " Cleanup complete!"
else
  echo "Resources preserved for inspection"
fi
```

</details>

### Test Results Summary

- [x] **Tests pass** (`bun test` - validates module structure)
- [x] **Code formatted** (`bun run fmt` - all files properly formatted)
- [x] **Terraform validation** (`terraform validate` - configuration is
valid)
- [x] **Real AWS testing** (Comprehensive test with actual EC2 instances
and AMIs)
- [x] **All 7 requirements validated** (Every requirement from issue #26
tested)

### Module Structure
```bash
$ tree registry/mavrickrishi/modules/aws-ami-snapshot/
registry/mavrickrishi/modules/aws-ami-snapshot/
├── main.test.ts          # Module tests
├── main.tf               # Terraform configuration
└── README.md             # Documentation
```

### Namespace Structure
```bash
$ tree registry/mavrickrishi/
registry/mavrickrishi/
├── .images/
│   └── avatar.svg        # Namespace avatar
├── README.md             # Namespace documentation
└── modules/
    └── aws-ami-snapshot/ # The module
```

## Key Features Implemented

### 🎯 **Core Functionality:**
- **Automatic AMI creation** on workspace transition to "stop"
- **Workspace-specific snapshot filtering** by owner, workspace, and
template
- **Dynamic AMI selection** - defaults to base AMI, switches to selected
snapshot
- **User-friendly parameters** - enable/disable, custom labels, snapshot
selection

### 🔧 **Technical Implementation:**
- **aws_ami_from_instance** resource with proper lifecycle management
- **Comprehensive tagging** for organization and cost tracking
- **Data Lifecycle Manager** integration for automated cleanup
- **Terraform conflict prevention** with `ignore_changes = [ami]`

### 🎛️ **User Experience:**
- **Enable AMI Snapshots** - Boolean toggle (default: true)
- **Snapshot Label** - Optional custom label for identification
- **Start from Snapshot** - Dropdown with available snapshots and
descriptions

### 💰 **Cost Management:**
- **Deprecation time** set to 7 days for automatic cleanup hints
- **Optional DLM policies** for automated snapshot retention
- **Configurable retention counts** to control storage costs

## Security & IAM

### Required IAM Permissions:
```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateImage",
        "ec2:DescribeImages",
        "ec2:DescribeInstances",
        "ec2:CreateTags",
        "ec2:DescribeTags"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "dlm:CreateLifecyclePolicy",
        "dlm:GetLifecyclePolicy",
        "dlm:UpdateLifecyclePolicy",
        "dlm:DeleteLifecyclePolicy"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "dlm:Target": "INSTANCE"
        }
      }
    }
  ]
}
```

## Usage Example

```hcl
module "ami_snapshot" {
  source = "registry.coder.com/modules/mavrickrishi/aws-ami-snapshot"

  instance_id     = aws_instance.workspace.id
  default_ami_id  = data.aws_ami.ubuntu.id
  template_name   = "my-workspace-template"

  # Optional: Enable automated cleanup
  enable_dlm_cleanup       = true
  dlm_role_arn            = aws_iam_role.dlm_lifecycle_role.arn
  snapshot_retention_count = 5

  tags = {
    Environment = "production"
    Team        = "engineering"
  }
}

resource "aws_instance" "workspace" {
  ami           = module.ami_snapshot.ami_id
  instance_type = "t3.large"

  # Prevent Terraform from recreating instance when AMI changes
  lifecycle {
    ignore_changes = [ami]
  }
}
```

## Related Issues

- **Closes #26** - AWS Snapshot functionality
- **Implements** all 7 requirements from the GitHub issue
- **Provides** persistent workspace state across stop/start cycles

## Video Demonstration




https://github.com/user-attachments/assets/9356e4b5-9a67-4988-a03f-57e950afa5c2


https://github.com/user-attachments/assets/b6af98db-5d01-4aff-853d-055b92911ea5

---------

Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: DevCats <chris@dualriver.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-25 20:21:56 -05:00
35C4n0r 240643d3b0 feat: remove node installation from gemini (#374)
Closes #

## Description
Standardising this across all modules:
- remove default node & nvm installation

<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder-labs/modules/gemini`  
**New version:** `v2.0.0`  
**Breaking change:** [x] Yes [ ] No

## Testing & Validation

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

## Related Issues

<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-08-25 12:53:48 -05:00
209 changed files with 10425 additions and 2059 deletions
+8 -3
View File
@@ -1,5 +1,3 @@
Closes #
## Description
<!-- Briefly describe what this PR does and why -->
@@ -7,6 +5,7 @@ Closes #
## Type of Change
- [ ] New module
- [ ] New template
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
@@ -20,10 +19,16 @@ Closes #
**New version:** `v1.0.0`
**Breaking change:** [ ] Yes [ ] No
## Template Information
<!-- Delete this section if not applicable -->
**Path:** `registry/[namespace]/templates/[template-name]`
## Testing & Validation
- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] Code formatted (`bun fmt`)
- [ ] Changes tested locally
## Related Issues
+1
View File
@@ -0,0 +1 @@
../AGENTS.md
+2 -2
View File
@@ -192,8 +192,8 @@ main() {
# Always run formatter to ensure consistent formatting
echo "🔧 Running formatter to ensure consistent formatting..."
if command -v bun >/dev/null 2>&1; then
bun fmt >/dev/null 2>&1 || echo "⚠️ Warning: bun fmt failed, but continuing..."
if command -v bun > /dev/null 2>&1; then
bun fmt > /dev/null 2>&1 || echo "⚠️ Warning: bun fmt failed, but continuing..."
else
echo "⚠️ Warning: bun not found, skipping formatting"
fi
+3
View File
@@ -1,7 +1,10 @@
[default.extend-words]
muc = "muc" # For Munich location code
tyo = "tyo" # For Tokyo location code
Hashi = "Hashi"
HashiCorp = "HashiCorp"
mavrickrishi = "mavrickrishi" # Username
mavrick = "mavrick" # Username
[files]
extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive
+4 -2
View File
@@ -28,6 +28,8 @@ jobs:
run: bun install
- name: Run TypeScript tests
run: bun test
- name: Run Terraform tests
run: ./scripts/terraform_test_all.sh
- name: Run Terraform Validate
run: bun terraform-validate
validate-style:
@@ -48,7 +50,7 @@ jobs:
- name: Validate formatting
run: bun fmt:ci
- name: Check for typos
uses: crate-ci/typos@v1.35.5
uses: crate-ci/typos@v1.38.1
with:
config: .github/typos.toml
validate-readme-files:
@@ -61,7 +63,7 @@ jobs:
- name: Check out code
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.23.2"
- name: Validate contributors
+2 -2
View File
@@ -30,12 +30,12 @@ jobs:
- name: Checkout code
uses: actions/checkout@v5
- name: Authenticate with Google Cloud
uses: google-github-actions/auth@b7593ed2efd1c1617e1b0254da33b86225adb2a5
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
with:
workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github
service_account: registry-v2-github@coder-registry-1.iam.gserviceaccount.com
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@26f734c2779b00b7dda794207734c511110a4368
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
- name: Deploy to dev.registry.coder.com
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch main
- name: Deploy to registry.coder.com
+2 -2
View File
@@ -15,10 +15,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version: stable
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.1
version: v2.1
+27 -22
View File
@@ -11,33 +11,33 @@ jobs:
permissions:
contents: write
pull-requests: read
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
- name: Extract tag information
id: tag_info
run: |
TAG=${GITHUB_REF#refs/tags/}
echo "tag=$TAG" >> $GITHUB_OUTPUT
IFS='/' read -ra PARTS <<< "$TAG"
NAMESPACE="${PARTS[1]}"
MODULE="${PARTS[2]}"
VERSION="${PARTS[3]}"
echo "namespace=$NAMESPACE" >> $GITHUB_OUTPUT
echo "module=$MODULE" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "module_path=registry/$NAMESPACE/modules/$MODULE" >> $GITHUB_OUTPUT
RELEASE_TITLE="$NAMESPACE/$MODULE $VERSION"
echo "release_title=$RELEASE_TITLE" >> $GITHUB_OUTPUT
- name: Find previous tag
id: prev_tag
env:
@@ -46,15 +46,15 @@ jobs:
CURRENT_TAG: ${{ steps.tag_info.outputs.tag }}
run: |
PREV_TAG=$(git tag -l "release/$NAMESPACE/$MODULE/v*" | sort -V | grep -B1 "$CURRENT_TAG" | head -1)
if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "$CURRENT_TAG" ]; then
echo "No previous tag found, using initial commit"
PREV_TAG=$(git rev-list --max-parents=0 HEAD)
fi
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
echo "Previous tag: $PREV_TAG"
- name: Generate changelog
id: changelog
env:
@@ -64,24 +64,29 @@ jobs:
CURRENT_TAG: ${{ steps.tag_info.outputs.tag }}
run: |
echo "Generating changelog for $MODULE_PATH between $PREV_TAG and $CURRENT_TAG"
COMMITS=$(git log --oneline --no-merges "$PREV_TAG..$CURRENT_TAG" -- "$MODULE_PATH")
if [ -z "$COMMITS" ]; then
echo "No commits found for this module"
echo "changelog=No changes found for this module." >> $GITHUB_OUTPUT
exit 0
fi
FULL_CHANGELOG=$(gh api repos/:owner/:repo/releases/generate-notes \
--field tag_name="$CURRENT_TAG" \
--field previous_tag_name="$PREV_TAG" \
--jq '.body')
if [[ "$PREV_TAG" == release/* ]]; then
FULL_CHANGELOG=$(gh api repos/:owner/:repo/releases/generate-notes \
--field tag_name="$CURRENT_TAG" \
--field previous_tag_name="$PREV_TAG" \
--jq '.body')
else
echo "New module detected, skipping GitHub API"
FULL_CHANGELOG=""
fi
MODULE_COMMIT_SHAS=$(git log --format="%H" --no-merges "$PREV_TAG..$CURRENT_TAG" -- "$MODULE_PATH")
FILTERED_CHANGELOG="## What's Changed\n\n"
for sha in $MODULE_COMMIT_SHAS; do
SHORT_SHA=${sha:0:7}
@@ -95,11 +100,11 @@ jobs:
FILTERED_CHANGELOG="${FILTERED_CHANGELOG}* $COMMIT_MSG by @$AUTHOR\n"
fi
done
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo -e "$FILTERED_CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -109,4 +114,4 @@ jobs:
run: |
gh release create "$TAG_NAME" \
--title "$RELEASE_TITLE" \
--notes "$CHANGELOG"
--notes "$CHANGELOG"
+1 -1
View File
@@ -95,7 +95,7 @@ jobs:
- name: Comment on PR - Failure
if: failure() && steps.version-check.outputs.versions_up_to_date == 'false'
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
+2 -2
View File
@@ -163,8 +163,8 @@ linters:
staticcheck:
checks:
- all
- SA4006 # Detects redundant assignments
- SA4009 # Detects redundant variable declarations
- SA4006 # Detects redundant assignments
- SA4009 # Detects redundant variable declarations
- SA1019
exclusions:
generated: lax
+4
View File
@@ -0,0 +1,4 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Akamai</title>
<path d="M13.0548 0C6.384 0 .961 5.3802.961 12.0078.961 18.6354 6.3698 24 13.0548 24c.6168 0 .6454-.3572.0859-.5293-4.9349-1.5063-8.5352-6.069-8.5352-11.4629 0-5.4656 3.6725-10.0706 8.6934-11.5195C13.8153.3448 13.6716 0 13.0548 0Zm2.3242 1.8223c-5.2648 0-9.5254 4.2606-9.5254 9.5254 0 1.2193.2285 2.3818.6445 3.4433.1722.459.4454.4584.4024.0137-.0287-.3156-.0567-.6447-.0567-.9746 0-5.2648 4.2606-9.5254 9.5254-9.5254 4.9779 0 6.4698 2.2235 6.6563 2.08.2008-.1577-1.808-4.5624-7.6465-4.5624zm.4687 4.0703c-1.8622.0592-3.651.7168-5.1035 1.8554-.2582.2009-.1567.3284.1445.1993 2.4675-1.076 5.5812-1.1046 8.6368-.043 2.0514.7173 3.2413 1.7364 3.3418 1.6934.1578-.0718-1.1915-2.2226-3.6446-3.1407-1.1135-.4196-2.2576-.6-3.375-.5644z" fill="#0096D6"/>
</svg>

After

Width:  |  Height:  |  Size: 852 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512pt" height="512pt" version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path d="m500.48 262.2-48.18 73.984c-0.73438 1.1367-2 1.8242-3.3555 1.8242-1.3516 0-2.6172-0.6875-3.3516-1.8242l-48.129-73.984c-0.78125-1.2227-0.83594-2.7773-0.14453-4.0547 0.69141-1.2734 2.0195-2.0742 3.4727-2.0898h24.781c-0.007813-29.523-7.7188-58.531-22.375-84.156-14.652-25.629-35.742-46.988-61.184-61.969-2.3711-1.3633-3.8633-3.8594-3.9453-6.5938-0.085937-2.7305 1.2539-5.3125 3.5352-6.8203l27.035-17.613c3.4766-2.3633 8.043-2.3633 11.52 0 28.473 19.934 51.723 46.441 67.773 77.27 16.051 30.828 24.434 65.074 24.438 99.832h24.781c1.4688 0 2.8203 0.80859 3.5156 2.1055 0.69531 1.293 0.62109 2.8633-0.1875 4.0898zm-85.043 79.359c-1.5078-2.2812-4.0898-3.6211-6.8203-3.5391-2.7344 0.085937-5.2305 1.5781-6.5938 3.9492-14.965 25.434-36.305 46.523-61.914 61.188-25.609 14.664-54.602 22.391-84.109 22.422v-24.781c-0.011719-1.4531-0.8125-2.7812-2.0898-3.4727-1.2773-0.69141-2.832-0.63672-4.0547 0.14453l-74.035 47.977c-1.1367 0.73438-1.8242 1.9961-1.8242 3.3516s0.6875 2.6172 1.8242 3.3555l73.984 48.18c1.2227 0.78125 2.7773 0.83594 4.0547 0.14453 1.2734-0.69141 2.0742-2.0234 2.0898-3.4727v-24.68c34.734-0.015624 68.957-8.3984 99.766-24.441 30.812-16.039 57.301-39.27 77.23-67.719 2.3672-3.4766 2.3672-8.043 0-11.52zm-245.45 60.52c-25.434-14.977-46.516-36.328-61.172-61.945-14.652-25.617-22.371-54.617-22.387-84.129h24.781c1.4531-0.011719 2.7812-0.8125 3.4727-2.0898 0.69141-1.2773 0.63672-2.832-0.14453-4.0547l-47.977-74.035c-0.73438-1.1367-1.9961-1.8242-3.3516-1.8242s-2.6172 0.6875-3.3555 1.8242l-48.332 73.984c-0.80859 1.2266-0.88281 2.7969-0.1875 4.0898 0.69531 1.2969 2.0469 2.1055 3.5156 2.1055h24.781c0.015625 34.734 8.3984 68.957 24.438 99.766 16.043 30.812 39.273 57.301 67.723 77.234 3.4766 2.3633 8.043 2.3633 11.52 0l27.086-17.664c2.2109-1.5195 3.4961-4.0625 3.4141-6.7422-0.082032-2.6836-1.5234-5.1406-3.8242-6.5195zm92.16-390.5c-1.2227-0.78125-2.7773-0.83594-4.0547-0.14453-1.2773 0.69141-2.0781 2.0195-2.0898 3.4727v24.73c-34.734 0.015625-68.957 8.3984-99.766 24.438-30.812 16.043-57.301 39.273-77.234 67.723-2.3633 3.4766-2.3633 8.043 0 11.52l17.664 27.086c1.5078 2.2812 4.0898 3.6211 6.8242 3.5352 2.7305-0.082032 5.2266-1.5742 6.5898-3.9453 14.965-25.41 36.289-46.48 61.879-61.133 25.59-14.652 54.555-22.383 84.043-22.426v24.781c0.011719 1.4531 0.8125 2.7812 2.0898 3.4727 1.2773 0.69141 2.832 0.63672 4.0547-0.14453l74.035-47.977c1.1367-0.73438 1.8242-1.9961 1.8242-3.3516s-0.6875-2.6172-1.8242-3.3555zm-6.1445 210.23c-9.0703 0-17.77 3.6055-24.184 10.02-6.4141 6.4141-10.02 15.113-10.02 24.184s3.6055 17.77 10.02 24.184c6.4141 6.4141 15.113 10.02 24.184 10.02s17.77-3.6055 24.184-10.02c6.4141-6.4141 10.02-15.113 10.02-24.184s-3.6055-17.77-10.02-24.184c-6.4141-6.4141-15.113-10.02-24.184-10.02zm90.727-26.828-10.344 14.953c4.0039 6.9414 7.0859 14.375 9.1641 22.117l17.973 2.9688c6.543 1.1445 11.316 6.8242 11.316 13.465v15.055c0 6.6406-4.7734 12.32-11.316 13.465l-17.766 3.125v-0.003907c-2.1562 7.6992-5.3086 15.082-9.3711 21.965l10.238 14.797h0.003906c3.8047 5.4375 3.1562 12.82-1.5352 17.512l-10.648 10.648h-0.003906c-4.6914 4.6953-12.074 5.3438-17.508 1.5391l-14.797-10.238v-0.003907c-6.9453 4.0039-14.379 7.0859-22.121 9.1641l-3.0195 18.023c-1.1445 6.543-6.8242 11.316-13.465 11.316h-15.055c-6.6406 0-12.32-4.7734-13.465-11.316l-3.125-17.766h0.003907c-7.7031-2.1758-15.086-5.3398-21.965-9.4219l-14.797 10.238v0.003907c-5.4375 3.8047-12.82 3.1562-17.512-1.5391l-10.648-10.648c-4.6953-4.6914-5.3438-12.074-1.5391-17.512l10.238-14.797h0.003907c-4.0039-6.9414-7.0859-14.375-9.1641-22.117l-18.023-2.9688c-6.543-1.1445-11.316-6.8242-11.316-13.465v-15.055c0-6.6406 4.7734-12.32 11.316-13.465l17.766-3.125v0.003907c2.1562-7.6992 5.3086-15.082 9.3711-21.965l-10.238-14.797h-0.003906c-3.8047-5.4375-3.1562-12.82 1.5352-17.512l10.648-10.648h0.003906c4.6914-4.6953 12.074-5.3438 17.508-1.5391l14.797 10.238v0.003907c6.9453-4.0039 14.379-7.0859 22.121-9.1641l3.0195-18.023c1.1445-6.543 6.8242-11.316 13.465-11.316h15.055c6.6406 0 12.32 4.7734 13.465 11.316l3.125 17.766h-0.003907c7.6992 2.1562 15.082 5.3086 21.965 9.3711l14.797-10.238v-0.003906c5.4375-3.8047 12.82-3.1562 17.512 1.5352l10.648 10.648v0.003906c4.6875 4.6367 5.3984 11.957 1.6914 17.406zm-36.047 61.031c0-14.504-5.7578-28.41-16.016-38.664-10.254-10.258-24.16-16.016-38.664-16.016s-28.41 5.7578-38.664 16.016c-10.258 10.254-16.016 24.16-16.016 38.664s5.7578 28.41 16.016 38.664c10.254 10.258 24.16 16.016 38.664 16.016 14.5-0.011719 28.398-5.7773 38.652-16.027 10.25-10.254 16.016-24.152 16.027-38.652z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><text x="50%" y="50%" font-size="96px" text-anchor="middle" dominant-baseline="middle" font-family="Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, sans-serif">🔌</text></svg>

After

Width:  |  Height:  |  Size: 247 B

+6
View File
@@ -0,0 +1,6 @@
<svg width="251" height="251" viewBox="0 0 251 251" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 47.0195C39.45 49.6394 71.06 81.3272 73.54 120.815H119.61C117.05 55.8589 64.93 3.64245 0 0.942627V47.0195Z" fill="#0DC09D"/>
<path d="M73.8 131.324C71.18 170.771 39.49 202.379 0 204.859V250.926C64.96 248.366 117.18 196.249 119.88 131.324H73.8Z" fill="#0DC09D"/>
<path d="M176.201 120.545C178.821 81.0972 210.511 49.4894 250.001 47.0095V0.942627C185.041 3.50245 132.821 55.619 130.121 120.545H176.201Z" fill="#0DC09D"/>
<path d="M250.001 204.849C210.551 202.229 178.941 170.542 176.461 131.054H130.391C132.951 196.01 185.071 248.226 250.001 250.926V204.849Z" fill="#0DC09D"/>
</svg>

After

Width:  |  Height:  |  Size: 693 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="64" viewBox="0 0 25.6 25.6" width="64"><style><![CDATA[.B{stroke-linecap:round}.C{stroke-linejoin:round}.D{stroke-linejoin:miter}.E{stroke-width:.716}]]></style><g fill="none" stroke="#fff"><path d="M18.983 18.636c.163-1.357.114-1.555 1.124-1.336l.257.023c.777.035 1.793-.125 2.4-.402 1.285-.596 2.047-1.592.78-1.33-2.89.596-3.1-.383-3.1-.383 3.053-4.53 4.33-10.28 3.227-11.687-3.004-3.84-8.205-2.024-8.292-1.976l-.028.005c-.57-.12-1.2-.19-1.93-.2-1.308-.02-2.3.343-3.054.914 0 0-9.277-3.822-8.846 4.807.092 1.836 2.63 13.9 5.66 10.25C8.29 15.987 9.36 14.86 9.36 14.86c.53.353 1.167.533 1.834.468l.052-.044a2.01 2.01 0 0 0 .021.518c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.162l-.046.183c.306.245.285 1.76.33 2.842s.116 2.093.337 2.688.48 2.13 2.53 1.7c1.713-.367 3.023-.896 3.143-5.81" fill="#000" stroke="#000" stroke-linecap="butt" stroke-width="2.149" class="D"/><path d="M23.535 15.6c-2.89.596-3.1-.383-3.1-.383 3.053-4.53 4.33-10.28 3.228-11.687-3.004-3.84-8.205-2.023-8.292-1.976l-.028.005a10.31 10.31 0 0 0-1.929-.201c-1.308-.02-2.3.343-3.054.914 0 0-9.278-3.822-8.846 4.807.092 1.836 2.63 13.9 5.66 10.25C8.29 15.987 9.36 14.86 9.36 14.86c.53.353 1.167.533 1.834.468l.052-.044a2.02 2.02 0 0 0 .021.518c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.162l-.046.183c.306.245.52 1.593.484 2.815s-.06 2.06.18 2.716.48 2.13 2.53 1.7c1.713-.367 2.6-1.32 2.725-2.906.088-1.128.286-.962.3-1.97l.16-.478c.183-1.53.03-2.023 1.085-1.793l.257.023c.777.035 1.794-.125 2.39-.402 1.285-.596 2.047-1.592.78-1.33z" fill="#336791" stroke="none"/><g class="E"><g class="B"><path d="M12.814 16.467c-.08 2.846.02 5.712.298 6.4s.875 2.05 2.926 1.612c1.713-.367 2.337-1.078 2.607-2.647l.633-5.017M10.356 2.2S1.072-1.596 1.504 7.033c.092 1.836 2.63 13.9 5.66 10.25C8.27 15.95 9.27 14.907 9.27 14.907m6.1-13.4c-.32.1 5.164-2.005 8.282 1.978 1.1 1.407-.175 7.157-3.228 11.687" class="C"/><path d="M20.425 15.17s.2.98 3.1.382c1.267-.262.504.734-.78 1.33-1.054.49-3.418.615-3.457-.06-.1-1.745 1.244-1.215 1.147-1.652-.088-.394-.69-.78-1.086-1.744-.347-.84-4.76-7.29 1.224-6.333.22-.045-1.56-5.7-7.16-5.782S7.99 8.196 7.99 8.196" stroke-linejoin="bevel"/></g><g class="C"><path d="M11.247 15.768c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.163.35-.49-.002-1.27-.482-1.468-.232-.096-.542-.216-.94.23z"/><path d="M11.196 15.753c-.08-.513.168-1.122.433-1.836.398-1.07 1.316-2.14.582-5.537-.547-2.53-4.22-.527-4.22-.184s.166 1.74-.06 3.365c-.297 2.122 1.35 3.916 3.246 3.733" class="B"/></g></g><g fill="#fff" class="D"><path d="M10.322 8.145c-.017.117.215.43.516.472s.558-.202.575-.32-.215-.246-.516-.288-.56.02-.575.136z" stroke-width=".239"/><path d="M19.486 7.906c.016.117-.215.43-.516.472s-.56-.202-.575-.32.215-.246.516-.288.56.02.575.136z" stroke-width=".119"/></g><path d="M20.562 7.095c.05.92-.198 1.545-.23 2.524-.046 1.422.678 3.05-.413 4.68" class="B C E"/></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#75aadb" d="M71.4 38.8c-1.5-.6-3.9-1-6.9-1.1-4.2-.1-9 .4-9.2.5v20c13.3.6 15.5-1.7 15.5-1.7 11.6-5.9 4.3-16.2.6-17.7z"/><path fill="#75aadb" d="M64 0C28.6 0 0 28.6 0 64s28.6 64 64 64 64-28.6 64-64S99.3 0 64 0zm28.6 89.8H82L64.4 63.5h-9V84h9v5.8H41.5v-5.7l7.6-.1-.1-45.9c-.8-.2-7.5-.8-7.5-.8V32c1 1 7.9 1.2 7.9 1.2 1.6.1 3.9.2 5.2-.1 9.3-1.7 16.4-.4 16.4-.4 14 3.2 14.2 15.8 10.3 22.6-3.5 5.8-10.3 7.2-10.3 7.2l14.4 21.8 7.2-.1v5.6z"/><path d="M41.595 87.073v-2.726l1.82-.141a59.125 59.125 0 013.752-.144h1.931V37.996l-.938-.127c-.516-.07-2.204-.248-3.752-.397l-2.813-.27v-2.51c0-2.332.027-2.495.39-2.3 1.583.847 10.7 1.07 15.83.388 4.202-.558 11.495-.425 14.035.257 5.483 1.472 9.11 4.646 10.824 9.473.717 2.018.817 5.847.216 8.224-.903 3.572-2.39 6.048-4.865 8.101-1.482 1.23-4.847 3.03-6.145 3.29-.397.079-.772.224-.832.321-.06.098 3.123 5.072 7.075 11.054l7.184 10.876 3.633-.068 3.634-.068V89.8l-5.242-.008-5.24-.007-8.82-13.234-8.817-13.234h-9.178V84.061h9.049V89.8H41.595zm25.158-29.162c3.476-.55 7.265-2.774 8.973-5.263 2.511-3.663 1.537-8.99-2.294-12.547-1.357-1.26-2.205-1.63-4.794-2.1-2.124-.386-8.66-.454-11.706-.122l-1.544.168-.058 10.083-.057 10.082.72.106c1.366.2 8.67-.075 10.76-.407z" fill="#fff" stroke="#fff" stroke-width=".788"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="32" viewBox="0 0 375 375" width="32" xmlns="http://www.w3.org/2000/svg">
<rect fill="#0071ff" height="375.001088" rx="58.59392" stroke-width=".91553" width="375.001088" x=".0009759" y="-.0066962"/>
<path d="m150.428 322.264c-29.063-6.202-53.897-22.439-73.115-47.804-19.507-25.746-27.838-55.355-25.723-91.414 6.655-62.013 47.667-106.753 99.687-120.411 4.509-.989 8.353-3.462 12.55-1.322 3.22 1.64 6.028 4.467 7.206 7.251 1.25 2.955 1.877 21.54.99 29.331-1.076 9.46-3.877 12.418-14.566 15.388-29.723 10.195-48.105 34.07-53.697 61.017-4.8 29.668 2.951 59.729 21.528 78.727 8.966 8.993 17.92 14.24 30.869 18.086 8.646 2.57 13.393 5.758 15.036 10.102 1.085 2.867 1.63 22.984.779 28.772-1.33 9.046-1.702 9.796-5.792 11.667-5.029 2.3-7.404 2.392-15.752.61zm50.708.29c-3.092-1.402-5.673-4.83-6.73-8.94-.134-9.408-2.366-25.754 1.02-33.373 1.88-4.128 4.65-5.999 12.433-8.396 21.267-6.551 37.593-19.88 46.806-38.213 11.11-22.108 11.877-55.183 1.808-77.975-9.154-20.723-25.7-35.217-48.555-42.534-8.872-2.84-12.004-5.065-12.968-9.21-1.002-4.31-1.435-19.87-.785-28.218.682-8.766 1.249-9.99 6.162-13.318 3.701-2.505 5.482-2.446 17.223.575 36.718 10.077 65.97 33.597 83.026 66.68 18.495 37.034 19.191 86.11 1.742 122.655-17.233 36.09-50.591 62.511-88.622 70.194-8.172 1.65-9.07 1.656-12.56.073z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+22
View File
@@ -0,0 +1,22 @@
# Ignore symlinks to avoid Prettier errors
CLAUDE.md
.github/copilot-instructions.md
# Ignore node_modules and dependencies
node_modules/
# Ignore Terraform files (formatted by terraform fmt)
*.tf
*.hcl
*.tfvars
# Ignore generated and temporary files
.terraform/
*.tfstate
*.tfstate.backup
*.tfstate.lock.info
# Ignore other files that shouldn't be formatted
bun.lock
go.sum
go.mod
+168
View File
@@ -0,0 +1,168 @@
# AGENTS.md
This file provides guidance to AI coding assistants when working with code in this repository.
## Project Overview
The Coder Registry is a community-driven repository for Terraform modules and templates that extend Coder workspaces. It's organized with:
- **Modules**: Individual components and tools (IDEs, auth integrations, dev tools)
- **Templates**: Complete workspace configurations for different platforms
- **Namespaces**: Each contributor has their own namespace under `/registry/[namespace]/`
## Common Development Commands
### Formatting
```bash
bun run fmt # Format all code (Prettier + Terraform)
bun run fmt:ci # Check formatting (CI mode)
```
### Testing
```bash
# Test all modules with .tftest.hcl files
bun run test
# Test specific module (from module directory)
terraform init -upgrade
terraform test -verbose
# Validate Terraform syntax
./scripts/terraform_validate.sh
```
### Module Creation
```bash
# Generate new module scaffold
./scripts/new_module.sh namespace/module-name
```
### TypeScript Testing & Setup
The repository uses Bun for TypeScript testing with utilities:
- `test/test.ts` - Testing utilities for container management and Terraform operations
- `setup.ts` - Test cleanup (removes .tfstate files and test containers)
- Container-based testing with Docker for module validation
## Architecture & Organization
### Directory Structure
```
registry/[namespace]/
├── README.md # Contributor info with frontmatter
├── .images/ # Namespace avatar (avatar.png/svg)
├── modules/ # Individual components
│ └── [module]/ # Each module has main.tf, README.md, tests
└── templates/ # Complete workspace configs
└── [template]/ # Each template has main.tf, README.md
```
### Key Components
**Module Structure**: Each module contains:
- `main.tf` - Terraform implementation
- `README.md` - Documentation with YAML frontmatter
- `.tftest.hcl` - Terraform test files (required)
- `run.sh` - Optional startup script
**Template Structure**: Each template contains:
- `main.tf` - Complete Coder template configuration
- `README.md` - Documentation with YAML frontmatter
- Additional configs, scripts as needed
### README Frontmatter Requirements
All modules/templates require YAML frontmatter:
```yaml
---
display_name: "Module Name"
description: "Brief description"
icon: "../../../../.icons/tool.svg"
verified: false
tags: ["tag1", "tag2"]
---
```
## Testing Requirements
### Module Testing
- Every module MUST have `.tftest.hcl` test files
- Optional `main.test.ts` files for container-based testing or complex business logic validation
- Tests use Docker containers with `--network=host` flag
- Linux required for testing (Docker Desktop on macOS/Windows won't work)
- Use Colima or OrbStack on macOS instead of Docker Desktop
### Test Utilities
The `test/test.ts` file provides:
- `runTerraformApply()` - Execute Terraform with variables
- `executeScriptInContainer()` - Run coder_script resources in containers
- `testRequiredVariables()` - Validate required variables
- Container management functions
## Validation & Quality
### Automated Validation
The Go validation tool (`cmd/readmevalidation/`) checks:
- Repository structure integrity
- Contributor README files
- Module and template documentation
- Frontmatter format compliance
### Versioning
Use semantic versioning for modules:
- **Patch** (1.2.3 → 1.2.4): Bug fixes
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
- **Major** (1.2.3 → 2.0.0): Breaking changes
## Dependencies & Tools
### Required Tools
- **Terraform** - Module development and testing
- **Docker** - Container-based testing
- **Bun** - JavaScript runtime for formatting/scripts
- **Go 1.23+** - Validation tooling
### Development Dependencies
- Prettier with Terraform and shell plugins
- TypeScript for test utilities
- Various npm packages for documentation processing
## Workflow Notes
### Contributing Process
1. Create namespace (first-time contributors)
2. Generate module/template files using scripts
3. Implement functionality and tests
4. Run formatting and validation
5. Submit PR with appropriate template
### Testing Workflow
- All modules must pass `terraform test`
- Use `bun run test` for comprehensive testing
- Format code with `bun run fmt` before submission
- Manual testing recommended for templates
### Namespace Management
- Each contributor gets unique namespace
- Namespace avatar required (avatar.png/svg in .images/)
- Namespace README with contributor info and frontmatter
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+19 -8
View File
@@ -124,18 +124,23 @@ This script generates:
- Accurate description and usage examples
- Correct icon path (usually `../../../../.icons/your-icon.svg`)
- Proper tags that describe your module
3. **Create at least one `.tftest.hcl`** to test your module with `terraform test`
3. **Create tests for your module:**
- **Terraform tests**: Create a `*.tftest.hcl` file and test with `terraform test`
- **TypeScript tests**: Create `main.test.ts` file if your module runs scripts or has business logic that Terraform tests can't cover
4. **Add any scripts** or additional files your module needs
### 4. Test and Submit
```bash
# Test your module (from the module directory)
# Test your module
cd registry/[namespace]/modules/[module-name]
# Required: Test Terraform functionality
terraform init -upgrade
terraform test -verbose
# Or run all tests in the repo
./scripts/terraform_test_all.sh
# Optional: Test TypeScript files if you have main.test.ts
bun test main.test.ts
# Format code
bun run fmt
@@ -343,8 +348,8 @@ coder templates push test-[template-name] -d .
terraform init -upgrade
terraform test -verbose
# Test all modules
./scripts/terraform_test_all.sh
# Optional: If you have TypeScript tests
bun test main.test.ts
```
### 3. Maintain Backward Compatibility
@@ -393,7 +398,9 @@ Example: `https://github.com/coder/registry/compare/main...your-branch?template=
### Every Module Must Have
- `main.tf` - Terraform code
- One or more `.tftest.hcl` files - Working tests with `terraform test`
- **Tests**:
- `*.tftest.hcl` files with `terraform test` (to test terraform specific logic)
- `main.test.ts` file with `bun test` (to test business logic, i.e., `coder_script` to install a package.)
- `README.md` - Documentation with frontmatter
### Every Template Must Have
@@ -493,6 +500,10 @@ When reporting bugs, include:
2. **No tests** or broken tests
3. **Hardcoded values** instead of variables
4. **Breaking changes** without defaults
5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`) before submitting
5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`, and `bun test main.test.ts` if applicable) before submitting
## For Maintainers
Guidelines for reviewing PRs, managing releases, and maintaining the registry. [See the maintainer guide for detailed information.](./MAINTAINER.md)
Happy contributing! 🚀
+3 -1
View File
@@ -23,6 +23,7 @@ Check that PRs have:
- [ ] Working tests (`terraform test`)
- [ ] Formatted code (`bun run fmt`)
- [ ] Avatar image for new namespaces (`avatar.png` or `avatar.svg` in `.images/`)
- [ ] Version label: `version:patch`, `version:minor`, or `version:major`
### Version Guidelines
@@ -32,7 +33,8 @@ When reviewing PRs, ensure the version change follows semantic versioning:
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
- **Major** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing types)
PRs should clearly indicate the version change (e.g., `v1.2.3 → v1.2.4`).
PRs should clearly indicate the intended version change (e.g., `v1.2.3 → v1.2.4`) and include the appropriate label: `version:patch`, `version:minor`, or `version:major`.
The “Version Bump” CI uses this label to validate required updates (README version refs, etc.).
### Validate READMEs
+4
View File
@@ -48,3 +48,7 @@ Simply include that snippet inside your Coder template, defining any data depend
## Contributing
We are always accepting new contributions. [Please see our contributing guide for more information.](./CONTRIBUTING.md)
## For Maintainers
Guidelines for maintainers reviewing PRs and managing releases. [See the maintainer guide for more information.](./MAINTAINER.md)
+6 -6
View File
@@ -4,11 +4,11 @@
"": {
"name": "registry",
"devDependencies": {
"@types/bun": "^1.2.18",
"bun-types": "^1.2.18",
"@types/bun": "^1.2.21",
"bun-types": "^1.2.21",
"dedent": "^1.6.0",
"gray-matter": "^4.0.3",
"marked": "^16.0.0",
"marked": "^16.2.0",
"prettier": "^3.6.2",
"prettier-plugin-sh": "^0.18.0",
"prettier-plugin-terraform-formatter": "^1.2.1",
@@ -21,7 +21,7 @@
"packages": {
"@reteps/dockerfmt": ["@reteps/dockerfmt@0.3.6", "", {}, "sha512-Tb5wIMvBf/nLejTQ61krK644/CEMB/cpiaIFXqGApfGqO3GwcR3qnI0DbmkFVCl2OyEp8LnLX3EkucoL0+tbFg=="],
"@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
"@types/node": ["@types/node@24.0.14", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw=="],
@@ -29,7 +29,7 @@
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
@@ -47,7 +47,7 @@
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"marked": ["marked@16.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-MUKMXDjsD/eptB7GPzxo4xcnLS6oo7/RHimUMHEDRhUooPwmN9BEpMl7AEOJv3bmso169wHI2wUF9VQgL7zfmA=="],
"marked": ["marked@16.2.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
+3
View File
@@ -94,6 +94,9 @@ func validateCoderModuleReadme(rm coderResourceReadme) []error {
for _, err := range validateCoderModuleReadmeBody(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateResourceGfmAlerts(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if fmErrs := validateCoderResourceFrontmatter("modules", rm.filePath, rm.frontmatter); len(fmErrs) != 0 {
errs = append(errs, fmErrs...)
}
+76
View File
@@ -1,6 +1,7 @@
package main
import (
"bufio"
"errors"
"net/url"
"os"
@@ -16,11 +17,16 @@ import (
var (
supportedResourceTypes = []string{"modules", "templates"}
operatingSystems = []string{"windows", "macos", "linux"}
gfmAlertTypes = []string{"NOTE", "IMPORTANT", "CAUTION", "WARNING", "TIP"}
// TODO: This is a holdover from the validation logic used by the Coder Modules repo. It gives us some assurance, but
// realistically, we probably want to parse any Terraform code snippets, and make some deeper guarantees about how it's
// structured. Just validating whether it *can* be parsed as Terraform would be a big improvement.
terraformVersionRe = regexp.MustCompile(`^\s*\bversion\s+=`)
// Matches the format "> [!INFO]". Deliberately using a broad pattern to catch formatting issues that can mess up
// the renderer for the Registry website
gfmAlertRegex = regexp.MustCompile(`^>(\s*)\[!(\w+)\](\s*)(.*)`)
)
type coderResourceFrontmatter struct {
@@ -277,3 +283,73 @@ func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
}
return allReadmeFiles, nil
}
func validateResourceGfmAlerts(readmeBody string) []error {
trimmed := strings.TrimSpace(readmeBody)
if trimmed == "" {
return nil
}
var errs []error
var sourceLine string
isInsideGfmQuotes := false
isInsideCodeBlock := false
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
for lineScanner.Scan() {
sourceLine = lineScanner.Text()
if strings.HasPrefix(sourceLine, "```") {
isInsideCodeBlock = !isInsideCodeBlock
continue
}
if isInsideCodeBlock {
continue
}
isInsideGfmQuotes = isInsideGfmQuotes && strings.HasPrefix(sourceLine, "> ")
currentMatch := gfmAlertRegex.FindStringSubmatch(sourceLine)
if currentMatch == nil {
continue
}
// Nested GFM alerts is such a weird mistake that it's probably not really safe to keep trying to process the
// rest of the content, so this will prevent any other validations from happening for the given line
if isInsideGfmQuotes {
errs = append(errs, errors.New("registry does not support nested GFM alerts"))
continue
}
leadingWhitespace := currentMatch[1]
if len(leadingWhitespace) != 1 {
errs = append(errs, errors.New("GFM alerts must have one space between the '>' and the start of the GFM brackets"))
}
isInsideGfmQuotes = true
alertHeader := currentMatch[2]
upperHeader := strings.ToUpper(alertHeader)
if !slices.Contains(gfmAlertTypes, upperHeader) {
errs = append(errs, xerrors.Errorf("GFM alert type %q is not supported", alertHeader))
}
if alertHeader != upperHeader {
errs = append(errs, xerrors.Errorf("GFM alerts must be in all caps"))
}
trailingWhitespace := currentMatch[3]
if trailingWhitespace != "" {
errs = append(errs, xerrors.Errorf("GFM alerts must not have any trailing whitespace after the closing bracket"))
}
extraContent := currentMatch[4]
if extraContent != "" {
errs = append(errs, xerrors.Errorf("GFM alerts must not have any extra content on the same line"))
}
}
if gfmAlertRegex.Match([]byte(sourceLine)) {
errs = append(errs, xerrors.Errorf("README has an incomplete GFM alert at the end of the file"))
}
return errs
}
+3
View File
@@ -70,6 +70,9 @@ func validateCoderTemplateReadme(rm coderResourceReadme) []error {
for _, err := range validateCoderTemplateReadmeBody(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateResourceGfmAlerts(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if fmErrs := validateCoderResourceFrontmatter("templates", rm.filePath, rm.frontmatter); len(fmErrs) != 0 {
errs = append(errs, fmErrs...)
}
+2 -2
View File
@@ -15,7 +15,7 @@ run "app_url_uses_port" {
}
assert {
condition = resource.coder_app.MODULE_NAME.url == "http://localhost:19999"
error_message = "Expected MODULE_NAME app URL to include configured port"
condition = resource.coder_app.module_name.url == "http://localhost:19999"
error_message = "Expected module-name app URL to include configured port"
}
}
+12 -12
View File
@@ -35,13 +35,13 @@ variable "agent_id" {
variable "log_path" {
type = string
description = "The path to log MODULE_NAME to."
default = "/tmp/MODULE_NAME.log"
description = "The path to the module log file."
default = "/tmp/module_name.log"
}
variable "port" {
type = number
description = "The port to run MODULE_NAME on."
description = "The port to run the application on."
default = 19999
}
@@ -59,9 +59,9 @@ variable "order" {
# Add other variables here
resource "coder_script" "MODULE_NAME" {
resource "coder_script" "module_name" {
agent_id = var.agent_id
display_name = "MODULE_NAME"
display_name = "Module Name"
icon = local.icon_url
script = templatefile("${path.module}/run.sh", {
LOG_PATH : var.log_path,
@@ -70,10 +70,10 @@ resource "coder_script" "MODULE_NAME" {
run_on_stop = false
}
resource "coder_app" "MODULE_NAME" {
resource "coder_app" "module_name" {
agent_id = var.agent_id
slug = "MODULE_NAME"
display_name = "MODULE_NAME"
slug = "module-name"
display_name = "Module Name"
url = "http://localhost:${var.port}"
icon = local.icon_url
subdomain = false
@@ -88,10 +88,10 @@ resource "coder_app" "MODULE_NAME" {
}
}
data "coder_parameter" "MODULE_NAME" {
type = "list(string)"
name = "MODULE_NAME"
display_name = "MODULE_NAME"
data "coder_parameter" "module_name" {
type = "string"
name = "module_name"
display_name = "Module Name"
icon = local.icon_url
mutable = var.mutable
default = local.options["Option 1"]["value"]
+33
View File
@@ -0,0 +1,33 @@
---
display_name: NAMESPACE_NAME
bio: Brief description of what this namespace provides
github: your-github-username
avatar: ./.images/avatar.svg
linkedin: https://www.linkedin.com/in/your-profile
website: https://your-website.com
status: community
---
# NAMESPACE_NAME
Brief description of what this namespace provides. Include information about:
- What types of templates/modules you offer
- Your focus areas (e.g., specific cloud providers, technologies)
- Any special features or configurations
## Templates
List your available templates here:
- **template-name**: Brief description
## Modules
List your available modules here:
- **module-name**: Brief description
## Contributing
If you'd like to contribute to this namespace, please [open an issue](https://github.com/coder/registry/issues) or submit a pull request.
+58
View File
@@ -0,0 +1,58 @@
---
name: TEMPLATE_NAME
description: A brief description of what this template does
tags: [tag1, tag2, tag3]
icon: /icon/TEMPLATE_NAME.svg
---
# TEMPLATE_NAME
A brief description of what this template provides and its use case.
## Features
- Feature 1
- Feature 2
- Feature 3
## Requirements
- List any prerequisites or requirements
- Provider-specific requirements (e.g., Docker, AWS credentials)
- Minimum Coder version if applicable
## Usage
1. Step-by-step instructions on how to use this template
2. Any configuration that needs to be done
3. How to customize the template
## Variables
| Name | Description | Type | Default | Required |
| ----------- | --------------------------- | -------- | ----------------- | -------- |
| example_var | Description of the variable | `string` | `"default_value"` | no |
## Resources Created
- List of resources that will be created
- Brief description of each resource
## Customization
Explain how users can customize this template for their needs:
- How to modify the startup script
- How to add additional software
- How to configure different providers
## Troubleshooting
### Common Issues
- Issue 1 and its solution
- Issue 2 and its solution
## Contributing
Contributions are welcome! Please see the [contributing guidelines](../../CONTRIBUTING.md) for more information.
+172
View File
@@ -0,0 +1,172 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
# Add your provider here (e.g., docker, aws, gcp, azure)
# docker = {
# source = "kreuzwerker/docker"
# }
}
}
locals {
username = data.coder_workspace_owner.me.name
}
# Add your variables here
# variable "example_var" {
# default = "default_value"
# description = "Description of the variable"
# type = string
# }
# Configure your provider here
# provider "docker" {
# host = var.docker_socket != "" ? var.docker_socket : null
# }
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
startup_script = <<-EOT
set -e
# Prepare user home with default files on first start.
if [ ! -f ~/.init_done ]; then
cp -rT /etc/skel ~
touch ~/.init_done
fi
# Add any commands that should be executed at workspace startup here
EOT
# These environment variables allow you to make Git commits right away after creating a
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
# You can remove this block if you'd prefer to configure Git manually or using
# dotfiles. (see docs/dotfiles.md)
env = {
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
}
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
# For basic templates, you can remove the "display_apps" block.
metadata {
display_name = "CPU Usage"
key = "0_cpu_usage"
script = "coder stat cpu"
interval = 10
timeout = 1
}
metadata {
display_name = "RAM Usage"
key = "1_ram_usage"
script = "coder stat mem"
interval = 10
timeout = 1
}
metadata {
display_name = "Home Disk"
key = "3_home_disk"
script = "coder stat disk --path $${HOME}"
interval = 60
timeout = 1
}
display_apps {
vscode = true
vscode_insiders = false
ssh_helper = false
port_forwarding_helper = true
web_terminal = true
}
}
# Add your resources here (e.g., docker container, VM, etc.)
# resource "docker_image" "main" {
# name = "codercom/enterprise-base:ubuntu"
# }
# resource "docker_container" "workspace" {
# count = data.coder_workspace.me.start_count
# image = docker_image.main.image_id
# # Uses lower() to avoid Docker restriction on container names.
# name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# # Hostname makes the shell more user friendly: coder@my-workspace:~$
# hostname = data.coder_workspace.me.name
# # Use the docker gateway if the access URL is 127.0.0.1
# entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\.0\.0\.1/", "host.docker.internal")]
# env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
# host {
# host = "host.docker.internal"
# ip = "host-gateway"
# }
# volumes {
# container_path = "/home/${local.username}"
# volume_name = docker_volume.home_volume[0].name
# read_only = false
# }
# # Add labels in Docker to keep track of orphan resources.
# labels {
# label = "coder.owner"
# value = data.coder_workspace_owner.me.name
# }
# labels {
# label = "coder.owner_id"
# value = data.coder_workspace_owner.me.id
# }
# labels {
# label = "coder.workspace_id"
# value = data.coder_workspace.me.id
# }
# labels {
# label = "coder.workspace_name"
# value = data.coder_workspace.me.name
# }
# }
# resource "docker_volume" "home_volume" {
# count = data.coder_workspace.me.start_count
# name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}-home"
# # Protect the volume from being deleted due to changes in attributes.
# lifecycle {
# ignore_changes = all
# }
# # Add labels in Docker to keep track of orphan resources.
# labels {
# label = "coder.owner"
# value = data.coder_workspace_owner.me.name
# }
# labels {
# label = "coder.owner_id"
# value = data.coder_workspace_owner.me.id
# }
# labels {
# label = "coder.workspace_id"
# value = data.coder_workspace.me.id
# }
# labels {
# label = "coder.workspace_name"
# value = data.coder_workspace.me.name
# }
# }
resource "coder_metadata" "workspace_info" {
resource_id = coder_agent.main.id
item {
key = "TEMPLATE_NAME"
value = "TEMPLATE_NAME"
}
}
+5 -5
View File
@@ -1,18 +1,18 @@
{
"name": "registry",
"scripts": {
"fmt": "bun x prettier --write **/*.sh **/*.ts **/*.md *.md && terraform fmt -recursive -diff",
"fmt:ci": "bun x prettier --check **/*.sh **/*.ts **/*.md *.md && terraform fmt -check -recursive -diff",
"fmt": "bun x prettier --write . && terraform fmt -recursive -diff",
"fmt:ci": "bun x prettier --check . && terraform fmt -check -recursive -diff",
"terraform-validate": "./scripts/terraform_validate.sh",
"test": "./scripts/terraform_test_all.sh",
"update-version": "./update-version.sh"
},
"devDependencies": {
"@types/bun": "^1.2.18",
"bun-types": "^1.2.18",
"@types/bun": "^1.2.21",
"bun-types": "^1.2.21",
"dedent": "^1.6.0",
"gray-matter": "^4.0.3",
"marked": "^16.0.0",
"marked": "^16.2.0",
"prettier": "^3.6.2",
"prettier-plugin-sh": "^0.18.0",
"prettier-plugin-terraform-formatter": "^1.2.1"
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

+7
View File
@@ -0,0 +1,7 @@
---
display_name: Jash
bio: Coder user and contributor.
github: AJ0070
avatar: ./.images/avatar.png
status: community
---
+23
View File
@@ -0,0 +1,23 @@
---
display_name: "pgAdmin"
description: "A web-based interface for managing PostgreSQL databases in your Coder workspace."
icon: "../../../../.icons/pgadmin.svg"
maintainer_github: "AJ0070"
verified: false
tags: ["database", "postgres", "pgadmin", "web-ide"]
---
# pgAdmin
This module adds a pgAdmin app to your Coder workspace, providing a powerful web-based interface for managing PostgreSQL databases.
It can be served on a Coder subdomain for easy access, or on `localhost` if you prefer to use port-forwarding.
```tf
module "pgadmin" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/AJ0070/pgadmin/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
@@ -0,0 +1,10 @@
import { describe } from "bun:test";
import { runTerraformInit, testRequiredVariables } from "~test";
describe("pgadmin", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
});
+108
View File
@@ -0,0 +1,108 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
variable "agent_id" {
type = string
description = "The agent to install pgAdmin on."
}
variable "port" {
type = number
description = "The port to run pgAdmin on."
default = 5050
}
variable "subdomain" {
type = bool
description = "If true, the app will be served on a subdomain."
default = true
}
variable "config" {
type = any
description = "A map of pgAdmin configuration settings."
default = {
DEFAULT_EMAIL = "admin@coder.com"
DEFAULT_PASSWORD = "coderPASSWORD"
SERVER_MODE = false
MASTER_PASSWORD_REQUIRED = false
LISTEN_ADDRESS = "127.0.0.1"
}
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_app" "pgadmin" {
count = data.coder_workspace.me.start_count
agent_id = var.agent_id
display_name = "pgAdmin"
slug = "pgadmin"
icon = "/icon/pgadmin.svg"
url = local.url
subdomain = var.subdomain
share = "owner"
healthcheck {
url = local.healthcheck_url
interval = 5
threshold = 6
}
}
resource "coder_script" "pgadmin" {
agent_id = var.agent_id
display_name = "Install and run pgAdmin"
icon = "/icon/pgadmin.svg"
run_on_start = true
script = templatefile("${path.module}/run.sh", {
PORT = var.port,
LOG_PATH = "/tmp/pgadmin.log",
SERVER_BASE_PATH = local.server_base_path,
CONFIG = local.config_content,
PGADMIN_DATA_DIR = local.pgadmin_data_dir,
PGADMIN_LOG_DIR = local.pgadmin_log_dir,
PGADMIN_VENV_DIR = local.pgadmin_venv_dir
})
}
locals {
server_base_path = var.subdomain ? "" : format("/@%s/%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, "pgadmin")
url = "http://localhost:${var.port}${local.server_base_path}"
healthcheck_url = "http://localhost:${var.port}${local.server_base_path}/"
# pgAdmin data directories (user-local paths)
pgadmin_data_dir = "$HOME/.pgadmin"
pgadmin_log_dir = "$HOME/.pgadmin/logs"
pgadmin_venv_dir = "$HOME/.pgadmin/venv"
base_config = merge(var.config, {
LISTEN_PORT = var.port
# Override paths for user installation
DATA_DIR = local.pgadmin_data_dir
LOG_FILE = "${local.pgadmin_log_dir}/pgadmin4.log"
SQLITE_PATH = "${local.pgadmin_data_dir}/pgadmin4.db"
SESSION_DB_PATH = "${local.pgadmin_data_dir}/sessions"
STORAGE_DIR = "${local.pgadmin_data_dir}/storage"
# Disable initial setup prompts for automated deployment
SETUP_AUTH = false
})
config_with_path = var.subdomain ? local.base_config : merge(local.base_config, {
APPLICATION_ROOT = local.server_base_path
})
config_content = join("\n", [
for key, value in local.config_with_path :
format("%s = %s", key,
can(regex("^(true|false)$", tostring(value))) ? (value ? "True" : "False") :
can(tonumber(value)) ? tostring(value) :
format("'%s'", tostring(value))
)
])
}
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
PORT=${PORT}
LOG_PATH=${LOG_PATH}
SERVER_BASE_PATH=${SERVER_BASE_PATH}
BOLD='\033[0;1m'
printf "$${BOLD}Installing pgAdmin!\n"
# Check if Python 3 is available
if ! command -v python3 > /dev/null 2>&1; then
echo "⚠️ Warning: Python 3 is not installed. Please install Python 3 before using this module."
exit 0
fi
# Setup pgAdmin directories (from Terraform configuration)
PGADMIN_DATA_DIR="${PGADMIN_DATA_DIR}"
PGADMIN_LOG_DIR="${PGADMIN_LOG_DIR}"
PGADMIN_VENV_DIR="${PGADMIN_VENV_DIR}"
printf "Setting up pgAdmin directories...\n"
mkdir -p "$PGADMIN_DATA_DIR"
mkdir -p "$PGADMIN_LOG_DIR"
# Check if pgAdmin virtual environment already exists and is working
if [ -f "$PGADMIN_VENV_DIR/bin/pgadmin4" ] && [ -f "$PGADMIN_VENV_DIR/bin/activate" ]; then
printf "🥳 pgAdmin virtual environment already exists\n\n"
else
printf "Creating Python virtual environment for pgAdmin...\n"
if ! python3 -m venv "$PGADMIN_VENV_DIR"; then
echo "⚠️ Warning: Failed to create virtual environment"
exit 0
fi
printf "Installing pgAdmin 4 in virtual environment...\n"
if ! "$PGADMIN_VENV_DIR/bin/pip" install pgadmin4; then
echo "⚠️ Warning: Failed to install pgAdmin4"
exit 0
fi
printf "🥳 pgAdmin has been installed successfully\n\n"
fi
printf "$${BOLD}Configuring pgAdmin...\n"
if [ -f "$PGADMIN_VENV_DIR/bin/pgadmin4" ]; then
# pgAdmin installs to a predictable location in the virtual environment
PYTHON_VERSION=$("$PGADMIN_VENV_DIR/bin/python" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
PGADMIN_INSTALL_DIR="$PGADMIN_VENV_DIR/lib/python$PYTHON_VERSION/site-packages/pgadmin4"
# Create pgAdmin config file in the correct location (next to config.py)
cat > "$PGADMIN_INSTALL_DIR/config_local.py" << EOF
# pgAdmin configuration for Coder workspace
${CONFIG}
EOF
printf "📄 Config written to $PGADMIN_INSTALL_DIR/config_local.py\n"
printf "$${BOLD}Starting pgAdmin in background...\n"
printf "📝 Check logs at $${LOG_PATH}\n"
printf "🌐 Serving at http://localhost:${PORT}${SERVER_BASE_PATH}\n"
# Create required directories
mkdir -p "$PGADMIN_DATA_DIR/sessions"
mkdir -p "$PGADMIN_DATA_DIR/storage"
# Start pgadmin4 from the virtual environment with proper environment
cd "$PGADMIN_DATA_DIR"
PYTHONPATH="$PGADMIN_INSTALL_DIR:$${PYTHONPATH:-}" "$PGADMIN_VENV_DIR/bin/pgadmin4" > "$${LOG_PATH}" 2>&1 &
else
printf "⚠️ Warning: pgAdmin4 virtual environment not found\n"
printf "📝 Installation may have failed - check logs above\n"
fi
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+14
View File
@@ -0,0 +1,14 @@
---
display_name: "Benraouane Soufiane"
bio: "Full stack developer creating awesome things."
avatar: "./.images/avatar.png"
github: "benraouanesoufiane"
linkedin: "https://www.linkedin.com/in/benraouane-soufiane" # Optional
website: "https://benraouanesoufiane.com" # Optional
support_email: "hello@benraouanesoufiane.com" # Optional
status: "community"
---
# Benraouane Soufiane
Full stack developer creating awesome things.
@@ -0,0 +1,82 @@
---
display_name: RustDesk
description: Run RustDesk in your workspace with virtual display
icon: ../../../../.icons/rustdesk.svg
verified: false
tags: [rustdesk, rdp, vm]
---
# RustDesk
Launches RustDesk within your workspace with a virtual display to provide remote desktop access. The module outputs the RustDesk ID and password needed to connect from external RustDesk clients.
```tf
module "rustdesk" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/BenraouaneSoufiane/rustdesk/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
## Features
- Automatically sets up virtual display (Xvfb)
- Downloads and configures RustDesk
- Outputs RustDesk ID and password for easy connection
- Provides external app link to RustDesk web client for browser-based access
- Starts virtual display (Xvfb) with customizable resolution
- Customizable screen resolution and RustDesk version
## Requirements
- Coder v2.5 or higher
- Linux workspace with `apt`, `dnf`, or `yum` package manager
## Examples
### Custom configuration with specific version
```tf
module "rustdesk" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/BenraouaneSoufiane/rustdesk/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
rustdesk_password = "mycustompass"
xvfb_resolution = "1920x1080x24"
rustdesk_version = "1.4.1"
}
```
### Docker container configuration
It requires coder' server to be run as root, when using with Docker, add the following to your `docker_container` resource:
```tf
resource "docker_container" "workspace" {
# ... other configuration ...
user = "root"
privileged = true
network_mode = "host"
ports {
internal = 21115
external = 21115
}
ports {
internal = 21116
external = 21116
}
ports {
internal = 21118
external = 21118
}
ports {
internal = 21119
external = 21119
}
}
```
@@ -0,0 +1,75 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
variable "log_path" {
type = string
description = "The path to log rustdesk to."
default = "/tmp/rustdesk.log"
}
variable "agent_id" {
description = "Attach RustDesk setup to this agent"
type = string
}
variable "order" {
description = "Run order among scripts/apps"
type = number
default = 1
}
# Optional knobs passed as env (you can expose these as variables too)
variable "rustdesk_password" {
description = "If empty, the script will generate one"
type = string
default = ""
sensitive = true
}
variable "xvfb_resolution" {
description = "Xvfb screen size/depth"
type = string
default = "1024x768x16"
}
variable "rustdesk_version" {
description = "RustDesk version to install (use 'latest' for most recent release)"
type = string
default = "latest"
}
resource "coder_script" "rustdesk" {
agent_id = var.agent_id
display_name = "RustDesk"
run_on_start = true
# Prepend env as bash exports, then append the script file literally.
script = <<-EOT
# --- module-provided env knobs ---
export RUSTDESK_PASSWORD="${var.rustdesk_password}"
export XVFB_RESOLUTION="${var.xvfb_resolution}"
export RUSTDESK_VERSION="${var.rustdesk_version}"
# ---------------------------------
${file("${path.module}/run.sh")}
EOT
}
resource "coder_app" "rustdesk" {
agent_id = var.agent_id
slug = "rustdesk"
display_name = "Rustdesk"
url = "https://rustdesk.com/web"
icon = "/icon/rustdesk.svg"
order = var.order
external = true
}
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
BOLD='\033[0;1m'
RESET='\033[0m'
printf "${BOLD}🖥️ Installing RustDesk Remote Desktop\n${RESET}"
# ---- configurable knobs (env overrides) ----
RUSTDESK_VERSION="${RUSTDESK_VERSION:-latest}"
LOG_PATH="${LOG_PATH:-/tmp/rustdesk.log}"
# ---- fetch latest version if needed ----
if [ "$RUSTDESK_VERSION" = "latest" ]; then
printf "🔍 Fetching latest RustDesk version...\n"
RUSTDESK_VERSION=$(curl -s https://api.github.com/repos/rustdesk/rustdesk/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' || echo "1.4.1")
printf "📌 Fetched RustDesk version: ${RUSTDESK_VERSION}\n"
else
printf "📌 Using specified RustDesk version: ${RUSTDESK_VERSION}\n"
fi
XVFB_RESOLUTION="${XVFB_RESOLUTION:-1024x768x16}"
RUSTDESK_PASSWORD="${RUSTDESK_PASSWORD:-}"
# ---- detect package manager & arch ----
ARCH="$(uname -m)"
case "$ARCH" in
x86_64 | amd64) PKG_ARCH="x86_64" ;;
aarch64 | arm64) PKG_ARCH="aarch64" ;;
*)
echo "❌ Unsupported arch: $ARCH"
exit 1
;;
esac
if command -v apt-get > /dev/null 2>&1; then
PKG_SYS="deb"
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.deb"
INSTALL_DEPS='apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y wget libva2 libva-drm2 libva-x11-2 libgstreamer-plugins-base1.0-0 gstreamer1.0-pipewire xfce4 xfce4-goodies xvfb x11-xserver-utils dbus-x11 libegl1 libgl1 libglx0 libglu1-mesa mesa-utils libxrandr2 libxss1 libgtk-3-0t64 libgbm1 libdrm2 libxcomposite1 libxdamage1 libxfixes3'
INSTALL_CMD="apt-get install -y ./${PKG_NAME}"
CLEAN_CMD="rm -f \"${PKG_NAME}\""
elif command -v dnf > /dev/null 2>&1; then
PKG_SYS="rpm"
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.rpm"
INSTALL_DEPS='dnf install -y wget libva libva-intel-driver gstreamer1-plugins-base pipewire xfce4-session xfce4-panel xorg-x11-server-Xvfb xorg-x11-xauth dbus-x11 mesa-libEGL mesa-libGL mesa-libGLU mesa-dri-drivers libXrandr libXScrnSaver gtk3 mesa-libgbm libdrm libXcomposite libXdamage libXfixes'
INSTALL_CMD="dnf install -y ./${PKG_NAME}"
CLEAN_CMD="rm -f \"${PKG_NAME}\""
elif command -v yum > /dev/null 2>&1; then
PKG_SYS="rpm"
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.rpm"
INSTALL_DEPS='yum install -y wget libva libva-intel-driver gstreamer1-plugins-base pipewire xfce4-session xfce4-panel xorg-x11-server-Xvfb xorg-x11-xauth dbus-x11 mesa-libEGL mesa-libGL mesa-libGLU mesa-dri-drivers libXrandr libXScrnSaver gtk3 mesa-libgbm libdrm libXcomposite libXdamage libXfixes'
INSTALL_CMD="yum install -y ./${PKG_NAME}"
CLEAN_CMD="rm -f \"${PKG_NAME}\""
else
echo "❌ Unsupported distro: need apt, dnf, or yum."
exit 1
fi
# ---- install rustdesk if missing ----
if ! command -v rustdesk > /dev/null 2>&1; then
printf "📦 Installing dependencies...\n"
sudo bash -c "$INSTALL_DEPS" 2>&1 | tee -a "${LOG_PATH}"
printf "⬇️ Downloading RustDesk ${RUSTDESK_VERSION} (${PKG_SYS}, ${PKG_ARCH})...\n"
URL="https://github.com/rustdesk/rustdesk/releases/download/${RUSTDESK_VERSION}/${PKG_NAME}"
wget -q "$URL" 2>&1 | tee -a "${LOG_PATH}"
printf "🔧 Installing RustDesk...\n"
sudo bash -c "$INSTALL_CMD" 2>&1 | tee -a "${LOG_PATH}"
printf "🧹 Cleaning up...\n"
bash -c "$CLEAN_CMD" 2>&1 | tee -a "${LOG_PATH}"
else
printf "✅ RustDesk already installed\n"
fi
# ---- start virtual display ----
echo "Starting Xvfb with resolution ${XVFB_RESOLUTION}"
Xvfb :99 -screen 0 "${XVFB_RESOLUTION}" >> "${LOG_PATH}" 2>&1 &
export DISPLAY=:99
# Wait for X to be ready
for i in {1..10}; do
if xdpyinfo -display :99 > /dev/null 2>&1; then
echo "X display is ready"
break
fi
sleep 1
done
# ---- create (or accept) password and start rustdesk ----
if [[ -z "${RUSTDESK_PASSWORD}" ]]; then
RUSTDESK_PASSWORD="$(tr -dc 'a-zA-Z0-9@' < /dev/urandom | head -c 10)@97"
fi
echo "Starting XFCE desktop environment..."
xfce4-session >> "${LOG_PATH}" 2>&1 &
echo "Waiting for xfce4-session to initialize..."
sleep 5
printf "🔐 Setting RustDesk password and starting service...\n"
rustdesk >> "${LOG_PATH}" 2>&1 &
sleep 2
rustdesk --password "${RUSTDESK_PASSWORD}" >> "${LOG_PATH}" 2>&1 &
sleep 3
RID="$(rustdesk --get-id 2> /dev/null || echo 'ID_PENDING')"
printf "🥳 RustDesk setup complete!\n\n"
printf "${BOLD}📋 Connection Details:${RESET}\n"
printf " RustDesk ID: ${RID}\n"
printf " RustDesk Password: ${RUSTDESK_PASSWORD}\n"
printf " Display: ${DISPLAY} (${XVFB_RESOLUTION})\n"
printf "\n📝 Logs available at: ${LOG_PATH}\n\n"
echo "Setup script completed successfully. All services running in background."
exit 0
+3 -1
View File
@@ -28,7 +28,9 @@ describe("tmux module", async () => {
// check that the script contains expected lines
expect(scriptResource.script).toContain("Installing tmux");
expect(scriptResource.script).toContain("Installing Tmux Plugin Manager (TPM)");
expect(scriptResource.script).toContain(
"Installing Tmux Plugin Manager (TPM)",
);
expect(scriptResource.script).toContain("tmux configuration created at");
expect(scriptResource.script).toContain("✅ tmux setup complete!");
});
+75 -75
View File
@@ -8,75 +8,75 @@ TMUX_CONFIG="${TMUX_CONFIG}"
# Function to install tmux
install_tmux() {
printf "Checking for tmux installation\n"
printf "Checking for tmux installation\n"
if command -v tmux &> /dev/null; then
printf "tmux is already installed \n\n"
return 0
fi
if command -v tmux &> /dev/null; then
printf "tmux is already installed \n\n"
return 0
fi
printf "Installing tmux \n\n"
printf "Installing tmux \n\n"
# Detect package manager and install tmux
if command -v apt-get &> /dev/null; then
sudo apt-get update
sudo apt-get install -y tmux
elif command -v yum &> /dev/null; then
sudo yum install -y tmux
elif command -v dnf &> /dev/null; then
sudo dnf install -y tmux
elif command -v zypper &> /dev/null; then
sudo zypper install -y tmux
elif command -v apk &> /dev/null; then
sudo apk add tmux
elif command -v brew &> /dev/null; then
brew install tmux
else
printf "No supported package manager found. Please install tmux manually. \n"
exit 1
fi
# Detect package manager and install tmux
if command -v apt-get &> /dev/null; then
sudo apt-get update
sudo apt-get install -y tmux
elif command -v yum &> /dev/null; then
sudo yum install -y tmux
elif command -v dnf &> /dev/null; then
sudo dnf install -y tmux
elif command -v zypper &> /dev/null; then
sudo zypper install -y tmux
elif command -v apk &> /dev/null; then
sudo apk add tmux
elif command -v brew &> /dev/null; then
brew install tmux
else
printf "No supported package manager found. Please install tmux manually. \n"
exit 1
fi
printf "tmux installed successfully \n"
printf "tmux installed successfully \n"
}
# Function to install Tmux Plugin Manager (TPM)
install_tpm() {
local tpm_dir="$HOME/.tmux/plugins/tpm"
local tpm_dir="$HOME/.tmux/plugins/tpm"
if [ -d "$tpm_dir" ]; then
printf "TPM is already installed"
return 0
fi
if [ -d "$tpm_dir" ]; then
printf "TPM is already installed"
return 0
fi
printf "Installing Tmux Plugin Manager (TPM) \n"
printf "Installing Tmux Plugin Manager (TPM) \n"
# Create plugins directory
mkdir -p "$HOME/.tmux/plugins"
# Create plugins directory
mkdir -p "$HOME/.tmux/plugins"
# Clone TPM repository
if command -v git &> /dev/null; then
git clone https://github.com/tmux-plugins/tpm "$tpm_dir"
printf "TPM installed successfully"
else
printf "Git is not installed. Please install git to use tmux plugins. \n"
exit 1
fi
# Clone TPM repository
if command -v git &> /dev/null; then
git clone https://github.com/tmux-plugins/tpm "$tpm_dir"
printf "TPM installed successfully"
else
printf "Git is not installed. Please install git to use tmux plugins. \n"
exit 1
fi
}
# Function to create tmux configuration
setup_tmux_config() {
printf "Setting up tmux configuration \n"
printf "Setting up tmux configuration \n"
local config_dir="$HOME/.tmux"
local config_file="$HOME/.tmux.conf"
local config_dir="$HOME/.tmux"
local config_file="$HOME/.tmux.conf"
mkdir -p "$config_dir"
mkdir -p "$config_dir"
if [ -n "$TMUX_CONFIG" ]; then
printf "$TMUX_CONFIG" > "$config_file"
printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n"
else
cat > "$config_file" << EOF
if [ -n "$TMUX_CONFIG" ]; then
printf "$TMUX_CONFIG" > "$config_file"
printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n"
else
cat > "$config_file" << EOF
# Tmux Configuration File
# =============================================================================
@@ -106,48 +106,48 @@ bind C-r run-shell "~/.tmux/plugins/tmux-resurrect/scripts/restore.sh"
# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf)
run '~/.tmux/plugins/tpm/tpm'
EOF
printf "tmux configuration created at {$config_file} \n\n"
fi
printf "tmux configuration created at {$config_file} \n\n"
fi
}
# Function to install tmux plugins
install_plugins() {
printf "Installing tmux plugins"
printf "Installing tmux plugins"
# Check if TPM is installed
if [ ! -d "$HOME/.tmux/plugins/tpm" ]; then
printf "TPM is not installed. Cannot install plugins. \n"
return 1
fi
# Check if TPM is installed
if [ ! -d "$HOME/.tmux/plugins/tpm" ]; then
printf "TPM is not installed. Cannot install plugins. \n"
return 1
fi
# Install plugins using TPM
"$HOME/.tmux/plugins/tpm/bin/install_plugins"
# Install plugins using TPM
"$HOME/.tmux/plugins/tpm/bin/install_plugins"
printf "tmux plugins installed successfully \n"
printf "tmux plugins installed successfully \n"
}
# Main execution
main() {
printf "$${BOLD} 🛠️Setting up tmux with session persistence! \n\n"
printf ""
printf "$${BOLD} 🛠️Setting up tmux with session persistence! \n\n"
printf ""
# Install dependencies
install_tmux
install_tpm
# Install dependencies
install_tmux
install_tpm
# Setup tmux configuration
setup_tmux_config
# Setup tmux configuration
setup_tmux_config
# Install plugins
install_plugins
# Install plugins
install_plugins
printf "$${BOLD}✅ tmux setup complete! \n\n"
printf "$${BOLD}✅ tmux setup complete! \n\n"
printf "$${BOLD} Attempting to restore sessions\n"
tmux new-session -d \; source-file ~/.tmux.conf \; run-shell '~/.tmux/plugins/tmux-resurrect/scripts/restore.sh'
printf "$${BOLD} Sessions restored: -> %s\n" "$(tmux ls)"
printf "$${BOLD} Attempting to restore sessions\n"
tmux new-session -d \; source-file ~/.tmux.conf \; run-shell '~/.tmux/plugins/tmux-resurrect/scripts/restore.sh'
printf "$${BOLD} Sessions restored: -> %s\n" "$(tmux ls)"
}
# Run main function
main
main
@@ -16,7 +16,7 @@ handle_session() {
local session_name="$1"
# Check if the session exists
if tmux has-session -t "$session_name" 2>/dev/null; then
if tmux has-session -t "$session_name" 2> /dev/null; then
echo "Session '$session_name' exists, attaching to it..."
tmux attach-session -t "$session_name"
else
+3 -3
View File
@@ -13,7 +13,7 @@ Run Auggie CLI in your workspace to access Augment's AI coding assistant with ad
```tf
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.1.0"
version = "0.2.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -47,7 +47,7 @@ module "coder-login" {
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.1.0"
version = "0.2.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
@@ -103,7 +103,7 @@ EOF
```tf
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.1.0"
version = "0.2.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
@@ -165,9 +165,9 @@ describe("auggie", async () => {
mcpServers: {
test: {
command: "test-cmd",
type: "stdio"
}
}
type: "stdio",
},
},
});
const { id } = await setup({
moduleVariables: {
@@ -187,13 +187,16 @@ describe("auggie", async () => {
const rules = "Always use TypeScript for new files";
const { id } = await setup({
moduleVariables: {
install_auggie: "false", // Don't need to install auggie to test rules file creation
install_auggie: "false", // Don't need to install auggie to test rules file creation
rules: rules,
},
});
await execModuleScript(id);
const rulesFile = await readFileContainer(id, "/home/coder/.augment/rules.md");
const rulesFile = await readFileContainer(
id,
"/home/coder/.augment/rules.md",
);
expect(rulesFile).toContain(rules);
});
@@ -309,12 +312,15 @@ describe("auggie", async () => {
test("coder-mcp-config-created", async () => {
const { id } = await setup({
moduleVariables: {
install_auggie: "false", // Don't need to install auggie to test MCP config creation
install_auggie: "false", // Don't need to install auggie to test MCP config creation
},
});
await execModuleScript(id);
const mcpConfig = await readFileContainer(id, "/home/coder/.augment/coder_mcp.json");
const mcpConfig = await readFileContainer(
id,
"/home/coder/.augment/coder_mcp.json",
);
expect(mcpConfig).toContain("mcpServers");
expect(mcpConfig).toContain("coder");
expect(mcpConfig).toContain("CODER_MCP_APP_STATUS_SLUG");
+2 -2
View File
@@ -66,7 +66,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.6.0"
default = "v0.10.0"
validation {
condition = can(regex("^v[0-9]+\\.[0-9]+\\.[0-9]+", var.agentapi_version))
error_message = "agentapi_version must be a valid semantic version starting with 'v', like 'v0.3.3'."
@@ -178,7 +178,7 @@ locals {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
@@ -25,7 +25,6 @@ printf "rules: %s\n" "$ARG_AUGGIE_RULES"
echo "--------------------------------"
function check_dependencies() {
if ! command_exists node; then
printf "Error: Node.js is not installed. Please install Node.js manually or use the pre_install_script to install it.\n"
@@ -51,28 +50,27 @@ function install_auggie() {
if [ ! -d "$NPM_GLOBAL_PREFIX" ]; then
mkdir -p "$NPM_GLOBAL_PREFIX"
fi
npm config set prefix "$NPM_GLOBAL_PREFIX"
export PATH="$NPM_GLOBAL_PREFIX/bin:$PATH"
if [ -n "$ARG_AUGGIE_VERSION" ]; then
npm install -g "@augmentcode/auggie@$ARG_AUGGIE_VERSION"
else
npm install -g "@augmentcode/auggie"
fi
if ! grep -q "export PATH=\"\$HOME/.npm-global/bin:\$PATH\"" "$HOME/.bashrc"; then
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> "$HOME/.bashrc"
fi
printf "%s Successfully installed Auggie CLI. Version: %s\n" "${BOLD}" "$(auggie --version)"
else
printf "Skipping Auggie CLI installation (install_auggie=false)\n"
fi
}
function create_coder_mcp() {
AUGGIE_CODER_MCP_FILE="$HOME/.augment/coder_mcp.json"
CODER_MCP=$(
@@ -39,7 +39,6 @@ printf "report_tasks: %s\n" "$ARG_REPORT_TASKS"
echo "--------------------------------"
function validate_auggie_installation() {
if command_exists auggie; then
printf "Auggie is installed\n"
+4 -4
View File
@@ -13,7 +13,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
openai_api_key = var.openai_api_key
folder = "/home/coder/project"
@@ -33,7 +33,7 @@ module "codex" {
module "codex" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/codex/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
openai_api_key = "..."
folder = "/home/coder/project"
@@ -60,7 +60,7 @@ module "coder-login" {
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
openai_api_key = "..."
ai_prompt = data.coder_parameter.ai_prompt.value
@@ -106,7 +106,7 @@ For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_se
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "2.0.0"
version = "2.1.1"
# ... other variables ...
# Override default configuration
+19 -16
View File
@@ -124,8 +124,8 @@ describe("codex", async () => {
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
expect(resp).toContain("sandbox_mode = \"danger-full-access\"");
expect(resp).toContain("preferred_auth_method = \"apikey\"");
expect(resp).toContain('sandbox_mode = "danger-full-access"');
expect(resp).toContain('preferred_auth_method = "apikey"');
expect(resp).toContain("[custom_section]");
expect(resp).toContain("[mcp_servers.Coder]");
});
@@ -221,7 +221,7 @@ describe("codex", async () => {
debug = true
logging_level = "verbose"
`.trim();
const additionalMCP = dedent`
[mcp_servers.CustomTool]
command = "/usr/local/bin/custom-tool"
@@ -235,7 +235,7 @@ describe("codex", async () => {
type = "stdio"
description = "Database query interface"
`.trim();
const { id } = await setup({
moduleVariables: {
base_config_toml: baseConfig,
@@ -244,14 +244,14 @@ describe("codex", async () => {
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
// Check base config
expect(resp).toContain("sandbox_mode = \"read-only\"");
expect(resp).toContain("preferred_auth_method = \"chatgpt\"");
expect(resp).toContain("custom_setting = \"test-value\"");
expect(resp).toContain('sandbox_mode = "read-only"');
expect(resp).toContain('preferred_auth_method = "chatgpt"');
expect(resp).toContain('custom_setting = "test-value"');
expect(resp).toContain("[advanced_settings]");
expect(resp).toContain("logging_level = \"verbose\"");
expect(resp).toContain('logging_level = "verbose"');
// Check MCP servers
expect(resp).toContain("[mcp_servers.Coder]");
expect(resp).toContain("[mcp_servers.CustomTool]");
@@ -268,17 +268,17 @@ describe("codex", async () => {
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
// Check default base config
expect(resp).toContain("sandbox_mode = \"workspace-write\"");
expect(resp).toContain("approval_policy = \"never\"");
expect(resp).toContain('sandbox_mode = "workspace-write"');
expect(resp).toContain('approval_policy = "never"');
expect(resp).toContain("[sandbox_workspace_write]");
expect(resp).toContain("network_access = true");
// Check only Coder MCP server is present
expect(resp).toContain("[mcp_servers.Coder]");
expect(resp).toContain("Report ALL tasks and statuses");
// Ensure no additional MCP servers
const mcpServerCount = (resp.match(/\[mcp_servers\./g) || []).length;
expect(mcpServerCount).toBe(1);
@@ -328,7 +328,10 @@ describe("codex", async () => {
},
});
await execModuleScript(id_2);
const resp_2 = await readFileContainer(id_2, "/home/coder/.codex/AGENTS.md");
const resp_2 = await readFileContainer(
id_2,
"/home/coder/.codex/AGENTS.md",
);
expect(resp_2).toContain(prompt_1);
const count = (resp_2.match(new RegExp(prompt_1, "g")) || []).length;
expect(count).toBe(1);
+3 -2
View File
@@ -80,7 +80,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.5.0"
default = "v0.10.0"
}
variable "codex_model" {
@@ -128,9 +128,10 @@ locals {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = var.agent_id
folder = var.folder
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
@@ -84,8 +84,8 @@ function install_codex() {
}
write_minimal_default_config() {
local config_path="$1"
cat << EOF > "$config_path"
local config_path="$1"
cat << EOF > "$config_path"
# Minimal Default Codex Configuration
sandbox_mode = "workspace-write"
approval_policy = "never"
@@ -98,9 +98,9 @@ EOF
}
append_mcp_servers_section() {
local config_path="$1"
cat << EOF >> "$config_path"
local config_path="$1"
cat << EOF >> "$config_path"
# MCP Servers Configuration
[mcp_servers.Coder]
@@ -112,32 +112,32 @@ type = "stdio"
EOF
if [ -n "$ARG_ADDITIONAL_MCP_SERVERS" ]; then
printf "Adding additional MCP servers\n"
echo "$ARG_ADDITIONAL_MCP_SERVERS" >> "$config_path"
fi
if [ -n "$ARG_ADDITIONAL_MCP_SERVERS" ]; then
printf "Adding additional MCP servers\n"
echo "$ARG_ADDITIONAL_MCP_SERVERS" >> "$config_path"
fi
}
function populate_config_toml() {
CONFIG_PATH="$HOME/.codex/config.toml"
mkdir -p "$(dirname "$CONFIG_PATH")"
if [ -n "$ARG_BASE_CONFIG_TOML" ]; then
printf "Using provided base configuration\n"
echo "$ARG_BASE_CONFIG_TOML" > "$CONFIG_PATH"
else
printf "Using minimal default configuration\n"
write_minimal_default_config "$CONFIG_PATH"
fi
append_mcp_servers_section "$CONFIG_PATH"
CONFIG_PATH="$HOME/.codex/config.toml"
mkdir -p "$(dirname "$CONFIG_PATH")"
if [ -n "$ARG_BASE_CONFIG_TOML" ]; then
printf "Using provided base configuration\n"
echo "$ARG_BASE_CONFIG_TOML" > "$CONFIG_PATH"
else
printf "Using minimal default configuration\n"
write_minimal_default_config "$CONFIG_PATH"
fi
append_mcp_servers_section "$CONFIG_PATH"
}
function add_instruction_prompt_if_exists() {
if [ -n "${ARG_CODEX_INSTRUCTION_PROMPT:-}" ]; then
AGENTS_PATH="$HOME/.codex/AGENTS.md"
printf "Creating AGENTS.md in .codex directory: %s\\n" "${AGENTS_PATH}"
mkdir -p "$HOME/.codex"
if [ -f "${AGENTS_PATH}" ] && grep -Fq "${ARG_CODEX_INSTRUCTION_PROMPT}" "${AGENTS_PATH}"; then
@@ -146,7 +146,7 @@ function add_instruction_prompt_if_exists() {
printf "Appending instruction prompt to AGENTS.md in .codex directory\n"
echo -e "\n${ARG_CODEX_INSTRUCTION_PROMPT}" >> "${AGENTS_PATH}"
fi
if [ ! -d "${ARG_CODEX_START_DIRECTORY}" ]; then
printf "Creating start directory '%s'\\n" "${ARG_CODEX_START_DIRECTORY}"
mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
@@ -55,8 +55,6 @@ if [ -n "$ARG_CODEX_MODEL" ]; then
CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL")
fi
if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
printf "Running the task prompt %s\n" "$ARG_CODEX_TASK_PROMPT"
PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
@@ -65,7 +63,6 @@ else
printf "No task prompt given.\n"
fi
# Terminal dimensions optimized for Coder Tasks UI sidebar:
# - Width 67: fits comfortably in sidebar
# - Height 1190: adjusted due to Codex terminal height bug
@@ -0,0 +1,210 @@
---
display_name: Copilot CLI
description: GitHub Copilot CLI agent for AI-powered terminal assistance
icon: ../../../../.icons/github.svg
verified: false
tags: [agent, copilot, ai, github, tasks]
---
# Copilot
Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-copilot-cli) in your workspace for AI-powered coding assistance directly from the terminal. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for task reporting in the Coder UI.
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
}
```
> [!IMPORTANT]
> This example assumes you have [Coder external authentication](https://coder.com/docs/admin/external-auth) configured with `id = "github"`. If not, you can provide a direct token using the `github_token` variable or provide the correct external authentication id for GitHub by setting `external_auth_id = "my-github"`.
> [!NOTE]
> By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details.
## Prerequisites
- **Node.js v22+** and **npm v10+**
- **[Active Copilot subscription](https://docs.github.com/en/copilot/about-github-copilot/subscription-plans-for-github-copilot)** (GitHub Copilot Pro, Pro+, Business, or Enterprise)
- **GitHub authentication** via one of:
- [Coder external authentication](https://coder.com/docs/admin/external-auth) (recommended)
- Direct token via `github_token` variable
- Interactive login in Copilot
## Examples
### Usage with Tasks
For development environments where you want Copilot to have full access to tools and automatically resume sessions:
```tf
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Initial task prompt for Copilot."
mutable = true
}
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
ai_prompt = data.coder_parameter.ai_prompt.value
copilot_model = "claude-sonnet-4.5"
allow_all_tools = true
resume_session = true
trusted_directories = ["/home/coder/projects", "/tmp"]
}
```
### Advanced Configuration
Customize tool permissions, MCP servers, and Copilot settings:
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
# Version pinning (defaults to "0.0.334", use "latest" for newest version)
copilot_version = "latest"
# Tool permissions
allow_tools = ["shell(git)", "shell(npm)", "write"]
trusted_directories = ["/home/coder/projects", "/tmp"]
# Custom Copilot configuration
copilot_config = jsonencode({
banner = "never"
theme = "dark"
})
# MCP server configuration
mcp_config = jsonencode({
mcpServers = {
filesystem = {
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/home/coder/projects"]
description = "Provides file system access to the workspace"
name = "Filesystem"
timeout = 3000
type = "local"
tools = ["*"]
trust = true
}
playwright = {
command = "npx"
args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated"]
description = "Browser automation for testing and previewing changes"
name = "Playwright"
timeout = 5000
type = "local"
tools = ["*"]
trust = false
}
}
})
# Pre-install Node.js if needed
pre_install_script = <<-EOT
#!/bin/bash
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
EOT
}
```
> [!NOTE]
> GitHub Copilot CLI does not automatically install MCP servers. You have two options:
>
> - Use `npx -y` in the MCP config (shown above) to auto-install on each run
> - Pre-install MCP servers in `pre_install_script` for faster startup (e.g., `npm install -g @modelcontextprotocol/server-filesystem`)
### Direct Token Authentication
Use this example when you want to provide a GitHub Personal Access Token instead of using Coder external auth:
```tf
variable "github_token" {
type = string
description = "GitHub Personal Access Token"
sensitive = true
}
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
github_token = var.github_token
}
```
### Standalone Mode
Run Copilot as a command-line tool without task reporting or web interface. This installs and configures Copilot, making it available as a CLI app in the Coder agent bar that you can launch to interact with Copilot directly from your terminal. Set `report_tasks = false` to disable integration with Coder Tasks.
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
report_tasks = false
cli_app = true
}
```
## Authentication
The module supports multiple authentication methods (in priority order):
1. **[Coder External Auth](https://coder.com/docs/admin/external-auth) (Recommended)** - Automatic if GitHub external auth is configured in Coder
2. **Direct Token** - Pass `github_token` variable (OAuth or Personal Access Token)
3. **Interactive** - Copilot prompts for login via `/login` command if no auth found
> [!NOTE]
> OAuth tokens work best with Copilot. Personal Access Tokens may have limited functionality.
## Session Resumption
By default, the module resumes the latest Copilot session when the workspace restarts. Set `resume_session = false` to always start fresh sessions.
> [!NOTE]
> Session resumption requires persistent storage for the home directory or workspace volume. Without persistent storage, sessions will not resume across workspace restarts.
## Troubleshooting
If you encounter any issues, check the log files in the `~/.copilot-module` directory within your workspace for detailed information.
```bash
# Installation logs
cat ~/.copilot-module/install.log
# Startup logs
cat ~/.copilot-module/agentapi-start.log
# Pre/post install script logs
cat ~/.copilot-module/pre_install.log
cat ~/.copilot-module/post_install.log
```
> [!NOTE]
> To use tasks with Copilot, you must have an active GitHub Copilot subscription.
> The `workdir` variable is required and specifies the directory where Copilot will run.
## References
- [GitHub Copilot CLI Documentation](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)
- [Installing GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)
- [AgentAPI Documentation](https://github.com/coder/agentapi)
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
@@ -0,0 +1,236 @@
run "defaults_are_correct" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
}
assert {
condition = var.copilot_model == "claude-sonnet-4.5"
error_message = "Default model should be 'claude-sonnet-4.5'"
}
assert {
condition = var.report_tasks == true
error_message = "Task reporting should be enabled by default"
}
assert {
condition = var.resume_session == true
error_message = "Session resumption should be enabled by default"
}
assert {
condition = var.allow_all_tools == false
error_message = "allow_all_tools should be disabled by default"
}
assert {
condition = resource.coder_env.mcp_app_status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug env var should be created"
}
assert {
condition = resource.coder_env.mcp_app_status_slug.value == "copilot"
error_message = "Status slug value should be 'copilot'"
}
}
run "github_token_creates_env_var" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
github_token = "test_github_token_abc123"
}
assert {
condition = length(resource.coder_env.github_token) == 1
error_message = "github_token env var should be created when token is provided"
}
assert {
condition = resource.coder_env.github_token[0].name == "GITHUB_TOKEN"
error_message = "github_token env var name should be 'GITHUB_TOKEN'"
}
assert {
condition = resource.coder_env.github_token[0].value == "test_github_token_abc123"
error_message = "github_token env var value should match input"
}
}
run "github_token_not_created_when_empty" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
github_token = ""
}
assert {
condition = length(resource.coder_env.github_token) == 0
error_message = "github_token env var should not be created when empty"
}
}
run "copilot_model_env_var_for_non_default" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
copilot_model = "claude-sonnet-4"
}
assert {
condition = length(resource.coder_env.copilot_model) == 1
error_message = "copilot_model env var should be created for non-default model"
}
assert {
condition = resource.coder_env.copilot_model[0].name == "COPILOT_MODEL"
error_message = "copilot_model env var name should be 'COPILOT_MODEL'"
}
assert {
condition = resource.coder_env.copilot_model[0].value == "claude-sonnet-4"
error_message = "copilot_model env var value should match input"
}
}
run "copilot_model_not_created_for_default" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
copilot_model = "claude-sonnet-4.5"
}
assert {
condition = length(resource.coder_env.copilot_model) == 0
error_message = "copilot_model env var should not be created for default model"
}
}
run "model_validation_accepts_valid_models" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
copilot_model = "gpt-5"
}
assert {
condition = contains(["claude-sonnet-4", "claude-sonnet-4.5", "gpt-5"], var.copilot_model)
error_message = "Model should be one of the valid options"
}
}
run "copilot_config_merges_with_trusted_directories" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
trusted_directories = ["/workspace", "/data"]
}
assert {
condition = length(local.final_copilot_config) > 0
error_message = "final_copilot_config should be computed"
}
# Verify workdir is trimmed of trailing slash
assert {
condition = local.workdir == "/home/coder/project"
error_message = "workdir should be trimmed of trailing slash"
}
}
run "custom_copilot_config_overrides_default" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
copilot_config = jsonencode({
banner = "always"
theme = "dark"
})
}
assert {
condition = var.copilot_config != ""
error_message = "Custom copilot config should be set"
}
assert {
condition = jsondecode(local.final_copilot_config).banner == "always"
error_message = "Custom banner setting should be applied"
}
assert {
condition = jsondecode(local.final_copilot_config).theme == "dark"
error_message = "Custom theme setting should be applied"
}
}
run "trusted_directories_merged_with_custom_config" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
copilot_config = jsonencode({
banner = "always"
theme = "dark"
trusted_folders = ["/custom"]
})
trusted_directories = ["/workspace", "/data"]
}
assert {
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/custom")
error_message = "Custom trusted folder should be included"
}
assert {
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/home/coder/project")
error_message = "Workdir should be included in trusted folders"
}
assert {
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/workspace")
error_message = "trusted_directories should be merged into config"
}
assert {
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/data")
error_message = "All trusted_directories should be merged into config"
}
}
run "app_slug_is_consistent" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
}
assert {
condition = local.app_slug == "copilot"
error_message = "app_slug should be 'copilot'"
}
assert {
condition = local.module_dir_name == ".copilot-module"
error_message = "module_dir_name should be '.copilot-module'"
}
}
@@ -0,0 +1,136 @@
import { describe, expect, it } from "bun:test";
import {
findResourceInstance,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
describe("copilot", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
});
it("creates mcp_app_status_slug env var", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
});
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"mcp_app_status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("copilot");
});
it("creates github_token env var with correct value", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
github_token: "test_token_12345",
});
const githubTokenEnv = findResourceInstance(
state,
"coder_env",
"github_token",
);
expect(githubTokenEnv).toBeDefined();
expect(githubTokenEnv.name).toBe("GITHUB_TOKEN");
expect(githubTokenEnv.value).toBe("test_token_12345");
});
it("does not create github_token env var when empty", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
github_token: "",
});
const githubTokenEnvs = state.resources.filter(
(r) => r.type === "coder_env" && r.name === "github_token",
);
expect(githubTokenEnvs.length).toBe(0);
});
it("creates copilot_model env var for non-default models", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
copilot_model: "claude-sonnet-4",
});
const modelEnv = findResourceInstance(state, "coder_env", "copilot_model");
expect(modelEnv).toBeDefined();
expect(modelEnv.name).toBe("COPILOT_MODEL");
expect(modelEnv.value).toBe("claude-sonnet-4");
});
it("does not create copilot_model env var for default model", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
copilot_model: "claude-sonnet-4.5",
});
const modelEnvs = state.resources.filter(
(r) => r.type === "coder_env" && r.name === "copilot_model",
);
expect(modelEnvs.length).toBe(0);
});
it("creates coder_script resources via agentapi module", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
});
// The agentapi module should create coder_script resources for install and start
const scripts = state.resources.filter((r) => r.type === "coder_script");
expect(scripts.length).toBeGreaterThan(0);
});
it("validates copilot_model accepts valid values", async () => {
// Test valid models don't throw errors
await expect(
runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
copilot_model: "gpt-5",
}),
).resolves.toBeDefined();
await expect(
runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
copilot_model: "claude-sonnet-4.5",
}),
).resolves.toBeDefined();
});
it("merges trusted_directories with custom copilot_config", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder/project",
trusted_directories: JSON.stringify(["/workspace", "/data"]),
copilot_config: JSON.stringify({
banner: "always",
theme: "dark",
trusted_folders: ["/custom"],
}),
});
// Verify that the state was created successfully with the merged config
// The actual merging logic is tested in the .tftest.hcl file
expect(state).toBeDefined();
expect(state.resources).toBeDefined();
});
});
+302
View File
@@ -0,0 +1,302 @@
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."
}
variable "workdir" {
type = string
description = "The folder to run Copilot in."
}
variable "external_auth_id" {
type = string
description = "ID of the GitHub external auth provider configured in Coder."
default = "github"
}
variable "github_token" {
type = string
description = "GitHub OAuth token or Personal Access Token. If provided, this will be used instead of auto-detecting authentication."
default = ""
sensitive = true
}
variable "copilot_model" {
type = string
description = "Model to use. Supported values: claude-sonnet-4, claude-sonnet-4.5 (default), gpt-5."
default = "claude-sonnet-4.5"
validation {
condition = contains(["claude-sonnet-4", "claude-sonnet-4.5", "gpt-5"], var.copilot_model)
error_message = "copilot_model must be one of: claude-sonnet-4, claude-sonnet-4.5, gpt-5."
}
}
variable "copilot_config" {
type = string
description = "Custom Copilot configuration as JSON string. Leave empty to use default configuration with banner disabled, theme set to auto, and workdir as trusted folder."
default = ""
}
variable "ai_prompt" {
type = string
description = "Initial task prompt for programmatic mode."
default = ""
}
variable "system_prompt" {
type = string
description = "The system prompt to use for the Copilot server. Task reporting instructions are automatically added when report_tasks is enabled."
default = "You are a helpful coding assistant that helps developers write, debug, and understand code. Provide clear explanations, follow best practices, and help solve coding problems efficiently."
}
variable "trusted_directories" {
type = list(string)
description = "Additional directories to trust for Copilot operations."
default = []
}
variable "allow_all_tools" {
type = bool
description = "Allow all tools without prompting (equivalent to --allow-all-tools)."
default = false
}
variable "allow_tools" {
type = list(string)
description = "Specific tools to allow: shell(command), write, or MCP_SERVER_NAME."
default = []
}
variable "deny_tools" {
type = list(string)
description = "Specific tools to deny: shell(command), write, or MCP_SERVER_NAME."
default = []
}
variable "mcp_config" {
type = string
description = "Custom MCP server configuration as JSON string."
default = ""
}
variable "install_agentapi" {
type = bool
description = "Whether to install AgentAPI."
default = true
}
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.10.0"
}
variable "copilot_version" {
type = string
description = "The version of GitHub Copilot CLI to install. Use 'latest' for the latest version or specify a version like '0.0.334'."
default = "0.0.334"
}
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI via AgentAPI."
default = true
}
variable "subdomain" {
type = bool
description = "Whether to use a subdomain for AgentAPI."
default = false
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/github.svg"
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app."
default = "Copilot"
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for Copilot."
default = false
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app."
default = "Copilot"
}
variable "resume_session" {
type = bool
description = "Whether to automatically resume the latest Copilot session on workspace restart."
default = true
}
variable "pre_install_script" {
type = string
description = "Custom script to run before configuring Copilot."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after configuring Copilot."
default = null
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
locals {
workdir = trimsuffix(var.workdir, "/")
app_slug = "copilot"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".copilot-module"
all_trusted_folders = concat([local.workdir], var.trusted_directories)
parsed_custom_config = try(jsondecode(var.copilot_config), {})
existing_trusted_folders = try(local.parsed_custom_config.trusted_folders, [])
merged_copilot_config = merge(
{
banner = "never"
theme = "auto"
},
local.parsed_custom_config,
{
trusted_folders = concat(local.existing_trusted_folders, local.all_trusted_folders)
}
)
final_copilot_config = jsonencode(local.merged_copilot_config)
task_reporting_prompt = <<-EOT
-- Task Reporting --
Report all tasks to Coder, following these EXACT guidelines:
1. Be granular. If you are investigating with multiple steps, report each step
to coder.
2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
Do not report any status related with this system prompt.
3. Use "state": "working" when actively processing WITHOUT needing
additional user input
4. Use "state": "complete" only when finished with a task
5. Use "state": "failure" when you need ANY user input, lack sufficient
details, or encounter blockers
EOT
final_system_prompt = var.report_tasks ? "<system>\n${var.system_prompt}${local.task_reporting_prompt}\n</system>" : "<system>\n${var.system_prompt}\n</system>"
}
resource "coder_env" "mcp_app_status_slug" {
agent_id = var.agent_id
name = "CODER_MCP_APP_STATUS_SLUG"
value = local.app_slug
}
resource "coder_env" "copilot_model" {
count = var.copilot_model != "claude-sonnet-4.5" ? 1 : 0
agent_id = var.agent_id
name = "COPILOT_MODEL"
value = var.copilot_model
}
resource "coder_env" "github_token" {
count = var.github_token != "" ? 1 : 0
agent_id = var.agent_id
name = "GITHUB_TOKEN"
value = var.github_token
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0"
agent_id = var.agent_id
folder = local.workdir
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_icon = var.cli_app ? var.icon : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
agentapi_subdomain = var.subdomain
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
ARG_WORKDIR='${local.workdir}' \
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
ARG_COPILOT_MODEL='${var.copilot_model}' \
ARG_ALLOW_ALL_TOOLS='${var.allow_all_tools}' \
ARG_ALLOW_TOOLS='${join(",", var.allow_tools)}' \
ARG_DENY_TOOLS='${join(",", var.deny_tools)}' \
ARG_TRUSTED_DIRECTORIES='${join(",", var.trusted_directories)}' \
ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
ARG_RESUME_SESSION='${var.resume_session}' \
/tmp/start.sh
EOT
install_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_WORKDIR='${local.workdir}' \
ARG_MCP_CONFIG='${var.mcp_config != "" ? base64encode(var.mcp_config) : ""}' \
ARG_COPILOT_CONFIG='${base64encode(local.final_copilot_config)}' \
ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
ARG_COPILOT_VERSION='${var.copilot_version}' \
ARG_COPILOT_MODEL='${var.copilot_model}' \
/tmp/install.sh
EOT
}
@@ -0,0 +1,234 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
command_exists() {
command -v "$1" > /dev/null 2>&1
}
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
ARG_MCP_CONFIG=$(echo -n "${ARG_MCP_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
ARG_COPILOT_CONFIG=$(echo -n "${ARG_COPILOT_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
ARG_COPILOT_VERSION=${ARG_COPILOT_VERSION:-0.0.334}
ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-claude-sonnet-4.5}
validate_prerequisites() {
if ! command_exists node; then
echo "ERROR: Node.js not found. Copilot requires Node.js v22+."
echo "Install with: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs"
exit 1
fi
if ! command_exists npm; then
echo "ERROR: npm not found. Copilot requires npm v10+."
exit 1
fi
node_version=$(node --version | sed 's/v//' | cut -d. -f1)
if [ "$node_version" -lt 22 ]; then
echo "WARNING: Node.js v$node_version detected. Copilot requires v22+."
fi
}
install_copilot() {
if ! command_exists copilot; then
echo "Installing GitHub Copilot CLI (version: ${ARG_COPILOT_VERSION})..."
if [ "$ARG_COPILOT_VERSION" = "latest" ]; then
npm install -g @github/copilot
else
npm install -g "@github/copilot@${ARG_COPILOT_VERSION}"
fi
if ! command_exists copilot; then
echo "ERROR: Failed to install Copilot"
exit 1
fi
echo "GitHub Copilot CLI installed successfully"
else
echo "GitHub Copilot CLI already installed"
fi
}
check_github_authentication() {
echo "Checking GitHub authentication..."
if [ -n "${GITHUB_TOKEN:-}" ]; then
echo "✓ GitHub token provided via module configuration"
return 0
fi
if command_exists coder; then
if coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" > /dev/null 2>&1; then
echo "✓ GitHub OAuth authentication via Coder external auth"
return 0
fi
fi
if command_exists gh && gh auth status > /dev/null 2>&1; then
echo "✓ GitHub OAuth authentication via GitHub CLI"
return 0
fi
echo "⚠ No GitHub authentication detected"
echo " Copilot will prompt for authentication when started"
echo " For seamless experience, configure GitHub external auth in Coder or run 'gh auth login'"
return 0
}
setup_copilot_configurations() {
mkdir -p "$ARG_WORKDIR"
local module_path="$HOME/.copilot-module"
mkdir -p "$module_path"
setup_copilot_config
echo "$ARG_WORKDIR" > "$module_path/trusted_directories"
}
setup_copilot_config() {
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
local copilot_config_dir="$XDG_CONFIG_HOME/.copilot"
local copilot_config_file="$copilot_config_dir/config.json"
local mcp_config_file="$copilot_config_dir/mcp-config.json"
mkdir -p "$copilot_config_dir"
if [ -n "$ARG_COPILOT_CONFIG" ]; then
echo "Setting up Copilot configuration..."
if command_exists jq; then
echo "$ARG_COPILOT_CONFIG" | jq 'del(.mcpServers)' > "$copilot_config_file"
else
echo "$ARG_COPILOT_CONFIG" > "$copilot_config_file"
fi
echo "Setting up MCP server configuration..."
setup_mcp_config "$mcp_config_file"
else
echo "ERROR: No Copilot configuration provided"
exit 1
fi
}
setup_mcp_config() {
local mcp_config_file="$1"
echo '{"mcpServers": {}}' > "$mcp_config_file"
if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
echo "Adding Coder MCP server for task reporting..."
setup_coder_mcp_server "$mcp_config_file"
fi
if [ -n "$ARG_MCP_CONFIG" ]; then
echo "Adding custom MCP servers..."
add_custom_mcp_servers "$mcp_config_file"
fi
echo "MCP configuration completed: $mcp_config_file"
}
setup_coder_mcp_server() {
local mcp_config_file="$1"
local coder_mcp_wrapper_script
coder_mcp_wrapper_script=$(
cat << EOF
#!/usr/bin/env bash
set -e
export CODER_MCP_APP_STATUS_SLUG="${ARG_MCP_APP_STATUS_SLUG}"
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
export CODER_AGENT_URL="${CODER_AGENT_URL}"
export CODER_AGENT_TOKEN="${CODER_AGENT_TOKEN}"
exec coder exp mcp server
EOF
)
echo "$coder_mcp_wrapper_script" > "/tmp/coder-mcp-server.sh"
chmod +x /tmp/coder-mcp-server.sh
local coder_mcp_config
coder_mcp_config=$(
cat << EOF
{
"mcpServers": {
"coder": {
"command": "/tmp/coder-mcp-server.sh",
"args": [],
"description": "CRITICAL: Call this tool immediately after receiving ANY user message to report task status. Rules: (1) Call FIRST before doing work - report what you will do with state='working'. (2) Be granular - report each step separately. (3) State 'working' = actively processing without needing user input. (4) State 'complete' = task 100% finished. (5) State 'failure' = need user input, missing info, or blocked. Example: User says 'fix the bug' -> call with state='working', description='Investigating authentication bug'. When done -> call with state='complete', description='Fixed token validation'. You MUST report on every interaction.",
"name": "Coder",
"timeout": 3000,
"type": "local",
"tools": ["*"],
"trust": true
}
}
}
EOF
)
echo "$coder_mcp_config" > "$mcp_config_file"
}
add_custom_mcp_servers() {
local mcp_config_file="$1"
if command_exists jq; then
local custom_servers
custom_servers=$(echo "$ARG_MCP_CONFIG" | jq '.mcpServers // {}')
local updated_config
updated_config=$(jq --argjson custom "$custom_servers" '.mcpServers += $custom' "$mcp_config_file")
echo "$updated_config" > "$mcp_config_file"
elif command_exists node; then
node -e "
const fs = require('fs');
const existing = JSON.parse(fs.readFileSync('$mcp_config_file', 'utf8'));
const input = JSON.parse(\`$ARG_MCP_CONFIG\`);
const custom = input.mcpServers || {};
existing.mcpServers = {...existing.mcpServers, ...custom};
fs.writeFileSync('$mcp_config_file', JSON.stringify(existing, null, 2));
"
else
echo "WARNING: jq and node not available, cannot merge custom MCP servers"
fi
}
configure_copilot_model() {
if [ -n "$ARG_COPILOT_MODEL" ] && [ "$ARG_COPILOT_MODEL" != "claude-sonnet-4.5" ]; then
echo "Setting Copilot model to: $ARG_COPILOT_MODEL"
copilot config model "$ARG_COPILOT_MODEL" || {
echo "WARNING: Failed to set model via copilot config, will use environment variable fallback"
export COPILOT_MODEL="$ARG_COPILOT_MODEL"
}
fi
}
configure_coder_integration() {
if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
echo "Configuring Copilot task reporting..."
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
echo "✓ Coder MCP server configured for task reporting"
else
echo "Task reporting disabled or no app status slug provided."
export CODER_MCP_APP_STATUS_SLUG=""
export CODER_MCP_AI_AGENTAPI_URL=""
fi
}
validate_prerequisites
install_copilot
check_github_authentication
setup_copilot_configurations
configure_copilot_model
configure_coder_integration
echo "Copilot module setup completed."
@@ -0,0 +1,157 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
export PATH="$HOME/.local/bin:$PATH"
command_exists() {
command -v "$1" > /dev/null 2>&1
}
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-}
ARG_ALLOW_ALL_TOOLS=${ARG_ALLOW_ALL_TOOLS:-false}
ARG_ALLOW_TOOLS=${ARG_ALLOW_TOOLS:-}
ARG_DENY_TOOLS=${ARG_DENY_TOOLS:-}
ARG_TRUSTED_DIRECTORIES=${ARG_TRUSTED_DIRECTORIES:-}
ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
ARG_RESUME_SESSION=${ARG_RESUME_SESSION:-true}
validate_copilot_installation() {
if ! command_exists copilot; then
echo "ERROR: Copilot not installed. Run: npm install -g @github/copilot"
exit 1
fi
}
build_initial_prompt() {
local initial_prompt=""
if [ -n "$ARG_AI_PROMPT" ]; then
if [ -n "$ARG_SYSTEM_PROMPT" ]; then
initial_prompt="$ARG_SYSTEM_PROMPT
$ARG_AI_PROMPT"
else
initial_prompt="$ARG_AI_PROMPT"
fi
fi
echo "$initial_prompt"
}
build_copilot_args() {
COPILOT_ARGS=()
if [ "$ARG_ALLOW_ALL_TOOLS" = "true" ]; then
COPILOT_ARGS+=(--allow-all-tools)
fi
if [ -n "$ARG_ALLOW_TOOLS" ]; then
IFS=',' read -ra ALLOW_ARRAY <<< "$ARG_ALLOW_TOOLS"
for tool in "${ALLOW_ARRAY[@]}"; do
if [ -n "$tool" ]; then
COPILOT_ARGS+=(--allow-tool "$tool")
fi
done
fi
if [ -n "$ARG_DENY_TOOLS" ]; then
IFS=',' read -ra DENY_ARRAY <<< "$ARG_DENY_TOOLS"
for tool in "${DENY_ARRAY[@]}"; do
if [ -n "$tool" ]; then
COPILOT_ARGS+=(--deny-tool "$tool")
fi
done
fi
}
check_existing_session() {
if [ "$ARG_RESUME_SESSION" = "true" ]; then
if copilot --help > /dev/null 2>&1; then
local session_dir="$HOME/.copilot/history-session-state"
if [ -d "$session_dir" ] && [ -n "$(ls "$session_dir"/session_*_*.json 2> /dev/null)" ]; then
echo "Found existing Copilot session. Will continue latest session." >&2
return 0
fi
fi
fi
return 1
}
setup_github_authentication() {
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
echo "Setting up GitHub authentication..."
if [ -n "${GITHUB_TOKEN:-}" ]; then
export GH_TOKEN="$GITHUB_TOKEN"
echo "✓ Using GitHub token from module configuration"
return 0
fi
if command_exists coder; then
local github_token
if github_token=$(coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" 2> /dev/null); then
if [ -n "$github_token" ] && [ "$github_token" != "null" ]; then
export GITHUB_TOKEN="$github_token"
export GH_TOKEN="$github_token"
echo "✓ Using Coder external auth OAuth token"
return 0
fi
fi
fi
if command_exists gh && gh auth status > /dev/null 2>&1; then
echo "✓ Using GitHub CLI OAuth authentication"
return 0
fi
echo "⚠ No GitHub authentication available"
echo " Copilot will prompt for login during first use"
echo " Use the '/login' command in Copilot to authenticate"
return 0
}
start_agentapi() {
echo "Starting in directory: $ARG_WORKDIR"
cd "$ARG_WORKDIR"
build_copilot_args
if check_existing_session; then
echo "Continuing latest Copilot session..."
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue "${COPILOT_ARGS[@]}"
else
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue
fi
else
echo "Starting new Copilot session..."
local initial_prompt
initial_prompt=$(build_initial_prompt)
if [ -n "$initial_prompt" ]; then
echo "Using initial prompt with system context"
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
else
agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot
fi
else
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
else
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot
fi
fi
fi
}
setup_github_authentication
validate_copilot_installation
start_agentapi
@@ -0,0 +1,12 @@
#!/bin/bash
set -euo pipefail
if [[ "$1" == "--version" ]]; then
echo "GitHub Copilot CLI v1.0.0"
exit 0
fi
while true; do
echo "$(date) - Copilot mock running..."
sleep 15
done
@@ -13,7 +13,7 @@ Run the Cursor Agent CLI in your workspace for interactive coding assistance and
```tf
module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder"
version = "0.1.1"
version = "0.2.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -42,7 +42,7 @@ module "coder-login" {
module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder"
version = "0.1.1"
version = "0.2.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
@@ -1,12 +1,22 @@
import { afterEach, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
import {
afterEach,
beforeAll,
describe,
expect,
setDefaultTimeout,
test,
} from "bun:test";
import { execContainer, runTerraformInit, writeFileContainer } from "~test";
import {
execModuleScript,
expectAgentAPIStarted,
loadTestFile,
setup as setupUtil
setup as setupUtil,
} from "../../../coder/modules/agentapi/test-util";
import {
setupContainer,
writeExecutable,
} from "../../../coder/modules/agentapi/test-util";
import { setupContainer, writeExecutable } from "../../../coder/modules/agentapi/test-util";
let cleanupFns: (() => Promise<void>)[] = [];
const registerCleanup = (fn: () => Promise<void>) => cleanupFns.push(fn);
@@ -72,11 +82,12 @@ describe("cursor-cli", async () => {
});
test("agentapi-mcp-json", async () => {
const mcpJson = '{"mcpServers": {"test": {"command": "test-cmd", "type": "stdio"}}}';
const mcpJson =
'{"mcpServers": {"test": {"command": "test-cmd", "type": "stdio"}}}';
const { id } = await setup({
moduleVariables: {
mcp: mcpJson,
}
},
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
@@ -99,7 +110,7 @@ describe("cursor-cli", async () => {
const { id } = await setup({
moduleVariables: {
rules_files: JSON.stringify({ "typescript.md": rulesContent }),
}
},
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
@@ -118,7 +129,7 @@ describe("cursor-cli", async () => {
const { id } = await setup({
moduleVariables: {
api_key: apiKey,
}
},
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
@@ -138,7 +149,7 @@ describe("cursor-cli", async () => {
model: model,
force: "true",
ai_prompt: "test prompt",
}
},
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
@@ -158,7 +169,7 @@ describe("cursor-cli", async () => {
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'cursor-pre-install-script'",
post_install_script: "#!/bin/bash\necho 'cursor-post-install-script'",
}
},
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
@@ -183,7 +194,7 @@ describe("cursor-cli", async () => {
const { id } = await setup({
moduleVariables: {
folder: folder,
}
},
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
@@ -205,8 +216,5 @@ describe("cursor-cli", async () => {
expect(resp.exitCode).toBe(0);
await expectAgentAPIStarted(id);
})
});
});
@@ -56,7 +56,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.5.0"
default = "v0.10.0"
}
variable "force" {
@@ -131,7 +131,7 @@ resource "coder_env" "cursor_api_key" {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
@@ -58,7 +58,7 @@ fi
if [ -n "$ARG_AI_PROMPT" ]; then
printf "AI prompt provided\n"
ARGS+=("Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AI_PROMPT")
ARGS+=("Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AI_PROMPT")
fi
# Log and run in background, redirecting all output to the log file
@@ -9,6 +9,6 @@ fi
set -e
while true; do
echo "$(date) - cursor-agent-mock"
sleep 15
done
echo "$(date) - cursor-agent-mock"
sleep 15
done
+5 -5
View File
@@ -13,7 +13,7 @@ Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace
```tf
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
version = "1.1.0"
version = "2.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -30,7 +30,7 @@ module "gemini" {
## Prerequisites
- Node.js and npm will be installed automatically if not present
- **Node.js and npm must be sourced/available before the gemini module installs** - ensure they are installed in your workspace image or via earlier provisioning steps
- The [Coder Login](https://registry.coder.com/modules/coder/coder-login) module is required
## Examples
@@ -46,7 +46,7 @@ variable "gemini_api_key" {
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
version = "1.1.0"
version = "2.1.0"
agent_id = coder_agent.example.id
gemini_api_key = var.gemini_api_key
folder = "/home/coder/project"
@@ -94,7 +94,7 @@ data "coder_parameter" "ai_prompt" {
module "gemini" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/gemini/coder"
version = "1.1.0"
version = "2.1.0"
agent_id = coder_agent.example.id
gemini_api_key = var.gemini_api_key
gemini_model = "gemini-2.5-flash"
@@ -118,7 +118,7 @@ For enterprise users who prefer Google's Vertex AI platform:
```tf
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
version = "1.1.0"
version = "2.1.0"
agent_id = coder_agent.example.id
gemini_api_key = var.gemini_api_key
folder = "/home/coder/project"
+42 -11
View File
@@ -127,7 +127,10 @@ describe("gemini", async () => {
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.gemini/settings.json");
const resp = await readFileContainer(
id,
"/home/coder/.gemini/settings.json",
);
expect(resp).toContain("foo");
expect(resp).toContain("bar");
});
@@ -141,7 +144,10 @@ describe("gemini", async () => {
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.gemini-module/agentapi-start.log");
const resp = await readFileContainer(
id,
"/home/coder/.gemini-module/agentapi-start.log",
);
expect(resp).toContain("Using direct Gemini API with API key");
});
@@ -153,8 +159,11 @@ describe("gemini", async () => {
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.gemini-module/install.log");
expect(resp).toContain('GOOGLE_GENAI_USE_VERTEXAI=\'true\'');
const resp = await readFileContainer(
id,
"/home/coder/.gemini-module/agentapi-start.log",
);
expect(resp).toContain("GOOGLE_GENAI_USE_VERTEXAI='true'");
});
test("gemini-model", async () => {
@@ -166,7 +175,10 @@ describe("gemini", async () => {
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.gemini-module/install.log");
const resp = await readFileContainer(
id,
"/home/coder/.gemini-module/agentapi-start.log",
);
expect(resp).toContain(model);
});
@@ -178,9 +190,15 @@ describe("gemini", async () => {
},
});
await execModuleScript(id);
const preInstallLog = await readFileContainer(id, "/home/coder/.gemini-module/pre_install.log");
const preInstallLog = await readFileContainer(
id,
"/home/coder/.gemini-module/pre_install.log",
);
expect(preInstallLog).toContain("pre-install-script");
const postInstallLog = await readFileContainer(id, "/home/coder/.gemini-module/post_install.log");
const postInstallLog = await readFileContainer(
id,
"/home/coder/.gemini-module/post_install.log",
);
expect(postInstallLog).toContain("post-install-script");
});
@@ -193,7 +211,10 @@ describe("gemini", async () => {
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.gemini-module/install.log");
const resp = await readFileContainer(
id,
"/home/coder/.gemini-module/agentapi-start.log",
);
expect(resp).toContain(folder);
});
@@ -205,7 +226,10 @@ describe("gemini", async () => {
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.gemini/settings.json");
const resp = await readFileContainer(
id,
"/home/coder/.gemini/settings.json",
);
expect(resp).toContain("custom");
expect(resp).toContain("enabled");
});
@@ -232,14 +256,21 @@ describe("gemini", async () => {
await execModuleScript(id, {
GEMINI_TASK_PROMPT: taskPrompt,
});
const resp = await readFileContainer(id, "/home/coder/.gemini-module/agentapi-start.log");
const resp = await readFileContainer(
id,
"/home/coder/.gemini-module/agentapi-start.log",
);
expect(resp).toContain("Running automated task:");
});
test("start-without-prompt", async () => {
const { id } = await setup();
await execModuleScript(id);
const prompt = await execContainer(id, ["ls", "-l", "/home/coder/GEMINI.md"]);
const prompt = await execContainer(id, [
"ls",
"-l",
"/home/coder/GEMINI.md",
]);
expect(prompt.exitCode).not.toBe(0);
expect(prompt.stderr).toContain("No such file or directory");
});
+2 -2
View File
@@ -81,7 +81,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.2.3"
default = "v0.10.0"
}
variable "gemini_model" {
@@ -176,7 +176,7 @@ EOT
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
@@ -1,9 +1,9 @@
#!/bin/bash
BOLD='\033[0;1m'
source "$HOME"/.bashrc
command_exists() {
command -v "$1" >/dev/null 2>&1
command -v "$1" > /dev/null 2>&1
}
set -o nounset
@@ -21,144 +21,132 @@ echo "--------------------------------"
set +o nounset
function install_node() {
if ! command_exists npm; then
printf "npm not found, checking for Node.js installation...\n"
if ! command_exists node; then
printf "Node.js not found, installing Node.js via NVM...\n"
export NVM_DIR="$HOME/.nvm"
if [ ! -d "$NVM_DIR" ]; then
mkdir -p "$NVM_DIR"
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
else
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
fi
function check_dependencies() {
if ! command_exists node; then
printf "Error: Node.js is not installed. Please install Node.js manually or use the pre_install_script to install it.\n"
exit 1
fi
nvm install --lts
nvm use --lts
nvm alias default node
if ! command_exists npm; then
printf "Error: npm is not installed. Please install npm manually or use the pre_install_script to install it.\n"
exit 1
fi
printf "Node.js installed: %s\n" "$(node --version)"
printf "npm installed: %s\n" "$(npm --version)"
else
printf "Node.js is installed but npm is not available. Please install npm manually.\n"
exit 1
fi
fi
printf "Node.js version: %s\n" "$(node --version)"
printf "npm version: %s\n" "$(npm --version)"
}
function install_gemini() {
if [ "${ARG_INSTALL}" = "true" ]; then
install_node
if ! command_exists nvm; then
printf "which node: %s\n" "$(which node)"
printf "which npm: %s\n" "$(which npm)"
mkdir -p "$HOME"/.npm-global
npm config set prefix "$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then
echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc
fi
fi
check_dependencies
printf "%s Installing Gemini CLI\n" "${BOLD}"
NPM_GLOBAL_PREFIX="${HOME}/.npm-global"
if [ ! -d "$NPM_GLOBAL_PREFIX" ]; then
mkdir -p "$NPM_GLOBAL_PREFIX"
fi
npm config set prefix "$NPM_GLOBAL_PREFIX"
export PATH="$NPM_GLOBAL_PREFIX/bin:$PATH"
if [ -n "$ARG_GEMINI_VERSION" ]; then
npm install -g "@google/gemini-cli@$ARG_GEMINI_VERSION"
else
npm install -g "@google/gemini-cli"
fi
if ! grep -q "export PATH=\"\$HOME/.npm-global/bin:\$PATH\"" "$HOME/.bashrc"; then
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> "$HOME/.bashrc"
fi
printf "%s Successfully installed Gemini CLI. Version: %s\n" "${BOLD}" "$(gemini --version)"
fi
}
function populate_settings_json() {
if [ "${ARG_GEMINI_CONFIG}" != "" ]; then
SETTINGS_PATH="$HOME/.gemini/settings.json"
mkdir -p "$(dirname "$SETTINGS_PATH")"
printf "Custom gemini_config is provided !\n"
echo "${ARG_GEMINI_CONFIG}" > "$HOME/.gemini/settings.json"
else
printf "No custom gemini_config provided, using default settings.json.\n"
append_extensions_to_settings_json
fi
if [ "${ARG_GEMINI_CONFIG}" != "" ]; then
SETTINGS_PATH="$HOME/.gemini/settings.json"
mkdir -p "$(dirname "$SETTINGS_PATH")"
printf "Custom gemini_config is provided !\n"
echo "${ARG_GEMINI_CONFIG}" > "$HOME/.gemini/settings.json"
else
printf "No custom gemini_config provided, using default settings.json.\n"
append_extensions_to_settings_json
fi
}
function append_extensions_to_settings_json() {
SETTINGS_PATH="$HOME/.gemini/settings.json"
mkdir -p "$(dirname "$SETTINGS_PATH")"
printf "[append_extensions_to_settings_json] Starting extension merge process...\n"
if [ -z "${BASE_EXTENSIONS:-}" ]; then
printf "[append_extensions_to_settings_json] BASE_EXTENSIONS is empty, skipping merge.\n"
return
fi
if [ ! -f "$SETTINGS_PATH" ]; then
printf "%s does not exist. Creating with merged mcpServers structure.\n" "$SETTINGS_PATH"
ADD_EXT_JSON='{}'
if [ -n "${ADDITIONAL_EXTENSIONS:-}" ]; then
ADD_EXT_JSON="$ADDITIONAL_EXTENSIONS"
fi
printf '{"mcpServers":%s}\n' "$(jq -s 'add' <(echo "$BASE_EXTENSIONS") <(echo "$ADD_EXT_JSON"))" > "$SETTINGS_PATH"
fi
TMP_SETTINGS=$(mktemp)
SETTINGS_PATH="$HOME/.gemini/settings.json"
mkdir -p "$(dirname "$SETTINGS_PATH")"
printf "[append_extensions_to_settings_json] Starting extension merge process...\n"
if [ -z "${BASE_EXTENSIONS:-}" ]; then
printf "[append_extensions_to_settings_json] BASE_EXTENSIONS is empty, skipping merge.\n"
return
fi
if [ ! -f "$SETTINGS_PATH" ]; then
printf "%s does not exist. Creating with merged mcpServers structure.\n" "$SETTINGS_PATH"
ADD_EXT_JSON='{}'
if [ -n "${ADDITIONAL_EXTENSIONS:-}" ]; then
printf "[append_extensions_to_settings_json] ADDITIONAL_EXTENSIONS is set.\n"
ADD_EXT_JSON="$ADDITIONAL_EXTENSIONS"
else
printf "[append_extensions_to_settings_json] ADDITIONAL_EXTENSIONS is empty or not set.\n"
fi
printf '{"mcpServers":%s}\n' "$(jq -s 'add' <(echo "$BASE_EXTENSIONS") <(echo "$ADD_EXT_JSON"))" > "$SETTINGS_PATH"
fi
printf "[append_extensions_to_settings_json] Merging BASE_EXTENSIONS and ADDITIONAL_EXTENSIONS into mcpServers...\n"
jq --argjson base "$BASE_EXTENSIONS" --argjson add "$ADD_EXT_JSON" \
'.mcpServers = (.mcpServers // {} + $base + $add)' \
"$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH"
TMP_SETTINGS=$(mktemp)
ADD_EXT_JSON='{}'
if [ -n "${ADDITIONAL_EXTENSIONS:-}" ]; then
printf "[append_extensions_to_settings_json] ADDITIONAL_EXTENSIONS is set.\n"
ADD_EXT_JSON="$ADDITIONAL_EXTENSIONS"
else
printf "[append_extensions_to_settings_json] ADDITIONAL_EXTENSIONS is empty or not set.\n"
fi
jq '.theme = "Default" | .selectedAuthType = "gemini-api-key"' "$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH"
printf "[append_extensions_to_settings_json] Merging BASE_EXTENSIONS and ADDITIONAL_EXTENSIONS into mcpServers...\n"
jq --argjson base "$BASE_EXTENSIONS" --argjson add "$ADD_EXT_JSON" \
'.mcpServers = (.mcpServers // {} + $base + $add)' \
"$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH"
printf "[append_extensions_to_settings_json] Merge complete.\n"
jq '.theme = "Default" | .selectedAuthType = "gemini-api-key"' "$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH"
printf "[append_extensions_to_settings_json] Merge complete.\n"
}
function add_system_prompt_if_exists() {
if [ -n "${GEMINI_SYSTEM_PROMPT:-}" ]; then
if [ -d "${GEMINI_START_DIRECTORY}" ]; then
printf "Directory '%s' exists. Changing to it.\\n" "${GEMINI_START_DIRECTORY}"
cd "${GEMINI_START_DIRECTORY}" || {
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
exit 1
}
else
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${GEMINI_START_DIRECTORY}"
mkdir -p "${GEMINI_START_DIRECTORY}" || {
printf "Error: Could not create directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
exit 1
}
cd "${GEMINI_START_DIRECTORY}" || {
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
exit 1
}
fi
touch GEMINI.md
printf "Setting GEMINI.md\n"
echo "${GEMINI_SYSTEM_PROMPT}" > GEMINI.md
if [ -n "${GEMINI_SYSTEM_PROMPT:-}" ]; then
if [ -d "${GEMINI_START_DIRECTORY}" ]; then
printf "Directory '%s' exists. Changing to it.\\n" "${GEMINI_START_DIRECTORY}"
cd "${GEMINI_START_DIRECTORY}" || {
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
exit 1
}
else
printf "GEMINI.md is not set.\n"
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${GEMINI_START_DIRECTORY}"
mkdir -p "${GEMINI_START_DIRECTORY}" || {
printf "Error: Could not create directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
exit 1
}
cd "${GEMINI_START_DIRECTORY}" || {
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
exit 1
}
fi
touch GEMINI.md
printf "Setting GEMINI.md\n"
echo "${GEMINI_SYSTEM_PROMPT}" > GEMINI.md
else
printf "GEMINI.md is not set.\n"
fi
}
function configure_mcp() {
export CODER_MCP_APP_STATUS_SLUG="gemini"
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
coder exp mcp configure gemini "${GEMINI_START_DIRECTORY}"
export CODER_MCP_APP_STATUS_SLUG="gemini"
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
coder exp mcp configure gemini "${GEMINI_START_DIRECTORY}"
}
install_gemini
gemini --version
populate_settings_json
add_system_prompt_if_exists
configure_mcp
@@ -5,7 +5,7 @@ set -o pipefail
source "$HOME"/.bashrc
command_exists() {
command -v "$1" >/dev/null 2>&1
command -v "$1" > /dev/null 2>&1
}
if [ -f "$HOME/.nvm/nvm.sh" ]; then
@@ -20,55 +20,55 @@ MODULE_DIR="$HOME/.gemini-module"
mkdir -p "$MODULE_DIR"
if command_exists gemini; then
printf "Gemini is installed\n"
printf "Gemini is installed\n"
else
printf "Error: Gemini is not installed. Please enable install_gemini or install it manually :)\n"
exit 1
printf "Error: Gemini is not installed. Please enable install_gemini or install it manually :)\n"
exit 1
fi
if [ -d "${GEMINI_START_DIRECTORY}" ]; then
printf "Directory '%s' exists. Changing to it.\\n" "${GEMINI_START_DIRECTORY}"
cd "${GEMINI_START_DIRECTORY}" || {
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
exit 1
}
printf "Directory '%s' exists. Changing to it.\\n" "${GEMINI_START_DIRECTORY}"
cd "${GEMINI_START_DIRECTORY}" || {
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
exit 1
}
else
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${GEMINI_START_DIRECTORY}"
mkdir -p "${GEMINI_START_DIRECTORY}" || {
printf "Error: Could not create directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
exit 1
}
cd "${GEMINI_START_DIRECTORY}" || {
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
exit 1
}
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${GEMINI_START_DIRECTORY}"
mkdir -p "${GEMINI_START_DIRECTORY}" || {
printf "Error: Could not create directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
exit 1
}
cd "${GEMINI_START_DIRECTORY}" || {
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
exit 1
}
fi
if [ -n "$GEMINI_TASK_PROMPT" ]; then
printf "Running automated task: %s\n" "$GEMINI_TASK_PROMPT"
PROMPT="Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GEMINI_TASK_PROMPT"
PROMPT_FILE="$MODULE_DIR/prompt.txt"
echo -n "$PROMPT" >"$PROMPT_FILE"
GEMINI_ARGS=(--prompt-interactive "$PROMPT")
printf "Running automated task: %s\n" "$GEMINI_TASK_PROMPT"
PROMPT="Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GEMINI_TASK_PROMPT"
PROMPT_FILE="$MODULE_DIR/prompt.txt"
echo -n "$PROMPT" > "$PROMPT_FILE"
GEMINI_ARGS=(--prompt-interactive "$PROMPT")
else
printf "Starting Gemini CLI in interactive mode.\n"
GEMINI_ARGS=()
printf "Starting Gemini CLI in interactive mode.\n"
GEMINI_ARGS=()
fi
if [ -n "$GEMINI_YOLO_MODE" ] && [ "$GEMINI_YOLO_MODE" = "true" ]; then
printf "YOLO mode enabled - will auto-approve all tool calls\n"
GEMINI_ARGS+=(--yolo)
printf "YOLO mode enabled - will auto-approve all tool calls\n"
GEMINI_ARGS+=(--yolo)
fi
if [ -n "$GEMINI_API_KEY" ] || [ -n "$GOOGLE_API_KEY" ]; then
if [ -n "$GOOGLE_GENAI_USE_VERTEXAI" ] && [ "$GOOGLE_GENAI_USE_VERTEXAI" = "true" ]; then
printf "Using Vertex AI with API key\n"
else
printf "Using direct Gemini API with API key\n"
fi
if [ -n "$GOOGLE_GENAI_USE_VERTEXAI" ] && [ "$GOOGLE_GENAI_USE_VERTEXAI" = "true" ]; then
printf "Using Vertex AI with API key\n"
else
printf "Using direct Gemini API with API key\n"
fi
else
printf "No API key provided (neither GEMINI_API_KEY nor GOOGLE_API_KEY)\n"
printf "No API key provided (neither GEMINI_API_KEY nor GOOGLE_API_KEY)\n"
fi
agentapi server --term-width 67 --term-height 1190 -- \
bash -c "$(printf '%q ' gemini "${GEMINI_ARGS[@]}")"
bash -c "$(printf '%q ' gemini "${GEMINI_ARGS[@]}")"
@@ -0,0 +1,22 @@
---
display_name: Nextflow
description: A module that adds Nextflow to your Coder template.
icon: ../../../../.icons/nextflow.svg
verified: true
tags: [nextflow, workflow, hpc, bioinformatics]
---
# Nextflow
A module that adds Nextflow to your Coder template.
![Nextflow](../../.images/nextflow.png)
```tf
module "nextflow" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/nextflow/coder"
version = "0.9.0"
agent_id = coder_agent.example.id
}
```
@@ -0,0 +1,106 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
# Add required variables for your modules and remove any unneeded variables
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "nextflow_version" {
type = string
description = "Nextflow version"
default = "25.04.7"
}
variable "project_path" {
type = string
description = "The path to Nextflow project, it will be mounted in the container."
}
variable "http_server_port" {
type = number
description = "The port to run HTTP server on."
default = 9876
}
variable "http_server_reports_dir" {
type = string
description = "Subdirectory for HTTP server reports, relative to the project path."
default = "reports"
}
variable "http_server_log_path" {
type = string
description = "HTTP server logs"
default = "/tmp/nextflow_reports.log"
}
variable "stub_run" {
type = bool
description = "Execute a stub run?"
default = false
}
variable "stub_run_command" {
type = string
description = "Nextflow command to be executed in the stub run."
default = "run rnaseq-nf -with-report reports/report.html -with-trace reports/trace.txt -with-timeline reports/timeline.html -with-dag reports/flowchart.png"
}
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 "share" {
type = string
default = "owner"
validation {
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
}
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
resource "coder_script" "nextflow" {
agent_id = var.agent_id
display_name = "nextflow"
icon = "/icon/nextflow.svg"
script = templatefile("${path.module}/run.sh", {
NEXTFLOW_VERSION : var.nextflow_version,
PROJECT_PATH : var.project_path,
HTTP_SERVER_PORT : var.http_server_port,
HTTP_SERVER_REPORTS_DIR : var.http_server_reports_dir,
HTTP_SERVER_LOG_PATH : var.http_server_log_path,
STUB_RUN : var.stub_run,
STUB_RUN_COMMAND : var.stub_run_command,
})
run_on_start = true
}
resource "coder_app" "nextflow" {
agent_id = var.agent_id
slug = "nextflow-reports"
display_name = "Nextflow Reports"
url = "http://localhost:${var.http_server_port}"
icon = "/icon/nextflow.svg"
subdomain = true
share = var.share
order = var.order
group = var.group
}
@@ -0,0 +1,49 @@
#!/usr/bin/env sh
set -eu
BOLD='\033[0;1m'
RESET='\033[0m'
printf "$${BOLD}Starting Nextflow...$${RESET}\n"
if ! command -v nextflow > /dev/null 2>&1; then
# Update system dependencies
sudo apt update
sudo apt install openjdk-21-jdk graphviz salmon fastqc multiqc -y
# Install nextflow
export NXF_VER=${NEXTFLOW_VERSION}
curl -s https://get.nextflow.io | bash
sudo mv nextflow /usr/local/bin/
sudo chmod +x /usr/local/bin/nextflow
# Verify installation
tmp_verify=$(mktemp -d coder-nextflow-XXXXXX)
nextflow run hello \
-with-report "$${tmp_verify}/report.html" \
-with-trace "$${tmp_verify}/trace.txt" \
-with-timeline "$${tmp_verify}/timeline.html" \
-with-dag "$${tmp_verify}/flowchart.png"
rm -r "$${tmp_verify}"
else
echo "Nextflow is already installed\n\n"
fi
if [ ! -z ${PROJECT_PATH} ]; then
# Project is located at PROJECT_PATH
echo "Change directory: ${PROJECT_PATH}"
cd ${PROJECT_PATH}
fi
# Start a web server to preview reports
mkdir -p ${HTTP_SERVER_REPORTS_DIR}
echo "Starting HTTP server in background, check logs: ${HTTP_SERVER_LOG_PATH}"
python3 -m http.server --directory ${HTTP_SERVER_REPORTS_DIR} ${HTTP_SERVER_PORT} > "${HTTP_SERVER_LOG_PATH}" 2>&1 &
# Stub run?
if [ "${STUB_RUN}" = "true" ]; then
nextflow ${STUB_RUN_COMMAND} -stub-run
fi
printf "\n$${BOLD}Nextflow ${NEXTFLOW_VERSION} is ready. HTTP server is listening on port ${HTTP_SERVER_PORT}$${RESET}\n"
@@ -2,7 +2,7 @@
display_name: Amp CLI
icon: ../../../../.icons/sourcegraph-amp.svg
description: Sourcegraph's AI coding agent with deep codebase understanding and intelligent code search capabilities
verified: false
verified: true
tags: [agent, sourcegraph, amp, ai, tasks]
---
@@ -13,7 +13,7 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
```tf
module "amp-cli" {
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "1.0.2"
version = "1.1.0"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
install_sourcegraph_amp = true
@@ -60,7 +60,7 @@ variable "sourcegraph_amp_api_key" {
module "amp-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "1.0.2"
version = "1.1.0"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key # recommended for authenticated usage
install_sourcegraph_amp = true
@@ -69,7 +69,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.3.0"
default = "v0.10.0"
}
variable "pre_install_script" {
@@ -151,7 +151,7 @@ locals {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.0.1"
version = "1.2.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
@@ -1,18 +1,18 @@
FROM ubuntu
RUN apt-get update \
&& apt-get install -y \
curl \
git \
golang \
sudo \
vim \
wget \
&& rm -rf /var/lib/apt/lists/*
&& apt-get install -y \
curl \
git \
golang \
sudo \
vim \
wget \
&& rm -rf /var/lib/apt/lists/*
ARG USER=coder
RUN useradd --groups sudo --no-create-home --shell /bin/bash ${USER} \
&& echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \
&& chmod 0440 /etc/sudoers.d/${USER}
&& echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \
&& chmod 0440 /etc/sudoers.d/${USER}
USER ${USER}
WORKDIR /home/${USER}
@@ -0,0 +1,16 @@
---
display_name: Externally Managed Workspace
description: A template to provision externally managed resources as Coder workspaces
icon: ../../../../.icons/electric-plug-emoji.svg
verified: true
tags: [external]
---
# Externally Managed Workspace Template
> [!IMPORTANT]
> External agents require a [Premium](https://coder.com/pricing) Coder license.
This template provides a minimal scaffolding for creating Coder workspaces that connect to externally provisioned compute resources.
Use this template as a starting point to build your own custom templates for scenarios where you need to connect to existing infrastructure.
@@ -0,0 +1,74 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.10"
}
}
}
data "coder_parameter" "agent_config" {
name = "agent_config"
display_name = "Agent Configuration"
description = "Select the operating system and architecture combination for the agent"
type = "string"
default = "linux-amd64"
option {
name = "Linux AMD64"
value = "linux-amd64"
}
option {
name = "Linux ARM64"
value = "linux-arm64"
}
option {
name = "Linux ARMv7"
value = "linux-armv7"
}
option {
name = "Windows AMD64"
value = "windows-amd64"
}
option {
name = "Windows ARM64"
value = "windows-arm64"
}
option {
name = "macOS AMD64"
value = "darwin-amd64"
}
option {
name = "macOS ARM64 (Apple Silicon)"
value = "darwin-arm64"
}
}
data "coder_workspace" "me" {}
locals {
agent_config = split("-", data.coder_parameter.agent_config.value)
agent_os = local.agent_config[0]
agent_arch = local.agent_config[1]
}
resource "coder_agent" "main" {
arch = local.agent_arch
os = local.agent_os
}
resource "coder_external_agent" "main" {
agent_id = coder_agent.main.id
}
# Adds code-server
# See all available modules at https://registry.coder.com/modules
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
}
@@ -22,31 +22,16 @@ provider "docker" {}
module "claude-code" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/claude-code/coder"
version = "2.0.0"
version = "3.0.0"
agent_id = coder_agent.main.id
folder = "/home/coder/projects"
install_claude_code = true
claude_code_version = "latest"
workdir = "/home/coder/projects"
order = 999
experiment_post_install_script = data.coder_parameter.setup_script.value
# This enables Coder Tasks
experiment_report_tasks = true
}
# You can also use a model provider, like AWS Bedrock or Vertex by replacing
# this with the special env vars from the Claude Code docs.
# see: https://docs.anthropic.com/en/docs/claude-code/third-party-integrations
variable "anthropic_api_key" {
type = string
description = "Generate one at: https://console.anthropic.com/settings/keys"
sensitive = true
}
resource "coder_env" "anthropic_api_key" {
agent_id = coder_agent.main.id
name = "CODER_MCP_CLAUDE_API_KEY"
value = var.anthropic_api_key
claude_api_key = ""
ai_prompt = data.coder_parameter.ai_prompt.value
system_prompt = data.coder_parameter.system_prompt.value
model = "sonnet"
permission_mode = "plan"
post_install_script = data.coder_parameter.setup_script.value
}
# We are using presets to set the prompts, image, and set up instructions
@@ -172,23 +157,6 @@ data "coder_parameter" "preview_port" {
mutable = false
}
# Other variables for Claude Code
resource "coder_env" "claude_task_prompt" {
agent_id = coder_agent.main.id
name = "CODER_MCP_CLAUDE_TASK_PROMPT"
value = data.coder_parameter.ai_prompt.value
}
resource "coder_env" "app_status_slug" {
agent_id = coder_agent.main.id
name = "CODER_MCP_APP_STATUS_SLUG"
value = "ccw"
}
resource "coder_env" "claude_system_prompt" {
agent_id = coder_agent.main.id
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
value = data.coder_parameter.system_prompt.value
}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
@@ -300,13 +268,6 @@ module "code-server" {
order = 1
}
module "vscode" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-desktop/coder"
version = "1.1.0"
agent_id = coder_agent.main.id
}
module "windsurf" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder"
@@ -321,23 +282,13 @@ module "cursor" {
agent_id = coder_agent.main.id
}
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder/projects"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
folder = "/home/coder/projects"
}
resource "docker_volume" "home_volume" {
@@ -422,4 +373,4 @@ resource "docker_container" "workspace" {
label = "coder.workspace_name"
value = data.coder_workspace.me.name
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 976 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

+1 -1
View File
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
```tf
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
+14 -6
View File
@@ -164,7 +164,9 @@ describe("agentapi", async () => {
id,
"/home/coder/test-agentapi-start.log",
);
expect(agentApiStartLog).toContain("Using AGENTAPI_CHAT_BASE_PATH: /@default/default.foo/apps/agentapi-web/chat");
expect(agentApiStartLog).toContain(
"Using AGENTAPI_CHAT_BASE_PATH: /@default/default.foo/apps/agentapi-web/chat",
);
});
test("validate-agentapi-version", async () => {
@@ -186,14 +188,16 @@ describe("agentapi", async () => {
agentapi_version: "v0.0.1",
agentapi_subdomain: "false",
},
shouldThrow: "Running with subdomain = false is only supported by agentapi >= v0.3.3.",
shouldThrow:
"Running with subdomain = false is only supported by agentapi >= v0.3.3.",
},
{
moduleVariables: {
agentapi_version: "v0.3.2",
agentapi_subdomain: "false",
},
shouldThrow: "Running with subdomain = false is only supported by agentapi >= v0.3.3.",
shouldThrow:
"Running with subdomain = false is only supported by agentapi >= v0.3.3.",
},
{
moduleVariables: {
@@ -226,13 +230,17 @@ describe("agentapi", async () => {
agentapi_version: "arbitrary-string-bypasses-validation",
},
shouldThrow: "",
}
},
];
for (const { moduleVariables, shouldThrow } of cases) {
if (shouldThrow) {
expect(setup({ moduleVariables: moduleVariables as Record<string, string> })).rejects.toThrow(shouldThrow);
expect(
setup({ moduleVariables: moduleVariables as Record<string, string> }),
).rejects.toThrow(shouldThrow);
} else {
expect(setup({ moduleVariables: moduleVariables as Record<string, string> })).resolves.toBeDefined();
expect(
setup({ moduleVariables: moduleVariables as Record<string, string> }),
).resolves.toBeDefined();
}
}
});
+1 -1
View File
@@ -117,7 +117,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.3.3"
default = "v0.10.0"
}
variable "agentapi_port" {
@@ -11,22 +11,22 @@ 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
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
echo "Error: agentapi server did not start on port $port after 15 seconds."
exit 1
fi
echo "agentapi server started on port $port."
+47 -47
View File
@@ -17,76 +17,76 @@ AGENTAPI_CHAT_BASE_PATH="${ARG_AGENTAPI_CHAT_BASE_PATH:-}"
set +o nounset
command_exists() {
command -v "$1" >/dev/null 2>&1
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."
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"
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"
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
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
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"
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"
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
@@ -97,5 +97,5 @@ cd "${WORKDIR}"
export AGENTAPI_CHAT_BASE_PATH="${AGENTAPI_CHAT_BASE_PATH:-}"
# Disable host header check since AgentAPI is proxied by Coder (which does its own validation)
export AGENTAPI_ALLOWED_HOSTS="*"
nohup "$module_path/scripts/agentapi-start.sh" true "${AGENTAPI_PORT}" &>"$module_path/agentapi-start.log" &
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}"
+9 -3
View File
@@ -25,14 +25,20 @@ export const setupContainer = async ({
const coderScript = findResourceInstance(state, "coder_script");
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
return {
id, coderScript, cleanup: async () => {
if (process.env["DEBUG"] === "true" || process.env["DEBUG"] === "1" || process.env["DEBUG"] === "yes") {
id,
coderScript,
cleanup: async () => {
if (
process.env["DEBUG"] === "true" ||
process.env["DEBUG"] === "1" ||
process.env["DEBUG"] === "yes"
) {
console.log(`Not removing container ${id} in debug mode`);
console.log(`Run "docker rm -f ${id}" to remove it manually.`);
} else {
await removeContainer(id);
}
}
},
};
};
+4 -1
View File
@@ -7,7 +7,10 @@ const portIdx = args.findIndex((arg) => arg === "--port") + 1;
const port = portIdx ? args[portIdx] : 3284;
console.log(`starting server on port ${port}`);
fs.writeFileSync("/home/coder/agentapi-mock.log", `AGENTAPI_ALLOWED_HOSTS: ${process.env.AGENTAPI_ALLOWED_HOSTS}`);
fs.writeFileSync(
"/home/coder/agentapi-mock.log",
`AGENTAPI_ALLOWED_HOSTS: ${process.env.AGENTAPI_ALLOWED_HOSTS}`,
);
http
.createServer(function (_request, response) {
+6 -6
View File
@@ -8,15 +8,15 @@ 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
echo "using prompt: $use_prompt" >> /home/coder/test-agentapi-start.log
echo "using port: $port" >> /home/coder/test-agentapi-start.log
AGENTAPI_CHAT_BASE_PATH="${AGENTAPI_CHAT_BASE_PATH:-}"
if [ -n "$AGENTAPI_CHAT_BASE_PATH" ]; then
echo "Using AGENTAPI_CHAT_BASE_PATH: $AGENTAPI_CHAT_BASE_PATH" >>/home/coder/test-agentapi-start.log
export AGENTAPI_CHAT_BASE_PATH
echo "Using AGENTAPI_CHAT_BASE_PATH: $AGENTAPI_CHAT_BASE_PATH" >> /home/coder/test-agentapi-start.log
export AGENTAPI_CHAT_BASE_PATH
fi
agentapi server --port "$port" --term-width 67 --term-height 1190 -- \
bash -c aiagent \
>"$log_file_path" 2>&1
bash -c aiagent \
> "$log_file_path" 2>&1
+346 -70
View File
@@ -1,23 +1,26 @@
---
display_name: Amazon Q
description: Run Amazon Q in your workspace to access Amazon's AI coding assistant.
description: Run Amazon Q in your workspace to access Amazon's AI coding assistant with MCP integration and task reporting.
icon: ../../../../.icons/amazon-q.svg
verified: true
tags: [agent, ai, aws, amazon-q]
tags: [agent, ai, aws, amazon-q, tasks]
---
# Amazon Q
Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's AI coding assistant. This module installs and launches Amazon Q, with support for background operation, task reporting, and custom pre/post install scripts.
Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's AI coding assistant. This module provides a complete integration with Coder workspaces, including automatic installation, MCP (Model Context Protocol) integration for task reporting, and support for custom pre/post install scripts.
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.1.2"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
# Required: see below for how to generate
experiment_auth_tarball = var.amazon_q_auth_tarball
# Required: Authentication tarball (see below for generation)
auth_tarball = <<-EOF
base64encoded-tarball
EOF
}
```
@@ -25,97 +28,370 @@ module "amazon-q" {
## Prerequisites
- You must generate an authenticated Amazon Q tarball on another machine:
```sh
cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0
```
Paste the result into the `experiment_auth_tarball` variable.
- To run in the background, your workspace must have `screen` or `tmux` installed.
- **zstd** - Required for compressing the authentication tarball
- **Ubuntu/Debian**: `sudo apt-get install zstd`
- **RHEL/CentOS/Fedora**: `sudo yum install zstd` or `sudo dnf install zstd`
- **auth_tarball** - Required for installation and authentication
<details>
<summary><strong>How to generate the Amazon Q auth tarball (step-by-step)</strong></summary>
### Authentication Tarball
**1. Install and authenticate Amazon Q on your local machine:**
You must generate an authenticated Amazon Q tarball on another machine where you have successfully logged in:
- Download and install Amazon Q from the [official site](https://aws.amazon.com/q/developer/).
- Run `q login` and complete the authentication process in your terminal.
```bash
# 1. Install Amazon Q and login on your local machine
q login
**2. Locate your Amazon Q config directory:**
# 2. Generate the authentication tarball
cd ~/.local/share/amazon-q
tar -c . | zstd | base64 -w 0
```
- The config is typically stored at `~/.local/share/amazon-q`.
Copy the output and use it as the `auth_tarball` variable.
**3. Generate the tarball:**
## Detailed Authentication Setup
- Run the following command in your terminal:
```sh
cd ~/.local/share/amazon-q
tar -c . | zstd | base64 -w 0
```
**Step 1: Install Amazon Q locally**
**4. Copy the output:**
- Download from [AWS Amazon Q Developer](https://aws.amazon.com/q/developer/)
- Follow the installation instructions for your platform
- The command will output a long string. Copy this entire string.
**Step 2: Authenticate**
**5. Paste into your Terraform variable:**
```bash
q login
```
- Assign the string to the `experiment_auth_tarball` variable in your Terraform configuration, for example:
```tf
variable "amazon_q_auth_tarball" {
type = string
default = "PASTE_LONG_STRING_HERE"
}
```
Complete the authentication process in your browser.
**Note:**
**Step 3: Generate tarball**
- You must re-generate the tarball if you log out or re-authenticate Amazon Q on your local machine.
- This process is required for each user who wants to use Amazon Q in their workspace.
```bash
cd ~/.local/share/amazon-q
tar -c . | zstd | base64 -w 0 > /tmp/amazon-q-auth.txt
```
[Reference: Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/generate-docs.html)
</details>
## Examples
### Run Amazon Q in the background with tmux
**Step 4: Use in Terraform**
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.1.2"
agent_id = coder_agent.example.id
experiment_auth_tarball = var.amazon_q_auth_tarball
experiment_use_tmux = true
variable "amazon_q_auth_tarball" {
type = string
sensitive = true
default = "PASTE_YOUR_TARBALL_HERE"
}
```
### Enable task reporting (experimental)
> [!IMPORTANT]
>
> - Regenerate the tarball if you logout or re-authenticate
> - Each user needs their own authentication tarball
> - Keep the tarball secure as it contains authentication credentials
### Coder Tasks Integration
A `coder_parameter` named **'AI Prompt'** is required to enable integration with [Coder Tasks](https://coder.com/docs/ai-coder/tasks).
```tf
data "coder_parameter" "ai_prompt" {
name = "AI Prompt"
display_name = "AI Prompt"
description = "Prompt for the AI task to execute"
type = "string"
mutable = true
default = ""
}
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.1.2"
agent_id = coder_agent.example.id
experiment_auth_tarball = var.amazon_q_auth_tarball
experiment_report_tasks = true
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
ai_prompt = data.coder_parameter.ai_prompt.value
trust_all_tools = true
# Task reporting configuration
report_tasks = true
# Enable CLI app alongside web app
cli_app = true
web_app_display_name = "Amazon Q"
cli_app_display_name = "Q CLI"
}
```
### Run custom scripts before/after install
> [!IMPORTANT]
>
> - The parameter name must be exactly **'AI Prompt'** (case-sensitive)
> - This parameter enables the AI task workflow integration
> - The parameter value is passed to the Amazon Q module via the `ai_prompt` variable
> - Without this parameter, `coder_ai_task` resources will not function properly
>
> **_Security Notice_**
> In order to allow the tasks flow non-interactively all the tools are trusted
> This flag bypasses standard permission checks and allows Amazon Q broader access to your system than normally permitted.
> While this enables more functionality, it also means Amazon Q can potentially execute commands with the same privileges as the user running it.
> Use this module only in trusted environments and be aware of the security implications.
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.1.2"
agent_id = coder_agent.example.id
experiment_auth_tarball = var.amazon_q_auth_tarball
experiment_pre_install_script = "echo Pre-install!"
experiment_post_install_script = "echo Post-install!"
### Default System Prompt
The module includes a simple system prompt that instructs Amazon Q:
```
You are a helpful Coding assistant. Aim to autonomously investigate
and solve issues the user gives you and test your work, whenever possible.
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
but opt for autonomy.
```
You can customize this behavior by providing your own system prompt via the `system_prompt` variable.
### Default Coder MCP Instructions
The module includes specific instructions for the Coder MCP server integration that are separate from the system prompt:
```
YOU MUST REPORT ALL TASKS TO CODER.
When reporting tasks you MUST follow these EXACT instructions:
- IMMEDIATELY report status after receiving ANY user message
- Be granular If you are investigating with multiple steps report each step to coder.
Task state MUST be one of the following:
- Use "state": "working" when actively processing WITHOUT needing additional user input
- Use "state": "complete" only when finished with a task
- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers.
Task summaries MUST:
- Include specifics about what you're doing
- Include clear and actionable steps for the user
- Be less than 160 characters in length
```
You can customize these instructions by providing your own via the `coder_mcp_instructions` variable.
## Default Agent Configuration
The module includes a default agent configuration template that provides a comprehensive setup for Amazon Q integration:
```json
{
"name": "agent",
"description": "This is an default agent config",
"prompt": "${system_prompt}",
"mcpServers": {},
"tools": [
"fs_read",
"fs_write",
"execute_bash",
"use_aws",
"@coder",
"knowledge"
],
"toolAliases": {},
"allowedTools": ["fs_read", "@coder"],
"resources": [
"file://AmazonQ.md",
"file://README.md",
"file://.amazonq/rules/**/*.md"
],
"hooks": {},
"toolsSettings": {},
"useLegacyMcpJson": true
}
```
## Notes
### Configuration Details:
- Only one of `experiment_use_screen` or `experiment_use_tmux` can be true at a time.
- If neither is set, Amazon Q runs in the foreground.
- For more details, see the [main.tf](./main.tf) source.
- **Tools Available:** File operations, bash execution, AWS CLI, Coder MCP integration, and knowledge base access
- **@coder Tool:** Enables Coder MCP integration for task reporting (`coder_report_task` and related tools)
- **Allowed Tools:** By default, only `fs_read` and `@coder` are allowed (can be customized for security)
- **Resources:** Access to documentation and rule files in the workspace
- **MCP Servers:** Empty by default, can be configured via `agent_config` variable
- **System Prompt:** Dynamically populated from the `system_prompt` variable
- **Legacy MCP:** Uses legacy MCP JSON format for compatibility
You can override this configuration by providing your own JSON via the `agent_config` variable.
### Agent Name Configuration
The module automatically extracts the agent name from the `"name"` field in the `agent_config` JSON and uses it for:
- **Configuration File:** Saves the agent config as `~/.aws/amazonq/cli-agents/{agent_name}.json`
- **Default Agent:** Sets the agent as the default using `q settings chat.defaultAgent {agent_name}`
- **MCP Integration:** Associates the Coder MCP server with the specified agent name
If no custom `agent_config` is provided, the default agent name "agent" is used.
## Usage Examples
### Basic Usage
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
}
```
This example will:
1. Download and install Amazon Q CLI v1.14.1
2. Extract authentication tarball to ~/.local/share/amazon-q
3. Configure Coder MCP integration for task reporting
4. Create default agent configuration file
5. Start Amazon Q in /home/coder directory
6. Provide web interface through AgentAPI
> [!IMPORTANT]
> By default `fs_write` tool is not allowed, which will pause the task execution
> an will wait for the prompt to approve it usage.
> To avoid this, and allow the normal task flow, user has two options:
>
> - Change the parameter `trust_all_tools` value to `true` (default to `false`)
> OR
> - Provide you own agent configuration with the tools of your choice allowed
### With Custom AI Prompt
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
trust_all_tools = true
}
```
> [!IMPORTANT]
> **_Security Notice_**
> In order to allow the tasks flow non-interactively all the tools are trusted
> This flag bypasses standard permission checks and allows Amazon Q broader access to your system than normally permitted.
> While this enables more functionality, it also means Amazon Q can potentially execute commands with the same privileges as the user running it.
> Use this module only in trusted environments and be aware of the security implications.
### With Custom Pre/Post Install Scripts
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
pre_install_script = <<-EOT
#!/bin/bash
echo "Setting up custom environment..."
# Install additional dependencies
sudo apt-get update && sudo apt-get install -y zstd
EOT
post_install_script = <<-EOT
#!/bin/bash
echo "Configuring Amazon Q settings..."
# Custom configuration commands
q settings chat.model claude-3-sonnet
EOT
}
```
### Specific Version Installation
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
amazon_q_version = "1.14.0" # Specific version
install_amazon_q = true
}
```
### Custom Agent Configuration
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
agent_config = <<-EOT
{
"name": "custom-agent",
"description": "Custom Amazon Q agent for my workspace",
"prompt": "You are a specialized DevOps assistant...",
"tools": ["fs_read", "fs_write", "execute_bash", "use_aws"]
}
EOT
}
```
### With Custom AgentAPI Configuration
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
# AgentAPI configuration for environments without wildcard access url. https://coder.com/docs/admin/setup#wildcard-access-url
agentapi_chat_based_path = true
agentapi_version = "v0.10.0"
}
```
### Air-Gapped Installation
For environments without direct internet access, you can host Amazon Q installation files internally and configure the module to use your internal repository:
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
# Point to internal artifact repository
q_install_url = "https://artifacts.internal.corp/amazon-q-releases"
# Use specific version available in your repository
amazon_q_version = "1.14.1"
}
```
**Prerequisites for Air-Gapped Setup:**
1. Download Amazon Q installation files from AWS and host them internally
2. Maintain the same directory structure: `{base_url}/{version}/q-{arch}-linux.zip`
3. Ensure both architectures are available:
- `q-x86_64-linux.zip` for Intel/AMD systems
- `q-aarch64-linux.zip` for ARM systems
4. Configure network access from Coder workspaces to your internal repository
## Troubleshooting
### Common Issues
**Authentication issues:**
- Regenerate the auth tarball on your local machine
- Ensure the tarball is properly base64 encoded
- Check that the original authentication is still valid
**MCP integration not working:**
- Verify that AgentAPI is installed (`install_agentapi = true`)
- Check that the Coder agent is properly configured
- Review the system prompt configuration
@@ -0,0 +1,372 @@
run "required_variables" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
}
}
run "minimal_config" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdA==" # base64 "test"
}
assert {
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug environment variable not configured correctly"
}
assert {
condition = resource.coder_env.status_slug.value == "amazonq"
error_message = "Status slug value should be 'amazonq'"
}
}
# Test Case 1: Basic Usage No Autonomous Use of Q
# Using vanilla Kubernetes Deployment Template configuration
run "test_case_1_basic_usage" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
}
# Q is installed and authenticated
assert {
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug environment variable should be configured for basic usage"
}
assert {
condition = resource.coder_env.status_slug.value == "amazonq"
error_message = "Status slug value should be 'amazonq' for basic usage"
}
# AgentAPI is installed and configured (default behavior)
assert {
condition = length(resource.coder_env.auth_tarball) == 1
error_message = "Auth tarball environment variable should be created for authentication"
}
# Foundational configuration applied
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated with foundational configuration"
}
# No additional parameters required (using defaults)
assert {
condition = local.agent_name == "agent"
error_message = "Default agent name should be 'agent' when no custom config provided"
}
}
# Test Case 2: Autonomous Usage Autonomous Use of Q
# AI prompt passed through from external source (Tasks interface or Issue Tracker CI)
run "test_case_2_autonomous_usage" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
}
# Q is installed and authenticated
assert {
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug environment variable should be configured for autonomous usage"
}
assert {
condition = resource.coder_env.status_slug.value == "amazonq"
error_message = "Status slug value should be 'amazonq' for autonomous usage"
}
# AgentAPI is installed and configured
assert {
condition = length(resource.coder_env.auth_tarball) == 1
error_message = "Auth tarball environment variable should be created for autonomous usage"
}
# Foundational configuration for all components applied
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated for autonomous usage"
}
# AI prompt is configured
assert {
condition = local.full_prompt == "Help me set up a Python FastAPI project with proper testing structure"
error_message = "AI prompt should be configured correctly for autonomous usage"
}
# Default agent name when no custom config
assert {
condition = local.agent_name == "agent"
error_message = "Default agent name should be 'agent' for autonomous usage"
}
}
# Test Case 3: Extended Configuration Parameter Validation and File Rendering
# Validates extended configuration options and parameter application
run "test_case_3_extended_configuration" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
amazon_q_version = "1.14.1"
q_install_url = "https://desktop-release.q.us-east-1.amazonaws.com"
install_amazon_q = true
install_agentapi = true
agentapi_version = "v0.6.0"
trust_all_tools = true
ai_prompt = "Help me create a production-grade TypeScript monorepo with testing and deployment"
system_prompt = "You are a helpful software assistant working in a secure enterprise environment"
pre_install_script = "echo 'Pre-install setup'"
post_install_script = "echo 'Post-install cleanup'"
agent_config = jsonencode({
name = "production-agent"
description = "Production Amazon Q agent for enterprise environment"
prompt = "You are a helpful software assistant working in a secure enterprise environment"
mcpServers = {}
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
toolAliases = {}
allowedTools = ["fs_read"]
resources = ["file://AmazonQ.md", "file://README.md", "file://.amazonq/rules/**/*.md"]
hooks = {}
toolsSettings = {}
useLegacyMcpJson = true
})
}
# All installation parameters are applied correctly
assert {
condition = resource.coder_env.status_slug.value == "amazonq"
error_message = "Status slug should be configured correctly with extended parameters"
}
assert {
condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
error_message = "Auth tarball should be configured correctly with extended parameters"
}
# Custom agent configuration is loaded and referenced correctly
assert {
condition = local.agent_name == "production-agent"
error_message = "Agent name should be extracted from custom agent config"
}
assert {
condition = length(local.agent_config) > 0
error_message = "Custom agent config should be processed correctly"
}
# AI prompt and system prompt are configured
assert {
condition = local.full_prompt == "Help me create a production-grade TypeScript monorepo with testing and deployment"
error_message = "AI prompt should be configured correctly in extended configuration"
}
# Pre-install and post-install scripts are provided
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated correctly for extended configuration"
}
}
run "full_config" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
install_amazon_q = true
install_agentapi = true
agentapi_version = "v0.5.0"
amazon_q_version = "latest"
trust_all_tools = true
ai_prompt = "Build a web application"
auth_tarball = "dGVzdA=="
order = 1
group = "AI Tools"
icon = "/icon/custom-amazon-q.svg"
pre_install_script = "echo 'pre-install'"
post_install_script = "echo 'post-install'"
agent_config = jsonencode({
name = "test-agent"
description = "Test agent configuration"
prompt = "You are a helpful AI assistant for testing."
mcpServers = {}
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
toolAliases = {}
allowedTools = ["fs_read"]
resources = ["file://AmazonQ.md", "file://README.md", "file://.amazonq/rules/**/*.md"]
hooks = {}
toolsSettings = {}
useLegacyMcpJson = true
})
}
assert {
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug environment variable not configured correctly"
}
assert {
condition = resource.coder_env.status_slug.value == "amazonq"
error_message = "Status slug value should be 'amazonq'"
}
assert {
condition = length(resource.coder_env.auth_tarball) == 1
error_message = "Auth tarball environment variable should be created when provided"
}
}
run "auth_tarball_environment" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
}
assert {
condition = resource.coder_env.auth_tarball[0].name == "AMAZON_Q_AUTH_TARBALL"
error_message = "Auth tarball environment variable name should be 'AMAZON_Q_AUTH_TARBALL'"
}
assert {
condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
error_message = "Auth tarball environment variable value should match input"
}
}
run "empty_auth_tarball" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = ""
}
assert {
condition = length(resource.coder_env.auth_tarball) == 0
error_message = "Auth tarball environment variable should not be created when empty"
}
}
run "custom_system_prompt" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
system_prompt = "Custom system prompt for testing"
}
# Test that the system prompt is used in the agent config template
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated with custom system prompt"
}
}
run "install_options" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
install_amazon_q = false
install_agentapi = false
}
assert {
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug should still be configured even when install options are disabled"
}
}
run "version_configuration" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
amazon_q_version = "2.15.0"
agentapi_version = "v0.4.0"
}
assert {
condition = resource.coder_env.status_slug.value == "amazonq"
error_message = "Status slug value should remain 'amazonq' regardless of version"
}
}
# Additional test for agent name extraction
run "agent_name_extraction" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
agent_config = jsonencode({
name = "custom-enterprise-agent"
description = "Custom enterprise agent configuration"
prompt = "You are a custom enterprise AI assistant."
mcpServers = {}
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
toolAliases = {}
allowedTools = ["fs_read", "fs_write"]
resources = ["file://README.md"]
hooks = {}
toolsSettings = {}
useLegacyMcpJson = true
})
}
assert {
condition = local.agent_name == "custom-enterprise-agent"
error_message = "Agent name should be extracted correctly from custom agent config"
}
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be processed correctly"
}
}
# Test for JSON encoding validation
run "json_encoding_validation" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
system_prompt = "Multi-line\nsystem prompt\nwith newlines"
}
assert {
condition = length(local.system_prompt) > 0
error_message = "System prompt should be JSON encoded correctly"
}
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated correctly with multi-line system prompt"
}
}
+509 -19
View File
@@ -2,40 +2,530 @@ import { describe, it, expect } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
findResourceInstance,
} from "~test";
import path from "path";
const moduleDir = path.resolve(__dirname);
// Always provide agent_config to bypass template parsing issues
const baseAgentConfig = JSON.stringify({
name: "test-agent",
description: "Test agent configuration",
prompt: "You are a helpful AI assistant.",
mcpServers: {},
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
toolAliases: {},
allowedTools: ["fs_read"],
resources: ["file://README.md", "file://.amazonq/rules/**/*.md"],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
});
const requiredVars = {
agent_id: "dummy-agent-id",
agent_config: baseAgentConfig,
workdir: "/tmp/test-workdir",
};
describe("amazon-q module", async () => {
const fullConfigVars = {
agent_id: "dummy-agent-id",
workdir: "/tmp/test-workdir",
install_amazon_q: true,
install_agentapi: true,
agentapi_version: "v0.6.0",
amazon_q_version: "1.14.1",
q_install_url: "https://desktop-release.q.us-east-1.amazonaws.com",
trust_all_tools: false,
ai_prompt: "Build a comprehensive test suite",
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
order: 1,
group: "AI Tools",
icon: "/icon/custom-amazon-q.svg",
pre_install_script: "echo 'Starting pre-install'",
post_install_script: "echo 'Completed post-install'",
agent_config: baseAgentConfig,
};
describe("amazon-q module v2.0.0", async () => {
await runTerraformInit(moduleDir);
// 1. Required variables
testRequiredVariables(moduleDir, requiredVars);
// Test Case 1: Basic Usage No Autonomous Use of Q
// Matches CDES-203 Test Case #1: Basic Usage
it("Test Case 1: Basic Usage - No Autonomous Use of Q", async () => {
const basicUsageVars = {
agent_id: "dummy-agent-id",
workdir: "/tmp/test-workdir",
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
};
// 2. coder_script resource is created
it("creates coder_script resource", async () => {
const state = await runTerraformApply(moduleDir, requiredVars);
const scriptResource = findResourceInstance(state, "coder_script");
expect(scriptResource).toBeDefined();
expect(scriptResource.agent_id).toBe(requiredVars.agent_id);
// Optionally, check that the script contains expected lines
expect(scriptResource.script).toContain("Installing Amazon Q");
const state = await runTerraformApply(moduleDir, basicUsageVars);
// Q is installed and authenticated
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("amazonq");
// AgentAPI is installed and configured (default behavior)
const authTarballEnv = findResourceInstance(
state,
"coder_env",
"auth_tarball",
);
expect(authTarballEnv).toBeDefined();
expect(authTarballEnv.name).toBe("AMAZON_Q_AUTH_TARBALL");
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
// Foundational configuration for all components is applied
// No additional parameters are required for the module to work
// Using the terminal application and Q chat returns a functional interface
});
// 3. coder_app resource is created
it("creates coder_app resource", async () => {
const state = await runTerraformApply(moduleDir, requiredVars);
const appResource = findResourceInstance(state, "coder_app", "amazon_q");
expect(appResource).toBeDefined();
expect(appResource.agent_id).toBe(requiredVars.agent_id);
// Test Case 2: Autonomous Usage Autonomous Use of Q
// Matches CDES-203 Test Case 2: Autonomous Usage
it("Test Case 2: Autonomous Usage - Autonomous Use of Q", async () => {
const autonomousUsageVars = {
agent_id: "dummy-agent-id",
workdir: "/tmp/test-workdir",
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
ai_prompt:
"Help me set up a Python FastAPI project with proper testing structure",
};
const state = await runTerraformApply(moduleDir, autonomousUsageVars);
// Q is installed and authenticated
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("amazonq");
// AgentAPI is installed and configured
const authTarballEnv = findResourceInstance(
state,
"coder_env",
"auth_tarball",
);
expect(authTarballEnv).toBeDefined();
expect(authTarballEnv.name).toBe("AMAZON_Q_AUTH_TARBALL");
// AI prompt is passed through from external source
// The Chat interface functions as required
// The Tasks interface functions as required
// The template can be invoked from GitHub integration as expected
});
// Add more state-based tests as needed
// Test Case 3: Extended Configuration Parameter Validation and File Rendering
// Matches CDES-203 Test Case 3: Extended Configuration
it("Test Case 3: Extended Configuration - Parameter Validation and File Rendering", async () => {
const extendedConfigVars = {
agent_id: "dummy-agent-id",
workdir: "/tmp/test-workdir",
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
amazon_q_version: "1.14.1",
q_install_url: "https://desktop-release.q.us-east-1.amazonaws.com",
install_amazon_q: true,
install_agentapi: true,
agentapi_version: "v0.6.0",
trust_all_tools: true,
ai_prompt:
"Help me create a production-grade TypeScript monorepo with testing and deployment",
system_prompt:
"You are a helpful software assistant working in a secure enterprise environment",
pre_install_script: "echo 'Pre-install setup'",
post_install_script: "echo 'Post-install cleanup'",
agent_config: JSON.stringify({
name: "production-agent",
description: "Production Amazon Q agent for enterprise environment",
prompt:
"You are a helpful software assistant working in a secure enterprise environment",
mcpServers: {},
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
toolAliases: {},
allowedTools: ["fs_read"],
resources: [
"file://AmazonQ.md",
"file://README.md",
"file://.amazonq/rules/**/*.md",
],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
}),
};
const state = await runTerraformApply(moduleDir, extendedConfigVars);
// All installation steps execute in the correct order
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("amazonq");
// auth_tarball is unpacked and used as expected
const authTarballEnv = findResourceInstance(
state,
"coder_env",
"auth_tarball",
);
expect(authTarballEnv).toBeDefined();
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
// agent_config is rendered correctly, and the name field is used as the agent's name
// The specified ai_prompt and system_prompt are respected by the Q agent
// Tools are trusted globally if trust_all_tools = true
// Files and scripts execute in proper sequence
});
// 1. Basic functionality test (replaces testRequiredVariables)
it("works with required variables", async () => {
const state = await runTerraformApply(moduleDir, requiredVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("amazonq");
});
// 2. Environment variables are created correctly
it("creates required environment variables", async () => {
const state = await runTerraformApply(moduleDir, fullConfigVars);
// Check status slug environment variable
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("amazonq");
// Check auth tarball environment variable
const authTarballEnv = findResourceInstance(
state,
"coder_env",
"auth_tarball",
);
expect(authTarballEnv).toBeDefined();
expect(authTarballEnv.name).toBe("AMAZON_Q_AUTH_TARBALL");
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
});
// 3. Empty auth tarball handling
it("handles empty auth tarball correctly", async () => {
const noAuthVars = {
...requiredVars,
auth_tarball: "",
};
const state = await runTerraformApply(moduleDir, noAuthVars);
// Auth tarball environment variable should not be created when empty
const authTarballEnv = state.resources?.find(
(r) => r.type === "coder_env" && r.name === "auth_tarball",
);
expect(authTarballEnv).toBeUndefined();
});
// 4. Status slug is always created
it("creates status slug environment variable", async () => {
const state = await runTerraformApply(moduleDir, requiredVars);
// Status slug should always be configured
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("amazonq");
});
// 5. Install options configuration
it("respects install option flags", async () => {
const noInstallVars = {
...requiredVars,
install_amazon_q: false,
install_agentapi: false,
};
const state = await runTerraformApply(moduleDir, noInstallVars);
// Status slug should still be configured even when install options are disabled
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.value).toBe("amazonq");
});
// 6. Configurable installation URL
it("uses configurable q_install_url parameter", async () => {
const customUrlVars = {
...requiredVars,
q_install_url: "https://internal-mirror.company.com/amazon-q",
};
const state = await runTerraformApply(moduleDir, customUrlVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 7. Version configuration
it("uses specified versions", async () => {
const versionVars = {
...requiredVars,
amazon_q_version: "1.14.1",
agentapi_version: "v0.6.0",
};
const state = await runTerraformApply(moduleDir, versionVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 8. UI configuration options
it("supports UI customization options", async () => {
const uiCustomVars = {
...requiredVars,
order: 5,
group: "Custom AI Tools",
icon: "/icon/custom-amazon-q-icon.svg",
};
const state = await runTerraformApply(moduleDir, uiCustomVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 9. Pre and post install scripts
it("supports pre and post install scripts", async () => {
const scriptVars = {
...requiredVars,
pre_install_script: "echo 'Pre-install setup'",
post_install_script: "echo 'Post-install cleanup'",
};
const state = await runTerraformApply(moduleDir, scriptVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 10. Valid agent_config JSON with different agent name
it("handles valid agent_config JSON with custom agent name", async () => {
const customAgentConfig = JSON.stringify({
name: "production-agent",
description: "Production Amazon Q agent",
prompt: "You are a production AI assistant.",
mcpServers: {},
tools: ["fs_read", "fs_write"],
toolAliases: {},
allowedTools: ["fs_read"],
resources: ["file://README.md"],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
});
const validAgentConfigVars = {
...requiredVars,
agent_config: customAgentConfig,
};
const state = await runTerraformApply(moduleDir, validAgentConfigVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 11. Air-gapped installation support
it("supports air-gapped installation with custom URL", async () => {
const airGappedVars = {
...requiredVars,
q_install_url: "https://artifacts.internal.corp/amazon-q-releases",
amazon_q_version: "1.14.1",
};
const state = await runTerraformApply(moduleDir, airGappedVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 12. Trust all tools configuration
it("handles trust_all_tools configuration", async () => {
const trustVars = {
...requiredVars,
trust_all_tools: true,
};
const state = await runTerraformApply(moduleDir, trustVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 13. AI prompt configuration
it("handles AI prompt configuration", async () => {
const promptVars = {
...requiredVars,
ai_prompt: "Create a comprehensive test suite for the application",
};
const state = await runTerraformApply(moduleDir, promptVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 14. Agent config with minimal structure
it("handles minimal agent config structure", async () => {
const minimalAgentConfig = JSON.stringify({
name: "minimal-agent",
description: "Minimal agent config",
prompt: "You are a minimal AI assistant.",
mcpServers: {},
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
toolAliases: {},
allowedTools: ["fs_read"],
resources: ["file://README.md"],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
});
const minimalVars = {
...requiredVars,
agent_config: minimalAgentConfig,
};
const state = await runTerraformApply(moduleDir, minimalVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 15. JSON encoding validation for system prompts with newlines
it("handles system prompts with newlines correctly", async () => {
const multilinePromptVars = {
...requiredVars,
system_prompt: "Multi-line\nsystem prompt\nwith newlines",
};
const state = await runTerraformApply(moduleDir, multilinePromptVars);
// Should create the basic resources without JSON parsing errors
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.value).toBe("amazonq");
});
// 16. Agent name extraction from custom config
it("extracts agent name from custom configuration correctly", async () => {
const customNameConfig = JSON.stringify({
name: "enterprise-production-agent",
description: "Enterprise production agent configuration",
prompt: "You are an enterprise production AI assistant.",
mcpServers: {},
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
toolAliases: {},
allowedTools: ["fs_read", "fs_write", "execute_bash"],
resources: ["file://README.md", "file://.amazonq/rules/**/*.md"],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
});
const customNameVars = {
...requiredVars,
agent_config: customNameConfig,
};
const state = await runTerraformApply(moduleDir, customNameVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.value).toBe("amazonq");
});
});
+187 -236
View File
@@ -1,10 +1,12 @@
# Improved amazon-q module main.tf
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 2.7"
}
}
}
@@ -15,7 +17,6 @@ variable "agent_id" {
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
@@ -36,10 +37,63 @@ variable "icon" {
default = "/icon/amazon-q.svg"
}
variable "folder" {
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI via AgentAPI"
default = true
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for Amazon Q"
default = false
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app"
default = "AmazonQ"
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app"
default = "AmazonQ CLI"
}
variable "install_agentapi" {
type = bool
description = "Whether to install AgentAPI."
default = true
}
variable "ai_prompt" {
type = string
description = "The initial task prompt to send to Amazon Q."
default = ""
}
variable "pre_install_script" {
type = string
description = "Optional script to run before installing Amazon Q."
default = null
}
variable "post_install_script" {
type = string
description = "Optional script to run after installing Amazon Q."
default = null
}
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.10.0"
}
variable "workdir" {
type = string
description = "The folder to run Amazon Q in."
default = "/home/coder"
}
variable "install_amazon_q" {
@@ -51,43 +105,19 @@ variable "install_amazon_q" {
variable "amazon_q_version" {
type = string
description = "The version of Amazon Q to install."
default = "latest"
default = "1.14.1"
}
variable "experiment_use_screen" {
type = bool
description = "Whether to use screen for running Amazon Q in the background."
default = false
}
variable "experiment_use_tmux" {
type = bool
description = "Whether to use tmux instead of screen for running Amazon Q in the background."
default = false
}
variable "experiment_report_tasks" {
type = bool
description = "Whether to enable task reporting."
default = false
}
variable "experiment_pre_install_script" {
variable "q_install_url" {
type = string
description = "Custom script to run before installing Amazon Q."
default = null
description = "Base URL for Amazon Q installation downloads."
default = "https://desktop-release.q.us-east-1.amazonaws.com"
}
variable "experiment_post_install_script" {
type = string
description = "Custom script to run after installing Amazon Q."
default = null
}
variable "experiment_auth_tarball" {
type = string
description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/amazon-q directory. After running `q login` on another machine, you may generate it with: `cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0`"
default = "tarball"
variable "trust_all_tools" {
type = bool
description = "Whether to trust all tools in Amazon Q."
default = false
}
variable "system_prompt" {
@@ -98,222 +128,143 @@ variable "system_prompt" {
and solve issues the user gives you and test your work, whenever possible.
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
but opt for autonomy.
YOU MUST REPORT ALL TASKS TO CODER.
When reporting tasks, you MUST follow these EXACT instructions:
- IMMEDIATELY report status after receiving ANY user message.
- Be granular. If you are investigating with multiple steps, report each step to coder.
Task state MUST be one of the following:
- Use "state": "working" when actively processing WITHOUT needing additional user input.
- Use "state": "complete" only when finished with a task.
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
Task summaries MUST:
- Include specifics about what you're doing.
- Include clear and actionable steps for the user.
- Be less than 160 characters in length.
EOT
}
variable "ai_prompt" {
variable "coder_mcp_instructions" {
type = string
description = "The initial task prompt to send to Amazon Q."
default = "Please help me with my coding tasks. I'll provide specific instructions as needed."
description = "Instructions for the Coder MCP server integration. This defines how the agent should report tasks to Coder."
default = <<-EOT
YOU MUST REPORT ALL TASKS TO CODER.
When reporting tasks you MUST follow these EXACT instructions:
- IMMEDIATELY report status after receiving ANY user message
- Be granular If you are investigating with multiple steps report each step to coder.
Task state MUST be one of the following:
- Use "state": "working" when actively processing WITHOUT needing additional user input
- Use "state": "complete" only when finished with a task
- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers.
Task summaries MUST:
- Include specifics about what you're doing
- Include clear and actionable steps for the user
- Be less than 160 characters in length
EOT
}
variable "auth_tarball" {
type = string
description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/amazon-q directory."
default = ""
sensitive = true
}
variable "agent_config" {
type = string
description = "Optional Agent configuration JSON for Amazon Q."
default = null
}
variable "agentapi_chat_based_path" {
type = bool
description = "Whether to use chat-based path for AgentAPI.Required if CODER_WILDCARD_ACCESS_URL is not defined in coder deployment"
default = false
}
# Expose status slug to the agent environment
resource "coder_env" "status_slug" {
agent_id = var.agent_id
name = "CODER_MCP_APP_STATUS_SLUG"
value = local.app_slug
}
# Expose auth tarball as environment variable for install script
resource "coder_env" "auth_tarball" {
count = var.auth_tarball != "" ? 1 : 0
agent_id = var.agent_id
name = "AMAZON_Q_AUTH_TARBALL"
value = var.auth_tarball
}
locals {
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) : ""
full_prompt = <<-EOT
${var.system_prompt}
app_slug = "amazonq"
workdir = trimsuffix(var.workdir, "/")
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".amazonq-module"
system_prompt = jsonencode(replace(var.system_prompt, "/[\r\n]/", ""))
coder_mcp_instructions = jsonencode(replace(var.coder_mcp_instructions, "/[\r\n]/", ""))
Your first task is:
# Create default agent config structure
default_agent_config = templatefile("${path.module}/templates/agent-config.json.tpl", {
system_prompt = local.system_prompt
})
${var.ai_prompt}
EOT
# Choose the JSON string: use var.agent_config if provided, otherwise encode default
agent_config = var.agent_config != null ? var.agent_config : local.default_agent_config
# Extract agent name from the selected config
agent_name = try(jsondecode(local.agent_config).name, "agent")
full_prompt = var.ai_prompt != null ? "${var.ai_prompt}" : ""
server_chat_parameters = var.agentapi_chat_based_path ? "--chat-base-path /@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.app_slug}/chat" : ""
}
resource "coder_script" "amazon_q" {
agent_id = var.agent_id
display_name = "Amazon Q"
icon = var.icon
script = <<-EOT
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0"
agent_id = var.agent_id
folder = local.workdir
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" >/dev/null 2>&1
}
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
ARG_TRUST_ALL_TOOLS='${var.trust_all_tools}' \
ARG_AI_PROMPT='${base64encode(local.full_prompt)}' \
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
ARG_WORKDIR='${var.workdir}' \
ARG_SERVER_PARAMETERS="${local.server_chat_parameters}" \
ARG_REPORT_TASKS='${var.report_tasks}' \
/tmp/start.sh
EOT
if [ -n "${local.encoded_pre_install_script}" ]; then
echo "Running pre-install script..."
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
chmod +x /tmp/pre_install.sh
/tmp/pre_install.sh
fi
if [ "${var.install_amazon_q}" = "true" ]; then
echo "Installing Amazon Q..."
PREV_DIR="$PWD"
TMP_DIR="$(mktemp -d)"
cd "$TMP_DIR"
ARCH="$(uname -m)"
case "$ARCH" in
"x86_64")
Q_URL="https://desktop-release.q.us-east-1.amazonaws.com/${var.amazon_q_version}/q-x86_64-linux.zip"
;;
"aarch64"|"arm64")
Q_URL="https://desktop-release.codewhisperer.us-east-1.amazonaws.com/${var.amazon_q_version}/q-aarch64-linux.zip"
;;
*)
echo "Error: Unsupported architecture: $ARCH. Amazon Q only supports x86_64 and arm64."
exit 1
;;
esac
echo "Downloading Amazon Q for $ARCH..."
curl --proto '=https' --tlsv1.2 -sSf "$Q_URL" -o "q.zip"
unzip q.zip
./q/install.sh --no-confirm
cd "$PREV_DIR"
export PATH="$PATH:$HOME/.local/bin"
echo "Installed Amazon Q version: $(q --version)"
fi
echo "Extracting auth tarball..."
PREV_DIR="$PWD"
echo "${var.experiment_auth_tarball}" | base64 -d > /tmp/auth.tar.zst
rm -rf ~/.local/share/amazon-q
mkdir -p ~/.local/share/amazon-q
cd ~/.local/share/amazon-q
tar -I zstd -xf /tmp/auth.tar.zst
rm /tmp/auth.tar.zst
cd "$PREV_DIR"
echo "Extracted auth tarball"
if [ "${var.experiment_report_tasks}" = "true" ]; then
echo "Configuring Amazon Q to report tasks via Coder MCP..."
q mcp add --name coder --command "coder" --args "exp,mcp,server,--allowed-tools,coder_report_task" --env "CODER_MCP_APP_STATUS_SLUG=amazon-q" --scope global --force
echo "Added Coder MCP server to Amazon Q configuration"
fi
if [ -n "${local.encoded_post_install_script}" ]; then
echo "Running post-install script..."
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
chmod +x /tmp/post_install.sh
/tmp/post_install.sh
fi
if [ "${var.experiment_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
if [ "${var.experiment_use_tmux}" = "true" ]; then
echo "Running Amazon Q in the background with tmux..."
if ! command_exists tmux; then
echo "Error: tmux is not installed. Please install tmux manually."
exit 1
fi
touch "$HOME/.amazon-q.log"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
tmux new-session -d -s amazon-q -c "${var.folder}" "q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log" && exec bash"
tmux send-keys -t amazon-q "${local.full_prompt}"
sleep 5
tmux send-keys -t amazon-q Enter
fi
if [ "${var.experiment_use_screen}" = "true" ]; then
echo "Running Amazon Q in the background..."
if ! command_exists screen; then
echo "Error: screen is not installed. Please install screen manually."
exit 1
fi
touch "$HOME/.amazon-q.log"
if [ ! -f "$HOME/.screenrc" ]; then
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.amazon-q.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/.amazon-q.log"
echo "multiuser on" >> "$HOME/.screenrc"
fi
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.amazon-q.log"
echo "acladd $(whoami)" >> "$HOME/.screenrc"
fi
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
screen -U -dmS amazon-q bash -c '
cd ${var.folder}
q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log
exec bash
'
# Extremely hacky way to send the prompt to the screen session
# This will be fixed in the future, but `amazon-q` was not sending MCP
# tasks when an initial prompt is provided.
screen -S amazon-q -X stuff "${local.full_prompt}"
sleep 5
screen -S amazon-q -X stuff "^M"
else
if ! command_exists q; then
echo "Error: Amazon Q is not installed. Please enable install_amazon_q or install it manually."
exit 1
fi
fi
EOT
run_on_start = true
}
resource "coder_app" "amazon_q" {
slug = "amazon-q"
display_name = "Amazon Q"
agent_id = var.agent_id
command = <<-EOT
install_script = <<-EOT
#!/bin/bash
set -e
set -o errexit
set -o pipefail
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 amazon-q 2>/dev/null; then
echo "Attaching to existing Amazon Q tmux session." | tee -a "$HOME/.amazon-q.log"
tmux attach-session -t amazon-q
else
echo "Starting a new Amazon Q tmux session." | tee -a "$HOME/.amazon-q.log"
tmux new-session -s amazon-q -c ${var.folder} "q chat --trust-all-tools | tee -a \"$HOME/.amazon-q.log\"; exec bash"
fi
elif [ "${var.experiment_use_screen}" = "true" ]; then
if screen -list | grep -q "amazon-q"; then
echo "Attaching to existing Amazon Q screen session." | tee -a "$HOME/.amazon-q.log"
screen -xRR amazon-q
else
echo "Starting a new Amazon Q screen session." | tee -a "$HOME/.amazon-q.log"
screen -S amazon-q bash -c 'q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log"; exec bash'
fi
else
cd ${var.folder}
q chat --trust-all-tools
fi
EOT
icon = var.icon
order = var.order
group = var.group
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_INSTALL='${var.install_amazon_q}' \
ARG_VERSION='${var.amazon_q_version}' \
ARG_Q_INSTALL_URL='${var.q_install_url}' \
ARG_AUTH_TARBALL='${var.auth_tarball}' \
ARG_AGENT_CONFIG='${local.agent_config != null ? base64encode(local.agent_config) : ""}' \
ARG_AGENT_NAME='${local.agent_name}' \
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_CODER_MCP_INSTRUCTIONS='${base64encode(local.coder_mcp_instructions)}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
/tmp/install.sh
EOT
}
@@ -0,0 +1,159 @@
#!/bin/bash
# Install script for amazon-q module
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" > /dev/null 2>&1
}
# Inputs
ARG_INSTALL=${ARG_INSTALL:-true}
ARG_VERSION=${ARG_VERSION:-latest}
ARG_Q_INSTALL_URL=${ARG_Q_INSTALL_URL:-https://desktop-release.q.us-east-1.amazonaws.com}
ARG_AUTH_TARBALL=${ARG_AUTH_TARBALL:-}
ARG_AGENT_CONFIG=${ARG_AGENT_CONFIG:-}
ARG_AGENT_NAME=${ARG_AGENT_NAME:-default-agent}
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.aws/.amazonq}
ARG_CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG:-}
ARG_CODER_MCP_INSTRUCTIONS=${ARG_CODER_MCP_INSTRUCTIONS:-}
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
# Decode base64 inputs
ARG_AGENT_CONFIG_DECODED=""
if [ -n "$ARG_AGENT_CONFIG" ]; then
ARG_AGENT_CONFIG_DECODED=$(echo -n "$ARG_AGENT_CONFIG" | base64 -d)
fi
ARG_CODER_MCP_INSTRUCTIONS_DECODED=""
if [ -n "$ARG_CODER_MCP_INSTRUCTIONS" ]; then
ARG_CODER_MCP_INSTRUCTIONS_DECODED=$(echo -n "$ARG_CODER_MCP_INSTRUCTIONS" | base64 -d)
fi
echo "--------------------------------"
echo "install: $ARG_INSTALL"
echo "version: $ARG_VERSION"
echo "q_install_url: $ARG_Q_INSTALL_URL"
echo "agent_name: $ARG_AGENT_NAME"
echo "coder_mcp_app_status_slug: $ARG_CODER_MCP_APP_STATUS_SLUG"
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
echo "auth_tarball_provided: ${ARG_AUTH_TARBALL}"
echo "report_tasks: ${ARG_REPORT_TASKS}"
echo "--------------------------------"
# Install Amazon Q if requested
function install_amazon_q() {
if [ "$ARG_INSTALL" = "true" ]; then
echo "Installing Amazon Q..."
PREV_DIR="$PWD"
TMP_DIR="$(mktemp -d)"
cd "$TMP_DIR"
ARCH="$(uname -m)"
case "$ARCH" in
"x86_64")
Q_URL="${ARG_Q_INSTALL_URL}/${ARG_VERSION}/q-x86_64-linux.zip"
;;
"aarch64" | "arm64")
Q_URL="${ARG_Q_INSTALL_URL}/${ARG_VERSION}/q-aarch64-linux.zip"
;;
*)
echo "Error: Unsupported architecture: $ARCH. Amazon Q only supports x86_64 and arm64."
exit 1
;;
esac
echo "Downloading Amazon Q for $ARCH from $Q_URL..."
curl --proto '=https' --tlsv1.2 -sSf "$Q_URL" -o "q.zip"
unzip q.zip
./q/install.sh --no-confirm
cd "$PREV_DIR"
rm -rf "$TMP_DIR"
# Ensure binaries are discoverable; create stable symlink to q
CANDIDATES=(
"$(command -v q || true)"
"$HOME/.local/bin/q"
)
FOUND_BIN=""
for c in "${CANDIDATES[@]}"; do
if [ -n "$c" ] && [ -x "$c" ]; then
FOUND_BIN="$c"
break
fi
done
export PATH="$PATH:$HOME/.local/bin"
echo "Installed Amazon Q at: $(command -v q || true) (resolved: $FOUND_BIN)"
fi
}
# Extract authentication tarball
function extract_auth_tarball() {
if [ -n "$ARG_AUTH_TARBALL" ]; then
echo "Extracting auth tarball..."
if ! command_exists zstd; then
echo "Error: zstd is required to extract the authentication tarball but is not installed."
echo "Please install zstd using the pre_install_script parameter."
exit 1
fi
PREV_DIR="$PWD"
echo "$ARG_AUTH_TARBALL" | base64 -d > /tmp/auth.tar.zst
rm -rf ~/.local/share/amazon-q
mkdir -p ~/.local/share/amazon-q
cd ~/.local/share/amazon-q
tar -I zstd -xf /tmp/auth.tar.zst
rm /tmp/auth.tar.zst
cd "$PREV_DIR"
echo "Extracted auth tarball to ~/.local/share/amazon-q"
else
echo "Warning: No auth tarball provided. Amazon Q may require manual authentication."
fi
}
# Configure MCP integration and create agent
function configure_agent() {
# Create Amazon Q agent configuration directory
AGENT_CONFIG_DIR="$HOME/.aws/amazonq/cli-agents"
mkdir -p "$AGENT_CONFIG_DIR"
ALLOWED_TOOLS="coder_get_workspace\,coder_create_workspace\,coder_list_workspaces\,coder_list_templates\,coder_template_version_parameters\,coder_get_authenticated_user\,coder_create_workspace_build\,coder_create_template_version\,coder_get_workspace_agent_logs\,coder_get_workspace_build_logs\,coder_get_template_version_logs\,coder_update_template_active_version\,coder_upload_tar_file\,coder_create_template\,coder_delete_template\,coder_workspace_bash"
if [ -n "$ARG_AGENT_CONFIG_DECODED" ]; then
echo "Applying custom MCP configuration..."
# Use agent name as filename for the configuration
echo "$ARG_AGENT_CONFIG_DECODED" > "$AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json"
echo "Custom configuration saved to $AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json"
fi
if [ "$ARG_REPORT_TASKS" = "true" ]; then
echo "Configuring Amazon Q to report tasks via Coder MCP..."
q mcp add --name coder \
--command "coder" \
--agent "$ARG_AGENT_NAME" \
--args "exp,mcp,server,--allowed-tools,coder_report_task,--instructions,'$ARG_CODER_MCP_INSTRUCTIONS_DECODED'" \
--env "CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG}" \
--env "CODER_MCP_AI_AGENTAPI_URL=http://localhost:3284" \
--env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
--env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
--force || echo "Warning: Failed to add Coder MCP server"
else
q mcp add --name coder \
--command "coder" \
--agent "$ARG_AGENT_NAME" \
--args "exp,mcp,server,--allowed-tools,coder_report_task" \
--env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
--env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
--force || echo "Warning: Failed to add Coder MCP server"
fi
echo "Added Coder MCP server into $ARG_AGENT_NAME in Amazon Q configuration"
q settings chat.defaultAgent "$ARG_AGENT_NAME"
}
# Main execution
install_amazon_q
extract_auth_tarball
configure_agent
echo "Amazon Q installation and configuration complete!"

Some files were not shown because too many files have changed in this diff Show More