Compare commits

..

175 Commits

Author SHA1 Message Date
Yevhenii Shcherbina 92ab526733 feat: change boundary rules according to new spec (#517) 2025-10-29 19:57:15 -04:00
Rhys Williams d6d0101f09 Fix Devolutions Auto-Complete (#508)
## Description

I’ve completed a set of modifications to improve the user experience and
session behaviour within Devolutions Gateway:

- Auto-Complete Fix: Resolved issues with auto-complete functionality.
- Container Visibility: Implemented logic to hide the app-net-scan
container, preventing it from displaying during the initial session
load.
- Default Settings: Enabled Unicode keyboard mode and dynamic window
resizing by default to enhance usability.
- Session Closure Behaviour: Modified the "Close Session" button to
fully close the session window, avoiding returns to the session manager.
- Dynamic Module Path Construction: Refactored the PowerShell module
path setup to be dynamically constructed.
- Input Variables: Added `slug` and `display_name` as input variables.

## Type of Change

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

## Module Information

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

**Path:** `registry/coder/modules/windows-rdp`  
**New version:** `v1.3.0`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

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

## Related Issues

"None"

---------

Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: DevelopmentCats <chris@dualriver.com>
Co-authored-by: Eric Paulsen <ericpaulsen@coder.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-28 10:00:41 +00:00
Luis 1a15ad650a Update Vault CLI download link to use architecture (#514)
## Description

The download command was downloading only the amd64 version,

## Type of Change

- [ ] New module
- [ ] New template
- [x] 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-27 17:12:24 -05:00
Atif Ali d64851774b fix(jetbrains): update Terraform version requirement to 1.9+ (#513)
## Summary

- Updated `required_version` constraint from `>= 1.0` to `>= 1.9` in
jetbrains module
- Added inline comment explaining the cross-variable validation
requirement
- Bumped module version from `1.1.0` to `1.1.1` (patch version)

## Issue

The jetbrains module uses cross-variable validation at line 169-171
where `var.options` is referenced within the `var.ide_config` validation
block:

```tf
validation {
  condition = alltrue([
    for code in var.options : contains(keys(var.ide_config), code)
  ])
  error_message = "The ide_config must be a superset of var.options."
}
```

This pattern requires Terraform 1.9+ and fails on earlier versions with:
```
Error: Invalid reference in variable validation
The condition for variable "ide_config" can only refer to the variable itself, using var.ide_config.
```

## References

- Terrafomr release blog that talks abut this feature:
https://www.hashicorp.com/en/blog/terraform-1-9-enhances-input-variable-validations
- Terraform PR that added this feature:
https://github.com/hashicorp/terraform/pull/34955
- HashiCorp Support Article:
https://support.hashicorp.com/hc/en-us/articles/43291233547027

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

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DevCats <christofer@coder.com>
2025-10-27 08:36:19 -05:00
DevCats d3b40c08f1 feat: add session resumption to codex (#506)
## Description

Add continue variable, and logic for resuming task sessions 
<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

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

**Path:** `registry/coder-labs/modules/codex`  
**New version:** `v3.1.0`  
**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-27 07:45:37 -05:00
Yevhenii Shcherbina 01f5100068 fix: drop perms for boundary process (#512) 2025-10-24 21:23:42 -04:00
Yevhenii Shcherbina 7e42a145fa feat: dropping perms before running claude (#509)
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-10-24 15:35:20 -05:00
Atif Ali 0ff3dbcc48 chore(claude-code): limit MCP tools for task reporting (#507) 2025-10-24 23:14:34 +05:00
netsgnut a327e79bc4 fix(kasmvnc): change installed check and bump default version (#505)
## Description

This PR makes the following changes to the `coder/modules/kasmvnc`:
- Change the installation check from checking `vncserver` to
`kasmvncserver`.
- Bump the default KasmVNC installation version to
[1.4.0](https://docs.kasmvnc.com/docs/release_notes/1.4.0).

In images where there is already TightVNC installed, the current
installation check will erroneously report that KasmVNC is already
installed. By checking `kasmvncserver` instead, it ensures KasmVNC is
installed.

Tested on Debian, Kali and Alpine-based images.

## Type of Change

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

## Module Information

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

## Testing & Validation

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

## Related Issues

None
2025-10-24 17:47:51 +00:00
Harsh Singh Panwar bc39c2ee29 Aider module support agentAPI (#356)
Closes #239

/claim #239

## Description

video :-
https://www.loom.com/share/d1d1d54d48bc45c4a48271ca9a387a88?sid=933e250d-78f8-4a7f-9745-0e908c0ee4d9
<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

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

**Path:** `registry/coder/modules/aider`  
**New version:** `v1.0.0`  
**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: DevCats <christofer@coder.com>
2025-10-24 10:25:40 -05:00
Danielle Maywood e3ff43c0a6 refactor(coder/agentapi): support terraform-provider-coder v2.12.0 (#485)
In terraform-provider-coder v2.12.0 and the
up-coming coder v2.28 release we have removed the
requirement for the "AI Prompt" parameter, and are intending on slightly
re-designing the API of the AI task modules.

Instead of `agentapi` defining the `coder_ai_task` resource, it will
output the `task_app_id`. Consumers of the module will then be expected
to create the `coder_ai_task` resource themselves with this
`task_app_id`.
2025-10-24 11:54:12 +01:00
Yevhenii Shcherbina 30123e7ea3 feat: add boundary pprof server in claude-code module (#503) 2025-10-23 14:18:30 -04:00
djarbz f7c1be71f7 Add [copyparty] module (#486)
## Description

This PR adds a module to install Copyparty as an alternative to
Filebrowser.

## Type of Change

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

## Module Information

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

**Path:** `registry/djarbz/modules/copyparty`  
**New version:** `v0.1.0`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

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

## Related Issues

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

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-10-23 11:19:05 -05:00
DevCats 19519a0a13 fix: add shebang to zed coder_script (#504)
## Description

Add `#!/bin/sh` to zed_settings coder_script
<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

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

**Path:** `registry/coder/modules/zed`  
**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

https://github.com/coder/registry/issues/482
<!-- Link related issues or write "None" if not applicable -->
2025-10-23 07:39:27 -05:00
Rowan Smith 63e42283ce chore: Update templates from Always to IfNotPresent for image_pull_policy (#501)
## Description

Change `image_pull_policy` from `Always` to `IfNotPresent` on Coder
owned templates. Given these are a reference point for users and
customers and they copy them into their own templates I think it makes
sense to encourage the use of caching of images.

## Type of Change

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

## Template Information

**Path:** 


https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes-devcontainer

https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes-envbox

https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes

## Testing & Validation

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

## Related Issues

None
2025-10-23 04:28:58 +00:00
Eric Paulsen 0c5a8a2354 add nfs-deployment template (#502)
## Description

this PR adds a new template to the registry, which shows how to mount an
NFS share to a K8s deployment workspace.

## Type of Change

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

## Template Information

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

**Path:** `registry/ericpaulsen/templates/nfs-deployment`

## Testing & Validation

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

## Related Issues

None

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-10-22 11:51:58 -05:00
DevCats 51ec6e3212 fix: resolve issues with claude-code session resumption (#496)
## Description

Fixes session resumption logic by having the continue flag decide
whether to continue a workspace based on session history

## Type of Change

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

## Module Information

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

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v3.2.2`  
**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 -->

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-22 10:58:01 -05:00
DevCats 843b1f1e5a chore: change copilot default version to latest (#499)
## Description

Changes `copilot_version` default to `latest`

## Type of Change

- [ ] New module
- [ ] New template
- [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.2`  
**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-22 07:33:09 -05:00
Jiachen Jiang 583918bfef added example of boundary to claude code module (#500) 2025-10-21 16:33:15 -05:00
Benjamin Peinhardt a1786a09ea update claude-code module version (#498)
The version for the claude-code module should have been updated in
https://github.com/coder/registry/pull/455. This PR updates the module
version so we can cut a release 😎
2025-10-21 13:46:32 -05:00
Benjamin Peinhardt a35986d7df feat: initial boundary integration with claude code (#455)
Closes #

## Description

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

## Type of Change

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

## Module Information

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

**Path:** `registry/[namespace]/modules/[module-name]`  
**New version:** `v1.0.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: YEVHENII SHCHERBINA <yevhenii@coder.com>
2025-10-21 13:44:26 -04:00
Mathias Fredriksson e34320cb0b feat: add archive module (#422)
This change adds a new `archive` module to the Coder registry. It can be
used to archive user-data from pre-defined locations and restore it as
well.

Here we also explore:

- A new method of passing arrays from Terraform to Bash
- A new method of writing Bash scripts that minimizes the interaction
with terraform interpolation
- Extensive test-suite that not only tests that Terraform options can be
selected, but also the resulting script behaviors

---------

Co-authored-by: Cian Johnston <cian@coder.com>
Co-authored-by: DevCats <christofer@coder.com>
2025-10-17 08:14:56 -05:00
35C4n0r ca7bc42946 feat: update auth setup in codex (#472)
Closes #

## Description

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

## Type of Change

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

## Module Information

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

**Path:** `registry/coder-labs/modules/codex`  
**New version:** `v3.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>
2025-10-16 15:25:57 -05:00
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
Atif Ali 44354b202d Fix claude-code module not passing workdir to agentapi (#439)
## Summary

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

## Changes

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

## Test Plan

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

## Verification

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

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

---------

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

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

## Type of Change

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

## Testing & Validation

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

## Related Issues

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

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

## Type of Change

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

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







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

## Testing & Validation

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

## Related Issues

/claim #79 (remain asset 150$)

---------

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

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

## Type of Change

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

## Module Information

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

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

## Testing & Validation

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

## Related Issues

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

---------

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

Refactor template for claude-code module update for tasks

## Type of Change

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

## Testing & Validation

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

## Related Issues

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

---------

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

## Description

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

## Type of Change

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

## Module Information

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

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

## Testing & Validation

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

## Related Issues

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

---------

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

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

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

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


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

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

## Type of Change

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

## Module Information

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

## Key Features & Enhancements

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

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

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

d## Breaking Changes

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

## Testing & Validation

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

## Related Issues

Closes #240 - Amazon Q module enhancement request

## Additional Notes

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

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


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



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

/claim #240

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



## Type of Change

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

## Testing & Validation

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

## Related Issues

#33 
## video


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

## Description

## Type of Change

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

## Testing & Validation

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

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

---------

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

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

## Type of Change

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

## Testing & Validation

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

## Related Issues

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

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

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

---------

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

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

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

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

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

## Description

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

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

## Type of Change

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

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

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

## Type of Change

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

## Module Information

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

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

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

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

## Testing & Validation

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

## Description

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

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

## Type of Change

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

## Module Information

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

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

## Testing & Validation

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

## Related Issues

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

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


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

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

## Type of Change

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

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

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

## Type of Change

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

## Module Information

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

## Implementation Details

### All Requirements from Issue #26 Implemented:

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

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

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

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

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

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

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

## Testing & Validation

### Comprehensive Test Suite

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

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

```bash
#!/bin/bash

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

set -e

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

echo "   Created AMI: $AMI_ID"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</details>

### Test Results Summary

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

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

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

## Key Features Implemented

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

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

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

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

## Security & IAM

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

## Usage Example

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

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

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

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

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

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

## Related Issues

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

## Video Demonstration




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


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

---------

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

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

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

## Type of Change

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

## Module Information

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

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

## Testing & Validation

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

## Related Issues

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

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-08-25 12:53:48 -05:00
Atif Ali 68f881e220 chore: update Sourcegraph AMP to Amp CLI (#382)
## Description

Update Sourcegraph AMP to Amp CLI as this seems the preferred name.

<!-- 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-labs/modules/sourcegraph-amp`  
**New version:** `v1.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 Issues

<!-- Link related issues or write "None" if not applicable -->
2025-08-25 11:58:04 -05:00
dependabot[bot] 94d938156d chore(deps): bump google-github-actions/setup-gcloud from 2.2.0 to 3.0.0 (#379)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 14:06:54 +00:00
Atif Ali 1d30ac954d chore: group github dependabot updates (#381) 2025-08-25 08:40:10 -05:00
dependabot[bot] a468ec68ea chore(deps): bump crate-ci/typos from 1.35.4 to 1.35.5 (#380)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 18:32:38 +05:00
dependabot[bot] 52c1d47161 chore(deps): bump actions/checkout from 4 to 5 (#378)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 13:24:41 +00:00
blink-so[bot] b206a6870c chore: add automated release workflow for module tags (#372)
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-24 01:28:56 +05:00
Atif Ali ac48f0d166 fix: update Sourcegraph AMP source URL (#370)
Co-authored-by: Amp <amp@ampcode.com>
2025-08-23 19:18:36 +00:00
Ben Potter 49ef1203e4 chore: change tmux module name (#369)
Co-authored-by: 35C4n0r <work.jaykumar@gmail.com>
2025-08-24 00:16:42 +05:00
35C4n0r b5837a704d chore: add tests for latest agent versions (#371) 2025-08-24 00:15:55 +05:00
Ben Potter 5764ff2fdc feat: add healthcheck and config options to JupyterLab Module (#363)
## Description

Simplified JupyterLab module configuration and added automatic CSP
headers for iFrame embedding for Coder Tasks. The module now works out
of the box without requiring users to manually configure
Content-Security-Policy headers.

**Changes:**
- Removed redundant configuration examples from README that duplicated
existing module variables
- Added fallback CSP configuration when user doesn't provide custom
config
- Cleaned up locals logic with better naming and clearer conditionals
- Updated README to show minimal usage with CSP example for custom
configurations

## Type of Change

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

## Module Information

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

*Breaking change: Config behavior changed - now automatically includes
CSP when no user config provided*

## Testing & Validation

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

## Related Issues

Closes #345
2025-08-23 23:43:24 +05:00
35C4n0r df2f4321a1 feat: add auggie cli (#350)
## Description
Adds the Auggie CLI module

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

## Type of Change

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

## Module Information

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

**Path:** `registry/coder-labs/modules/auggie`  
**New version:** `v0.1.0`  

## 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: Atif Ali <me@matifali.dev>
Co-authored-by: DevelopmentCats <christofer@coder.com>
Co-authored-by: DevCats <chris@dualriver.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-22 17:04:42 -05:00
DevCats 8677e7d52b feat: add validation for module and namespace names (#359)
Closes #

## Description

add validation for module and namespace names to ensure they contain
only alphanumeric characters and hyphens
<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Testing & Validation

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Michael Smith <michaelsmith@coder.com>
2025-08-22 09:45:15 -04:00
DevCats d6ae51fad0 feat: codex qol updates (#348)
## Description

- Removed variables for hardcoded configuration options, and replaced
with variables for base config, and additional mcp servers.
- Set module defaults so that this will run with minimal module
configuration for tasks, while allowing further configuration if needed
by the user for codex through the base configuration.
- Updated tests for expected responses and new configuration options.
- Move all codex related files outside of project folder.
<!-- 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/codex`  
**New version:** `v1.1.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: Atif Ali <atif@coder.com>
2025-08-21 10:56:09 -05:00
Atif Ali eca289be3a chore: set coder-labs publisher as official (#340)
@Parkreiner, please double-check if it will work and not fail any
validation at build time.
2025-08-21 09:14:14 -05:00
Ben Potter 7aa75f9451 fix: use correct app slug for status reporting (#344)
## Description

I don't know how long this was broken for, but we had a [customer run
into
this](https://codercom.slack.com/archives/C04EHNF3A0Y/p1755635729486939).
I also noticed that the MCP seemed to still report "ok" when this was
reported with the wrong slug. Ultimately, I'm not sure if this should
even be in our example template or if it should be in the module itself,
or if its needed at all. Perhaps @35C4n0r, @matifali, or
@DevelopmentCats has thoughts on how we can improve this UX overall.

## Type of Change

- [ ] New module
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other
2025-08-21 03:55:07 -07:00
Anas efac32ad9f fix: Update default value for workspace variable to an empty string (#353) 2025-08-21 15:47:13 +05:00
Rowan Smith a69accbad5 feat: Add support to the vscode-web module for loading workspaces at startup (#349) 2025-08-21 13:43:00 +05:00
chgl 29e5307121 Actually use the home dir kasmvnc.yaml to retrieve the httpd_directory setting (#342) 2025-08-20 14:06:10 +05:00
Harsh Singh Panwar 545a245530 feat: Sourcegraph Amp module (#257)
Closes #238
/claim #238 

## Description

Video -
https://www.loom.com/share/59e80a7fa3e54973bb0318132bc849a7?sid=4900077a-6fdb-4760-978c-9ad2e2daa9d8
 
<img width="1365" height="599" alt="Screenshot 2025-08-02 164234"
src="https://github.com/user-attachments/assets/56ec7dc3-bc41-4976-9b78-3d6c011d80fe"
/>

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

## Type of Change

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

## Module Information

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

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

## Testing & Validation

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

## Related Issues

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

---------

Co-authored-by: Atif Ali <atif@coder.com>
Co-authored-by: DevCats <christofer@coder.com>
2025-08-19 15:25:17 -05:00
m4rrypro c554463d4d Add Proxmox-Vm template (#329)
Closes #212
/claim #212 

## Description

Adds a Proxmox VM template 

## Type of Change

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

## Testing & Validation

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


https://github.com/user-attachments/assets/d0fcdb6a-3451-4eaa-855d-912ac0cd4c45

---------

Co-authored-by: Atif Ali <me@matifali.dev>
2025-08-19 14:29:49 -05:00
Atif Ali 4ea87a6e01 chore: use light openai logo (#341)
Co-authored-by: DevelopmentCats <christofer@coder.com>
2025-08-19 19:24:45 +05:00
Atif Ali f5a571679a chore: fix module detection to exclude hidden dirs (#339)
## Before
```console
🚀 Coder Registry Tag Release Script
       Operating on commit: 4238f38353

       🔍 Scanning all modules for missing release tags...

       ⚠️  anomaly/.coder: No version found in README, skipping
        anomaly/tmux: v1.0.0 (already tagged)
       ⚠️  coder-labs/.coder: No version found in README, skipping
        coder-labs/cursor-cli: v0.1.1 (already tagged)
        coder-labs/gemini: v1.1.0 (already tagged)
       ⚠️  coder-labs/jetbrains-fleet: No version found in README, skipping
       ⚠️  coder/.coder: No version found in README, skipping
        coder/agentapi: v1.1.1 (already tagged)
        coder/aider: v1.1.2 (already tagged)
        coder/amazon-dcv-windows: v1.1.1 (already tagged)
        coder/amazon-q: v1.1.2 (already tagged)
        coder/aws-region: v1.0.31 (already tagged)
        coder/azure-region: v1.0.31 (already tagged)
        coder/claude-code: v2.1.0 (already tagged)
        coder/code-server: v1.3.1 (already tagged)
        coder/coder-login: v1.0.31 (already tagged)
        coder/cursor: v1.3.1 (already tagged)
        coder/devcontainers-cli: v1.0.32 (already tagged)
        coder/dotfiles: v1.2.1 (already tagged)
        coder/filebrowser: v1.1.2 (already tagged)
        coder/fly-region: v1.0.31 (already tagged)
        coder/gcp-region: v1.0.31 (already tagged)
        coder/git-clone: v1.1.1 (already tagged)
        coder/git-commit-signing: v1.0.31 (already tagged)
        coder/git-config: v1.0.31 (already tagged)
        coder/github-upload-public-key: v1.0.31 (already tagged)
        coder/goose: v2.1.1 (already tagged)
        coder/hcp-vault-secrets: v1.0.34 (already tagged)
        coder/jetbrains: v1.0.3 (already tagged)
        coder/jetbrains-fleet: v1.0.1 (already tagged)
        coder/jetbrains-gateway: v1.2.2 (already tagged)
        coder/jfrog-oauth: v1.0.31 (already tagged)
        coder/jfrog-token: v1.0.31 (already tagged)
        coder/jupyter-notebook: v1.2.0 (already tagged)
        coder/jupyterlab: v1.1.1 (already tagged)
        coder/kasmvnc: v1.2.1 (already tagged)
        coder/kiro: v1.0.0 (already tagged)
        coder/local-windows-rdp: v1.0.2 (already tagged)
        coder/personalize: v1.0.31 (already tagged)
        coder/slackme: v1.0.31 (already tagged)
        coder/vault-github: v1.0.31 (already tagged)
        coder/vault-jwt: v1.1.1 (already tagged)
        coder/vault-token: v1.2.1 (already tagged)
        coder/vscode-desktop: v1.1.1 (already tagged)
        coder/vscode-desktop-core: v1.0.0 (already tagged)
        coder/vscode-web: v1.3.1 (already tagged)
        coder/windows-rdp: v1.2.3 (already tagged)
        coder/windsurf: v1.1.1 (already tagged)
        coder/zed: v1.1.0 (already tagged)
        nataindata/apache-airflow: v1.0.14 (already tagged)
        thezoker/nodejs: v1.0.11 (already tagged)
       ⚠️  whizus/.coder: No version found in README, skipping
        whizus/exoscale-instance-type: v1.0.13 (already tagged)
        whizus/exoscale-zone: v1.0.13 (already tagged)

       📊 Summary: 0 of 54 modules need tagging

        🎉 All modules are up to date! No tags needed.
```

## After

```console
🚀 Coder Registry Tag Release Script
Operating on commit: 7f9725209f

🔍 Scanning all modules for missing release tags...

 anomaly/tmux: v1.0.0 (already tagged)
 coder-labs/cursor-cli: v0.1.1 (already tagged)
 coder-labs/gemini: v1.1.0 (already tagged)
 coder/agentapi: v1.1.1 (already tagged)
 coder/aider: v1.1.2 (already tagged)
 coder/amazon-dcv-windows: v1.1.1 (already tagged)
 coder/amazon-q: v1.1.2 (already tagged)
 coder/aws-region: v1.0.31 (already tagged)
 coder/azure-region: v1.0.31 (already tagged)
 coder/claude-code: v2.1.0 (already tagged)
 coder/code-server: v1.3.1 (already tagged)
 coder/coder-login: v1.0.31 (already tagged)
 coder/cursor: v1.3.1 (already tagged)
 coder/devcontainers-cli: v1.0.32 (already tagged)
 coder/dotfiles: v1.2.1 (already tagged)
 coder/filebrowser: v1.1.2 (already tagged)
 coder/fly-region: v1.0.31 (already tagged)
 coder/gcp-region: v1.0.31 (already tagged)
 coder/git-clone: v1.1.1 (already tagged)
 coder/git-commit-signing: v1.0.31 (already tagged)
 coder/git-config: v1.0.31 (already tagged)
 coder/github-upload-public-key: v1.0.31 (already tagged)
 coder/goose: v2.1.1 (already tagged)
 coder/hcp-vault-secrets: v1.0.34 (already tagged)
 coder/jetbrains: v1.0.3 (already tagged)
 coder/jetbrains-fleet: v1.0.1 (already tagged)
 coder/jetbrains-gateway: v1.2.2 (already tagged)
 coder/jfrog-oauth: v1.0.31 (already tagged)
 coder/jfrog-token: v1.0.31 (already tagged)
 coder/jupyter-notebook: v1.2.0 (already tagged)
 coder/jupyterlab: v1.1.1 (already tagged)
 coder/kasmvnc: v1.2.1 (already tagged)
 coder/kiro: v1.0.0 (already tagged)
 coder/local-windows-rdp: v1.0.2 (already tagged)
 coder/personalize: v1.0.31 (already tagged)
 coder/slackme: v1.0.31 (already tagged)
 coder/vault-github: v1.0.31 (already tagged)
 coder/vault-jwt: v1.1.1 (already tagged)
 coder/vault-token: v1.2.1 (already tagged)
 coder/vscode-desktop: v1.1.1 (already tagged)
 coder/vscode-desktop-core: v1.0.0 (already tagged)
 coder/vscode-web: v1.3.1 (already tagged)
 coder/windows-rdp: v1.2.3 (already tagged)
 coder/windsurf: v1.1.1 (already tagged)
 coder/zed: v1.1.0 (already tagged)
 nataindata/apache-airflow: v1.0.14 (already tagged)
 thezoker/nodejs: v1.0.11 (already tagged)
 whizus/exoscale-instance-type: v1.0.13 (already tagged)
 whizus/exoscale-zone: v1.0.13 (already tagged)

📊 Summary: 0 of 49 modules need tagging

 🎉 All modules are up to date! No tags needed.
```
2025-08-19 17:34:54 +05:00
35C4n0r 0e1dcd3a80 feat: support codex cli (#281)
Co-authored-by: Hugo Dutka <dutkahugo@gmail.com>
Co-authored-by: DevCats <christofer@coder.com>
2025-08-19 13:59:00 +05:00
35C4n0r 4238f38353 fix: reformat cursor cli readme (#336)
Closes #

## 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/[namespace]/modules/[module-name]`  
**New version:** `v1.0.0`  
**Breaking change:** [ ] Yes [ ] No

## Testing & Validation

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

## Related Issues

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

---------

Co-authored-by: Atif Ali <me@matifali.dev>
Co-authored-by: DevCats <christofer@coder.com>
2025-08-18 14:43:32 -05:00
DevCats 858799ce20 fix: update version extraction to be more robust, ensure compatibility (#337)
## Description

Update version detection to always detect named module block, and
extract version from same module block.
Ensure that script is completely compatible for all Unix environments.
<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Testing & Validation

- [ ] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally
2025-08-18 14:36:29 -05:00
Atif Ali 32246a99c1 feat(cursor-cli): add Cursor CLI module (#309)
Closes #305

## Summary
- Add new module `registry/coder-labs/modules/cursor-cli` to run Cursor
Agent CLI directly (no AgentAPI)
- Interactive chat by default; supports non-interactive mode (-p) with
output-format
- Supports model (-m) and force (-f) flags, initial prompt, and
CURSOR_API_KEY
- Merges MCP settings into ~/.cursor/settings.json
- Installs via npm, bootstrapping Node via NVM if missing (mirrors
gemini approach)
- Adds Terraform-native tests (.tftest.hcl); all pass locally

## Test plan
- From module dir:
  - terraform init -upgrade
  - terraform test -verbose
- Expect 4 tests passing covering defaults, flag plumbing, and MCP
settings injection
- Basic smoke run: ensure `cursor-agent` is on PATH or set
install_cursor_cli=true

---------

Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: 35C4n0r <work.jaykumar@gmail.com>
Co-authored-by: 35C4n0r <70096901+35C4n0r@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-18 13:08:48 -05:00
blink-so[bot] bb667d2209 fix(tag_release): improve macOS and Linux compatibility (#335)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2025-08-18 19:09:10 +05:00
dependabot[bot] f08bb30b53 chore(deps): bump actions/checkout from 4 to 5 (#334)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-18 12:25:16 +00:00
dependabot[bot] 32b039a838 chore(deps): bump crate-ci/typos from 1.35.3 to 1.35.4 (#333)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-18 12:22:19 +00:00
blink-so[bot] 4dcaea7bf9 feat(claude-code): support binary distribution without Node.js dependency (#332)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: kyle <kyle@coder.com>
Co-authored-by: 35C4n0r <work.jaykumar@gmail.com>
2025-08-18 17:00:34 +05:00
Atif Ali c2bc5cd314 chore(cursor): update mcp example to use GitHub (#330) 2025-08-18 16:57:30 +05:00
blink-so[bot] c73b923e40 Fix broken main.tf link in tasks-docker template README (#331)
Fixes the broken main.tf link reported in the tasks-docker template
README.

The relative link `./main.tf` was returning a 404 error on the registry
website. This PR replaces it with the correct GitHub URL pointing to the
main.tf file.

**Changes:**
- Updated the main.tf link in
`/registry/coder-labs/templates/tasks-docker/README.md`
- Changed from `[main.tf](./main.tf)` to
`[main.tf](https://github.com/coder/registry/blob/main/registry/coder-labs/templates/tasks-docker/main.tf)`

Tested: Verified the new link works correctly.

---------

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2025-08-15 20:31:00 -04:00
Michael Smith 08ed594bfd chore: add validation for Coder Template README files (#326)
Closes #194 alongside #325

## Description

This PR adds the missing base layer of validation for all Coder template
README files, ensuring that they all follow a consistent structure when
processed by the Registry website's build step. It also updates a few
README files to match the new standards.

## Type of Change

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

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally
2025-08-13 13:38:11 -05:00
Michael Smith fd074a5643 fix: improve logic for existing README validation (#325)
Addresses part of https://github.com/coder/registry/issues/194

## Description

This PR beefs up the validation for the validation logic that we already
had in place. This PR does not include adding validation for templates
(which will be addressed in a second PR).

### Changes made
- Added logic to reject unknown frontmatter fields for modules and
contributor profile README files
- Added logic to handle frontmatter fields that were previously missed
in validation steps (GitHub username for contributors and Operating
Systems for modules)
- Updated a few comments (added some new comments, formatted existing
comments to meet 100-column width)

### Type of Change

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

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally
2025-08-13 11:07:08 -05:00
Phorcys 40863c0aa7 chore: add Disclaimer for docker-based templates (#324) 2025-08-13 09:03:47 -05:00
Atif Ali d9b223ac3c feat(zed): settings input and MCP servers example (#317)
This PR adds an optional `settings` input to the Zed module and updates
the README with an example for configuring MCP servers.

Changes:
- Add `settings` variable to modules/zed/main.tf
- Add `coder_script` to write/merge `~/.config/zed/settings.json`
(respects `$XDG_CONFIG_HOME` and merges with existing settings if `jq`
is available)
- Update README with a `settings` example configuring MCP context
servers and clarify default settings path

Test plan:
- Syntax-only: `bun test --filter zed` fails in CI without Terraform;
this change only adds inputs and a startup script. No behavior change to
existing outputs.

Co-authored-by: Atif Ali <10648092+matifali@users.noreply.github.com>

---------

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: DevCats <christofer@coder.com>
2025-08-12 20:26:28 -05:00
DevCats 1749f9ca05 feat(gemini): gemini cleanup and refactor (#300)
Closes #260

## Description

Cleans up and fixes issues with Gemini module.

## Type of Change

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

## Module Information

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

**Path:** `registry/coder-labs/modules/gemini`  
**New version:** `v1.1.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
 
#260 
<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-08-12 11:37:45 -05:00
Atif Ali 61554aaa8c feat(cursor): add mcp input to configure MCP servers for Cursor (#314)
This adds a new optional input `mcp` to the cursor module.

- Accepts a JSON-encoded string with MCP server configuration
- When provided, a `coder_script` writes it to `~/.cursor/mcp.json` on
start
- Keeps existing behavior unchanged if `mcp` is empty
- Adds tests verifying the `mcp.json` is written
- Updates README with `mcp` usage example
- Fixes Prettier and `terraform fmt` formatting issues flagged by CI

CI should now pass after the latest commits.

---------

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2025-08-12 11:18:31 -05:00
DevCats f4fcae7c0f chore(jetbrains): version bump to 1.0.3 (#323)
## Description

Patch version bump to 1.0.3 since it was missed in last PR.
<!-- 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/coder/modules/jetbrains`  
**New version:** `v1.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

None
<!-- Link related issues or write "None" if not applicable -->
2025-08-11 22:36:41 -05:00
DevCats 05b9bb1ae4 feat(tag_release): add advanced options and devops friendly features (#322)
## Description

- Add CLI argument parsing with short and long options
- Implement JSON output format for programmatic consumption
- Add dry-run mode for safe testing
- Include verbose and quiet logging modes
- Add namespace and module filtering capabilities
- Implement skip-push option for tag creation without remote push
- Add pre-flight checks for git repository validation
- Enhance error handling with structured logging
- Add exit codes for different operation states
- Include comprehensive help documentation with examples

```
Usage: ./scripts/tag_release.sh [OPTIONS]

OPTIONS:
  -y, --auto-approve       Skip confirmation prompt
  -d, --dry-run           Preview without creating tags
  -v, --verbose           Detailed output
  -q, --quiet             Minimal output
  -f, --format=FORMAT     Output format: 'plain' or 'json'
  -n, --namespace=NAME    Target specific namespace
  -m, --module=NAME       Target specific module
  -s, --skip-push         Create tags but don't push
  -h, --help              Show this help

EXAMPLES:
  ./scripts/tag_release.sh                      # Interactive mode
  ./scripts/tag_release.sh -y -q -f json        # CI/CD automation
  ./scripts/tag_release.sh -d -v                # Test with verbose output
  ./scripts/tag_release.sh -m code-server -d    # Target specific module
  ./scripts/tag_release.sh -n coder -m code-server -d  # Target module in namespace

Exit codes: 0=success, 1=error, 2=no action needed, 3=validation failed
```

## Type of Change

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

## Testing & Validation

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

## Related Issues

<!-- Link related issues or write "None" if not applicable -->## Module
Information
None
2025-08-11 21:18:39 -05:00
Atif Ali 45b72c7241 Migrate tests to Terraform for jetbrains, zed, and code-server (#307)
## Summary
- Introduces Terraform native tests (`terraform test`) alongside
existing Bun tests
- Migrates tests for modules: jetbrains, zed, and code-server
- Removes Bun test files for these migrated modules only
- Adds repo-wide test runner script for Terraform tests
- Updates docs and new-module sample to reflect Terraform tests

## Transition plan
- Mixed mode: Other modules retain Bun tests; CI should run both Bun and
Terraform tests temporarily
- Follow the linked epic to migrate remaining modules

## Test plan
- Run: `./scripts/terraform_test_all.sh` (passes locally)
- Bun tests still available for non-migrated modules

## Affected paths
- registry/coder/modules/jetbrains/jetbrains.tftest.hcl
- registry/coder/modules/zed/zed.tftest.hcl
- registry/coder/modules/code-server/code-server.tftest.hcl
- scripts/terraform_test_all.sh
- examples/modules/MODULE_NAME.tftest.hcl
- CONTRIBUTING.md


Contributes to #308
2025-08-11 21:09:09 -05:00
dependabot[bot] 2646b36cb1 chore(deps): bump google-github-actions/setup-gcloud from 2.1.5 to 2.2.0 (#319)
Bumps
[google-github-actions/setup-gcloud](https://github.com/google-github-actions/setup-gcloud)
from 2.1.5 to 2.2.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/google-github-actions/setup-gcloud/releases">google-github-actions/setup-gcloud's
releases</a>.</em></p>
<blockquote>
<h2>v2.2.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Introduce an option to skip the tool cache by <a
href="https://github.com/sethvargo"><code>@​sethvargo</code></a> in <a
href="https://redirect.github.com/google-github-actions/setup-gcloud/pull/718">google-github-actions/setup-gcloud#718</a></li>
<li>Release: v2.2.0 by <a
href="https://github.com/google-github-actions-bot"><code>@​google-github-actions-bot</code></a>
in <a
href="https://redirect.github.com/google-github-actions/setup-gcloud/pull/719">google-github-actions/setup-gcloud#719</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/google-github-actions/setup-gcloud/compare/v2.1.5...v2.2.0">https://github.com/google-github-actions/setup-gcloud/compare/v2.1.5...v2.2.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/google-github-actions/setup-gcloud/commit/cb1e50a9932213ecece00a606661ae9ca44f3397"><code>cb1e50a</code></a>
Release: v2.2.0 (<a
href="https://redirect.github.com/google-github-actions/setup-gcloud/issues/719">#719</a>)</li>
<li><a
href="https://github.com/google-github-actions/setup-gcloud/commit/ef52f8c087fe78d43262625448b746144fe6448c"><code>ef52f8c</code></a>
Introduce an option to skip the tool cache (<a
href="https://redirect.github.com/google-github-actions/setup-gcloud/issues/718">#718</a>)</li>
<li>See full diff in <a
href="https://github.com/google-github-actions/setup-gcloud/compare/6a7c903a70c8625ed6700fa299f5ddb4ca6022e9...cb1e50a9932213ecece00a606661ae9ca44f3397">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google-github-actions/setup-gcloud&package-manager=github_actions&previous-version=2.1.5&new-version=2.2.0)](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 this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: DevCats <christofer@coder.com>
2025-08-11 20:57:05 -05:00
dependabot[bot] 3202e4899a chore(deps): bump crate-ci/typos from 1.34.0 to 1.35.3 (#318)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.34.0 to
1.35.3.
<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.35.3</h2>
<h2>[1.35.3] - 2025-08-08</h2>
<h3>Fixes</h3>
<ul>
<li>Don't correct <code>ratatui</code> in Rust files</li>
</ul>
<h2>v1.35.2</h2>
<h2>[1.35.2] - 2025-08-07</h2>
<h3>Fixes</h3>
<ul>
<li>Don't correct <code>unmarshaling</code></li>
</ul>
<h2>v1.35.1</h2>
<h2>[1.35.1] - 2025-08-04</h2>
<h3>Fixes</h3>
<ul>
<li>Fix typo in correction to <code>apostroph</code></li>
<li>Fix typo in correction to <code>cordinate</code></li>
<li>Fix typo in correction to <code>reproduceability</code></li>
<li>Fix typo in correction to <code>revolutionss</code></li>
<li>Fix typo in correction to <code>transivity</code></li>
</ul>
<h2>v1.35.0</h2>
<h2>[1.35.0] - 2025-08-04</h2>
<h3>Features</h3>
<ul>
<li>Updated the dictionary with the <a
href="https://redirect.github.com/crate-ci/typos/issues/1331">July
2025</a> changes</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.35.3] - 2025-08-08</h2>
<h3>Fixes</h3>
<ul>
<li>Don't correct <code>ratatui</code> in Rust files</li>
</ul>
<h2>[1.35.2] - 2025-08-07</h2>
<h3>Fixes</h3>
<ul>
<li>Don't correct <code>unmarshaling</code></li>
</ul>
<h2>[1.35.1] - 2025-08-04</h2>
<h3>Fixes</h3>
<ul>
<li>Fix typo in correction to <code>apostroph</code></li>
<li>Fix typo in correction to <code>cordinate</code></li>
<li>Fix typo in correction to <code>reproduceability</code></li>
<li>Fix typo in correction to <code>revolutionss</code></li>
<li>Fix typo in correction to <code>transivity</code></li>
</ul>
<h2>[1.35.0] - 2025-08-04</h2>
<h3>Features</h3>
<ul>
<li>Updated the dictionary with the <a
href="https://redirect.github.com/crate-ci/typos/issues/1331">July
2025</a> changes</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/crate-ci/typos/commit/52bd719c2c91f9d676e2aa359fc8e0db8925e6d8"><code>52bd719</code></a>
chore: Release</li>
<li><a
href="https://github.com/crate-ci/typos/commit/c6f77dda9e9bf82551f03a500347eb06ce8a90b1"><code>c6f77dd</code></a>
docs: Update changelog</li>
<li><a
href="https://github.com/crate-ci/typos/commit/e35d08c453d5ac2a4630b633dbb63e819b129193"><code>e35d08c</code></a>
Merge pull request <a
href="https://redirect.github.com/crate-ci/typos/issues/1353">#1353</a>
from Rolv-Apneseth/ratatui</li>
<li><a
href="https://github.com/crate-ci/typos/commit/9d6691bc8cf087436d192d03414b9c2420570343"><code>9d6691b</code></a>
fix: Ignore <code>ratatui</code> in Rust files</li>
<li><a
href="https://github.com/crate-ci/typos/commit/f1231bc2bcc92b2b18da70a877cf89afce08dd42"><code>f1231bc</code></a>
chore: Release</li>
<li><a
href="https://github.com/crate-ci/typos/commit/66def6387b9bb7954423333521eed23e75651f6e"><code>66def63</code></a>
docs: Update changelog</li>
<li><a
href="https://github.com/crate-ci/typos/commit/623f09b5bc658227e7e051fc494f3af24030d1cf"><code>623f09b</code></a>
chore: Release</li>
<li><a
href="https://github.com/crate-ci/typos/commit/1080316783320230c1f65e1c374e44dfc13829c6"><code>1080316</code></a>
chore: Release</li>
<li><a
href="https://github.com/crate-ci/typos/commit/442605b52920ac6faab2e457d3bafc0a6d05a5d1"><code>442605b</code></a>
Merge pull request <a
href="https://redirect.github.com/crate-ci/typos/issues/1352">#1352</a>
from epage/marshaling</li>
<li><a
href="https://github.com/crate-ci/typos/commit/983f866bac2164c77fc4ad8a06cdb0738c38ddba"><code>983f866</code></a>
fix(dict): Don't correct marshaling</li>
<li>Additional commits viewable in <a
href="https://github.com/crate-ci/typos/compare/v1.34.0...v1.35.3">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.34.0&new-version=1.35.3)](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 this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-11 20:50:46 -05:00
Hugo Dutka c4a5184725 chore: bump agentapi versions in the goose and gemini modules (#321)
related to https://github.com/coder/registry/pull/320
2025-08-11 19:28:14 +02:00
Phorcys 63d56eadc9 feat: add vscode-desktop-core module (#278)
Co-authored-by: Atif Ali <atif@coder.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-11 21:21:23 +05:00
Hugo Dutka 507b73a07e feat(agentapi): use wildcard alllowed hosts (#320)
Since https://github.com/coder/agentapi/pull/49 was merged, agentapi by
default only accepts requests with the `Host` header set to localhost,
127.0.0.1, or [::1]. In Coder, agentapi is served behind a reverse proxy
as a workspace app, so we need to use a wildcard
`AGENTAPI_ALLOWED_HOSTS` for agentapi-based modules to continue working.

This PR updates the claude code and agentapi modules, and a subsequent
PR will update modules that are based on the agentapi module.
2025-08-11 16:23:01 +02:00
Jullian Pepito 814f765313 fix(jetbrains): Ties var.group to the coder_app. (#310)
Co-authored-by: Jullian Pepito <jullian@mac.lan>
2025-08-11 13:22:11 +05:00
Atif Ali 92a154f54a chore: deploy registry on changes to contributers information (#315) 2025-08-10 00:53:39 +05:00
Ben Potter 7aa7dea5ad Fix contributor avatars and docs: use avatar key and correct anomaly image extension (#312)
Co-authored-by: bpmct <22407953+bpmct@users.noreply.github.com>
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2025-08-10 00:27:31 +05:00
sharkymark 59b0472125 feat: sharkymark profile and claude docker template (#304)
Closes #

## Description

registry profile creation and template submission for claude on docker

## Type of Change

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

## Template Information

**Path:** `registry/sharkymark/templates/docker-claude`  
**New version:** `v1.0.0`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

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

## Related Issues

n/a

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-08-09 18:40:04 +02:00
Muhammad Atif Ali 673caf2e95 Revert "chore(examples): add MODULE_NAME.tftest.hcl to new module sample and make run.sh optional"
This reverts commit a5edad7f17.
2025-08-08 17:29:11 +05:00
Muhammad Atif Ali ab5ff4b4be Revert "chore(test): add terraform tests for jetbrains, zed, code-server and keep mixed mode"
This reverts commit fb657b875d.
2025-08-08 17:29:11 +05:00
Muhammad Atif Ali f5a68b500b Revert "chore(test): migrate to terraform test and add initial .tftest for zed"
This reverts commit 016d4dc523.
2025-08-08 17:29:11 +05:00
Muhammad Atif Ali a5edad7f17 chore(examples): add MODULE_NAME.tftest.hcl to new module sample and make run.sh optional 2025-08-08 16:36:30 +05:00
Muhammad Atif Ali fb657b875d chore(test): add terraform tests for jetbrains, zed, code-server and keep mixed mode
- Add .tftest.hcl for jetbrains, zed, and code-server
- Remove Bun tests for these migrated modules only
- Keep Bun tests for other modules during transition
- Update contributing guide to mention terraform test
- Include runner script to execute terraform tests across modules
2025-08-08 16:33:35 +05:00
Muhammad Atif Ali 016d4dc523 chore(test): migrate to terraform test and add initial .tftest for zed
Replace Bun-based test runner with Terraform native testing. Adds script to discover and run tests across modules and updates docs/scripts to use terraform test.
2025-08-08 13:31:35 +05:00
Muhammad Atif Ali c8d99cfba3 fix: correct terraform state arg and log typos
- test/test.ts: ensure `-state` is immediately followed by the state file to avoid apply failures
- readmevalidation: fix two logger message typos (processing/processed)
2025-08-08 13:15:52 +05:00
Cian Johnston 74c8698566 feat: goose: add support for subdomain=false (#299)
Updates https://github.com/coder/coder/issues/18779
Builds on https://github.com/coder/registry/pull/297

## Description

Adds support for specifying `subdomain = false` in the agentapi module.
Change added in https://github.com/coder/registry/pull/297
NOTE: `AGENTAPI_CHAT_BASE_PATH` is exported before running `main.sh` in
agentapi, so this environment variable is available to calling modules
if `var.subdomain = false`.

## Type of Change

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

## Testing & Validation

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

## Related Issues

https://github.com/coder/coder/issues/18779
2025-08-07 22:12:32 -05:00
DevCats 03333991a4 feat: introduce automated tag and release process in maintainer guide (#280) 2025-08-07 16:06:13 +05:00
Susana Ferreira 2b0dba4ed1 chore: add description to JetBrains IDEs parameter (#303)
## Description

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

This PR adds a description field to the `jetbrains_ides`
`coder_parameter`.

This allows the JetBrains IDEs parameter to display a helpful
description in both the Coder UI and the CLI, improving clarity for
users when selecting which IDEs to configure in a workspace.

<img width="1102" height="252" alt="Screenshot 2025-08-07 at 11 04 13"
src="https://github.com/user-attachments/assets/90c78088-700a-4152-8a16-4b8c88c52e2c"
/>

## Type of Change

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

## Testing & Validation

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

## Related Issues

https://github.com/coder/coder/issues/19145
2025-08-07 11:42:48 +01:00
Cian Johnston 57c900b2c9 feat(agentapi): Add support for running under a subdomain (#297)
Updates https://github.com/coder/coder/issues/18779

A separate PR will update dependant modules `goose` and `aider`.

## Description

* Adds `subdomain` argument to `agentapi` module
* Updates `agentapi` module to set `AGENTAPI_CHAT_BASE_PATH` to an
autogenerated path if `var.subdomain = false`
* Updates default `agentapi` version to `v0.3.3` to support running
without subdomain

## Type of Change

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


## Testing & Validation

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

## Related Issues

- https://github.com/coder/coder/issues/18779
2025-08-06 12:38:49 +01:00
Ben Potter 0ccee61192 chore: remove unnecessary parameter (#282)
Co-authored-by: DevCats <christofer@coder.com>
2025-08-06 07:05:18 +05:00
dependabot[bot] 494dc4b8a1 chore(deps): bump google-github-actions/auth from 2.1.11 to 2.1.12 (#288)
Bumps
[google-github-actions/auth](https://github.com/google-github-actions/auth)
from 2.1.11 to 2.1.12.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/google-github-actions/auth/releases">google-github-actions/auth's
releases</a>.</em></p>
<blockquote>
<h2>v2.1.12</h2>
<h2>What's Changed</h2>
<ul>
<li>Add retries for getIDToken by <a
href="https://github.com/sethvargo"><code>@​sethvargo</code></a> in <a
href="https://redirect.github.com/google-github-actions/auth/pull/502">google-github-actions/auth#502</a></li>
<li>Release: v2.1.12 by <a
href="https://github.com/google-github-actions-bot"><code>@​google-github-actions-bot</code></a>
in <a
href="https://redirect.github.com/google-github-actions/auth/pull/503">google-github-actions/auth#503</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/google-github-actions/auth/compare/v2.1.11...v2.1.12">https://github.com/google-github-actions/auth/compare/v2.1.11...v2.1.12</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/google-github-actions/auth/commit/b7593ed2efd1c1617e1b0254da33b86225adb2a5"><code>b7593ed</code></a>
Release: v2.1.12 (<a
href="https://redirect.github.com/google-github-actions/auth/issues/503">#503</a>)</li>
<li><a
href="https://github.com/google-github-actions/auth/commit/c1ee334b4fb145a02e9d8343bb2e9f0dd06e586b"><code>c1ee334</code></a>
Add retries for getIDToken (<a
href="https://redirect.github.com/google-github-actions/auth/issues/502">#502</a>)</li>
<li>See full diff in <a
href="https://github.com/google-github-actions/auth/compare/140bb5113ffb6b65a7e9b937a81fa96cf5064462...b7593ed2efd1c1617e1b0254da33b86225adb2a5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google-github-actions/auth&package-manager=github_actions&previous-version=2.1.11&new-version=2.1.12)](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 this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 21:01:49 -05:00
Hugo Dutka 3b135ad4a4 fix(claude-code): revert workaround (#298)
The workaround introduced in https://github.com/coder/registry/pull/283
sometimes causes Coder to associate the Coder agent with the
`terraform_data` resource instead of a compute resource. Additionally,
it creates a new agent once a workspace is stopped.

See https://codercom.slack.com/archives/C08PHACTZRB/p1754391012982629
for more details.

<img width="2480" height="1312" alt="image (6)"
src="https://github.com/user-attachments/assets/6ccf2b40-92e7-4c67-b61f-b26888753e72"
/>

Related to https://github.com/coder/coder/issues/18776.
2025-08-05 13:28:58 +02:00
Danielle Maywood 258591833f fix(devcontainers-cli): allow yarn to install when packageManager not yarn (#287)
On our dogfood workspaces, we fail to install `@devcontainers/cli` with
`yarn` because our agent directory `/home/coder/coder` contains a
`package.json` with `packageManager` being set to `pnpm`. This change
instead ensures to run `yarn global add` inside the
`$CODER_SCRIPT_DATA_DIR` so that we don't read a `package.json` and
cause things to break.
2025-08-04 13:00:13 +01:00
Michael Orlov 3efc22c589 fix/amazon-q mcp integration (#248)
Co-authored-by: Michael Orlov <michaelo@amdocs.com>
Co-authored-by: DevCats <christofer@coder.com>
2025-08-03 06:27:46 +00:00
Hugo Dutka 8ba4c323c2 fix(claude-code): workaround for a coder bug (#283)
Workaround to address https://github.com/coder/coder/issues/18776
2025-08-02 16:21:13 +02:00
Phorcys 3afa72095b chore: polish some modules readme (#272) 2025-08-01 21:12:05 -05:00
Michael Smith cf66809349 fix: update HCP vault links to use current URL patterns (#275)
Realized this was an issue while fixing how we're rendering GFM alerts
on the Registry website.

## Description

Basically, the URLs we're using right now are technically valid, but
they were using the old URL pattern from before we created the
`coder/registry` repo. The Registry website has logic to handle
redirects for these, but we should avoid triggering it as it adds
unnecessary entries to the user's browser history.

### Changes made
- Updated all Registry links in the HCP Vault alert to include `coder`
as the namespace

## Type of Change

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

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally
2025-07-31 19:50:37 -05:00
Michael Smith 020a2cba79 fix: update icon path for k8s-username (#276)
## Description

This is an issue that doesn't exist within `coder/registry`, but was
breaking our build process for the Registry website. We were using an
invalid image path.

## Type of Change

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

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally
2025-07-31 20:58:06 +01:00
Eric Paulsen 3fd7b47097 feat: dynamic username template (#261)
## Description

this PR adds a new template that creates & runs Coder workspaces on K8s
with the user's Coder `username` as the Linux UID. a commonly requested
use-case by customers.

## Type of Change

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

## Module Information

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

**Path:** `registry/ericpaulsen/templates/k8s-pod-username.tf`  

## Testing & Validation

- [x] Changes tested locally
2025-07-31 14:25:29 +00:00
Atif Ali e1f077dac3 chore: deploy registry changes nightly (#264) 2025-07-30 22:33:25 -04:00
Marcin Tojek 29c52b7072 feat: jupyter-notebook: preinstall Python packages (#263) 2025-07-30 20:25:53 -05:00
Hugo Dutka 312cb71bf0 chore: bump agentapi version in the claude code module (#265)
Bump agentapi to v0.3.0 and claude code module version.
2025-07-30 13:37:30 +02:00
35C4n0r f89ea12d9e feat: gemini cli module (#246)
Closes #237
/claim #237

## Description

~https://www.loom.com/share/5b099c73935f4f87b8fdafe1509bb79d?sid=7fea43d6-86e9-45ae-9892-efeb3c820b82~
Updated:
https://www.loom.com/share/62e907eae8544d8cbbe560d7f63bd02d?sid=201212fa-eb58-44b5-8706-8bf9c2c37433
<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

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

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

## Testing & Validation

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

## Related Issues

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

---------

Co-authored-by: Atif Ali <me@matifali.dev>
Co-authored-by: Hugo Dutka <dutkahugo@gmail.com>
Co-authored-by: DevCats <christofer@coder.com>
2025-07-29 05:26:30 -05:00
dependabot[bot] 0fe47943aa chore(deps): bump google-github-actions/auth from 2.1.10 to 2.1.11 (#255)
Bumps
[google-github-actions/auth](https://github.com/google-github-actions/auth)
from 2.1.10 to 2.1.11.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/google-github-actions/auth/releases">google-github-actions/auth's
releases</a>.</em></p>
<blockquote>
<h2>v2.1.11</h2>
<h2>What's Changed</h2>
<ul>
<li>Update troubleshooting docs for Python by <a
href="https://github.com/sethvargo"><code>@​sethvargo</code></a> in <a
href="https://redirect.github.com/google-github-actions/auth/pull/488">google-github-actions/auth#488</a></li>
<li>Add linters by <a
href="https://github.com/sethvargo"><code>@​sethvargo</code></a> in <a
href="https://redirect.github.com/google-github-actions/auth/pull/499">google-github-actions/auth#499</a></li>
<li>Update deps by <a
href="https://github.com/sethvargo"><code>@​sethvargo</code></a> in <a
href="https://redirect.github.com/google-github-actions/auth/pull/500">google-github-actions/auth#500</a></li>
<li>Release: v2.1.11 by <a
href="https://github.com/google-github-actions-bot"><code>@​google-github-actions-bot</code></a>
in <a
href="https://redirect.github.com/google-github-actions/auth/pull/501">google-github-actions/auth#501</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/google-github-actions/auth/compare/v2.1.10...v2.1.11">https://github.com/google-github-actions/auth/compare/v2.1.10...v2.1.11</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/google-github-actions/auth/commit/140bb5113ffb6b65a7e9b937a81fa96cf5064462"><code>140bb51</code></a>
Release: v2.1.11 (<a
href="https://redirect.github.com/google-github-actions/auth/issues/501">#501</a>)</li>
<li><a
href="https://github.com/google-github-actions/auth/commit/ab3132e2ad698521ee1355566103fa838732e48c"><code>ab3132e</code></a>
Update deps (<a
href="https://redirect.github.com/google-github-actions/auth/issues/500">#500</a>)</li>
<li><a
href="https://github.com/google-github-actions/auth/commit/25b96bac992fdf64486c6fd3fd3d9c4cddb3a812"><code>25b96ba</code></a>
Add linters (<a
href="https://redirect.github.com/google-github-actions/auth/issues/499">#499</a>)</li>
<li><a
href="https://github.com/google-github-actions/auth/commit/0920706a19e9d22c3d0da43d1db5939c6ad837a8"><code>0920706</code></a>
Update troubleshooting docs for Python (<a
href="https://redirect.github.com/google-github-actions/auth/issues/488">#488</a>)</li>
<li>See full diff in <a
href="https://github.com/google-github-actions/auth/compare/ba79af03959ebeac9769e648f473a284504d9193...140bb5113ffb6b65a7e9b937a81fa96cf5064462">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google-github-actions/auth&package-manager=github_actions&previous-version=2.1.10&new-version=2.1.11)](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 this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-28 18:17:31 -05:00
dependabot[bot] 4f225fd7d3 chore(deps): bump google-github-actions/setup-gcloud from 2.1.4 to 2.1.5 (#256)
Bumps
[google-github-actions/setup-gcloud](https://github.com/google-github-actions/setup-gcloud)
from 2.1.4 to 2.1.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/google-github-actions/setup-gcloud/releases">google-github-actions/setup-gcloud's
releases</a>.</em></p>
<blockquote>
<h2>v2.1.5</h2>
<h2>What's Changed</h2>
<ul>
<li>security: bump undici from 5.28.5 to 5.29.0 in the npm_and_yarn
group by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/google-github-actions/setup-gcloud/pull/711">google-github-actions/setup-gcloud#711</a></li>
<li>Update linters by <a
href="https://github.com/sethvargo"><code>@​sethvargo</code></a> in <a
href="https://redirect.github.com/google-github-actions/setup-gcloud/pull/715">google-github-actions/setup-gcloud#715</a></li>
<li>Update deps by <a
href="https://github.com/sethvargo"><code>@​sethvargo</code></a> in <a
href="https://redirect.github.com/google-github-actions/setup-gcloud/pull/716">google-github-actions/setup-gcloud#716</a></li>
<li>Release: v2.1.5 by <a
href="https://github.com/google-github-actions-bot"><code>@​google-github-actions-bot</code></a>
in <a
href="https://redirect.github.com/google-github-actions/setup-gcloud/pull/717">google-github-actions/setup-gcloud#717</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/google-github-actions/setup-gcloud/compare/v2.1.4...v2.1.5">https://github.com/google-github-actions/setup-gcloud/compare/v2.1.4...v2.1.5</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/google-github-actions/setup-gcloud/commit/6a7c903a70c8625ed6700fa299f5ddb4ca6022e9"><code>6a7c903</code></a>
Release: v2.1.5 (<a
href="https://redirect.github.com/google-github-actions/setup-gcloud/issues/717">#717</a>)</li>
<li><a
href="https://github.com/google-github-actions/setup-gcloud/commit/e838bc6edfe3907980c74d5aad506fd6e173b0d6"><code>e838bc6</code></a>
Update deps (<a
href="https://redirect.github.com/google-github-actions/setup-gcloud/issues/716">#716</a>)</li>
<li><a
href="https://github.com/google-github-actions/setup-gcloud/commit/98d8f78fcc2354c736499a506ad9e7be3f4c2640"><code>98d8f78</code></a>
Update linters (<a
href="https://redirect.github.com/google-github-actions/setup-gcloud/issues/715">#715</a>)</li>
<li><a
href="https://github.com/google-github-actions/setup-gcloud/commit/a8b58010a5b2a061afd605f50e88629c9ec7536b"><code>a8b5801</code></a>
security: bump undici from 5.28.5 to 5.29.0 in the npm_and_yarn group
(<a
href="https://redirect.github.com/google-github-actions/setup-gcloud/issues/711">#711</a>)</li>
<li>See full diff in <a
href="https://github.com/google-github-actions/setup-gcloud/compare/77e7a554d41e2ee56fc945c52dfd3f33d12def9a...6a7c903a70c8625ed6700fa299f5ddb4ca6022e9">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google-github-actions/setup-gcloud&package-manager=github_actions&previous-version=2.1.4&new-version=2.1.5)](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 this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-28 18:13:18 -05:00
35C4n0r f04d7d2808 feat: tmux module (#229)
Closes #203
/claim #203

## Description

Introduce the `tmux` module

## Demo

https://www.loom.com/share/ec8169d34c3043f7af2163b1a1a14a4b?sid=1ea8bcb2-3db0-43ca-965a-5ed42eec3448

## Type of Change

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

## Module Information

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

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

## Testing & Validation

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

## Related Issues
#203
2025-07-24 20:31:57 -05:00
DevCats 4ae6370bcf feat(tag_release): add script for automatically tagging and releasing modules (#250)
## Description

This PR introduces a script `.github/scripts/tag_release.sh` which
allows the maintainer and other org members to automatically tag and
release modules based on the checked out commit id. This script relies
on the README's for the updated modules to accurately reflect the
version bump to work properly.
<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Testing & Validation

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

## Reference

Run on this Commit:
https://github.com/coder/registry/commit/9ed5084bfbad9266469a9d189635d7fccb1ed277

### Script Output:

```
coder@tagging-test:~/workspace/registry$ ./.github/scripts/tag_release.sh 
🚀 Coder Registry Tag Release Script
Operating on commit: 9ed5084bfb

🔍 Scanning all modules for missing release tags...

find: warning: you have specified the global option -mindepth after the argument -type, but global options are not positional, i.e., -mindepth affects tests specified before it as well as those specified after it.  Please specify global options before other arguments.
find: warning: you have specified the global option -maxdepth after the argument -type, but global options are not positional, i.e., -maxdepth affects tests specified before it as well as those specified after it.  Please specify global options before other arguments.
📦 coder/agentapi: v1.0.1 (needs tag)
📦 coder/aider: v1.1.1 (needs tag)
📦 coder/amazon-dcv-windows: v1.1.1 (needs tag)
📦 coder/amazon-q: v1.1.1 (needs tag)
📦 coder/aws-region: v1.0.31 (needs tag)
📦 coder/azure-region: v1.0.31 (needs tag)
 coder/claude-code: v2.0.3 (already tagged)
📦 coder/coder-login: v1.0.31 (needs tag)
📦 coder/code-server: v1.3.1 (needs tag)
📦 coder/cursor: v1.2.1 (needs tag)
📦 coder/devcontainers-cli: v1.0.31 (needs tag)
📦 coder/dotfiles: v1.2.1 (needs tag)
📦 coder/filebrowser: v1.1.2 (needs tag)
📦 coder/fly-region: v1.0.31 (needs tag)
📦 coder/gcp-region: v1.0.31 (needs tag)
📦 coder/git-clone: v1.1.1 (needs tag)
📦 coder/git-commit-signing: v1.0.31 (needs tag)
📦 coder/git-config: v1.0.31 (needs tag)
📦 coder/github-upload-public-key: v1.0.31 (needs tag)
📦 coder/goose: v2.0.1 (needs tag)
📦 coder/hcp-vault-secrets: v1.0.33 (needs tag)
📦 coder/jetbrains: v1.0.1 (needs tag)
 coder/jetbrains-fleet: v1.0.1 (already tagged)
📦 coder/jetbrains-gateway: v1.2.2 (needs tag)
📦 coder/jfrog-oauth: v1.0.31 (needs tag)
📦 coder/jfrog-token: v1.0.31 (needs tag)
📦 coder/jupyterlab: v1.1.1 (needs tag)
📦 coder/jupyter-notebook: v1.1.1 (needs tag)
📦 coder/kasmvnc: v1.2.1 (needs tag)
 coder/kiro: v1.0.0 (already tagged)
📦 coder/local-windows-rdp: v1.0.2 (needs tag)
📦 coder/personalize: v1.0.31 (needs tag)
📦 coder/slackme: v1.0.31 (needs tag)
📦 coder/vault-github: v1.0.31 (needs tag)
📦 coder/vault-jwt: v1.1.1 (needs tag)
📦 coder/vault-token: v1.2.1 (needs tag)
📦 coder/vscode-desktop: v1.1.1 (needs tag)
📦 coder/vscode-web: v1.3.1 (needs tag)
📦 coder/windows-rdp: v1.2.3 (needs tag)
📦 coder/windsurf: v1.1.1 (needs tag)
📦 coder/zed: v1.0.1 (needs tag)
 nataindata/apache-airflow: v1.0.14 (already tagged)
 thezoker/nodejs: v1.0.11 (already tagged)
 whizus/exoscale-instance-type: v1.0.13 (already tagged)
 whizus/exoscale-zone: v1.0.13 (already tagged)

📊 Summary: 38 of 45 modules need tagging

## Tags to be created:
- `release/coder/agentapi/v1.0.1`
- `release/coder/aider/v1.1.1`
- `release/coder/amazon-dcv-windows/v1.1.1`
- `release/coder/amazon-q/v1.1.1`
- `release/coder/aws-region/v1.0.31`
- `release/coder/azure-region/v1.0.31`
- `release/coder/coder-login/v1.0.31`
- `release/coder/code-server/v1.3.1`
- `release/coder/cursor/v1.2.1`
- `release/coder/devcontainers-cli/v1.0.31`
- `release/coder/dotfiles/v1.2.1`
- `release/coder/filebrowser/v1.1.2`
- `release/coder/fly-region/v1.0.31`
- `release/coder/gcp-region/v1.0.31`
- `release/coder/git-clone/v1.1.1`
- `release/coder/git-commit-signing/v1.0.31`
- `release/coder/git-config/v1.0.31`
- `release/coder/github-upload-public-key/v1.0.31`
- `release/coder/goose/v2.0.1`
- `release/coder/hcp-vault-secrets/v1.0.33`
- `release/coder/jetbrains/v1.0.1`
- `release/coder/jetbrains-gateway/v1.2.2`
- `release/coder/jfrog-oauth/v1.0.31`
- `release/coder/jfrog-token/v1.0.31`
- `release/coder/jupyterlab/v1.1.1`
- `release/coder/jupyter-notebook/v1.1.1`
- `release/coder/kasmvnc/v1.2.1`
- `release/coder/local-windows-rdp/v1.0.2`
- `release/coder/personalize/v1.0.31`
- `release/coder/slackme/v1.0.31`
- `release/coder/vault-github/v1.0.31`
- `release/coder/vault-jwt/v1.1.1`
- `release/coder/vault-token/v1.2.1`
- `release/coder/vscode-desktop/v1.1.1`
- `release/coder/vscode-web/v1.3.1`
- `release/coder/windows-rdp/v1.2.3`
- `release/coder/windsurf/v1.1.1`
- `release/coder/zed/v1.0.1`


 Do you want to proceed with creating and pushing these release tags?
   This will create git tags and push them to the remote repository.

Continue? [y/N]: y

🏷️  Creating release tags for commit: 9ed5084bfb

Creating tag: release/coder/agentapi/v1.0.1
 Created: release/coder/agentapi/v1.0.1
Creating tag: release/coder/aider/v1.1.1
 Created: release/coder/aider/v1.1.1
Creating tag: release/coder/amazon-dcv-windows/v1.1.1
 Created: release/coder/amazon-dcv-windows/v1.1.1
Creating tag: release/coder/amazon-q/v1.1.1
 Created: release/coder/amazon-q/v1.1.1
Creating tag: release/coder/aws-region/v1.0.31
 Created: release/coder/aws-region/v1.0.31
Creating tag: release/coder/azure-region/v1.0.31
 Created: release/coder/azure-region/v1.0.31
Creating tag: release/coder/coder-login/v1.0.31
 Created: release/coder/coder-login/v1.0.31
Creating tag: release/coder/code-server/v1.3.1
 Created: release/coder/code-server/v1.3.1
Creating tag: release/coder/cursor/v1.2.1
 Created: release/coder/cursor/v1.2.1
Creating tag: release/coder/devcontainers-cli/v1.0.31
 Created: release/coder/devcontainers-cli/v1.0.31
Creating tag: release/coder/dotfiles/v1.2.1
 Created: release/coder/dotfiles/v1.2.1
Creating tag: release/coder/filebrowser/v1.1.2
 Created: release/coder/filebrowser/v1.1.2
Creating tag: release/coder/fly-region/v1.0.31
 Created: release/coder/fly-region/v1.0.31
Creating tag: release/coder/gcp-region/v1.0.31
 Created: release/coder/gcp-region/v1.0.31
Creating tag: release/coder/git-clone/v1.1.1
 Created: release/coder/git-clone/v1.1.1
Creating tag: release/coder/git-commit-signing/v1.0.31
 Created: release/coder/git-commit-signing/v1.0.31
Creating tag: release/coder/git-config/v1.0.31
 Created: release/coder/git-config/v1.0.31
Creating tag: release/coder/github-upload-public-key/v1.0.31
 Created: release/coder/github-upload-public-key/v1.0.31
Creating tag: release/coder/goose/v2.0.1
 Created: release/coder/goose/v2.0.1
Creating tag: release/coder/hcp-vault-secrets/v1.0.33
 Created: release/coder/hcp-vault-secrets/v1.0.33
Creating tag: release/coder/jetbrains/v1.0.1
 Created: release/coder/jetbrains/v1.0.1
Creating tag: release/coder/jetbrains-gateway/v1.2.2
 Created: release/coder/jetbrains-gateway/v1.2.2
Creating tag: release/coder/jfrog-oauth/v1.0.31
 Created: release/coder/jfrog-oauth/v1.0.31
Creating tag: release/coder/jfrog-token/v1.0.31
 Created: release/coder/jfrog-token/v1.0.31
Creating tag: release/coder/jupyterlab/v1.1.1
 Created: release/coder/jupyterlab/v1.1.1
Creating tag: release/coder/jupyter-notebook/v1.1.1
 Created: release/coder/jupyter-notebook/v1.1.1
Creating tag: release/coder/kasmvnc/v1.2.1
 Created: release/coder/kasmvnc/v1.2.1
Creating tag: release/coder/local-windows-rdp/v1.0.2
 Created: release/coder/local-windows-rdp/v1.0.2
Creating tag: release/coder/personalize/v1.0.31
 Created: release/coder/personalize/v1.0.31
Creating tag: release/coder/slackme/v1.0.31
 Created: release/coder/slackme/v1.0.31
Creating tag: release/coder/vault-github/v1.0.31
 Created: release/coder/vault-github/v1.0.31
Creating tag: release/coder/vault-jwt/v1.1.1
 Created: release/coder/vault-jwt/v1.1.1
Creating tag: release/coder/vault-token/v1.2.1
 Created: release/coder/vault-token/v1.2.1
Creating tag: release/coder/vscode-desktop/v1.1.1
 Created: release/coder/vscode-desktop/v1.1.1
Creating tag: release/coder/vscode-web/v1.3.1
 Created: release/coder/vscode-web/v1.3.1
Creating tag: release/coder/windows-rdp/v1.2.3
 Created: release/coder/windows-rdp/v1.2.3
Creating tag: release/coder/windsurf/v1.1.1
 Created: release/coder/windsurf/v1.1.1
Creating tag: release/coder/zed/v1.0.1
 Created: release/coder/zed/v1.0.1

📊 Tag creation summary:
  Created: 38
  Failed: 0

🚀 Pushing tags to origin...
Pushing: release/coder/agentapi/v1.0.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 187 bytes | 26.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/agentapi/v1.0.1 -> release/coder/agentapi/v1.0.1
 Pushed: release/coder/agentapi/v1.0.1
Pushing: release/coder/aider/v1.1.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 184 bytes | 26.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/aider/v1.1.1 -> release/coder/aider/v1.1.1
 Pushed: release/coder/aider/v1.1.1
Pushing: release/coder/amazon-dcv-windows/v1.1.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 195 bytes | 65.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/amazon-dcv-windows/v1.1.1 -> release/coder/amazon-dcv-windows/v1.1.1
 Pushed: release/coder/amazon-dcv-windows/v1.1.1
Pushing: release/coder/amazon-q/v1.1.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 187 bytes | 26.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/amazon-q/v1.1.1 -> release/coder/amazon-q/v1.1.1
 Pushed: release/coder/amazon-q/v1.1.1
Pushing: release/coder/aws-region/v1.0.31
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 190 bytes | 23.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/aws-region/v1.0.31 -> release/coder/aws-region/v1.0.31
 Pushed: release/coder/aws-region/v1.0.31
Pushing: release/coder/azure-region/v1.0.31
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 192 bytes | 27.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/azure-region/v1.0.31 -> release/coder/azure-region/v1.0.31
 Pushed: release/coder/azure-region/v1.0.31
Pushing: release/coder/coder-login/v1.0.31
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 189 bytes | 31.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/coder-login/v1.0.31 -> release/coder/coder-login/v1.0.31
 Pushed: release/coder/coder-login/v1.0.31
Pushing: release/coder/code-server/v1.3.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 188 bytes | 31.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/code-server/v1.3.1 -> release/coder/code-server/v1.3.1
 Pushed: release/coder/code-server/v1.3.1
Pushing: release/coder/cursor/v1.2.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 186 bytes | 62.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/cursor/v1.2.1 -> release/coder/cursor/v1.2.1
 Pushed: release/coder/cursor/v1.2.1
Pushing: release/coder/devcontainers-cli/v1.0.31
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 194 bytes | 32.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/devcontainers-cli/v1.0.31 -> release/coder/devcontainers-cli/v1.0.31
 Pushed: release/coder/devcontainers-cli/v1.0.31
Pushing: release/coder/dotfiles/v1.2.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 187 bytes | 31.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/dotfiles/v1.2.1 -> release/coder/dotfiles/v1.2.1
 Pushed: release/coder/dotfiles/v1.2.1
Pushing: release/coder/filebrowser/v1.1.2
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 190 bytes | 63.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/filebrowser/v1.1.2 -> release/coder/filebrowser/v1.1.2
 Pushed: release/coder/filebrowser/v1.1.2
Pushing: release/coder/fly-region/v1.0.31
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 190 bytes | 63.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/fly-region/v1.0.31 -> release/coder/fly-region/v1.0.31
 Pushed: release/coder/fly-region/v1.0.31
Pushing: release/coder/gcp-region/v1.0.31
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 190 bytes | 63.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/gcp-region/v1.0.31 -> release/coder/gcp-region/v1.0.31
 Pushed: release/coder/gcp-region/v1.0.31
Pushing: release/coder/git-clone/v1.1.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 187 bytes | 93.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/git-clone/v1.1.1 -> release/coder/git-clone/v1.1.1
 Pushed: release/coder/git-clone/v1.1.1
Pushing: release/coder/git-commit-signing/v1.0.31
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 194 bytes | 38.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/git-commit-signing/v1.0.31 -> release/coder/git-commit-signing/v1.0.31
 Pushed: release/coder/git-commit-signing/v1.0.31
Pushing: release/coder/git-config/v1.0.31
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 190 bytes | 38.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/git-config/v1.0.31 -> release/coder/git-config/v1.0.31
 Pushed: release/coder/git-config/v1.0.31
Pushing: release/coder/github-upload-public-key/v1.0.31
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 200 bytes | 40.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/github-upload-public-key/v1.0.31 -> release/coder/github-upload-public-key/v1.0.31
 Pushed: release/coder/github-upload-public-key/v1.0.31
Pushing: release/coder/goose/v2.0.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 185 bytes | 46.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/goose/v2.0.1 -> release/coder/goose/v2.0.1
 Pushed: release/coder/goose/v2.0.1
Pushing: release/coder/hcp-vault-secrets/v1.0.33
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 195 bytes | 39.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/hcp-vault-secrets/v1.0.33 -> release/coder/hcp-vault-secrets/v1.0.33
 Pushed: release/coder/hcp-vault-secrets/v1.0.33
Pushing: release/coder/jetbrains/v1.0.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 187 bytes | 46.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/jetbrains/v1.0.1 -> release/coder/jetbrains/v1.0.1
 Pushed: release/coder/jetbrains/v1.0.1
Pushing: release/coder/jetbrains-gateway/v1.2.2
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 194 bytes | 97.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/jetbrains-gateway/v1.2.2 -> release/coder/jetbrains-gateway/v1.2.2
 Pushed: release/coder/jetbrains-gateway/v1.2.2
Pushing: release/coder/jfrog-oauth/v1.0.31
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 191 bytes | 47.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/jfrog-oauth/v1.0.31 -> release/coder/jfrog-oauth/v1.0.31
 Pushed: release/coder/jfrog-oauth/v1.0.31
Pushing: release/coder/jfrog-token/v1.0.31
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 191 bytes | 47.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/jfrog-token/v1.0.31 -> release/coder/jfrog-token/v1.0.31
 Pushed: release/coder/jfrog-token/v1.0.31
Pushing: release/coder/jupyterlab/v1.1.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 188 bytes | 47.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/jupyterlab/v1.1.1 -> release/coder/jupyterlab/v1.1.1
 Pushed: release/coder/jupyterlab/v1.1.1
Pushing: release/coder/jupyter-notebook/v1.1.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 192 bytes | 96.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/jupyter-notebook/v1.1.1 -> release/coder/jupyter-notebook/v1.1.1
 Pushed: release/coder/jupyter-notebook/v1.1.1
Pushing: release/coder/kasmvnc/v1.2.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 187 bytes | 46.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/kasmvnc/v1.2.1 -> release/coder/kasmvnc/v1.2.1
 Pushed: release/coder/kasmvnc/v1.2.1
Pushing: release/coder/local-windows-rdp/v1.0.2
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 194 bytes | 97.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/local-windows-rdp/v1.0.2 -> release/coder/local-windows-rdp/v1.0.2
 Pushed: release/coder/local-windows-rdp/v1.0.2
Pushing: release/coder/personalize/v1.0.31
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 191 bytes | 63.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/personalize/v1.0.31 -> release/coder/personalize/v1.0.31
 Pushed: release/coder/personalize/v1.0.31
Pushing: release/coder/slackme/v1.0.31
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 187 bytes | 62.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/slackme/v1.0.31 -> release/coder/slackme/v1.0.31
 Pushed: release/coder/slackme/v1.0.31
Pushing: release/coder/vault-github/v1.0.31
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 192 bytes | 64.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/vault-github/v1.0.31 -> release/coder/vault-github/v1.0.31
 Pushed: release/coder/vault-github/v1.0.31
Pushing: release/coder/vault-jwt/v1.1.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 188 bytes | 62.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/vault-jwt/v1.1.1 -> release/coder/vault-jwt/v1.1.1
 Pushed: release/coder/vault-jwt/v1.1.1
Pushing: release/coder/vault-token/v1.2.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 190 bytes | 63.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/vault-token/v1.2.1 -> release/coder/vault-token/v1.2.1
 Pushed: release/coder/vault-token/v1.2.1
Pushing: release/coder/vscode-desktop/v1.1.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 191 bytes | 191.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/vscode-desktop/v1.1.1 -> release/coder/vscode-desktop/v1.1.1
 Pushed: release/coder/vscode-desktop/v1.1.1
Pushing: release/coder/vscode-web/v1.3.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 188 bytes | 47.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/vscode-web/v1.3.1 -> release/coder/vscode-web/v1.3.1
 Pushed: release/coder/vscode-web/v1.3.1
Pushing: release/coder/windows-rdp/v1.2.3
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 190 bytes | 63.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/windows-rdp/v1.2.3 -> release/coder/windows-rdp/v1.2.3
 Pushed: release/coder/windows-rdp/v1.2.3
Pushing: release/coder/windsurf/v1.1.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 187 bytes | 93.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/windsurf/v1.1.1 -> release/coder/windsurf/v1.1.1
 Pushed: release/coder/windsurf/v1.1.1
Pushing: release/coder/zed/v1.0.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 183 bytes | 91.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/coder/registry
 * [new tag]         release/coder/zed/v1.0.1 -> release/coder/zed/v1.0.1
 Pushed: release/coder/zed/v1.0.1

📊 Push summary:
  Pushed: 38
  Failed: 0

🎉 Successfully created and pushed 38 release tags!

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

After

Width:  |  Height:  |  Size: 852 B

+8
View File
@@ -0,0 +1,8 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="25" height="25"/>
<path d="M5.50694 21.1637C5.17323 21.1637 4.89218 21.1064 4.66378 20.9926C4.436 20.8787 4.26333 20.7052 4.1476 20.4763C4.03187 20.2473 3.97247 19.9672 3.97247 19.6382V16.2463C3.97247 15.8281 3.88859 15.5239 3.72265 15.3341C3.55549 15.1449 3.25912 15.0449 2.83418 15.0353C2.70191 15.0353 2.59598 14.9859 2.51577 14.8853C2.43433 14.7859 2.39453 14.6708 2.39453 14.5425C2.39453 14.4033 2.43433 14.2882 2.51577 14.1984C2.5966 14.1087 2.70375 14.0593 2.83418 14.0496C3.25912 14.0394 3.55549 13.94 3.72265 13.7508C3.88981 13.5616 3.97247 13.2622 3.97247 12.8537V9.46177C3.97247 8.96352 4.10474 8.58456 4.36742 8.3261C4.6301 8.06763 5.01035 7.9375 5.50694 7.9375H9.55926C9.71173 7.9375 9.83725 7.98269 9.9389 8.07185C10.0399 8.16162 10.0914 8.27669 10.0914 8.41466C10.0914 8.5448 10.0485 8.65626 9.96278 8.75145C9.87706 8.84664 9.76316 8.89423 9.62049 8.89423H5.8578C5.6441 8.89423 5.48245 8.94906 5.37162 9.05871C5.26018 9.16836 5.20446 9.33766 5.20446 9.5678V12.9754C5.20446 13.2742 5.14323 13.5454 5.02199 13.7894C4.90075 14.034 4.73848 14.2256 4.53581 14.3659C4.33313 14.5051 4.09616 14.575 3.82246 14.575V14.5147C4.09616 14.5147 4.33313 14.5846 4.53581 14.7238C4.73848 14.863 4.90075 15.0552 5.02199 15.3004C5.14323 15.5444 5.20446 15.8155 5.20446 16.1143V19.537C5.20446 19.7671 5.26018 19.9358 5.37162 20.0461C5.48306 20.1569 5.64533 20.2106 5.8578 20.2106H9.62049C9.76194 20.2106 9.87583 20.2582 9.96278 20.3527C10.0497 20.4479 10.0914 20.56 10.0914 20.6895C10.0914 20.8191 10.0412 20.9299 9.9389 21.0251C9.83725 21.1203 9.71112 21.1673 9.55926 21.1673H5.50694V21.1643V21.1637Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
<path d="M15.4423 21.1634C15.2898 21.1634 15.1643 21.1158 15.0626 21.0212C14.961 20.926 14.9102 20.8139 14.9102 20.6856C14.9102 20.5573 14.953 20.444 15.0387 20.3488C15.1245 20.2536 15.2384 20.2067 15.381 20.2067H19.1437C19.3574 20.2067 19.5191 20.153 19.6299 20.0422C19.7413 19.9325 19.7971 19.7632 19.7971 19.5331V16.1104C19.7971 15.8116 19.8583 15.5405 19.9795 15.2965C20.1008 15.0519 20.263 14.8603 20.4657 14.7199C20.6684 14.5807 20.9054 14.5108 21.1791 14.5108V14.5711C20.9054 14.5711 20.6684 14.5012 20.4657 14.362C20.263 14.2229 20.1008 14.0307 19.9795 13.7855C19.8583 13.5415 19.7971 13.2703 19.7971 12.9715V9.5639C19.7971 9.33496 19.7413 9.16566 19.6299 9.0548C19.5185 8.94515 19.3562 8.89033 19.1437 8.89033H15.381C15.2396 8.89033 15.1257 8.84273 15.0387 8.74754C14.953 8.65355 14.9102 8.54089 14.9102 8.41076C14.9102 8.27158 14.9604 8.15771 15.0626 8.06795C15.1637 7.97818 15.2898 7.93359 15.4423 7.93359H19.4946C19.9912 7.93359 20.3702 8.06373 20.6341 8.32219C20.898 8.58065 21.029 8.95961 21.029 9.45786V12.8498C21.029 13.2583 21.1129 13.5583 21.2789 13.7469C21.446 13.9361 21.7424 14.0361 22.1673 14.0457C22.2996 14.0554 22.4055 14.1048 22.4858 14.1945C22.5672 14.2843 22.607 14.3994 22.607 14.5385C22.607 14.6687 22.5672 14.7826 22.4858 14.8814C22.4055 14.9808 22.2978 15.0314 22.1673 15.0314C21.7424 15.041 21.4466 15.141 21.2789 15.3302C21.1117 15.5194 21.029 15.823 21.029 16.2424V19.6343C21.029 19.9639 20.9709 20.2422 20.8539 20.4723C20.737 20.7025 20.5655 20.8736 20.3377 20.9887C20.1093 21.1025 19.8283 21.1598 19.4946 21.1598H15.4423V21.1628V21.1634Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
<path d="M16.4845 15.8401C17.2224 15.8401 17.8206 15.2515 17.8206 14.5255C17.8206 13.7996 17.2224 13.2109 16.4845 13.2109C15.7467 13.2109 15.1484 13.7996 15.1484 14.5255C15.1484 15.2515 15.7467 15.8401 16.4845 15.8401Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
<path d="M9.00014 15.8401C9.73798 15.8401 10.3362 15.2515 10.3362 14.5255C10.3362 13.7996 9.73798 13.2109 9.00014 13.2109C8.2623 13.2109 7.66406 13.7996 7.66406 14.5255C7.66406 15.2515 8.2623 15.8401 9.00014 15.8401Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
<path d="M12.0442 4.13327L11.942 6.81971C11.942 6.97033 11.7974 7.04564 11.5084 7.04564C11.2194 7.04564 11.0749 6.97033 11.0749 6.81971C11.0492 6.15036 11.0284 5.63103 11.0112 5.26291C11.0027 4.88637 10.9941 4.61826 10.9855 4.45921C10.9769 4.30016 10.9727 4.20376 10.9727 4.17062V4.12062C10.9727 3.92843 11.1515 3.83203 11.5084 3.83203C11.8654 3.83203 12.0442 3.93264 12.0442 4.13327ZM14.213 4.13327L14.1108 6.81971C14.1108 6.97033 13.9663 7.04564 13.6773 7.04564C13.3883 7.04564 13.2437 6.97033 13.2437 6.81971C13.218 6.15036 13.1972 5.63103 13.1801 5.26291C13.1715 4.88637 13.1629 4.61826 13.1543 4.45921C13.1458 4.30016 13.1415 4.20376 13.1415 4.17062V4.12062C13.1415 3.92843 13.3203 3.83203 13.6773 3.83203C14.0342 3.83203 14.213 3.93264 14.213 4.13327Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

+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

+210
View File
@@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="300mm"
height="207mm"
viewBox="0 0 300 207"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<title
id="title1">copyparty_logo</title>
<defs
id="defs1">
<linearGradient
inkscape:collect="always"
id="linearGradient1">
<stop
style="stop-color:#ffcc55;stop-opacity:1"
offset="0"
id="stop1" />
<stop
style="stop-color:#ffcc00;stop-opacity:1"
offset="0.2"
id="stop2" />
<stop
style="stop-color:#ff8800;stop-opacity:1"
offset="1"
id="stop3" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="15"
y1="15"
x2="15"
y2="143"
gradientUnits="userSpaceOnUse" />
</defs>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>copyparty_logo</dc:title>
<dc:source>github.com/9001/copyparty</dc:source>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="kassett">
<rect
style="fill:#333333"
id="rect1"
width="300"
height="205"
x="0"
y="0"
rx="12"
ry="12" />
<rect
style="fill:url(#linearGradient2)"
id="rect2"
width="270"
height="128"
x="15"
y="15"
rx="8"
ry="8" />
<rect
style="fill:#333333"
id="rect3"
width="172"
height="52"
x="64"
y="72"
rx="26"
ry="26" />
<circle
style="fill:#cccccc"
id="circle1"
cx="91"
cy="98"
r="18" />
<circle
style="fill:#cccccc"
id="circle2"
cx="209"
cy="98"
r="18" />
<path
style="fill:#737373;stroke-width:1px"
d="m 48,207 10,-39 c 1.79,-6.2 5.6,-7.8 12,-8 60,-1 100,-1 160,0 6.4,0.2 10,1.8 12,8 l 10,39 z"
id="path1"
sodipodi:nodetypes="ccccccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="tekst"
style="display:none">
<text
xml:space="preserve"
style="font-size:38.8056px;line-height:1.25;font-family:Akbar;-inkscape-font-specification:Akbar;letter-spacing:3.70417px;word-spacing:0px;fill:#333333"
x="47.153069"
y="55.548954"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
x="47.153069"
y="55.548954"
style="-inkscape-font-specification:Akbar"
rotate="0 0">copyparty</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="stensatt">
<path
d="m 63.5,50.9 q -0.85,0.93 -4.73,2.3 -3.6,1.3 -4.4,1.3 -3.3,0 -5.1,-2.1 -1.75,-2 -1.75,-5.36 0,-4.6 3.76,-7.64 3.3,-2.7 7.3,-2.7 0.4,0 0.93,0.74 0.54,0.7 0.54,1.16 0,2.06 -2.2,2.7 -1.36,0.4 -4.04,1.16 -2.2,1.16 -2.2,4.4 0,3.2 2.9,3.2 0.85,0 0.85,0 0.54,0 1.44,-0.16 1.1,-0.23 2.9,-0.74 1.8,-0.54 2.13,-0.54 0.4,0 1.75,0.6 z"
style="fill:#333333"
id="path11" />
<path
d="m 87.6,45 q 0,4.2 -3.7,6.95 -3.2,2.3 -6.87,2.3 -3.4,0 -6,-2.6 -2.5,-2.6 -2.5,-6 0,-3.6 3.14,-6.64 3.2,-3 6.8,-3 3.5,0 6.3,2.76 2.83,2.76 2.83,6.25 z m -3.4,0.16 q 0,-2.25 -1.75,-3.7 -1.7,-1.5 -4,-1.5 -0.1,0 -1.6,1.6 -1.44,1.55 -2.44,1.55 -0.6,0 -0.8,-0.3 -1.16,2.3 -1.16,3 0,2.25 2.13,3.4 1.6,0.9 3.6,0.9 2,0 3.76,-1.1 2.25,-1.4 2.25,-3.84 z"
style="fill:#333333"
id="path12" />
<path
d="m 112.8,46.8 q 0,2.8 -1.9,4.4 -1.8,1.5 -4.7,1.5 -0.7,0 -2.7,-0.4 -1.9,-0.4 -2.6,-0.4 -2.1,0 -2.1,2.64 0,0.85 0.23,2.6 0.2,1.75 0.2,2.6 0,1.9 -0.77,2.83 -1.44,0 -3,-0.85 -1.46,-9.5 -1.46,-12 0,-3.65 1.75,-8.1 2.37,-6.05 6.45,-6.05 3.7,0 7.3,4.1 3.3,3.84 3.3,7.14 z m -3.8,0.2 q -0.6,-2.2 -2.6,-4.4 -2.3,-2.5 -4.3,-2.5 -1.3,0 -2.33,2.2 -0.9,1.8 -0.9,3.26 0,0.47 0.38,1.24 0.43,0.8 0.85,0.8 1.1,0 3.2,0.3 2.1,0.3 3.2,0.3 0.3,0 1.3,-0.4 1,-0.47 1.3,-0.74 z"
style="fill:#333333"
id="path13" />
<path
d="m 133,40 q -2.1,4.1 -3.2,7 -0.1,0.3 -1.6,4.5 -0.4,1.36 -1,4.2 -0.5,2.83 -1,4.2 -1,2.83 -2.3,2.64 -1.4,-0.2 -1.6,-1.6 0,-0.2 0,-0.5 0,-0.16 0.3,-1.5 1,-5.04 1,-6.44 0,-0.54 -0.1,-0.74 -1.4,-2.44 -4.1,-7.4 -2.7,-4.97 -2.4,-7.7 1.5,-1.36 2.1,-1.36 0.4,0 1.1,0.6 0.6,0.6 0.7,1.1 0.8,6.2 4.9,11.1 1,-1.8 1.8,-4.04 0.5,-1.4 1.6,-4.15 1.9,-4.46 3.4,-4.46 0.2,0 0.4,0.1 0.9,0.3 1.3,2.8 z"
style="fill:#333333"
id="path14" />
<path
d="m 157.5,48 q 0,2.8 -1.9,4.4 -1.8,1.5 -4.7,1.5 -0.7,0 -2.7,-0.4 -1.9,-0.4 -2.6,-0.4 -2,0 -2,2.64 0,0.85 0.2,2.6 0.2,1.75 0.2,2.6 0,1.9 -0.7,2.83 -1.5,0 -3,-0.85 -1.5,-9.5 -1.5,-11.95 0,-3.65 1.8,-8.1 2.3,-6.05 6.4,-6.05 3.7,0 7.2,4.1 3.3,3.84 3.3,7.14 z m -3.8,0.2 q -0.6,-2.2 -2.6,-4.4 -2.3,-2.5 -4.3,-2.5 -1.3,0 -2.3,2.2 -0.9,1.8 -0.9,3.26 0,0.47 0.4,1.24 0.4,0.8 0.8,0.8 1.1,0 3.2,0.3 2.1,0.3 3.2,0.3 0.3,0 1.3,-0.4 1,-0.47 1.3,-0.74 z"
style="fill:#333333"
id="path15" />
<path
d="m 182,53.3 q 0,0.9 -0.6,1.5 -0.6,0.6 -1.4,0.6 -1.6,0 -3,-0.9 -1.4,-0.93 -2.1,-2.3 -0.7,-0.1 -1.5,0.85 -0.9,1.16 -1.1,1.24 -1.2,0.54 -3.9,0.54 -2.2,0 -3.9,-2.44 -1.5,-2.13 -1.5,-4 0,-3.4 3.4,-6.4 3.2,-2.9 6.7,-2.9 0.9,0 1.7,0.6 0.8,0.6 0.8,1.44 0,0.54 -0.4,1.1 2.4,0.9 2.4,2.83 0,0.35 -0.1,1.05 -0.1,0.7 -0.1,1.05 0,0.4 0.1,0.6 0.5,1.3 2.5,3.4 1.9,1.9 1.9,2.2 z m -8.1,-10.1 q -0.4,0 -1.1,-0.1 -0.8,-0.16 -1.1,-0.16 -1.3,0 -3.2,1.94 -1.9,1.94 -1.9,3.3 0,0.8 0.7,1.8 0.9,1.3 2.2,1.3 2.6,0 3.5,-2.9 0.5,-2.6 1,-5.16 z"
style="fill:#333333"
id="path16" />
<path
d="m 203.8,42.4 q -0.4,0.4 -1.5,0.4 -0.9,0 -2.5,-0.3 -1.7,-0.3 -2.5,-0.3 -4.7,0 -5.5,6.9 -0.3,3.1 -0.4,3.3 -0.4,1 -1.7,2.3 h -1.1 q -0.7,-1.2 -1.3,-4.1 -0.6,-2.76 -0.6,-4.27 0,-1.16 0.1,-1.5 0.2,-0.54 1,-0.54 0.3,0 0.6,0.3 0.4,0.3 0.4,0.3 1.9,-3.53 3.1,-4.6 1.8,-1.7 5.1,-1.7 1.4,0 3.6,0.9 2.8,1.16 3.3,2.8 z"
style="fill:#333333"
id="path17" />
<path
d="m 229.5,37.16 q 0.3,0.8 0.3,1.44 0,1.86 -2.4,1.86 -1,0 -3.5,-0.5 -2.5,-0.54 -3.4,-0.54 -1.3,0 -1.5,0.1 -0.4,0.2 -0.4,1.2 0,2.2 0.6,6.9 0.7,5.86 1.6,6.13 -0.4,0.35 -0.4,1.1 -1.2,0.7 -2.6,0.7 -1.4,0 -2,-3.9 -0.2,-1.36 -0.5,-7.76 -0.2,-4.6 -0.8,-5.5 -0.3,-0.47 -4.3,-0.35 -1,0 -1.6,0.1 -0.5,0 -0.3,0 -0.8,0 -1.2,-0.7 -0.5,-1.3 -0.5,-1.4 0,-1.44 4.1,-2 1.6,-0.16 4.7,-0.5 0,-0.85 -0.1,-2.56 0,-1.75 0,-2.6 0,-4.35 2.1,-4.35 0.5,0 1.1,0.6 0.6,0.6 0.6,1.1 v 7.9 q 1.1,1.2 5,1.7 3.9,0.5 5.3,1.86 z"
style="fill:#333333"
id="path18" />
<path
d="m 251.2,40.2 q -2,4.1 -3.2,7 -0.1,0.3 -1.5,4.5 -0.5,1.36 -1,4.2 -0.5,2.83 -1,4.2 -1,2.83 -2.4,2.64 -1.4,-0.2 -1.5,-1.6 -0.1,-0.2 -0.1,-0.5 0,-0.16 0.3,-1.5 1.1,-5.04 1.1,-6.44 0,-0.54 -0.1,-0.74 -1.4,-2.44 -4.1,-7.4 -2.7,-4.97 -2.4,-7.7 1.4,-1.36 2.1,-1.36 0.4,0 1,0.6 0.6,0.6 0.7,1.1 0.9,6.2 4.9,11.1 1,-1.8 1.9,-4.04 0.5,-1.4 1.6,-4.15 1.8,-4.46 3.4,-4.46 0.2,0 0.4,0.1 0.8,0.3 1.2,2.8 z"
style="fill:#333333"
id="path19" />
</g>
<g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="tagger">
<g
id="g1">
<path
id="path4"
style="fill:#333333"
d="m 111.4,83.335 -9.526,5.5 2.5,4.33 9.526,-5.5 z m -33.775,19.5 -9.526,5.5 2.5,4.33 9.526,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path5"
style="fill:#333333"
d="M 88.5,73 V 84 h 5 V 73 Z m 0,39 v 11 h 5 V 112 Z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path6"
style="fill:#333333"
d="m 68.1,87.665 9.526,5.5 2.5,-4.33 -9.526,-5.5 z m 33.775,19.5 9.527,5.5 2.5,-4.33 -9.527,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
</g>
<g
id="g2"
transform="rotate(30,150,318.19)">
<path
id="path7"
style="fill:#333333"
d="m 111.4,83.335 -9.526,5.5 2.5,4.33 9.526,-5.5 z m -33.775,19.5 -9.526,5.5 2.5,4.33 9.526,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path8"
style="fill:#333333"
d="M 88.5,73 V 84 h 5 V 73 Z m 0,39 v 11 h 5 V 112 Z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path9"
style="fill:#333333"
d="m 68.1,87.665 9.526,5.5 2.5,-4.33 -9.526,-5.5 z m 33.775,19.5 9.527,5.5 2.5,-4.33 -9.527,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

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

After

Width:  |  Height:  |  Size: 247 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#FFF"><path d="M7.05 40q-1.2 0-2.1-.925-.9-.925-.9-2.075V11q0-1.15.9-2.075Q5.85 8 7.05 8h14l3 3h17q1.15 0 2.075.925.925.925.925 2.075v23q0 1.15-.925 2.075Q42.2 40 41.05 40Zm0-29v26h34V14H22.8l-3-3H7.05Zm0 0v26Z"/></svg>

After

Width:  |  Height:  |  Size: 289 B

+1
View File
@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="#3186FF"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-0)"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-1)"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-2)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-0" x1="7" x2="11" y1="15.5" y2="12"><stop stop-color="#08B962"></stop><stop offset="1" stop-color="#08B962" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-1" x1="8" x2="11.5" y1="5.5" y2="11"><stop stop-color="#F94543"></stop><stop offset="1" stop-color="#F94543" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-2" x1="3.5" x2="17.5" y1="13.5" y2="12"><stop stop-color="#FABC12"></stop><stop offset=".46" stop-color="#FABC12" stop-opacity="0"></stop></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

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

After

Width:  |  Height:  |  Size: 693 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg width="721" height="721" viewBox="0 0 721 721" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1637_2935)">
<g clip-path="url(#clip1_1637_2935)">
<path d="M304.246 295.411V249.828C304.246 245.989 305.687 243.109 309.044 241.191L400.692 188.412C413.167 181.215 428.042 177.858 443.394 177.858C500.971 177.858 537.44 222.482 537.44 269.982C537.44 273.34 537.44 277.179 536.959 281.018L441.954 225.358C436.197 222 430.437 222 424.68 225.358L304.246 295.411ZM518.245 472.945V364.024C518.245 357.304 515.364 352.507 509.608 349.149L389.174 279.096L428.519 256.543C431.877 254.626 434.757 254.626 438.115 256.543L529.762 309.323C556.154 324.679 573.905 357.304 573.905 388.971C573.905 425.436 552.315 459.024 518.245 472.941V472.945ZM275.937 376.982L236.592 353.952C233.235 352.034 231.794 349.154 231.794 345.315V239.756C231.794 188.416 271.139 149.548 324.4 149.548C344.555 149.548 363.264 156.268 379.102 168.262L284.578 222.964C278.822 226.321 275.942 231.119 275.942 237.838V376.986L275.937 376.982ZM360.626 425.922L304.246 394.255V327.083L360.626 295.416L417.002 327.083V394.255L360.626 425.922ZM396.852 571.789C376.698 571.789 357.989 565.07 342.151 553.075L436.674 498.374C442.431 495.017 445.311 490.219 445.311 483.499V344.352L485.138 367.382C488.495 369.299 489.936 372.179 489.936 376.018V481.577C489.936 532.917 450.109 571.785 396.852 571.785V571.789ZM283.134 464.79L191.486 412.01C165.094 396.654 147.343 364.029 147.343 332.362C147.343 295.416 169.415 262.309 203.48 248.393V357.791C203.48 364.51 206.361 369.308 212.117 372.665L332.074 442.237L292.729 464.79C289.372 466.707 286.491 466.707 283.134 464.79ZM277.859 543.48C223.639 543.48 183.813 502.695 183.813 452.314C183.813 448.475 184.294 444.636 184.771 440.797L279.295 495.498C285.051 498.856 290.812 498.856 296.568 495.498L417.002 425.927V471.509C417.002 475.349 415.562 478.229 412.204 480.146L320.557 532.926C308.081 540.122 293.206 543.48 277.854 543.48H277.859ZM396.852 600.576C454.911 600.576 503.37 559.313 514.41 504.612C568.149 490.696 602.696 440.315 602.696 388.976C602.696 355.387 588.303 322.762 562.392 299.25C564.791 289.173 566.231 279.096 566.231 269.024C566.231 200.411 510.571 149.067 446.274 149.067C433.322 149.067 420.846 150.984 408.37 155.305C386.775 134.192 357.026 120.758 324.4 120.758C266.342 120.758 217.883 162.02 206.843 216.721C153.104 230.637 118.557 281.018 118.557 332.357C118.557 365.946 132.95 398.571 158.861 422.083C156.462 432.16 155.022 442.237 155.022 452.309C155.022 520.922 210.682 572.266 274.978 572.266C287.931 572.266 300.407 570.349 312.883 566.028C334.473 587.141 364.222 600.576 396.852 600.576Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_1637_2935">
<rect width="720" height="720" fill="white" transform="translate(0.606934 0.899902)"/>
</clipPath>
<clipPath id="clip1_1637_2935">
<rect width="484.139" height="479.818" fill="white" transform="translate(118.557 120.758)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

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

After

Width:  |  Height:  |  Size: 3.0 KiB

+137
View File
@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="800"
height="800"
viewBox="0 0 211.66666 211.66666"
version="1.1"
id="svg924"
inkscape:export-filename="/home/daniela/Documents/proxmox/Proxmox/Marketing/Logo/proxmox-logo/Screen/Full Lockup/stacked/proxmox-logo-color-stacked-bgblack.png"
inkscape:export-xdpi="360"
inkscape:export-ydpi="360"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
sodipodi:docname="proxmox-logo-stacked-inverted-color-bgtrans.svg">
<defs
id="defs918" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="0.38489655"
inkscape:cx="541.41545"
inkscape:cy="189.70435"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
inkscape:pagecheckerboard="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1720"
inkscape:window-height="1343"
inkscape:window-x="1720"
inkscape:window-y="27"
inkscape:window-maximized="0"
units="px" />
<metadata
id="metadata921">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-11.346916,-31.368461)">
<g
id="g209">
<g
transform="matrix(0.84666672,0,0,0.84666672,544.05161,-814.30036)"
id="g1288"
style="fill:#ffffff">
<g
id="g1286"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:25.8336px;line-height:125%;font-family:Helion;-inkscape-font-specification:Helion;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none"
transform="matrix(1.2435137,0,0,1.2435137,-791.06481,553.75862)">
<path
inkscape:connector-curvature="0"
id="path1272"
d="m 168.85142,500.9595 h -11.85747 c -0.46554,0.0129 -0.85842,0.18085 -1.17864,0.50374 -0.32023,0.32294 -0.48707,0.72335 -0.50052,1.20125 v 16.3783 c 1.27229,-0.0318 2.33145,-0.472 3.17749,-1.32073 0.84604,-0.84873 1.2852,-1.91543 1.3175,-3.2001 h 9.04164 c 1.28573,-0.0317 2.35673,-0.47199 3.21302,-1.32072 0.85624,-0.84873 1.30079,-1.91543 1.33364,-3.2001 v -4.49499 c -0.0328,-1.28573 -0.4774,-2.35673 -1.33364,-3.21301 -0.85629,-0.85625 -1.92729,-1.3008 -3.21302,-1.33364 z m -9.04164,9.60997 v -5.06332 h 7.93081 c 0.0463,-0.0231 0.23141,0.0232 0.55542,0.13885 0.32398,0.11573 0.50912,0.43972 0.55541,0.97198 v 2.81583 c 0.0231,0.0474 -0.0231,0.23681 -0.13885,0.56833 -0.11573,0.33154 -0.43972,0.52098 -0.97198,0.56833 z"
style="fill:#ffffff;fill-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path1274"
d="m 194.05931,508.89031 v -3.38416 c -0.0318,-1.28573 -0.47201,-2.35673 -1.32072,-3.21301 -0.84875,-0.85625 -1.91545,-1.3008 -3.2001,-1.33364 h -11.85747 c -0.47684,0.0129 -0.87295,0.18085 -1.18833,0.50374 -0.31538,0.32294 -0.47899,0.72335 -0.49083,1.20125 v 16.3783 c 1.27336,-0.0318 2.33683,-0.472 3.1904,-1.32073 0.85357,-0.84873 1.29705,-1.91543 1.33042,-3.2001 v -1.13666 h 5.14082 l 2.60916,3.71999 c 0.4187,0.60063 0.94398,1.07208 1.57583,1.41437 0.63182,0.34229 1.33793,0.51667 2.11833,0.52313 0.37618,-5.4e-4 0.74107,-0.0447 1.09468,-0.1324 0.35358,-0.0877 0.68618,-0.21582 0.99781,-0.38427 l -3.64249,-5.19249 c 1.05592,-0.22872 1.92133,-0.74647 2.59625,-1.55322 0.67487,-0.80675 1.02362,-1.77011 1.04624,-2.8901 z m -13.53663,0.5425 v -3.92666 h 7.87915 c 0.0474,-0.0231 0.23679,0.0232 0.56833,0.13885 0.33151,0.11573 0.52096,0.43972 0.56833,0.97198 v 1.705 c 0.0237,0.0463 -0.0237,0.23143 -0.14208,0.55541 -0.11842,0.324 -0.44995,0.50914 -0.99458,0.55542 z"
style="fill:#ffffff;fill-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path1276"
d="m 210.1751,500.9595 h -9.01581 c -1.28467,0.0328 -2.35137,0.47739 -3.2001,1.33364 -0.84873,0.85628 -1.28897,1.92728 -1.32072,3.21301 v 9.01581 c 0.0317,1.28467 0.47199,2.35137 1.32072,3.2001 0.84873,0.84873 1.91543,1.28897 3.2001,1.32073 h 9.01581 c 1.28465,-0.0318 2.35135,-0.472 3.2001,-1.32073 0.84871,-0.84873 1.28895,-1.91543 1.32072,-3.2001 v -9.01581 c -0.0318,-1.28573 -0.47201,-2.35673 -1.32072,-3.21301 -0.84875,-0.85625 -1.91545,-1.3008 -3.2001,-1.33364 z m 0,12.4258 c 0.0237,0.0474 -0.0237,0.23681 -0.14208,0.56833 -0.11842,0.33153 -0.44995,0.52098 -0.99458,0.56833 h -6.74249 c -0.0474,0.0237 -0.23681,-0.0237 -0.56833,-0.14208 -0.33153,-0.1184 -0.52098,-0.44993 -0.56833,-0.99458 v -6.76832 c -0.0237,-0.0463 0.0237,-0.23141 0.14208,-0.55541 0.1184,-0.32398 0.44993,-0.50912 0.99458,-0.55542 h 6.74249 c 0.0473,-0.0231 0.23679,0.0232 0.56833,0.13885 0.33151,0.11573 0.52096,0.43972 0.56833,0.97198 z"
style="fill:#ffffff;fill-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path1278"
d="m 237.4767,502.25116 c -0.39183,-0.39179 -0.84822,-0.69964 -1.36917,-0.92354 -0.52099,-0.22387 -1.08071,-0.33797 -1.67916,-0.34229 -0.6367,0.005 -1.22333,0.1308 -1.75989,0.37781 -0.53659,0.24705 -1.00052,0.58611 -1.39177,1.01719 l -3.90082,4.28832 -3.92666,-4.28832 c -0.4015,-0.44238 -0.86434,-0.78467 -1.38854,-1.02688 -0.5242,-0.24217 -1.1033,-0.36487 -1.73729,-0.36812 -0.59847,0.004 -1.15819,0.11842 -1.67916,0.34229 -0.52097,0.2239 -0.97736,0.53175 -1.36916,0.92354 l 7.05248,7.74998 -7.05248,7.74998 c 0.3918,0.40419 0.84819,0.71957 1.36916,0.94615 0.52097,0.22657 1.08069,0.34175 1.67916,0.34552 0.62538,-0.005 1.20878,-0.13079 1.75021,-0.37782 0.54142,-0.24703 1.00857,-0.58609 1.40145,-1.01718 l 3.90083,-4.28832 3.90082,4.28832 c 0.39125,0.43109 0.85518,0.77015 1.39177,1.01718 0.53656,0.24703 1.12319,0.37297 1.75989,0.37782 0.59845,-0.004 1.15817,-0.11895 1.67916,-0.34552 0.52096,-0.22658 0.97734,-0.54196 1.36917,-0.94615 l -7.05249,-7.74998 z"
style="fill:#ffffff;fill-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path1280"
d="m 260.98042,500.9595 h -2.84166 c -0.92947,0.0129 -1.75721,0.2648 -2.48322,0.75562 -0.72604,0.49085 -1.27607,1.14314 -1.6501,1.95687 l 0.0258,-0.0517 -2.66082,5.83832 -2.635,-5.83832 v 0.0517 c -0.36275,-0.81373 -0.90955,-1.46602 -1.64041,-1.95687 -0.73087,-0.49082 -1.56184,-0.74269 -2.49291,-0.75562 h -2.81583 c -0.48922,0.0129 -0.89286,0.18085 -1.21093,0.50374 -0.31808,0.32294 -0.48276,0.72335 -0.49406,1.20125 v 16.3783 c 1.27336,-0.0318 2.33683,-0.472 3.19041,-1.32073 0.85357,-0.84873 1.29704,-1.91543 1.33041,-3.2001 v -8.65414 c 0.002,-0.11785 0.0371,-0.2115 0.10656,-0.28094 0.0694,-0.0694 0.16307,-0.10493 0.28094,-0.10656 0.0673,0.002 0.13293,0.0237 0.19698,0.0646 0.064,0.0409 0.11032,0.0883 0.13885,0.14208 l 5.01166,11.05664 c 0.0947,0.19752 0.23464,0.3579 0.41979,0.48115 0.18512,0.12325 0.38964,0.18675 0.61354,0.19052 0.22226,-0.003 0.42354,-0.0619 0.60385,-0.1776 0.18028,-0.11571 0.32344,-0.27179 0.42948,-0.46823 L 257.4154,505.687 c 0.0393,-0.0538 0.0899,-0.10116 0.15177,-0.14208 0.0619,-0.0409 0.13184,-0.0624 0.2099,-0.0646 0.10547,0.002 0.19158,0.0371 0.25833,0.10656 0.0667,0.0694 0.10116,0.16309 0.10333,0.28094 v 8.65414 c 0.0333,1.28467 0.47682,2.35137 1.33042,3.2001 0.85355,0.84873 1.91702,1.28897 3.19041,1.32073 v -16.3783 c -0.0119,-0.4779 -0.17548,-0.87831 -0.49084,-1.20125 -0.3154,-0.32289 -0.71151,-0.49081 -1.18833,-0.50374 z"
style="fill:#ffffff;fill-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path1282"
d="m 278.79561,500.9595 h -9.01581 c -1.28467,0.0328 -2.35137,0.47739 -3.20009,1.33364 -0.84874,0.85628 -1.28898,1.92728 -1.32073,3.21301 v 9.01581 c 0.0317,1.28467 0.47199,2.35137 1.32073,3.2001 0.84872,0.84873 1.91542,1.28897 3.20009,1.32073 h 9.01581 c 1.28466,-0.0318 2.35135,-0.472 3.2001,-1.32073 0.84871,-0.84873 1.28896,-1.91543 1.32073,-3.2001 v -9.01581 c -0.0318,-1.28573 -0.47202,-2.35673 -1.32073,-3.21301 -0.84875,-0.85625 -1.91544,-1.3008 -3.2001,-1.33364 z m 0,12.4258 c 0.0237,0.0474 -0.0237,0.23681 -0.14208,0.56833 -0.11842,0.33153 -0.44994,0.52098 -0.99458,0.56833 h -6.74248 c -0.0474,0.0237 -0.23681,-0.0237 -0.56834,-0.14208 -0.33153,-0.1184 -0.52097,-0.44993 -0.56833,-0.99458 v -6.76832 c -0.0237,-0.0463 0.0237,-0.23141 0.14209,-0.55541 0.11839,-0.32398 0.44992,-0.50912 0.99458,-0.55542 h 6.74248 c 0.0473,-0.0231 0.23679,0.0232 0.56833,0.13885 0.33152,0.11573 0.52096,0.43972 0.56833,0.97198 z"
style="fill:#ffffff;fill-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path1284"
d="m 306.0972,502.25116 c -0.39182,-0.39179 -0.84821,-0.69964 -1.36916,-0.92354 -0.52099,-0.22387 -1.08071,-0.33797 -1.67916,-0.34229 -0.6367,0.005 -1.22333,0.1308 -1.75989,0.37781 -0.5366,0.24705 -1.00052,0.58611 -1.39177,1.01719 l -3.90083,4.28832 -3.92665,-4.28832 c -0.4015,-0.44238 -0.86435,-0.78467 -1.38854,-1.02688 -0.52421,-0.24217 -1.1033,-0.36487 -1.73729,-0.36812 -0.59847,0.004 -1.15819,0.11842 -1.67916,0.34229 -0.52097,0.2239 -0.97736,0.53175 -1.36917,0.92354 l 7.05249,7.74998 -7.05249,7.74998 c 0.39181,0.40419 0.8482,0.71957 1.36917,0.94615 0.52097,0.22657 1.08069,0.34175 1.67916,0.34552 0.62538,-0.005 1.20878,-0.13079 1.7502,-0.37782 0.54142,-0.24703 1.00857,-0.58609 1.40146,-1.01718 l 3.90082,-4.28832 3.90083,4.28832 c 0.39125,0.43109 0.85517,0.77015 1.39177,1.01718 0.53656,0.24703 1.12319,0.37297 1.75989,0.37782 0.59845,-0.004 1.15817,-0.11895 1.67916,-0.34552 0.52095,-0.22658 0.97734,-0.54196 1.36916,-0.94615 l -7.05248,-7.74998 z"
style="fill:#ffffff;fill-opacity:1" />
</g>
</g>
<path
inkscape:connector-curvature="0"
id="path1290"
style="fill:#e57000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.730891"
d="m 141.89758,116.88641 25.41237,27.92599 c -2.7927,2.88547 -6.70224,4.65495 -10.98527,4.65495 -4.56076,0 -8.56315,-1.95514 -11.35592,-5.02707 l -14.05575,-15.45172 -10.9846,-12.10215 10.9846,-12.00854 14.05575,-15.452532 c 2.79277,-3.071175 6.79516,-5.027074 11.35592,-5.027074 4.28303,0 8.19257,1.768759 10.98527,4.561525 z"
sodipodi:nodetypes="ccscccccscc" />
<path
inkscape:connector-curvature="0"
id="path1292"
style="fill:#e57000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.730891"
d="m 92.334245,116.8861 -25.41238,27.92603 c 2.7927,2.88547 6.702237,4.65495 10.985287,4.65495 4.560744,0 8.563138,-1.95514 11.355905,-5.02707 L 103.3188,128.98825 114.30338,116.8861 103.3188,104.87756 89.263057,89.425028 c -2.792767,-3.071215 -6.795161,-5.027074 -11.355905,-5.027074 -4.28305,0 -8.192587,1.768759 -10.985287,4.561526 z"
sodipodi:nodetypes="ccscccccscc" />
<path
inkscape:connector-curvature="0"
id="path1294"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.66712"
d="m 127.12922,130.73289 -10.02585,-11.04587 -10.0263,11.04587 -23.195348,25.48982 c 2.548811,2.54902 6.118169,4.16327 10.025148,4.16327 4.16359,0 7.64778,-1.69975 10.28111,-4.58817 l 12.91539,-14.10405 12.82882,14.10405 c 2.5493,2.80293 6.20218,4.58817 10.36513,4.58817 3.9091,0 7.47768,-1.61425 10.02652,-4.16327 z"
sodipodi:nodetypes="ccccscccscc" />
<path
inkscape:connector-curvature="0"
id="path1296"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.66712"
d="M 127.1286,103.03813 117.10275,114.084 107.07645,103.03813 83.881116,77.548321 c 2.548838,-2.548932 6.118156,-4.163226 10.025162,-4.163226 4.163603,0 7.647762,1.699732 10.281082,4.588143 l 12.91539,14.104094 12.82882,-14.104094 c 2.54934,-2.802999 6.20221,-4.588143 10.36513,-4.588143 3.90911,0 7.47772,1.614294 10.02653,4.163226 z"
sodipodi:nodetypes="ccccscccscc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

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

After

Width:  |  Height:  |  Size: 1.3 KiB

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

After

Width:  |  Height:  |  Size: 1.3 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="400" height="400" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.41508 17.2983L7.88484 12.7653L9.51146 18.9412L11.8745 18.2949L9.52018 9.32758L0.69527 6.93747L0.066864 9.35199L6.13926 11.0015L1.68806 15.5279L3.41508 17.2983Z" fill="#F34E3F"/>
<path d="M16.3044 12.0436L18.6675 11.3973L16.3132 2.43003L7.48824 0.0399246L6.85984 2.45444L14.312 4.47881L16.3044 12.0436Z" fill="#F34E3F"/>
<path d="M12.9126 15.4902L15.2756 14.8439L12.9213 5.87659L4.09639 3.48648L3.46799 5.901L10.9201 7.92537L12.9126 15.4902Z" fill="#F34E3F"/>
</svg>

After

Width:  |  Height:  |  Size: 576 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" height="2500" width="2500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 160"><g clip-rule="evenodd" fill-rule="evenodd"><path d="M0 116h160v28.996c0 8.287-6.722 15.004-14.998 15.004H14.998C6.716 160 0 153.293 0 144.996zm0 0h160v30H0z" fill="#1bb91f"/><path d="M83 70V0h-6v146h6V76h77v-6zM0 15.007C0 6.719 6.722 0 14.998 0h130.004C153.285 0 160 6.725 160 15.007V146H0z" fill="#3c3c3c"/></g></svg>

After

Width:  |  Height:  |  Size: 419 B

+22
View File
@@ -0,0 +1,22 @@
# Ignore symlinks to avoid Prettier errors
CLAUDE.md
.github/copilot-instructions.md
# Ignore node_modules and dependencies
node_modules/
# Ignore Terraform files (formatted by terraform fmt)
*.tf
*.hcl
*.tfvars
# Ignore generated and temporary files
.terraform/
*.tfstate
*.tfstate.backup
*.tfstate.lock.info
# Ignore other files that shouldn't be formatted
bun.lock
go.sum
go.mod
+168
View File
@@ -0,0 +1,168 @@
# AGENTS.md
This file provides guidance to AI coding assistants when working with code in this repository.
## Project Overview
The Coder Registry is a community-driven repository for Terraform modules and templates that extend Coder workspaces. It's organized with:
- **Modules**: Individual components and tools (IDEs, auth integrations, dev tools)
- **Templates**: Complete workspace configurations for different platforms
- **Namespaces**: Each contributor has their own namespace under `/registry/[namespace]/`
## Common Development Commands
### Formatting
```bash
bun run fmt # Format all code (Prettier + Terraform)
bun run fmt:ci # Check formatting (CI mode)
```
### Testing
```bash
# Test all modules with .tftest.hcl files
bun run test
# Test specific module (from module directory)
terraform init -upgrade
terraform test -verbose
# Validate Terraform syntax
./scripts/terraform_validate.sh
```
### Module Creation
```bash
# Generate new module scaffold
./scripts/new_module.sh namespace/module-name
```
### TypeScript Testing & Setup
The repository uses Bun for TypeScript testing with utilities:
- `test/test.ts` - Testing utilities for container management and Terraform operations
- `setup.ts` - Test cleanup (removes .tfstate files and test containers)
- Container-based testing with Docker for module validation
## Architecture & Organization
### Directory Structure
```
registry/[namespace]/
├── README.md # Contributor info with frontmatter
├── .images/ # Namespace avatar (avatar.png/svg)
├── modules/ # Individual components
│ └── [module]/ # Each module has main.tf, README.md, tests
└── templates/ # Complete workspace configs
└── [template]/ # Each template has main.tf, README.md
```
### Key Components
**Module Structure**: Each module contains:
- `main.tf` - Terraform implementation
- `README.md` - Documentation with YAML frontmatter
- `.tftest.hcl` - Terraform test files (required)
- `run.sh` - Optional startup script
**Template Structure**: Each template contains:
- `main.tf` - Complete Coder template configuration
- `README.md` - Documentation with YAML frontmatter
- Additional configs, scripts as needed
### README Frontmatter Requirements
All modules/templates require YAML frontmatter:
```yaml
---
display_name: "Module Name"
description: "Brief description"
icon: "../../../../.icons/tool.svg"
verified: false
tags: ["tag1", "tag2"]
---
```
## Testing Requirements
### Module Testing
- Every module MUST have `.tftest.hcl` test files
- Optional `main.test.ts` files for container-based testing or complex business logic validation
- Tests use Docker containers with `--network=host` flag
- Linux required for testing (Docker Desktop on macOS/Windows won't work)
- Use Colima or OrbStack on macOS instead of Docker Desktop
### Test Utilities
The `test/test.ts` file provides:
- `runTerraformApply()` - Execute Terraform with variables
- `executeScriptInContainer()` - Run coder_script resources in containers
- `testRequiredVariables()` - Validate required variables
- Container management functions
## Validation & Quality
### Automated Validation
The Go validation tool (`cmd/readmevalidation/`) checks:
- Repository structure integrity
- Contributor README files
- Module and template documentation
- Frontmatter format compliance
### Versioning
Use semantic versioning for modules:
- **Patch** (1.2.3 → 1.2.4): Bug fixes
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
- **Major** (1.2.3 → 2.0.0): Breaking changes
## Dependencies & Tools
### Required Tools
- **Terraform** - Module development and testing
- **Docker** - Container-based testing
- **Bun** - JavaScript runtime for formatting/scripts
- **Go 1.23+** - Validation tooling
### Development Dependencies
- Prettier with Terraform and shell plugins
- TypeScript for test utilities
- Various npm packages for documentation processing
## Workflow Notes
### Contributing Process
1. Create namespace (first-time contributors)
2. Generate module/template files using scripts
3. Implement functionality and tests
4. Run formatting and validation
5. Submit PR with appropriate template
### Testing Workflow
- All modules must pass `terraform test`
- Use `bun run test` for comprehensive testing
- Format code with `bun run fmt` before submission
- Manual testing recommended for templates
### Namespace Management
- Each contributor gets unique namespace
- Namespace avatar required (avatar.png/svg in .images/)
- Namespace README with contributor info and frontmatter
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+29 -13
View File
@@ -24,7 +24,7 @@ The Coder Registry is a collection of Terraform modules and templates for Coder
### Install Dependencies
Install Bun:
Install Bun (for formatting and scripts):
```bash
curl -fsSL https://bun.sh/install | bash
@@ -89,7 +89,7 @@ Create `registry/[your-username]/README.md`:
---
display_name: "Your Name"
bio: "Brief description of who you are and what you do"
avatar_url: "./.images/avatar.png"
avatar: "./.images/avatar.png"
github: "your-username"
linkedin: "https://www.linkedin.com/in/your-username" # Optional
website: "https://yourwebsite.com" # Optional
@@ -102,7 +102,7 @@ status: "community"
Brief description of who you are and what you do.
```
> **Note**: The `avatar_url` must point to `./.images/avatar.png` or `./.images/avatar.svg`.
> **Note**: The `avatar` must point to `./.images/avatar.png` or `./.images/avatar.svg`.
### 2. Generate Module Files
@@ -124,19 +124,28 @@ This script generates:
- Accurate description and usage examples
- Correct icon path (usually `../../../../.icons/your-icon.svg`)
- Proper tags that describe your module
3. **Create `main.test.ts`** to test your module
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
bun test -t 'module-name'
cd registry/[namespace]/modules/[module-name]
# Required: Test Terraform functionality
terraform init -upgrade
terraform test -verbose
# Optional: Test TypeScript files if you have main.test.ts
bun test main.test.ts
# Format code
bun fmt
bun run fmt
# Commit and create PR
# Commit and create PR (do not push to main directly)
git add .
git commit -m "Add [module-name] module"
git push origin your-branch
@@ -335,11 +344,12 @@ coder templates push test-[template-name] -d .
### 2. Test Your Changes
```bash
# Test a specific module
bun test -t 'module-name'
# Test a specific module (from the module directory)
terraform init -upgrade
terraform test -verbose
# Test all modules
bun test
# Optional: If you have TypeScript tests
bun test main.test.ts
```
### 3. Maintain Backward Compatibility
@@ -388,7 +398,9 @@ Example: `https://github.com/coder/registry/compare/main...your-branch?template=
### Every Module Must Have
- `main.tf` - Terraform code
- `main.test.ts` - Working tests
- **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
@@ -488,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** `bun fmt` 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! 🚀
+56 -8
View File
@@ -18,11 +18,12 @@ sudo apt install golang-go
Check that PRs have:
- [ ] All required files (`main.tf`, `main.test.ts`, `README.md`)
- [ ] All required files (`main.tf`, `README.md`, at least one `.tftest.hcl`)
- [ ] Proper frontmatter in README
- [ ] Working tests (`bun test`)
- [ ] 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
@@ -42,12 +44,58 @@ go build ./cmd/readmevalidation && ./readmevalidation
## Making a Release
### Create Release Tags
### Automated Tag and Release Process
After merging a PR:
After merging a PR, use the automated script to create and push release tags:
1. Get the new version from the PR (shown as `old → new`)
2. Checkout the merge commit and create the tag:
**Prerequisites:**
- Ensure all module versions are updated in their respective README files (the script uses this as the source of truth)
- Make sure you have the necessary permissions to push tags to the repository
**Steps:**
1. **Checkout the merge commit:**
```bash
git checkout MERGE_COMMIT_ID
```
2. **Run the tag release script:**
```bash
./scripts/tag_release.sh
```
3. **Review and confirm:**
- The script will automatically scan all modules in the registry
- It will detect which modules need version bumps by comparing README versions to existing tags
- A summary will be displayed showing which modules need tagging
- Confirm the list is correct when prompted
4. **Automatic tagging:**
- After confirmation, the script will automatically create all necessary release tags
- Tags will be pushed to the remote repository
- The script operates on the current checked-out commit
**Example output:**
```text
🔍 Scanning all modules for missing release tags...
📦 coder/code-server: v4.1.2 (needs tag)
✅ coder/dotfiles: v1.0.5 (already tagged)
## Tags to be created:
- `release/coder/code-server/v4.1.2`
❓ Do you want to proceed with creating and pushing these release tags?
Continue? [y/N]: y
```
### Manual Process (Fallback)
If the automated script fails, you can manually tag and release modules:
```bash
# Checkout the merge commit
@@ -81,7 +129,7 @@ tags: ["tag1", "tag2"]
```yaml
display_name: "Your Name"
bio: "Brief description of who you are and what you do"
avatar_url: "./.images/avatar.png"
avatar: "./.images/avatar.png"
github: "username"
linkedin: "https://www.linkedin.com/in/username" # Optional
website: "https://yourwebsite.com" # Optional
+4
View File
@@ -48,3 +48,7 @@ Simply include that snippet inside your Coder template, defining any data depend
## Contributing
We are always accepting new contributions. [Please see our contributing guide for more information.](./CONTRIBUTING.md)
## For Maintainers
Guidelines for maintainers reviewing PRs and managing releases. [See the maintainer guide for more information.](./MAINTAINER.md)
+6 -6
View File
@@ -4,11 +4,11 @@
"": {
"name": "registry",
"devDependencies": {
"@types/bun": "^1.2.18",
"bun-types": "^1.2.18",
"@types/bun": "^1.2.21",
"bun-types": "^1.2.21",
"dedent": "^1.6.0",
"gray-matter": "^4.0.3",
"marked": "^16.0.0",
"marked": "^16.2.0",
"prettier": "^3.6.2",
"prettier-plugin-sh": "^0.18.0",
"prettier-plugin-terraform-formatter": "^1.2.1",
@@ -21,7 +21,7 @@
"packages": {
"@reteps/dockerfmt": ["@reteps/dockerfmt@0.3.6", "", {}, "sha512-Tb5wIMvBf/nLejTQ61krK644/CEMB/cpiaIFXqGApfGqO3GwcR3qnI0DbmkFVCl2OyEp8LnLX3EkucoL0+tbFg=="],
"@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
"@types/node": ["@types/node@24.0.14", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw=="],
@@ -29,7 +29,7 @@
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
@@ -47,7 +47,7 @@
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"marked": ["marked@16.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-MUKMXDjsD/eptB7GPzxo4xcnLS6oo7/RHimUMHEDRhUooPwmN9BEpMl7AEOJv3bmso169wHI2wUF9VQgL7zfmA=="],
"marked": ["marked@16.2.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
+146
View File
@@ -0,0 +1,146 @@
package main
import (
"bufio"
"context"
"strings"
"golang.org/x/xerrors"
)
func validateCoderModuleReadmeBody(body string) []error {
var errs []error
trimmed := strings.TrimSpace(body)
if baseErrs := validateReadmeBody(trimmed); len(baseErrs) != 0 {
errs = append(errs, baseErrs...)
}
foundParagraph := false
terraformCodeBlockCount := 0
foundTerraformVersionRef := false
lineNum := 0
isInsideCodeBlock := false
isInsideTerraform := false
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
for lineScanner.Scan() {
lineNum++
nextLine := lineScanner.Text()
// Code assumes that invalid headers would've already been handled by the base validation function, so we don't
// need to check deeper if the first line isn't an h1.
if lineNum == 1 {
if !strings.HasPrefix(nextLine, "# ") {
break
}
continue
}
if strings.HasPrefix(nextLine, "```") {
isInsideCodeBlock = !isInsideCodeBlock
isInsideTerraform = isInsideCodeBlock && strings.HasPrefix(nextLine, "```tf")
if isInsideTerraform {
terraformCodeBlockCount++
}
if strings.HasPrefix(nextLine, "```hcl") {
errs = append(errs, xerrors.New("all hcl code blocks must be converted to tf"))
}
continue
}
if isInsideCodeBlock {
if isInsideTerraform {
foundTerraformVersionRef = foundTerraformVersionRef || terraformVersionRe.MatchString(nextLine)
}
continue
}
// Code assumes that we can treat this case as the end of the "h1 section" and don't need to process any further lines.
if lineNum > 1 && strings.HasPrefix(nextLine, "#") {
break
}
// Code assumes that if we've reached this point, the only other options are:
// (1) empty spaces, (2) paragraphs, (3) HTML, and (4) asset references made via [] syntax.
trimmedLine := strings.TrimSpace(nextLine)
isParagraph := trimmedLine != "" && !strings.HasPrefix(trimmedLine, "![") && !strings.HasPrefix(trimmedLine, "<")
foundParagraph = foundParagraph || isParagraph
}
if terraformCodeBlockCount == 0 {
errs = append(errs, xerrors.New("did not find Terraform code block within h1 section"))
} else {
if terraformCodeBlockCount > 1 {
errs = append(errs, xerrors.New("cannot have more than one Terraform code block in h1 section"))
}
if !foundTerraformVersionRef {
errs = append(errs, xerrors.New("did not find Terraform code block that specifies 'version' field"))
}
}
if !foundParagraph {
errs = append(errs, xerrors.New("did not find paragraph within h1 section"))
}
if isInsideCodeBlock {
errs = append(errs, xerrors.New("code blocks inside h1 section do not all terminate before end of file"))
}
return errs
}
func validateCoderModuleReadme(rm coderResourceReadme) []error {
var errs []error
for _, err := range validateCoderModuleReadmeBody(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateResourceGfmAlerts(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if fmErrs := validateCoderResourceFrontmatter("modules", rm.filePath, rm.frontmatter); len(fmErrs) != 0 {
errs = append(errs, fmErrs...)
}
return errs
}
func validateAllCoderModuleReadmes(resources []coderResourceReadme) error {
var yamlValidationErrors []error
for _, readme := range resources {
errs := validateCoderModuleReadme(readme)
if len(errs) > 0 {
yamlValidationErrors = append(yamlValidationErrors, errs...)
}
}
if len(yamlValidationErrors) != 0 {
return validationPhaseError{
phase: validationPhaseReadme,
errors: yamlValidationErrors,
}
}
return nil
}
func validateAllCoderModules() error {
const resourceType = "modules"
allReadmeFiles, err := aggregateCoderResourceReadmeFiles(resourceType)
if err != nil {
return err
}
logger.Info(context.Background(), "processing template README files", "resource_type", resourceType, "num_files", len(allReadmeFiles))
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles)
if err != nil {
return err
}
err = validateAllCoderModuleReadmes(resources)
if err != nil {
return err
}
logger.Info(context.Background(), "processed README files as valid Coder resources", "resource_type", resourceType, "num_files", len(resources))
if err := validateCoderResourceRelativeURLs(resources); err != nil {
return err
}
logger.Info(context.Background(), "all relative URLs for READMEs are valid", "resource_type", resourceType)
return nil
}
@@ -14,7 +14,7 @@ func TestValidateCoderResourceReadmeBody(t *testing.T) {
t.Run("Parses a valid README body with zero issues", func(t *testing.T) {
t.Parallel()
errs := validateCoderResourceReadmeBody(testBody)
errs := validateCoderModuleReadmeBody(testBody)
for _, e := range errs {
t.Error(e)
}
+145 -141
View File
@@ -2,7 +2,6 @@ package main
import (
"bufio"
"context"
"errors"
"net/url"
"os"
@@ -17,19 +16,35 @@ import (
var (
supportedResourceTypes = []string{"modules", "templates"}
operatingSystems = []string{"windows", "macos", "linux"}
gfmAlertTypes = []string{"NOTE", "IMPORTANT", "CAUTION", "WARNING", "TIP"}
// TODO: This is a holdover from the validation logic used by the Coder Modules repo. It gives us some assurance, but
// realistically, we probably want to parse any Terraform code snippets, and make some deeper guarantees about how it's
// structured. Just validating whether it *can* be parsed as Terraform would be a big improvement.
terraformVersionRe = regexp.MustCompile(`^\s*\bversion\s+=`)
// Matches the format "> [!INFO]". Deliberately using a broad pattern to catch formatting issues that can mess up
// the renderer for the Registry website
gfmAlertRegex = regexp.MustCompile(`^>(\s*)\[!(\w+)\](\s*)(.*)`)
)
type coderResourceFrontmatter struct {
Description string `yaml:"description"`
IconURL string `yaml:"icon"`
DisplayName *string `yaml:"display_name"`
Verified *bool `yaml:"verified"`
Tags []string `yaml:"tags"`
Description string `yaml:"description"`
IconURL string `yaml:"icon"`
DisplayName *string `yaml:"display_name"`
Verified *bool `yaml:"verified"`
Tags []string `yaml:"tags"`
OperatingSystems []string `yaml:"supported_os"`
}
// A slice version of the struct tags from coderResourceFrontmatter. Might be worth using reflection to generate this
// list at runtime in the future, but this should be okay for now
var supportedCoderResourceStructKeys = []string{
"description", "icon", "display_name", "verified", "tags", "supported_os",
// TODO: This is an old, officially deprecated key from the archived coder/modules repo. We can remove this once we
// make sure that the Registry Server is no longer checking this field.
"maintainer_github",
}
// coderResourceReadme represents a README describing a Terraform resource used
@@ -42,6 +57,17 @@ type coderResourceReadme struct {
frontmatter coderResourceFrontmatter
}
func validateSupportedOperatingSystems(systems []string) []error {
var errs []error
for _, s := range systems {
if slices.Contains(operatingSystems, s) {
continue
}
errs = append(errs, xerrors.Errorf("detected unknown operating system %q", s))
}
return errs
}
func validateCoderResourceDisplayName(displayName *string) error {
if displayName != nil && *displayName == "" {
return xerrors.New("if defined, display_name must not be empty string")
@@ -67,7 +93,7 @@ func validateCoderResourceIconURL(iconURL string) []error {
return []error{xerrors.New("icon URL cannot be empty")}
}
errs := []error{}
var errs []error
// If the URL does not have a relative path.
if !strings.HasPrefix(iconURL, ".") && !strings.HasPrefix(iconURL, "/") {
@@ -98,7 +124,7 @@ func validateCoderResourceTags(tags []string) error {
// All of these tags are used for the module/template filter controls in the Registry site. Need to make sure they
// can all be placed in the browser URL without issue.
invalidTags := []string{}
var invalidTags []string
for _, t := range tags {
if t != url.QueryEscape(t) {
invalidTags = append(invalidTags, t)
@@ -111,119 +137,50 @@ func validateCoderResourceTags(tags []string) error {
return nil
}
func validateCoderResourceReadmeBody(body string) []error {
func validateCoderResourceFrontmatter(resourceType string, filePath string, fm coderResourceFrontmatter) []error {
if !slices.Contains(supportedResourceTypes, resourceType) {
return []error{xerrors.Errorf("cannot process unknown resource type %q", resourceType)}
}
var errs []error
trimmed := strings.TrimSpace(body)
// TODO: this may cause unexpected behavior since the errors slice may have a 0 length. Add a test.
errs = append(errs, validateReadmeBody(trimmed)...)
foundParagraph := false
terraformCodeBlockCount := 0
foundTerraformVersionRef := false
lineNum := 0
isInsideCodeBlock := false
isInsideTerraform := false
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
for lineScanner.Scan() {
lineNum++
nextLine := lineScanner.Text()
// Code assumes that invalid headers would've already been handled by the base validation function, so we don't
// need to check deeper if the first line isn't an h1.
if lineNum == 1 {
if !strings.HasPrefix(nextLine, "# ") {
break
}
continue
}
if strings.HasPrefix(nextLine, "```") {
isInsideCodeBlock = !isInsideCodeBlock
isInsideTerraform = isInsideCodeBlock && strings.HasPrefix(nextLine, "```tf")
if isInsideTerraform {
terraformCodeBlockCount++
}
if strings.HasPrefix(nextLine, "```hcl") {
errs = append(errs, xerrors.New("all .hcl language references must be converted to .tf"))
}
continue
}
if isInsideCodeBlock {
if isInsideTerraform {
foundTerraformVersionRef = foundTerraformVersionRef || terraformVersionRe.MatchString(nextLine)
}
continue
}
// Code assumes that we can treat this case as the end of the "h1 section" and don't need to process any further lines.
if lineNum > 1 && strings.HasPrefix(nextLine, "#") {
break
}
// Code assumes that if we've reached this point, the only other options are:
// (1) empty spaces, (2) paragraphs, (3) HTML, and (4) asset references made via [] syntax.
trimmedLine := strings.TrimSpace(nextLine)
isParagraph := trimmedLine != "" && !strings.HasPrefix(trimmedLine, "![") && !strings.HasPrefix(trimmedLine, "<")
foundParagraph = foundParagraph || isParagraph
if err := validateCoderResourceDisplayName(fm.DisplayName); err != nil {
errs = append(errs, addFilePathToError(filePath, err))
}
if err := validateCoderResourceDescription(fm.Description); err != nil {
errs = append(errs, addFilePathToError(filePath, err))
}
if err := validateCoderResourceTags(fm.Tags); err != nil {
errs = append(errs, addFilePathToError(filePath, err))
}
if terraformCodeBlockCount == 0 {
errs = append(errs, xerrors.New("did not find Terraform code block within h1 section"))
} else {
if terraformCodeBlockCount > 1 {
errs = append(errs, xerrors.New("cannot have more than one Terraform code block in h1 section"))
}
if !foundTerraformVersionRef {
errs = append(errs, xerrors.New("did not find Terraform code block that specifies 'version' field"))
}
for _, err := range validateCoderResourceIconURL(fm.IconURL) {
errs = append(errs, addFilePathToError(filePath, err))
}
if !foundParagraph {
errs = append(errs, xerrors.New("did not find paragraph within h1 section"))
}
if isInsideCodeBlock {
errs = append(errs, xerrors.New("code blocks inside h1 section do not all terminate before end of file"))
for _, err := range validateSupportedOperatingSystems(fm.OperatingSystems) {
errs = append(errs, addFilePathToError(filePath, err))
}
return errs
}
func validateCoderResourceReadme(rm coderResourceReadme) []error {
var errs []error
for _, err := range validateCoderResourceReadmeBody(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if err := validateCoderResourceDisplayName(rm.frontmatter.DisplayName); err != nil {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if err := validateCoderResourceDescription(rm.frontmatter.Description); err != nil {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if err := validateCoderResourceTags(rm.frontmatter.Tags); err != nil {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateCoderResourceIconURL(rm.frontmatter.IconURL) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
return errs
}
func parseCoderResourceReadme(resourceType string, rm readme) (coderResourceReadme, error) {
func parseCoderResourceReadme(resourceType string, rm readme) (coderResourceReadme, []error) {
fm, body, err := separateFrontmatter(rm.rawText)
if err != nil {
return coderResourceReadme{}, xerrors.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
return coderResourceReadme{}, []error{xerrors.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)}
}
keyErrs := validateFrontmatterYamlKeys(fm, supportedCoderResourceStructKeys)
if len(keyErrs) != 0 {
var remapped []error
for _, e := range keyErrs {
remapped = append(remapped, addFilePathToError(rm.filePath, e))
}
return coderResourceReadme{}, remapped
}
yml := coderResourceFrontmatter{}
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
return coderResourceReadme{}, xerrors.Errorf("%q: failed to parse: %v", rm.filePath, err)
return coderResourceReadme{}, []error{xerrors.Errorf("%q: failed to parse: %v", rm.filePath, err)}
}
return coderResourceReadme{
@@ -234,13 +191,17 @@ func parseCoderResourceReadme(resourceType string, rm readme) (coderResourceRead
}, nil
}
func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[string]coderResourceReadme, error) {
func parseCoderResourceReadmeFiles(resourceType string, rms []readme) ([]coderResourceReadme, error) {
if !slices.Contains(supportedResourceTypes, resourceType) {
return nil, xerrors.Errorf("cannot process unknown resource type %q", resourceType)
}
resources := map[string]coderResourceReadme{}
var yamlParsingErrs []error
for _, rm := range rms {
p, err := parseCoderResourceReadme(resourceType, rm)
if err != nil {
yamlParsingErrs = append(yamlParsingErrs, err)
p, errs := parseCoderResourceReadme(resourceType, rm)
if len(errs) != 0 {
yamlParsingErrs = append(yamlParsingErrs, errs...)
continue
}
@@ -253,30 +214,27 @@ func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[strin
}
}
yamlValidationErrors := []error{}
for _, readme := range resources {
errs := validateCoderResourceReadme(readme)
if len(errs) > 0 {
yamlValidationErrors = append(yamlValidationErrors, errs...)
}
var serialized []coderResourceReadme
for _, r := range resources {
serialized = append(serialized, r)
}
if len(yamlValidationErrors) != 0 {
return nil, validationPhaseError{
phase: validationPhaseReadme,
errors: yamlValidationErrors,
}
}
return resources, nil
slices.SortFunc(serialized, func(r1 coderResourceReadme, r2 coderResourceReadme) int {
return strings.Compare(r1.filePath, r2.filePath)
})
return serialized, nil
}
// Todo: Need to beef up this function by grabbing each image/video URL from
// the body's AST.
func validateCoderResourceRelativeURLs(_ map[string]coderResourceReadme) error {
func validateCoderResourceRelativeURLs(_ []coderResourceReadme) error {
return nil
}
func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
if !slices.Contains(supportedResourceTypes, resourceType) {
return nil, xerrors.Errorf("cannot process unknown resource type %q", resourceType)
}
registryFiles, err := os.ReadDir(rootRegistryPath)
if err != nil {
return nil, err
@@ -326,26 +284,72 @@ func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
return allReadmeFiles, nil
}
func validateAllCoderResourceFilesOfType(resourceType string) error {
if !slices.Contains(supportedResourceTypes, resourceType) {
return xerrors.Errorf("resource type %q is not part of supported list [%s]", resourceType, strings.Join(supportedResourceTypes, ", "))
func validateResourceGfmAlerts(readmeBody string) []error {
trimmed := strings.TrimSpace(readmeBody)
if trimmed == "" {
return nil
}
allReadmeFiles, err := aggregateCoderResourceReadmeFiles(resourceType)
if err != nil {
return err
var errs []error
var sourceLine string
isInsideGfmQuotes := false
isInsideCodeBlock := false
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
for lineScanner.Scan() {
sourceLine = lineScanner.Text()
if strings.HasPrefix(sourceLine, "```") {
isInsideCodeBlock = !isInsideCodeBlock
continue
}
if isInsideCodeBlock {
continue
}
isInsideGfmQuotes = isInsideGfmQuotes && strings.HasPrefix(sourceLine, "> ")
currentMatch := gfmAlertRegex.FindStringSubmatch(sourceLine)
if currentMatch == nil {
continue
}
// Nested GFM alerts is such a weird mistake that it's probably not really safe to keep trying to process the
// rest of the content, so this will prevent any other validations from happening for the given line
if isInsideGfmQuotes {
errs = append(errs, errors.New("registry does not support nested GFM alerts"))
continue
}
leadingWhitespace := currentMatch[1]
if len(leadingWhitespace) != 1 {
errs = append(errs, errors.New("GFM alerts must have one space between the '>' and the start of the GFM brackets"))
}
isInsideGfmQuotes = true
alertHeader := currentMatch[2]
upperHeader := strings.ToUpper(alertHeader)
if !slices.Contains(gfmAlertTypes, upperHeader) {
errs = append(errs, xerrors.Errorf("GFM alert type %q is not supported", alertHeader))
}
if alertHeader != upperHeader {
errs = append(errs, xerrors.Errorf("GFM alerts must be in all caps"))
}
trailingWhitespace := currentMatch[3]
if trailingWhitespace != "" {
errs = append(errs, xerrors.Errorf("GFM alerts must not have any trailing whitespace after the closing bracket"))
}
extraContent := currentMatch[4]
if extraContent != "" {
errs = append(errs, xerrors.Errorf("GFM alerts must not have any extra content on the same line"))
}
}
logger.Info(context.Background(), "rocessing README files", "num_files", len(allReadmeFiles))
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles)
if err != nil {
return err
if gfmAlertRegex.Match([]byte(sourceLine)) {
errs = append(errs, xerrors.Errorf("README has an incomplete GFM alert at the end of the file"))
}
logger.Info(context.Background(), "rocessed README files as valid Coder resources", "num_files", len(resources), "type", resourceType)
if err := validateCoderResourceRelativeURLs(resources); err != nil {
return err
}
logger.Info(context.Background(), "all relative URLs for READMEs are valid", "type", resourceType)
return nil
return errs
}
+122
View File
@@ -0,0 +1,122 @@
package main
import (
"bufio"
"context"
"strings"
"golang.org/x/xerrors"
)
func validateCoderTemplateReadmeBody(body string) []error {
var errs []error
trimmed := strings.TrimSpace(body)
if baseErrs := validateReadmeBody(trimmed); len(baseErrs) != 0 {
errs = append(errs, baseErrs...)
}
var nextLine string
foundParagraph := false
isInsideCodeBlock := false
lineNum := 0
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
for lineScanner.Scan() {
lineNum++
nextLine = lineScanner.Text()
// Code assumes that invalid headers would've already been handled by the base validation function, so we don't
// need to check deeper if the first line isn't an h1.
if lineNum == 1 {
if !strings.HasPrefix(nextLine, "# ") {
break
}
continue
}
if strings.HasPrefix(nextLine, "```") {
isInsideCodeBlock = !isInsideCodeBlock
if strings.HasPrefix(nextLine, "```hcl") {
errs = append(errs, xerrors.New("all .hcl language references must be converted to .tf"))
}
continue
}
// Code assumes that we can treat this case as the end of the "h1 section" and don't need to process any further lines.
if lineNum > 1 && strings.HasPrefix(nextLine, "#") {
break
}
// Code assumes that if we've reached this point, the only other options are:
// (1) empty spaces, (2) paragraphs, (3) HTML, and (4) asset references made via [] syntax.
trimmedLine := strings.TrimSpace(nextLine)
isParagraph := trimmedLine != "" && !strings.HasPrefix(trimmedLine, "![") && !strings.HasPrefix(trimmedLine, "<")
foundParagraph = foundParagraph || isParagraph
}
if !foundParagraph {
errs = append(errs, xerrors.New("did not find paragraph within h1 section"))
}
if isInsideCodeBlock {
errs = append(errs, xerrors.New("code blocks inside h1 section do not all terminate before end of file"))
}
return errs
}
func validateCoderTemplateReadme(rm coderResourceReadme) []error {
var errs []error
for _, err := range validateCoderTemplateReadmeBody(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateResourceGfmAlerts(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if fmErrs := validateCoderResourceFrontmatter("templates", rm.filePath, rm.frontmatter); len(fmErrs) != 0 {
errs = append(errs, fmErrs...)
}
return errs
}
func validateAllCoderTemplateReadmes(resources []coderResourceReadme) error {
var yamlValidationErrors []error
for _, readme := range resources {
errs := validateCoderTemplateReadme(readme)
if len(errs) > 0 {
yamlValidationErrors = append(yamlValidationErrors, errs...)
}
}
if len(yamlValidationErrors) != 0 {
return validationPhaseError{
phase: validationPhaseReadme,
errors: yamlValidationErrors,
}
}
return nil
}
func validateAllCoderTemplates() error {
const resourceType = "templates"
allReadmeFiles, err := aggregateCoderResourceReadmeFiles(resourceType)
if err != nil {
return err
}
logger.Info(context.Background(), "processing template README files", "resource_type", resourceType, "num_files", len(allReadmeFiles))
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles)
if err != nil {
return err
}
err = validateAllCoderTemplateReadmes(resources)
if err != nil {
return err
}
logger.Info(context.Background(), "processed README files as valid Coder resources", "resource_type", resourceType, "num_files", len(resources))
if err := validateCoderResourceRelativeURLs(resources); err != nil {
return err
}
logger.Info(context.Background(), "all relative URLs for READMEs are valid", "resource_type", resourceType)
return nil
}
+46 -13
View File
@@ -19,11 +19,16 @@ type contributorProfileFrontmatter struct {
Bio string `yaml:"bio"`
ContributorStatus string `yaml:"status"`
AvatarURL *string `yaml:"avatar"`
GithubUsername *string `yaml:"github"`
LinkedinURL *string `yaml:"linkedin"`
WebsiteURL *string `yaml:"website"`
SupportEmail *string `yaml:"support_email"`
}
// A slice version of the struct tags from contributorProfileFrontmatter. Might be worth using reflection to generate
// this list at runtime in the future, but this should be okay for now
var supportedContributorProfileStructKeys = []string{"display_name", "bio", "status", "avatar", "linkedin", "github", "website", "support_email"}
type contributorProfileReadme struct {
frontmatter contributorProfileFrontmatter
namespace string
@@ -50,6 +55,22 @@ func validateContributorLinkedinURL(linkedinURL *string) error {
return nil
}
func validateGithubUsername(username *string) error {
if username == nil {
return nil
}
name := *username
trimmed := strings.TrimSpace(name)
if trimmed == "" {
return xerrors.New("username must have non-whitespace characters")
}
if name != trimmed {
return xerrors.Errorf("username %q has extra whitespace", trimmed)
}
return nil
}
// validateContributorSupportEmail does best effort validation of a contributors email address. We can't 100% validate
// that this is correct without actually sending an email, especially because some contributors are individual developers
// and we don't want to do that on every single run of the CI pipeline. The best we can do is verify the general structure.
@@ -58,7 +79,7 @@ func validateContributorSupportEmail(email *string) []error {
return nil
}
errs := []error{}
var errs []error
username, server, ok := strings.Cut(*email, "@")
if !ok {
@@ -119,7 +140,7 @@ func validateContributorAvatarURL(avatarURL *string) []error {
return []error{xerrors.New("avatar URL must be omitted or non-empty string")}
}
errs := []error{}
var errs []error
// Have to use .Parse instead of .ParseRequestURI because this is the one field that's allowed to be a relative URL.
if _, err := url.Parse(*avatarURL); err != nil {
errs = append(errs, xerrors.Errorf("URL %q is not a valid relative or absolute URL", *avatarURL))
@@ -145,7 +166,7 @@ func validateContributorAvatarURL(avatarURL *string) []error {
}
func validateContributorReadme(rm contributorProfileReadme) []error {
allErrs := []error{}
var allErrs []error
if err := validateContributorDisplayName(rm.frontmatter.DisplayName); err != nil {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
@@ -153,6 +174,9 @@ func validateContributorReadme(rm contributorProfileReadme) []error {
if err := validateContributorLinkedinURL(rm.frontmatter.LinkedinURL); err != nil {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
}
if err := validateGithubUsername(rm.frontmatter.GithubUsername); err != nil {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
}
if err := validateContributorWebsite(rm.frontmatter.WebsiteURL); err != nil {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
}
@@ -170,15 +194,24 @@ func validateContributorReadme(rm contributorProfileReadme) []error {
return allErrs
}
func parseContributorProfile(rm readme) (contributorProfileReadme, error) {
func parseContributorProfile(rm readme) (contributorProfileReadme, []error) {
fm, _, err := separateFrontmatter(rm.rawText)
if err != nil {
return contributorProfileReadme{}, xerrors.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
return contributorProfileReadme{}, []error{xerrors.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)}
}
keyErrs := validateFrontmatterYamlKeys(fm, supportedContributorProfileStructKeys)
if len(keyErrs) != 0 {
var remapped []error
for _, e := range keyErrs {
remapped = append(remapped, addFilePathToError(rm.filePath, e))
}
return contributorProfileReadme{}, remapped
}
yml := contributorProfileFrontmatter{}
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
return contributorProfileReadme{}, xerrors.Errorf("%q: failed to parse: %v", rm.filePath, err)
return contributorProfileReadme{}, []error{xerrors.Errorf("%q: failed to parse: %v", rm.filePath, err)}
}
return contributorProfileReadme{
@@ -190,11 +223,11 @@ func parseContributorProfile(rm readme) (contributorProfileReadme, error) {
func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfileReadme, error) {
profilesByNamespace := map[string]contributorProfileReadme{}
yamlParsingErrors := []error{}
var yamlParsingErrors []error
for _, rm := range readmeEntries {
p, err := parseContributorProfile(rm)
if err != nil {
yamlParsingErrors = append(yamlParsingErrors, err)
p, errs := parseContributorProfile(rm)
if len(errs) != 0 {
yamlParsingErrors = append(yamlParsingErrors, errs...)
continue
}
@@ -211,7 +244,7 @@ func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfil
}
}
yamlValidationErrors := []error{}
var yamlValidationErrors []error
for _, p := range profilesByNamespace {
if errors := validateContributorReadme(p); len(errors) > 0 {
yamlValidationErrors = append(yamlValidationErrors, errors...)
@@ -234,8 +267,8 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
return nil, err
}
allReadmeFiles := []readme{}
errs := []error{}
var allReadmeFiles []readme
var errs []error
dirPath := ""
for _, e := range dirEntries {
if !e.IsDir() {
+5 -1
View File
@@ -31,7 +31,11 @@ func main() {
if err != nil {
errs = append(errs, err)
}
err = validateAllCoderResourceFilesOfType("modules")
err = validateAllCoderModules()
if err != nil {
errs = append(errs, err)
}
err = validateAllCoderTemplates()
if err != nil {
errs = append(errs, err)
}
+26 -1
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"fmt"
"regexp"
"slices"
"strings"
"golang.org/x/xerrors"
@@ -39,7 +40,9 @@ const (
var (
supportedAvatarFileFormats = []string{".png", ".jpeg", ".jpg", ".gif", ".svg"}
// Matches markdown headers, must be at the beginning of a line, such as "# " or "### ".
// Matches markdown headers placed at the beginning of a line (e.g., "# " or "### "). To make the logic for
// validateReadmeBody easier, this pattern deliberately matches on invalid headers (header levels must be in the
// range 16 to be valid). The function has checks to see if the level is correct.
readmeHeaderRe = regexp.MustCompile(`^(#+)(\s*)`)
)
@@ -168,3 +171,25 @@ func validateReadmeBody(body string) []error {
return errs
}
func validateFrontmatterYamlKeys(frontmatter string, allowedKeys []string) []error {
if len(allowedKeys) == 0 {
return []error{xerrors.New("Set of allowed keys is empty")}
}
var key string
var cutOk bool
var line string
var errs []error
lineScanner := bufio.NewScanner(strings.NewReader(frontmatter))
for lineScanner.Scan() {
line = lineScanner.Text()
key, _, cutOk = strings.Cut(line, ":")
if !cutOk || slices.Contains(allowedKeys, key) {
continue
}
errs = append(errs, xerrors.Errorf("detected unknown key %q", key))
}
return errs
}
+49 -21
View File
@@ -4,24 +4,32 @@ import (
"errors"
"os"
"path"
"regexp"
"slices"
"strings"
"golang.org/x/xerrors"
)
var supportedUserNameSpaceDirectories = append(supportedResourceTypes, ".icons", ".images")
var supportedUserNameSpaceDirectories = append(supportedResourceTypes, ".images")
// validNameRe validates that names contain only alphanumeric characters and hyphens
var validNameRe = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$`)
// validateCoderResourceSubdirectory validates that the structure of a module or template within a namespace follows all
// expected file conventions
func validateCoderResourceSubdirectory(dirPath string) []error {
subDir, err := os.Stat(dirPath)
resourceDir, err := os.Stat(dirPath)
if err != nil {
// It's valid for a specific resource directory not to exist. It's just that if it does exist, it must follow specific rules.
// It's valid for a specific resource directory not to exist. It's just that if it does exist, it must follow
// specific rules.
if !errors.Is(err, os.ErrNotExist) {
return []error{addFilePathToError(dirPath, err)}
}
}
if !subDir.IsDir() {
if !resourceDir.IsDir() {
return []error{xerrors.Errorf("%q: path is not a directory", dirPath)}
}
@@ -30,14 +38,21 @@ func validateCoderResourceSubdirectory(dirPath string) []error {
return []error{addFilePathToError(dirPath, err)}
}
errs := []error{}
var errs []error
for _, f := range files {
// The .coder subdirectories are sometimes generated as part of Bun tests. These subdirectories will never be
// committed to the repo, but in the off chance that they don't get cleaned up properly, we want to skip over them.
// The .coder subdirectories are sometimes generated as part of our Bun tests. These subdirectories will never
// be committed to the repo, but in the off chance that they don't get cleaned up properly, we want to skip over
// them.
if !f.IsDir() || f.Name() == ".coder" {
continue
}
// Validate module/template name
if !validNameRe.MatchString(f.Name()) {
errs = append(errs, xerrors.Errorf("%q: name contains invalid characters (only alphanumeric characters and hyphens are allowed)", path.Join(dirPath, f.Name())))
continue
}
resourceReadmePath := path.Join(dirPath, f.Name(), "README.md")
if _, err := os.Stat(resourceReadmePath); err != nil {
if errors.Is(err, os.ErrNotExist) {
@@ -59,49 +74,59 @@ func validateCoderResourceSubdirectory(dirPath string) []error {
return errs
}
// validateRegistryDirectory validates that the contents of `/registry` follow all expected file conventions. This
// includes the top-level structure of the individual namespace directories.
func validateRegistryDirectory() []error {
userDirs, err := os.ReadDir(rootRegistryPath)
namespaceDirs, err := os.ReadDir(rootRegistryPath)
if err != nil {
return []error{err}
}
allErrs := []error{}
for _, d := range userDirs {
dirPath := path.Join(rootRegistryPath, d.Name())
if !d.IsDir() {
allErrs = append(allErrs, xerrors.Errorf("detected non-directory file %q at base of main Registry directory", dirPath))
var allErrs []error
for _, nDir := range namespaceDirs {
namespacePath := path.Join(rootRegistryPath, nDir.Name())
if !nDir.IsDir() {
allErrs = append(allErrs, xerrors.Errorf("detected non-directory file %q at base of main Registry directory", namespacePath))
continue
}
contributorReadmePath := path.Join(dirPath, "README.md")
// Validate namespace name
if !validNameRe.MatchString(nDir.Name()) {
allErrs = append(allErrs, xerrors.Errorf("%q: namespace name contains invalid characters (only alphanumeric characters and hyphens are allowed)", namespacePath))
continue
}
contributorReadmePath := path.Join(namespacePath, "README.md")
if _, err := os.Stat(contributorReadmePath); err != nil {
allErrs = append(allErrs, err)
}
files, err := os.ReadDir(dirPath)
files, err := os.ReadDir(namespacePath)
if err != nil {
allErrs = append(allErrs, err)
continue
}
for _, f := range files {
// TODO: Decide if there's anything more formal that we want to ensure about non-directories scoped to user namespaces.
// TODO: Decide if there's anything more formal that we want to ensure about non-directories at the top
// level of each user namespace.
if !f.IsDir() {
continue
}
segment := f.Name()
filePath := path.Join(dirPath, segment)
filePath := path.Join(namespacePath, segment)
if !slices.Contains(supportedUserNameSpaceDirectories, segment) {
allErrs = append(allErrs, xerrors.Errorf("%q: only these sub-directories are allowed at top of user namespace: [%s]", filePath, strings.Join(supportedUserNameSpaceDirectories, ", ")))
continue
}
if !slices.Contains(supportedResourceTypes, segment) {
continue
}
if slices.Contains(supportedResourceTypes, segment) {
if errs := validateCoderResourceSubdirectory(filePath); len(errs) != 0 {
allErrs = append(allErrs, errs...)
}
if errs := validateCoderResourceSubdirectory(filePath); len(errs) != 0 {
allErrs = append(allErrs, errs...)
}
}
}
@@ -109,6 +134,9 @@ func validateRegistryDirectory() []error {
return allErrs
}
// validateRepoStructure validates that the structure of the repo is "correct enough" to do all necessary validation
// checks. It is NOT an exhaustive validation of the entire repo structure it only checks the parts of the repo that
// are relevant for the main validation steps
func validateRepoStructure() error {
var errs []error
if vrdErrs := validateRegistryDirectory(); len(vrdErrs) != 0 {
+21
View File
@@ -0,0 +1,21 @@
run "plan_with_required_vars" {
command = plan
variables {
agent_id = "example-agent-id"
}
}
run "app_url_uses_port" {
command = plan
variables {
agent_id = "example-agent-id"
port = 19999
}
assert {
condition = resource.coder_app.module_name.url == "http://localhost:19999"
error_message = "Expected module-name app URL to include configured port"
}
}
+12 -12
View File
@@ -35,13 +35,13 @@ variable "agent_id" {
variable "log_path" {
type = string
description = "The path to log MODULE_NAME to."
default = "/tmp/MODULE_NAME.log"
description = "The path to the module log file."
default = "/tmp/module_name.log"
}
variable "port" {
type = number
description = "The port to run MODULE_NAME on."
description = "The port to run the application on."
default = 19999
}
@@ -59,9 +59,9 @@ variable "order" {
# Add other variables here
resource "coder_script" "MODULE_NAME" {
resource "coder_script" "module_name" {
agent_id = var.agent_id
display_name = "MODULE_NAME"
display_name = "Module Name"
icon = local.icon_url
script = templatefile("${path.module}/run.sh", {
LOG_PATH : var.log_path,
@@ -70,10 +70,10 @@ resource "coder_script" "MODULE_NAME" {
run_on_stop = false
}
resource "coder_app" "MODULE_NAME" {
resource "coder_app" "module_name" {
agent_id = var.agent_id
slug = "MODULE_NAME"
display_name = "MODULE_NAME"
slug = "module-name"
display_name = "Module Name"
url = "http://localhost:${var.port}"
icon = local.icon_url
subdomain = false
@@ -88,10 +88,10 @@ resource "coder_app" "MODULE_NAME" {
}
}
data "coder_parameter" "MODULE_NAME" {
type = "list(string)"
name = "MODULE_NAME"
display_name = "MODULE_NAME"
data "coder_parameter" "module_name" {
type = "string"
name = "module_name"
display_name = "Module Name"
icon = local.icon_url
mutable = var.mutable
default = local.options["Option 1"]["value"]
+33
View File
@@ -0,0 +1,33 @@
---
display_name: NAMESPACE_NAME
bio: Brief description of what this namespace provides
github: your-github-username
avatar: ./.images/avatar.svg
linkedin: https://www.linkedin.com/in/your-profile
website: https://your-website.com
status: community
---
# NAMESPACE_NAME
Brief description of what this namespace provides. Include information about:
- What types of templates/modules you offer
- Your focus areas (e.g., specific cloud providers, technologies)
- Any special features or configurations
## Templates
List your available templates here:
- **template-name**: Brief description
## Modules
List your available modules here:
- **module-name**: Brief description
## Contributing
If you'd like to contribute to this namespace, please [open an issue](https://github.com/coder/registry/issues) or submit a pull request.
+58
View File
@@ -0,0 +1,58 @@
---
name: TEMPLATE_NAME
description: A brief description of what this template does
tags: [tag1, tag2, tag3]
icon: /icon/TEMPLATE_NAME.svg
---
# TEMPLATE_NAME
A brief description of what this template provides and its use case.
## Features
- Feature 1
- Feature 2
- Feature 3
## Requirements
- List any prerequisites or requirements
- Provider-specific requirements (e.g., Docker, AWS credentials)
- Minimum Coder version if applicable
## Usage
1. Step-by-step instructions on how to use this template
2. Any configuration that needs to be done
3. How to customize the template
## Variables
| Name | Description | Type | Default | Required |
| ----------- | --------------------------- | -------- | ----------------- | -------- |
| example_var | Description of the variable | `string` | `"default_value"` | no |
## Resources Created
- List of resources that will be created
- Brief description of each resource
## Customization
Explain how users can customize this template for their needs:
- How to modify the startup script
- How to add additional software
- How to configure different providers
## Troubleshooting
### Common Issues
- Issue 1 and its solution
- Issue 2 and its solution
## Contributing
Contributions are welcome! Please see the [contributing guidelines](../../CONTRIBUTING.md) for more information.
+172
View File
@@ -0,0 +1,172 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
# Add your provider here (e.g., docker, aws, gcp, azure)
# docker = {
# source = "kreuzwerker/docker"
# }
}
}
locals {
username = data.coder_workspace_owner.me.name
}
# Add your variables here
# variable "example_var" {
# default = "default_value"
# description = "Description of the variable"
# type = string
# }
# Configure your provider here
# provider "docker" {
# host = var.docker_socket != "" ? var.docker_socket : null
# }
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
startup_script = <<-EOT
set -e
# Prepare user home with default files on first start.
if [ ! -f ~/.init_done ]; then
cp -rT /etc/skel ~
touch ~/.init_done
fi
# Add any commands that should be executed at workspace startup here
EOT
# These environment variables allow you to make Git commits right away after creating a
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
# You can remove this block if you'd prefer to configure Git manually or using
# dotfiles. (see docs/dotfiles.md)
env = {
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
}
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
# For basic templates, you can remove the "display_apps" block.
metadata {
display_name = "CPU Usage"
key = "0_cpu_usage"
script = "coder stat cpu"
interval = 10
timeout = 1
}
metadata {
display_name = "RAM Usage"
key = "1_ram_usage"
script = "coder stat mem"
interval = 10
timeout = 1
}
metadata {
display_name = "Home Disk"
key = "3_home_disk"
script = "coder stat disk --path $${HOME}"
interval = 60
timeout = 1
}
display_apps {
vscode = true
vscode_insiders = false
ssh_helper = false
port_forwarding_helper = true
web_terminal = true
}
}
# Add your resources here (e.g., docker container, VM, etc.)
# resource "docker_image" "main" {
# name = "codercom/enterprise-base:ubuntu"
# }
# resource "docker_container" "workspace" {
# count = data.coder_workspace.me.start_count
# image = docker_image.main.image_id
# # Uses lower() to avoid Docker restriction on container names.
# name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# # Hostname makes the shell more user friendly: coder@my-workspace:~$
# hostname = data.coder_workspace.me.name
# # Use the docker gateway if the access URL is 127.0.0.1
# entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\.0\.0\.1/", "host.docker.internal")]
# env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
# host {
# host = "host.docker.internal"
# ip = "host-gateway"
# }
# volumes {
# container_path = "/home/${local.username}"
# volume_name = docker_volume.home_volume[0].name
# read_only = false
# }
# # Add labels in Docker to keep track of orphan resources.
# labels {
# label = "coder.owner"
# value = data.coder_workspace_owner.me.name
# }
# labels {
# label = "coder.owner_id"
# value = data.coder_workspace_owner.me.id
# }
# labels {
# label = "coder.workspace_id"
# value = data.coder_workspace.me.id
# }
# labels {
# label = "coder.workspace_name"
# value = data.coder_workspace.me.name
# }
# }
# resource "docker_volume" "home_volume" {
# count = data.coder_workspace.me.start_count
# name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}-home"
# # Protect the volume from being deleted due to changes in attributes.
# lifecycle {
# ignore_changes = all
# }
# # Add labels in Docker to keep track of orphan resources.
# labels {
# label = "coder.owner"
# value = data.coder_workspace_owner.me.name
# }
# labels {
# label = "coder.owner_id"
# value = data.coder_workspace_owner.me.id
# }
# labels {
# label = "coder.workspace_id"
# value = data.coder_workspace.me.id
# }
# labels {
# label = "coder.workspace_name"
# value = data.coder_workspace.me.name
# }
# }
resource "coder_metadata" "workspace_info" {
resource_id = coder_agent.main.id
item {
key = "TEMPLATE_NAME"
value = "TEMPLATE_NAME"
}
}
+7 -6
View File
@@ -1,18 +1,19 @@
{
"name": "registry",
"scripts": {
"fmt": "bun x prettier --write **/*.sh **/*.ts **/*.md *.md && terraform fmt -recursive -diff",
"fmt:ci": "bun x prettier --check **/*.sh **/*.ts **/*.md *.md && terraform fmt -check -recursive -diff",
"fmt": "bun x prettier --write . && terraform fmt -recursive -diff",
"fmt:ci": "bun x prettier --check . && terraform fmt -check -recursive -diff",
"terraform-validate": "./scripts/terraform_validate.sh",
"test": "bun test",
"tftest": "./scripts/terraform_test_all.sh",
"tstest": "./scripts/ts_test_auto.sh",
"update-version": "./update-version.sh"
},
"devDependencies": {
"@types/bun": "^1.2.18",
"bun-types": "^1.2.18",
"@types/bun": "^1.2.21",
"bun-types": "^1.2.21",
"dedent": "^1.6.0",
"gray-matter": "^4.0.3",
"marked": "^16.0.0",
"marked": "^16.2.0",
"prettier": "^3.6.2",
"prettier-plugin-sh": "^0.18.0",
"prettier-plugin-terraform-formatter": "^1.2.1"
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

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

After

Width:  |  Height:  |  Size: 28 KiB

+14
View File
@@ -0,0 +1,14 @@
---
display_name: "Benraouane Soufiane"
bio: "Full stack developer creating awesome things."
avatar: "./.images/avatar.png"
github: "benraouanesoufiane"
linkedin: "https://www.linkedin.com/in/benraouane-soufiane" # Optional
website: "https://benraouanesoufiane.com" # Optional
support_email: "hello@benraouanesoufiane.com" # Optional
status: "community"
---
# Benraouane Soufiane
Full stack developer creating awesome things.
@@ -0,0 +1,82 @@
---
display_name: RustDesk
description: Run RustDesk in your workspace with virtual display
icon: ../../../../.icons/rustdesk.svg
verified: false
tags: [rustdesk, rdp, vm]
---
# RustDesk
Launches RustDesk within your workspace with a virtual display to provide remote desktop access. The module outputs the RustDesk ID and password needed to connect from external RustDesk clients.
```tf
module "rustdesk" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/BenraouaneSoufiane/rustdesk/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
## Features
- Automatically sets up virtual display (Xvfb)
- Downloads and configures RustDesk
- Outputs RustDesk ID and password for easy connection
- Provides external app link to RustDesk web client for browser-based access
- Starts virtual display (Xvfb) with customizable resolution
- Customizable screen resolution and RustDesk version
## Requirements
- Coder v2.5 or higher
- Linux workspace with `apt`, `dnf`, or `yum` package manager
## Examples
### Custom configuration with specific version
```tf
module "rustdesk" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/BenraouaneSoufiane/rustdesk/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
rustdesk_password = "mycustompass"
xvfb_resolution = "1920x1080x24"
rustdesk_version = "1.4.1"
}
```
### Docker container configuration
It requires coder' server to be run as root, when using with Docker, add the following to your `docker_container` resource:
```tf
resource "docker_container" "workspace" {
# ... other configuration ...
user = "root"
privileged = true
network_mode = "host"
ports {
internal = 21115
external = 21115
}
ports {
internal = 21116
external = 21116
}
ports {
internal = 21118
external = 21118
}
ports {
internal = 21119
external = 21119
}
}
```
@@ -0,0 +1,75 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
variable "log_path" {
type = string
description = "The path to log rustdesk to."
default = "/tmp/rustdesk.log"
}
variable "agent_id" {
description = "Attach RustDesk setup to this agent"
type = string
}
variable "order" {
description = "Run order among scripts/apps"
type = number
default = 1
}
# Optional knobs passed as env (you can expose these as variables too)
variable "rustdesk_password" {
description = "If empty, the script will generate one"
type = string
default = ""
sensitive = true
}
variable "xvfb_resolution" {
description = "Xvfb screen size/depth"
type = string
default = "1024x768x16"
}
variable "rustdesk_version" {
description = "RustDesk version to install (use 'latest' for most recent release)"
type = string
default = "latest"
}
resource "coder_script" "rustdesk" {
agent_id = var.agent_id
display_name = "RustDesk"
run_on_start = true
# Prepend env as bash exports, then append the script file literally.
script = <<-EOT
# --- module-provided env knobs ---
export RUSTDESK_PASSWORD="${var.rustdesk_password}"
export XVFB_RESOLUTION="${var.xvfb_resolution}"
export RUSTDESK_VERSION="${var.rustdesk_version}"
# ---------------------------------
${file("${path.module}/run.sh")}
EOT
}
resource "coder_app" "rustdesk" {
agent_id = var.agent_id
slug = "rustdesk"
display_name = "Rustdesk"
url = "https://rustdesk.com/web"
icon = "/icon/rustdesk.svg"
order = var.order
external = true
}
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
BOLD='\033[0;1m'
RESET='\033[0m'
printf "${BOLD}🖥️ Installing RustDesk Remote Desktop\n${RESET}"
# ---- configurable knobs (env overrides) ----
RUSTDESK_VERSION="${RUSTDESK_VERSION:-latest}"
LOG_PATH="${LOG_PATH:-/tmp/rustdesk.log}"
# ---- fetch latest version if needed ----
if [ "$RUSTDESK_VERSION" = "latest" ]; then
printf "🔍 Fetching latest RustDesk version...\n"
RUSTDESK_VERSION=$(curl -s https://api.github.com/repos/rustdesk/rustdesk/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' || echo "1.4.1")
printf "📌 Fetched RustDesk version: ${RUSTDESK_VERSION}\n"
else
printf "📌 Using specified RustDesk version: ${RUSTDESK_VERSION}\n"
fi
XVFB_RESOLUTION="${XVFB_RESOLUTION:-1024x768x16}"
RUSTDESK_PASSWORD="${RUSTDESK_PASSWORD:-}"
# ---- detect package manager & arch ----
ARCH="$(uname -m)"
case "$ARCH" in
x86_64 | amd64) PKG_ARCH="x86_64" ;;
aarch64 | arm64) PKG_ARCH="aarch64" ;;
*)
echo "❌ Unsupported arch: $ARCH"
exit 1
;;
esac
if command -v apt-get > /dev/null 2>&1; then
PKG_SYS="deb"
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.deb"
INSTALL_DEPS='apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y wget libva2 libva-drm2 libva-x11-2 libgstreamer-plugins-base1.0-0 gstreamer1.0-pipewire xfce4 xfce4-goodies xvfb x11-xserver-utils dbus-x11 libegl1 libgl1 libglx0 libglu1-mesa mesa-utils libxrandr2 libxss1 libgtk-3-0t64 libgbm1 libdrm2 libxcomposite1 libxdamage1 libxfixes3'
INSTALL_CMD="apt-get install -y ./${PKG_NAME}"
CLEAN_CMD="rm -f \"${PKG_NAME}\""
elif command -v dnf > /dev/null 2>&1; then
PKG_SYS="rpm"
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.rpm"
INSTALL_DEPS='dnf install -y wget libva libva-intel-driver gstreamer1-plugins-base pipewire xfce4-session xfce4-panel xorg-x11-server-Xvfb xorg-x11-xauth dbus-x11 mesa-libEGL mesa-libGL mesa-libGLU mesa-dri-drivers libXrandr libXScrnSaver gtk3 mesa-libgbm libdrm libXcomposite libXdamage libXfixes'
INSTALL_CMD="dnf install -y ./${PKG_NAME}"
CLEAN_CMD="rm -f \"${PKG_NAME}\""
elif command -v yum > /dev/null 2>&1; then
PKG_SYS="rpm"
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.rpm"
INSTALL_DEPS='yum install -y wget libva libva-intel-driver gstreamer1-plugins-base pipewire xfce4-session xfce4-panel xorg-x11-server-Xvfb xorg-x11-xauth dbus-x11 mesa-libEGL mesa-libGL mesa-libGLU mesa-dri-drivers libXrandr libXScrnSaver gtk3 mesa-libgbm libdrm libXcomposite libXdamage libXfixes'
INSTALL_CMD="yum install -y ./${PKG_NAME}"
CLEAN_CMD="rm -f \"${PKG_NAME}\""
else
echo "❌ Unsupported distro: need apt, dnf, or yum."
exit 1
fi
# ---- install rustdesk if missing ----
if ! command -v rustdesk > /dev/null 2>&1; then
printf "📦 Installing dependencies...\n"
sudo bash -c "$INSTALL_DEPS" 2>&1 | tee -a "${LOG_PATH}"
printf "⬇️ Downloading RustDesk ${RUSTDESK_VERSION} (${PKG_SYS}, ${PKG_ARCH})...\n"
URL="https://github.com/rustdesk/rustdesk/releases/download/${RUSTDESK_VERSION}/${PKG_NAME}"
wget -q "$URL" 2>&1 | tee -a "${LOG_PATH}"
printf "🔧 Installing RustDesk...\n"
sudo bash -c "$INSTALL_CMD" 2>&1 | tee -a "${LOG_PATH}"
printf "🧹 Cleaning up...\n"
bash -c "$CLEAN_CMD" 2>&1 | tee -a "${LOG_PATH}"
else
printf "✅ RustDesk already installed\n"
fi
# ---- start virtual display ----
echo "Starting Xvfb with resolution ${XVFB_RESOLUTION}"
Xvfb :99 -screen 0 "${XVFB_RESOLUTION}" >> "${LOG_PATH}" 2>&1 &
export DISPLAY=:99
# Wait for X to be ready
for i in {1..10}; do
if xdpyinfo -display :99 > /dev/null 2>&1; then
echo "X display is ready"
break
fi
sleep 1
done
# ---- create (or accept) password and start rustdesk ----
if [[ -z "${RUSTDESK_PASSWORD}" ]]; then
RUSTDESK_PASSWORD="$(tr -dc 'a-zA-Z0-9@' < /dev/urandom | head -c 10)@97"
fi
echo "Starting XFCE desktop environment..."
xfce4-session >> "${LOG_PATH}" 2>&1 &
echo "Waiting for xfce4-session to initialize..."
sleep 5
printf "🔐 Setting RustDesk password and starting service...\n"
rustdesk >> "${LOG_PATH}" 2>&1 &
sleep 2
rustdesk --password "${RUSTDESK_PASSWORD}" >> "${LOG_PATH}" 2>&1 &
sleep 3
RID="$(rustdesk --get-id 2> /dev/null || echo 'ID_PENDING')"
printf "🥳 RustDesk setup complete!\n\n"
printf "${BOLD}📋 Connection Details:${RESET}\n"
printf " RustDesk ID: ${RID}\n"
printf " RustDesk Password: ${RUSTDESK_PASSWORD}\n"
printf " Display: ${DISPLAY} (${XVFB_RESOLUTION})\n"
printf "\n📝 Logs available at: ${LOG_PATH}\n\n"
echo "Setup script completed successfully. All services running in background."
exit 0
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

+13
View File
@@ -0,0 +1,13 @@
---
display_name: "Jay Kumar"
bio: "I'm a Software Engineer :)"
avatar: "./.images/avatar.jpeg"
github: "35C4n0r"
linkedin: "https://www.linkedin.com/in/jaykum4r"
support_email: "work.jaykumar@gmail.com"
status: "community"
---
# Your Name
I'm a Software Engineer :)
+99
View File
@@ -0,0 +1,99 @@
---
display_name: "tmux"
description: "tmux with session persistence and plugins"
icon: "../../../../.icons/tmux.svg"
verified: false
tags: ["tmux", "terminal", "persistent"]
---
# tmux
This module provisions and configures [tmux](https://github.com/tmux/tmux) with session persistence and plugin support
for a Coder agent. It automatically installs tmux, the Tmux Plugin Manager (TPM), and a set of useful plugins, and sets
up a default or custom tmux configuration with session save/restore capabilities.
```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
}
```
## Features
- Installs tmux if not already present
- Installs TPM (Tmux Plugin Manager)
- Configures tmux with plugins for sensible defaults, session persistence, and automation:
- `tmux-plugins/tpm`
- `tmux-plugins/tmux-sensible`
- `tmux-plugins/tmux-resurrect`
- `tmux-plugins/tmux-continuum`
- Supports custom tmux configuration
- Enables automatic session save
- Configurable save interval
- **Supports multiple named tmux sessions, each as a separate app in the Coder UI**
## Usage
```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
tmux_config = "" # Optional: custom tmux.conf content
save_interval = 1 # Optional: save interval in minutes
sessions = ["default", "dev", "ops"] # Optional: list of tmux sessions
order = 1 # Optional: UI order
group = "Terminal" # Optional: UI group
icon = "/icon/tmux.svg" # Optional: app icon
}
```
## Multi-Session Support
This module can provision multiple tmux sessions, each as a separate app in the Coder UI. Use the `sessions` variable to specify a list of session names. For each session, a `coder_app` is created, allowing you to launch or attach to that session directly from the UI.
- **sessions**: List of tmux session names (default: `["default"]`).
## How It Works
- **tmux Installation:**
- Checks if tmux is installed; if not, installs it using the system's package manager (supports apt, yum, dnf,
zypper, apk, brew).
- **TPM Installation:**
- Installs the Tmux Plugin Manager (TPM) to `~/.tmux/plugins/tpm` if not already present.
- **tmux Configuration:**
- If `tmux_config` is provided, writes it to `~/.tmux.conf`.
- Otherwise, generates a default configuration with plugin support and session persistence (using tmux-resurrect and
tmux-continuum).
- Sets up key bindings for quick session save (`Ctrl+s`) and restore (`Ctrl+r`).
- **Plugin Installation:**
- Installs plugins via TPM.
- **Session Persistence:**
- Enables automatic session save/restore at the configured interval.
## Example
```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.1"
agent_id = var.agent_id
sessions = ["default", "dev", "anomaly"]
tmux_config = <<-EOT
set -g mouse on
set -g history-limit 10000
EOT
group = "Terminal"
order = 2
}
```
> [!IMPORTANT]
> If you provide a custom `tmux_config`, it will completely replace the default configuration. Ensure you include plugin and TPM initialization lines if you want plugin support and session persistence.
> The script will attempt to install dependencies using `sudo` where required.
> If `git` is not installed, TPM installation will fail.
> If you are using custom config, you'll be responsible for setting up persistence and plugins.
> The `order`, `group`, and `icon` variables allow you to customize how tmux apps appear in the Coder UI.
> In case of session restart or shh reconnection, the tmux session will be automatically restored :)
@@ -0,0 +1,37 @@
import { describe, it, expect } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
findResourceInstance,
} from "~test";
import path from "path";
const moduleDir = path.resolve(__dirname);
const requiredVars = {
agent_id: "dummy-agent-id",
};
describe("tmux module", async () => {
await runTerraformInit(moduleDir);
// 1. Required variables
testRequiredVariables(moduleDir, requiredVars);
// 2. coder_script resource is created
it("creates coder_script resource", async () => {
const state = await runTerraformApply(moduleDir, requiredVars);
const scriptResource = findResourceInstance(state, "coder_script");
expect(scriptResource).toBeDefined();
expect(scriptResource.agent_id).toBe(requiredVars.agent_id);
// check that the script contains expected lines
expect(scriptResource.script).toContain("Installing tmux");
expect(scriptResource.script).toContain(
"Installing Tmux Plugin Manager (TPM)",
);
expect(scriptResource.script).toContain("tmux configuration created at");
expect(scriptResource.script).toContain("✅ tmux setup complete!");
});
});
+78
View File
@@ -0,0 +1,78 @@
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 "tmux_config" {
type = string
description = "Custom tmux configuration to apply."
default = ""
}
variable "save_interval" {
type = number
description = "Save interval (in minutes)."
default = 1
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/tmux.svg"
}
variable "sessions" {
type = list(string)
description = "List of tmux sessions to create or start."
default = ["default"]
}
resource "coder_script" "tmux" {
agent_id = var.agent_id
display_name = "tmux"
icon = "/icon/terminal.svg"
script = templatefile("${path.module}/scripts/run.sh", {
TMUX_CONFIG = var.tmux_config
SAVE_INTERVAL = var.save_interval
})
run_on_start = true
run_on_stop = false
}
resource "coder_app" "tmux_sessions" {
for_each = toset(var.sessions)
agent_id = var.agent_id
slug = "tmux-${each.value}"
display_name = "tmux - ${each.value}"
icon = var.icon
order = var.order
group = var.group
command = templatefile("${path.module}/scripts/start.sh", {
SESSION_NAME = each.value
})
}
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env bash
BOLD='\033[0;1m'
# Convert templated variables to shell variables
SAVE_INTERVAL="${SAVE_INTERVAL}"
TMUX_CONFIG="${TMUX_CONFIG}"
# Function to install tmux
install_tmux() {
printf "Checking for tmux installation\n"
if command -v tmux &> /dev/null; then
printf "tmux is already installed \n\n"
return 0
fi
printf "Installing tmux \n\n"
# Detect package manager and install tmux
if command -v apt-get &> /dev/null; then
sudo apt-get update
sudo apt-get install -y tmux
elif command -v yum &> /dev/null; then
sudo yum install -y tmux
elif command -v dnf &> /dev/null; then
sudo dnf install -y tmux
elif command -v zypper &> /dev/null; then
sudo zypper install -y tmux
elif command -v apk &> /dev/null; then
sudo apk add tmux
elif command -v brew &> /dev/null; then
brew install tmux
else
printf "No supported package manager found. Please install tmux manually. \n"
exit 1
fi
printf "tmux installed successfully \n"
}
# Function to install Tmux Plugin Manager (TPM)
install_tpm() {
local tpm_dir="$HOME/.tmux/plugins/tpm"
if [ -d "$tpm_dir" ]; then
printf "TPM is already installed"
return 0
fi
printf "Installing Tmux Plugin Manager (TPM) \n"
# Create plugins directory
mkdir -p "$HOME/.tmux/plugins"
# Clone TPM repository
if command -v git &> /dev/null; then
git clone https://github.com/tmux-plugins/tpm "$tpm_dir"
printf "TPM installed successfully"
else
printf "Git is not installed. Please install git to use tmux plugins. \n"
exit 1
fi
}
# Function to create tmux configuration
setup_tmux_config() {
printf "Setting up tmux configuration \n"
local config_dir="$HOME/.tmux"
local config_file="$HOME/.tmux.conf"
mkdir -p "$config_dir"
if [ -n "$TMUX_CONFIG" ]; then
printf "$TMUX_CONFIG" > "$config_file"
printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n"
else
cat > "$config_file" << EOF
# Tmux Configuration File
# =============================================================================
# PLUGIN CONFIGURATION
# =============================================================================
# List of plugins
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'
# tmux-continuum configuration
set -g @continuum-restore 'on'
set -g @continuum-save-interval '$${SAVE_INTERVAL}'
set -g @continuum-boot 'on'
set -g status-right 'Continuum status: #{continuum_status}'
# =============================================================================
# KEY BINDINGS FOR SESSION MANAGEMENT
# =============================================================================
# Quick session save and restore
bind C-s run-shell "~/.tmux/plugins/tmux-resurrect/scripts/save.sh"
bind C-r run-shell "~/.tmux/plugins/tmux-resurrect/scripts/restore.sh"
# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf)
run '~/.tmux/plugins/tpm/tpm'
EOF
printf "tmux configuration created at {$config_file} \n\n"
fi
}
# Function to install tmux plugins
install_plugins() {
printf "Installing tmux plugins"
# Check if TPM is installed
if [ ! -d "$HOME/.tmux/plugins/tpm" ]; then
printf "TPM is not installed. Cannot install plugins. \n"
return 1
fi
# Install plugins using TPM
"$HOME/.tmux/plugins/tpm/bin/install_plugins"
printf "tmux plugins installed successfully \n"
}
# Main execution
main() {
printf "$${BOLD} 🛠️Setting up tmux with session persistence! \n\n"
printf ""
# Install dependencies
install_tmux
install_tpm
# Setup tmux configuration
setup_tmux_config
# Install plugins
install_plugins
printf "$${BOLD}✅ tmux setup complete! \n\n"
printf "$${BOLD} Attempting to restore sessions\n"
tmux new-session -d \; source-file ~/.tmux.conf \; run-shell '~/.tmux/plugins/tmux-resurrect/scripts/restore.sh'
printf "$${BOLD} Sessions restored: -> %s\n" "$(tmux ls)"
}
# Run main function
main
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Convert templated variables to shell variables
SESSION_NAME='${SESSION_NAME}'
# Function to check if tmux is installed
check_tmux() {
if ! command -v tmux &> /dev/null; then
echo "tmux is not installed. Please run the tmux setup script first."
exit 1
fi
}
# Function to handle a single session
handle_session() {
local session_name="$1"
# Check if the session exists
if tmux has-session -t "$session_name" 2> /dev/null; then
echo "Session '$session_name' exists, attaching to it..."
tmux attach-session -t "$session_name"
else
echo "Session '$session_name' does not exist, creating it..."
tmux new-session -d -s "$session_name"
tmux attach-session -t "$session_name"
fi
}
# Main function
main() {
# Check if tmux is installed
check_tmux
handle_session "${SESSION_NAME}"
}
# Run the main function
main
+1 -1
View File
@@ -5,7 +5,7 @@ github: coder
avatar: ./.images/avatar.svg
linkedin: https://www.linkedin.com/company/coderhq
website: https://discord.gg/coder
status: community
status: official
---
å
@@ -0,0 +1,163 @@
---
display_name: Archive
description: Create automated and user-invocable scripts that archive and extract selected files/directories with optional compression (gzip or zstd).
icon: ../../../../.icons/folder.svg
verified: false
tags: [backup, archive, tar, helper]
---
# Archive
This module installs small, robust scripts in your workspace to create and extract tar archives from a list of files and directories. It supports optional compression (gzip or zstd). The create command prints only the resulting archive path to stdout; operational logs go to stderr. An optional stop hook can also create an archive automatically when the workspace stops, and an optional start hook can wait for an archive on-disk and extract it on start.
```tf
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.1"
agent_id = coder_agent.example.id
paths = ["./projects", "./code"]
}
```
## Features
- Installs two commands into the workspace `$PATH`: `coder-archive-create` and `coder-archive-extract`.
- Creates a single `.tar`, `.tar.gz`, or `.tar.zst` containing selected paths (depends on `tar`).
- Optional compression: `gzip`, `zstd` (depends on `gzip` or `zstd`).
- Stores defaults so commands can be run without arguments (supports overriding via CLI flags).
- Logs and status messages go to stderr, the create command prints only the final archive path to stdout.
- Optional:
- `create_on_stop` to create an archive automatically when the workspace stops.
- `extract_on_start` to wait for an archive to appear and extract it on start.
> [!WARNING]
> The `create_on_stop` feature uses the `coder_script` `run_on_stop` which may not work as expected on certain templates without additional provider configuration. The agent may be terminated before the script completes. See [coder/coder#6174](https://github.com/coder/coder/issues/6174) for provider-specific workarounds and [coder/coder#6175](https://github.com/coder/coder/issues/6175) for tracking a fix.
## Usage
Basic example:
```tf
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.1"
agent_id = coder_agent.example.id
# Paths to include in the archive (files or directories).
directory = "~"
paths = [
"./projects",
"./code",
]
}
```
Customize compression and output:
```tf
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.1"
agent_id = coder_agent.example.id
directory = "/"
paths = ["/etc", "/home"]
compression = "zstd" # "gzip" | "zstd" | "none"
output_dir = "/tmp/backup" # defaults to /tmp
archive_name = "my-backup" # base name (extension is inferred from compression)
}
```
Enable auto-archive on stop:
```tf
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.1"
agent_id = coder_agent.example.id
# Creates /tmp/coder-archive.tar.gz of the users home directory (defaults).
create_on_stop = true
}
```
Extract on start:
```tf
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.1"
agent_id = coder_agent.example.id
# Where to look for the archive file to extract:
output_dir = "/tmp"
archive_name = "my-archive"
compression = "gzip"
# Waits up to 5 minutes for /tmp/my-archive.tar.gz to be present, note that
# using a long timeout will delay every workspace start by this much until the
# archive is present.
extract_on_start = true
extract_wait_timeout_seconds = 300
}
```
## Command usage
The installer writes the following files:
- `$CODER_SCRIPT_DATA_DIR/archive-lib.sh`
- `$CODER_SCRIPT_BIN_DIR/coder-archive-create`
- `$CODER_SCRIPT_BIN_DIR/coder-archive-extract`
Create usage:
```console
coder-archive-create [OPTIONS] [PATHS...]
-c, --compression <gzip|zstd|none> Compression algorithm (default from module)
-C, --directory <DIRECTORY> Change to directory for archiving (default from module)
-f, --file <ARCHIVE> Output archive file (default from module)
-h, --help Show help
```
Extract usage:
```console
coder-archive-extract [OPTIONS]
-c, --compression <gzip|zstd|none> Compression algorithm (default from module)
-C, --directory <DIRECTORY> Extract into directory (default from module)
-f, --file <ARCHIVE> Archive file to extract (default from module)
-h, --help Show help
```
Examples:
- Use Terraform defaults:
```
coder-archive-create
```
- Override compression and output file at runtime:
```
coder-archive-create --compression zstd --file /tmp/backups/archive.tar.zst
```
- Add extra paths on the fly (in addition to the Terraform defaults):
```
coder-archive-create /etc/hosts
```
- Extract an archive into a directory:
```
coder-archive-extract --file /tmp/backups/archive.tar.gz --directory /tmp/restore
```
@@ -0,0 +1,33 @@
mock_provider "coder" {}
run "apply_defaults" {
command = apply
variables {
agent_id = "agent-123"
paths = ["~/project", "/etc/hosts"]
}
assert {
condition = output.archive_path == "/tmp/coder-archive.tar.gz"
error_message = "archive_path should be empty when archive_name is not set"
}
}
run "apply_with_name" {
command = apply
variables {
agent_id = "agent-123"
paths = ["/etc/hosts"]
archive_name = "nightly"
output_dir = "/tmp/backups"
compression = "zstd"
create_archive_on_stop = true
}
assert {
condition = output.archive_path == "/tmp/backups/nightly.tar.zst"
error_message = "archive_path should be computed from archive_name + output_dir + extension"
}
}
@@ -0,0 +1,348 @@
import { describe, expect, it, beforeAll } from "bun:test";
import {
execContainer,
findResourceInstance,
runContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
type TerraformState,
} from "~test";
const USE_XTRACE =
process.env.ARCHIVE_TEST_XTRACE === "1" || process.env.XTRACE === "1";
const IMAGE = "alpine";
const BIN_DIR = "/tmp/coder-script-data/bin";
const DATA_DIR = "/tmp/coder-script-data";
type ExecResult = {
exitCode: number;
stdout: string;
stderr: string;
};
const ensureRunOk = (label: string, res: ExecResult) => {
if (res.exitCode !== 0) {
console.error(
`[${label}] non-zero exit code: ${res.exitCode}\n--- stdout ---\n${res.stdout.trim()}\n--- stderr ---\n${res.stderr.trim()}\n--------------`,
);
}
expect(res.exitCode).toBe(0);
};
const sh = async (id: string, cmd: string): Promise<ExecResult> => {
const res = await execContainer(id, ["sh", "-c", cmd]);
return res;
};
const bashRun = async (id: string, cmd: string): Promise<ExecResult> => {
const injected = USE_XTRACE ? `/bin/bash -x ${cmd}` : cmd;
return sh(id, injected);
};
const prepareContainer = async (image = IMAGE) => {
const id = await runContainer(image);
// Prepare script dirs and deps.
ensureRunOk(
"mkdirs",
await sh(id, `mkdir -p ${BIN_DIR} ${DATA_DIR} /tmp/backup`),
);
// Install tools used by tests.
ensureRunOk(
"apk add",
await sh(id, "apk add --no-cache bash tar gzip zstd coreutils"),
);
return id;
};
const installArchive = async (
state: TerraformState,
opts?: { env?: string[] },
) => {
const instance = findResourceInstance(state, "coder_script");
const id = await prepareContainer();
// Run installer script with correct env for CODER_SCRIPT paths.
const args = ["bash"];
if (USE_XTRACE) args.push("-x");
args.push("-c", instance.script);
const resp = await execContainer(id, args, [
"--env",
`CODER_SCRIPT_BIN_DIR=${BIN_DIR}`,
"--env",
`CODER_SCRIPT_DATA_DIR=${DATA_DIR}`,
...(opts?.env ?? []),
]);
return {
id,
install: {
exitCode: resp.exitCode,
stdout: resp.stdout.trim(),
stderr: resp.stderr.trim(),
},
};
};
const fileExists = async (id: string, path: string) => {
const res = await sh(id, `test -f ${path} && echo yes || echo no`);
return res.stdout.trim() === "yes";
};
const isExecutable = async (id: string, path: string) => {
const res = await sh(id, `test -x ${path} && echo yes || echo no`);
return res.stdout.trim() === "yes";
};
const listTar = async (id: string, path: string) => {
// Try to autodetect compression flags from extension.
let cmd = "";
if (path.endsWith(".tar.gz")) {
cmd = `tar -tzf ${path}`;
} else if (path.endsWith(".tar.zst")) {
// validate with zstd and ask tar to list via --zstd.
cmd = `zstd -t -q ${path} && tar --zstd -tf ${path}`;
} else {
cmd = `tar -tf ${path}`;
}
return sh(id, cmd);
};
describe("archive", () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
// Ensure required variables are enforced.
testRequiredVariables(import.meta.dir, {
agent_id: "agent-123",
});
it("installs wrapper scripts to BIN_DIR and library to DATA_DIR", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
});
// The Terraform output should reflect defaults from main.tf.
expect(state.outputs.archive_path.value).toEqual(
"/tmp/coder-archive.tar.gz",
);
const { id, install } = await installArchive(state);
ensureRunOk("install", install);
expect(install.stdout).toContain(
`Installed archive library to: ${DATA_DIR}/archive-lib.sh`,
);
expect(install.stdout).toContain(
`Installed create script to: ${BIN_DIR}/coder-archive-create`,
);
expect(install.stdout).toContain(
`Installed extract script to: ${BIN_DIR}/coder-archive-extract`,
);
expect(await isExecutable(id, `${BIN_DIR}/coder-archive-create`)).toBe(
true,
);
expect(await isExecutable(id, `${BIN_DIR}/coder-archive-extract`)).toBe(
true,
);
});
it("uses sane defaults: creates gzip archive at the default path and logs to stderr", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
// Keep defaults: compression=gzip, output_dir=/tmp, archive_name=coder-archive.
});
const { id } = await installArchive(state);
const createTestdata = await bashRun(
id,
`mkdir ~/gzip; touch ~/gzip/defaults.txt`,
);
ensureRunOk("create testdata", createTestdata);
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create`);
ensureRunOk("archive-create default run", run);
// Only the archive path should print to stdout.
expect(run.stdout.trim()).toEqual("/tmp/coder-archive.tar.gz");
expect(await fileExists(id, "/tmp/coder-archive.tar.gz")).toBe(true);
// Some useful diagnostics should be on stderr.
expect(run.stderr).toContain("Creating archive:");
expect(run.stderr).toContain("Compression: gzip");
const list = await listTar(id, "/tmp/coder-archive.tar.gz");
ensureRunOk("list default archive", list);
expect(list.stdout).toContain("gzip/defaults.txt");
}, 20000);
it("creates a gzip archive with explicit -f and includes extra CLI paths", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
// Provide a simple default path so we can assert contents.
paths: `["~/gzip"]`,
compression: "gzip",
});
const { id } = await installArchive(state);
const createTestdata = await bashRun(
id,
`mkdir ~/gzip; touch ~/gzip/test.txt; touch ~/gziptest.txt`,
);
ensureRunOk("create testdata", createTestdata);
const out = "/tmp/backup/test-archive.tar.gz";
const run = await bashRun(
id,
`${BIN_DIR}/coder-archive-create -f ${out} ~/gziptest.txt`,
);
ensureRunOk("archive-create gzip explicit -f", run);
expect(run.stdout.trim()).toEqual(out);
expect(await fileExists(id, out)).toBe(true);
const list = await sh(id, `tar -tzf ${out}`);
ensureRunOk("tar -tzf contents (gzip)", list);
expect(list.stdout).toContain("gzip/test.txt");
expect(list.stdout).toContain("gziptest.txt");
}, 20000);
it("creates a zstd-compressed archive when requested via CLI override", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
paths: `["/etc/hostname"]`,
// Module default is gzip, override at runtime to zstd.
});
const { id } = await installArchive(state);
const out = "/tmp/backup/zstd-archive.tar.zst";
const run = await bashRun(
id,
`${BIN_DIR}/coder-archive-create --compression zstd -f ${out}`,
);
ensureRunOk("archive-create zstd", run);
expect(run.stdout.trim()).toEqual(out);
// Check integrity via zstd and that tar can list it.
ensureRunOk("zstd -t", await sh(id, `test -f ${out} && zstd -t -q ${out}`));
ensureRunOk("tar --zstd -tf", await sh(id, `tar --zstd -tf ${out}`));
}, 30000);
it("creates an uncompressed tar when compression=none", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
// Keep module defaults but override at runtime.
});
const { id } = await installArchive(state);
const out = "/tmp/backup/raw-archive.tar";
const run = await bashRun(
id,
`${BIN_DIR}/coder-archive-create --compression none -f ${out}`,
);
ensureRunOk("archive-create none", run);
expect(run.stdout.trim()).toEqual(out);
ensureRunOk("tar -tf (none)", await sh(id, `tar -tf ${out} >/dev/null`));
}, 20000);
it("applies exclude patterns from Terraform", async () => {
// Include a file, but also exclude it via Terraform defaults to ensure
// exclusion flows through.
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
paths: `["/etc/hostname"]`,
exclude_patterns: `["/etc/hostname"]`,
});
const { id } = await installArchive(state);
const out = "/tmp/backup/excluded.tar.gz";
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create -f ${out}`);
ensureRunOk("archive-create with exclude_patterns", run);
const list = await sh(id, `tar -tzf ${out}`);
ensureRunOk("tar -tzf contents (exclude)", list);
expect(list.stdout).not.toContain("etc/hostname"); // Excluded by Terraform default.
}, 20000);
it("adds a run_on_stop script when enabled", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
create_on_stop: true,
});
const coderScripts = state.resources.filter(
(r) => r.type === "coder_script",
);
// Installer (run_on_start) + run_on_stop.
expect(coderScripts.length).toBe(2);
});
it("extracts a previously created archive into a target directory", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
paths: `["/etc/hostname"]`,
compression: "gzip",
});
const { id } = await installArchive(state);
// Create archive.
const out = "/tmp/backup/extract-test.tar.gz";
const created = await bashRun(
id,
`${BIN_DIR}/coder-archive-create -f ${out} /etc/hosts`,
);
ensureRunOk("create for extract", created);
// Extract archive.
const extractDir = "/tmp/extract";
const extract = await bashRun(
id,
`${BIN_DIR}/coder-archive-extract -f ${out} -C ${extractDir}`,
);
ensureRunOk("archive-extract", extract);
// Verify a known file exists after extraction.
const exists = await sh(
id,
`test -f ${extractDir}/etc/hosts && echo ok || echo no`,
);
expect(exists.stdout.trim()).toEqual("ok");
}, 20000);
it("honors Terraform defaults without CLI args (compression, name, output_dir)", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
compression: "zstd",
archive_name: "my-default",
output_dir: "/tmp/defout",
});
const { id } = await installArchive(state);
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create`);
ensureRunOk("archive-create terraform defaults", run);
expect(run.stdout.trim()).toEqual("/tmp/defout/my-default.tar.zst");
expect(run.stderr).toContain("Creating archive:");
expect(run.stderr).toContain("Compression: zstd");
ensureRunOk(
"zstd -t",
await sh(id, "zstd -t -q /tmp/defout/my-default.tar.zst"),
);
ensureRunOk(
"tar --zstd -tf",
await sh(id, "tar --zstd -tf /tmp/defout/my-default.tar.zst"),
);
}, 30000);
});
+134
View File
@@ -0,0 +1,134 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
}
}
}
variable "agent_id" {
description = "The ID of a Coder agent."
type = string
}
variable "paths" {
description = "List of files/directories to include in the archive. Defaults to the current directory."
type = list(string)
default = ["."]
}
variable "exclude_patterns" {
description = "Exclude patterns for the archive."
type = list(string)
default = []
}
variable "compression" {
description = "Compression algorithm for the archive. Supported: gzip, zstd, none."
type = string
default = "gzip"
validation {
condition = contains(["gzip", "zstd", "none"], var.compression)
error_message = "compression must be one of: gzip, zstd, none."
}
}
variable "archive_name" {
description = "Optional archive base name without extension. If empty, defaults to \"coder-archive\"."
type = string
default = "coder-archive"
}
variable "output_dir" {
description = "Optional output directory where the archive will be written. Defaults to \"/tmp\"."
type = string
default = "/tmp"
}
variable "directory" {
description = "Change current directory to this path before creating or extracting the archive. Defaults to the user's home directory."
type = string
default = "~"
}
variable "create_on_stop" {
description = "If true, also create a run_on_stop script that creates the archive automatically on workspace stop."
type = bool
default = false
}
variable "extract_on_start" {
description = "If true, the installer will wait for an archive and extract it on start."
type = bool
default = false
}
variable "extract_wait_timeout_seconds" {
description = "Timeout (seconds) to wait for an archive when extract_on_start is true."
type = number
default = 5
}
# Provide a stable script filename and sensible defaults.
locals {
extension = var.compression == "gzip" ? ".tar.gz" : var.compression == "zstd" ? ".tar.zst" : ".tar"
# Ensure ~ is expanded because it cannot be expanded inside quotes in a
# templated shell script.
paths = [for v in var.paths : replace(v, "/^~(\\/|$)/", "$$HOME$1")]
exclude_patterns = [for v in var.exclude_patterns : replace(v, "/^~(\\/|$)/", "$$HOME$1")]
directory = replace(var.directory, "/^~(\\/|$)/", "$$HOME$1")
output_dir = replace(var.output_dir, "/^~(\\/|$)/", "$$HOME$1")
archive_path = "${local.output_dir}/${var.archive_name}${local.extension}"
}
output "archive_path" {
description = "Full path to the archive file that will be created, extracted, or both."
value = local.archive_path
}
# This script installs the user-facing archive script into $CODER_SCRIPT_BIN_DIR.
# The installed script can be run manually by the user to create an archive.
resource "coder_script" "archive_start_script" {
agent_id = var.agent_id
display_name = "Archive"
icon = "/icon/folder.svg"
run_on_start = true
start_blocks_login = var.extract_on_start
# Render the user-facing archive script with Terraform defaults, then write it to $CODER_SCRIPT_BIN_DIR
script = templatefile("${path.module}/run.sh", {
TF_LIB_B64 = base64encode(file("${path.module}/scripts/archive-lib.sh")),
TF_PATHS = join(" ", formatlist("%q", local.paths)),
TF_EXCLUDE_PATTERNS = join(" ", formatlist("%q", local.exclude_patterns)),
TF_COMPRESSION = var.compression,
TF_ARCHIVE_PATH = local.archive_path,
TF_DIRECTORY = local.directory,
TF_EXTRACT_ON_START = var.extract_on_start,
TF_EXTRACT_WAIT_TIMEOUT = var.extract_wait_timeout_seconds,
})
}
# Optionally, also register a run_on_stop script that creates the archive automatically
# when the workspace stops. It simply invokes the installed archive script.
resource "coder_script" "archive_stop_script" {
count = var.create_on_stop ? 1 : 0
agent_id = var.agent_id
display_name = "Archive"
icon = "/icon/folder.svg"
run_on_stop = true
start_blocks_login = false
# Call the installed script. It will log to stderr and print the archive path to stdout.
# We redirect stdout to stderr to avoid surfacing the path in system logs if undesired.
# Remove the redirection if you want the path to appear in stdout on stop as well.
script = <<-EOT
#!/usr/bin/env bash
set -euo pipefail
"$CODER_SCRIPT_BIN_DIR/coder-archive-create"
EOT
}
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
LIB_B64="${TF_LIB_B64}"
EXTRACT_ON_START="${TF_EXTRACT_ON_START}"
EXTRACT_WAIT_TIMEOUT="${TF_EXTRACT_WAIT_TIMEOUT}"
# Set script defaults from Terraform.
DEFAULT_PATHS=(${TF_PATHS})
DEFAULT_EXCLUDE_PATTERNS=(${TF_EXCLUDE_PATTERNS})
DEFAULT_COMPRESSION="${TF_COMPRESSION}"
DEFAULT_ARCHIVE_PATH="${TF_ARCHIVE_PATH}"
DEFAULT_DIRECTORY="${TF_DIRECTORY}"
# 1) Decode the library into $CODER_SCRIPT_DATA_DIR/archive-lib.sh (static, sourceable).
LIB_PATH="$CODER_SCRIPT_DATA_DIR/archive-lib.sh"
lib_tmp="$(mktemp -t coder-module-archive.XXXXXX))"
trap 'rm -f "$lib_tmp" 2>/dev/null || true' EXIT
# Decode the base64 content safely.
if ! printf '%s' "$LIB_B64" | base64 -d > "$lib_tmp"; then
echo "ERROR: Failed to decode archive library from base64." >&2
exit 1
fi
chmod 0644 "$lib_tmp"
mv "$lib_tmp" "$LIB_PATH"
# 2) Generate the wrapper scripts (create and extract).
create_wrapper() {
tmp="$(mktemp -t coder-module-archive.XXXXXX)"
trap 'rm -f "$tmp" 2>/dev/null || true' EXIT
cat > "$tmp" << EOF
#!/usr/bin/env bash
set -euo pipefail
. "$LIB_PATH"
# Set defaults from Terraform (through installer).
$(
declare -p \
DEFAULT_PATHS \
DEFAULT_EXCLUDE_PATTERNS \
DEFAULT_COMPRESSION \
DEFAULT_ARCHIVE_PATH \
DEFAULT_DIRECTORY
)
$1 "\$@"
EOF
chmod 0755 "$tmp"
mv "$tmp" "$2"
}
CREATE_WRAPPER_PATH="$CODER_SCRIPT_BIN_DIR/coder-archive-create"
EXTRACT_WRAPPER_PATH="$CODER_SCRIPT_BIN_DIR/coder-archive-extract"
create_wrapper archive_create "$CREATE_WRAPPER_PATH"
create_wrapper archive_extract "$EXTRACT_WRAPPER_PATH"
echo "Installed archive library to: $LIB_PATH"
echo "Installed create script to: $CREATE_WRAPPER_PATH"
echo "Installed extract script to: $EXTRACT_WRAPPER_PATH"
# 3) Optionally wait for and extract an archive on start.
if [[ $EXTRACT_ON_START = true ]]; then
. "$LIB_PATH"
archive_wait_and_extract "$EXTRACT_WAIT_TIMEOUT" quiet || {
exit_code=$?
if [[ $exit_code -eq 2 ]]; then
echo "WARNING: Archive not found in backup path (this is expected with new workspaces)."
else
exit $exit_code
fi
}
fi
@@ -0,0 +1,279 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '%s\n' "$@" >&2
}
warn() {
printf 'WARNING: %s\n' "$1" >&2
}
error() {
printf 'ERROR: %s\n' "$1" >&2
exit 1
}
load_defaults() {
DEFAULT_PATHS=("${DEFAULT_PATHS[@]:-.}")
DEFAULT_EXCLUDE_PATTERNS=("${DEFAULT_EXCLUDE_PATTERNS[@]:-}")
DEFAULT_COMPRESSION="${DEFAULT_COMPRESSION:-gzip}"
DEFAULT_ARCHIVE_PATH="${DEFAULT_ARCHIVE_PATH:-/tmp/coder-archive.tar.gz}"
DEFAULT_DIRECTORY="${DEFAULT_DIRECTORY:-$HOME}"
}
ensure_tools() {
command -v tar > /dev/null 2>&1 || error "tar is required"
case "$1" in
gzip)
command -v gzip > /dev/null 2>&1 || error "gzip is required for gzip compression"
;;
zstd)
command -v zstd > /dev/null 2>&1 || error "zstd is required for zstd compression"
;;
none) ;;
*)
error "Unsupported compression algorithm: $1"
;;
esac
}
usage_archive_create() {
load_defaults
cat >&2 << USAGE
Usage: coder-archive-create [OPTIONS] [[PATHS] ...]
Options:
-c, --compression <gzip|zstd|none> Compression algorithm (default "${DEFAULT_COMPRESSION}")
-C, --directory <DIRECTORY> Change to directory (default "${DEFAULT_DIRECTORY}")
-f, --file <ARCHIVE> Output archive file (default "${DEFAULT_ARCHIVE_PATH}")
-h, --help Show this help
USAGE
}
archive_create() {
load_defaults
local compression="${DEFAULT_COMPRESSION}"
local directory="${DEFAULT_DIRECTORY}"
local file="${DEFAULT_ARCHIVE_PATH}"
local paths=("${DEFAULT_PATHS[@]}")
while [[ $# -gt 0 ]]; do
case "$1" in
-c | --compression)
if [[ $# -lt 2 ]]; then
usage_archive_create
error "Missing value for $1"
fi
compression="$2"
shift 2
;;
-C | --directory)
if [[ $# -lt 2 ]]; then
usage_archive_create
error "Missing value for $1"
fi
directory="$2"
shift 2
;;
-f | --file)
if [[ $# -lt 2 ]]; then
usage_archive_create
error "Missing value for $1"
fi
file="$2"
shift 2
;;
-h | --help)
usage_archive_create
exit 0
;;
--)
shift
while [[ $# -gt 0 ]]; do
paths+=("$1")
shift
done
;;
-*)
usage_archive_create
error "Unknown option: $1"
;;
*)
paths+=("$1")
shift
;;
esac
done
ensure_tools "$compression"
local -a tar_opts=(-c -f "$file" -C "$directory")
case "$compression" in
gzip)
tar_opts+=(-z)
;;
zstd)
tar_opts+=(--zstd)
;;
none) ;;
*)
error "Unsupported compression algorithm: $compression"
;;
esac
for path in "${DEFAULT_EXCLUDE_PATTERNS[@]}"; do
if [[ -n $path ]]; then
tar_opts+=(--exclude "$path")
fi
done
# Ensure destination directory exists.
dest="$(dirname "$file")"
mkdir -p "$dest" 2> /dev/null || error "Failed to create output dir: $dest"
log "Creating archive:"
log " Compression: $compression"
log " Directory: $directory"
log " Archive: $file"
log " Paths: ${paths[*]}"
log " Exclude: ${DEFAULT_EXCLUDE_PATTERNS[*]}"
umask 077
tar "${tar_opts[@]}" "${paths[@]}"
printf '%s\n' "$file"
}
usage_archive_extract() {
load_defaults
cat >&2 << USAGE
Usage: coder-archive-extract [OPTIONS]
Options:
-c, --compression <gzip|zstd|none> Compression algorithm (default "${DEFAULT_COMPRESSION}")
-C, --directory <DIRECTORY> Change to directory (default "${DEFAULT_DIRECTORY}")
-f, --file <ARCHIVE> Output archive file (default "${DEFAULT_ARCHIVE_PATH}")
-h, --help Show this help
USAGE
}
archive_extract() {
load_defaults
local compression="${DEFAULT_COMPRESSION}"
local directory="${DEFAULT_DIRECTORY}"
local file="${DEFAULT_ARCHIVE_PATH}"
while [[ $# -gt 0 ]]; do
case "$1" in
-c | --compression)
if [[ $# -lt 2 ]]; then
usage_archive_extract
error "Missing value for $1"
fi
compression="$2"
shift 2
;;
-C | --directory)
if [[ $# -lt 2 ]]; then
usage_archive_extract
error "Missing value for $1"
fi
directory="$2"
shift 2
;;
-f | --file)
if [[ $# -lt 2 ]]; then
usage_archive_extract
error "Missing value for $1"
fi
file="$2"
shift 2
;;
-h | --help)
usage_archive_extract
exit 0
;;
--)
shift
while [[ $# -gt 0 ]]; do
shift
done
;;
-*)
usage_archive_extract
error "Unknown option: $1"
;;
*)
shift
;;
esac
done
ensure_tools "$compression"
local -a tar_opts=(-x -f "$file" -C "$directory")
case "$compression" in
gzip)
tar_opts+=(-z)
;;
zstd)
tar_opts+=(--zstd)
;;
none) ;;
*)
error "Unsupported compression algorithm: $compression"
;;
esac
for path in "${DEFAULT_EXCLUDE_PATTERNS[@]}"; do
if [[ -n $path ]]; then
tar_opts+=(--exclude "$path")
fi
done
# Ensure destination directory exists.
mkdir -p "$directory" || error "Failed to create directory: $directory"
log "Extracting archive:"
log " Compression: $compression"
log " Directory: $directory"
log " Archive: $file"
log " Exclude: ${DEFAULT_EXCLUDE_PATTERNS[*]}"
umask 077
tar "${tar_opts[@]}" "${paths[@]}"
printf 'Extracted %s into %s\n' "$file" "$directory"
}
archive_wait_and_extract() {
load_defaults
local timeout="${1:-300}"
local quiet="${2:-}"
local file="${DEFAULT_ARCHIVE_PATH}"
local start now
start=$(date +%s)
while true; do
if [[ -f "$file" ]]; then
archive_extract -f "$file"
return 0
fi
if ((timeout <= 0)); then
break
fi
now=$(date +%s)
if ((now - start >= timeout)); then
break
fi
sleep 5
done
if [[ -z $quiet ]]; then
printf 'ERROR: Timed out waiting for archive: %s\n' "$file" >&2
fi
return 2
}
@@ -0,0 +1,161 @@
---
display_name: Auggie CLI
icon: ../../../../.icons/auggie.svg
description: Run Auggie CLI in your workspace for AI-powered coding assistance with AgentAPI integration
verified: true
tags: [agent, auggie, ai, tasks, augment]
---
# Auggie CLI
Run Auggie CLI in your workspace to access Augment's AI coding assistant with advanced context understanding and codebase integration. This module integrates with [AgentAPI](https://github.com/coder/agentapi).
```tf
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
```
## Prerequisites
- **Node.js and npm must be sourced/available before the auggie module installs** - ensure they are installed in your workspace image or via earlier provisioning steps
- You must add the [Coder Login](https://registry.coder.com/modules/coder/coder-login) module to your template
- **Augment session token for authentication (required for tasks). [Instructions](https://docs.augmentcode.com/cli/setup-auggie/authentication) to get the session token**
## Examples
### Usage with Tasks and Configuration
```tf
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Initial task prompt for Auggie CLI"
mutable = true
}
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/coder-login/coder"
version = "1.0.31"
agent_id = coder_agent.example.id
}
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
# Authentication
augment_session_token = <<-EOF
{"accessToken":"xxxx-yyyy-zzzz-jjjj","tenantURL":"https://d1.api.augmentcode.com/","scopes":["read","write"]}
EOF # Required for tasks
# Version
auggie_version = "0.3.0"
# Task configuration
ai_prompt = data.coder_parameter.ai_prompt.value
continue_previous_conversation = true
interaction_mode = "quiet"
auggie_model = "gpt5"
report_tasks = true
# MCP configuration for additional integrations
mcp = <<-EOF
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/coder/project"]
}
}
}
EOF
# Workspace guidelines
rules = <<-EOT
# Project Guidelines
## Code Style
- Use TypeScript for all new JavaScript files
- Follow consistent naming conventions
- Add comprehensive comments for complex logic
## Testing
- Write unit tests for all new functions
- Ensure test coverage above 80%
## Documentation
- Update README.md for any new features
- Document API changes in CHANGELOG.md
EOT
}
```
### Using Multiple MCP Configuration Files
```tf
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
# Multiple MCP configuration files
mcp_files = [
"/path/to/filesystem-mcp.json",
"/path/to/database-mcp.json",
"/path/to/api-mcp.json"
]
mcp = <<-EOF
{
"mcpServers": {
"Test MCP": {
"command": "uv",
"args": [
"--directory",
"/home/coder/test-mcp",
"run",
"server.py"
],
"timeout": 600
}
}
}
EOF
}
```
### Troubleshooting
If you have any issues, please take a look at the log files below.
```bash
# Installation logs
cat ~/.auggie-module/install.log
# Startup logs
cat ~/.auggie-module/agentapi-start.log
# Pre/post install script logs
cat ~/.auggie-module/pre_install.log
cat ~/.auggie-module/post_install.log
```
> [!NOTE]
> To use tasks with Auggie CLI, create a `coder_parameter` named `"AI Prompt"` and pass its value to the auggie module's `ai_prompt` variable. The `folder` variable is required for the module to function correctly.
## References
- [Auggie CLI Reference](https://docs.augmentcode.com/cli/reference)
- [Auggie CLI MCP Integration](https://docs.augmentcode.com/cli/integrations#mcp-integrations)
- [Augment Code Documentation](https://docs.augmentcode.com/)
- [AgentAPI Documentation](https://github.com/coder/agentapi)
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
@@ -0,0 +1,186 @@
run "test_auggie_basic" {
command = plan
variables {
agent_id = "test-agent-123"
folder = "/home/coder/projects"
}
assert {
condition = coder_env.auggie_session_auth.name == "AUGMENT_SESSION_AUTH"
error_message = "Auggie session auth environment variable should be set correctly"
}
assert {
condition = var.folder == "/home/coder/projects"
error_message = "Folder variable should be set correctly"
}
assert {
condition = var.agent_id == "test-agent-123"
error_message = "Agent ID variable should be set correctly"
}
assert {
condition = var.install_auggie == true
error_message = "Install auggie should default to true"
}
assert {
condition = var.install_agentapi == true
error_message = "Install agentapi should default to true"
}
}
run "test_auggie_with_session_token" {
command = plan
variables {
agent_id = "test-agent-456"
folder = "/home/coder/workspace"
augment_session_token = "test-session-token-123"
}
assert {
condition = coder_env.auggie_session_auth.value == "test-session-token-123"
error_message = "Auggie session token value should match the input"
}
}
run "test_auggie_with_custom_options" {
command = plan
variables {
agent_id = "test-agent-789"
folder = "/home/coder/custom"
order = 5
group = "development"
icon = "/icon/custom.svg"
auggie_model = "gpt-4"
ai_prompt = "Help me write better code"
interaction_mode = "compact"
continue_previous_conversation = true
install_auggie = false
install_agentapi = false
auggie_version = "1.0.0"
agentapi_version = "v0.6.0"
}
assert {
condition = var.order == 5
error_message = "Order variable should be set to 5"
}
assert {
condition = var.group == "development"
error_message = "Group variable should be set to 'development'"
}
assert {
condition = var.icon == "/icon/custom.svg"
error_message = "Icon variable should be set to custom icon"
}
assert {
condition = var.auggie_model == "gpt-4"
error_message = "Auggie model variable should be set to 'gpt-4'"
}
assert {
condition = var.ai_prompt == "Help me write better code"
error_message = "AI prompt variable should be set correctly"
}
assert {
condition = var.interaction_mode == "compact"
error_message = "Interaction mode should be set to 'compact'"
}
assert {
condition = var.continue_previous_conversation == true
error_message = "Continue previous conversation should be set to true"
}
assert {
condition = var.auggie_version == "1.0.0"
error_message = "Auggie version should be set to '1.0.0'"
}
assert {
condition = var.agentapi_version == "v0.6.0"
error_message = "AgentAPI version should be set to 'v0.6.0'"
}
}
run "test_auggie_with_mcp_and_rules" {
command = plan
variables {
agent_id = "test-agent-mcp"
folder = "/home/coder/mcp-test"
mcp = jsonencode({
mcpServers = {
test = {
command = "test-server"
args = ["--config", "test.json"]
}
}
})
mcp_files = [
"/path/to/mcp1.json",
"/path/to/mcp2.json"
]
rules = "# General coding rules\n- Write clean code\n- Add comments"
}
assert {
condition = var.mcp != ""
error_message = "MCP configuration should be provided"
}
assert {
condition = length(var.mcp_files) == 2
error_message = "Should have 2 MCP files"
}
assert {
condition = var.rules != ""
error_message = "Rules should be provided"
}
}
run "test_auggie_with_scripts" {
command = plan
variables {
agent_id = "test-agent-scripts"
folder = "/home/coder/scripts"
pre_install_script = "echo 'Pre-install script'"
post_install_script = "echo 'Post-install script'"
}
assert {
condition = var.pre_install_script == "echo 'Pre-install script'"
error_message = "Pre-install script should be set correctly"
}
assert {
condition = var.post_install_script == "echo 'Post-install script'"
error_message = "Post-install script should be set correctly"
}
}
run "test_auggie_interaction_mode_validation" {
command = plan
variables {
agent_id = "test-agent-validation"
folder = "/home/coder/test"
interaction_mode = "print"
}
assert {
condition = contains(["interactive", "print", "quiet", "compact"], var.interaction_mode)
error_message = "Interaction mode should be one of the valid options"
}
}
@@ -0,0 +1,348 @@
import {
test,
afterEach,
describe,
setDefaultTimeout,
beforeAll,
expect,
} from "bun:test";
import { execContainer, readFileContainer, runTerraformInit } from "~test";
import {
loadTestFile,
writeExecutable,
setup as setupUtil,
execModuleScript,
expectAgentAPIStarted,
} from "../../../coder/modules/agentapi/test-util";
import dedent from "dedent";
let cleanupFunctions: (() => Promise<void>)[] = [];
const registerCleanup = (cleanup: () => Promise<void>) => {
cleanupFunctions.push(cleanup);
};
afterEach(async () => {
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
cleanupFunctions = [];
for (const cleanup of cleanupFnsCopy) {
try {
await cleanup();
} catch (error) {
console.error("Error during cleanup:", error);
}
}
});
interface SetupProps {
skipAgentAPIMock?: boolean;
skipAuggieMock?: boolean;
moduleVariables?: Record<string, string>;
agentapiMockScript?: string;
}
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const projectDir = "/home/coder/project";
const { id } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
install_auggie: props?.skipAuggieMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
folder: projectDir,
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
if (!props?.skipAuggieMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/auggie",
content: await loadTestFile(import.meta.dir, "auggie-mock.sh"),
});
}
return { id };
};
setDefaultTimeout(60 * 1000);
describe("auggie", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
test("happy-path", async () => {
const { id } = await setup();
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("install-auggie-version", async () => {
const version_to_install = "0.3.0";
const { id } = await setup({
skipAuggieMock: true,
moduleVariables: {
install_auggie: "true",
auggie_version: version_to_install,
pre_install_script: dedent`
#!/usr/bin/env bash
set -euo pipefail
# Install Node.js and npm via system package manager
if ! command -v node >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y nodejs npm
fi
# Configure npm to use user directory (avoids permission issues)
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
# Persist npm user directory configuration
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
echo "prefix=$HOME/.npm-global" > ~/.npmrc
`,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`cat /home/coder/.auggie-module/install.log`,
]);
expect(resp.stdout).toContain(version_to_install);
});
test("check-latest-auggie-version-works", async () => {
const { id } = await setup({
skipAuggieMock: true,
skipAgentAPIMock: true,
moduleVariables: {
install_auggie: "true",
pre_install_script: dedent`
#!/usr/bin/env bash
set -euo pipefail
# Install Node.js and npm via system package manager
if ! command -v node >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y nodejs npm
fi
# Configure npm to use user directory (avoids permission issues)
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
# Persist npm user directory configuration
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
echo "prefix=$HOME/.npm-global" > ~/.npmrc
`,
},
});
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("auggie-session-token", async () => {
const sessionToken = "test-session-token-123";
const { id } = await setup({
moduleVariables: {
augment_session_token: sessionToken,
},
});
await execModuleScript(id);
const envCheck = await execContainer(id, [
"bash",
"-c",
`env | grep AUGMENT_SESSION_AUTH || echo "AUGMENT_SESSION_AUTH not found"`,
]);
expect(envCheck.stdout).toContain("AUGMENT_SESSION_AUTH");
});
test("auggie-mcp-config", async () => {
const mcpConfig = JSON.stringify({
mcpServers: {
test: {
command: "test-cmd",
type: "stdio",
},
},
});
const { id } = await setup({
moduleVariables: {
mcp: mcpConfig,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.auggie-module/agentapi-start.log",
);
expect(resp).toContain("--mcp-config");
});
test("auggie-rules", async () => {
const rules = "Always use TypeScript for new files";
const { id } = await setup({
moduleVariables: {
install_auggie: "false", // Don't need to install auggie to test rules file creation
rules: rules,
},
});
await execModuleScript(id);
const rulesFile = await readFileContainer(
id,
"/home/coder/.augment/rules.md",
);
expect(rulesFile).toContain(rules);
});
test("auggie-ai-task-prompt", async () => {
const prompt = "This is a task prompt for Auggie.";
const { id } = await setup({
moduleVariables: {
ai_prompt: prompt,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`cat /home/coder/.auggie-module/agentapi-start.log`,
]);
expect(resp.stdout).toContain(prompt);
});
test("auggie-interaction-mode", async () => {
const mode = "compact";
const { id } = await setup({
moduleVariables: {
interaction_mode: mode,
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.auggie-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain(`--${mode}`);
});
test("auggie-model", async () => {
const model = "gpt-4";
const { id } = await setup({
moduleVariables: {
auggie_model: model,
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.auggie-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain(`--model ${model}`);
});
test("auggie-continue-previous-conversation", async () => {
const { id } = await setup({
moduleVariables: {
continue_previous_conversation: "true",
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.auggie-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain("--continue");
});
test("pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'auggie-pre-install-script'",
post_install_script: "#!/bin/bash\necho 'auggie-post-install-script'",
},
});
await execModuleScript(id);
const preInstallLog = await readFileContainer(
id,
"/home/coder/.auggie-module/pre_install.log",
);
expect(preInstallLog).toContain("auggie-pre-install-script");
const postInstallLog = await readFileContainer(
id,
"/home/coder/.auggie-module/post_install.log",
);
expect(postInstallLog).toContain("auggie-post-install-script");
});
test("folder-variable", async () => {
const folder = "/home/coder/auggie-test-folder";
const { id } = await setup({
skipAuggieMock: false,
moduleVariables: {
folder,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.auggie-module/agentapi-start.log",
);
expect(resp).toContain(folder);
});
test("coder-mcp-config-created", async () => {
const { id } = await setup({
moduleVariables: {
install_auggie: "false", // Don't need to install auggie to test MCP config creation
},
});
await execModuleScript(id);
const mcpConfig = await readFileContainer(
id,
"/home/coder/.augment/coder_mcp.json",
);
expect(mcpConfig).toContain("mcpServers");
expect(mcpConfig).toContain("coder");
expect(mcpConfig).toContain("CODER_MCP_APP_STATUS_SLUG");
expect(mcpConfig).toContain("CODER_MCP_AI_AGENTAPI_URL");
});
test("mcp-files-array", async () => {
const mcpFiles = ["/path/to/mcp1.json", "/path/to/mcp2.json"];
const { id } = await setup({
moduleVariables: {
mcp_files: JSON.stringify(mcpFiles),
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.auggie-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain("mcp1.json");
expect(startLog.stdout).toContain("mcp2.json");
});
});
+232
View File
@@ -0,0 +1,232 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/auggie.svg"
}
variable "folder" {
type = string
description = "The folder to run Auggie in."
}
variable "install_auggie" {
type = bool
description = "Whether to install Auggie CLI."
default = true
}
variable "auggie_version" {
type = string
description = "The version of Auggie to install."
default = "" # empty string means the latest available version
validation {
condition = var.auggie_version == "" || can(regex("^v?[0-9]+\\.[0-9]+\\.[0-9]+", var.auggie_version))
error_message = "auggie_version must be empty (for latest) or a valid semantic version like 'v1.2.3' or '1.2.3'."
}
}
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"
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'."
}
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing Auggie."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing Auggie."
default = null
}
# ----------------------------------------------
variable "ai_prompt" {
type = string
description = "Task prompt for the Auggie CLI"
default = ""
}
variable "mcp" {
type = string
description = "MCP configuration as a JSON string for the auggie cli, check https://docs.augmentcode.com/cli/integrations#mcp-integrations"
default = ""
}
variable "mcp_files" {
type = list(string)
description = "MCP configuration from a JSON file for the auggie cli, check https://docs.augmentcode.com/cli/integrations#mcp-integrations"
default = []
}
variable "rules" {
type = string
description = "Additional rules to append to workspace guidelines (markdown format)"
default = ""
}
variable "continue_previous_conversation" {
type = bool
description = "Whether to resume the previous conversation."
default = false
}
variable "interaction_mode" {
type = string
description = "Interaction mode with the Auggie CLI. Options: interactive, print, quiet, compact. https://docs.augmentcode.com/cli/reference#cli-flags"
default = "interactive"
validation {
condition = contains(["interactive", "print", "quiet", "compact"], var.interaction_mode)
error_message = "interaction_mode must be one of: interactive, print, quiet, compact."
}
}
variable "augment_session_token" {
type = string
description = "Auggie session token for authentication. https://docs.augmentcode.com/cli/setup-auggie/authentication"
default = ""
}
variable "auggie_model" {
type = string
description = "The model to use for Auggie, find available models using auggie --list-models"
default = ""
}
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI via AgentAPI"
default = false
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for Auggie"
default = false
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app"
default = "Auggie"
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app"
default = "Auggie CLI"
}
resource "coder_env" "auggie_session_auth" {
agent_id = var.agent_id
name = "AUGMENT_SESSION_AUTH"
value = var.augment_session_token
}
locals {
app_slug = "auggie"
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.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
web_app_icon = var.icon
web_app_display_name = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
ARG_AUGGIE_START_DIRECTORY='${var.folder}' \
ARG_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_MCP_FILES='${jsonencode(var.mcp_files)}' \
ARG_AUGGIE_RULES='${base64encode(var.rules)}' \
ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION='${var.continue_previous_conversation}' \
ARG_AUGGIE_INTERACTION_MODE='${var.interaction_mode}' \
ARG_AUGMENT_SESSION_AUTH='${var.augment_session_token}' \
ARG_AUGGIE_MODEL='${var.auggie_model}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
/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_AUGGIE_INSTALL='${var.install_auggie}' \
ARG_AUGGIE_VERSION='${var.auggie_version}' \
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_AUGGIE_RULES='${base64encode(var.rules)}' \
ARG_MCP_CONFIG='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \
/tmp/install.sh
EOT
}
@@ -0,0 +1,125 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
BOLD='\033[0;1m'
# Function to check if a command exists
command_exists() {
command -v "$1" > /dev/null 2>&1
}
ARG_AUGGIE_INSTALL=${ARG_AUGGIE_INSTALL:-true}
ARG_AUGGIE_VERSION=${ARG_AUGGIE_VERSION:-}
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
ARG_AUGGIE_RULES=$(echo -n "${ARG_AUGGIE_RULES:-}" | base64 -d)
ARG_MCP_CONFIG=${ARG_MCP_CONFIG:-}
echo "--------------------------------"
printf "install auggie: %s\n" "$ARG_AUGGIE_INSTALL"
printf "auggie_version: %s\n" "$ARG_AUGGIE_VERSION"
printf "app_slug: %s\n" "$ARG_MCP_APP_STATUS_SLUG"
printf "rules: %s\n" "$ARG_AUGGIE_RULES"
echo "--------------------------------"
function check_dependencies() {
if ! command_exists node; then
printf "Error: Node.js is not installed. Please install Node.js manually or use the pre_install_script to install it.\n"
exit 1
fi
if ! command_exists npm; then
printf "Error: npm is not installed. Please install npm manually or use the pre_install_script to install it.\n"
exit 1
fi
printf "Node.js version: %s\n" "$(node --version)"
printf "npm version: %s\n" "$(npm --version)"
}
function install_auggie() {
if [ "${ARG_AUGGIE_INSTALL}" = "true" ]; then
check_dependencies
printf "%s Installing Auggie CLI\n" "${BOLD}"
NPM_GLOBAL_PREFIX="${HOME}/.npm-global"
if [ ! -d "$NPM_GLOBAL_PREFIX" ]; then
mkdir -p "$NPM_GLOBAL_PREFIX"
fi
npm config set prefix "$NPM_GLOBAL_PREFIX"
export PATH="$NPM_GLOBAL_PREFIX/bin:$PATH"
if [ -n "$ARG_AUGGIE_VERSION" ]; then
npm install -g "@augmentcode/auggie@$ARG_AUGGIE_VERSION"
else
npm install -g "@augmentcode/auggie"
fi
if ! grep -q "export PATH=\"\$HOME/.npm-global/bin:\$PATH\"" "$HOME/.bashrc"; then
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> "$HOME/.bashrc"
fi
printf "%s Successfully installed Auggie CLI. Version: %s\n" "${BOLD}" "$(auggie --version)"
else
printf "Skipping Auggie CLI installation (install_auggie=false)\n"
fi
}
function create_coder_mcp() {
AUGGIE_CODER_MCP_FILE="$HOME/.augment/coder_mcp.json"
CODER_MCP=$(
cat << EOF
{
"mcpServers":{
"coder": {
"args": ["exp", "mcp", "server"],
"command": "coder",
"env": {
"CODER_MCP_APP_STATUS_SLUG": "${ARG_MCP_APP_STATUS_SLUG}",
"CODER_MCP_AI_AGENTAPI_URL": "http://localhost:3284",
"CODER_AGENT_URL": "${CODER_AGENT_URL:-}",
"CODER_AGENT_TOKEN": "${CODER_AGENT_TOKEN:-}"
}
}
}
}
EOF
)
mkdir -p "$(dirname "$AUGGIE_CODER_MCP_FILE")"
echo "$CODER_MCP" > "$AUGGIE_CODER_MCP_FILE"
printf "Coder MCP config created at: %s\n" "$AUGGIE_CODER_MCP_FILE"
}
function create_user_mcp() {
if [ -n "$ARG_MCP_CONFIG" ]; then
USER_MCP_CONFIG_FILE="$HOME/.augment/user_mcp.json"
USER_MCP_CONTENT=$(echo -n "$ARG_MCP_CONFIG" | base64 -d)
mkdir -p "$(dirname "$USER_MCP_CONFIG_FILE")"
echo "$USER_MCP_CONTENT" > "$USER_MCP_CONFIG_FILE"
printf "User MCP config created at: %s\n" "$USER_MCP_CONFIG_FILE"
else
printf "No user MCP config provided, skipping user MCP config creation.\n"
fi
}
function create_rules_file() {
AUGGIE_RULES_FILE="$HOME/.augment/rules.md"
if [ -n "$ARG_AUGGIE_RULES" ]; then
mkdir -p "$(dirname "$AUGGIE_RULES_FILE")"
echo -n "$ARG_AUGGIE_RULES" > "$AUGGIE_RULES_FILE"
printf "Rules file created at: %s\n" "$AUGGIE_RULES_FILE"
else
printf "No rules provided, skipping rules file creation.\n"
fi
}
install_auggie
create_coder_mcp
create_user_mcp
create_rules_file
@@ -0,0 +1,103 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
command_exists() {
command -v "$1" > /dev/null 2>&1
}
if [ -f "$HOME/.nvm/nvm.sh" ]; then
source "$HOME"/.nvm/nvm.sh
else
export PATH="$HOME/.npm-global/bin:$PATH"
fi
ARG_AUGGIE_START_DIRECTORY=${ARG_AUGGIE_START_DIRECTORY:-"$HOME"}
ARG_TASK_PROMPT=$(echo -n "${ARG_TASK_PROMPT:-}" | base64 -d)
ARG_MCP_FILES=${ARG_MCP_FILES:-[]}
ARG_AUGGIE_RULES=${ARG_AUGGIE_RULES:-}
ARG_AUGMENT_SESSION_AUTH=${ARG_AUGMENT_SESSION_AUTH:-}
ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION=${ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION:-false}
ARG_AUGGIE_INTERACTION_MODE=${ARG_AUGGIE_INTERACTION_MODE:-"interactive"}
ARG_AUGGIE_MODEL=${ARG_AUGGIE_MODEL:-}
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-false}
ARGS=()
echo "--------------------------------"
printf "auggie_start_directory: %s\n" "$ARG_AUGGIE_START_DIRECTORY"
printf "task_prompt: %s\n" "$ARG_TASK_PROMPT"
printf "mcp_files: %s\n" "$ARG_MCP_FILES"
printf "auggie_rules: %s\n" "$ARG_AUGGIE_RULES"
printf "continue_previous_conversation: %s\n" "$ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION"
printf "auggie_interaction_mode: %s\n" "$ARG_AUGGIE_INTERACTION_MODE"
printf "augment_session_auth: %s\n" "$ARG_AUGMENT_SESSION_AUTH"
printf "auggie_model: %s\n" "$ARG_AUGGIE_MODEL"
printf "report_tasks: %s\n" "$ARG_REPORT_TASKS"
echo "--------------------------------"
function validate_auggie_installation() {
if command_exists auggie; then
printf "Auggie is installed\n"
else
printf "Error: Auggie is not installed. Please enable install_auggie or install it manually\n"
exit 1
fi
}
function build_auggie_args() {
if [ -n "$ARG_AUGGIE_INTERACTION_MODE" ]; then
if [ "$ARG_AUGGIE_INTERACTION_MODE" != "interactive" ]; then
ARGS+=(--"$ARG_AUGGIE_INTERACTION_MODE")
fi
fi
if [ -n "$ARG_AUGGIE_MODEL" ]; then
ARGS+=(--model "$ARG_AUGGIE_MODEL")
fi
if [ -f "$HOME/.augment/user_mcp.json" ]; then
ARGS+=(--mcp-config "$HOME/.augment/user_mcp.json")
fi
if [ -n "$ARG_MCP_FILES" ] && [ "$ARG_MCP_FILES" != "[]" ]; then
for file in $(echo "$ARG_MCP_FILES" | jq -r '.[]'); do
ARGS+=(--mcp-config "$file")
done
fi
ARGS+=(--mcp-config "$HOME/.augment/coder_mcp.json")
if [ -n "$ARG_AUGGIE_RULES" ]; then
AUGGIE_RULES_FILE="$HOME/.augment/rules.md"
ARGS+=(--rules "$AUGGIE_RULES_FILE")
fi
if [ "$ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION" == "true" ]; then
ARGS+=(--continue)
fi
if [ -n "$ARG_TASK_PROMPT" ]; then
if [ "$ARG_REPORT_TASKS" == "true" ]; then
PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_TASK_PROMPT"
else
PROMPT="$ARG_TASK_PROMPT"
fi
ARGS+=(--instruction "$PROMPT")
fi
}
function start_agentapi_server() {
mkdir -p "$ARG_AUGGIE_START_DIRECTORY"
cd "$ARG_AUGGIE_START_DIRECTORY"
ARGS+=(--workspace-root "$ARG_AUGGIE_START_DIRECTORY")
printf "Running auggie with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
agentapi server --term-width 67 --term-height 1190 -- auggie "${ARGS[@]}"
}
validate_auggie_installation
build_auggie_args
start_agentapi_server
@@ -0,0 +1,14 @@
#!/bin/bash
if [[ "$1" == "--version" ]]; then
echo "HELLO: $(bash -c env)"
echo "auggie version v1.0.0"
exit 0
fi
set -e
while true; do
echo "$(date) - auggie-mock"
sleep 15
done
+148
View File
@@ -0,0 +1,148 @@
---
display_name: Codex CLI
icon: ../../../../.icons/openai.svg
description: Run Codex CLI in your workspace with AgentAPI integration
verified: true
tags: [agent, codex, ai, openai, tasks]
---
# Codex CLI
Run Codex CLI in your workspace to access OpenAI's models through the Codex interface, with custom pre/post install scripts. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for Coder Tasks compatibility.
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "3.1.0"
agent_id = coder_agent.example.id
openai_api_key = var.openai_api_key
workdir = "/home/coder/project"
}
```
## Prerequisites
- You must add the [Coder Login](https://registry.coder.com/modules/coder/coder-login) module to your template
- OpenAI API key for Codex access
## Examples
### Run standalone
```tf
module "codex" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/codex/coder"
version = "3.1.0"
agent_id = coder_agent.example.id
openai_api_key = "..."
workdir = "/home/coder/project"
report_tasks = false
}
```
### Tasks integration
```tf
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Initial prompt for the Codex CLI"
mutable = true
}
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/coder-login/coder"
version = "1.0.31"
agent_id = coder_agent.example.id
}
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "3.1.0"
agent_id = coder_agent.example.id
openai_api_key = "..."
ai_prompt = data.coder_parameter.ai_prompt.value
workdir = "/home/coder/project"
# Custom configuration for full auto mode
base_config_toml = <<-EOT
approval_policy = "never"
preferred_auth_method = "apikey"
EOT
}
```
> [!WARNING]
> This module configures Codex with a `workspace-write` sandbox that allows AI tasks to read/write files in the specified workdir. While the sandbox provides security boundaries, Codex can still modify files within the workspace. Use this module _only_ in trusted environments and be aware of the security implications.
## How it Works
- **Install**: The module installs Codex CLI and sets up the environment
- **System Prompt**: If `codex_system_prompt` is set, writes the prompt to `AGENTS.md` in the `~/.codex/` directory
- **Start**: Launches Codex CLI in the specified directory, wrapped by AgentAPI
- **Configuration**: Sets `OPENAI_API_KEY` environment variable and passes `--model` flag to Codex CLI (if variables provided)
- **Session Continuity**: When `continue = true` (default), the module automatically tracks task sessions in `~/.codex-module/.codex-task-session`. On workspace restart, it resumes the existing session with full conversation history. Set `continue = false` to always start fresh sessions.
## Configuration
### Default Configuration
When no custom `base_config_toml` is provided, the module uses these secure defaults:
```toml
sandbox_mode = "workspace-write"
approval_policy = "never"
preferred_auth_method = "apikey"
[sandbox_workspace_write]
network_access = true
```
### Custom Configuration
For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_servers`:
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "3.1.0"
# ... other variables ...
# Override default configuration
base_config_toml = <<-EOT
sandbox_mode = "danger-full-access"
approval_policy = "never"
preferred_auth_method = "apikey"
EOT
# Add extra MCP servers
additional_mcp_servers = <<-EOT
[mcp_servers.GitHub]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
type = "stdio"
EOT
}
```
> [!NOTE]
> If no custom configuration is provided, the module uses secure defaults. The Coder MCP server is always included automatically. For containerized workspaces (Docker/Kubernetes), you may need `sandbox_mode = "danger-full-access"` to avoid permission issues. For advanced options, see [Codex config docs](https://github.com/openai/codex/blob/main/codex-rs/config.md).
## Troubleshooting
- Check installation and startup logs in `~/.codex-module/`
- Ensure your OpenAI API key has access to the specified model
> [!IMPORTANT]
> To use tasks with Codex CLI, ensure you have the `openai_api_key` variable set, and **you create a `coder_parameter` named `"AI Prompt"` and pass its value to the codex module's `ai_prompt` variable**. [Tasks Template Example](https://registry.coder.com/templates/coder-labs/tasks-docker).
> The module automatically configures Codex with your API key and model preferences.
> workdir is a required variable for the module to function correctly.
## References
- [Codex CLI Documentation](https://github.com/openai/codex)
- [AgentAPI Documentation](https://github.com/coder/agentapi)
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
@@ -0,0 +1,457 @@
import {
test,
afterEach,
describe,
setDefaultTimeout,
beforeAll,
expect,
} from "bun:test";
import { execContainer, readFileContainer, runTerraformInit } from "~test";
import {
loadTestFile,
writeExecutable,
setup as setupUtil,
execModuleScript,
expectAgentAPIStarted,
} from "../../../coder/modules/agentapi/test-util";
import dedent from "dedent";
let cleanupFunctions: (() => Promise<void>)[] = [];
const registerCleanup = (cleanup: () => Promise<void>) => {
cleanupFunctions.push(cleanup);
};
afterEach(async () => {
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
cleanupFunctions = [];
for (const cleanup of cleanupFnsCopy) {
try {
await cleanup();
} catch (error) {
console.error("Error during cleanup:", error);
}
}
});
interface SetupProps {
skipAgentAPIMock?: boolean;
skipCodexMock?: boolean;
moduleVariables?: Record<string, string>;
agentapiMockScript?: string;
}
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const projectDir = "/home/coder/project";
const { id } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
install_codex: props?.skipCodexMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
codex_model: "gpt-4-turbo",
workdir: "/home/coder",
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
if (!props?.skipCodexMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/codex",
content: await loadTestFile(import.meta.dir, "codex-mock.sh"),
});
}
return { id };
};
setDefaultTimeout(60 * 1000);
describe("codex", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
test("happy-path", async () => {
const { id } = await setup();
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("install-codex-version", async () => {
const version_to_install = "0.10.0";
const { id } = await setup({
skipCodexMock: true,
moduleVariables: {
install_codex: "true",
codex_version: version_to_install,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`cat /home/coder/.codex-module/install.log`,
]);
expect(resp.stdout).toContain(version_to_install);
});
test("check-latest-codex-version-works", async () => {
const { id } = await setup({
skipCodexMock: true,
skipAgentAPIMock: true,
moduleVariables: {
install_codex: "true",
},
});
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("base-config-toml", async () => {
const baseConfig = dedent`
sandbox_mode = "danger-full-access"
approval_policy = "never"
preferred_auth_method = "apikey"
[custom_section]
new_feature = true
`.trim();
const { id } = await setup({
moduleVariables: {
base_config_toml: baseConfig,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
expect(resp).toContain('sandbox_mode = "danger-full-access"');
expect(resp).toContain('preferred_auth_method = "apikey"');
expect(resp).toContain("[custom_section]");
expect(resp).toContain("[mcp_servers.Coder]");
});
test("codex-api-key", async () => {
const apiKey = "test-api-key-123";
const { id } = await setup({
moduleVariables: {
openai_api_key: apiKey,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.codex-module/agentapi-start.log",
);
expect(resp).toContain("OpenAI API Key: Provided");
});
test("pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
post_install_script: "#!/bin/bash\necho 'post-install-script'",
},
});
await execModuleScript(id);
const preInstallLog = await readFileContainer(
id,
"/home/coder/.codex-module/pre_install.log",
);
expect(preInstallLog).toContain("pre-install-script");
const postInstallLog = await readFileContainer(
id,
"/home/coder/.codex-module/post_install.log",
);
expect(postInstallLog).toContain("post-install-script");
});
test("workdir-variable", async () => {
const workdir = "/tmp/codex-test-workdir";
const { id } = await setup({
skipCodexMock: false,
moduleVariables: {
workdir,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.codex-module/install.log",
);
expect(resp).toContain(workdir);
});
test("additional-mcp-servers", async () => {
const additional = dedent`
[mcp_servers.GitHub]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
type = "stdio"
description = "GitHub integration"
[mcp_servers.FileSystem]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
type = "stdio"
description = "File system access"
`.trim();
const { id } = await setup({
moduleVariables: {
additional_mcp_servers: additional,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
expect(resp).toContain("[mcp_servers.GitHub]");
expect(resp).toContain("[mcp_servers.FileSystem]");
expect(resp).toContain("[mcp_servers.Coder]");
expect(resp).toContain("GitHub integration");
});
test("full-custom-config", async () => {
const baseConfig = dedent`
sandbox_mode = "read-only"
approval_policy = "untrusted"
preferred_auth_method = "chatgpt"
custom_setting = "test-value"
[advanced_settings]
timeout = 30000
debug = true
logging_level = "verbose"
`.trim();
const additionalMCP = dedent`
[mcp_servers.CustomTool]
command = "/usr/local/bin/custom-tool"
args = ["--serve", "--port", "8080"]
type = "stdio"
description = "Custom development tool"
[mcp_servers.DatabaseMCP]
command = "python"
args = ["-m", "database_mcp_server"]
type = "stdio"
description = "Database query interface"
`.trim();
const { id } = await setup({
moduleVariables: {
base_config_toml: baseConfig,
additional_mcp_servers: additionalMCP,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
// Check base config
expect(resp).toContain('sandbox_mode = "read-only"');
expect(resp).toContain('preferred_auth_method = "chatgpt"');
expect(resp).toContain('custom_setting = "test-value"');
expect(resp).toContain("[advanced_settings]");
expect(resp).toContain('logging_level = "verbose"');
// Check MCP servers
expect(resp).toContain("[mcp_servers.Coder]");
expect(resp).toContain("[mcp_servers.CustomTool]");
expect(resp).toContain("[mcp_servers.DatabaseMCP]");
expect(resp).toContain("Custom development tool");
expect(resp).toContain("Database query interface");
});
test("minimal-default-config", async () => {
const { id } = await setup({
moduleVariables: {
// No base_config_toml or additional_mcp_servers - should use defaults
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
// Check default base config
expect(resp).toContain('sandbox_mode = "workspace-write"');
expect(resp).toContain('approval_policy = "never"');
expect(resp).toContain("[sandbox_workspace_write]");
expect(resp).toContain("network_access = true");
// Check only Coder MCP server is present
expect(resp).toContain("[mcp_servers.Coder]");
expect(resp).toContain("Report ALL tasks and statuses");
// Ensure no additional MCP servers
const mcpServerCount = (resp.match(/\[mcp_servers\./g) || []).length;
expect(mcpServerCount).toBe(1);
});
test("codex-system-prompt", async () => {
const prompt = "This is a system prompt for Codex.";
const { id } = await setup({
moduleVariables: {
codex_system_prompt: prompt,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/AGENTS.md");
expect(resp).toContain(prompt);
});
test("codex-system-prompt-skip-append-if-exists", async () => {
const prompt_1 = "This is a system prompt for Codex.";
const prompt_2 = "This is a system prompt for Goose.";
const prompt_3 = dedent`
This is a system prompt for Codex.
This is a system prompt for Gemini.
`.trim();
const pre_install_script = dedent`
#!/bin/bash
mkdir -p /home/coder/.codex
echo -e "${prompt_3}" >> /home/coder/.codex/AGENTS.md
`.trim();
const { id } = await setup({
moduleVariables: {
pre_install_script,
codex_system_prompt: prompt_2,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/AGENTS.md");
expect(resp).toContain(prompt_1);
expect(resp).toContain(prompt_2);
// Re-run with a prompt that already exists, it should not append again
const { id: id_2 } = await setup({
moduleVariables: {
pre_install_script,
codex_system_prompt: prompt_1,
},
});
await execModuleScript(id_2);
const resp_2 = await readFileContainer(
id_2,
"/home/coder/.codex/AGENTS.md",
);
expect(resp_2).toContain(prompt_1);
const count = (resp_2.match(new RegExp(prompt_1, "g")) || []).length;
expect(count).toBe(1);
});
test("codex-ai-task-prompt", async () => {
const prompt = "This is a system prompt for Codex.";
const { id } = await setup({
moduleVariables: {
ai_prompt: prompt,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`cat /home/coder/.codex-module/agentapi-start.log`,
]);
expect(resp.stdout).toContain(prompt);
});
test("start-without-prompt", async () => {
const { id } = await setup({
moduleVariables: {
codex_system_prompt: "", // Explicitly disable system prompt
},
});
await execModuleScript(id);
const prompt = await execContainer(id, [
"ls",
"-l",
"/home/coder/.codex/AGENTS.md",
]);
expect(prompt.exitCode).not.toBe(0);
expect(prompt.stderr).toContain("No such file or directory");
});
test("codex-continue-capture-new-session", async () => {
const { id } = await setup({
moduleVariables: {
continue: "true",
ai_prompt: "test task",
},
});
const workdir = "/home/coder";
const expectedSessionId = "019a1234-5678-9abc-def0-123456789012";
const sessionsDir = "/home/coder/.codex/sessions";
const sessionFile = `${sessionsDir}/${expectedSessionId}.jsonl`;
await execContainer(id, ["mkdir", "-p", sessionsDir]);
await execContainer(id, [
"bash",
"-c",
`echo '{"id":"${expectedSessionId}","cwd":"${workdir}","created":"2024-10-24T20:00:00Z","model":"gpt-4-turbo"}' > ${sessionFile}`,
]);
await execModuleScript(id);
await expectAgentAPIStarted(id);
const trackingFile = "/home/coder/.codex-module/.codex-task-session";
const maxAttempts = 30;
let trackingFileContents = "";
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const result = await execContainer(id, [
"bash",
"-c",
`cat ${trackingFile} 2>/dev/null || echo ""`,
]);
if (result.stdout.trim().length > 0) {
trackingFileContents = result.stdout;
break;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
expect(trackingFileContents).toContain(`${workdir}|${expectedSessionId}`);
const startLog = await readFileContainer(
id,
"/home/coder/.codex-module/agentapi-start.log",
);
expect(startLog).toContain("Capturing new session ID");
expect(startLog).toContain("Session tracked");
expect(startLog).toContain(expectedSessionId);
});
test("codex-continue-resume-existing-session", async () => {
const { id } = await setup({
moduleVariables: {
continue: "true",
ai_prompt: "test prompt",
},
});
const workdir = "/home/coder";
const mockSessionId = "019a1234-5678-9abc-def0-123456789012";
const trackingFile = "/home/coder/.codex-module/.codex-task-session";
await execContainer(id, ["mkdir", "-p", "/home/coder/.codex-module"]);
await execContainer(id, [
"bash",
"-c",
`echo "${workdir}|${mockSessionId}" > ${trackingFile}`,
]);
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.codex-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain("Found existing task session");
expect(startLog.stdout).toContain(mockSessionId);
expect(startLog.stdout).toContain("Resuming existing session");
expect(startLog.stdout).toContain(
`Starting Codex with arguments: --model gpt-4-turbo resume ${mockSessionId}`,
);
expect(startLog.stdout).not.toContain("test prompt");
});
});
+220
View File
@@ -0,0 +1,220 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/openai.svg"
}
variable "workdir" {
type = string
description = "The folder to run Codex in."
}
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 "cli_app" {
type = bool
description = "Whether to create a CLI app for Codex"
default = false
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app"
default = "Codex"
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app"
default = "Codex CLI"
}
variable "install_codex" {
type = bool
description = "Whether to install Codex."
default = true
}
variable "codex_version" {
type = string
description = "The version of Codex to install."
default = "" # empty string means the latest available version
}
variable "base_config_toml" {
type = string
description = "Complete base TOML configuration for Codex (without mcp_servers section). If empty, uses minimal default configuration with workspace-write sandbox mode and never approval policy. For advanced options, see https://github.com/openai/codex/blob/main/codex-rs/config.md"
default = ""
}
variable "additional_mcp_servers" {
type = string
description = "Additional MCP servers configuration in TOML format. These will be merged with the required Coder MCP server in the [mcp_servers] section."
default = ""
}
variable "openai_api_key" {
type = string
description = "OpenAI API key for Codex CLI"
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 "codex_model" {
type = string
description = "The model for Codex to use. Defaults to gpt-5."
default = ""
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing Codex."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing Codex."
default = null
}
variable "ai_prompt" {
type = string
description = "Initial task prompt for Codex CLI when launched via Tasks"
default = ""
}
variable "continue" {
type = bool
description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)."
default = true
}
variable "codex_system_prompt" {
type = string
description = "System instructions written to AGENTS.md in the ~/.codex directory"
default = "You are a helpful coding assistant. Start every response with `Codex says:`"
}
resource "coder_env" "openai_api_key" {
agent_id = var.agent_id
name = "OPENAI_API_KEY"
value = var.openai_api_key
}
locals {
workdir = trimsuffix(var.workdir, "/")
app_slug = "codex"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".codex-module"
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0"
agent_id = var.agent_id
folder = local.workdir
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_subdomain = var.subdomain
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_OPENAI_API_KEY='${var.openai_api_key}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_CODEX_MODEL='${var.codex_model}' \
ARG_CODEX_START_DIRECTORY='${local.workdir}' \
ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_CONTINUE='${var.continue}' \
/tmp/start.sh
EOT
install_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_OPENAI_API_KEY='${var.openai_api_key}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_INSTALL='${var.install_codex}' \
ARG_CODEX_VERSION='${var.codex_version}' \
ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \
ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_CODEX_START_DIRECTORY='${local.workdir}' \
ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \
/tmp/install.sh
EOT
}
@@ -0,0 +1,188 @@
#!/bin/bash
source "$HOME"/.bashrc
BOLD='\033[0;1m'
command_exists() {
command -v "$1" > /dev/null 2>&1
}
set -o errexit
set -o pipefail
set -o nounset
ARG_BASE_CONFIG_TOML=$(echo -n "$ARG_BASE_CONFIG_TOML" | base64 -d)
ARG_ADDITIONAL_MCP_SERVERS=$(echo -n "$ARG_ADDITIONAL_MCP_SERVERS" | base64 -d)
ARG_CODEX_INSTRUCTION_PROMPT=$(echo -n "$ARG_CODEX_INSTRUCTION_PROMPT" | base64 -d)
echo "=== Codex Module Configuration ==="
printf "Install Codex: %s\n" "$ARG_INSTALL"
printf "Codex Version: %s\n" "$ARG_CODEX_VERSION"
printf "App Slug: %s\n" "$ARG_CODER_MCP_APP_STATUS_SLUG"
printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
printf "Has Base Config: %s\n" "$([ -n "$ARG_BASE_CONFIG_TOML" ] && echo "Yes" || echo "No")"
printf "Has Additional MCP: %s\n" "$([ -n "$ARG_ADDITIONAL_MCP_SERVERS" ] && echo "Yes" || echo "No")"
printf "Has System Prompt: %s\n" "$([ -n "$ARG_CODEX_INSTRUCTION_PROMPT" ] && echo "Yes" || echo "No")"
printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")"
printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS"
echo "======================================"
set +o nounset
function install_node() {
if ! command_exists npm; then
printf "npm not found, checking for Node.js installation...\n"
if ! command_exists node; then
printf "Node.js not found, installing Node.js via NVM...\n"
export NVM_DIR="$HOME/.nvm"
if [ ! -d "$NVM_DIR" ]; then
mkdir -p "$NVM_DIR"
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
else
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
fi
nvm install --lts
nvm use --lts
nvm alias default node
printf "Node.js installed: %s\n" "$(node --version)"
printf "npm installed: %s\n" "$(npm --version)"
else
printf "Node.js is installed but npm is not available. Please install npm manually.\n"
exit 1
fi
fi
}
function install_codex() {
if [ "${ARG_INSTALL}" = "true" ]; then
install_node
if ! command_exists nvm; then
printf "which node: %s\n" "$(which node)"
printf "which npm: %s\n" "$(which npm)"
mkdir -p "$HOME"/.npm-global
npm config set prefix "$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then
echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc
fi
fi
printf "%s Installing Codex CLI\n" "${BOLD}"
if [ -n "$ARG_CODEX_VERSION" ]; then
npm install -g "@openai/codex@$ARG_CODEX_VERSION"
else
npm install -g "@openai/codex"
fi
printf "%s Successfully installed Codex CLI. Version: %s\n" "${BOLD}" "$(codex --version)"
fi
}
write_minimal_default_config() {
local config_path="$1"
cat << EOF > "$config_path"
# Minimal Default Codex Configuration
sandbox_mode = "workspace-write"
approval_policy = "never"
preferred_auth_method = "apikey"
[sandbox_workspace_write]
network_access = true
EOF
}
append_mcp_servers_section() {
local config_path="$1"
if [ "${ARG_REPORT_TASKS}" == "false" ]; then
ARG_CODER_MCP_APP_STATUS_SLUG=""
CODER_MCP_AI_AGENTAPI_URL=""
else
CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
fi
cat << EOF >> "$config_path"
# MCP Servers Configuration
[mcp_servers.Coder]
command = "coder"
args = ["exp", "mcp", "server"]
env = { "CODER_MCP_APP_STATUS_SLUG" = "${ARG_CODER_MCP_APP_STATUS_SLUG}", "CODER_MCP_AI_AGENTAPI_URL" = "${CODER_MCP_AI_AGENTAPI_URL}" , "CODER_AGENT_URL" = "${CODER_AGENT_URL}", "CODER_AGENT_TOKEN" = "${CODER_AGENT_TOKEN}" }
description = "Report ALL tasks and statuses (in progress, done, failed) you are working on."
type = "stdio"
EOF
if [ -n "$ARG_ADDITIONAL_MCP_SERVERS" ]; then
printf "Adding additional MCP servers\n"
echo "$ARG_ADDITIONAL_MCP_SERVERS" >> "$config_path"
fi
}
function populate_config_toml() {
CONFIG_PATH="$HOME/.codex/config.toml"
mkdir -p "$(dirname "$CONFIG_PATH")"
if [ -n "$ARG_BASE_CONFIG_TOML" ]; then
printf "Using provided base configuration\n"
echo "$ARG_BASE_CONFIG_TOML" > "$CONFIG_PATH"
else
printf "Using minimal default configuration\n"
write_minimal_default_config "$CONFIG_PATH"
fi
append_mcp_servers_section "$CONFIG_PATH"
}
function add_instruction_prompt_if_exists() {
if [ -n "${ARG_CODEX_INSTRUCTION_PROMPT:-}" ]; then
AGENTS_PATH="$HOME/.codex/AGENTS.md"
printf "Creating AGENTS.md in .codex directory: %s\\n" "${AGENTS_PATH}"
mkdir -p "$HOME/.codex"
if [ -f "${AGENTS_PATH}" ] && grep -Fq "${ARG_CODEX_INSTRUCTION_PROMPT}" "${AGENTS_PATH}"; then
printf "AGENTS.md already contains the instruction prompt. Skipping append.\n"
else
printf "Appending instruction prompt to AGENTS.md in .codex directory\n"
echo -e "\n${ARG_CODEX_INSTRUCTION_PROMPT}" >> "${AGENTS_PATH}"
fi
if [ ! -d "${ARG_CODEX_START_DIRECTORY}" ]; then
printf "Creating start directory '%s'\\n" "${ARG_CODEX_START_DIRECTORY}"
mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
exit 1
}
fi
else
printf "AGENTS.md instruction prompt is not set.\n"
fi
}
function add_auth_json() {
AUTH_JSON_PATH="$HOME/.codex/auth.json"
mkdir -p "$(dirname "$AUTH_JSON_PATH")"
AUTH_JSON=$(
cat << EOF
{
"OPENAI_API_KEY": "${ARG_OPENAI_API_KEY}"
}
EOF
)
echo "$AUTH_JSON" > "$AUTH_JSON_PATH"
}
install_codex
codex --version
populate_config_toml
add_instruction_prompt_if_exists
add_auth_json
@@ -0,0 +1,211 @@
#!/bin/bash
source "$HOME"/.bashrc
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" > /dev/null 2>&1
}
if [ -f "$HOME/.nvm/nvm.sh" ]; then
source "$HOME"/.nvm/nvm.sh
else
export PATH="$HOME/.npm-global/bin:$PATH"
fi
printf "Version: %s\n" "$(codex --version)"
set -o nounset
ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d)
ARG_CONTINUE=${ARG_CONTINUE:-true}
echo "=== Codex Launch Configuration ==="
printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")"
printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}"
printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")"
printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS"
printf "Continue Sessions: %s\n" "$ARG_CONTINUE"
echo "======================================"
set +o nounset
SESSION_TRACKING_FILE="$HOME/.codex-module/.codex-task-session"
find_session_for_directory() {
local target_dir="$1"
if [ ! -f "$SESSION_TRACKING_FILE" ]; then
return 1
fi
local session_id=$(grep "^$target_dir|" "$SESSION_TRACKING_FILE" | cut -d'|' -f2 | head -1)
if [ -n "$session_id" ]; then
echo "$session_id"
return 0
fi
return 1
}
store_session_mapping() {
local dir="$1"
local session_id="$2"
mkdir -p "$(dirname "$SESSION_TRACKING_FILE")"
if [ -f "$SESSION_TRACKING_FILE" ]; then
grep -v "^$dir|" "$SESSION_TRACKING_FILE" > "$SESSION_TRACKING_FILE.tmp" 2> /dev/null || true
mv "$SESSION_TRACKING_FILE.tmp" "$SESSION_TRACKING_FILE"
fi
echo "$dir|$session_id" >> "$SESSION_TRACKING_FILE"
}
find_recent_session_file() {
local target_dir="$1"
local sessions_dir="$HOME/.codex/sessions"
if [ ! -d "$sessions_dir" ]; then
return 1
fi
local latest_file=""
local latest_time=0
while IFS= read -r session_file; do
local file_time=$(stat -c %Y "$session_file" 2> /dev/null || stat -f %m "$session_file" 2> /dev/null || echo "0")
local first_line=$(head -n 1 "$session_file" 2> /dev/null)
local session_cwd=$(echo "$first_line" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4)
if [ "$session_cwd" = "$target_dir" ] && [ "$file_time" -gt "$latest_time" ]; then
latest_file="$session_file"
latest_time="$file_time"
fi
done < <(find "$sessions_dir" -type f -name "*.jsonl" 2> /dev/null)
if [ -n "$latest_file" ]; then
local first_line=$(head -n 1 "$latest_file")
local session_id=$(echo "$first_line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
if [ -n "$session_id" ]; then
echo "$session_id"
return 0
fi
fi
return 1
}
wait_for_session_file() {
local target_dir="$1"
local max_attempts=20
local attempt=0
while [ $attempt -lt $max_attempts ]; do
local session_id=$(find_recent_session_file "$target_dir" 2> /dev/null || echo "")
if [ -n "$session_id" ]; then
echo "$session_id"
return 0
fi
sleep 0.5
attempt=$((attempt + 1))
done
return 1
}
validate_codex_installation() {
if command_exists codex; then
printf "Codex is installed\n"
else
printf "Error: Codex is not installed. Please enable install_codex or install it manually\n"
exit 1
fi
}
setup_workdir() {
if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then
printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
cd "${ARG_CODEX_START_DIRECTORY}" || {
printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
exit 1
}
else
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
exit 1
}
cd "${ARG_CODEX_START_DIRECTORY}" || {
printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
exit 1
}
fi
}
build_codex_args() {
CODEX_ARGS=()
if [ -n "$ARG_CODEX_MODEL" ]; then
CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL")
fi
if [ "$ARG_CONTINUE" = "true" ]; then
existing_session=$(find_session_for_directory "$ARG_CODEX_START_DIRECTORY" 2> /dev/null || echo "")
if [ -n "$existing_session" ]; then
printf "Found existing task session for this directory: %s\n" "$existing_session"
printf "Resuming existing session...\n"
CODEX_ARGS+=("resume" "$existing_session")
else
printf "No existing task session found for this directory\n"
printf "Starting new task session...\n"
if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
if [ "${ARG_REPORT_TASKS}" == "true" ]; then
PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
else
PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT"
fi
CODEX_ARGS+=("$PROMPT")
fi
fi
else
printf "Continue disabled, starting fresh session\n"
if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
if [ "${ARG_REPORT_TASKS}" == "true" ]; then
PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
else
PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT"
fi
CODEX_ARGS+=("$PROMPT")
fi
fi
}
capture_session_id() {
if [ "$ARG_CONTINUE" = "true" ] && [ -z "$existing_session" ]; then
printf "Capturing new session ID...\n"
new_session=$(wait_for_session_file "$ARG_CODEX_START_DIRECTORY" || echo "")
if [ -n "$new_session" ]; then
store_session_mapping "$ARG_CODEX_START_DIRECTORY" "$new_session"
printf "✓ Session tracked: %s\n" "$new_session"
printf "This session will be automatically resumed on next restart\n"
else
printf "⚠ Could not capture session ID after 10s timeout\n"
fi
fi
}
start_codex() {
printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}"
agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}" &
capture_session_id
}
validate_codex_installation
setup_workdir
build_codex_args
start_codex
@@ -0,0 +1,38 @@
#!/bin/bash
# Handle --version flag
if [[ "$1" == "--version" ]]; then
echo "HELLO: $(bash -c env)"
echo "codex version v1.0.0"
exit 0
fi
set -e
SESSION_ID=""
IS_RESUME=false
while [[ $# -gt 0 ]]; do
case $1 in
resume)
IS_RESUME=true
SESSION_ID="$2"
shift 2
;;
*)
shift
;;
esac
done
if [ "$IS_RESUME" = false ]; then
SESSION_ID="019a1234-5678-9abc-def0-123456789012"
echo "Created new session: $SESSION_ID"
else
echo "Resuming session: $SESSION_ID"
fi
while true; do
echo "$(date) - codex-mock (session: $SESSION_ID)"
sleep 15
done
@@ -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.2"
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.2"
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.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
# Version pinning (defaults to "latest", use specific version if desired)
copilot_version = "0.0.334"
# 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.2"
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.2"
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 = "latest"
}
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
@@ -0,0 +1,135 @@
---
display_name: Cursor CLI
icon: ../../../../.icons/cursor.svg
description: Run Cursor Agent CLI in your workspace for AI pair programming
verified: true
tags: [agent, cursor, ai, tasks]
---
# Cursor CLI
Run the Cursor Agent CLI in your workspace for interactive coding assistance and automated task execution.
```tf
module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder"
version = "0.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
```
## Basic setup
A full example with MCP, rules, and pre/post install scripts:
```tf
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Build a Minesweeper in Python."
mutable = true
}
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/coder-login/coder"
version = "1.0.31"
agent_id = coder_agent.main.id
}
module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder"
version = "0.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
# Optional
install_cursor_cli = true
force = true
model = "gpt-5"
ai_prompt = data.coder_parameter.ai_prompt.value
api_key = "xxxx-xxxx-xxxx" # Required while using tasks, see note below
# Minimal MCP server (writes `folder/.cursor/mcp.json`):
mcp = jsonencode({
mcpServers = {
playwright = {
command = "npx"
args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated", "--no-sandbox"]
}
desktop-commander = {
command = "npx"
args = ["-y", "@wonderwhy-er/desktop-commander"]
}
}
})
# Use a pre_install_script to install the CLI
pre_install_script = <<-EOT
#!/usr/bin/env bash
set -euo pipefail
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
EOT
# Use post_install_script to wait for the repo to be ready
post_install_script = <<-EOT
#!/usr/bin/env bash
set -euo pipefail
TARGET="$${FOLDER}/.git/config"
echo "[cursor-cli] waiting for $${TARGET}..."
for i in $(seq 1 600); do
[ -f "$TARGET" ] && { echo "ready"; exit 0; }
sleep 1
done
echo "timeout waiting for $${TARGET}" >&2
EOT
# Provide a map of file name to content; files are written to `folder/.cursor/rules/<name>`.
rules_files = {
"python.mdc" = <<-EOT
---
description: RPC Service boilerplate
globs:
alwaysApply: false
---
- Use our internal RPC pattern when defining services
- Always use snake_case for service names.
@service-template.ts
EOT
"frontend.mdc" = <<-EOT
---
description: RPC Service boilerplate
globs:
alwaysApply: false
---
- Use our internal RPC pattern when defining services
- Always use snake_case for service names.
@service-template.ts
EOT
}
}
```
> [!NOTE]
> A `.cursor` directory will be created in the specified `folder`, containing the MCP configuration, rules.
> To use this module with tasks, please pass the API Key obtained from Cursor to the `api_key` variable. To obtain the api key follow the instructions [here](https://docs.cursor.com/en/cli/reference/authentication#step-1%3A-generate-an-api-key)
## References
- See Cursor CLI docs: `https://docs.cursor.com/en/cli/overview`
- For MCP project config, see `https://docs.cursor.com/en/context/mcp#using-mcp-json`. This module writes your `mcp_json` into `folder/.cursor/mcp.json`.
- For Rules, see `https://docs.cursor.com/en/context/rules#project-rules`. Provide `rules_files` (map of file name to content) to populate `folder/.cursor/rules/`.
## Troubleshooting
- Ensure the CLI is installed (enable `install_cursor_cli = true` or preinstall it in your image)
- Logs are written to `~/.cursor-cli-module/`
@@ -0,0 +1,152 @@
run "test_cursor_cli_basic" {
command = plan
variables {
agent_id = "test-agent-123"
folder = "/home/coder/projects"
}
assert {
condition = coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug environment variable should be set correctly"
}
assert {
condition = coder_env.status_slug.value == "cursorcli"
error_message = "Status slug value should be 'cursorcli'"
}
assert {
condition = var.folder == "/home/coder/projects"
error_message = "Folder variable should be set correctly"
}
assert {
condition = var.agent_id == "test-agent-123"
error_message = "Agent ID variable should be set correctly"
}
}
run "test_cursor_cli_with_api_key" {
command = plan
variables {
agent_id = "test-agent-456"
folder = "/home/coder/workspace"
api_key = "test-api-key-123"
}
assert {
condition = coder_env.cursor_api_key[0].name == "CURSOR_API_KEY"
error_message = "Cursor API key environment variable should be set correctly"
}
assert {
condition = coder_env.cursor_api_key[0].value == "test-api-key-123"
error_message = "Cursor API key value should match the input"
}
}
run "test_cursor_cli_with_custom_options" {
command = plan
variables {
agent_id = "test-agent-789"
folder = "/home/coder/custom"
order = 5
group = "development"
icon = "/icon/custom.svg"
model = "sonnet-4"
ai_prompt = "Help me write better code"
force = false
install_cursor_cli = false
install_agentapi = false
}
assert {
condition = var.order == 5
error_message = "Order variable should be set to 5"
}
assert {
condition = var.group == "development"
error_message = "Group variable should be set to 'development'"
}
assert {
condition = var.icon == "/icon/custom.svg"
error_message = "Icon variable should be set to custom icon"
}
assert {
condition = var.model == "sonnet-4"
error_message = "Model variable should be set to 'sonnet-4'"
}
assert {
condition = var.ai_prompt == "Help me write better code"
error_message = "AI prompt variable should be set correctly"
}
assert {
condition = var.force == false
error_message = "Force variable should be set to false"
}
}
run "test_cursor_cli_with_mcp_and_rules" {
command = plan
variables {
agent_id = "test-agent-mcp"
folder = "/home/coder/mcp-test"
mcp = jsonencode({
mcpServers = {
test = {
command = "test-server"
args = ["--config", "test.json"]
}
}
})
rules_files = {
"general.md" = "# General coding rules\n- Write clean code\n- Add comments"
"security.md" = "# Security rules\n- Never commit secrets\n- Validate inputs"
}
}
assert {
condition = var.mcp != null
error_message = "MCP configuration should be provided"
}
assert {
condition = var.rules_files != null
error_message = "Rules files should be provided"
}
assert {
condition = length(var.rules_files) == 2
error_message = "Should have 2 rules files"
}
}
run "test_cursor_cli_with_scripts" {
command = plan
variables {
agent_id = "test-agent-scripts"
folder = "/home/coder/scripts"
pre_install_script = "echo 'Pre-install script'"
post_install_script = "echo 'Post-install script'"
}
assert {
condition = var.pre_install_script == "echo 'Pre-install script'"
error_message = "Pre-install script should be set correctly"
}
assert {
condition = var.post_install_script == "echo 'Post-install script'"
error_message = "Post-install script should be set correctly"
}
}
@@ -0,0 +1,220 @@
import {
afterEach,
beforeAll,
describe,
expect,
setDefaultTimeout,
test,
} from "bun:test";
import { execContainer, runTerraformInit, writeFileContainer } from "~test";
import {
execModuleScript,
expectAgentAPIStarted,
loadTestFile,
setup as setupUtil,
} from "../../../coder/modules/agentapi/test-util";
import {
setupContainer,
writeExecutable,
} from "../../../coder/modules/agentapi/test-util";
let cleanupFns: (() => Promise<void>)[] = [];
const registerCleanup = (fn: () => Promise<void>) => cleanupFns.push(fn);
afterEach(async () => {
const fns = cleanupFns.slice().reverse();
cleanupFns = [];
for (const fn of fns) {
try {
await fn();
} catch (err) {
console.error(err);
}
}
});
interface SetupProps {
skipAgentAPIMock?: boolean;
skipCursorCliMock?: boolean;
moduleVariables?: Record<string, string>;
agentapiMockScript?: string;
}
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const projectDir = "/home/coder/project";
const { id } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
enable_agentapi: "true",
install_cursor_cli: props?.skipCursorCliMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
folder: projectDir,
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
if (!props?.skipCursorCliMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/cursor-agent",
content: await loadTestFile(import.meta.dir, "cursor-cli-mock.sh"),
});
}
return { id };
};
setDefaultTimeout(180 * 1000);
describe("cursor-cli", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
test("agentapi-happy-path", async () => {
const { id } = await setup({});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
await expectAgentAPIStarted(id);
});
test("agentapi-mcp-json", async () => {
const mcpJson =
'{"mcpServers": {"test": {"command": "test-cmd", "type": "stdio"}}}';
const { id } = await setup({
moduleVariables: {
mcp: mcpJson,
},
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
const mcpContent = await execContainer(id, [
"bash",
"-c",
`cat '/home/coder/project/.cursor/mcp.json'`,
]);
expect(mcpContent.exitCode).toBe(0);
expect(mcpContent.stdout).toContain("mcpServers");
expect(mcpContent.stdout).toContain("test");
expect(mcpContent.stdout).toContain("test-cmd");
expect(mcpContent.stdout).toContain("/tmp/mcp-hack.sh");
expect(mcpContent.stdout).toContain("coder");
});
test("agentapi-rules-files", async () => {
const rulesContent = "Always use TypeScript";
const { id } = await setup({
moduleVariables: {
rules_files: JSON.stringify({ "typescript.md": rulesContent }),
},
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
const rulesFile = await execContainer(id, [
"bash",
"-c",
`cat '/home/coder/project/.cursor/rules/typescript.md'`,
]);
expect(rulesFile.exitCode).toBe(0);
expect(rulesFile.stdout).toContain(rulesContent);
});
test("agentapi-api-key", async () => {
const apiKey = "test-cursor-api-key-123";
const { id } = await setup({
moduleVariables: {
api_key: apiKey,
},
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
const envCheck = await execContainer(id, [
"bash",
"-c",
`env | grep CURSOR_API_KEY || echo "CURSOR_API_KEY not found"`,
]);
expect(envCheck.stdout).toContain("CURSOR_API_KEY");
});
test("agentapi-model-and-force-flags", async () => {
const model = "sonnet-4";
const { id } = await setup({
moduleVariables: {
model: model,
force: "true",
ai_prompt: "test prompt",
},
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.cursor-cli-module/agentapi-start.log || cat /home/coder/.cursor-cli-module/start.log || true",
]);
expect(startLog.stdout).toContain(`-m ${model}`);
expect(startLog.stdout).toContain("-f");
expect(startLog.stdout).toContain("test prompt");
});
test("agentapi-pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'cursor-pre-install-script'",
post_install_script: "#!/bin/bash\necho 'cursor-post-install-script'",
},
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
const preInstallLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.cursor-cli-module/pre_install.log || true",
]);
expect(preInstallLog.stdout).toContain("cursor-pre-install-script");
const postInstallLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.cursor-cli-module/post_install.log || true",
]);
expect(postInstallLog.stdout).toContain("cursor-post-install-script");
});
test("agentapi-folder-variable", async () => {
const folder = "/tmp/cursor-test-folder";
const { id } = await setup({
moduleVariables: {
folder: folder,
},
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
const installLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.cursor-cli-module/install.log || true",
]);
expect(installLog.stdout).toContain(folder);
});
test("install-test-cursor-cli-latest", async () => {
const { id } = await setup({
skipCursorCliMock: true,
skipAgentAPIMock: true,
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
await expectAgentAPIStarted(id);
});
});
@@ -0,0 +1,181 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/cursor.svg"
}
variable "folder" {
type = string
description = "The folder to run Cursor CLI in."
}
variable "install_cursor_cli" {
type = bool
description = "Whether to install Cursor CLI."
default = true
}
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 "force" {
type = bool
description = "Force allow commands unless explicitly denied"
default = true
}
variable "model" {
type = string
description = "Model to use (e.g., sonnet-4, sonnet-4-thinking, gpt-5)"
default = ""
}
variable "ai_prompt" {
type = string
description = "AI prompt/task passed to cursor-agent."
default = ""
}
variable "api_key" {
type = string
description = "API key for Cursor CLI."
default = ""
sensitive = true
}
variable "mcp" {
type = string
description = "Workspace-specific MCP JSON to write to folder/.cursor/mcp.json. See https://docs.cursor.com/en/context/mcp#using-mcp-json"
default = null
}
variable "rules_files" {
type = map(string)
description = "Optional map of rule file name to content. Files will be written to folder/.cursor/rules/<name>. See https://docs.cursor.com/en/context/rules#project-rules"
default = null
}
variable "pre_install_script" {
type = string
description = "Optional script to run before installing Cursor CLI."
default = null
}
variable "post_install_script" {
type = string
description = "Optional script to run after installing Cursor CLI."
default = null
}
locals {
app_slug = "cursorcli"
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
resource "coder_env" "status_slug" {
agent_id = var.agent_id
name = "CODER_MCP_APP_STATUS_SLUG"
value = local.app_slug
}
resource "coder_env" "cursor_api_key" {
count = var.api_key != "" ? 1 : 0
agent_id = var.agent_id
name = "CURSOR_API_KEY"
value = var.api_key
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
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
web_app_icon = var.icon
web_app_display_name = "Cursor CLI"
cli_app_slug = local.app_slug
cli_app_display_name = "Cursor CLI"
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-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_FORCE='${var.force}' \
ARG_MODEL='${var.model}' \
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
ARG_FOLDER='${var.folder}' \
/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_INSTALL='${var.install_cursor_cli}' \
ARG_WORKSPACE_MCP_JSON='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \
ARG_WORKSPACE_RULES_JSON='${var.rules_files != null ? base64encode(jsonencode(var.rules_files)) : ""}' \
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
ARG_FOLDER='${var.folder}' \
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
/tmp/install.sh
EOT
}

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