Compare commits

...

35 Commits

Author SHA1 Message Date
35C4n0r a599302774 feat: amp upgrades for better ux (#390)
Closes #

## Description
- remove default node installation
- users can pass amp versions now
- move env variables to terraform variable (system prompt and ai prompt)

<!-- 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/sourcegraph-amp`  
**New version:** `v2.0.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 -->

---------

Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Atif Ali <me@matifali.dev>
2025-10-16 15:21:17 -05:00
DevCats ff09c415e8 feat: change tf test and validation to use paths-filter (#483)
## Description

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

## Type of Change

- [ ] New module
- [ ] New template
- [ ] 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

## Template Information

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

**Path:** `registry/[namespace]/templates/[template-name]`

## Testing & Validation

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

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-10-16 14:21:03 -05:00
DevCats 90873e8009 ci: update CI workflow to run TypeScript tests with new script (#480) 2025-10-15 14:03:12 -05:00
DevCats 2168360195 fix: add folder to all Agent Modules (#481)
## Description

Make sure folder is passed to agentapi in all Agent modules.
<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

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

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

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

**Path:** `registry/coder-labs/modules/sourcegraph_amp`  
**New version:** `v1.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

<!-- Link related issues or write "None" if not applicable -->
2025-10-15 12:20:08 -05:00
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
64 changed files with 3927 additions and 318 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
+36 -2
View File
@@ -13,6 +13,26 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@v5
- name: Detect changed files
uses: dorny/paths-filter@v3
id: filter
with:
list-files: shell
filters: |
shared:
- 'test/**'
- 'package.json'
- 'bun.lock'
- 'bunfig.toml'
- 'tsconfig.json'
- '.github/workflows/ci.yaml'
- 'scripts/ts_test_auto.sh'
- 'scripts/terraform_test_all.sh'
- 'scripts/terraform_validate.sh'
modules:
- 'registry/**/modules/**'
all:
- '**'
- name: Set up Terraform
uses: coder/coder/.github/actions/setup-tf@main
- name: Set up Bun
@@ -27,8 +47,22 @@ jobs:
- name: Install dependencies
run: bun install
- name: Run TypeScript tests
run: bun test
env:
ALL_CHANGED_FILES: ${{ steps.filter.outputs.all_files }}
SHARED_CHANGED: ${{ steps.filter.outputs.shared }}
MODULE_CHANGED_FILES: ${{ steps.filter.outputs.modules_files }}
run: bun tstest
- name: Run Terraform tests
env:
ALL_CHANGED_FILES: ${{ steps.filter.outputs.all_files }}
SHARED_CHANGED: ${{ steps.filter.outputs.shared }}
MODULE_CHANGED_FILES: ${{ steps.filter.outputs.modules_files }}
run: bun tftest
- name: Run Terraform Validate
env:
ALL_CHANGED_FILES: ${{ steps.filter.outputs.all_files }}
SHARED_CHANGED: ${{ steps.filter.outputs.shared }}
MODULE_CHANGED_FILES: ${{ steps.filter.outputs.modules_files }}
run: bun terraform-validate
validate-style:
name: Check for typos and unformatted code
@@ -48,7 +82,7 @@ jobs:
- name: Validate formatting
run: bun fmt:ci
- name: Check for typos
uses: crate-ci/typos@v1.36.3
uses: crate-ci/typos@v1.38.1
with:
config: .github/typos.toml
validate-readme-files:
+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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

+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)
+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"]
+2 -1
View File
@@ -4,7 +4,8 @@
"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",
"tftest": "./scripts/terraform_test_all.sh",
"tstest": "./scripts/ts_test_auto.sh",
"update-version": "./update-version.sh"
},
"devDependencies": {
+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.1"
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.1"
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.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
+4 -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'."
@@ -174,13 +174,15 @@ locals {
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".auggie-module"
folder = trimsuffix(var.folder, "/")
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = var.agent_id
folder = local.folder
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
+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
+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
@@ -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.1"
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.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
@@ -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" {
@@ -113,6 +113,7 @@ locals {
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".cursor-cli-module"
folder = trimsuffix(var.folder, "/")
}
# Expose status slug and API key to the agent environment
@@ -131,9 +132,10 @@ 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
folder = local.folder
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
+4 -4
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 = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -46,7 +46,7 @@ variable "gemini_api_key" {
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
version = "2.0.0"
version = "2.1.1"
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 = "2.0.0"
version = "2.1.1"
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 = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
gemini_api_key = var.gemini_api_key
folder = "/home/coder/project"
+4 -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" {
@@ -172,13 +172,15 @@ EOT
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".gemini-module"
folder = trimsuffix(var.folder, "/")
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = var.agent_id
folder = local.folder
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
@@ -1,5 +1,5 @@
---
display_name: Amp CLI
display_name: Amp
icon: ../../../../.icons/sourcegraph-amp.svg
description: Sourcegraph's AI coding agent with deep codebase understanding and intelligent code search capabilities
verified: true
@@ -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.3"
version = "2.0.0"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
install_sourcegraph_amp = true
@@ -23,8 +23,10 @@ module "amp-cli" {
## Prerequisites
- Include the [Coder Login](https://registry.coder.com/modules/coder-login/coder) module in your template
- Node.js and npm are automatically installed (via NVM) if not already available
- **Default (official installer)**: No prerequisites - the official installer includes its own runtime (Bun)
- **npm installation (`install_via_npm = true`)**: Requires Node.js and npm to be installed before Amp installation
- Required for Alpine Linux or other musl-based systems
- Ensure Node.js and npm are available in your workspace image or via earlier provisioning steps
## Usage Example
@@ -35,52 +37,55 @@ data "coder_parameter" "ai_prompt" {
type = "string"
default = ""
mutable = true
}
# Set system prompt for Amp CLI via environment variables
resource "coder_agent" "main" {
# ...
env = {
SOURCEGRAPH_AMP_SYSTEM_PROMPT = <<-EOT
You are an Amp assistant that helps developers debug and write code efficiently.
Always log task status to Coder.
EOT
SOURCEGRAPH_AMP_TASK_PROMPT = data.coder_parameter.ai_prompt.value
}
}
variable "sourcegraph_amp_api_key" {
variable "amp_api_key" {
type = string
description = "Sourcegraph Amp API key. Get one at https://ampcode.com/settings"
sensitive = true
}
module "amp-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "1.0.3"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key # recommended for authenticated usage
install_sourcegraph_amp = true
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
amp_version = "2.0.0"
agent_id = coder_agent.example.id
amp_api_key = var.amp_api_key # recommended for tasks usage
workdir = "/home/coder/project"
instruction_prompt = <<-EOT
# Instructions
- Start every response with `amp > `
EOT
ai_prompt = data.coder_parameter.ai_prompt.value
base_amp_config = jsonencode({
"amp.anthropic.thinking.enabled" = true
"amp.todos.enabled" = true
"amp.tools.stopTimeout" = 600
"amp.git.commit.ampThread.enabled" = true
"amp.git.commit.coauthor.enabled" = true
"amp.terminal.commands.nodeSpawn.loadProfile" = "daily"
"amp.permissions" = [
{ "tool" : "mcp__coder__*", "action" : "allow" },
{ "tool" : "Bash", "action" : "allow", "context" : "thread" },
{ "tool" : "Bash", "matches" : { "cmd" : ["rm -rf /*", "rm -rf ~/*"] }, "action" : "reject", "context" : "subagent" },
{ "tool" : "edit_file", "action" : "allow" },
{ "tool" : "write_file", "action" : "allow" },
{ "tool" : "read_file", "action" : "allow" },
{ "tool" : "Grep", "action" : "allow" }
]
})
}
```
## How it Works
- **Install**: Installs Sourcegraph Amp CLI using npm (installs Node.js via NVM if required)
- **Start**: Launches Amp CLI in the specified directory, wrapped with AgentAPI to enable tasks and AI interactions
- **Environment Variables**: Sets `SOURCEGRAPH_AMP_API_KEY` and `SOURCEGRAPH_AMP_START_DIRECTORY` for the CLI execution
## Troubleshooting
- If `amp` is not found, ensure `install_sourcegraph_amp = true` and your API key is valid
- Logs are written under `/home/coder/.sourcegraph-amp-module/` (`install.log`, `agentapi-start.log`) for debugging
- If `amp` is not found, ensure `install_amp = true` and your API key is valid
- Logs are written under `/home/coder/.amp-module/` (`install.log`, `agentapi-start.log`) for debugging
- If AgentAPI fails to start, verify that your container has network access and executable permissions for the scripts
> [!IMPORTANT]
> For using **Coder Tasks** with Amp CLI, make sure to pass the `AI Prompt` parameter and set `sourcegraph_amp_api_key`.
> To use tasks with Amp CLI, create a `coder_parameter` named `"AI Prompt"` and pass its value to the amp-cli module's `ai_prompt` variable. The `folder` variable is required for the module to function correctly.
> For using **Coder Tasks** with Amp CLI, make sure to set `amp_api_key`.
> This ensures task reporting and status updates work seamlessly.
## References
@@ -43,9 +43,9 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const { id } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
install_sourcegraph_amp: props?.skipAmpMock ? "true" : "false",
workdir: "/home/coder",
install_amp: props?.skipAmpMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
sourcegraph_amp_model: "test-model",
...props?.moduleVariables,
},
registerCleanup,
@@ -68,45 +68,94 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
setDefaultTimeout(60 * 1000);
describe("sourcegraph-amp", async () => {
describe("amp", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
test("happy-path", async () => {
const { id } = await setup();
// test("happy-path", async () => {
// const { id } = await setup();
// await execModuleScript(id);
// await expectAgentAPIStarted(id);
// });
//
// test("api-key", async () => {
// const apiKey = "test-api-key-123";
// const { id } = await setup({
// moduleVariables: {
// amp_api_key: apiKey,
// },
// });
// await execModuleScript(id);
// const resp = await readFileContainer(
// id,
// "/home/coder/.amp-module/agentapi-start.log",
// );
// expect(resp).toContain("amp_api_key provided !");
// });
//
test("install-latest-version", async () => {
const { id } = await setup({
skipAmpMock: true,
skipAgentAPIMock: true,
moduleVariables: {
amp_version: "",
},
});
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("api-key", async () => {
const apiKey = "test-api-key-123";
test("install-specific-version", async () => {
const { id } = await setup({
skipAmpMock: true,
moduleVariables: {
sourcegraph_amp_api_key: apiKey,
amp_version: "0.0.1755964909-g31e083",
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/agentapi-start.log",
"/home/coder/.amp-module/agentapi-start.log",
);
expect(resp).toContain("sourcegraph_amp_api_key provided !");
expect(resp).toContain("0.0.1755964909-g31e08");
});
test("custom-folder", async () => {
const folder = "/tmp/sourcegraph-amp-test";
test("install-via-npm", async () => {
const { id } = await setup({
skipAmpMock: true,
moduleVariables: {
install_via_npm: "true",
},
});
await execModuleScript(id);
const installLog = await readFileContainer(
id,
"/home/coder/.amp-module/install.log",
);
expect(installLog).toContain("Installing Amp via npm");
const startLog = await readFileContainer(
id,
"/home/coder/.amp-module/agentapi-start.log",
);
expect(startLog).toContain("AMP version:");
});
test("custom-workdir", async () => {
const workdir = "/tmp/amp-test";
const { id } = await setup({
moduleVariables: {
folder,
workdir,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/install.log",
"/home/coder/.amp-module/agentapi-start.log",
);
expect(resp).toContain(folder);
expect(resp).toContain(workdir);
});
test("pre-post-install-scripts", async () => {
@@ -119,39 +168,104 @@ describe("sourcegraph-amp", async () => {
await execModuleScript(id);
const preLog = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/pre_install.log",
"/home/coder/.amp-module/pre_install.log",
);
expect(preLog).toContain("pre-install-script");
const postLog = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/post_install.log",
"/home/coder/.amp-module/post_install.log",
);
expect(postLog).toContain("post-install-script");
});
test("system-prompt", async () => {
const prompt = "this is a system prompt for AMP";
const { id } = await setup();
await execModuleScript(id, {
SOURCEGRAPH_AMP_SYSTEM_PROMPT: prompt,
test("instruction-prompt", async () => {
const prompt = "this is a instruction prompt for AMP";
const { id } = await setup({
moduleVariables: {
instruction_prompt: prompt,
},
});
const resp = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/SYSTEM_PROMPT.md",
);
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.config/AGENTS.md");
expect(resp).toContain(prompt);
});
test("task-prompt", async () => {
test("ai-prompt", async () => {
const prompt = "this is a task prompt for AMP";
const { id } = await setup();
await execModuleScript(id, {
SOURCEGRAPH_AMP_TASK_PROMPT: prompt,
const { id } = await setup({
moduleVariables: {
ai_prompt: prompt,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/agentapi-start.log",
"/home/coder/.amp-module/agentapi-start.log",
);
expect(resp).toContain(`sourcegraph amp task prompt provided : ${prompt}`);
expect(resp).toContain(`amp task prompt provided : ${prompt}`);
});
test("custom-base-config", async () => {
const customConfig = JSON.stringify({
"amp.anthropic.thinking.enabled": false,
"amp.todos.enabled": false,
"amp.tools.stopTimeout": 900,
"amp.git.commit.ampThread.enabled": true,
});
const customMcp = JSON.stringify({
"test-server": {
command: "/usr/bin/test-mcp",
args: ["--test-arg"],
type: "stdio",
},
});
const { id } = await setup({
moduleVariables: {
base_amp_config: customConfig,
mcp: customMcp,
},
});
await execModuleScript(id, {
CODER_AGENT_TOKEN: "test-token",
CODER_AGENT_URL: "http://test-url:3000",
});
const settingsContent = await readFileContainer(
id,
"/home/coder/.config/amp/settings.json",
);
const settings = JSON.parse(settingsContent);
expect(settings["amp.anthropic.thinking.enabled"]).toBe(false);
expect(settings["amp.todos.enabled"]).toBe(false);
expect(settings["amp.tools.stopTimeout"]).toBe(900);
expect(settings["amp.git.commit.ampThread.enabled"]).toBe(true);
expect(settings["amp.mcpServers"]).toBeDefined();
expect(settings["amp.mcpServers"].coder).toBeDefined();
expect(settings["amp.mcpServers"]["test-server"]).toBeDefined();
expect(settings["amp.mcpServers"]["test-server"].command).toBe(
"/usr/bin/test-mcp",
);
expect(settings["amp.mcpServers"]["test-server"].args).toEqual([
"--test-arg",
]);
});
test("default-base-config", async () => {
const { id } = await setup();
await execModuleScript(id, {
CODER_AGENT_TOKEN: "test-token",
CODER_AGENT_URL: "http://test-url:3000",
});
const settingsContent = await readFileContainer(
id,
"/home/coder/.config/amp/settings.json",
);
const settings = JSON.parse(settingsContent);
expect(settings["amp.anthropic.thinking.enabled"]).toBe(true);
expect(settings["amp.todos.enabled"]).toBe(true);
expect(settings["amp.mcpServers"]).toBeDefined();
expect(settings["amp.mcpServers"].coder).toBeDefined();
expect(settings["amp.mcpServers"].coder.command).toBe("coder");
});
});
@@ -36,28 +36,9 @@ variable "icon" {
default = "/icon/sourcegraph-amp.svg"
}
variable "folder" {
variable "workdir" {
type = string
description = "The folder to run sourcegraph_amp in."
default = "/home/coder"
}
variable "install_sourcegraph_amp" {
type = bool
description = "Whether to install sourcegraph-amp."
default = true
}
variable "sourcegraph_amp_api_key" {
type = string
description = "sourcegraph-amp API Key"
default = ""
}
resource "coder_env" "sourcegraph_amp_api_key" {
agent_id = var.agent_id
name = "SOURCEGRAPH_AMP_API_KEY"
value = var.sourcegraph_amp_api_key
description = "The folder to run AMP CLI in."
}
variable "install_agentapi" {
@@ -69,21 +50,87 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.3.0"
default = "v0.10.0"
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for Claude Code"
default = false
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app"
default = "Amp"
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app"
default = "Amp CLI"
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing sourcegraph_amp"
description = "Custom script to run before installing amp cli"
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing sourcegraph_amp."
description = "Custom script to run after installing amp cli."
default = null
}
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI"
default = true
}
variable "install_amp" {
type = bool
description = "Whether to install amp cli."
default = true
}
variable "install_via_npm" {
type = bool
description = "Install Amp via npm instead of the official installer."
default = false
}
variable "amp_api_key" {
type = string
description = "amp cli API Key"
default = ""
}
variable "amp_version" {
type = string
description = "The version of amp cli to install."
default = ""
}
variable "ai_prompt" {
type = string
description = "Task prompt for the Amp CLI"
default = ""
}
variable "instruction_prompt" {
type = string
description = "Instruction prompt for the Amp CLI. https://ampcode.com/manual#AGENTS.md"
default = ""
}
resource "coder_env" "amp_api_key" {
agent_id = var.agent_id
name = "AMP_API_KEY"
value = var.amp_api_key
}
variable "base_amp_config" {
type = string
description = <<-EOT
@@ -102,22 +149,25 @@ variable "base_amp_config" {
default = ""
}
variable "additional_mcp_servers" {
variable "mcp" {
type = string
description = "Additional MCP servers configuration in JSON format to append to amp.mcpServers."
default = null
}
data "external" "env" {
program = ["sh", "-c", "echo '{\"CODER_AGENT_TOKEN\":\"'$CODER_AGENT_TOKEN'\",\"CODER_AGENT_URL\":\"'$CODER_AGENT_URL'\"}'"]
}
locals {
app_slug = "amp"
default_base_config = {
default_base_config = jsonencode({
"amp.anthropic.thinking.enabled" = true
"amp.todos.enabled" = true
}
})
# Use provided config or default, then extract base settings (excluding mcpServers)
user_config = var.base_amp_config != "" ? jsondecode(var.base_amp_config) : local.default_base_config
user_config = jsondecode(var.base_amp_config != "" ? var.base_amp_config : local.default_base_config)
base_amp_settings = { for k, v in local.user_config : k => v if k != "amp.mcpServers" }
coder_mcp = {
@@ -125,14 +175,16 @@ locals {
"command" = "coder"
"args" = ["exp", "mcp", "server"]
"env" = {
"CODER_MCP_APP_STATUS_SLUG" = local.app_slug
"CODER_MCP_AI_AGENTAPI_URL" = "http://localhost:3284"
"CODER_MCP_APP_STATUS_SLUG" = var.report_tasks == true ? local.app_slug : ""
"CODER_MCP_AI_AGENTAPI_URL" = var.report_tasks == true ? "http://localhost:3284" : ""
"CODER_AGENT_TOKEN" = data.external.env.result.CODER_AGENT_TOKEN
"CODER_AGENT_URL" = data.external.env.result.CODER_AGENT_URL
}
"type" = "stdio"
}
}
additional_mcp = var.additional_mcp_servers != null ? jsondecode(var.additional_mcp_servers) : {}
additional_mcp = var.mcp != null ? jsondecode(var.mcp) : {}
merged_mcp_servers = merge(
lookup(local.user_config, "amp.mcpServers", {}),
@@ -146,21 +198,24 @@ locals {
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".sourcegraph-amp-module"
module_dir_name = ".amp-module"
workdir = trimsuffix(var.workdir, "/")
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.0.1"
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 = "Sourcegraph Amp"
cli_app_slug = "${local.app_slug}-cli"
cli_app_display_name = "Sourcegraph Amp CLI"
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
@@ -173,8 +228,10 @@ module "agentapi" {
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
SOURCEGRAPH_AMP_API_KEY='${var.sourcegraph_amp_api_key}' \
SOURCEGRAPH_AMP_START_DIRECTORY='${var.folder}' \
ARG_AMP_API_KEY='${var.amp_api_key}' \
ARG_AMP_START_DIRECTORY='${var.workdir}' \
ARG_AMP_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
/tmp/start.sh
EOT
@@ -185,9 +242,11 @@ module "agentapi" {
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_INSTALL_SOURCEGRAPH_AMP='${var.install_sourcegraph_amp}' \
SOURCEGRAPH_AMP_START_DIRECTORY='${var.folder}' \
ARG_AMP_CONFIG="$(echo -n '${base64encode(jsonencode(local.final_config))}' | base64 -d)" \
ARG_INSTALL_AMP='${var.install_amp}' \
ARG_INSTALL_VIA_NPM='${var.install_via_npm}' \
ARG_AMP_CONFIG="${base64encode(jsonencode(local.final_config))}" \
ARG_AMP_VERSION='${var.amp_version}' \
ARG_AMP_INSTRUCTION_PROMPT='${base64encode(var.instruction_prompt)}' \
/tmp/install.sh
EOT
}
@@ -1,77 +1,119 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
# ANSI colors
BOLD='\033[1m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
ARG_INSTALL_AMP=${ARG_INSTALL_AMP:-true}
ARG_INSTALL_VIA_NPM=${ARG_INSTALL_VIA_NPM:-false}
ARG_AMP_VERSION=${ARG_AMP_VERSION:-}
ARG_AMP_INSTRUCTION_PROMPT=$(echo -n "${ARG_AMP_INSTRUCTION_PROMPT:-}" | base64 -d)
ARG_AMP_CONFIG=$(echo -n "${ARG_AMP_CONFIG:-}" | base64 -d)
echo "--------------------------------"
echo "Install flag: $ARG_INSTALL_SOURCEGRAPH_AMP"
echo "Workspace: $SOURCEGRAPH_AMP_START_DIRECTORY"
printf "Install flag: %s\n" "$ARG_INSTALL_AMP"
printf "Install via npm: %s\n" "$ARG_INSTALL_VIA_NPM"
printf "Amp Version: %s\n" "$ARG_AMP_VERSION"
printf "AMP Config: %s\n" "$ARG_AMP_CONFIG"
printf "Instruction Prompt: %s\n" "$ARG_AMP_INSTRUCTION_PROMPT"
echo "--------------------------------"
# Helper function to check if a command exists
command_exists() {
command -v "$1" > /dev/null 2>&1
}
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
install_amp_npm() {
printf "%s${YELLOW}Installing Amp via npm${NC}\n" "${BOLD}"
# Temporarily disable nounset (-u) for nvm to avoid PROVIDED_VERSION error
set +u
nvm install --lts
nvm use --lts
nvm alias default node
set -u
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
# Load nvm if available
# shellcheck source=/dev/null
if [ -f "$HOME/.nvm/nvm.sh" ]; then
source "$HOME/.nvm/nvm.sh"
fi
}
function install_sourcegraph_amp() {
if [ "${ARG_INSTALL_SOURCEGRAPH_AMP}" = "true" ]; then
install_node
# If nvm is not used, set up user npm global directory
if ! command_exists nvm; then
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
printf "%s Installing Sourcegraph AMP CLI...\n" "${BOLD}"
npm install -g @sourcegraph/amp@0.0.1754179307-gba1f97
printf "%s Successfully installed Sourcegraph AMP CLI. Version: %s\n" "${BOLD}" "$(amp --version)"
if ! command_exists node || ! command_exists npm; then
printf "${YELLOW}Warning: Node.js/npm not found. Skipping Amp installation.${NC}\n"
printf "To install Amp via npm, please install Node.js and npm first.\n"
return 1
fi
}
function setup_system_prompt() {
if [ -n "${SOURCEGRAPH_AMP_SYSTEM_PROMPT:-}" ]; then
echo "Setting Sourcegraph AMP system prompt..."
mkdir -p "$HOME/.sourcegraph-amp-module"
echo "$SOURCEGRAPH_AMP_SYSTEM_PROMPT" > "$HOME/.sourcegraph-amp-module/SYSTEM_PROMPT.md"
echo "System prompt saved to $HOME/.sourcegraph-amp-module/SYSTEM_PROMPT.md"
printf "Node.js version: %s\n" "$(node --version)"
printf "npm version: %s\n" "$(npm --version)"
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_AMP_VERSION" ]; then
npm install -g "@sourcegraph/amp@$ARG_AMP_VERSION"
else
echo "No system prompt provided for Sourcegraph AMP."
npm install -g "@sourcegraph/amp"
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
}
install_amp_official() {
printf "%s Installing Amp using official installer\n" "${BOLD}"
if [ -n "$ARG_AMP_VERSION" ]; then
export AMP_VERSION="$ARG_AMP_VERSION"
printf "Installing Amp version: %s\n" "$AMP_VERSION"
fi
if curl -fsSL https://ampcode.com/install.sh | bash; then
export PATH="$HOME/.local/bin:$HOME/.amp/bin:$PATH"
if ! grep -q 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc"; then
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc"
fi
else
printf "${YELLOW}Warning: Official installer failed. Installation skipped.${NC}\n"
return 1
fi
}
function install_amp() {
if [ "${ARG_INSTALL_AMP}" = "true" ]; then
if [ "${ARG_INSTALL_VIA_NPM}" = "true" ]; then
install_amp_npm || {
printf "${YELLOW}Amp installation via npm failed.${NC}\n"
return 0
}
else
install_amp_official || {
printf "${YELLOW}Amp installation via official installer failed.${NC}\n"
return 0
}
fi
if command_exists amp; then
printf "%s${GREEN}Successfully installed Sourcegraph Amp CLI. Version: %s${NC}\n" "${BOLD}" "$(amp --version)"
fi
else
printf "Skipping Sourcegraph Amp CLI installation (install_amp=false)\n"
fi
}
function setup_instruction_prompt() {
if [ -n "${ARG_AMP_INSTRUCTION_PROMPT:-}" ]; then
echo "Setting AMP instruction prompt..."
mkdir -p "$HOME/.config"
echo "$ARG_AMP_INSTRUCTION_PROMPT" > "$HOME/.config/AGENTS.md"
echo "Instruction prompt saved to $HOME/.config/AGENTS.md"
else
echo "No instruction prompt provided for Sourcegraph AMP."
fi
}
@@ -86,11 +128,17 @@ function configure_amp_settings() {
fi
echo "Writing AMP configuration to $SETTINGS_PATH"
printf '%s\n' "$ARG_AMP_CONFIG" > "$SETTINGS_PATH"
UPDATED_CONFIG=$(echo "$ARG_AMP_CONFIG" | jq --arg token "$CODER_AGENT_TOKEN" --arg url "$CODER_AGENT_URL" \
".[\"amp.mcpServers\"].coder.env += {
\"CODER_AGENT_TOKEN\": \"$CODER_AGENT_TOKEN\",
\"CODER_AGENT_URL\": \"$CODER_AGENT_URL\"
}")
printf "UPDATED_CONFIG: %s\n" "$UPDATED_CONFIG"
printf '%s\n' "$UPDATED_CONFIG" > "$SETTINGS_PATH"
echo "AMP configuration complete"
}
install_sourcegraph_amp
setup_system_prompt
install_amp
setup_instruction_prompt
configure_amp_settings
@@ -6,11 +6,11 @@ set -euo pipefail
source "$HOME/.bashrc"
# shellcheck source=/dev/null
if [ -f "$HOME/.nvm/nvm.sh" ]; then
source "$HOME"/.nvm/nvm.sh
else
export PATH="$HOME/.npm-global/bin:$PATH"
source "$HOME/.nvm/nvm.sh"
fi
export PATH="$HOME/.local/bin:$HOME/.amp/bin:$HOME/.npm-global/bin:$PATH"
function ensure_command() {
command -v "$1" &> /dev/null || {
echo "Error: '$1' not found." >&2
@@ -18,10 +18,21 @@ function ensure_command() {
}
}
ARG_AMP_START_DIRECTORY=${ARG_AMP_START_DIRECTORY:-"$HOME"}
ARG_AMP_API_KEY=${ARG_AMP_API_KEY:-}
ARG_AMP_TASK_PROMPT=$(echo -n "${ARG_AMP_TASK_PROMPT:-}" | base64 -d)
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
echo "--------------------------------"
printf "Workspace: %s\n" "$ARG_AMP_START_DIRECTORY"
printf "Task Prompt: %s\n" "$ARG_AMP_TASK_PROMPT"
printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
echo "--------------------------------"
ensure_command amp
echo "AMP version: $(amp --version)"
dir="$SOURCEGRAPH_AMP_START_DIRECTORY"
dir="$ARG_AMP_START_DIRECTORY"
if [[ -d "$dir" ]]; then
echo "Using existing directory: $dir"
else
@@ -30,20 +41,23 @@ else
fi
cd "$dir"
if [ -n "$SOURCEGRAPH_AMP_API_KEY" ]; then
printf "sourcegraph_amp_api_key provided !\n"
export AMP_API_KEY=$SOURCEGRAPH_AMP_API_KEY
if [ -n "$ARG_AMP_API_KEY" ]; then
printf "amp_api_key provided !\n"
export AMP_API_KEY=$ARG_AMP_API_KEY
else
printf "sourcegraph_amp_api_key not provided\n"
printf "amp_api_key not provided\n"
fi
if [ -n "${SOURCEGRAPH_AMP_TASK_PROMPT:-}" ]; then
printf "sourcegraph amp task prompt provided : $SOURCEGRAPH_AMP_TASK_PROMPT"
PROMPT="Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $SOURCEGRAPH_AMP_TASK_PROMPT"
if [ -n "$ARG_AMP_TASK_PROMPT" ]; then
if [ "$ARG_REPORT_TASKS" == "true" ]; then
printf "amp task prompt provided : %s" "$ARG_AMP_TASK_PROMPT\n"
PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AMP_TASK_PROMPT"
else
PROMPT="$ARG_AMP_TASK_PROMPT"
fi
# Pipe the prompt into amp, which will be run inside agentapi
agentapi server --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp"
agentapi server --type amp --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp"
else
printf "No task prompt given.\n"
agentapi server --term-width=67 --term-height=1190 -- amp
agentapi server --type amp --term-width=67 --term-height=1190 -- amp
fi
+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
+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" {
+10 -10
View File
@@ -13,7 +13,7 @@ Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's A
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
@@ -102,7 +102,7 @@ data "coder_parameter" "ai_prompt" {
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
@@ -228,7 +228,7 @@ If no custom `agent_config` is provided, the default agent name "agent" is used.
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
@@ -258,7 +258,7 @@ This example will:
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
@@ -279,7 +279,7 @@ module "amazon-q" {
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
@@ -305,7 +305,7 @@ module "amazon-q" {
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
@@ -319,7 +319,7 @@ module "amazon-q" {
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
@@ -340,14 +340,14 @@ module "amazon-q" {
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.0.0"
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.6.1"
agentapi_version = "v0.10.0"
}
```
@@ -358,7 +358,7 @@ For environments without direct internet access, you can host Amazon Q installat
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
+4 -4
View File
@@ -88,7 +88,7 @@ variable "post_install_script" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.6.1"
default = "v0.10.0"
}
variable "workdir" {
@@ -96,8 +96,6 @@ variable "workdir" {
description = "The folder to run Amazon Q in."
}
# ---------------------------------------------
variable "install_amazon_q" {
type = bool
description = "Whether to install Amazon Q."
@@ -190,6 +188,7 @@ resource "coder_env" "auth_tarball" {
locals {
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"
@@ -215,9 +214,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 = local.workdir
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
@@ -94,6 +94,13 @@ function install_amazon_q() {
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
+160 -5
View File
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.0.1"
version = "3.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
@@ -49,7 +49,7 @@ data "coder_parameter" "ai_prompt" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.0.1"
version = "3.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
@@ -58,7 +58,7 @@ module "claude-code" {
claude_code_oauth_token = "xxxxx-xxxx-xxxx"
claude_code_version = "1.0.82" # Pin to a specific version
agentapi_version = "v0.6.1"
agentapi_version = "v0.10.0"
ai_prompt = data.coder_parameter.ai_prompt.value
model = "sonnet"
@@ -85,7 +85,7 @@ Run and configure Claude Code as a standalone CLI in your workspace.
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.0.1"
version = "3.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
install_claude_code = true
@@ -108,13 +108,168 @@ variable "claude_code_oauth_token" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.0.1"
version = "3.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
}
```
### Usage with AWS Bedrock
#### Prerequisites
AWS account with Bedrock access, Claude models enabled in Bedrock console, appropriate IAM permissions.
Configure Claude Code to use AWS Bedrock for accessing Claude models through your AWS infrastructure.
```tf
resource "coder_env" "bedrock_use" {
agent_id = coder_agent.example.id
name = "CLAUDE_CODE_USE_BEDROCK"
value = "1"
}
resource "coder_env" "aws_region" {
agent_id = coder_agent.example.id
name = "AWS_REGION"
value = "us-east-1" # Choose your preferred region
}
# Option 1: Using AWS credentials
variable "aws_access_key_id" {
type = string
description = "Your AWS access key ID. Create this in the AWS IAM console under 'Security credentials'."
sensitive = true
value = "xxxx-xxx-xxxx"
}
variable "aws_secret_access_key" {
type = string
description = "Your AWS secret access key. This is shown once when you create an access key in the AWS IAM console."
sensitive = true
value = "xxxx-xxx-xxxx"
}
resource "coder_env" "aws_access_key_id" {
agent_id = coder_agent.example.id
name = "AWS_ACCESS_KEY_ID"
value = var.aws_access_key_id
}
resource "coder_env" "aws_secret_access_key" {
agent_id = coder_agent.example.id
name = "AWS_SECRET_ACCESS_KEY"
value = var.aws_secret_access_key
}
# Option 2: Using Bedrock API key (simpler)
variable "aws_bearer_token_bedrock" {
type = string
description = "Your AWS Bedrock bearer token. This provides access to Bedrock without needing separate access key and secret key."
sensitive = true
value = "xxxx-xxx-xxxx"
}
resource "coder_env" "bedrock_api_key" {
agent_id = coder_agent.example.id
name = "AWS_BEARER_TOKEN_BEDROCK"
value = var.aws_bearer_token_bedrock
}
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
}
```
> [!NOTE]
> For additional Bedrock configuration options (model selection, token limits, region overrides, etc.), see the [Claude Code Bedrock documentation](https://docs.claude.com/en/docs/claude-code/amazon-bedrock).
### Usage with Google Vertex AI
#### Prerequisites
GCP project with Vertex AI API enabled, Claude models enabled through Model Garden, service account with Vertex AI permissions, appropriate IAM permissions (Vertex AI User role).
Configure Claude Code to use Google Vertex AI for accessing Claude models through Google Cloud Platform.
```tf
variable "vertex_sa_json" {
type = string
description = "The complete JSON content of your Google Cloud service account key file. Create a service account in the GCP Console under 'IAM & Admin > Service Accounts', then create and download a JSON key. Copy the entire JSON content into this variable."
sensitive = true
}
resource "coder_env" "vertex_use" {
agent_id = coder_agent.example.id
name = "CLAUDE_CODE_USE_VERTEX"
value = "1"
}
resource "coder_env" "vertex_project_id" {
agent_id = coder_agent.example.id
name = "ANTHROPIC_VERTEX_PROJECT_ID"
value = "your-gcp-project-id"
}
resource "coder_env" "cloud_ml_region" {
agent_id = coder_agent.example.id
name = "CLOUD_ML_REGION"
value = "global"
}
resource "coder_env" "vertex_sa_json" {
agent_id = coder_agent.example.id
name = "VERTEX_SA_JSON"
value = var.vertex_sa_json
}
resource "coder_env" "google_application_credentials" {
agent_id = coder_agent.example.id
name = "GOOGLE_APPLICATION_CREDENTIALS"
value = "/tmp/gcp-sa.json"
}
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514"
pre_install_script = <<-EOT
#!/bin/bash
# Write the service account JSON to a file
echo "$VERTEX_SA_JSON" > /tmp/gcp-sa.json
# Install prerequisite packages
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates gnupg curl
# Add Google Cloud public key
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
# Add Google Cloud SDK repo to apt sources
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee /etc/apt/sources.list.d/google-cloud-sdk.list
# Update and install the Google Cloud SDK
sudo apt-get update && sudo apt-get install -y google-cloud-cli
# Authenticate gcloud with the service account
gcloud auth activate-service-account --key-file=/tmp/gcp-sa.json
EOT
}
```
> [!NOTE]
> For additional Vertex AI configuration options (model selection, token limits, region overrides, etc.), see the [Claude Code Vertex AI documentation](https://docs.claude.com/en/docs/claude-code/google-vertex-ai).
## Troubleshooting
If you encounter any issues, check the log files in the `~/.claude-module` directory within your workspace for detailed information.
+35 -6
View File
@@ -86,7 +86,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.7.1"
default = "v0.10.0"
}
variable "ai_prompt" {
@@ -183,7 +183,7 @@ variable "claude_code_oauth_token" {
variable "system_prompt" {
type = string
description = "The system prompt to use for the Claude Code server."
default = "Send a task status update to notify the user that you are ready for input, and then wait for user input."
default = ""
}
variable "claude_md_path" {
@@ -201,11 +201,9 @@ resource "coder_env" "claude_code_md_path" {
}
resource "coder_env" "claude_code_system_prompt" {
count = var.system_prompt == "" ? 0 : 1
agent_id = var.agent_id
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
value = var.system_prompt
value = local.final_system_prompt
}
resource "coder_env" "claude_code_oauth_token" {
@@ -231,12 +229,43 @@ locals {
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".claude-module"
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
# Required prompts for the module to properly report task status to Coder
report_tasks_system_prompt = <<-EOT
-- Tool Selection --
- coder_report_task: providing status updates or requesting user input.
-- 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
In your summary on coder_report_task:
- Be specific about what you're doing
- Clearly indicate what information you need from the user when in "failure" state
- Keep it under 160 characters
- Make it actionable
EOT
# Only include coder system prompts if report_tasks is enabled
custom_system_prompt = trimspace(try(var.system_prompt, ""))
final_system_prompt = format("<system>%s%s</system>",
var.report_tasks ? format("\n%s\n", local.report_tasks_system_prompt) : "",
local.custom_system_prompt != "" ? format("\n%s\n", local.custom_system_prompt) : ""
)
}
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
@@ -42,7 +42,7 @@ run "test_claude_code_with_api_key" {
}
assert {
condition = coder_env.claude_api_key.value == "test-api-key-123"
condition = coder_env.claude_api_key[0].value == "test-api-key-123"
error_message = "Claude API key value should match the input"
}
}
@@ -187,3 +187,84 @@ run "test_claude_code_permission_mode_validation" {
error_message = "Permission mode should be one of the valid options"
}
}
run "test_claude_code_system_prompt" {
command = plan
variables {
agent_id = "test-agent-system-prompt"
workdir = "/home/coder/test"
system_prompt = "Custom addition"
}
assert {
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
error_message = "System prompt should not be empty"
}
assert {
condition = length(regexall("Custom addition", coder_env.claude_code_system_prompt.value)) > 0
error_message = "System prompt should have system_prompt variable value"
}
}
run "test_claude_report_tasks_default" {
command = plan
variables {
agent_id = "test-agent-report-tasks"
workdir = "/home/coder/test"
# report_tasks: default is true
}
assert {
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
error_message = "System prompt should not be empty"
}
# Ensure system prompt is wrapped by <system>
assert {
condition = startswith(trimspace(coder_env.claude_code_system_prompt.value), "<system>")
error_message = "System prompt should start with <system>"
}
assert {
condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "</system>")
error_message = "System prompt should end with </system>"
}
# Ensure Coder sections are injected when report_tasks=true (default)
assert {
condition = length(regexall("-- Tool Selection --", coder_env.claude_code_system_prompt.value)) > 0
error_message = "System prompt should have Tool Selection section"
}
assert {
condition = length(regexall("-- Task Reporting --", coder_env.claude_code_system_prompt.value)) > 0
error_message = "System prompt should have Task Reporting section"
}
}
run "test_claude_report_tasks_disabled" {
command = plan
variables {
agent_id = "test-agent-report-tasks"
workdir = "/home/coder/test"
report_tasks = false
}
assert {
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
error_message = "System prompt should not be empty"
}
# Ensure system prompt is wrapped by <system>
assert {
condition = startswith(trimspace(coder_env.claude_code_system_prompt.value), "<system>")
error_message = "System prompt should start with <system>"
}
assert {
condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "</system>")
error_message = "System prompt should end with </system>"
}
}
@@ -1,7 +1,9 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi
BOLD='\033[0;1m'
@@ -1,7 +1,9 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi
export PATH="$HOME/.local/bin:$PATH"
command_exists() {
+34 -11
View File
@@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
@@ -28,7 +28,7 @@ module "git-clone" {
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
base_dir = "~/projects/coder"
@@ -43,12 +43,12 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
data "coder_git_auth" "github" {
data "coder_external_auth" "github" {
id = "github"
}
```
@@ -69,7 +69,7 @@ data "coder_parameter" "git_repo" {
module "git_clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = data.coder_parameter.git_repo.value
}
@@ -103,7 +103,7 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.example.com/coder/coder/tree/feat/example"
git_providers = {
@@ -122,7 +122,7 @@ To GitLab clone with a specific branch like `feat/example`
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
}
@@ -134,7 +134,7 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
git_providers = {
@@ -155,7 +155,7 @@ For example, to clone the `feat/example` branch:
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
branch_name = "feat/example"
@@ -173,7 +173,7 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder:
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
folder_name = "coder-dev"
@@ -192,9 +192,32 @@ If not defined, the default, `0`, performs a full clone.
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-clone/coder"
version = "1.1.0"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
depth = 1
}
```
## Post-clone script
Run a custom script after cloning the repository by setting the `post_clone_script` variable.
This is useful for running initialization tasks like installing dependencies or setting up the environment.
```tf
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
post_clone_script = <<-EOT
#!/bin/bash
echo "Repository cloned successfully!"
# Install dependencies
npm install
# Run any other initialization tasks
make setup
EOT
}
```
+18 -1
View File
@@ -30,11 +30,12 @@ describe("git-clone", async () => {
url: "fake-url",
});
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.exitCode).toBe(128);
expect(output.stdout).toEqual([
"Creating directory ~/fake-url...",
"Cloning fake-url to ~/fake-url...",
]);
expect(output.stderr.join(" ")).toContain("fatal");
expect(output.stderr.join(" ")).toContain("fake-url");
});
it("repo_dir should match repo name for https", async () => {
@@ -244,4 +245,20 @@ describe("git-clone", async () => {
"Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...",
]);
});
it("runs post-clone script", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "fake-url",
post_clone_script: "echo 'Post-clone script executed'",
});
const output = await executeScriptInContainer(
state,
"alpine/git",
"sh",
"mkdir -p ~/fake-url && echo 'existing' > ~/fake-url/file.txt",
);
expect(output.stdout).toContain("Running post-clone script...");
expect(output.stdout).toContain("Post-clone script executed");
});
});
+9
View File
@@ -62,6 +62,12 @@ variable "depth" {
default = 0
}
variable "post_clone_script" {
description = "Custom script to run after cloning the repository. Runs always after git clone, even if the repository already exists."
type = string
default = null
}
locals {
# Remove query parameters and fragments from the URL
url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "")
@@ -81,6 +87,8 @@ locals {
clone_path = var.base_dir != "" ? join("/", [var.base_dir, local.folder_name]) : join("/", ["~", local.folder_name])
# Construct the web URL
web_url = startswith(local.clone_url, "git@") ? replace(replace(local.clone_url, ":", "/"), "git@", "https://") : local.clone_url
# Encode the post_clone_script for passing to the shell script
encoded_post_clone_script = var.post_clone_script != null ? base64encode(var.post_clone_script) : ""
}
output "repo_dir" {
@@ -120,6 +128,7 @@ resource "coder_script" "git_clone" {
REPO_URL : local.clone_url,
BRANCH_NAME : local.branch_name,
DEPTH = var.depth,
POST_CLONE_SCRIPT : local.encoded_post_clone_script,
})
display_name = "Git Clone"
icon = "/icon/git.svg"
+11 -1
View File
@@ -6,6 +6,7 @@ BRANCH_NAME="${BRANCH_NAME}"
# Expand home if it's specified!
CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}"
DEPTH="${DEPTH}"
POST_CLONE_SCRIPT="${POST_CLONE_SCRIPT}"
# Check if the variable is empty...
if [ -z "$REPO_URL" ]; then
@@ -52,5 +53,14 @@ if [ -z "$(ls -A "$CLONE_PATH")" ]; then
fi
else
echo "$CLONE_PATH already exists and isn't empty, skipping clone!"
exit 0
fi
# Run post-clone script if provided
if [ -n "$POST_CLONE_SCRIPT" ]; then
echo "Running post-clone script..."
echo "$POST_CLONE_SCRIPT" | base64 -d > /tmp/post_clone.sh
chmod +x /tmp/post_clone.sh
cd "$CLONE_PATH"
/tmp/post_clone.sh
rm /tmp/post_clone.sh
fi
+2 -2
View File
@@ -13,7 +13,7 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener
```tf
module "goose" {
source = "registry.coder.com/coder/goose/coder"
version = "2.1.2"
version = "2.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
@@ -79,7 +79,7 @@ resource "coder_agent" "main" {
module "goose" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/goose/coder"
version = "2.1.2"
version = "2.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
+4 -2
View File
@@ -63,7 +63,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 "subdomain" {
@@ -135,11 +135,12 @@ EOT
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".goose-module"
folder = trimsuffix(var.folder, "/")
}
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
@@ -156,6 +157,7 @@ module "agentapi" {
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = local.start_script
folder = local.folder
install_script = <<-EOT
#!/bin/bash
set -o errexit
@@ -10,6 +10,9 @@ tags: [ide, jetbrains, parameter, gateway]
This module adds a JetBrains Gateway Button to open any workspace with a single click.
> [!TIP]
> We recommend using the [Coder Toolbox module](https://registry.coder.com/modules/coder/jetbrains), which offers significant stability and connectivity benefits over Gateway. Reference our [documentation](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) for more information.
JetBrains recommends a minimum of 4 CPU cores and 8GB of RAM.
Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prerequisites.html#min_requirements) to confirm other system requirements.
@@ -17,7 +20,7 @@ Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prereq
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.2"
version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
@@ -35,7 +38,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.2"
version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
@@ -49,7 +52,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.2"
version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["IU", "PY"]
@@ -64,7 +67,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.2"
version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["IU", "PY"]
@@ -89,7 +92,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.2"
version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
@@ -107,7 +110,7 @@ Due to the highest priority of the `ide_download_link` parameter in the `(jetbra
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.2"
version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
@@ -20,7 +20,7 @@ describe("jetbrains-gateway", async () => {
folder: "/home/coder",
});
expect(state.outputs.url.value).toBe(
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent_id=foo",
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent=",
);
const coder_app = state.resources.find(
@@ -40,4 +40,28 @@ describe("jetbrains-gateway", async () => {
});
expect(state.outputs.identifier.value).toBe("IU");
});
it("optionally includes agent when an agent name is provided", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
agent_name: "main",
folder: "/home/coder",
});
expect(state.outputs.url.value).toBe(
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent=main",
);
});
it("includes the agent parameter even when the provided value is blank", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
agent_name: " ",
folder: "/home/coder",
});
expect(state.outputs.url.value).toBe(
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent= ",
);
});
});
@@ -30,15 +30,14 @@ variable "agent_id" {
variable "slug" {
type = string
description = "The slug for the coder_app. Allows resuing the module with the same template."
description = "The slug for the coder_app. Allows reusing the module with the same template."
default = "gateway"
}
variable "agent_name" {
type = string
description = "Agent name. (unused). Will be removed in a future version"
default = ""
description = "Agent name."
default = ""
}
variable "folder" {
@@ -348,8 +347,8 @@ resource "coder_app" "gateway" {
local.build_number,
"&ide_download_link=",
local.download_link,
"&agent_id=",
var.agent_id,
"&agent=",
var.agent_name,
])
}
+1 -1
View File
@@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
module "kasmvnc" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kasmvnc/coder"
version = "1.2.3"
version = "1.2.4"
agent_id = coder_agent.example.id
desktop_environment = "xfce"
subdomain = true
+9 -8
View File
@@ -60,6 +60,9 @@ install_deb() {
sudo apt-get -o DPkg::Lock::Timeout=300 -qq update
fi
echo "Installing required Perl DateTime module..."
DEBIAN_FRONTEND=noninteractive sudo apt-get -o DPkg::Lock::Timeout=300 install --yes -qq --no-install-recommends --no-install-suggests libdatetime-perl
DEBIAN_FRONTEND=noninteractive sudo apt-get -o DPkg::Lock::Timeout=300 install --yes -qq --no-install-recommends --no-install-suggests "$kasmdeb"
rm "$kasmdeb"
}
@@ -233,19 +236,17 @@ get_http_dir() {
# Check the system configuration path
if [[ -e /etc/kasmvnc/kasmvnc.yaml ]]; then
d=($(grep -E "^\s*httpd_directory:.*$" /etc/kasmvnc/kasmvnc.yaml))
# If this grep is successful, it will return:
# httpd_directory: /usr/share/kasmvnc/www
if [[ $${#d[@]} -eq 2 && -d "$${d[1]}" ]]; then
httpd_directory="$${d[1]}"
d=$(grep -E '^\s*httpd_directory:.*$' "/etc/kasmvnc/kasmvnc.yaml" | awk '{print $$2}')
if [[ -n "$d" && -d "$d" ]]; then
httpd_directory=$d
fi
fi
# Check the home directory for overriding values
if [[ -e "$HOME/.vnc/kasmvnc.yaml" ]]; then
d=($(grep -E "^\s*httpd_directory:.*$" "$HOME/.vnc/kasmvnc.yaml"))
if [[ $${#d[@]} -eq 2 && -d "$${d[1]}" ]]; then
httpd_directory="$${d[1]}"
d=$(grep -E '^\s*httpd_directory:.*$' "$HOME/.vnc/kasmvnc.yaml" | awk '{print $$2}')
if [[ -n "$d" && -d "$d" ]]; then
httpd_directory=$d
fi
fi
echo $httpd_directory
@@ -426,15 +426,14 @@ module "code-server" {
# 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
agent_name = "main"
order = 1
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
source = "registry.coder.com/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
+2
View File
@@ -19,3 +19,5 @@ participating in LFX CNCF programs, and helping the developer community grow.
## Modules
- **aws-ami-snapshot**: Create and manage AMI snapshots for Coder workspaces with restore capabilities
- [nexus-repository](./modules/nexus-repository/) - Configure package managers to use Sonatype Nexus Repository
- [auto-start-dev-server](modules/auto-start-dev-server/README.md) - Automatically detect and start development servers for various project types
@@ -0,0 +1,151 @@
---
display_name: Auto-Start Dev Servers
description: Automatically detect and start development servers for various project types
icon: ../../../../.icons/auto-dev-server.svg
verified: false
tags: [development, automation, servers]
---
# Auto-Start Development Servers
Automatically detect and start development servers for various project types when a workspace starts. This module scans your workspace for common project structures and starts the appropriate development servers in the background without manual intervention.
```tf
module "auto_start_dev_servers" {
source = "registry.coder.com/mavrickrishi/auto-start-dev-server/coder"
version = "1.0.1"
agent_id = coder_agent.main.id
}
```
## Features
- **Multi-language support**: Detects and starts servers for Node.js, Python (Django/Flask), Ruby (Rails), Java (Spring Boot), Go, PHP, Rust, and .NET projects
- **Smart script prioritization**: Prioritizes `dev` scripts over `start` scripts for better development experience
- **Intelligent frontend detection**: Automatically identifies frontend projects (React, Vue, Angular, Next.js, Nuxt, Svelte, Vite) and prioritizes them for preview apps
- **Devcontainer integration**: Respects custom start commands defined in `.devcontainer/devcontainer.json`
- **Configurable scanning**: Adjustable directory scan depth and project type toggles
- **Non-blocking startup**: Servers start in the background with configurable startup delay
- **Comprehensive logging**: All server output and detection results logged to a central file
- **Smart detection**: Uses project-specific files and configurations to identify project types
- **Integrated live preview**: Automatically creates a preview app for the primary frontend project
## Supported Project Types
| Framework/Language | Detection Files | Start Commands (in priority order) |
| ------------------ | -------------------------------------------- | ----------------------------------------------------- |
| **Node.js/npm** | `package.json` | `npm run dev`, `npm run serve`, `npm start` (or yarn) |
| **Ruby on Rails** | `Gemfile` with rails gem | `bundle exec rails server` |
| **Django** | `manage.py` | `python manage.py runserver` |
| **Flask** | `requirements.txt` with Flask | `python app.py/main.py/run.py` |
| **Spring Boot** | `pom.xml` or `build.gradle` with spring-boot | `mvn spring-boot:run`, `gradle bootRun` |
| **Go** | `go.mod` | `go run main.go` |
| **PHP** | `composer.json` | `php -S 0.0.0.0:8080` |
| **Rust** | `Cargo.toml` | `cargo run` |
| **.NET** | `*.csproj` | `dotnet run` |
## Examples
### Basic Usage
```tf
module "auto_start" {
source = "./modules/auto-start-dev-server"
version = "1.0.1"
agent_id = coder_agent.main.id
}
```
### Advanced Usage
```tf
module "auto_start_dev_servers" {
source = "./modules/auto-start-dev-server"
version = "1.0.1"
agent_id = coder_agent.main.id
# Optional: Configure which project types to detect
enable_npm = true
enable_rails = true
enable_django = true
enable_flask = true
enable_spring_boot = true
enable_go = true
enable_php = true
enable_rust = true
enable_dotnet = true
# Optional: Enable devcontainer.json integration
enable_devcontainer = true
# Optional: Workspace directory to scan (supports environment variables)
workspace_directory = "$HOME"
# Optional: Directory scan depth (1-5)
scan_depth = 2
# Optional: Startup delay in seconds
startup_delay = 10
# Optional: Log file path
log_path = "/tmp/dev-servers.log"
# Optional: Enable automatic preview app (default: true)
enable_preview_app = true
}
```
### Disable Preview App
```tf
module "auto_start" {
source = "./modules/auto-start-dev-server"
version = "1.0.1"
agent_id = coder_agent.main.id
# Disable automatic preview app creation
enable_preview_app = false
}
```
### Selective Project Types
```tf
module "auto_start" {
source = "./modules/auto-start-dev-server"
version = "1.0.1"
agent_id = coder_agent.main.id
# Only enable web development projects
enable_npm = true
enable_rails = true
enable_django = true
enable_flask = true
# Disable other project types
enable_spring_boot = false
enable_go = false
enable_php = false
enable_rust = false
enable_dotnet = false
}
```
### Deep Workspace Scanning
```tf
module "auto_start" {
source = "./modules/auto-start-dev-server"
version = "1.0.1"
agent_id = coder_agent.main.id
workspace_directory = "/workspaces"
scan_depth = 3
startup_delay = 5
log_path = "/var/log/dev-servers.log"
}
```
## License
This module is provided under the same license as the Coder Registry.
@@ -0,0 +1,109 @@
import { describe, expect, it } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
describe("auto-start-dev-server", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "test-agent-123",
});
it("validates scan_depth range", () => {
const t1 = async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "test-agent-123",
scan_depth: "0",
});
};
expect(t1).toThrow("Scan depth must be between 1 and 5");
const t2 = async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "test-agent-123",
scan_depth: "6",
});
};
expect(t2).toThrow("Scan depth must be between 1 and 5");
});
it("applies successfully with default values", async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "test-agent-123",
});
});
it("applies successfully with all project types enabled", async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "test-agent-123",
enable_npm: "true",
enable_rails: "true",
enable_django: "true",
enable_flask: "true",
enable_spring_boot: "true",
enable_go: "true",
enable_php: "true",
enable_rust: "true",
enable_dotnet: "true",
enable_devcontainer: "true",
});
});
it("applies successfully with all project types disabled", async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "test-agent-123",
enable_npm: "false",
enable_rails: "false",
enable_django: "false",
enable_flask: "false",
enable_spring_boot: "false",
enable_go: "false",
enable_php: "false",
enable_rust: "false",
enable_dotnet: "false",
enable_devcontainer: "false",
});
});
it("applies successfully with custom configuration", async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "test-agent-123",
workspace_directory: "/custom/workspace",
scan_depth: "3",
startup_delay: "5",
log_path: "/var/log/custom-dev-servers.log",
display_name: "Custom Dev Server Startup",
});
});
it("validates scan_depth boundary values", async () => {
// Test valid boundary values
await runTerraformApply(import.meta.dir, {
agent_id: "test-agent-123",
scan_depth: "1",
});
await runTerraformApply(import.meta.dir, {
agent_id: "test-agent-123",
scan_depth: "5",
});
});
it("applies with selective project type configuration", async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "test-agent-123",
enable_npm: "true",
enable_django: "true",
enable_go: "true",
enable_rails: "false",
enable_flask: "false",
enable_spring_boot: "false",
enable_php: "false",
enable_rust: "false",
enable_dotnet: "false",
});
});
});
@@ -0,0 +1,195 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "workspace_directory" {
type = string
description = "The directory to scan for development projects."
default = "$HOME"
}
variable "project_detection" {
type = bool
description = "Enable automatic project detection for all supported types. When true, all project types are detected unless individually disabled. When false, only explicitly enabled project types are detected."
default = true
}
variable "enable_npm" {
type = bool
description = "Enable auto-detection and startup of npm projects."
default = null
}
variable "enable_rails" {
type = bool
description = "Enable auto-detection and startup of Rails projects."
default = null
}
variable "enable_django" {
type = bool
description = "Enable auto-detection and startup of Django projects."
default = null
}
variable "enable_flask" {
type = bool
description = "Enable auto-detection and startup of Flask projects."
default = null
}
variable "enable_spring_boot" {
type = bool
description = "Enable auto-detection and startup of Spring Boot projects."
default = null
}
variable "enable_go" {
type = bool
description = "Enable auto-detection and startup of Go projects."
default = null
}
variable "enable_php" {
type = bool
description = "Enable auto-detection and startup of PHP projects."
default = null
}
variable "enable_rust" {
type = bool
description = "Enable auto-detection and startup of Rust projects."
default = null
}
variable "enable_dotnet" {
type = bool
description = "Enable auto-detection and startup of .NET projects."
default = null
}
variable "enable_devcontainer" {
type = bool
description = "Enable integration with devcontainer.json configuration."
default = null
}
variable "log_path" {
type = string
description = "The path to log development server output to."
default = "/tmp/dev-servers.log"
}
variable "scan_depth" {
type = number
description = "Maximum directory depth to scan for projects (1-5)."
default = 2
validation {
condition = var.scan_depth >= 1 && var.scan_depth <= 5
error_message = "Scan depth must be between 1 and 5."
}
}
variable "startup_delay" {
type = number
description = "Delay in seconds before starting dev servers (allows other setup to complete)."
default = 10
}
variable "display_name" {
type = string
description = "Display name for the auto-start dev server script."
default = "Auto-Start Dev Servers"
}
variable "enable_preview_app" {
type = bool
description = "Enable automatic creation of a preview app for the first detected project."
default = true
}
# Read the detected port from the file written by the script
locals {
detected_port = var.enable_preview_app ? try(tonumber(trimspace(file("/tmp/detected-port.txt"))), 3000) : 3000
# Attempt to read project information for better preview naming
detected_projects = try(jsondecode(file("/tmp/detected-projects.json")), [])
preview_project = length(local.detected_projects) > 0 ? local.detected_projects[0] : null
}
resource "coder_script" "auto_start_dev_server" {
agent_id = var.agent_id
display_name = var.display_name
icon = "/icon/auto-dev-server.svg"
script = templatefile("${path.module}/run.sh", {
WORKSPACE_DIR = var.workspace_directory
ENABLE_NPM = coalesce(var.enable_npm, var.project_detection)
ENABLE_RAILS = coalesce(var.enable_rails, var.project_detection)
ENABLE_DJANGO = coalesce(var.enable_django, var.project_detection)
ENABLE_FLASK = coalesce(var.enable_flask, var.project_detection)
ENABLE_SPRING_BOOT = coalesce(var.enable_spring_boot, var.project_detection)
ENABLE_GO = coalesce(var.enable_go, var.project_detection)
ENABLE_PHP = coalesce(var.enable_php, var.project_detection)
ENABLE_RUST = coalesce(var.enable_rust, var.project_detection)
ENABLE_DOTNET = coalesce(var.enable_dotnet, var.project_detection)
ENABLE_DEVCONTAINER = coalesce(var.enable_devcontainer, var.project_detection)
LOG_PATH = var.log_path
SCAN_DEPTH = var.scan_depth
STARTUP_DELAY = var.startup_delay
})
run_on_start = true
}
# Create preview app for first detected project
resource "coder_app" "preview" {
count = var.enable_preview_app ? 1 : 0
agent_id = var.agent_id
slug = "dev-preview"
display_name = "Live Preview"
url = "http://localhost:${local.detected_port}"
icon = "/icon/auto-dev-server.svg"
subdomain = true
share = "owner"
}
output "log_path" {
value = var.log_path
description = "Path to the log file for dev server output"
}
# Example output values for common port mappings
output "common_ports" {
value = {
nodejs = 3000
rails = 3000
django = 8000
flask = 5000
spring = 8080
go = 8080
php = 8080
rust = 8000
dotnet = 5000
}
description = "Common default ports for different project types"
}
output "preview_url" {
value = var.enable_preview_app ? try(coder_app.preview[0].url, null) : null
description = "URL of the live preview app (if enabled)"
}
output "detected_port" {
value = local.detected_port
description = "Port of the first detected development server"
}
+468
View File
@@ -0,0 +1,468 @@
#!/usr/bin/env bash
set -euo pipefail
# Color codes for output
BOLD='\033[0;1m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
RED='\033[0;31m'
RESET='\033[0m'
echo -e "$${BOLD}🚀 Auto-Start Development Servers$${RESET}"
echo "Workspace Directory: ${WORKSPACE_DIR}"
echo "Log Path: ${LOG_PATH}"
echo "Scan Depth: ${SCAN_DEPTH}"
# Wait for startup delay to allow other setup to complete
if [ "${STARTUP_DELAY}" -gt 0 ]; then
echo -e "$${YELLOW}⏳ Waiting ${STARTUP_DELAY} seconds for system initialization...$${RESET}"
sleep "${STARTUP_DELAY}"
fi
# Initialize log file
echo "=== Auto-Start Dev Servers Log ===" > "${LOG_PATH}"
echo "Started at: $(date)" >> "${LOG_PATH}"
# Initialize detected projects JSON file
DETECTED_PROJECTS_FILE="/tmp/detected-projects.json"
echo '[]' > "$DETECTED_PROJECTS_FILE"
# Initialize detected port file for preview app
DETECTED_PORT_FILE="/tmp/detected-port.txt"
FIRST_PORT_DETECTED=false
FRONTEND_PROJECT_DETECTED=false
# Function to log messages
log_message() {
echo -e "$1"
echo "$1" >> "${LOG_PATH}"
}
# Function to determine if a project is likely a frontend project
is_frontend_project() {
local project_dir="$1"
local project_type="$2"
# Check for common frontend indicators
if [ "$project_type" = "nodejs" ]; then
# Check package.json for frontend dependencies
if [ -f "$project_dir/package.json" ] && command -v jq &> /dev/null; then
# Check for common frontend frameworks
local has_react=$(jq '.dependencies.react // .devDependencies.react // empty' "$project_dir/package.json")
local has_vue=$(jq '.dependencies.vue // .devDependencies.vue // empty' "$project_dir/package.json")
local has_angular=$(jq '.dependencies["@angular/core"] // .devDependencies["@angular/core"] // empty' "$project_dir/package.json")
local has_next=$(jq '.dependencies.next // .devDependencies.next // empty' "$project_dir/package.json")
local has_nuxt=$(jq '.dependencies.nuxt // .devDependencies.nuxt // empty' "$project_dir/package.json")
local has_svelte=$(jq '.dependencies.svelte // .devDependencies.svelte // empty' "$project_dir/package.json")
local has_vite=$(jq '.dependencies.vite // .devDependencies.vite // empty' "$project_dir/package.json")
if [ -n "$has_react" ] || [ -n "$has_vue" ] || [ -n "$has_angular" ] \
|| [ -n "$has_next" ] || [ -n "$has_nuxt" ] || [ -n "$has_svelte" ] \
|| [ -n "$has_vite" ]; then
return 0 # It's a frontend project
fi
fi
# Check for common frontend directory structures
if [ -d "$project_dir/src/components" ] || [ -d "$project_dir/components" ] \
|| [ -d "$project_dir/pages" ] || [ -d "$project_dir/views" ] \
|| [ -f "$project_dir/index.html" ] || [ -f "$project_dir/public/index.html" ]; then
return 0 # It's likely a frontend project
fi
fi
# Rails projects with webpack/webpacker are frontend-enabled
if [ "$project_type" = "rails" ]; then
if [ -f "$project_dir/config/webpacker.yml" ] || [ -f "$project_dir/webpack.config.js" ]; then
return 0
fi
fi
# Django projects with static/templates are frontend-enabled
if [ "$project_type" = "django" ]; then
if [ -d "$project_dir/static" ] || [ -d "$project_dir/templates" ]; then
return 0
fi
fi
return 1 # Not a frontend project
}
# Function to add detected project to JSON
add_detected_project() {
local project_dir="$1"
local project_type="$2"
local port="$3"
local command="$4"
# Check if this is a frontend project
local is_frontend=false
if is_frontend_project "$project_dir" "$project_type"; then
is_frontend=true
log_message "$${BLUE}🎨 Detected frontend project at $project_dir$${RESET}"
fi
# Prioritize frontend projects for the preview app
# Set port if: 1) No port set yet, OR 2) This is frontend and no frontend detected yet
if [ "$FIRST_PORT_DETECTED" = false ] || ([ "$is_frontend" = true ] && [ "$FRONTEND_PROJECT_DETECTED" = false ]); then
echo "$port" > "$DETECTED_PORT_FILE"
FIRST_PORT_DETECTED=true
if [ "$is_frontend" = true ]; then
FRONTEND_PROJECT_DETECTED=true
log_message "$${BLUE}🎯 Frontend project detected - Preview app will be available on port $port$${RESET}"
else
log_message "$${BLUE}🎯 Project detected - Preview app will be available on port $port$${RESET}"
fi
fi
# Create JSON entry for this project
local project_json=$(jq -n \
--arg dir "$project_dir" \
--arg type "$project_type" \
--arg port "$port" \
--arg cmd "$command" \
--arg frontend "$is_frontend" \
'{"directory": $dir, "type": $type, "port": $port, "command": $cmd, "is_frontend": ($frontend == "true")}')
# Append to the detected projects file
jq ". += [$project_json]" "$DETECTED_PROJECTS_FILE" > "$DETECTED_PROJECTS_FILE.tmp" \
&& mv "$DETECTED_PROJECTS_FILE.tmp" "$DETECTED_PROJECTS_FILE"
}
# Function to detect and start npm/yarn projects
detect_npm_projects() {
if [ "${ENABLE_NPM}" != "true" ]; then
return
fi
log_message "$${BLUE}🔍 Scanning for Node.js/npm projects...$${RESET}"
# Use find with maxdepth to respect scan depth
while IFS= read -r -d '' package_json; do
project_dir=$(dirname "$package_json")
log_message "$${GREEN}📦 Found Node.js project: $project_dir$${RESET}"
cd "$project_dir"
# Check package.json for start script
if [ -f "package.json" ] && command -v jq &> /dev/null; then
start_script=$(jq -r '.scripts.start // empty' package.json)
dev_script=$(jq -r '.scripts.dev // empty' package.json)
serve_script=$(jq -r '.scripts.serve // empty' package.json)
# Determine port (check for common port configurations)
local project_port=3000
if [ -n "$dev_script" ] && echo "$dev_script" | grep -q "\-\-port"; then
project_port=$(echo "$dev_script" | grep -oE "\-\-port[[:space:]]+[0-9]+" | grep -oE "[0-9]+$" || echo "3000")
fi
# Use yarn if yarn.lock exists
local pkg_manager="npm"
local cmd_prefix=""
if [ -f "yarn.lock" ] && command -v yarn &> /dev/null; then
pkg_manager="yarn"
cmd_prefix=""
else
cmd_prefix="run "
fi
# Prioritize scripts: 'dev' > 'serve' > 'start' for development environments
if [ -n "$dev_script" ]; then
if [ "$pkg_manager" = "yarn" ]; then
log_message "$${GREEN}🟢 Starting project with 'yarn dev' in $project_dir$${RESET}"
nohup yarn dev >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "nodejs" "$project_port" "yarn dev"
else
log_message "$${GREEN}🟢 Starting project with 'npm run dev' in $project_dir$${RESET}"
nohup npm run dev >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "nodejs" "$project_port" "npm run dev"
fi
elif [ -n "$serve_script" ]; then
if [ "$pkg_manager" = "yarn" ]; then
log_message "$${GREEN}🟢 Starting project with 'yarn serve' in $project_dir$${RESET}"
nohup yarn serve >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "nodejs" "$project_port" "yarn serve"
else
log_message "$${GREEN}🟢 Starting project with 'npm run serve' in $project_dir$${RESET}"
nohup npm run serve >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "nodejs" "$project_port" "npm run serve"
fi
elif [ -n "$start_script" ]; then
if [ "$pkg_manager" = "yarn" ]; then
log_message "$${GREEN}🟢 Starting project with 'yarn start' in $project_dir$${RESET}"
nohup yarn start >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "nodejs" "$project_port" "yarn start"
else
log_message "$${GREEN}🟢 Starting project with 'npm start' in $project_dir$${RESET}"
nohup npm start >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "nodejs" "$project_port" "npm start"
fi
fi
fi
done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "package.json" -type f -print0)
}
# Function to detect and start Rails projects
detect_rails_projects() {
if [ "${ENABLE_RAILS}" != "true" ]; then
return
fi
log_message "$${BLUE}🔍 Scanning for Ruby on Rails projects...$${RESET}"
while IFS= read -r -d '' gemfile; do
project_dir=$(dirname "$gemfile")
log_message "$${GREEN}💎 Found Rails project: $project_dir$${RESET}"
cd "$project_dir"
# Check if it's actually a Rails project
if grep -q "gem ['\"]rails['\"]" Gemfile 2> /dev/null; then
log_message "$${GREEN}🟢 Starting Rails server in $project_dir$${RESET}"
nohup bundle exec rails server >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "rails" "3000" "bundle exec rails server"
fi
done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "Gemfile" -type f -print0)
}
# Function to detect and start Django projects
detect_django_projects() {
if [ "${ENABLE_DJANGO}" != "true" ]; then
return
fi
log_message "$${BLUE}🔍 Scanning for Django projects...$${RESET}"
while IFS= read -r -d '' manage_py; do
project_dir=$(dirname "$manage_py")
log_message "$${GREEN}🐍 Found Django project: $project_dir$${RESET}"
cd "$project_dir"
log_message "$${GREEN}🟢 Starting Django development server in $project_dir$${RESET}"
nohup python manage.py runserver 0.0.0.0:8000 >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "django" "8000" "python manage.py runserver"
done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "manage.py" -type f -print0)
}
# Function to detect and start Flask projects
detect_flask_projects() {
if [ "${ENABLE_FLASK}" != "true" ]; then
return
fi
log_message "$${BLUE}🔍 Scanning for Flask projects...$${RESET}"
while IFS= read -r -d '' requirements_txt; do
project_dir=$(dirname "$requirements_txt")
# Check if Flask is in requirements
if grep -q -i "flask" "$requirements_txt" 2> /dev/null; then
log_message "$${GREEN}🌶️ Found Flask project: $project_dir$${RESET}"
cd "$project_dir"
# Look for common Flask app files
for app_file in app.py main.py run.py; do
if [ -f "$app_file" ]; then
log_message "$${GREEN}🟢 Starting Flask application ($app_file) in $project_dir$${RESET}"
export FLASK_ENV=development
nohup python "$app_file" >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "flask" "5000" "python $app_file"
break
fi
done
fi
done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "requirements.txt" -type f -print0)
}
# Function to detect and start Spring Boot projects
detect_spring_boot_projects() {
if [ "${ENABLE_SPRING_BOOT}" != "true" ]; then
return
fi
log_message "$${BLUE}🔍 Scanning for Spring Boot projects...$${RESET}"
# Maven projects
while IFS= read -r -d '' pom_xml; do
project_dir=$(dirname "$pom_xml")
# Check if it's a Spring Boot project
if grep -q "spring-boot" "$pom_xml" 2> /dev/null; then
log_message "$${GREEN}🍃 Found Spring Boot Maven project: $project_dir$${RESET}"
cd "$project_dir"
if command -v ./mvnw &> /dev/null; then
log_message "$${GREEN}🟢 Starting Spring Boot application with Maven wrapper in $project_dir$${RESET}"
nohup ./mvnw spring-boot:run >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "spring-boot" "8080" "./mvnw spring-boot:run"
elif command -v mvn &> /dev/null; then
log_message "$${GREEN}🟢 Starting Spring Boot application with Maven in $project_dir$${RESET}"
nohup mvn spring-boot:run >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "spring-boot" "8080" "mvn spring-boot:run"
fi
fi
done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "pom.xml" -type f -print0)
# Gradle projects
while IFS= read -r -d '' build_gradle; do
project_dir=$(dirname "$build_gradle")
# Check if it's a Spring Boot project
if grep -q "spring-boot" "$build_gradle" 2> /dev/null; then
log_message "$${GREEN}🍃 Found Spring Boot Gradle project: $project_dir$${RESET}"
cd "$project_dir"
if command -v ./gradlew &> /dev/null; then
log_message "$${GREEN}🟢 Starting Spring Boot application with Gradle wrapper in $project_dir$${RESET}"
nohup ./gradlew bootRun >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "spring-boot" "8080" "./gradlew bootRun"
elif command -v gradle &> /dev/null; then
log_message "$${GREEN}🟢 Starting Spring Boot application with Gradle in $project_dir$${RESET}"
nohup gradle bootRun >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "spring-boot" "8080" "gradle bootRun"
fi
fi
done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "build.gradle" -type f -print0)
}
# Function to detect and start Go projects
detect_go_projects() {
if [ "${ENABLE_GO}" != "true" ]; then
return
fi
log_message "$${BLUE}🔍 Scanning for Go projects...$${RESET}"
while IFS= read -r -d '' go_mod; do
project_dir=$(dirname "$go_mod")
log_message "$${GREEN}🐹 Found Go project: $project_dir$${RESET}"
cd "$project_dir"
# Look for main.go or check if there's a main function
if [ -f "main.go" ]; then
log_message "$${GREEN}🟢 Starting Go application in $project_dir$${RESET}"
nohup go run main.go >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "go" "8080" "go run main.go"
elif [ -f "cmd/main.go" ]; then
log_message "$${GREEN}🟢 Starting Go application (cmd/main.go) in $project_dir$${RESET}"
nohup go run cmd/main.go >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "go" "8080" "go run cmd/main.go"
fi
done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "go.mod" -type f -print0)
}
# Function to detect and start PHP projects
detect_php_projects() {
if [ "${ENABLE_PHP}" != "true" ]; then
return
fi
log_message "$${BLUE}🔍 Scanning for PHP projects...$${RESET}"
while IFS= read -r -d '' composer_json; do
project_dir=$(dirname "$composer_json")
log_message "$${GREEN}🐘 Found PHP project: $project_dir$${RESET}"
cd "$project_dir"
# Look for common PHP entry points
for entry_file in index.php public/index.php; do
if [ -f "$entry_file" ]; then
log_message "$${GREEN}🟢 Starting PHP development server in $project_dir$${RESET}"
nohup php -S 0.0.0.0:8080 -t "$(dirname "$entry_file")" >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "php" "8080" "php -S 0.0.0.0:8080"
break
fi
done
done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "composer.json" -type f -print0)
}
# Function to detect and start Rust projects
detect_rust_projects() {
if [ "${ENABLE_RUST}" != "true" ]; then
return
fi
log_message "$${BLUE}🔍 Scanning for Rust projects...$${RESET}"
while IFS= read -r -d '' cargo_toml; do
project_dir=$(dirname "$cargo_toml")
log_message "$${GREEN}🦀 Found Rust project: $project_dir$${RESET}"
cd "$project_dir"
# Check if it's a binary project (has [[bin]] or default main.rs)
if grep -q "\[\[bin\]\]" Cargo.toml 2> /dev/null || [ -f "src/main.rs" ]; then
log_message "$${GREEN}🟢 Starting Rust application in $project_dir$${RESET}"
nohup cargo run >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "rust" "8000" "cargo run"
fi
done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "Cargo.toml" -type f -print0)
}
# Function to detect and start .NET projects
detect_dotnet_projects() {
if [ "${ENABLE_DOTNET}" != "true" ]; then
return
fi
log_message "$${BLUE}🔍 Scanning for .NET projects...$${RESET}"
while IFS= read -r -d '' csproj; do
project_dir=$(dirname "$csproj")
log_message "$${GREEN}🔷 Found .NET project: $project_dir$${RESET}"
cd "$project_dir"
log_message "$${GREEN}🟢 Starting .NET application in $project_dir$${RESET}"
nohup dotnet run >> "${LOG_PATH}" 2>&1 &
add_detected_project "$project_dir" "dotnet" "5000" "dotnet run"
done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "*.csproj" -type f -print0)
}
log_message "Starting auto-detection of development projects..."
# Expand workspace directory if it contains variables
WORKSPACE_DIR=$(eval echo "${WORKSPACE_DIR}")
# Check if workspace directory exists
if [ ! -d "$WORKSPACE_DIR" ]; then
log_message "$${RED}❌ Workspace directory does not exist: $WORKSPACE_DIR$${RESET}"
exit 1
fi
cd "$WORKSPACE_DIR"
# Run all detection functions
detect_npm_projects
detect_rails_projects
detect_django_projects
detect_flask_projects
detect_spring_boot_projects
detect_go_projects
detect_php_projects
detect_rust_projects
detect_dotnet_projects
log_message "$${GREEN}✅ Auto-start scan completed!$${RESET}"
log_message "$${YELLOW}💡 Check running processes with 'ps aux | grep -E \"(npm|rails|python|java|go|php|cargo|dotnet)\"'$${RESET}"
log_message "$${YELLOW}💡 View logs: tail -f ${LOG_PATH}$${RESET}"
# Set default port if no projects were detected
if [ "$FIRST_PORT_DETECTED" = false ]; then
echo "3000" > "$DETECTED_PORT_FILE"
log_message "$${YELLOW}⚠️ No projects detected - Preview app will default to port 3000$${RESET}"
fi
@@ -0,0 +1,149 @@
---
display_name: Nexus Repository
description: Configure package managers to use Sonatype Nexus Repository for Maven, npm, PyPI, and Docker registries.
icon: ../../../../.icons/nexus-repository.svg
verified: false
tags: [integration, nexus-repository, maven, npm, pypi, docker]
---
# Sonatype Nexus Repository
Configure package managers (Maven, npm, Go, PyPI, Docker) to use [Sonatype Nexus Repository](https://help.sonatype.com/en/sonatype-nexus-repository.html) with API token authentication. This module provides secure credential handling, multiple repository support per package manager, and flexible username configuration.
```tf
module "nexus_repository" {
source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
nexus_url = "https://nexus.example.com"
nexus_password = var.nexus_api_token
package_managers = {
maven = ["maven-public", "maven-releases"]
npm = ["npm-public", "@scoped:npm-private"]
go = ["go-public", "go-private"]
pypi = ["pypi-public", "pypi-private"]
docker = ["docker-public", "docker-private"]
}
}
```
## Requirements
- Nexus Repository Manager 3.x
- Valid API token or user credentials
- Package managers installed on the workspace (Maven, npm, Go, pip, Docker as needed)
> [!NOTE]
> This module configures package managers but does not install them. You need to handle the installation of Maven, npm, Go, Python pip, and Docker yourself.
## Examples
### Configure Maven to use Nexus repositories
```tf
module "nexus_repository" {
source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
nexus_url = "https://nexus.example.com"
nexus_password = var.nexus_api_token
package_managers = {
maven = ["maven-public", "maven-releases", "maven-snapshots"]
}
}
```
### Configure npm with scoped packages
```tf
module "nexus_repository" {
source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
nexus_url = "https://nexus.example.com"
nexus_password = var.nexus_api_token
package_managers = {
npm = ["npm-public", "@mycompany:npm-private"]
}
}
```
### Configure Go module proxy
```tf
module "nexus_repository" {
source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
nexus_url = "https://nexus.example.com"
nexus_password = var.nexus_api_token
package_managers = {
go = ["go-public", "go-private"]
}
}
```
### Configure Python PyPI repositories
```tf
module "nexus_repository" {
source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
nexus_url = "https://nexus.example.com"
nexus_password = var.nexus_api_token
package_managers = {
pypi = ["pypi-public", "pypi-private"]
}
}
```
### Configure Docker registries
```tf
module "nexus_repository" {
source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
nexus_url = "https://nexus.example.com"
nexus_password = var.nexus_api_token
package_managers = {
docker = ["docker-public", "docker-private"]
}
}
```
### Use custom username
```tf
module "nexus_repository" {
source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
nexus_url = "https://nexus.example.com"
nexus_username = "custom-user"
nexus_password = var.nexus_api_token
package_managers = {
maven = ["maven-public"]
}
}
```
### Complete configuration for all package managers
```tf
module "nexus_repository" {
source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
nexus_url = "https://nexus.example.com"
nexus_password = var.nexus_api_token
package_managers = {
maven = ["maven-public", "maven-releases"]
npm = ["npm-public", "@company:npm-private"]
go = ["go-public", "go-private"]
pypi = ["pypi-public", "pypi-private"]
docker = ["docker-public", "docker-private"]
}
}
```
@@ -0,0 +1,147 @@
import { describe, expect, it } from "bun:test";
import {
executeScriptInContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
describe("nexus-repository", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "test-agent",
nexus_url: "https://nexus.example.com",
nexus_password: "test-password",
});
it("configures Maven settings", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
nexus_url: "https://nexus.example.com",
nexus_password: "test-token",
package_managers: JSON.stringify({
maven: ["maven-public"],
}),
});
const output = await executeScriptInContainer(state, "ubuntu:20.04");
expect(output.stdout.join("\n")).toContain("☕ Configuring Maven...");
expect(output.stdout.join("\n")).toContain("🥳 Configuration complete!");
});
it("configures npm registry", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
nexus_url: "https://nexus.example.com",
nexus_password: "test-token",
package_managers: JSON.stringify({
npm: ["npm-public"],
}),
});
const output = await executeScriptInContainer(state, "ubuntu:20.04");
expect(output.stdout.join("\n")).toContain("📦 Configuring npm...");
expect(output.stdout.join("\n")).toContain("🥳 Configuration complete!");
});
it("configures PyPI repository", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
nexus_url: "https://nexus.example.com",
nexus_password: "test-token",
package_managers: JSON.stringify({
pypi: ["pypi-public"],
}),
});
const output = await executeScriptInContainer(state, "ubuntu:20.04");
expect(output.stdout.join("\n")).toContain("🐍 Configuring pip...");
expect(output.stdout.join("\n")).toContain("🥳 Configuration complete!");
});
it("configures multiple package managers", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
nexus_url: "https://nexus.example.com",
nexus_password: "test-token",
package_managers: JSON.stringify({
maven: ["maven-public"],
npm: ["npm-public"],
pypi: ["pypi-public"],
}),
});
const output = await executeScriptInContainer(state, "ubuntu:20.04");
expect(output.stdout.join("\n")).toContain("☕ Configuring Maven...");
expect(output.stdout.join("\n")).toContain("📦 Configuring npm...");
expect(output.stdout.join("\n")).toContain("🐍 Configuring pip...");
expect(output.stdout.join("\n")).toContain(
"✅ Nexus repository configuration completed!",
);
});
it("handles empty package managers", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
nexus_url: "https://nexus.example.com",
nexus_password: "test-token",
package_managers: JSON.stringify({}),
});
const output = await executeScriptInContainer(state, "ubuntu:20.04");
expect(output.stdout.join("\n")).toContain(
"🤔 no maven repository is set, skipping maven configuration.",
);
expect(output.stdout.join("\n")).toContain(
"🤔 no npm repository is set, skipping npm configuration.",
);
expect(output.stdout.join("\n")).toContain(
"🤔 no pypi repository is set, skipping pypi configuration.",
);
expect(output.stdout.join("\n")).toContain(
"🤔 no docker repository is set, skipping docker configuration.",
);
});
it("configures Go module proxy", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
nexus_url: "https://nexus.example.com",
nexus_password: "test-token",
package_managers: JSON.stringify({
go: ["go-public", "go-private"],
}),
});
const output = await executeScriptInContainer(state, "ubuntu:20.04");
expect(output.stdout.join("\n")).toContain("🐹 Configuring Go...");
expect(output.stdout.join("\n")).toContain(
"Go proxy configured via GOPROXY environment variable",
);
expect(output.stdout.join("\n")).toContain("🥳 Configuration complete!");
});
it("validates nexus_url format", async () => {
await expect(
runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
nexus_url: "invalid-url",
nexus_password: "test-token",
package_managers: JSON.stringify({}),
}),
).rejects.toThrow();
});
it("validates username_field values", async () => {
await expect(
runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
nexus_url: "https://nexus.example.com",
nexus_password: "test-token",
username_field: "invalid",
package_managers: JSON.stringify({}),
}),
).rejects.toThrow();
});
});
@@ -0,0 +1,137 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
variable "nexus_url" {
type = string
description = "The base URL of your Nexus repository manager (e.g. https://nexus.example.com)"
validation {
condition = can(regex("^(https|http)://", var.nexus_url))
error_message = "nexus_url must be a valid URL starting with either 'https://' or 'http://'"
}
}
variable "nexus_username" {
type = string
description = "Custom username for Nexus authentication. If not provided, defaults to the Coder username based on the username_field setting"
default = null
}
variable "nexus_password" {
type = string
description = "API token or password for Nexus authentication. This value is sensitive and should be stored securely"
sensitive = true
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "package_managers" {
type = object({
maven = optional(list(string), [])
npm = optional(list(string), [])
go = optional(list(string), [])
pypi = optional(list(string), [])
docker = optional(list(string), [])
})
default = {
maven = []
npm = []
go = []
pypi = []
docker = []
}
description = <<-EOF
Configuration for package managers. Each key maps to a list of Nexus repository names:
- maven: List of Maven repository names
- npm: List of npm repository names (supports scoped packages with "@scope:repo-name")
- go: List of Go proxy repository names
- pypi: List of PyPI repository names
- docker: List of Docker registry names
Unused package managers can be omitted.
Example:
{
maven = ["maven-public", "maven-releases"]
npm = ["npm-public", "@scoped:npm-private"]
go = ["go-public", "go-private"]
pypi = ["pypi-public", "pypi-private"]
docker = ["docker-public", "docker-private"]
}
EOF
}
variable "username_field" {
type = string
description = "Field to use for username (\"username\" or \"email\"). Defaults to \"username\". Only used when nexus_username is not provided"
default = "username"
validation {
condition = can(regex("^(email|username)$", var.username_field))
error_message = "username_field must be either 'email' or 'username'"
}
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
locals {
username = coalesce(var.nexus_username, var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name)
nexus_host = split("/", replace(replace(var.nexus_url, "https://", ""), "http://", ""))[0]
}
locals {
# Get first repository name or use default
maven_repo = length(var.package_managers.maven) > 0 ? var.package_managers.maven[0] : "maven-public"
npm_repo = length(var.package_managers.npm) > 0 ? var.package_managers.npm[0] : "npm-public"
go_repo = length(var.package_managers.go) > 0 ? var.package_managers.go[0] : "go-public"
pypi_repo = length(var.package_managers.pypi) > 0 ? var.package_managers.pypi[0] : "pypi-public"
npmrc = <<-EOF
registry=${var.nexus_url}/repository/${local.npm_repo}/
//${local.nexus_host}/repository/${local.npm_repo}/:username=${local.username}
//${local.nexus_host}/repository/${local.npm_repo}/:_password=${base64encode(var.nexus_password)}
//${local.nexus_host}/repository/${local.npm_repo}/:always-auth=true
EOF
}
resource "coder_script" "nexus" {
agent_id = var.agent_id
display_name = "nexus-repository"
icon = "/icon/nexus-repository.svg"
script = templatefile("${path.module}/run.sh", {
NEXUS_URL = var.nexus_url
NEXUS_HOST = local.nexus_host
NEXUS_USERNAME = local.username
NEXUS_PASSWORD = var.nexus_password
HAS_MAVEN = length(var.package_managers.maven) == 0 ? "" : "YES"
MAVEN_REPO = local.maven_repo
HAS_NPM = length(var.package_managers.npm) == 0 ? "" : "YES"
NPMRC = local.npmrc
HAS_GO = length(var.package_managers.go) == 0 ? "" : "YES"
GO_REPO = local.go_repo
HAS_PYPI = length(var.package_managers.pypi) == 0 ? "" : "YES"
PYPI_REPO = local.pypi_repo
HAS_DOCKER = length(var.package_managers.docker) == 0 ? "" : "YES"
REGISTER_DOCKER = join("\n ", formatlist("register_docker \"%s\"", var.package_managers.docker))
})
run_on_start = true
}
resource "coder_env" "goproxy" {
count = length(var.package_managers.go) == 0 ? 0 : 1
agent_id = var.agent_id
name = "GOPROXY"
value = join(",", [
for repo in var.package_managers.go :
"https://${local.username}:${var.nexus_password}@${local.nexus_host}/repository/${repo}"
])
}
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
not_configured() {
type=$1
echo "🤔 no $type repository is set, skipping $type configuration."
}
config_complete() {
echo "🥳 Configuration complete!"
}
register_docker() {
repo=$1
echo -n "${NEXUS_PASSWORD}" | docker login "${NEXUS_HOST}/repository/$${repo}" --username "${NEXUS_USERNAME}" --password-stdin
}
echo "🚀 Configuring Nexus repository access..."
# Configure Maven
if [ -n "${HAS_MAVEN}" ]; then
echo "☕ Configuring Maven..."
mkdir -p ~/.m2
cat > ~/.m2/settings.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0">
<servers>
<server>
<id>nexus</id>
<username>${NEXUS_USERNAME}</username>
<password>${NEXUS_PASSWORD}</password>
</server>
</servers>
<mirrors>
<mirror>
<id>nexus-mirror</id>
<mirrorOf>*</mirrorOf>
<url>${NEXUS_URL}/repository/${MAVEN_REPO}</url>
</mirror>
</mirrors>
</settings>
EOF
config_complete
else
not_configured maven
fi
# Configure npm
if [ -n "${HAS_NPM}" ]; then
echo "📦 Configuring npm..."
cat > ~/.npmrc << 'EOF'
${NPMRC}
EOF
config_complete
else
not_configured npm
fi
# Configure Go
if [ -n "${HAS_GO}" ]; then
echo "🐹 Configuring Go..."
# Go configuration is handled via GOPROXY environment variable
# which is set by the Terraform configuration
echo "Go proxy configured via GOPROXY environment variable"
config_complete
else
not_configured go
fi
# Configure pip
if [ -n "${HAS_PYPI}" ]; then
echo "🐍 Configuring pip..."
mkdir -p ~/.pip
# Create .netrc file for secure credential storage
cat > ~/.netrc << EOF
machine ${NEXUS_HOST}
login ${NEXUS_USERNAME}
password ${NEXUS_PASSWORD}
EOF
chmod 600 ~/.netrc
# Update pip.conf to use index-url without embedded credentials
cat > ~/.pip/pip.conf << 'EOF'
[global]
index-url = https://${NEXUS_HOST}/repository/${PYPI_REPO}/simple
EOF
config_complete
else
not_configured pypi
fi
# Configure Docker
if [ -n "${HAS_DOCKER}" ]; then
if command -v docker > /dev/null 2>&1; then
echo "🐳 Configuring Docker credentials..."
mkdir -p ~/.docker
${REGISTER_DOCKER}
config_complete
else
echo "🤔 Docker is not installed, skipping Docker configuration."
fi
else
not_configured docker
fi
echo "✅ Nexus repository configuration completed!"
+69 -3
View File
@@ -1,7 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
# Find all directories that contain any .tftest.hcl files and run terraform test in each
# Auto-detect which Terraform tests to run based on changed files from paths-filter
# Uses paths-filter outputs from GitHub Actions:
# ALL_CHANGED_FILES - all files changed in the PR (for logging)
# SHARED_CHANGED - boolean indicating if shared infrastructure changed
# MODULE_CHANGED_FILES - only files in registry/**/modules/** (for processing)
# Runs all tests if shared infrastructure changes, or skips if no changes detected
#
# This script only runs tests for changed modules. Documentation and template changes are ignored.
run_dir() {
local dir="$1"
@@ -9,13 +16,72 @@ run_dir() {
(cd "$dir" && terraform init -upgrade -input=false -no-color > /dev/null && terraform test -no-color -verbose)
}
mapfile -t test_dirs < <(find . -type f -name "*.tftest.hcl" -print0 | xargs -0 -I{} dirname {} | sort -u)
echo "==> Detecting changed files..."
if [[ -n "${ALL_CHANGED_FILES:-}" ]]; then
echo "Changed files in PR:"
echo "$ALL_CHANGED_FILES" | tr ' ' '\n' | sed 's/^/ - /'
echo ""
fi
if [[ "${SHARED_CHANGED:-false}" == "true" ]]; then
echo "==> Shared infrastructure changed"
echo "==> Running all tests for safety"
mapfile -t test_dirs < <(find . -type f -name "*.tftest.hcl" -print0 | xargs -0 -I{} dirname {} | sort -u)
elif [[ -z "${MODULE_CHANGED_FILES:-}" ]]; then
echo "✓ No module files changed, skipping tests"
exit 0
else
CHANGED_FILES=$(echo "$MODULE_CHANGED_FILES" | tr ' ' '\n')
MODULE_DIRS=()
while IFS= read -r file; do
if [[ "$file" =~ \.(md|png|jpg|jpeg|svg)$ ]]; then
continue
fi
if [[ "$file" =~ ^registry/([^/]+)/modules/([^/]+)/ ]]; then
namespace="${BASH_REMATCH[1]}"
module="${BASH_REMATCH[2]}"
module_dir="registry/${namespace}/modules/${module}"
if [[ -d "$module_dir" ]] && [[ ! " ${MODULE_DIRS[*]} " =~ " ${module_dir} " ]]; then
MODULE_DIRS+=("$module_dir")
fi
fi
done <<< "$CHANGED_FILES"
if [[ ${#MODULE_DIRS[@]} -eq 0 ]]; then
echo "✓ No Terraform tests to run"
echo " (documentation, templates, namespace files, or modules without changes)"
exit 0
fi
echo "==> Finding .tftest.hcl files in ${#MODULE_DIRS[@]} changed module(s):"
for dir in "${MODULE_DIRS[@]}"; do
echo " - $dir"
done
echo ""
test_dirs=()
for module_dir in "${MODULE_DIRS[@]}"; do
while IFS= read -r test_file; do
test_dir=$(dirname "$test_file")
if [[ ! " ${test_dirs[*]} " =~ " ${test_dir} " ]]; then
test_dirs+=("$test_dir")
fi
done < <(find "$module_dir" -type f -name "*.tftest.hcl")
done
fi
if [[ ${#test_dirs[@]} -eq 0 ]]; then
echo "No .tftest.hcl tests found."
echo "No .tftest.hcl tests found in changed modules"
exit 0
fi
echo "==> Running terraform test in ${#test_dirs[@]} directory(ies)"
echo ""
status=0
for d in "${test_dirs[@]}"; do
if ! run_dir "$d"; then
+66 -12
View File
@@ -2,36 +2,90 @@
set -euo pipefail
# Auto-detect which Terraform modules to validate based on changed files from paths-filter
# Uses paths-filter outputs from GitHub Actions:
# ALL_CHANGED_FILES - all files changed in the PR (for logging)
# SHARED_CHANGED - boolean indicating if shared infrastructure changed
# MODULE_CHANGED_FILES - only files in registry/**/modules/** (for processing)
# Validates all modules if shared infrastructure changes, or skips if no changes detected
#
# This script only validates changed modules. Documentation and template changes are ignored.
validate_terraform_directory() {
local dir="$1"
echo "Running \`terraform validate\` in $dir"
pushd "$dir"
pushd "$dir" > /dev/null
terraform init -upgrade
terraform validate
popd
popd > /dev/null
}
main() {
# Get the directory of the script
echo "==> Detecting changed files..."
if [[ -n "${ALL_CHANGED_FILES:-}" ]]; then
echo "Changed files in PR:"
echo "$ALL_CHANGED_FILES" | tr ' ' '\n' | sed 's/^/ - /'
echo ""
fi
local script_dir=$(dirname "$(readlink -f "$0")")
local registry_dir=$(readlink -f "$script_dir/../registry")
# Code assumes that registry directory will always be in same position
# relative to the main script directory
local registry_dir="$script_dir/../registry"
if [[ "${SHARED_CHANGED:-false}" == "true" ]]; then
echo "==> Shared infrastructure changed"
echo "==> Validating all modules for safety"
local subdirs=$(find "$registry_dir" -mindepth 3 -maxdepth 3 -path "*/modules/*" -type d | sort)
elif [[ -z "${MODULE_CHANGED_FILES:-}" ]]; then
echo "✓ No module files changed, skipping validation"
exit 0
else
CHANGED_FILES=$(echo "$MODULE_CHANGED_FILES" | tr ' ' '\n')
# Get all module subdirectories in the registry directory. Code assumes that
# Terraform module directories won't begin to appear until three levels deep into
# the registry (e.g., registry/coder/modules/coder-login, which will then
# have a main.tf file inside it)
local subdirs=$(find "$registry_dir" -mindepth 3 -path "*/modules/*" -type d | sort)
MODULE_DIRS=()
while IFS= read -r file; do
if [[ "$file" =~ \.(md|png|jpg|jpeg|svg)$ ]]; then
continue
fi
if [[ "$file" =~ ^registry/([^/]+)/modules/([^/]+)/ ]]; then
namespace="${BASH_REMATCH[1]}"
module="${BASH_REMATCH[2]}"
module_dir="registry/${namespace}/modules/${module}"
if [[ -d "$module_dir" ]] && [[ ! " ${MODULE_DIRS[*]} " =~ " ${module_dir} " ]]; then
MODULE_DIRS+=("$module_dir")
fi
fi
done <<< "$CHANGED_FILES"
if [[ ${#MODULE_DIRS[@]} -eq 0 ]]; then
echo "✓ No modules to validate"
echo " (documentation, templates, namespace files, or modules without changes)"
exit 0
fi
echo "==> Validating ${#MODULE_DIRS[@]} changed module(s):"
for dir in "${MODULE_DIRS[@]}"; do
echo " - $dir"
done
echo ""
local subdirs="${MODULE_DIRS[*]}"
fi
status=0
for dir in $subdirs; do
# Skip over any directories that obviously don't have the necessary
# files
if test -f "$dir/main.tf"; then
validate_terraform_directory "$dir"
if ! validate_terraform_directory "$dir"; then
status=1
fi
fi
done
exit $status
}
main
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
set -euo pipefail
# Auto-detect which TypeScript tests to run based on changed files from paths-filter
# Uses paths-filter outputs from GitHub Actions:
# ALL_CHANGED_FILES - all files changed in the PR (for logging)
# SHARED_CHANGED - boolean indicating if shared infrastructure changed
# MODULE_CHANGED_FILES - only files in registry/**/modules/** (for processing)
# Runs all tests if shared infrastructure changes
#
# This script only runs tests for changed modules. Documentation and template changes are ignored.
echo "==> Detecting changed files..."
if [[ -n "${ALL_CHANGED_FILES:-}" ]]; then
echo "Changed files in PR:"
echo "$ALL_CHANGED_FILES" | tr ' ' '\n' | sed 's/^/ - /'
echo ""
fi
if [[ "${SHARED_CHANGED:-false}" == "true" ]]; then
echo "==> Shared infrastructure changed"
echo "==> Running all tests for safety"
exec bun test
fi
if [[ -z "${MODULE_CHANGED_FILES:-}" ]]; then
echo "✓ No module files changed, skipping tests"
exit 0
fi
CHANGED_FILES=$(echo "$MODULE_CHANGED_FILES" | tr ' ' '\n')
MODULE_DIRS=()
while IFS= read -r file; do
if [[ "$file" =~ \.(md|png|jpg|jpeg|svg)$ ]]; then
continue
fi
if [[ "$file" =~ ^registry/([^/]+)/modules/([^/]+)/ ]]; then
namespace="${BASH_REMATCH[1]}"
module="${BASH_REMATCH[2]}"
module_dir="registry/${namespace}/modules/${module}"
if [[ -f "$module_dir/main.test.ts" ]] && [[ ! " ${MODULE_DIRS[*]} " =~ " ${module_dir} " ]]; then
MODULE_DIRS+=("$module_dir")
fi
fi
done <<< "$CHANGED_FILES"
if [[ ${#MODULE_DIRS[@]} -eq 0 ]]; then
echo "✓ No TypeScript tests to run"
echo " (documentation, templates, namespace files, or modules without tests)"
exit 0
fi
echo "==> Running TypeScript tests for ${#MODULE_DIRS[@]} changed module(s):"
for dir in "${MODULE_DIRS[@]}"; do
echo " - $dir"
done
echo ""
exec bun test "${MODULE_DIRS[@]}"