Compare commits

..

54 Commits

Author SHA1 Message Date
Atif Ali 2c4cd86130 feat(mux): update logo (#638) 2026-01-08 00:19:12 +05:00
Yevhenii Shcherbina 28fc956110 fix: minor boundary bug in claude-code module (#637)
Removing existing boundary directory to allow re-running the script
safely
2026-01-07 10:17:40 -05:00
Atif Ali 2701dc09af feat(coder/modules/jetbrains): update to latest build numbers and clean up tests (#636)
Co-authored-by: DevCats <christofer@coder.com>
2026-01-07 01:06:31 +05:00
Scai 60611ed593 feat: make dynamic locations & server types on Hetzner template (#618)
## Description

Make Server Types & Locations dynamic based on API endpoints provided by
Hetzner Docs.

## Type of Change

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

## Template Information

**Path:** `registry/Excellencedev/templates/hetzner-linux`

## Testing & Validation

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: DevCats <christofer@coder.com>
2026-01-06 08:29:40 -06:00
dependabot[bot] 7df0cb25c5 chore(deps): bump crate-ci/typos from 1.40.0 to 1.41.0 in the github-actions group (#632)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-05 22:10:18 +00:00
DevCats cbb39bda6f chore: remove test from cloud-dev template (#635)
## Description

Remove test from `cloud-dev` template since templates generally have no
tests.
<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Template Information

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

**Path:** `registry/nboyers/templates/cloud-dev`

## 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 -->
2026-01-05 15:59:03 -06:00
DevCats 99bd4a4139 fix(github-upload-public-key): resolve issues with flaky tests (#634)
## Description

Better test cleanup, and resolve flakiness.
<!-- 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/github-upload-public-key`  
**New version:** `v1.0.32`  
**Breaking change:** [ ] Yes [ ] 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 -->
2026-01-05 14:59:06 -06:00
35C4n0r c819ca7f83 fix(nboyers/templates/cloud-dev): fix broken template icon (#633)
## Description

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

## Type of Change

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

## Template Information

**Path:** `registry/nboyers/templates/cloud-devops`

## 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 -->
2026-01-05 13:47:22 +05:30
Sebastian Mengwall accf5a34ab fix(modules/anomaly/tmux): fix config handling in run scripts (#629)
## Description

Fix custom tmux config handling. Two bugs:

1. `TMUX_CONFIG="${TMUX_CONFIG}"` - Terraform substitutes config inline,
bash interprets `set -g` etc as shell commands
2. `printf "$TMUX_CONFIG"` - `%` in `bind %` treated as format specifier


## Type of Change

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

## Module Information

**Path:** `registry/anomaly/modules/tmux`  
**New version:** 1.0.4  
**Breaking change:** [x] No

## Testing & Validation

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

## Related Issues

None
2026-01-04 03:12:20 +00:00
Sebastian Mengwall bb222f36a5 fix(mux): fix sed pattern escaping for npm tarball fallback (#628) 2026-01-02 22:18:24 +00:00
Noah 3677e93e36 Add Cloud DevOps workspace template for EKS (#518)
Co-authored-by: Noah Boyers <noah@MacBook-Pro.local>
Co-authored-by: Atif Ali <atif@coder.com>
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Noah Boyers <noah@coder.com>
2025-12-31 11:42:19 +00:00
netsgnut a3ba616aec fix(filebrowser): use updated flag for baseURL (#626)
Co-authored-by: Atif Ali <atif@coder.com>
2025-12-31 11:40:52 +00:00
Mossy 24dc52fb17 feat: Add Scaleway Instance Template (#449)
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-12-31 16:39:08 +05:00
Atif Ali 678c3e631e fix(vault-cli): use if/elif/else instead of case for HTTP client detection (#625) 2025-12-31 09:04:47 +05:00
35C4n0r 36089612ef chore(coder/modules/claude-code): bump agentapi version to 0.11.6 (#619)
## Description
bump agentapi version to 0.11.6. 
This version of agentapi introduces tool_call logging and improved
tool_call parsing

## 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/modules/claude-code`  
**New version:** `v4.2.8`  
**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-12-19 23:45:25 +05:30
Rowan Smith ac44ad862a chore: update Kubernetes resources to v1 API for provider v3 compatibility (#616)
## Description

As part of
https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/guides/v3-upgrade-guide
various resources change from non versioned to versioned. This PR
changes the Coder authored templates to versioned resources.

- Updated kubernetes_persistent_volume_claim to
kubernetes_persistent_volume_claim_v1
- Updated kubernetes_deployment to kubernetes_deployment_v1
- Updated kubernetes_pod to kubernetes_pod_v1
- Updated kubernetes_secret to kubernetes_secret_v1
- Updated all resource references and dependencies

I also had to fix up a couple of templates, i.e. remove `agent_name` as
it wasn't valid usage, `agent_id` remains. The `source` parameter for
jetbrains module in
[registry/coder/templates/kubernetes-envbox/main.tf](https://github.com/coder/registry/pull/616/changes#diff-83996ad9def3fae3b69981faee7d682964acc8716a4d04edfd7c4374f0a1d15c)
also had to be fixed.

## Type of Change

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

## Template Information

**Path:** 

- registry/coder/templates/kubernetes
- registry/coder/templates/kubernetes-devcontainer
- registry/coder/templates/kubernetes-envbox

## Testing & Validation

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

## Related Issues
2025-12-19 08:11:30 +11:00
Atif Ali ef5a903edf fix(zed): fix settings JSON parsing with base64 encoding (#604)
## Problem

The Zed module's settings parsing was broken. The previous
implementation used quote escaping:

```hcl
SETTINGS_JSON='${replace(var.settings, "\"", "\\\"")}'
```

This produced invalid JSON like `{\"theme\":\"dark\"}` which **jq could
not parse** because the backslash-escaped quotes are not valid JSON
syntax.

## Solution

Changed to use base64 encoding internally:

```hcl
locals {
  settings_b64 = var.settings != "" ? base64encode(var.settings) : ""
}

# In the script:
SETTINGS_B64='${local.settings_b64}'
SETTINGS_JSON="$(echo -n "${SETTINGS_B64}" | base64 -d)"
```

**User interface remains the same** - users still provide plain JSON via
`jsonencode()` or heredoc:

```hcl
module "zed" {
  source   = "..."
  agent_id = coder_agent.main.id
  settings = jsonencode({
    theme    = "dark"
    fontSize = 14
  })
}
```

## Testing

Added comprehensive tests:

**Terraform tests (5):**
- URL generation (default, folder, agent_name)
- Settings base64 encoding verification
- Empty settings edge case

**Container e2e tests (3):**
- Creates settings file with correct JSON (including special chars:
quotes, backslashes, URLs)
- Merges with existing settings via jq
- Exits early with empty settings

Also fixed existing tests to use `override_data` for proper workspace
mocking.

---------

Signed-off-by: Muhammad Atif Ali <me@matifali.dev>
Co-authored-by: DevCats <christofer@coder.com>
2025-12-17 16:17:06 -06:00
Atif Ali 4b7128b17e chore: link vscode-web and code-server comparison docs (#607)
link vscode-web and code-server comparison docs in README. Also fixes a
drive-by version to match expected versioning.

Co-authored-by: DevCats <christofer@coder.com>
2025-12-17 15:46:10 -06:00
DevCats 77a3e74e0b feat: enhance version bump script and CI workflow with ci mode (#615)
## Description

Add CI mode to version bump script to output Pass or Fail with a less
verbose comment when it fails
<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## 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-12-17 15:31:11 -06:00
Danielle Maywood 311de23454 refactor(coder-labs/auggie): support terraform provider coder v2.12.0 (#494)
## Description

Updates the module to use the new version of the agentapi module for
Coder 2.28

## Type of Change

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

## Module Information

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

## Testing & Validation

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

## Related Issues

- https://github.com/coder/internal/issues/1065

## Related PRs

- https://github.com/coder/registry/pull/485

Co-authored-by: DevCats <christofer@coder.com>
2025-12-17 13:16:12 -06:00
Danielle Maywood f66f61d724 refactor(coder-labs/sourcegraph-amp): support terraform provider coder v2.12.0 (#495)
## Description

Updates the module to use the new version of the agentapi module for
Coder 2.28

## Type of Change

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

## Module Information

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

## Testing & Validation

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

## Related Issues

- https://github.com/coder/internal/issues/1065

## Related PRs

- https://github.com/coder/registry/pull/485

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-12-17 11:54:28 -06:00
Danielle Maywood 631bf027c6 refactor(coder-labs/gemini): support terraform provider coder v2.12.0 (#493)
## Description

Updates the module to use the new version of the agentapi module for
Coder 2.28

## Type of Change

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

## Module Information

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

## Testing & Validation

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

## Related Issues

- https://github.com/coder/internal/issues/1065

## Related PRs

- https://github.com/coder/registry/pull/485

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-12-17 10:00:44 -06:00
Danielle Maywood eb4c28fc61 refactor(coder-labs/copilot): support terraform provider coder v2.12.0 (#492)
## Description

Updates the module to use the new version of the agentapi module for
Coder 2.28

## Type of Change

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

## Module Information

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

## Testing & Validation

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

## Related Issues

- https://github.com/coder/internal/issues/1065

## Related PRs

- https://github.com/coder/registry/pull/485

Co-authored-by: DevCats <christofer@coder.com>
2025-12-17 15:19:21 +00:00
Danielle Maywood c551c4d84a refactor(coder-labs/cursor-cli): support terraform provider coder v2.12.0 (#491)
## Description

Updates the module to use the new version of the agentapi module for
Coder 2.28

## Type of Change

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

## Module Information

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

## Testing & Validation

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

## Related Issues

- https://github.com/coder/internal/issues/1065

## Related PRs

- https://github.com/coder/registry/pull/485

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-12-17 15:12:34 +00:00
imgbot[bot] 4b9da4036a [ImgBot] Optimize images (#608)
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2025-12-16 23:25:21 +05:00
Excellencedev 96c5f3219d Add Hetzner Cloud server template example (#560)
## Description

This PR adds a template example for the Hetzner Cloud Sever. 


This video shows the provisioning of multiple hetzner instances in coder
with dynamic param enabled, running simultaneously and shown in the
Hetzner console, checking labels on both the OS Filesystem and on the
hetzner console and then shutting down through coder demonstrating clean
up on hetzner and deleting workspaces without errors
DEMO VIDEO:
https://drive.google.com/file/d/1JkhjszCRM9K27XDlMi_2nXtJosoflns_/view?usp=sharing

## Type of Change

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


## Template Information

**Path:** `registry/Excellencedev/templates/hetzner-linux`

## Testing & Validation

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

## Related Issues
/closes #209 
/claim #209

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: DevCats <christofer@coder.com>
2025-12-16 14:03:52 +00:00
Marcin Tojek 146540c1e9 feat: add perplexica module (#596) 2025-12-16 13:00:06 +00:00
35C4n0r a85436fdf4 chore(claude-code): use $HOME variable instead of hardcoded path and remove symlink (#592)
## Description

- Remove hardcoded `/home/coder` path.
- Remove symlink in favour of coder_env "PATH".

## Type of Change

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

## Module Information

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

## Testing & Validation

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

## Related Issues

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

---------

Signed-off-by: 35C4n0r <work.jaykumar@gmail.com>
Co-authored-by: DevelopmentCats <christofer@coder.com>
2025-12-15 15:07:14 -06:00
netsgnut aa4890fe62 fix(kasmvnc): update kasmvnc desktop environment list to match upstream (#598)
## Description

This PR makes the following changes to `coder/modules/kasmvnc`:

- Update desktop environment list for the KasmVNC module

Currently upstream supports a number of [additional Desktop
Environments](https://github.com/kasmtech/KasmVNC/blob/v1.4.0/unix/vncserver.man#L56-L67).
The change updates the list so that DEs like Mate are supported. Do note
that `manual` is also supported if `$HOME/.vnc/xstartup` is supplied, so
this has been added as another option, too.

## 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/kasmvnc`
**New version:** `v1.2.7`  
**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>
2025-12-15 13:00:18 -06:00
blinkagent[bot] ab6799ac07 fix(git-clone): use unique temp file for post_clone_script to avoid race condition (#601)
## Summary

Fixes a race condition when multiple `git-clone` modules with
`post_clone_script` run concurrently.

## Problem

All instances of the git-clone module use the same hardcoded
`/tmp/post_clone.sh` path. When multiple modules run concurrently (or
overlap), they collide on the same temp file, causing:

```
rm: cannot remove '/tmp/post_clone.sh': No such file or directory
```

This results in a non-zero exit code, causing the workspace to appear
unhealthy.

## Solution

Use `mktemp` to generate a unique temporary filename for each module
instance:

```bash
POST_CLONE_TMP=$(mktemp /tmp/post_clone_XXXXXX.sh)
```

This ensures each concurrent execution uses its own temp file,
eliminating the race condition.

Fixes #600

---------

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: Matyas Danter <mdanter@gmail.com>
2025-12-15 11:19:11 -06:00
Atif Ali bda3eb96e8 fix(coder/modules/zed): update main example to version 1.1.3 (#603) 2025-12-13 17:08:50 +05:00
Atif Ali 6b16cd3529 fix(coder/modules/zed): change script shebang from sh to bash (#602) 2025-12-13 16:46:02 +05:30
35C4n0r 43d05a9da4 feat(coder/agentapi/test-utils): add feature to optionally use coder_env (#595)
## Description
`setup` now returns `coderEnvVariables` that can be used in
`execModuleScript`.

## Type of Change

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

## 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 -->

---------

Signed-off-by: 35C4n0r <work.jaykumar@gmail.com>
2025-12-12 19:03:48 +05:30
DevCats e3f8b6450e chore: fix source references and cleanup readme (#593)
## Description

Update source references in README to
`registry.coder.com/harleylrn/kiro-cli/coder` from
`registry.coder.com/coder/kiro-cli/coder`

Remove verbose tf docs from README since it is visualized in the
registry.

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

## Type of Change

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

## Module Information

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

**Path:** `registry/harleylrn/modules/kiro-cli`  
**New version:** `v1.0.1`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun fmt`)
- [X] Changes tested locally
2025-12-09 14:46:56 -06:00
Michael Orlov c03986f9cb feat: add kiro-cli module (#552)
## Description

Due to rebranding of the Amazon Q to kiro-cli, new module was created

## Type of Change

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

## Module Information

All defined in the README
**Path:** `registry/harleylrn/modules/kiro-cli`  
**New version:** `v1.0.0`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

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

## Related Issues

Resolves #547

---------

Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Harley Davidson <harleylrn@users.noreply.github.com>
2025-12-09 11:52:05 -06:00
Michael Suchacz 758aba4c2a fix(mux): delete server.lock before starting server (#591)
## Summary

Remove stale `~/.mux/server.lock` file before starting the mux server to
prevent startup failures when a previous server didn't clean up
properly.

## Changes

- Added `rm -f "${HOME}/.mux/server.lock"` at the start of the `run_mux`
function
- Bumped version to 1.0.5

## Testing

- All Terraform tests pass
- Shellcheck passes with only style warnings (unrelated to this change)

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-12-09 21:33:01 +05:00
35C4n0r d745117782 chore(coder/modules/claude-code): bump agentapi version in claude-code (#590)
## Description
bump agentapi version to 0.11.4

## Type of Change

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

## Module Information

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

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v4.2.6`  
**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: DevCats <christofer@coder.com>
2025-12-09 21:35:50 +05:30
DevCats a99d3385c3 refactor(claude-code): simplify session resumption logic for standalone and task mode (#579)
## Description

Brings session resumption up to speed with current claude-code
capabilities, and should make session resumption less prone to errors
across the board.

I am still testing this further to ensure that all logic path's are
verified.

<!-- 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/claude-code`  
**New version:** `v4.2.5`  
**Breaking change:** [ ] Yes [ ] 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: 35C4n0r <70096901+35C4n0r@users.noreply.github.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-12-09 09:28:58 -06:00
35C4n0r c62fe569a0 fix(registry/coder/claude): fix versions in claude-code readme (#589)
## Description

<!-- 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/[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

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

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-12-09 14:49:48 +05:30
Atif Ali ce2087bc09 chore: update development dependencies versions (#588) 2025-12-09 08:39:13 +00:00
35C4n0r 67f18cd4de chore(registry/coder/claude-code): hides coder_report_task tool messages (#586)
## Description
- Bump agentapi version for feature: hide coder_report_task tool
messages

## 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/modules/claude-code`  
**New version:** `v4.2.3`  
**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-12-09 13:58:35 +05:30
Atif Ali e0697562c1 fix(positron): fix readme version and verification status (#587) 2025-12-09 13:35:29 +05:30
Marcin Tojek 499aaa676c feat: add open-webui module (#580)
This module installs and runs Open WebUI using Python and pip within
your Coder workspace.
2025-12-08 13:20:10 -06:00
Rowan Smith 3ae8c7dcff feat: support optional installation of vault enterprise binary (#582)
## Description

When using the SAML auth method with Vault and authenticating via CLI it
is required to use the enterprise version of the binary, as SAML support
is not built into the non enterprise version of the CLI. This PR adds an
optional `enterprise` variable to support this.

@matifali can you let me know the appropriate tag command to run to
release this once approved, please?

## Type of Change

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

## Module Information

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


## Testing & Validation

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

## Related Issues

None
2025-12-08 07:56:03 -06:00
Atif Ali 2cfbe5f69c feat: add vault-cli module with optional token configuration (#575) 2025-12-04 11:11:35 +05:00
35C4n0r 186e0c4de6 feat(module/coder-labs/amp): add mode flag and disable animation (#550)
## Description
1. "mode" flag: Set the agent mode (free, rush, smart) — controls the
model, system prompt, and tool selection.
2. `"amp.terminal.animation": false`: This disables the animation.
3. Update the readme

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

## Type of Change

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

## Module Information

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

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

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-12-03 20:27:14 +00:00
DevCats 69e5dc5c80 feat: new google antigravity ide module with icon (#577)
## Description

<!-- Briefly describe what this PR does and why -->
Adds a module to open coder workspaces in Antigravity.

Redoing PR with icon included

## Type of Change

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

## Module Information

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

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

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun fmt`)
- [ ] 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: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-02 15:41:21 -06:00
Atif Ali b143b7d9ba Revert "feat: add Antigravity IDE module" (#576)
Reverts coder/registry#558
2025-12-02 14:02:47 -06:00
DevCats 0a8930d60d feat: add Antigravity IDE module (#558)
## Description

<!-- Briefly describe what this PR does and why -->
Adds a module to open coder workspaces in Antigravity.

## Type of Change

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

## Module Information

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

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

## Testing & Validation

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

## Related Issues

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

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-12-02 13:51:49 -06:00
Atif Ali d21db0d220 fix(jfrog-oauth): fail when access_token is empty (#574)
## Summary

Fixes #72 - The `jfrog-oauth` module now fails with a clear error
message when the JFrog access token is empty, instead of silently
creating configurations with empty tokens.

## Changes

### 1. Added Precondition Validation (`main.tf`)

```hcl
lifecycle {
  precondition {
    condition     = data.coder_external_auth.jfrog.access_token != ""
    error_message = "JFrog access token is empty. Please authenticate with JFrog using external auth."
  }
}
```

This ensures the module fails at **plan time** with a clear error when
users haven't authenticated via external auth.

### 2. Replaced `main.test.ts` with `jfrog-oauth.tftest.hcl`

**Why we removed the TypeScript tests:**

The TypeScript tests used `runTerraformApply()` which runs `terraform
apply` directly. This approach **cannot mock data sources** like
`coder_external_auth`. The Coder provider returns empty strings for
tokens by default when running outside a real Coder workspace.

With our new precondition, the TypeScript tests would always fail
because:
1. `terraform apply` runs → empty `access_token` from mock provider
2. Precondition check fails → "JFrog access token is empty"
3. Test fails before any assertions run

**The solution:** Terraform's native `.tftest.hcl` format supports
`override_data` blocks that can properly mock data sources:

```hcl
override_data {
  target = data.coder_external_auth.jfrog
  values = {
    access_token = "valid-token-value"  # or "" to test failure
  }
}
```

### 3. Comprehensive Test Coverage

The new `jfrog-oauth.tftest.hcl` includes **12 tests** (up from 7):

| Test | What it validates |
|------|------------------|
| `test_required_vars` | Basic module works with required variables |
| `test_empty_access_token_fails` | **NEW:** Precondition rejects empty
tokens |
| `test_valid_access_token_succeeds` | Module works with valid token |
| `test_jfrog_url_validation` | **NEW:** URL must start with http(s)://
|
| `test_username_field_validation` | **NEW:** Must be "email" or
"username" |
| `test_with_npm_package_manager` | NPM config with scoped repos (script
content) |
| `test_configure_code_server` | **NEW:** IDE env vars created when
enabled |
| `test_go_proxy_env` | GOPROXY env value with multiple repos |
| `test_pypi_package_manager` | pip.conf with extra-index-url |
| `test_docker_package_manager` | register_docker commands for all repos
|
| `test_conda_package_manager` | .condarc channels configuration |
| `test_maven_package_manager` | settings.xml with servers and repos |

All package manager tests use `strcontains()` to verify the actual
script content matches expected configuration formats.

## Test Limitations (Acknowledged)

The tests verify **template rendering** but not **runtime execution**:

|  What we test |  What we don't test |
|----------------|----------------------|
| Configuration file formats | Script syntax errors at runtime |
| Variable interpolation | JFrog CLI compatibility |
| Precondition validation | Actual JFrog authentication |
| Script contains expected content | Commands execute successfully |

**Rationale:** The original TypeScript tests also only checked script
content (`toContain()`), not execution. Full execution testing would
require a mock JFrog server, which adds significant complexity for
limited benefit. The script is straightforward bash that configures
files and runs CLI commands.

## Testing

```bash
cd registry/coder/modules/jfrog-oauth
terraform test
# Success! 12 passed, 0 failed.
```

_Generated with [mux](https://github.com/coder/mux)_
2025-12-02 13:17:39 -06:00
Atif Ali 392f6b120a fix(mux): move image to shared folder and fix path (#573)
## Summary
Fix the mux module image rendering by moving the image to the shared
images folder and updating the path.

## Changes
- Move `product-hero.webp` from `registry/coder/modules/mux/.images/` to
`registry/coder/.images/`
- Rename to `mux-product-hero.webp` for consistency with other module
images (e.g., `amazon-q.png`, `amazon-dcv-windows.png`)
- Update README path from `.images/product-hero.webp` to
`../../.images/mux-product-hero.webp`

This follows the same pattern as other modules like `amazon-q`,
`amazon-dcv-windows`, and `jetbrains-gateway` which all use
`../../.images/` paths.
2025-12-02 12:42:05 -06:00
Atif Ali 7de72fc7cc feat(mux): add GitHub link, set verified, bump to v1.0.3 (#572) 2025-12-02 23:03:49 +05:00
dependabot[bot] 3e1ddbf624 chore(deps): bump crate-ci/typos from 1.39.2 to 1.40.0 in the github-actions group (#570)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 14:15:39 +05:00
Phorcys 0021a9fe7d feat: update vscode-based desktop IDE modules to use vscode-desktop-core (#279) 2025-11-27 17:35:40 +05:00
140 changed files with 7687 additions and 2290 deletions
+40 -11
View File
@@ -1,14 +1,18 @@
#!/bin/bash
# Version Bump Script
# Usage: ./version-bump.sh <bump_type> [base_ref]
# Usage: ./version-bump.sh [--ci] <bump_type> [base_ref]
# --ci: CI mode - run bump, check for changes, exit 1 if changes needed
# bump_type: patch, minor, or major
# base_ref: base reference for diff (default: origin/main)
set -euo pipefail
CI_MODE=false
usage() {
echo "Usage: $0 <bump_type> [base_ref]"
echo "Usage: $0 [--ci] <bump_type> [base_ref]"
echo " --ci: CI mode - validates versions are already bumped (exits 1 if not)"
echo " bump_type: patch, minor, or major"
echo " base_ref: base reference for diff (default: origin/main)"
echo ""
@@ -16,6 +20,7 @@ usage() {
echo " $0 patch # Update versions with patch bump"
echo " $0 minor # Update versions with minor bump"
echo " $0 major # Update versions with major bump"
echo " $0 --ci patch # CI check: verify patch bump has been applied"
exit 1
}
@@ -85,7 +90,7 @@ update_readme_version() {
in_module_block = 0
if (module_has_target_source) {
num_lines = split(module_content, lines, "\n")
for (i = 1; i <= num_lines; i++) {
for (i = 1; i < num_lines; i++) {
line = lines[i]
if (line ~ /^[[:space:]]*version[[:space:]]*=/) {
match(line, /^[[:space:]]*/)
@@ -115,6 +120,11 @@ update_readme_version() {
}
main() {
if [ "${1:-}" = "--ci" ]; then
CI_MODE=true
shift
fi
if [ $# -lt 1 ] || [ $# -gt 2 ]; then
usage
fi
@@ -152,6 +162,8 @@ main() {
local untagged_modules=""
local has_changes=false
declare -a modified_readme_files=()
while IFS= read -r module_path; do
if [ -z "$module_path" ]; then continue; fi
@@ -202,6 +214,7 @@ main() {
if update_readme_version "$readme_path" "$namespace" "$module_name" "$new_version"; then
updated_readmes="$updated_readmes\n- $namespace/$module_name"
modified_readme_files+=("$readme_path")
has_changes=true
fi
@@ -210,19 +223,22 @@ main() {
done <<< "$modules"
# 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..."
else
echo "⚠️ Warning: bun not found, skipping formatting"
if [ ${#modified_readme_files[@]} -gt 0 ]; then
echo "🔧 Formatting modified README files..."
if command -v bun > /dev/null 2>&1; then
for readme_file in "${modified_readme_files[@]}"; do
bun run prettier --write "$readme_file" 2> /dev/null || true
done
else
echo "⚠️ Warning: bun not found, skipping formatting"
fi
echo ""
fi
echo ""
echo "📋 Summary:"
echo "Bump Type: $bump_type"
echo ""
echo "Modules Updated:"
echo "Modules Processed:"
echo -e "$bumped_modules"
echo ""
@@ -239,6 +255,19 @@ main() {
echo ""
fi
if [ "$CI_MODE" = true ]; then
echo "🔍 Comparing files to committed versions..."
if git diff --quiet; then
echo "✅ PASS: All versions match - no changes needed"
exit 0
else
echo "❌ FAIL: Module versions need to be updated"
echo ""
echo "Run './.github/scripts/version-bump.sh $bump_type' locally and commit the changes"
exit 1
fi
fi
if [ "$has_changes" = true ]; then
echo "✅ Version bump completed successfully!"
echo "📝 README files have been updated with new versions."
+1
View File
@@ -3,6 +3,7 @@ muc = "muc" # For Munich location code
tyo = "tyo" # For Tokyo location code
Hashi = "Hashi"
HashiCorp = "HashiCorp"
hel = "hel" # For Helsinki location code
mavrickrishi = "mavrickrishi" # Username
mavrick = "mavrick" # Username
inh = "inh" # Option in setpriv command
+1 -1
View File
@@ -93,7 +93,7 @@ jobs:
- name: Validate formatting
run: bun fmt:ci
- name: Check for typos
uses: crate-ci/typos@v1.39.2
uses: crate-ci/typos@v1.41.0
with:
config: .github/typos.toml
validate-readme-files:
+22 -49
View File
@@ -55,62 +55,35 @@ jobs:
;;
esac
- name: Check version bump requirements
id: version-check
run: |
output_file=$(mktemp)
if ./.github/scripts/version-bump.sh "${{ steps.bump-type.outputs.type }}" origin/main > "$output_file" 2>&1; then
echo "Script completed successfully"
else
echo "Script failed"
cat "$output_file"
exit 1
fi
- name: Check version bump
run: ./.github/scripts/version-bump.sh --ci "${{ steps.bump-type.outputs.type }}" origin/main
{
echo "output<<EOF"
cat "$output_file"
echo "EOF"
} >> $GITHUB_OUTPUT
cat "$output_file"
if git diff --quiet; then
echo "versions_up_to_date=true" >> $GITHUB_OUTPUT
echo "✅ All module versions are already up to date"
else
echo "versions_up_to_date=false" >> $GITHUB_OUTPUT
echo "❌ Module versions need to be updated"
echo "Files that would be changed:"
git diff --name-only
echo ""
echo "Diff preview:"
git diff
git checkout .
git clean -fd
exit 1
fi
- name: Comment on PR - Failure
if: failure() && steps.version-check.outputs.versions_up_to_date == 'false'
- name: Comment on PR - Version bump required
if: failure()
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `${{ steps.version-check.outputs.output }}`;
const bumpType = `${{ steps.bump-type.outputs.type }}`;
let comment = `## ❌ Version Bump Validation Failed\n\n`;
comment += `**Bump Type:** \`${bumpType}\`\n\n`;
comment += `Module versions need to be updated but haven't been bumped yet.\n\n`;
comment += `**Required Actions:**\n`;
comment += `1. Run the version bump script locally: \`./.github/scripts/version-bump.sh ${bumpType}\`\n`;
comment += `2. Commit the changes: \`git add . && git commit -m "chore: bump module versions (${bumpType})"\`\n`;
comment += `3. Push the changes: \`git push\`\n\n`;
comment += `### Script Output:\n\`\`\`\n${output}\n\`\`\`\n\n`;
comment += `> Please update the module versions and push the changes to continue.`;
const comment = [
'## Version Bump Required',
'',
'One or more modules in this PR need their versions updated.',
'',
'**To fix this:**',
'1. Run the version bump script locally:',
' ```bash',
` ./.github/scripts/version-bump.sh ${bumpType}`,
' ```',
'2. Commit the changes:',
' ```bash',
` git add . && git commit -m "chore: bump module versions (${bumpType})"`,
' ```',
'3. Push your changes',
'',
'The CI will automatically re-run once you push the updated versions.'
].join('\n');
github.rest.issues.createComment({
issue_number: context.issue.number,
+1
View File
@@ -1,3 +1,4 @@
.DS_Store
# Logs
logs
*.log
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.3 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

+5 -47
View File
@@ -1,47 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="1024" height="1024">
<path d="M0 0 C337.92 0 675.84 0 1024 0 C1024 337.92 1024 675.84 1024 1024 C686.08 1024 348.16 1024 0 1024 C0 686.08 0 348.16 0 0 Z " fill="#020708" transform="translate(0,0)"/>
<path d="M0 0 C40.22460379 0 80.4466421 0.00304941 120.67057991 0.15463066 C125.95372492 0.17442425 131.23687063 0.19398844 136.52001762 0.21324348 C147.77834068 0.25435249 159.03666181 0.29592484 170.29497623 0.33933926 C170.99953161 0.34205531 171.70408698 0.34477135 172.42999252 0.34756971 C173.13533008 0.3502892 173.84066764 0.35300869 174.56737906 0.35581058 C175.99454502 0.36131272 177.42171098 0.36681401 178.84887695 0.37231445 C179.55666117 0.37504276 180.26444539 0.37777106 180.99367761 0.38058204 C192.50410753 0.42485496 204.01454726 0.46550406 215.52499563 0.50465261 C227.56090905 0.54564378 239.59680781 0.58979401 251.63269895 0.63687855 C258.30394122 0.66290814 264.97518119 0.68769341 271.64644051 0.70907402 C277.91414179 0.72924498 284.18181589 0.75352717 290.4494915 0.78049088 C292.69376491 0.78952735 294.93804399 0.79730645 297.18232727 0.80341911 C322.46351662 0.87417207 347.71815265 1.5042775 373 2 C373 96.05 373 190.1 373 287 C249.91 287 126.82 287 0 287 C0 192.29 0 97.58 0 0 Z " fill="#999F9E" transform="translate(326,395)"/>
<path d="M0 0 C37.29 0 74.58 0 113 0 C113 46.86 113 93.72 113 142 C75.71 142 38.42 142 0 142 C0 95.14 0 48.28 0 0 Z " fill="#181E23" transform="translate(550,445)"/>
<path d="M0 0 C37.29 0 74.58 0 113 0 C113 46.53 113 93.06 113 141 C95.613125 141.0309375 95.613125 141.0309375 77.875 141.0625 C74.3054248 141.071604 70.73584961 141.08070801 67.05810547 141.09008789 C62.46899414 141.09460449 62.46899414 141.09460449 60.27615356 141.09544373 C58.84198693 141.09691347 57.40782093 141.10027703 55.97366333 141.10557556 C37.3007381 141.17069509 18.67741441 140.49151091 0 140 C0 93.8 0 47.6 0 0 Z " fill="#181E23" transform="translate(362,446)"/>
<path d="M0 0 C0 14.52 0 29.04 0 44 C-118.14 44 -236.28 44 -358 44 C-359.71802382 40.56395235 -359.1196618 36.61561104 -359.09765625 32.8359375 C-359.0962413 31.92872955 -359.09482635 31.02152161 -359.09336853 30.08682251 C-359.08776096 27.18284845 -359.07520718 24.27895085 -359.0625 21.375 C-359.05748598 19.40885528 -359.0529229 17.44270935 -359.04882812 15.4765625 C-359.03778906 10.6510121 -359.02051935 5.82551903 -359 1 C-319.04215869 0.87185681 -279.08431526 0.74439427 -239.12646896 0.61781773 C-234.3862332 0.60280127 -229.64599745 0.58777929 -224.90576172 0.57275391 C-223.49049118 0.56826798 -223.49049118 0.56826798 -222.04662931 0.56369144 C-206.8611473 0.51554559 -191.67566674 0.46695532 -176.49018669 0.41819459 C-160.85514345 0.36800044 -145.22009869 0.3182983 -129.58505249 0.26903296 C-120.82560112 0.24141872 -112.06615054 0.21359008 -103.30670166 0.18519592 C-68.87102254 0.07367311 -34.43592726 -0.03058253 0 0 Z " fill="#FEFEFE" transform="translate(692,838)"/>
<path d="M0 0 C117.15 0 234.3 0 355 0 C355 13.86 355 27.72 355 42 C237.85 42 120.7 42 0 42 C0 28.14 0 14.28 0 0 Z " fill="#FEFEFE" transform="translate(334,135)"/>
<path d="M0 0 C11.88 0 23.76 0 36 0 C36 19.8 36 39.6 36 60 C91.44 60 146.88 60 204 60 C204 71.55 204 83.1 204 95 C168.85263573 95.02994071 133.70661685 94.95253395 98.55956407 94.80645666 C91.61749391 94.77764681 84.67541903 94.75005412 77.7333438 94.72249681 C65.36625264 94.67337442 52.99916523 94.62336685 40.63208008 94.57275391 C28.66105336 94.52376386 16.69002497 94.47522104 4.71899414 94.42724609 C3.97369616 94.4242591 3.22839818 94.4212721 2.46051542 94.41819459 C-1.2806363 94.4032035 -5.02178805 94.38822085 -8.76293981 94.37324238 C-39.50863188 94.25013489 -70.25431782 94.12555501 -101 94 C-101 93.67 -101 93.34 -101 93 C-134.66 92.505 -134.66 92.505 -169 92 C-169 81.44 -169 70.88 -169 60 C-113.23 60 -57.46 60 0 60 C0 40.2 0 20.4 0 0 Z " fill="#191F24" transform="translate(495,302)"/>
<path d="M0 0 C62.7 0 125.4 0 190 0 C190 11.88 190 23.76 190 36 C127.3 36 64.6 36 0 36 C0 24.12 0 12.24 0 0 Z " fill="#474B51" transform="translate(418,786)"/>
<path d="M0 0 C49.5 0 99 0 150 0 C150 14.52 150 29.04 150 44 C100.5 44 51 44 0 44 C0 29.48 0 14.96 0 0 Z " fill="#FDFEFE" transform="translate(200,239)"/>
<path d="M0 0 C48.18 0 96.36 0 146 0 C146 14.52 146 29.04 146 44 C131.979151 44.02742613 117.95836252 44.05097148 103.9375 44.0625 C102.90375118 44.06335602 101.87000237 44.06421204 100.80492783 44.06509399 C67.19004349 44.08969196 33.60574878 43.83234053 0 43 C0 28.81 0 14.62 0 0 Z " fill="#FEFEFE" transform="translate(677,239)"/>
<path d="M0 0 C51.48 0 102.96 0 156 0 C156 13.53 156 27.06 156 41 C104.52 41 53.04 41 0 41 C0 27.47 0 13.94 0 0 Z " fill="#FEFEFE" transform="translate(259,188)"/>
<path d="M0 0 C1.28729507 -0.00281982 2.57459015 -0.00563965 3.90089417 -0.00854492 C5.34078993 -0.00660032 6.78068557 -0.00455213 8.22058105 -0.00241089 C9.72647647 -0.00376487 11.23237154 -0.00554479 12.73826599 -0.00772095 C16.839391 -0.01229564 20.94049097 -0.01050558 25.04161644 -0.00734186 C29.32396896 -0.00481844 33.60631927 -0.00715575 37.88867188 -0.00872803 C45.08150621 -0.01054978 52.2743303 -0.00814327 59.46716309 -0.00338745 C67.79516627 0.00205518 76.12314497 0.00029253 84.45114756 -0.00521386 C91.58828139 -0.00974483 98.72540783 -0.01039561 105.86254263 -0.00777519 C110.13098648 -0.00621233 114.39941881 -0.00601889 118.66786194 -0.00931168 C122.67984227 -0.01219017 126.69179089 -0.01021511 130.70376778 -0.00441742 C132.18045054 -0.0030897 133.65713595 -0.00348413 135.13381767 -0.00568008 C137.1412128 -0.0083911 139.14861372 -0.0043972 141.15600586 0 C142.28205986 0.00037703 143.40811386 0.00075405 144.56829071 0.0011425 C147.07800293 0.12698364 147.07800293 0.12698364 148.07800293 1.12698364 C148.07800293 14.32698364 148.07800293 27.52698364 148.07800293 41.12698364 C96.92800293 41.12698364 45.77800293 41.12698364 -6.92199707 41.12698364 C-6.92199707 0.00231762 -6.92199707 0.00231762 0 0 Z " fill="#FEFEFE" transform="translate(616.9219970703125,187.87301635742188)"/>
<path d="M0 0 C43.23 0 86.46 0 131 0 C131 15.84 131 31.68 131 48 C87.77 48 44.54 48 0 48 C0 32.16 0 16.32 0 0 Z " fill="#FEFEFE" transform="translate(128,353)"/>
<path d="M0 0 C67.815 0.495 67.815 0.495 137 1 C137 15.19 137 29.38 137 44 C136.34 44 135.68 44 135 44 C135 44.66 135 45.32 135 46 C90.45 46 45.9 46 0 46 C0 30.82 0 15.64 0 0 Z " fill="#FDFEFE" transform="translate(156,294)"/>
<path d="M0 0 C45.21 0 90.42 0 137 0 C137 14.19 137 28.38 137 43 C136.34 43 135.68 43 135 43 C135 43.66 135 44.32 135 45 C90.45 45 45.9 45 0 45 C0 30.15 0 15.3 0 0 Z " fill="#FBFCFC" transform="translate(732,295)"/>
<path d="M0 0 C43.23 0 86.46 0 131 0 C131 15.18 131 30.36 131 46 C98.474264 46.72107728 65.97062256 47.09604795 33.4375 47.0625 C32.60141457 47.06164398 31.76532913 47.06078796 30.90390778 47.05990601 C20.60257228 47.04857042 10.30132061 47.02553343 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FEFEFE" transform="translate(765,354)"/>
<path d="M0 0 C26.73 0 53.46 0 81 0 C82.19877676 50.94801223 82.19877676 50.94801223 82 74 C77.05167364 74.80762419 72.35065215 75.12200785 67.32055664 75.11352539 C66.57629897 75.11364593 65.8320413 75.11376646 65.06523037 75.11389065 C62.68017418 75.11315027 60.29519828 75.10556237 57.91015625 75.09765625 C56.42801959 75.09618411 54.9458824 75.09516508 53.46374512 75.09460449 C47.99665543 75.08938309 42.52957691 75.07542183 37.0625 75.0625 C24.831875 75.041875 12.60125 75.02125 0 75 C0 50.25 0 25.5 0 0 Z " fill="#FB4740" transform="translate(473,226)"/>
<path d="M0 0 C36.96 0 73.92 0 112 0 C112.6261198 6.88731776 113.15084094 13.48218949 113.1328125 20.3515625 C113.13376923 21.17707611 113.13472595 22.00258972 113.13571167 22.8531189 C113.1363846 24.57216469 113.13459319 26.29121317 113.13037109 28.01025391 C113.12494037 30.65402778 113.13038111 33.29763707 113.13671875 35.94140625 C113.13605916 37.61979204 113.13478047 39.29817773 113.1328125 40.9765625 C113.13483673 41.76809723 113.13686096 42.55963196 113.13894653 43.37515259 C113.11499765 48.88500235 113.11499765 48.88500235 112 50 C110.54518505 50.0956161 109.08574514 50.12188351 107.62779236 50.12025452 C106.68403244 50.12162918 105.74027252 50.12300385 104.76791382 50.12442017 C103.72422638 50.12082489 102.68053894 50.11722961 101.60522461 50.11352539 C100.51267868 50.11367142 99.42013275 50.11381744 98.29447937 50.1139679 C94.66374767 50.11326822 91.03306654 50.10547329 87.40234375 50.09765625 C84.89270557 50.09579226 82.38306702 50.09436827 79.87342834 50.09336853 C73.93065407 50.08993348 67.98789616 50.08204273 62.04513031 50.07201904 C53.43224996 50.0578058 44.81936343 50.05254739 36.2064743 50.04621124 C24.13763437 50.03649856 12.06884226 50.01734306 0 50 C0 33.5 0 17 0 0 Z " fill="#FDFDFD" transform="translate(117,474)"/>
<path d="M0 0 C36.63 0 73.26 0 111 0 C111 48 111 48 110 50 C73.7 50 37.4 50 0 50 C0 33.5 0 17 0 0 Z " fill="#FEFEFE" transform="translate(795,474)"/>
<path d="M0 0 C54.12 0 108.24 0 164 0 C164 10.89 164 21.78 164 33 C109.88 33 55.76 33 0 33 C0 22.11 0 11.22 0 0 Z " fill="#181E23" transform="translate(431,622)"/>
<path d="M0 0 C14.58571527 -0.0225479 29.17141907 -0.04091327 43.75714779 -0.05181217 C50.52910421 -0.05697491 57.30104905 -0.06401718 64.07299805 -0.07543945 C70.60224426 -0.08644714 77.13148294 -0.09227625 83.66073799 -0.09487724 C86.15793612 -0.09673199 88.65513355 -0.10035354 91.15232658 -0.10573006 C94.63664069 -0.11293684 98.12090431 -0.1139911 101.60522461 -0.11352539 C102.64891205 -0.11712067 103.69259949 -0.12071594 104.76791382 -0.12442017 C105.71167374 -0.1230455 106.65543365 -0.12167084 107.62779236 -0.12025452 C108.45279708 -0.12117631 109.2778018 -0.1220981 110.12780666 -0.12304783 C112 0 112 0 113 1 C113 16.18 113 31.36 113 47 C75.71 47 38.42 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FCFCFC" transform="translate(117,414)"/>
<path d="M0 0 C0 15.84 0 31.68 0 48 C-36.96 48 -73.92 48 -112 48 C-112 32.49 -112 16.98 -112 1 C-92.15950898 0.75043408 -92.15950898 0.75043408 -83.5078125 0.64453125 C-77.67218771 0.57308961 -71.83656856 0.50132395 -66.00097656 0.42724609 C-43.99861261 0.14838814 -22.0044946 -0.06103882 0 0 Z " fill="#FAFAFA" transform="translate(906,537)"/>
<path d="M0 0 C36.63 0 73.26 0 111 0 C111 15.51 111 31.02 111 47 C74.37 47 37.74 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FBFCFC" transform="translate(118,538)"/>
<path d="M0 0 C36.3 0 72.6 0 110 0 C110 15.18 110 30.36 110 46 C86.98227311 46.96572558 64.03031552 47.12476904 40.99664307 47.06866455 C36.24190736 47.05823048 31.48716586 47.05382501 26.73242188 47.04882812 C17.48826669 47.03826689 8.24413623 47.02131845 -1 47 C-1.02529825 40.62793828 -1.04284496 34.25588946 -1.05493164 27.88378906 C-1.05996891 25.71483883 -1.06679891 23.545892 -1.07543945 21.37695312 C-1.08753151 18.26431437 -1.09323428 15.1517204 -1.09765625 12.0390625 C-1.10281754 11.065065 -1.10797882 10.0910675 -1.11329651 9.08755493 C-1.11337204 8.18677216 -1.11344757 7.28598938 -1.11352539 6.35791016 C-1.115746 5.56293121 -1.11796661 4.76795227 -1.12025452 3.94888306 C-1 2 -1 2 0 0 Z " fill="#FBFCFB" transform="translate(795,414)"/>
<path d="M0 0 C34.98 0 69.96 0 106 0 C106 15.84 106 31.68 106 48 C71.02 48 36.04 48 0 48 C0 32.16 0 16.32 0 0 Z " fill="#FBFCFC" transform="translate(154,658)"/>
<path d="M0 0 C34.32 0 68.64 0 104 0 C104 15.84 104 31.68 104 48 C69.35 48 34.7 48 -1 48 C-1 31.99652815 -0.6951464 15.9883671 0 0 Z " fill="#FBFBFB" transform="translate(765,658)"/>
<path d="M0 0 C10.56252046 -0.02735617 21.1249617 -0.05092969 31.6875 -0.0625 C32.46348038 -0.06335602 33.23946075 -0.06421204 34.03895569 -0.06509399 C57.71503621 -0.08818645 81.33439347 0.16962784 105 1 C105 16.18 105 31.36 105 47 C70.35 47 35.7 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FCFDFD" transform="translate(125,598)"/>
<path d="M0 0 C34.32 0 68.64 0 104 0 C104 15.51 104 31.02 104 47 C69.68 47 35.36 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FBFCFC" transform="translate(794,598)"/>
<path d="M0 0 C37.62 0 75.24 0 114 0 C114 12.54 114 25.08 114 38 C76.38 38 38.76 38 0 38 C0 25.46 0 12.92 0 0 Z " fill="#474B51" transform="translate(456,716)"/>
<path d="M0 0 C30.69 0 61.38 0 93 0 C93 15.18 93 30.36 93 46 C62.31 46 31.62 46 0 46 C0 30.82 0 15.64 0 0 Z " fill="#FCFDFD" transform="translate(732,719)"/>
<path d="M0 0 C30.69 0 61.38 0 93 0 C93 14.85 93 29.7 93 45 C62.31 45 31.62 45 0 45 C0 30.15 0 15.3 0 0 Z " fill="#FEFEFE" transform="translate(200,720)"/>
<path d="M0 0 C11.22 0 22.44 0 34 0 C34.6943434 30.12954393 35.09747148 60.23833134 35.0625 90.375 C35.06121597 91.48948643 35.06121597 91.48948643 35.05990601 92.62648773 C35.04860423 101.7510267 35.02558276 110.87548153 35 120 C23.45 120 11.9 120 0 120 C0 80.4 0 40.8 0 0 Z " fill="#474B50" transform="translate(258,470)"/>
<path d="M0 0 C30.36 0 60.72 0 92 0 C92 14.19 92 28.38 92 43 C61.64 43 31.28 43 0 43 C0 28.81 0 14.62 0 0 Z " fill="#FCFDFD" transform="translate(259,781)"/>
<path d="M0 0 C30.03 0 60.06 0 91 0 C91 14.19 91 28.38 91 43 C60.97 43 30.94 43 0 43 C0 28.81 0 14.62 0 0 Z " fill="#FCFDFD" transform="translate(674,781)"/>
<path d="M0 0 C16.5 0 33 0 50 0 C50 25.41 50 50.82 50 77 C33.5 77 17 77 0 77 C0 51.59 0 26.18 0 0 Z " fill="#2CCDD4" transform="translate(582,478)"/>
<path d="M0 0 C10.56 0 21.12 0 32 0 C32 39.27 32 78.54 32 119 C21.44 119 10.88 119 0 119 C0 79.73 0 40.46 0 0 Z " fill="#474B50" transform="translate(732,471)"/>
<path d="M0 0 C16.17 0 32.34 0 49 0 C49 25.41 49 50.82 49 77 C32.83 77 16.66 77 0 77 C0 51.59 0 26.18 0 0 Z " fill="#2DCCD2" transform="translate(394,478)"/>
<path d="M0 0 C15.84 0 31.68 0 48 0 C48 0.33 48 0.66 48 1 C32.49 1 16.98 1 1 1 C1 25.75 1 50.5 1 76 C28.06 75.67 55.12 75.34 83 75 C82.67 65.76 82.34 56.52 82 47 C81.93702338 38.74754429 81.90172306 30.50164687 81.9375 22.25 C81.94254243 20.16145945 81.94710032 18.07291767 81.95117188 15.984375 C81.96192066 10.98955155 81.97902162 5.99479052 82 1 C82.33 1 82.66 1 83 1 C83.57831229 19.95510799 84.11107346 38.91589618 84.23217773 57.88024902 C84.24403757 59.45720133 84.26110851 61.03412324 84.28344727 62.61096191 C84.31307026 64.8085447 84.32331211 67.00536665 84.328125 69.203125 C84.3374707 70.44739258 84.34681641 71.69166016 84.35644531 72.97363281 C84 76 84 76 82.73034668 77.68478394 C80.56608158 79.32981626 79.63802574 79.28920572 76.95800781 79.06982422 C75.71830231 78.98657394 75.71830231 78.98657394 74.45355225 78.90164185 C73.54591125 78.82839691 72.63827026 78.75515198 71.703125 78.6796875 C62.53186675 78.09888659 53.42327768 77.92843278 44.234375 78.0078125 C42.339151 78.0199881 42.339151 78.0199881 40.40563965 78.03240967 C35.17784805 78.06788344 29.95020985 78.1100683 24.72265625 78.17138672 C20.85162217 78.21602536 16.98061225 78.23702797 13.109375 78.2578125 C11.32536285 78.28574387 11.32536285 78.28574387 9.50531006 78.3142395 C7.87024506 78.31942093 7.87024506 78.31942093 6.20214844 78.32470703 C4.76279938 78.33922409 4.76279938 78.33922409 3.29437256 78.35403442 C2.15865814 78.17878738 2.15865814 78.17878738 1 78 C-1.11766624 74.82350064 -1.24885987 74.11349575 -1.23046875 70.48828125 C-1.22917969 69.52510986 -1.22789062 68.56193848 -1.2265625 67.56958008 C-1.21367187 66.49474365 -1.20078125 65.41990723 -1.1875 64.3125 C-1.18105469 63.18158936 -1.17460937 62.05067871 -1.16796875 60.88549805 C-1.00069226 40.5888483 -0.4546929 20.29067045 0 0 Z " fill="#4C0F0C" transform="translate(472,225)"/>
<path d="M0 0 C8.45159814 -0.02350035 16.90318192 -0.04110214 25.35480499 -0.05181217 C29.28212158 -0.05696064 33.20941531 -0.06390567 37.13671875 -0.07543945 C53.56520732 -0.12238185 69.94755695 -0.05605073 86.36132812 0.74389648 C96.40041688 1.22706882 106.45208983 1.36454744 116.5 1.5625 C118.60677558 1.60589009 120.71354645 1.64950917 122.8203125 1.69335938 C127.88015843 1.79819282 132.94005639 1.8999973 138 2 C138 2.33 138 2.66 138 3 C92.79 3 47.58 3 1 3 C1 5.64 1 8.28 1 11 C0.67 11 0.34 11 0 11 C0 7.37 0 3.74 0 0 Z " fill="#6F7576" transform="translate(326,395)"/>
<path d="M0 0 C0.33 0 0.66 0 1 0 C1 7.26 1 14.52 1 22 C37.3 22 73.6 22 111 22 C111.495 22.99 111.495 22.99 112 24 C74.71 24 37.42 24 -1 24 C-0.67 16.08 -0.34 8.16 0 0 Z " fill="#0E1418" transform="translate(551,563)"/>
<path d="M0 0 C0.33 0 0.66 0 1 0 C1.57831229 18.95510799 2.11107346 37.91589618 2.23217773 56.88024902 C2.24403757 58.45720133 2.26110851 60.03412324 2.28344727 61.61096191 C2.31307026 63.8085447 2.32331211 66.00536665 2.328125 68.203125 C2.3374707 69.44739258 2.34681641 70.69166016 2.35644531 71.97363281 C2 75 2 75 0.79858398 76.92016602 C-1.65515594 78.39334257 -3.26106965 78.2550765 -6.10546875 78.07421875 C-7.08837891 78.01943359 -8.07128906 77.96464844 -9.08398438 77.90820312 C-10.62022461 77.79895508 -10.62022461 77.79895508 -12.1875 77.6875 C-13.22326172 77.62626953 -14.25902344 77.56503906 -15.32617188 77.50195312 C-17.88531957 77.34864013 -20.44267045 77.1807893 -23 77 C-23.33 76.34 -23.66 75.68 -24 75 C-11.625 74.505 -11.625 74.505 1 74 C0.835 70.32875 0.67 66.6575 0.5 62.875 C-0.33621628 41.9290255 -0.08802477 20.95802133 0 0 Z " fill="#390D0B" transform="translate(554,226)"/>
<path d="M0 0 C0.66 0 1.32 0 2 0 C2 15.18 2 30.36 2 46 C0.68 45.67 -0.64 45.34 -2 45 C-1.88269497 38.75188831 -1.75785931 32.50396027 -1.62768555 26.25610352 C-1.58426577 24.12885993 -1.54259607 22.0015799 -1.50268555 19.87426758 C-1.44515293 16.82361849 -1.38141033 13.77315411 -1.31640625 10.72265625 C-1.29969376 9.76537125 -1.28298126 8.80808624 -1.26576233 7.8217926 C-1.24581711 6.94095505 -1.22587189 6.06011749 -1.20532227 5.15258789 C-1.18977798 4.37315323 -1.1742337 3.59371857 -1.15821838 2.79066467 C-1 1 -1 1 0 0 Z " fill="#D1D3D3" transform="translate(228,599)"/>
<path d="M0 0 C35.64 0.33 71.28 0.66 108 1 C108 1.33 108 1.66 108 2 C54.54 2.495 54.54 2.495 0 3 C0 2.01 0 1.02 0 0 Z " fill="#F5F7F7" transform="translate(117,521)"/>
<path d="M0 0 C6.93 0 13.86 0 21 0 C21 0.99 21 1.98 21 3 C14.07 3 7.14 3 0 3 C0 2.01 0 1.02 0 0 Z " fill="#272A2A" transform="translate(158,292)"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="135" height="62" viewBox="0 0 135 62" fill="none">
<path d="M3.168 48V22.272H9.648L9.888 28.464L9.216 28.176C9.568 26.8 10.096 25.632 10.8 24.672C11.536 23.712 12.416 22.976 13.44 22.464C14.464 21.952 15.584 21.696 16.8 21.696C18.944 21.696 20.672 22.32 21.984 23.568C23.328 24.816 24.192 26.496 24.576 28.608L23.664 28.656C23.952 27.152 24.448 25.888 25.152 24.864C25.888 23.808 26.784 23.024 27.84 22.512C28.896 21.968 30.08 21.696 31.392 21.696C33.184 21.696 34.72 22.064 36 22.8C37.28 23.536 38.272 24.64 38.976 26.112C39.68 27.552 40.032 29.328 40.032 31.44V48H32.832V33.456C32.832 31.44 32.528 29.936 31.92 28.944C31.312 27.92 30.32 27.408 28.944 27.408C28.08 27.408 27.344 27.648 26.736 28.128C26.128 28.608 25.648 29.312 25.296 30.24C24.976 31.136 24.816 32.24 24.816 33.552V48H18.336V33.552C18.336 31.568 18.048 30.048 17.472 28.992C16.896 27.936 15.904 27.408 14.496 27.408C13.632 27.408 12.88 27.648 12.24 28.128C11.632 28.608 11.168 29.312 10.848 30.24C10.528 31.168 10.368 32.272 10.368 33.552V48H3.168ZM54.2254 48.576C51.5694 48.576 49.4894 47.728 47.9854 46.032C46.5134 44.304 45.7774 41.904 45.7774 38.832V22.272H52.9774V37.152C52.9774 39.136 53.2814 40.592 53.8894 41.52C54.4974 42.416 55.4574 42.864 56.7694 42.864C58.2414 42.864 59.3774 42.368 60.1774 41.376C61.0094 40.352 61.4254 38.832 61.4254 36.816V22.272H68.6254V48H62.0494L61.8574 40.608L62.7694 40.8C62.3854 43.36 61.4734 45.296 60.0334 46.608C58.5934 47.92 56.6574 48.576 54.2254 48.576ZM72.8486 48L82.0166 34.944L73.0886 22.272H80.7206L86.2406 30.528L91.5686 22.272H99.3926L90.5126 34.992L99.6326 48H92.0006L86.3366 39.264L80.6246 48H72.8486Z" fill="white"/>
<rect x="109" y="13" width="26" height="35" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
<circle cx="250" cy="250" r="250" fill="#fff"/>
<path d="m335 150h40v200h-40zm-130 0a100 100 0 1 0 0 200 100 100 0 1 0 0-200zm0 40a60 60 0 1 1 0 120 60 60 0 1 1 0-120z"/>
</svg>

After

Width:  |  Height:  |  Size: 293 B

+8
View File
@@ -0,0 +1,8 @@
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<circle cx="128" cy="128" r="120" fill="black"/>
<polygon
points="128,70 178,170 78,170"
fill="white"
/>
</svg>

After

Width:  |  Height:  |  Size: 216 B

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>Scaleway icon</title><path d="M16.61 11.11v5.72a1.77 1.77 0 0 1-1.54 1.69h-4a1.43 1.43 0 0 1-1.31-1.22 1.09 1.09 0 0 1 0-.18 1.37 1.37 0 0 1 1.37-1.36h1.74a1 1 0 0 0 1-1v-3.62a1.4 1.4 0 0 1 1.18-1.39h.17a1.37 1.37 0 0 1 1.39 1.36zm-6.46 1.74V9.26a1 1 0 0 1 1-1H13a1.37 1.37 0 0 0 1.37-1.37 1 1 0 0 0 0-.17 1.45 1.45 0 0 0-1.41-1.2H9a1.81 1.81 0 0 0-1.58 1.66v5.7a1.37 1.37 0 0 0 1.37 1.37H9a1.4 1.4 0 0 0 1.15-1.4zm12-4.29V20A4.53 4.53 0 0 1 18 24h-7.58a8.57 8.57 0 0 1-8.56-8.57V4.54A4.54 4.54 0 0 1 6.4 0h7.18a8.56 8.56 0 0 1 8.56 8.56zm-2.74 0a5.83 5.83 0 0 0-5.82-5.82H6.4a1.79 1.79 0 0 0-1.8 1.8v10.89a5.83 5.83 0 0 0 5.82 5.8h7.44a1.79 1.79 0 0 0 1.54-1.48z"/></svg>

After

Width:  |  Height:  |  Size: 913 B

+11 -14
View File
@@ -1,15 +1,16 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "registry",
"devDependencies": {
"@types/bun": "^1.2.21",
"bun-types": "^1.2.21",
"dedent": "^1.6.0",
"@types/bun": "^1.3.4",
"bun-types": "^1.3.4",
"dedent": "^1.7.0",
"gray-matter": "^4.0.3",
"marked": "^16.2.0",
"prettier": "^3.6.2",
"marked": "^16.4.2",
"prettier": "^3.7.4",
"prettier-plugin-sh": "^0.18.0",
"prettier-plugin-terraform-formatter": "^1.2.1",
"shellcheck": "^4.1.0",
@@ -30,12 +31,10 @@
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
"@types/node": ["@types/node@24.0.14", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw=="],
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
"@xhmikosr/decompress-tar": ["@xhmikosr/decompress-tar@8.1.0", "", { "dependencies": { "file-type": "^20.5.0", "is-stream": "^2.0.1", "tar-stream": "^3.1.7" } }, "sha512-m0q8x6lwxenh1CrsTby0Jrjq4vzW/QU1OLhTHMQLEdHpmjR1lgahGz++seZI0bXF3XcZw3U3xHfqZSz+JPP2Gg=="],
"@xhmikosr/decompress-unzip": ["@xhmikosr/decompress-unzip@7.1.0", "", { "dependencies": { "file-type": "^20.5.0", "get-stream": "^6.0.1", "yauzl": "^3.1.2" } }, "sha512-oqTYAcObqTlg8owulxFTqiaJkfv2SHsxxxz9Wg4krJAHVzGWlZsU8tAB30R6ow+aHrfv4Kub6WQ8u04NWVPUpA=="],
@@ -64,7 +63,7 @@
"buffer-fill": ["buffer-fill@1.0.0", "", {}, "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="],
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
@@ -76,8 +75,6 @@
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decompress": ["decompress@4.2.1", "", { "dependencies": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", "decompress-targz": "^4.0.0", "decompress-unzip": "^4.0.1", "graceful-fs": "^4.1.10", "make-dir": "^1.0.0", "pify": "^2.3.0", "strip-dirs": "^2.0.0" } }, "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ=="],
@@ -90,7 +87,7 @@
"decompress-unzip": ["decompress-unzip@4.0.1", "", { "dependencies": { "file-type": "^3.8.0", "get-stream": "^2.2.0", "pify": "^2.3.0", "yauzl": "^2.4.2" } }, "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw=="],
"dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="],
"dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
@@ -182,7 +179,7 @@
"make-dir": ["make-dir@1.3.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ=="],
"marked": ["marked@16.2.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg=="],
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
@@ -206,7 +203,7 @@
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
"prettier-plugin-sh": ["prettier-plugin-sh@0.18.0", "", { "dependencies": { "@reteps/dockerfmt": "^0.3.6", "sh-syntax": "^0.5.8" }, "peerDependencies": { "prettier": "^3.6.0" } }, "sha512-cW1XL27FOJQ/qGHOW6IHwdCiNWQsAgK+feA8V6+xUTaH0cD3Mh+tFAtBvEEWvuY6hTDzRV943Fzeii+qMOh7nQ=="],
+5 -5
View File
@@ -10,12 +10,12 @@
"update-version": "./update-version.sh"
},
"devDependencies": {
"@types/bun": "^1.2.21",
"bun-types": "^1.2.21",
"dedent": "^1.6.0",
"@types/bun": "^1.3.4",
"bun-types": "^1.3.4",
"dedent": "^1.7.0",
"gray-matter": "^4.0.3",
"marked": "^16.2.0",
"prettier": "^3.6.2",
"marked": "^16.4.2",
"prettier": "^3.7.4",
"prettier-plugin-sh": "^0.18.0",
"prettier-plugin-terraform-formatter": "^1.2.1",
"shellcheck": "^4.1.0"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 KiB

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

+7
View File
@@ -0,0 +1,7 @@
---
display_name: "Excellencedev"
bio: "Love to contribute"
avatar: "./.images/avatar.png"
support_email: "ademiluyisuccessandexcellence@gmail.com"
status: "community"
---
@@ -0,0 +1,32 @@
---
display_name: Hetzner Cloud Server
description: Provision Hetzner Cloud servers as Coder workspaces
icon: ../../../../.icons/hetzner.svg
tags: [vm, linux, hetzner]
---
# Remote Development on Hetzner Cloud (Linux)
Provision Hetzner Cloud servers as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
> [!WARNING]
> **Workspace Storage Persistence:** When a workspace is stopped, the Hetzner Cloud server instance is stopped but your home volume and stored data persist. This means your files and data remain intact when you resume the workspace.
> [!IMPORTANT]
> **Volume Management & Costs:** Hetzner Cloud volumes persist even when workspaces are stopped and will continue to incur storage costs (€0.0476/GB/month). Volumes are only automatically deleted when the workspace is completely deleted. Monitor your volumes in the [Hetzner Cloud Console](https://console.hetzner.cloud/) to manage costs effectively.
## Prerequisites
To deploy workspaces as Hetzner Cloud servers, you'll need:
- Hetzner Cloud [API token](https://console.hetzner.cloud/projects) (create under Security > API Tokens)
### Authentication
This template assumes that the Coder Provisioner is run in an environment that is authenticated with Hetzner Cloud.
Obtain a Hetzner Cloud API token from your [Hetzner Cloud Console](https://console.hetzner.cloud/projects) and provide it as the `hcloud_token` variable when creating a workspace.
For more authentication options, see the [Terraform provider documentation](https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs#authentication).
> [!NOTE]
> This template is designed to be a starting point. Edit the Terraform to extend the template to support your use case.
@@ -0,0 +1,62 @@
#cloud-config
users:
- name: ${username}
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
groups: sudo
shell: /bin/bash
packages:
- git
%{ if home_volume_label != "" ~}
fs_setup:
- device: /dev/disk/by-id/scsi-0HC_Volume_${volume_id}
filesystem: ext4
label: ${home_volume_label}
overwrite: false # This prevents reformatting the disk on every boot
mounts:
- [
"/dev/disk/by-id/scsi-0HC_Volume_${volume_id}",
"/home/${username}",
ext4,
"defaults,uid=1000,gid=1000",
]
%{ endif ~}
write_files:
- path: /opt/coder/init
permissions: "0755"
encoding: b64
content: ${init_script}
- path: /etc/systemd/system/coder-agent.service
permissions: "0644"
content: |
[Unit]
Description=Coder Agent
After=network-online.target
Wants=network-online.target
[Service]
User=${username}
ExecStart=/opt/coder/init
Environment=CODER_AGENT_TOKEN=${coder_agent_token}
Restart=always
RestartSec=10
TimeoutStopSec=90
KillMode=process
OOMScoreAdjust=-900
SyslogIdentifier=coder-agent
[Install]
WantedBy=multi-user.target
runcmd:
%{ if home_volume_label != "" ~}
- |
until [ -e /dev/disk/by-id/scsi-0HC_Volume_${volume_id} ]; do
echo "Waiting for volume device..."
sleep 2
done
%{ endif ~}
- mount -a
- chown ${username}:${username} /home/${username}
- systemctl enable coder-agent
- systemctl start coder-agent
@@ -0,0 +1,224 @@
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
}
coder = {
source = "coder/coder"
}
http = {
source = "hashicorp/http"
version = "~> 3.0"
}
}
}
variable "hcloud_token" {
sensitive = true
}
provider "hcloud" {
token = var.hcloud_token
}
data "http" "hcloud_locations" {
url = "https://api.hetzner.cloud/v1/locations"
request_headers = {
Authorization = "Bearer ${var.hcloud_token}"
Accept = "application/json"
}
}
data "http" "hcloud_server_types" {
url = "https://api.hetzner.cloud/v1/server_types"
request_headers = {
Authorization = "Bearer ${var.hcloud_token}"
Accept = "application/json"
}
}
# Available locations: https://docs.hetzner.com/cloud/general/locations/
data "coder_parameter" "hcloud_location" {
name = "hcloud_location"
display_name = "Hetzner Location"
description = "Select the Hetzner Cloud location for your workspace."
type = "string"
default = "fsn1"
dynamic "option" {
for_each = local.hcloud_locations
content {
name = format(
"%s (%s, %s)",
upper(option.value.name),
option.value.city,
option.value.country
)
value = option.value.name
}
}
}
# Available server types: https://docs.hetzner.com/cloud/servers/overview/
data "coder_parameter" "hcloud_server_type" {
name = "hcloud_server_type"
display_name = "Hetzner Server Type"
description = "Select the Hetzner Cloud server type for your workspace."
type = "string"
dynamic "option" {
for_each = local.hcloud_server_type_options_for_selected_location
content {
name = option.value.name
value = option.value.value
}
}
}
resource "hcloud_server" "dev" {
count = data.coder_workspace.me.start_count
name = "coder-${data.coder_workspace.me.name}-dev"
image = "ubuntu-24.04"
server_type = data.coder_parameter.hcloud_server_type.value
location = data.coder_parameter.hcloud_location.value
public_net {
ipv4_enabled = true
ipv6_enabled = true
}
user_data = templatefile("cloud-config.yaml.tftpl", {
username = lower(data.coder_workspace_owner.me.name)
home_volume_label = "coder-${data.coder_workspace.me.id}-home"
volume_id = hcloud_volume.home_volume.id
init_script = base64encode(coder_agent.main.init_script)
coder_agent_token = coder_agent.main.token
})
labels = {
"coder_workspace_name" = data.coder_workspace.me.name,
"coder_workspace_owner" = data.coder_workspace_owner.me.name,
}
}
resource "hcloud_volume" "home_volume" {
name = "coder-${data.coder_workspace.me.id}-home"
size = data.coder_parameter.home_volume_size.value
location = data.coder_parameter.hcloud_location.value
labels = {
"coder_workspace_name" = data.coder_workspace.me.name,
"coder_workspace_owner" = data.coder_workspace_owner.me.name,
}
}
resource "hcloud_volume_attachment" "home_volume_attachment" {
count = data.coder_workspace.me.start_count
volume_id = hcloud_volume.home_volume.id
server_id = hcloud_server.dev[count.index].id
automount = false
}
locals {
username = lower(data.coder_workspace_owner.me.name)
# --------------------
# Locations
# --------------------
hcloud_locations = [
for loc in jsondecode(data.http.hcloud_locations.response_body).locations : {
name = loc.name
city = loc.city
country = loc.country
}
]
# --------------------
# Server Types
# --------------------
hcloud_server_types = {
for st in jsondecode(data.http.hcloud_server_types.response_body).server_types :
st.name => {
cores = st.cores
memory_gb = st.memory
disk_gb = st.disk
locations = [for l in st.locations : l.name]
deprecated = st.deprecated
}
if st.deprecated == false
}
hcloud_server_type_options_for_selected_location = [
for name, meta in local.hcloud_server_types : {
name = format(
"%s (%d vCPU, %dGB RAM, %dGB)",
upper(name),
meta.cores,
meta.memory_gb,
meta.disk_gb
)
value = name
}
if contains(
meta.locations,
data.coder_parameter.hcloud_location.value
)
]
}
data "coder_provisioner" "me" {}
provider "coder" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "home_volume_size" {
name = "home_volume_size"
display_name = "Home volume size"
description = "How large would you like your home volume to be (in GB)?"
type = "number"
default = "20"
mutable = false
validation {
min = 1
max = 100 # Adjust the max size as needed
}
}
resource "coder_agent" "main" {
os = "linux"
arch = "amd64"
metadata {
key = "cpu"
display_name = "CPU Usage"
interval = 5
timeout = 5
script = "coder stat cpu"
}
metadata {
key = "memory"
display_name = "Memory Usage"
interval = 5
timeout = 5
script = "coder stat mem"
}
metadata {
key = "home"
display_name = "Home Usage"
interval = 600 # every 10 minutes
timeout = 30 # df can take a while on large filesystems
script = "coder stat disk --path /home/${local.username}"
}
}
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

+3 -3
View File
@@ -15,7 +15,7 @@ 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.3"
version = "1.0.4"
agent_id = coder_agent.example.id
}
```
@@ -39,7 +39,7 @@ module "tmux" {
```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.3"
version = "1.0.4"
agent_id = coder_agent.example.id
tmux_config = "" # Optional: custom tmux.conf content
save_interval = 1 # Optional: save interval in minutes
@@ -78,7 +78,7 @@ This module can provision multiple tmux sessions, each as a separate app in the
```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.3"
version = "1.0.4"
agent_id = var.agent_id
sessions = ["default", "dev", "anomaly"]
tmux_config = <<-EOT
+1 -1
View File
@@ -55,7 +55,7 @@ resource "coder_script" "tmux" {
display_name = "tmux"
icon = "/icon/terminal.svg"
script = templatefile("${path.module}/scripts/run.sh", {
TMUX_CONFIG = var.tmux_config
TMUX_CONFIG = base64encode(var.tmux_config)
SAVE_INTERVAL = var.save_interval
})
run_on_start = true
+2 -2
View File
@@ -4,7 +4,7 @@ BOLD='\033[0;1m'
# Convert templated variables to shell variables
SAVE_INTERVAL="${SAVE_INTERVAL}"
TMUX_CONFIG="${TMUX_CONFIG}"
TMUX_CONFIG=$(echo -n "${TMUX_CONFIG}" | base64 -d)
# Function to install tmux
install_tmux() {
@@ -73,7 +73,7 @@ setup_tmux_config() {
mkdir -p "$config_dir"
if [ -n "$TMUX_CONFIG" ]; then
printf "$TMUX_CONFIG" > "$config_file"
printf "%s" "$TMUX_CONFIG" > "$config_file"
printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n"
else
cat > "$config_file" << EOF
Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

+3 -3
View File
@@ -13,7 +13,7 @@ Run Auggie CLI in your workspace to access Augment's AI coding assistant with ad
```tf
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.2.2"
version = "0.3.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -47,7 +47,7 @@ module "coder-login" {
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.2.2"
version = "0.3.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
@@ -103,7 +103,7 @@ EOF
```tf
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.2.2"
version = "0.3.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
+7 -3
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
version = ">= 2.12"
}
}
}
@@ -179,7 +179,7 @@ locals {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0"
version = "2.0.0"
agent_id = var.agent_id
folder = local.folder
@@ -229,4 +229,8 @@ module "agentapi" {
ARG_MCP_CONFIG='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \
/tmp/install.sh
EOT
}
}
output "task_app_id" {
value = module.agentapi.task_app_id
}
@@ -13,7 +13,7 @@ Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-c
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3"
version = "0.3.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
}
@@ -51,7 +51,7 @@ data "coder_parameter" "ai_prompt" {
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3"
version = "0.3.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
@@ -71,7 +71,7 @@ Customize tool permissions, MCP servers, and Copilot settings:
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3"
version = "0.3.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
@@ -142,7 +142,7 @@ variable "github_token" {
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3"
version = "0.3.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
github_token = var.github_token
@@ -156,7 +156,7 @@ Run Copilot as a command-line tool without task reporting or web interface. This
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3"
version = "0.3.0"
agent_id = coder_agent.example.id
workdir = "/home/coder"
report_tasks = false
+9 -5
View File
@@ -3,7 +3,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
version = ">= 2.12"
}
}
}
@@ -242,7 +242,7 @@ resource "coder_env" "github_token" {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0"
version = "2.0.0"
agent_id = var.agent_id
folder = local.workdir
@@ -268,7 +268,7 @@ module "agentapi" {
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)}' \
@@ -288,7 +288,7 @@ module "agentapi" {
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}' \
@@ -299,4 +299,8 @@ module "agentapi" {
ARG_COPILOT_MODEL='${var.copilot_model}' \
/tmp/install.sh
EOT
}
}
output "task_app_id" {
value = module.agentapi.task_app_id
}
@@ -13,7 +13,7 @@ Run the Cursor Agent CLI in your workspace for interactive coding assistance and
```tf
module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder"
version = "0.2.2"
version = "0.3.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
}
@@ -42,7 +42,7 @@ module "coder-login" {
module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder"
version = "0.2.2"
version = "0.3.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
@@ -159,7 +159,7 @@ describe("cursor-cli", async () => {
"-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(`--model ${model}`);
expect(startLog.stdout).toContain("-f");
expect(startLog.stdout).toContain("test prompt");
});
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
version = ">= 2.12"
}
}
}
@@ -132,7 +132,7 @@ resource "coder_env" "cursor_api_key" {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0"
version = "2.0.0"
agent_id = var.agent_id
folder = local.folder
@@ -179,3 +179,7 @@ module "agentapi" {
/tmp/install.sh
EOT
}
output "task_app_id" {
value = module.agentapi.task_app_id
}
@@ -50,7 +50,7 @@ ARGS=()
# global flags
if [ -n "$ARG_MODEL" ]; then
ARGS+=("-m" "$ARG_MODEL")
ARGS+=("--model" "$ARG_MODEL")
fi
if [ "$ARG_FORCE" = "true" ]; then
ARGS+=("-f")
+4 -4
View File
@@ -13,7 +13,7 @@ Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace
```tf
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
version = "2.1.2"
version = "3.0.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
}
@@ -46,7 +46,7 @@ variable "gemini_api_key" {
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
version = "2.1.2"
version = "3.0.0"
agent_id = coder_agent.main.id
gemini_api_key = var.gemini_api_key
folder = "/home/coder/project"
@@ -94,7 +94,7 @@ data "coder_parameter" "ai_prompt" {
module "gemini" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/gemini/coder"
version = "2.1.2"
version = "3.0.0"
agent_id = coder_agent.main.id
gemini_api_key = var.gemini_api_key
gemini_model = "gemini-2.5-flash"
@@ -118,7 +118,7 @@ For enterprise users who prefer Google's Vertex AI platform:
```tf
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
version = "2.1.2"
version = "3.0.0"
agent_id = coder_agent.main.id
gemini_api_key = var.gemini_api_key
folder = "/home/coder/project"
+7 -3
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
version = ">= 2.12"
}
}
}
@@ -177,7 +177,7 @@ EOT
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0"
version = "2.0.0"
agent_id = var.agent_id
folder = local.folder
@@ -225,4 +225,8 @@ module "agentapi" {
GEMINI_TASK_PROMPT='${var.task_prompt}' \
/tmp/start.sh
EOT
}
}
output "task_app_id" {
value = module.agentapi.task_app_id
}
@@ -0,0 +1,64 @@
---
display_name: Open WebUI
description: A self-hosted AI chat interface supporting various LLM providers
icon: ../../../../.icons/openwebui.svg
verified: false
tags: [ai, llm, chat, web, python]
---
# Open WebUI
Open WebUI is a user-friendly web interface for interacting with Large Language Models. It provides a ChatGPT-like interface that can connect to various LLM providers including OpenAI, Ollama, and more.
```tf
module "open-webui" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/open-webui/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
}
```
![Open WebUI](../../.images/openwebui.png)
## Prerequisites
- **Python 3.11 or higher** must be installed in your image (with `venv` module)
- Port 7800 (default) or your custom port must be available
For Ubuntu/Debian, you can install Python 3.11 from [deadsnakes PPA](https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa):
```shell
sudo add-apt-repository -y ppa:deadsnakes/ppa
sudo apt-get update
sudo apt-get install -y python3.11 python3.11-venv
```
## Examples
### With OpenAI API Key
```tf
module "open-webui" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/open-webui/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
openai_api_key = var.openai_api_key
}
```
### Custom Port and Data Directory
```tf
module "open-webui" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/open-webui/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
http_server_port = 8080
data_dir = "/home/coder/open-webui-data"
}
```
@@ -0,0 +1,94 @@
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 "http_server_log_path" {
type = string
description = "The path to log Open WebUI to."
default = "/tmp/open-webui.log"
}
variable "http_server_port" {
type = number
description = "The port to run Open WebUI on."
default = 7800
}
variable "open_webui_version" {
type = string
description = "The version of Open WebUI to install"
default = "latest"
}
variable "data_dir" {
type = string
description = "The directory where Open WebUI stores its data (database, uploads, vector_db, cache)."
default = ".open-webui"
}
variable "openai_api_key" {
type = string
description = "OpenAI API key for accessing OpenAI models. If not provided, OpenAI integration will need to be configured manually in the UI."
default = ""
sensitive = true
}
variable "share" {
type = string
description = "The sharing level for the Open WebUI app. Set to 'owner' for private access, 'authenticated' for access by any authenticated user, or 'public' for public access."
default = "owner"
validation {
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
}
}
variable "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
}
resource "coder_script" "open-webui" {
agent_id = var.agent_id
display_name = "open-webui"
icon = "/icon/openwebui.svg"
script = templatefile("${path.module}/run.sh", {
HTTP_SERVER_LOG_PATH : var.http_server_log_path,
HTTP_SERVER_PORT : var.http_server_port,
VERSION : var.open_webui_version,
DATA_DIR : var.data_dir,
OPENAI_API_KEY : var.openai_api_key,
})
run_on_start = true
}
resource "coder_app" "open-webui" {
agent_id = var.agent_id
slug = "open-webui"
display_name = "Open WebUI"
url = "http://localhost:${var.http_server_port}"
icon = "/icon/openwebui.svg"
subdomain = true
share = var.share
order = var.order
group = var.group
}
@@ -0,0 +1,188 @@
mock_provider "coder" {}
run "test_defaults" {
command = plan
variables {
agent_id = "test-agent-123"
}
assert {
condition = var.http_server_port == 7800
error_message = "Default port should be 7800"
}
assert {
condition = var.http_server_log_path == "/tmp/open-webui.log"
error_message = "Default log path should be /tmp/open-webui.log"
}
assert {
condition = var.share == "owner"
error_message = "Default share should be 'owner'"
}
assert {
condition = var.open_webui_version == "latest"
error_message = "Default version should be 'latest'"
}
assert {
condition = coder_app.open-webui.subdomain == true
error_message = "App should use subdomain"
}
assert {
condition = coder_app.open-webui.display_name == "Open WebUI"
error_message = "App display name should be 'Open WebUI'"
}
}
run "test_custom_port" {
command = plan
variables {
agent_id = "test-agent-456"
http_server_port = 9000
}
assert {
condition = var.http_server_port == 9000
error_message = "Custom port should be 9000"
}
assert {
condition = coder_app.open-webui.url == "http://localhost:9000"
error_message = "App URL should use custom port"
}
}
run "test_custom_log_path" {
command = plan
variables {
agent_id = "test-agent-789"
http_server_log_path = "/var/log/open-webui.log"
}
assert {
condition = var.http_server_log_path == "/var/log/open-webui.log"
error_message = "Custom log path should be set"
}
}
run "test_share_authenticated" {
command = plan
variables {
agent_id = "test-agent-auth"
share = "authenticated"
}
assert {
condition = coder_app.open-webui.share == "authenticated"
error_message = "Share should be 'authenticated'"
}
}
run "test_share_public" {
command = plan
variables {
agent_id = "test-agent-public"
share = "public"
}
assert {
condition = coder_app.open-webui.share == "public"
error_message = "Share should be 'public'"
}
}
run "test_order_and_group" {
command = plan
variables {
agent_id = "test-agent-order"
order = 10
group = "AI Tools"
}
assert {
condition = coder_app.open-webui.order == 10
error_message = "Order should be 10"
}
assert {
condition = coder_app.open-webui.group == "AI Tools"
error_message = "Group should be 'AI Tools'"
}
}
run "test_custom_version" {
command = plan
variables {
agent_id = "test-agent-version"
open_webui_version = "0.5.0"
}
assert {
condition = var.open_webui_version == "0.5.0"
error_message = "Custom version should be '0.5.0'"
}
}
run "test_custom_data_dir" {
command = plan
variables {
agent_id = "test-agent-data"
data_dir = "/home/coder/open-webui-data"
}
assert {
condition = var.data_dir == "/home/coder/open-webui-data"
error_message = "Custom data_dir should be set"
}
}
run "test_default_data_dir" {
command = plan
variables {
agent_id = "test-agent-data-default"
}
assert {
condition = var.data_dir == ".open-webui"
error_message = "Default data_dir should be '.open-webui'"
}
}
run "test_openai_api_key" {
command = plan
variables {
agent_id = "test-agent-openai"
openai_api_key = "sk-test-key-123"
}
assert {
condition = var.openai_api_key == "sk-test-key-123"
error_message = "OpenAI API key should be set"
}
}
run "test_default_openai_api_key" {
command = plan
variables {
agent_id = "test-agent-openai-default"
}
assert {
condition = var.openai_api_key == ""
error_message = "Default OpenAI API key should be empty"
}
}
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env sh
set -eu
printf '\033[0;1mInstalling Open WebUI %s...\n\n' "${VERSION}"
check_python_version() {
python_cmd="$1"
if command -v "$python_cmd" > /dev/null 2>&1; then
version=$("$python_cmd" --version 2>&1 | awk '{print $2}')
major=$(echo "$version" | cut -d. -f1)
minor=$(echo "$version" | cut -d. -f2)
if [ "$major" -eq 3 ] && [ "$minor" -ge 11 ]; then
echo "$python_cmd"
return 0
fi
fi
return 1
}
PYTHON_CMD=""
for cmd in python3.13 python3.12 python3.11 python3 python; do
if result=$(check_python_version "$cmd"); then
PYTHON_CMD="$result"
echo "✅ Found suitable Python: $PYTHON_CMD ($($PYTHON_CMD --version 2>&1))"
break
fi
done
if [ -z "$PYTHON_CMD" ]; then
echo "❌ Python 3.11 or higher is required but not found."
echo ""
echo "Please install Python 3.11+ in your image. For example on Ubuntu/Debian:"
echo " sudo add-apt-repository -y ppa:deadsnakes/ppa"
echo " sudo apt-get update"
echo " sudo apt-get install -y python3.11 python3.11-venv"
exit 1
fi
VENV_DIR="$HOME/.open-webui-venv"
if [ ! -d "$VENV_DIR" ]; then
echo "📦 Creating virtual environment..."
"$PYTHON_CMD" -m venv "$VENV_DIR"
fi
. "$VENV_DIR/bin/activate"
if ! pip show open-webui > /dev/null 2>&1; then
echo "📦 Installing Open WebUI version ${VERSION}..."
if [ "${VERSION}" = "latest" ]; then
pip install open-webui
else
pip install "open-webui==${VERSION}"
fi
echo "🥳 Open WebUI has been installed"
else
echo "✅ Open WebUI is already installed"
fi
echo "👷 Starting Open WebUI in background..."
echo "Check logs at ${HTTP_SERVER_LOG_PATH}"
DATA_DIR="${DATA_DIR}" \
OPENAI_API_KEY="${OPENAI_API_KEY}" \
open-webui serve --host 0.0.0.0 --port "${HTTP_SERVER_PORT}" > "${HTTP_SERVER_LOG_PATH}" 2>&1 &
echo "🥳 Open WebUI is ready. HTTP server is listening on port ${HTTP_SERVER_PORT}"
@@ -0,0 +1,55 @@
---
display_name: Perplexica
description: Run Perplexica AI search engine in your workspace via Docker
icon: ../../../../.icons/perplexica.svg
verified: false
tags: [ai, search, docker]
---
# Perplexica
Run [Perplexica](https://github.com/ItzCrazyKns/Perplexica), a privacy-focused AI search engine, in your Coder workspace. Supports cloud providers (OpenAI, Anthropic Claude) and local LLMs via Ollama.
```tf
module "perplexica" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/perplexica/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
}
```
This module uses the full Perplexica image with embedded SearXNG for simpler setup with no external dependencies.
![Perplexica](../../.images/perplexica.png)
## Prerequisites
This module requires Docker to be available on the host.
## Examples
### With API Keys
```tf
module "perplexica" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/perplexica/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
openai_api_key = var.openai_api_key
anthropic_api_key = var.anthropic_api_key
}
```
### With Local Ollama
```tf
module "perplexica" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/perplexica/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
ollama_api_url = "http://ollama-external-endpoint:11434"
}
```
@@ -0,0 +1,108 @@
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 "docker_socket" {
type = string
description = "(Optional) Docker socket URI"
default = ""
}
variable "port" {
type = number
description = "The port to run Perplexica on."
default = 3000
}
variable "data_path" {
type = string
description = "Host path to mount for Perplexica data persistence."
default = "./perplexica-data"
}
variable "uploads_path" {
type = string
description = "Host path to mount for Perplexica file uploads."
default = "./perplexica-uploads"
}
variable "openai_api_key" {
type = string
description = "OpenAI API key."
default = ""
sensitive = true
}
variable "anthropic_api_key" {
type = string
description = "Anthropic API key for Claude models."
default = ""
sensitive = true
}
variable "ollama_api_url" {
type = string
description = "Ollama API URL for local LLM support."
default = ""
}
variable "share" {
type = string
default = "owner"
validation {
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
}
}
variable "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
}
resource "coder_script" "perplexica" {
agent_id = var.agent_id
display_name = "Perplexica"
icon = "/icon/perplexica.svg"
script = templatefile("${path.module}/run.sh", {
DOCKER_HOST : var.docker_socket,
PORT : var.port,
DATA_PATH : var.data_path,
UPLOADS_PATH : var.uploads_path,
OPENAI_API_KEY : var.openai_api_key,
ANTHROPIC_API_KEY : var.anthropic_api_key,
OLLAMA_API_URL : var.ollama_api_url,
})
run_on_start = true
}
resource "coder_app" "perplexica" {
agent_id = var.agent_id
slug = "perplexica"
display_name = "Perplexica"
url = "http://localhost:${var.port}"
icon = "/icon/perplexica.svg"
subdomain = true
share = var.share
order = var.order
group = var.group
}
@@ -0,0 +1,26 @@
run "plan_basic" {
command = plan
variables {
agent_id = "test-agent"
}
assert {
condition = resource.coder_app.perplexica.url == "http://localhost:3000"
error_message = "Default port should be 3000"
}
}
run "plan_custom_port" {
command = plan
variables {
agent_id = "test-agent"
port = 8080
}
assert {
condition = resource.coder_app.perplexica.url == "http://localhost:8080"
error_message = "Should use custom port"
}
}
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env sh
set -eu
BOLD='\033[0;1m'
RESET='\033[0m'
printf "$${BOLD}Starting Perplexica...$${RESET}\n"
# Set Docker host if provided
if [ -n "${DOCKER_HOST}" ]; then
export DOCKER_HOST="${DOCKER_HOST}"
fi
# Wait for docker to become ready
max_attempts=10
delay=2
attempt=1
while ! docker ps; do
if [ $attempt -ge $max_attempts ]; then
echo "Failed to list containers after $${max_attempts} attempts."
exit 1
fi
echo "Attempt $${attempt} failed, retrying in $${delay}s..."
sleep $delay
attempt=$(expr "$attempt" + 1)
delay=$(expr "$delay" \* 2)
done
# Pull the image
IMAGE="itzcrazykns1337/perplexica:latest"
docker pull "$${IMAGE}"
# Build docker run command
DOCKER_ARGS="-d --rm --name perplexica -p ${PORT}:3000"
# Add mounts - convert relative paths to absolute
DATA_PATH="${DATA_PATH}"
UPLOADS_PATH="${UPLOADS_PATH}"
mkdir -p "$${DATA_PATH}"
mkdir -p "$${UPLOADS_PATH}"
DATA_PATH_ABS=$(cd "$${DATA_PATH}" && pwd)
UPLOADS_PATH_ABS=$(cd "$${UPLOADS_PATH}" && pwd)
DOCKER_ARGS="$${DOCKER_ARGS} -v $${DATA_PATH_ABS}:/home/perplexica/data"
DOCKER_ARGS="$${DOCKER_ARGS} -v $${UPLOADS_PATH_ABS}:/home/perplexica/uploads"
# Add environment variables if provided
if [ -n "${OPENAI_API_KEY}" ]; then
DOCKER_ARGS="$${DOCKER_ARGS} -e OPENAI_API_KEY=${OPENAI_API_KEY}"
fi
if [ -n "${ANTHROPIC_API_KEY}" ]; then
DOCKER_ARGS="$${DOCKER_ARGS} -e ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}"
fi
if [ -n "${OLLAMA_API_URL}" ]; then
DOCKER_ARGS="$${DOCKER_ARGS} -e OLLAMA_API_URL=${OLLAMA_API_URL}"
fi
# Run container
docker run $${DOCKER_ARGS} "$${IMAGE}"
printf "\n$${BOLD}Perplexica is running on port ${PORT}$${RESET}\n"
@@ -12,12 +12,12 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
```tf
module "amp-cli" {
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "2.0.2"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
install_sourcegraph_amp = true
agentapi_version = "2.0.2"
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "3.0.0"
agent_id = coder_agent.example.id
amp_api_key = var.amp_api_key
install_amp = true
agentapi_version = "latest"
}
```
@@ -48,7 +48,7 @@ variable "amp_api_key" {
module "amp-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
amp_version = "2.0.2"
amp_version = "3.0.0"
agent_id = coder_agent.example.id
amp_api_key = var.amp_api_key # recommended for tasks usage
workdir = "/home/coder/project"
@@ -110,6 +110,7 @@ describe("amp", async () => {
const { id } = await setup({
skipAmpMock: true,
moduleVariables: {
install_via_npm: "true",
amp_version: "0.0.1755964909-g31e083",
},
});
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
version = ">= 2.12"
}
external = {
source = "hashicorp/external"
@@ -55,7 +55,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.10.0"
default = "v0.11.1"
}
variable "cli_app" {
@@ -140,7 +140,7 @@ variable "base_amp_config" {
type = string
description = <<-EOT
Base AMP configuration in JSON format. Can be overridden to customize AMP settings.
If empty, defaults enable thinking and todos for autonomous operation. Additional options include:
- "amp.permissions": [] (tool permissions)
- "amp.tools.stopTimeout": 600 (extend timeout for long operations)
@@ -148,7 +148,7 @@ variable "base_amp_config" {
- "amp.tools.disable": ["builtin:open"] (disable tools for containers)
- "amp.git.commit.ampThread.enabled": true (link commits to threads)
- "amp.git.commit.coauthor.enabled": true (add Amp as co-author)
Reference: https://ampcode.com/manual
EOT
default = ""
@@ -160,6 +160,16 @@ variable "mcp" {
default = null
}
variable "mode" {
type = string
description = "Set the agent mode (free, rush, smart) — controls the model, system prompt, and tool selection. Default: smart"
default = "smart"
validation {
condition = contains(["", "free", "rush", "smart"], var.mode)
error_message = "Invalid mode. Select one from (free, rush, smart)"
}
}
data "external" "env" {
program = ["sh", "-c", "echo '{\"CODER_AGENT_TOKEN\":\"'$CODER_AGENT_TOKEN'\",\"CODER_AGENT_URL\":\"'$CODER_AGENT_URL'\"}'"]
}
@@ -170,6 +180,7 @@ locals {
default_base_config = jsonencode({
"amp.anthropic.thinking.enabled" = true
"amp.todos.enabled" = true
"amp.terminal.animation" = false
})
user_config = jsondecode(var.base_amp_config != "" ? var.base_amp_config : local.default_base_config)
@@ -209,7 +220,7 @@ locals {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0"
version = "2.0.0"
agent_id = var.agent_id
folder = local.workdir
@@ -237,6 +248,7 @@ module "agentapi" {
ARG_AMP_START_DIRECTORY='${var.workdir}' \
ARG_AMP_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_MODE='${var.mode}' \
/tmp/start.sh
EOT
@@ -256,4 +268,6 @@ module "agentapi" {
EOT
}
output "task_app_id" {
value = module.agentapi.task_app_id
}
@@ -1,9 +1,4 @@
#!/bin/bash
if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi
set -euo pipefail
# ANSI colors
@@ -29,6 +29,7 @@ echo "--------------------------------"
printf "Workspace: %s\n" "$ARG_AMP_START_DIRECTORY"
printf "Task Prompt: %s\n" "$ARG_AMP_TASK_PROMPT"
printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
printf "ARG_MODE: %s\n" "$ARG_MODE"
echo "--------------------------------"
ensure_command amp
@@ -50,6 +51,13 @@ else
printf "amp_api_key not provided\n"
fi
ARGS=()
if [ -n "$ARG_MODE" ]; then
printf "Running agent in: %s mode" "$ARG_MODE"
ARGS+=(--mode "$ARG_MODE")
fi
if [ -n "$ARG_AMP_TASK_PROMPT" ]; then
if [ "$ARG_REPORT_TASKS" == "true" ]; then
printf "amp task prompt provided : %s" "$ARG_AMP_TASK_PROMPT\n"
@@ -58,8 +66,8 @@ if [ -n "$ARG_AMP_TASK_PROMPT" ]; then
PROMPT="$ARG_AMP_TASK_PROMPT"
fi
# Pipe the prompt into amp, which will be run inside agentapi
agentapi server --type amp --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp"
agentapi server --type amp --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp" "${ARGS[@]}"
else
printf "No task prompt given.\n"
agentapi server --type amp --term-width=67 --term-height=1190 -- amp
agentapi server --type amp --term-width=67 --term-height=1190 -- amp "${ARGS[@]}"
fi
Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 KiB

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 191 KiB

+41 -15
View File
@@ -4,11 +4,35 @@ import {
removeContainer,
runContainer,
runTerraformApply,
TerraformState,
writeFileContainer,
} from "~test";
import path from "path";
import { expect } from "bun:test";
/**
* Extracts all coder_env resources from Terraform state and returns them as
* a Record of environment variable names to values.
*/
export const extractCoderEnvVars = (
state: TerraformState,
): Record<string, string> => {
const envVars: Record<string, string> = {};
for (const resource of state.resources) {
if (resource.type === "coder_env" && resource.instances.length > 0) {
const instance = resource.instances[0].attributes;
const name = instance.name as string;
const value = instance.value as string;
if (name && value) {
envVars[name] = value;
}
}
}
return envVars;
};
export const setupContainer = async ({
moduleDir,
image,
@@ -23,10 +47,12 @@ export const setupContainer = async ({
...vars,
});
const coderScript = findResourceInstance(state, "coder_script");
const coderEnvVars = extractCoderEnvVars(state);
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
return {
id,
coderScript,
coderEnvVars,
cleanup: async () => {
if (
process.env["DEBUG"] === "true" ||
@@ -79,9 +105,11 @@ interface SetupProps {
agentapiMockScript?: string;
}
export const setup = async (props: SetupProps): Promise<{ id: string }> => {
export const setup = async (
props: SetupProps,
): Promise<{ id: string; coderEnvVars: Record<string, string> }> => {
const projectDir = props.projectDir ?? "/home/coder/project";
const { id, coderScript, cleanup } = await setupContainer({
const { id, coderScript, coderEnvVars, cleanup } = await setupContainer({
moduleDir: props.moduleDir,
vars: props.moduleVariables,
});
@@ -101,7 +129,7 @@ export const setup = async (props: SetupProps): Promise<{ id: string }> => {
filePath: "/home/coder/script.sh",
content: coderScript.script,
});
return { id };
return { id, coderEnvVars };
};
export const expectAgentAPIStarted = async (
@@ -125,18 +153,16 @@ export const execModuleScript = async (
id: string,
env?: Record<string, string>,
) => {
const envArgs = Object.entries(env ?? {})
.map(([key, value]) => ["--env", `${key}=${value}`])
.flat();
const resp = await execContainer(
id,
[
"bash",
"-c",
`set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`,
],
envArgs,
);
const envArgs = env
? Object.entries(env)
.map(([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`)
.join(" && ") + " && "
: "";
const resp = await execContainer(id, [
"bash",
"-c",
`${envArgs}set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`,
]);
if (resp.exitCode !== 0) {
console.log(resp.stdout);
console.log(resp.stderr);
@@ -0,0 +1,67 @@
---
display_name: Antigravity
description: Add a one-click button to launch Google Antigravity
icon: ../../../../.icons/antigravity.svg
verified: true
tags: [ide, antigravity, ai, google]
---
# Antigravity IDE
Add a button to open any workspace with a single click in [Antigravity IDE](https://antigravity.google).
Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder).
```tf
module "antigravity" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/antigravity/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
## Examples
### Open in a specific directory
```tf
module "antigravity" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/antigravity/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
```
### Configure MCP servers for Antigravity
Provide a JSON-encoded string via the `mcp` input. When set, the module writes the value to `~/.gemini/antigravity/mcp_config.json` using a `coder_script` on workspace start.
The following example configures Antigravity to use the GitHub MCP server with authentication facilitated by the [`coder_external_auth`](https://coder.com/docs/admin/external-auth#configure-a-github-oauth-app) resource.
```tf
module "antigravity" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/antigravity/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
mcp = jsonencode({
mcpServers = {
"github" : {
"url" : "https://api.githubcopilot.com/mcp/",
"headers" : {
"Authorization" : "Bearer ${data.coder_external_auth.github.access_token}",
},
"type" : "http"
}
}
})
}
data "coder_external_auth" "github" {
id = "github"
}
```
@@ -0,0 +1,130 @@
import { describe, it, expect } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
runContainer,
execContainer,
removeContainer,
findResourceInstance,
readFileContainer,
} from "~test";
describe("antigravity", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
it("default output", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
expect(state.outputs.antigravity_url.value).toBe(
"antigravity://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
const coder_app = state.resources.find(
(res) =>
res.type === "coder_app" &&
res.module === "module.vscode-desktop-core" &&
res.name === "vscode-desktop",
);
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBeNull();
});
it("adds folder", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/foo/bar",
});
expect(state.outputs.antigravity_url.value).toBe(
"antigravity://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
it("adds folder and open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/foo/bar",
open_recent: "true",
});
expect(state.outputs.antigravity_url.value).toBe(
"antigravity://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
it("adds folder but not open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/foo/bar",
open_recent: "false",
});
expect(state.outputs.antigravity_url.value).toBe(
"antigravity://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
it("adds open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
open_recent: "true",
});
expect(state.outputs.antigravity_url.value).toBe(
"antigravity://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
it("expect order to be set", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
order: "22",
});
const coder_app = state.resources.find(
(res) =>
res.type === "coder_app" &&
res.module === "module.vscode-desktop-core" &&
res.name === "vscode-desktop",
);
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBe(22);
});
it("writes ~/.gemini/antigravity/mcp_config.json when mcp provided", async () => {
const id = await runContainer("alpine");
try {
const mcp = JSON.stringify({
servers: { demo: { url: "http://localhost:1234" } },
});
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
mcp,
});
const script = findResourceInstance(
state,
"coder_script",
"antigravity_mcp",
).script;
const resp = await execContainer(id, ["sh", "-c", script]);
if (resp.exitCode !== 0) {
console.log(resp.stdout);
console.log(resp.stderr);
}
expect(resp.exitCode).toBe(0);
const content = await readFileContainer(
id,
"/root/.gemini/antigravity/mcp_config.json",
);
expect(content).toBe(mcp);
} finally {
await removeContainer(id);
}
}, 10000);
});
+104
View File
@@ -0,0 +1,104 @@
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 "folder" {
type = string
description = "The folder to open in Antigravity IDE."
default = ""
}
variable "open_recent" {
type = bool
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
default = false
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "slug" {
type = string
description = "The slug of the app."
default = "antigravity"
}
variable "display_name" {
type = string
description = "The display name of the app."
default = "Antigravity IDE"
}
variable "mcp" {
type = string
description = "JSON-encoded string to configure MCP servers for Antigravity. When set, writes ~/.gemini/antigravity/mcp_config.json."
default = ""
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
locals {
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
}
module "vscode-desktop-core" {
source = "registry.coder.com/coder/vscode-desktop-core/coder"
version = "1.0.1"
agent_id = var.agent_id
web_app_icon = "/icon/antigravity.svg"
web_app_slug = var.slug
web_app_display_name = var.display_name
web_app_order = var.order
web_app_group = var.group
folder = var.folder
open_recent = var.open_recent
protocol = "antigravity"
}
resource "coder_script" "antigravity_mcp" {
count = var.mcp != "" ? 1 : 0
agent_id = var.agent_id
display_name = "Antigravity MCP"
icon = "/icon/antigravity.svg"
run_on_start = true
start_blocks_login = false
script = <<-EOT
#!/bin/sh
set -eu
mkdir -p "$HOME/.gemini/antigravity"
echo -n "${local.mcp_b64}" | base64 -d > "$HOME/.gemini/antigravity/mcp_config.json"
chmod 600 "$HOME/.gemini/antigravity/mcp_config.json"
EOT
}
output "antigravity_url" {
value = module.vscode-desktop-core.ide_uri
description = "Antigravity IDE URL."
}
+31 -30
View File
@@ -13,8 +13,8 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.2"
agent_id = coder_agent.example.id
version = "4.2.9"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
}
@@ -45,13 +45,15 @@ This example shows how to configure the Claude Code module to run the agent behi
```tf
module "claude-code" {
source = "dev.registry.coder.com/coder/claude-code/coder"
version = "4.2.9"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
enable_boundary = true
boundary_version = "4.2.2"
boundary_version = "main"
boundary_log_dir = "/tmp/boundary_logs"
boundary_log_level = "WARN"
boundary_additional_allowed_urls = ["GET *google.com"]
boundary_proxy_port = "8087"
version = "4.2.2"
}
```
@@ -70,16 +72,16 @@ data "coder_parameter" "ai_prompt" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.2"
agent_id = coder_agent.example.id
version = "4.2.9"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
# OR
claude_code_oauth_token = "xxxxx-xxxx-xxxx"
claude_code_version = "4.2.2" # Pin to a specific version
agentapi_version = "4.2.2"
claude_code_version = "2.0.62" # Pin to a specific version
agentapi_version = "0.11.4"
ai_prompt = data.coder_parameter.ai_prompt.value
model = "sonnet"
@@ -90,7 +92,7 @@ module "claude-code" {
{
"mcpServers": {
"my-custom-tool": {
"command": "my-tool-server"
"command": "my-tool-server",
"args": ["--port", "8080"]
}
}
@@ -106,13 +108,12 @@ Run and configure Claude Code as a standalone CLI in your workspace.
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder"
version = "4.2.9"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
install_claude_code = true
claude_code_version = "4.2.2"
claude_code_version = "2.0.62"
report_tasks = false
cli_app = true
}
```
@@ -129,8 +130,8 @@ variable "claude_code_oauth_token" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.2"
agent_id = coder_agent.example.id
version = "4.2.9"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
}
@@ -146,13 +147,13 @@ Configure Claude Code to use AWS Bedrock for accessing Claude models through you
```tf
resource "coder_env" "bedrock_use" {
agent_id = coder_agent.example.id
agent_id = coder_agent.main.id
name = "CLAUDE_CODE_USE_BEDROCK"
value = "1"
}
resource "coder_env" "aws_region" {
agent_id = coder_agent.example.id
agent_id = coder_agent.main.id
name = "AWS_REGION"
value = "us-east-1" # Choose your preferred region
}
@@ -174,13 +175,13 @@ variable "aws_secret_access_key" {
}
resource "coder_env" "aws_access_key_id" {
agent_id = coder_agent.example.id
agent_id = coder_agent.main.id
name = "AWS_ACCESS_KEY_ID"
value = var.aws_access_key_id
}
resource "coder_env" "aws_secret_access_key" {
agent_id = coder_agent.example.id
agent_id = coder_agent.main.id
name = "AWS_SECRET_ACCESS_KEY"
value = var.aws_secret_access_key
}
@@ -195,15 +196,15 @@ variable "aws_bearer_token_bedrock" {
}
resource "coder_env" "bedrock_api_key" {
agent_id = coder_agent.example.id
agent_id = coder_agent.main.id
name = "AWS_BEARER_TOKEN_BEDROCK"
value = var.aws_bearer_token_bedrock
}
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.2"
agent_id = coder_agent.example.id
version = "4.2.9"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
}
@@ -228,39 +229,39 @@ variable "vertex_sa_json" {
}
resource "coder_env" "vertex_use" {
agent_id = coder_agent.example.id
agent_id = coder_agent.main.id
name = "CLAUDE_CODE_USE_VERTEX"
value = "1"
}
resource "coder_env" "vertex_project_id" {
agent_id = coder_agent.example.id
agent_id = coder_agent.main.id
name = "ANTHROPIC_VERTEX_PROJECT_ID"
value = "your-gcp-project-id"
}
resource "coder_env" "cloud_ml_region" {
agent_id = coder_agent.example.id
agent_id = coder_agent.main.id
name = "CLOUD_ML_REGION"
value = "global"
}
resource "coder_env" "vertex_sa_json" {
agent_id = coder_agent.example.id
agent_id = coder_agent.main.id
name = "VERTEX_SA_JSON"
value = var.vertex_sa_json
}
resource "coder_env" "google_application_credentials" {
agent_id = coder_agent.example.id
agent_id = coder_agent.main.id
name = "GOOGLE_APPLICATION_CREDENTIALS"
value = "/tmp/gcp-sa.json"
}
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.2"
agent_id = coder_agent.example.id
version = "4.2.9"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514"
+153 -47
View File
@@ -39,9 +39,11 @@ interface SetupProps {
agentapiMockScript?: string;
}
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const setup = async (
props?: SetupProps,
): Promise<{ id: string; coderEnvVars: Record<string, string> }> => {
const projectDir = "/home/coder/project";
const { id } = await setupUtil({
const { id, coderEnvVars } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
install_claude_code: props?.skipClaudeMock ? "true" : "false",
@@ -61,7 +63,7 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
content: await loadTestFile(import.meta.dir, "claude-mock.sh"),
});
}
return { id };
return { id, coderEnvVars };
};
setDefaultTimeout(60 * 1000);
@@ -79,14 +81,14 @@ describe("claude-code", async () => {
test("install-claude-code-version", async () => {
const version_to_install = "1.0.40";
const { id } = await setup({
const { id, coderEnvVars } = await setup({
skipClaudeMock: true,
moduleVariables: {
install_claude_code: "true",
claude_code_version: version_to_install,
},
});
await execModuleScript(id);
await execModuleScript(id, coderEnvVars);
const resp = await execContainer(id, [
"bash",
"-c",
@@ -96,14 +98,14 @@ describe("claude-code", async () => {
});
test("check-latest-claude-code-version-works", async () => {
const { id } = await setup({
const { id, coderEnvVars } = await setup({
skipClaudeMock: true,
skipAgentAPIMock: true,
moduleVariables: {
install_claude_code: "true",
},
});
await execModuleScript(id);
await execModuleScript(id, coderEnvVars);
await expectAgentAPIStarted(id);
});
@@ -133,13 +135,13 @@ describe("claude-code", async () => {
},
},
});
const { id } = await setup({
const { id, coderEnvVars } = await setup({
skipClaudeMock: true,
moduleVariables: {
mcp: mcpConfig,
},
});
await execModuleScript(id);
await execModuleScript(id, coderEnvVars);
const resp = await readFileContainer(id, "/home/coder/.claude.json");
expect(resp).toContain("test-cmd");
@@ -208,13 +210,17 @@ describe("claude-code", async () => {
});
// Create a mock task session file with the hardcoded task session ID
// Note: Claude CLI creates files without "session-" prefix when using --session-id
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
await execContainer(id, ["mkdir", "-p", sessionDir]);
await execContainer(id, [
"bash",
"-c",
`touch ${sessionDir}/session-${taskSessionId}.jsonl`,
`cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF'
{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"}
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
SESSIONEOF`,
]);
await execModuleScript(id);
@@ -226,46 +232,10 @@ describe("claude-code", async () => {
]);
expect(startLog.stdout).toContain("--resume");
expect(startLog.stdout).toContain(taskSessionId);
expect(startLog.stdout).toContain("Resuming existing task session");
expect(startLog.stdout).toContain("Resuming task session");
expect(startLog.stdout).toContain("--dangerously-skip-permissions");
});
test("claude-continue-resume-standalone-session", async () => {
const { id } = await setup({
moduleVariables: {
continue: "true",
report_tasks: "false",
ai_prompt: "test prompt",
},
});
const sessionId = "some-random-session-id";
const workdir = "/home/coder/project";
const claudeJson = {
projects: {
[workdir]: {
lastSessionId: sessionId,
},
},
};
await execContainer(id, [
"bash",
"-c",
`echo '${JSON.stringify(claudeJson)}' > /home/coder/.claude.json`,
]);
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain("--continue");
expect(startLog.stdout).toContain("Resuming existing session");
});
test("pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
@@ -360,4 +330,140 @@ describe("claude-code", async () => {
"ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat",
);
});
test("partial-initialization-detection", async () => {
const { id } = await setup({
moduleVariables: {
continue: "true",
report_tasks: "true",
ai_prompt: "test prompt",
},
});
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
await execContainer(id, ["mkdir", "-p", sessionDir]);
await execContainer(id, [
"bash",
"-c",
`echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`,
]);
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude-module/agentapi-start.log",
]);
// Should start new session, not try to resume invalid one
expect(startLog.stdout).toContain("Starting new task session");
expect(startLog.stdout).toContain("--session-id");
});
test("standalone-first-build-no-sessions", async () => {
const { id } = await setup({
moduleVariables: {
continue: "true",
report_tasks: "false",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude-module/agentapi-start.log",
]);
// Should start fresh, not try to continue
expect(startLog.stdout).toContain("No sessions found");
expect(startLog.stdout).toContain("starting fresh standalone session");
expect(startLog.stdout).not.toContain("--continue");
});
test("standalone-with-sessions-continues", async () => {
const { id } = await setup({
moduleVariables: {
continue: "true",
report_tasks: "false",
},
});
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
await execContainer(id, ["mkdir", "-p", sessionDir]);
await execContainer(id, [
"bash",
"-c",
`cat > ${sessionDir}/generic-123.jsonl << 'EOF'
{"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"}
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
EOF`,
]);
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude-module/agentapi-start.log",
]);
// Should continue existing session
expect(startLog.stdout).toContain("Sessions found");
expect(startLog.stdout).toContain(
"Continuing most recent standalone session",
);
expect(startLog.stdout).toContain("--continue");
});
test("task-mode-ignores-manual-sessions", async () => {
const { id } = await setup({
moduleVariables: {
continue: "true",
report_tasks: "true",
ai_prompt: "test prompt",
},
});
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
await execContainer(id, ["mkdir", "-p", sessionDir]);
// Create task session (without "session-" prefix, as CLI does)
await execContainer(id, [
"bash",
"-c",
`cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF'
{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"}
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
EOF`,
]);
// Create manual session (newer)
await execContainer(id, [
"bash",
"-c",
`cat > ${sessionDir}/manual-456.jsonl << 'EOF'
{"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"}
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"}
EOF`,
]);
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude-module/agentapi-start.log",
]);
// Should resume task session, not manual session
expect(startLog.stdout).toContain("Resuming task session");
expect(startLog.stdout).toContain(taskSessionId);
expect(startLog.stdout).not.toContain("manual-456");
});
});
+12 -9
View File
@@ -86,7 +86,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.10.0"
default = "v0.11.6"
}
variable "ai_prompt" {
@@ -288,15 +288,20 @@ resource "coder_env" "disable_autoupdater" {
value = "1"
}
resource "coder_env" "claude_binary_path" {
agent_id = var.agent_id
name = "PATH"
value = "$HOME/.local/bin:$PATH"
}
locals {
# we have to trim the slash because otherwise coder exp mcp will
# set up an invalid claude config
workdir = trimsuffix(var.workdir, "/")
app_slug = "ccw"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".claude-module"
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
workdir = trimsuffix(var.workdir, "/")
app_slug = "ccw"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".claude-module"
# Extract hostname from access_url for boundary --allow flag
coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "")
@@ -357,9 +362,7 @@ module "agentapi" {
set -o errexit
set -o pipefail
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "/tmp/remove-last-session-id.sh"
chmod +x /tmp/start.sh
chmod +x /tmp/remove-last-session-id.sh
ARG_MODEL='${var.model}' \
ARG_RESUME_SESSION_ID='${var.resume_session_id}' \
@@ -1,10 +1,5 @@
#!/bin/bash
if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi
# Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles
set -euo pipefail
BOLD='\033[0;1m'
@@ -45,11 +40,6 @@ function install_claude_code_cli() {
if [ $CURL_EXIT -ne 0 ]; then
echo "Claude Code installer failed with exit code $$CURL_EXIT"
fi
# Ensure binaries are discoverable.
echo "Creating a symlink for claude"
sudo ln -s /home/coder/.local/bin/claude /usr/local/bin/claude
echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')"
else
echo "Skipping Claude Code installation as per configuration."
@@ -90,12 +80,63 @@ function setup_claude_configurations() {
}
function configure_standalone_mode() {
echo "Configuring Claude Code for standalone mode..."
if [ -z "${CLAUDE_API_KEY:-}" ]; then
echo "Note: CLAUDE_API_KEY not set, skipping authentication setup"
return
fi
local claude_config="$HOME/.claude.json"
local workdir_normalized
workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
# Create or update .claude.json with minimal configuration for API key auth
# This skips the interactive login prompt and onboarding screens
if [ -f "$claude_config" ]; then
echo "Updating existing Claude configuration at $claude_config"
jq --arg apikey "${CLAUDE_API_KEY:-}" \
--arg workdir "$ARG_WORKDIR" \
'.autoUpdaterStatus = "disabled" |
.bypassPermissionsModeAccepted = true |
.hasAcknowledgedCostThreshold = true |
.hasCompletedOnboarding = true |
.primaryApiKey = $apikey |
.projects[$workdir].hasCompletedProjectOnboarding = true |
.projects[$workdir].hasTrustDialogAccepted = true' \
"$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config"
else
echo "Creating new Claude configuration at $claude_config"
cat > "$claude_config" << EOF
{
"autoUpdaterStatus": "disabled",
"bypassPermissionsModeAccepted": true,
"hasAcknowledgedCostThreshold": true,
"hasCompletedOnboarding": true,
"primaryApiKey": "${CLAUDE_API_KEY:-}",
"projects": {
"$ARG_WORKDIR": {
"hasCompletedProjectOnboarding": true,
"hasTrustDialogAccepted": true
}
}
}
EOF
fi
echo "Standalone mode configured successfully"
}
function report_tasks() {
if [ "$ARG_REPORT_TASKS" = "true" ]; then
echo "Configuring Claude Code to report tasks via Coder MCP..."
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
coder exp mcp configure claude-code "$ARG_WORKDIR"
else
configure_standalone_mode
fi
}
@@ -1,44 +0,0 @@
# If lastSessionId is present in .claude.json, claude --continue will start a
# conversation starting from that session. The problem is that lastSessionId
# doesn't always point to the last session. The field is updated by claude only
# at the point of normal CLI exit. If Claude exits with an error, or if the user
# restarts the Coder workspace, lastSessionId will be stale, and claude --continue
# will start from an old session.
#
# If lastSessionId is missing, claude seems to accurately figure out where to
# start using the conversation history - even if the CLI previously exited with
# an error.
#
# This script removes the lastSessionId field from .claude.json.
if [ $# -eq 0 ]; then
echo "No working directory provided - it must be the first argument"
exit 1
fi
# Get absolute path of working directory
working_dir=$(realpath "$1")
echo "workingDir $working_dir"
# Path to .claude.json
claude_json_path="$HOME/.claude.json"
echo ".claude.json path $claude_json_path"
# Check if .claude.json exists
if [ ! -f "$claude_json_path" ]; then
echo "No .claude.json file found"
exit 1
fi
# Use jq to check if lastSessionId exists for the working directory and remove it
if jq -e ".projects[\"$working_dir\"].lastSessionId" "$claude_json_path" > /dev/null 2>&1; then
# Remove lastSessionId and update the file
if jq "del(.projects[\"$working_dir\"].lastSessionId)" "$claude_json_path" > "${claude_json_path}.tmp" && mv "${claude_json_path}.tmp" "$claude_json_path"; then
echo "Removed lastSessionId from .claude.json"
exit 0
else
echo "Failed to remove lastSessionId from .claude.json"
fi
else
echo "No lastSessionId found in .claude.json - nothing to do"
fi
@@ -1,14 +1,7 @@
#!/bin/bash
if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi
# Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles
set -euo pipefail
export PATH="$HOME/.local/bin:$PATH"
command_exists() {
command -v "$1" > /dev/null 2>&1
}
@@ -51,23 +44,17 @@ printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST"
echo "--------------------------------"
# Clean up stale session data (see remove-last-session-id.sh for details)
CAN_CONTINUE_CONVERSATION=false
set +e
bash "/tmp/remove-last-session-id.sh" "$(pwd)" 2> /dev/null
session_cleanup_exit_code=$?
set -e
case $session_cleanup_exit_code in
0)
CAN_CONTINUE_CONVERSATION=true
;;
esac
function install_boundary() {
if [ "${ARG_COMPILE_FROM_SOURCE:-false}" = "true" ]; then
# Install boundary by compiling from source
echo "Compiling boundary from source (version: $ARG_BOUNDARY_VERSION)"
echo "Removing existing boundary directory to allow re-running the script safely"
if [ -d boundary ]; then
rm -rf boundary
fi
echo "Clone boundary repository"
git clone https://github.com/coder/boundary.git
cd boundary
git checkout "$ARG_BOUNDARY_VERSION"
@@ -99,18 +86,85 @@ function validate_claude_installation() {
# This ensures all task sessions use a consistent, predictable ID
TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2"
task_session_exists() {
get_project_dir() {
local workdir_normalized
workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
local project_dir="$HOME/.claude/projects/${workdir_normalized}"
echo "$HOME/.claude/projects/${workdir_normalized}"
}
printf "PROJECT_DIR: %s, workdir_normalized: %s\n" "$project_dir" "$workdir_normalized"
get_task_session_file() {
echo "$(get_project_dir)/${TASK_SESSION_ID}.jsonl"
}
if [ -d "$project_dir" ] && find "$project_dir" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then
printf "TASK_SESSION_ID: %s file found\n" "$TASK_SESSION_ID"
task_session_exists() {
local session_file
session_file=$(get_task_session_file)
if [ -f "$session_file" ]; then
printf "Task session file found: %s\n" "$session_file"
return 0
else
printf "TASK_SESSION_ID: %s file not found\n" "$TASK_SESSION_ID"
printf "Task session file not found: %s\n" "$session_file"
return 1
fi
}
is_valid_session() {
local session_file="$1"
# Check if file exists and is not empty
# Empty files indicate the session was created but never used so they need to be removed
if [ ! -f "$session_file" ]; then
printf "Session validation failed: file does not exist\n"
return 1
fi
if [ ! -s "$session_file" ]; then
printf "Session validation failed: file is empty, removing stale file\n"
rm -f "$session_file"
return 1
fi
# Check for minimum session content
# Valid sessions need at least 2 lines: initial message and first response
local line_count
line_count=$(wc -l < "$session_file")
if [ "$line_count" -lt 2 ]; then
printf "Session validation failed: incomplete (only %s lines), removing incomplete file\n" "$line_count"
rm -f "$session_file"
return 1
fi
# Validate JSONL format by checking first 3 lines
# Claude session files use JSONL (JSON Lines) format where each line is valid JSON
if ! head -3 "$session_file" | jq empty 2> /dev/null; then
printf "Session validation failed: invalid JSONL format, removing corrupt file\n"
rm -f "$session_file"
return 1
fi
# Verify the session has a valid sessionId field
# This ensures the file structure matches Claude's session format
if ! grep -q '"sessionId"' "$session_file" \
|| ! grep -m 1 '"sessionId"' "$session_file" | jq -e '.sessionId' > /dev/null 2>&1; then
printf "Session validation failed: no valid sessionId found, removing malformed file\n"
rm -f "$session_file"
return 1
fi
printf "Session validation passed: %s\n" "$session_file"
return 0
}
has_any_sessions() {
local project_dir
project_dir=$(get_project_dir)
if [ -d "$project_dir" ] && find "$project_dir" -maxdepth 1 -name "*.jsonl" -size +0c 2> /dev/null | grep -q .; then
printf "Sessions found in: %s\n" "$project_dir"
return 0
else
printf "No sessions found in: %s\n" "$project_dir"
return 1
fi
}
@@ -133,75 +187,41 @@ function start_agentapi() {
fi
if [ -n "$ARG_RESUME_SESSION_ID" ]; then
echo "Resuming task session by ID: $ARG_RESUME_SESSION_ID"
echo "Resuming specified session: $ARG_RESUME_SESSION_ID"
ARGS+=(--resume "$ARG_RESUME_SESSION_ID")
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions)
fi
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
elif [ "$ARG_CONTINUE" = "true" ]; then
if [ "$ARG_REPORT_TASKS" = "true" ] && task_session_exists; then
echo "Task session detected (ID: $TASK_SESSION_ID)"
ARGS+=(--resume "$TASK_SESSION_ID")
ARGS+=(--dangerously-skip-permissions)
echo "Resuming existing task session"
elif [ "$ARG_REPORT_TASKS" = "false" ] && [ "$CAN_CONTINUE_CONVERSATION" = true ]; then
echo "Previous session exists"
ARGS+=(--continue)
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions)
fi
echo "Resuming existing session"
else
echo "No existing session found"
if [ "$ARG_REPORT_TASKS" = "true" ]; then
if task_session_exists; then
ARGS+=(--resume "$TASK_SESSION_ID")
else
ARGS+=(--session-id "$TASK_SESSION_ID")
fi
fi
if [ -n "$ARG_AI_PROMPT" ]; then
if [ "$ARG_REPORT_TASKS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions -- "$ARG_AI_PROMPT")
else
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions)
fi
ARGS+=(-- "$ARG_AI_PROMPT")
fi
echo "Starting new session with prompt"
if [ "$ARG_REPORT_TASKS" = "true" ]; then
local session_file
session_file=$(get_task_session_file)
if task_session_exists && is_valid_session "$session_file"; then
echo "Resuming task session: $TASK_SESSION_ID"
ARGS+=(--resume "$TASK_SESSION_ID" --dangerously-skip-permissions)
else
if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions)
fi
echo "Starting new session"
echo "Starting new task session: $TASK_SESSION_ID"
ARGS+=(--session-id "$TASK_SESSION_ID" --dangerously-skip-permissions)
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
fi
else
if has_any_sessions; then
echo "Continuing most recent standalone session"
ARGS+=(--continue)
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
else
echo "No sessions found, starting fresh standalone session"
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
fi
fi
else
echo "Continue disabled, starting fresh session"
if [ "$ARG_REPORT_TASKS" = "true" ]; then
if task_session_exists; then
ARGS+=(--resume "$TASK_SESSION_ID")
else
ARGS+=(--session-id "$TASK_SESSION_ID")
fi
fi
if [ -n "$ARG_AI_PROMPT" ]; then
if [ "$ARG_REPORT_TASKS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions -- "$ARG_AI_PROMPT")
else
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions)
fi
ARGS+=(-- "$ARG_AI_PROMPT")
fi
echo "Starting new session with prompt"
else
if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions)
fi
echo "Starting claude code session"
fi
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
fi
printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
+11 -9
View File
@@ -14,7 +14,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
version = "1.4.2"
agent_id = coder_agent.example.id
}
```
@@ -29,9 +29,9 @@ module "code-server" {
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
version = "1.4.2"
agent_id = coder_agent.example.id
install_version = "1.4.1"
install_version = "4.106.3"
}
```
@@ -43,7 +43,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
version = "1.4.2"
agent_id = coder_agent.example.id
extensions = [
"dracula-theme.theme-dracula"
@@ -61,7 +61,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
version = "1.4.2"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
@@ -78,7 +78,7 @@ Just run code-server in the background, don't fetch it from GitHub:
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
version = "1.4.2"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
}
@@ -92,7 +92,7 @@ You can pass additional command-line arguments to code-server using the `additio
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
version = "1.4.2"
agent_id = coder_agent.example.id
additional_args = "--disable-workspace-trust"
}
@@ -108,7 +108,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub:
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
version = "1.4.2"
agent_id = coder_agent.example.id
use_cached = true
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
@@ -121,8 +121,10 @@ Just run code-server in the background, don't fetch it from GitHub:
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
version = "1.4.2"
agent_id = coder_agent.example.id
offline = true
}
```
Some of the key differences between code-server and [VS Code Web](https://registry.coder.com/modules/coder/vscode-web) are listed in [docs](https://coder.com/docs/user-guides/workspace-access/code-server#differences-between-code-server-and-vs-code-web).
+4 -3
View File
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.3.3"
version = "1.4.0"
agent_id = coder_agent.main.id
}
```
@@ -29,7 +29,7 @@ module "cursor" {
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.3.3"
version = "1.4.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
}
@@ -45,7 +45,7 @@ The following example configures Cursor to use the GitHub MCP server with authen
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.3.3"
version = "1.4.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
mcp = jsonencode({
@@ -58,6 +58,7 @@ module "cursor" {
"type" : "http"
}
}
})
}
+4 -16
View File
@@ -26,7 +26,10 @@ describe("cursor", async () => {
);
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "cursor",
(res) =>
res.type === "coder_app" &&
res.module === "module.vscode-desktop-core" &&
res.name === "vscode-desktop",
);
expect(coder_app).not.toBeNull();
@@ -76,21 +79,6 @@ describe("cursor", async () => {
);
});
it("expect order to be set", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
order: "22",
});
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "cursor",
);
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBe(22);
});
it("writes ~/.cursor/mcp.json when mcp provided", async () => {
const id = await runContainer("alpine");
try {
+17 -22
View File
@@ -64,26 +64,21 @@ locals {
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
}
resource "coder_app" "cursor" {
agent_id = var.agent_id
external = true
icon = "/icon/cursor.svg"
slug = var.slug
display_name = var.display_name
order = var.order
group = var.group
url = join("", [
"cursor://coder.coder-remote/open",
"?owner=",
data.coder_workspace_owner.me.name,
"&workspace=",
data.coder_workspace.me.name,
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
var.open_recent ? "&openRecent" : "",
"&url=",
data.coder_workspace.me.access_url,
"&token=$SESSION_TOKEN",
])
module "vscode-desktop-core" {
source = "registry.coder.com/coder/vscode-desktop-core/coder"
version = "1.0.0"
agent_id = var.agent_id
coder_app_icon = "/icon/cursor.svg"
coder_app_slug = var.slug
coder_app_display_name = var.display_name
coder_app_order = var.order
coder_app_group = var.group
folder = var.folder
open_recent = var.open_recent
protocol = "cursor"
}
resource "coder_script" "cursor_mcp" {
@@ -103,6 +98,6 @@ resource "coder_script" "cursor_mcp" {
}
output "cursor_url" {
value = coder_app.cursor.url
value = module.vscode-desktop-core.ide_uri
description = "Cursor IDE Desktop URL."
}
}
+4 -4
View File
@@ -14,7 +14,7 @@ A file browser for your workspace.
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.3"
version = "1.1.4"
agent_id = coder_agent.main.id
}
```
@@ -29,7 +29,7 @@ module "filebrowser" {
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.3"
version = "1.1.4"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
}
@@ -41,7 +41,7 @@ module "filebrowser" {
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.3"
version = "1.1.4"
agent_id = coder_agent.main.id
database_path = ".config/filebrowser.db"
}
@@ -53,7 +53,7 @@ module "filebrowser" {
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.3"
version = "1.1.4"
agent_id = coder_agent.main.id
agent_name = "main"
subdomain = false
+1 -1
View File
@@ -28,7 +28,7 @@ if [[ ! -f "${DB_PATH}" ]]; then
filebrowser users add admin "coderPASSWORD" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH}
fi
filebrowser config set --baseurl=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH}
filebrowser config set --baseURL=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH}
printf "👷 Starting filebrowser in background... \n\n"
+16 -12
View File
@@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.2"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
@@ -28,7 +28,7 @@ module "git-clone" {
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.2"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
base_dir = "~/projects/coder"
@@ -43,11 +43,12 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.2"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
data "coder_external_auth" "github" {
id = "github"
}
@@ -69,11 +70,12 @@ data "coder_parameter" "git_repo" {
module "git_clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.2"
version = "1.2.3"
agent_id = coder_agent.example.id
url = data.coder_parameter.git_repo.value
}
# Create a code-server instance for the cloned repository
module "code-server" {
count = data.coder_workspace.me.start_count
@@ -103,13 +105,14 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.2"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.example.com/coder/coder/tree/feat/example"
git_providers = {
"https://github.example.com/" = {
provider = "github"
}
}
}
```
@@ -122,7 +125,7 @@ To GitLab clone with a specific branch like `feat/example`
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.2"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
}
@@ -134,13 +137,14 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.2"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
git_providers = {
"https://gitlab.example.com/" = {
provider = "gitlab"
}
}
}
```
@@ -155,7 +159,7 @@ For example, to clone the `feat/example` branch:
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.2"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
branch_name = "feat/example"
@@ -173,7 +177,7 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder:
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.2"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
folder_name = "coder-dev"
@@ -191,8 +195,8 @@ If not defined, the default, `0`, performs a full clone.
```tf
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-clone/coder"
version = "1.2.2"
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
depth = 1
@@ -208,7 +212,7 @@ This is useful for running initialization tasks like installing dependencies or
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.2"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
post_clone_script = <<-EOT
+5 -4
View File
@@ -58,9 +58,10 @@ fi
# Run post-clone script if provided
if [ -n "$POST_CLONE_SCRIPT" ]; then
echo "Running post-clone script..."
echo "$POST_CLONE_SCRIPT" | base64 -d > /tmp/post_clone.sh
chmod +x /tmp/post_clone.sh
POST_CLONE_TMP=$(mktemp)
echo "$POST_CLONE_SCRIPT" | base64 -d > "$POST_CLONE_TMP"
chmod +x "$POST_CLONE_TMP"
cd "$CLONE_PATH" || exit
/tmp/post_clone.sh
rm /tmp/post_clone.sh
$POST_CLONE_TMP
rm "$POST_CLONE_TMP"
fi
@@ -1,9 +1,17 @@
import { type Server, serve } from "bun";
import { describe, expect, it } from "bun:test";
import { serve } from "bun";
import {
afterEach,
beforeAll,
describe,
expect,
it,
setDefaultTimeout,
} from "bun:test";
import {
createJSONResponse,
execContainer,
findResourceInstance,
removeContainer,
runContainer,
runTerraformApply,
runTerraformInit,
@@ -11,77 +19,48 @@ import {
writeCoder,
} from "~test";
describe("github-upload-public-key", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
it("creates new key if one does not exist", async () => {
const { instance, id, server } = await setupContainer();
await writeCoder(id, "echo foo");
const url = server.url.toString().slice(0, -1);
const exec = await execContainer(id, [
"env",
`CODER_ACCESS_URL=${url}`,
`GITHUB_API_URL=${url}`,
"CODER_OWNER_SESSION_TOKEN=foo",
"CODER_EXTERNAL_AUTH_ID=github",
"bash",
"-c",
instance.script,
]);
expect(exec.stdout).toContain(
"Your Coder public key has been added to GitHub!",
);
expect(exec.exitCode).toBe(0);
// we need to increase timeout to pull the container
}, 15000);
it("does nothing if one already exists", async () => {
const { instance, id, server } = await setupContainer();
// use keyword to make server return a existing key
await writeCoder(id, "echo findkey");
const url = server.url.toString().slice(0, -1);
const exec = await execContainer(id, [
"env",
`CODER_ACCESS_URL=${url}`,
`GITHUB_API_URL=${url}`,
"CODER_OWNER_SESSION_TOKEN=foo",
"CODER_EXTERNAL_AUTH_ID=github",
"bash",
"-c",
instance.script,
]);
expect(exec.stdout).toContain(
"Your Coder public key is already on GitHub!",
);
expect(exec.exitCode).toBe(0);
});
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);
}
}
});
const setupContainer = async (
image = "lorello/alpine-bash",
vars: Record<string, string> = {},
) => {
const server = await setupServer();
const server = setupServer();
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
...vars,
});
const instance = findResourceInstance(state, "coder_script");
const id = await runContainer(image);
registerCleanup(async () => {
server.stop();
});
registerCleanup(async () => {
await removeContainer(id);
});
return { id, instance, server };
};
const setupServer = async (): Promise<Server> => {
let url: URL;
const fakeSlackHost = serve({
const setupServer = () => {
const fakeGithubHost = serve({
fetch: (req) => {
url = new URL(req.url);
const url = new URL(req.url);
if (url.pathname === "/api/v2/users/me/gitsshkey") {
return createJSONResponse({
public_key: "exists",
@@ -128,5 +107,60 @@ const setupServer = async (): Promise<Server> => {
port: 0,
});
return fakeSlackHost;
return fakeGithubHost;
};
setDefaultTimeout(30 * 1000);
describe("github-upload-public-key", () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
it("creates new key if one does not exist", async () => {
const { instance, id, server } = await setupContainer();
await writeCoder(id, "echo foo");
const url = server.url.toString().slice(0, -1);
const exec = await execContainer(id, [
"env",
`CODER_ACCESS_URL=${url}`,
`GITHUB_API_URL=${url}`,
"CODER_OWNER_SESSION_TOKEN=foo",
"CODER_EXTERNAL_AUTH_ID=github",
"bash",
"-c",
instance.script,
]);
expect(exec.stdout).toContain(
"Your Coder public key has been added to GitHub!",
);
expect(exec.exitCode).toBe(0);
});
it("does nothing if one already exists", async () => {
const { instance, id, server } = await setupContainer();
// use keyword to make server return a existing key
await writeCoder(id, "echo findkey");
const url = server.url.toString().slice(0, -1);
const exec = await execContainer(id, [
"env",
`CODER_ACCESS_URL=${url}`,
`GITHUB_API_URL=${url}`,
"CODER_OWNER_SESSION_TOKEN=foo",
"CODER_EXTERNAL_AUTH_ID=github",
"bash",
"-c",
instance.script,
]);
expect(exec.stdout).toContain(
"Your Coder public key is already on GitHub!",
);
expect(exec.exitCode).toBe(0);
});
});
+8 -16
View File
@@ -14,10 +14,9 @@ This module adds JetBrains IDE buttons to launch IDEs directly from the dashboar
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.2.1"
version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
# tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button." # Optional
}
```
@@ -40,7 +39,7 @@ When `default` contains IDE codes, those IDEs are created directly without user
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.2.1"
version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA
@@ -53,7 +52,7 @@ module "jetbrains" {
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.2.1"
version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
# Show parameter with limited options
@@ -67,7 +66,7 @@ module "jetbrains" {
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.2.1"
version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
default = ["IU", "PY"]
@@ -82,7 +81,7 @@ module "jetbrains" {
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.2.1"
version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/workspace/project"
@@ -109,7 +108,7 @@ module "jetbrains" {
module "jetbrains_pycharm" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.2.1"
version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/workspace/project"
@@ -129,11 +128,11 @@ Add helpful tooltip text that appears when users hover over the IDE app buttons:
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.2.1"
version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
default = ["IU", "PY"]
tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."
tooltip = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
}
```
@@ -170,13 +169,6 @@ resource "coder_metadata" "container_info" {
- If the API is unreachable (air-gapped environments), the module automatically falls back to build numbers from `ide_config`
- `major_version` and `channel` control which API endpoint is queried (when API access is available)
### Tooltip
- **`tooltip`**: Optional markdown text displayed when hovering over IDE app buttons
- If not specified, no tooltip is shown
- Supports markdown formatting for rich text (bold, italic, links, etc.)
- All IDE apps created by this module will show the same tooltip text
## Supported IDEs
All JetBrains IDEs with remote development capabilities:
@@ -2,15 +2,15 @@ variables {
# Default IDE config, mirrored from main.tf for test assertions.
# If main.tf defaults change, update this map to match.
expected_ide_config = {
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "251.26927.60" },
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "251.26927.74" },
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "251.26927.67" },
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "251.26927.47" },
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "251.26927.79" },
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "251.26927.40" }
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "253.29346.151" },
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "253.29346.142" },
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "253.29346.144" },
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "253.29346.140" },
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "253.29346.139" },
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "253.29346.143" }
}
}
@@ -187,16 +187,16 @@ run "tooltip_when_provided" {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."
tooltip = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
}
assert {
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."])
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."])
error_message = "Expected coder_app tooltip to be set when provided"
}
}
run "tooltip_null_when_not_provided" {
run "tooltip_default_when_not_provided" {
command = plan
variables {
@@ -206,8 +206,41 @@ run "tooltip_null_when_not_provided" {
}
assert {
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == null])
error_message = "Expected coder_app tooltip to be null when not provided"
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."])
error_message = "Expected coder_app tooltip to be the default JetBrains Toolbox message when not provided"
}
}
run "channel_eap" {
command = plan
variables {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
channel = "eap"
major_version = "latest"
}
assert {
condition = output.ide_metadata["GO"].json_data.type == "eap"
error_message = "Expected the API to return a release of type 'eap', but got '${output.ide_metadata["GO"].json_data.type}'"
}
}
run "specific_major_version" {
command = plan
variables {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
major_version = "2025.3"
}
assert {
condition = output.ide_metadata["GO"].json_data.majorVersion == "2025.3"
error_message = "Expected the API to return a release for major version '2025.3', but got '${output.ide_metadata["GO"].json_data.majorVersion}'"
}
}
@@ -294,3 +327,27 @@ run "output_multiple_ides" {
error_message = "Expected ide_metadata['PY'].build to be the fallback '${var.expected_ide_config["PY"].build}'"
}
}
run "validate_output_schema" {
command = plan
variables {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
}
assert {
condition = alltrue([
for key, meta in output.ide_metadata : (
can(meta.icon) &&
can(meta.name) &&
can(meta.identifier) &&
can(meta.key) &&
can(meta.build) &&
# json_data can be null, but the key must exist
can(meta.json_data)
)
])
error_message = "The ide_metadata output schema has changed. Please update the 'main.tf' and this test."
}
}
File diff suppressed because it is too large Load Diff
+33 -22
View File
@@ -62,7 +62,7 @@ variable "coder_parameter_order" {
variable "tooltip" {
type = string
description = "Markdown text that is displayed when hovering over workspace apps."
default = null
default = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
}
variable "major_version" {
@@ -70,8 +70,8 @@ variable "major_version" {
description = "The major version of the IDE. i.e. 2025.1"
default = "latest"
validation {
condition = can(regex("^[0-9]{4}\\.[0-2]{1}$", var.major_version)) || var.major_version == "latest"
error_message = "The major_version must be a valid version number. i.e. 2025.1 or latest"
condition = can(regex("^[0-9]{4}\\.[1-3]$", var.major_version)) || var.major_version == "latest"
error_message = "The major_version must be a valid version number (e.g., 2025.1) or 'latest'"
}
}
@@ -126,7 +126,7 @@ variable "download_base_link" {
data "http" "jetbrains_ide_versions" {
for_each = length(var.default) == 0 ? var.options : var.default
url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}&latest=true${var.major_version == "latest" ? "" : "&major_version=${var.major_version}"}"
url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}${var.major_version == "latest" ? "&latest=true" : ""}"
}
variable "ide_config" {
@@ -138,9 +138,9 @@ variable "ide_config" {
- build: The build number of the IDE.
Example:
{
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
}
EOT
type = map(object({
@@ -149,15 +149,15 @@ variable "ide_config" {
build = string
}))
default = {
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "251.26927.60" },
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "251.26927.74" },
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "251.26927.67" },
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "251.26927.47" },
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "251.26927.79" },
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "251.26927.40" }
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "253.29346.151" },
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "253.29346.142" },
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "253.29346.144" },
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "253.29346.140" },
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "253.29346.139" },
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "253.29346.143" }
}
validation {
condition = length(var.ide_config) > 0
@@ -182,6 +182,20 @@ locals {
)
}
# Filter the parsed response for the requested major version if not "latest"
filtered_releases = {
for code in length(var.default) == 0 ? var.options : var.default : code => [
for r in try(local.parsed_responses[code][keys(local.parsed_responses[code])[0]], []) :
r if var.major_version == "latest" || r.majorVersion == var.major_version
]
}
# Select the latest release for the requested major version (first item in the filtered list)
selected_releases = {
for code in length(var.default) == 0 ? var.options : var.default : code =>
length(local.filtered_releases[code]) > 0 ? local.filtered_releases[code][0] : null
}
# Dynamically generate IDE configurations based on options with fallback to ide_config
options_metadata = {
for code in length(var.default) == 0 ? var.options : var.default : code => {
@@ -191,13 +205,10 @@ locals {
key = code
# Use API build number if available, otherwise fall back to ide_config build number
build = length(keys(local.parsed_responses[code])) > 0 ? (
local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0].build
) : var.ide_config[code].build
build = local.selected_releases[code] != null ? local.selected_releases[code].build : var.ide_config[code].build
# Store API data for potential future use (only if API is available)
json_data = length(keys(local.parsed_responses[code])) > 0 ? local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0] : null
response_key = length(keys(local.parsed_responses[code])) > 0 ? keys(local.parsed_responses[code])[0] : null
# Store API data for potential future use
json_data = local.selected_releases[code]
}
}
+2 -2
View File
@@ -16,7 +16,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
module "jfrog" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jfrog-oauth/coder"
version = "1.2.3"
version = "1.2.4"
agent_id = coder_agent.main.id
jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
@@ -57,7 +57,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
module "jfrog" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jfrog-oauth/coder"
version = "1.2.3"
version = "1.2.4"
agent_id = coder_agent.main.id
jfrog_url = "https://example.jfrog.io"
username_field = "email"
@@ -0,0 +1,400 @@
# Test for jfrog-oauth module
run "test_required_vars" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {}
}
# Mock external auth with valid access token for basic test
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
}
run "test_empty_access_token_fails" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {}
}
# Mock external auth with empty access token
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = ""
}
}
expect_failures = [
resource.coder_script.jfrog
]
}
run "test_valid_access_token_succeeds" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {}
}
# Mock external auth with valid access token
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
# Verify the script resource is created
assert {
condition = resource.coder_script.jfrog.agent_id == "test-agent-id"
error_message = "coder_script agent_id should match the input variable"
}
assert {
condition = resource.coder_script.jfrog.display_name == "jfrog"
error_message = "coder_script display_name should be 'jfrog'"
}
}
run "test_jfrog_url_validation" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "invalid-url"
package_managers = {}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
expect_failures = [
var.jfrog_url
]
}
run "test_username_field_validation" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
username_field = "invalid"
package_managers = {}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
expect_failures = [
var.username_field
]
}
run "test_with_npm_package_manager" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
npm = ["global", "@foo:foo", "@bar:bar"]
}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
assert {
condition = resource.coder_script.jfrog.run_on_start == true
error_message = "coder_script should run on start"
}
# Verify npm configuration is in script
assert {
condition = strcontains(resource.coder_script.jfrog.script, "jf npmc --global --repo-resolve \"global\"")
error_message = "script should contain jf npmc command for npm"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "@foo:registry=https://example.jfrog.io/artifactory/api/npm/foo")
error_message = "script should contain scoped npm registry for @foo"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "@bar:registry=https://example.jfrog.io/artifactory/api/npm/bar")
error_message = "script should contain scoped npm registry for @bar"
}
}
run "test_configure_code_server" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
configure_code_server = true
package_managers = {}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
# When configure_code_server is true, env vars should be created
assert {
condition = length(resource.coder_env.jfrog_ide_url) == 1
error_message = "coder_env.jfrog_ide_url should be created when configure_code_server is true"
}
assert {
condition = length(resource.coder_env.jfrog_ide_access_token) == 1
error_message = "coder_env.jfrog_ide_access_token should be created when configure_code_server is true"
}
}
run "test_go_proxy_env" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
go = ["foo", "bar", "baz"]
}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
# When go package manager is configured, GOPROXY env should be set
assert {
condition = length(resource.coder_env.goproxy) == 1
error_message = "coder_env.goproxy should be created when go package manager is configured"
}
# Verify GOPROXY contains all repos
assert {
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/foo")
error_message = "GOPROXY should contain foo repo"
}
assert {
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/bar")
error_message = "GOPROXY should contain bar repo"
}
assert {
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/baz")
error_message = "GOPROXY should contain baz repo"
}
# Verify script contains go configuration
assert {
condition = strcontains(resource.coder_script.jfrog.script, "jf goc --global --repo-resolve \"foo\"")
error_message = "script should contain jf goc command"
}
}
run "test_pypi_package_manager" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
pypi = ["global", "foo", "bar"]
}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
# Verify pip configuration in script
assert {
condition = strcontains(resource.coder_script.jfrog.script, "jf pipc --global --repo-resolve \"global\"")
error_message = "script should contain jf pipc command"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "index-url = https://default:valid-token-value@example.jfrog.io/artifactory/api/pypi/global/simple")
error_message = "script should contain pip index-url configuration"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "extra-index-url")
error_message = "script should contain extra-index-url for additional repos"
}
}
run "test_docker_package_manager" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
docker = ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"]
}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
# Verify docker registration commands in script
assert {
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"foo.jfrog.io\"")
error_message = "script should contain register_docker for foo.jfrog.io"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"bar.jfrog.io\"")
error_message = "script should contain register_docker for bar.jfrog.io"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"baz.jfrog.io\"")
error_message = "script should contain register_docker for baz.jfrog.io"
}
}
run "test_conda_package_manager" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
conda = ["conda-main", "conda-secondary", "conda-local"]
}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
# Verify conda configuration in script
assert {
condition = strcontains(resource.coder_script.jfrog.script, "channels:")
error_message = "script should contain conda channels configuration"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-main")
error_message = "script should contain conda-main channel"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-secondary")
error_message = "script should contain conda-secondary channel"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-local")
error_message = "script should contain conda-local channel"
}
}
run "test_maven_package_manager" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
maven = ["central", "snapshots", "local"]
}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
# Verify maven jf mvnc command
assert {
condition = strcontains(resource.coder_script.jfrog.script, "jf mvnc --global")
error_message = "script should contain jf mvnc command"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "--repo-resolve-releases \"central\"")
error_message = "script should contain repo-resolve-releases for central"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "--repo-resolve-snapshots \"central\"")
error_message = "script should contain repo-resolve-snapshots for central"
}
# Verify settings.xml content
assert {
condition = strcontains(resource.coder_script.jfrog.script, "<servers>")
error_message = "script should contain maven servers configuration"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "<id>central</id>")
error_message = "script should contain central server id"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "<id>snapshots</id>")
error_message = "script should contain snapshots server id"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "<id>local</id>")
error_message = "script should contain local server id"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "<url>https://example.jfrog.io/artifactory/central</url>")
error_message = "script should contain central repository URL"
}
}
@@ -1,189 +0,0 @@
import { describe, expect, it } from "bun:test";
import {
findResourceInstance,
runTerraformInit,
runTerraformApply,
testRequiredVariables,
} from "~test";
describe("jfrog-oauth", async () => {
type TestVariables = {
agent_id: string;
jfrog_url: string;
package_managers: string;
username_field?: string;
jfrog_server_id?: string;
external_auth_id?: string;
configure_code_server?: boolean;
};
await runTerraformInit(import.meta.dir);
const fakeFrogApi = "localhost:8081/artifactory/api";
const fakeFrogUrl = "http://localhost:8081";
const user = "default";
testRequiredVariables<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: "{}",
});
it("generates an npmrc with scoped repos", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: JSON.stringify({
npm: ["global", "@foo:foo", "@bar:bar"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
const npmrcStanza = `cat << EOF > ~/.npmrc
email=${user}@example.com
registry=http://${fakeFrogApi}/npm/global
//${fakeFrogApi}/npm/global/:_authToken=
@foo:registry=http://${fakeFrogApi}/npm/foo
//${fakeFrogApi}/npm/foo/:_authToken=
@bar:registry=http://${fakeFrogApi}/npm/bar
//${fakeFrogApi}/npm/bar/:_authToken=
EOF`;
expect(coderScript.script).toContain(npmrcStanza);
expect(coderScript.script).toContain(
'jf npmc --global --repo-resolve "global"',
);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured npm',
);
});
it("generates a pip config with extra-indexes", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: JSON.stringify({
pypi: ["global", "foo", "bar"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
const pipStanza = `cat << EOF > ~/.pip/pip.conf
[global]
index-url = https://${user}:@${fakeFrogApi}/pypi/global/simple
extra-index-url =
https://${user}:@${fakeFrogApi}/pypi/foo/simple
https://${user}:@${fakeFrogApi}/pypi/bar/simple
EOF`;
expect(coderScript.script).toContain(pipStanza);
expect(coderScript.script).toContain(
'jf pipc --global --repo-resolve "global"',
);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured pypi',
);
});
it("registers multiple docker repos", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: JSON.stringify({
docker: ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
const dockerStanza = ["foo", "bar", "baz"]
.map((r) => `register_docker "${r}.jfrog.io"`)
.join("\n");
expect(coderScript.script).toContain(dockerStanza);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured docker',
);
});
it("sets goproxy with multiple repos", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: JSON.stringify({
go: ["foo", "bar", "baz"],
}),
});
const proxyEnv = findResourceInstance(state, "coder_env", "goproxy");
const proxies = ["foo", "bar", "baz"]
.map((r) => `https://${user}:@${fakeFrogApi}/go/${r}`)
.join(",");
expect(proxyEnv.value).toEqual(proxies);
const coderScript = findResourceInstance(state, "coder_script");
expect(coderScript.script).toContain(
'jf goc --global --repo-resolve "foo"',
);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured go',
);
});
it("generates a conda config with multiple repos", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: JSON.stringify({
conda: ["conda-main", "conda-secondary", "conda-local"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
const condaStanza = `cat << EOF > ~/.condarc
channels:
- https://${user}:@${fakeFrogApi}/conda/conda-main
- https://${user}:@${fakeFrogApi}/conda/conda-secondary
- https://${user}:@${fakeFrogApi}/conda/conda-local
- defaults
ssl_verify: true
EOF`;
expect(coderScript.script).toContain(condaStanza);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured conda',
);
});
it("generates a maven settings.xml with multiple repos", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: JSON.stringify({
maven: ["central", "snapshots", "local"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
expect(coderScript.script).toContain("jf mvnc --global");
expect(coderScript.script).toContain('--server-id-resolve="0"');
expect(coderScript.script).toContain('--repo-resolve-releases "central"');
expect(coderScript.script).toContain('--repo-resolve-snapshots "central"');
expect(coderScript.script).toContain('--server-id-deploy="0"');
expect(coderScript.script).toContain('--repo-deploy-releases "central"');
expect(coderScript.script).toContain('--repo-deploy-snapshots "central"');
expect(coderScript.script).toContain("<servers>");
expect(coderScript.script).toContain("<id>central</id>");
expect(coderScript.script).toContain("<id>snapshots</id>");
expect(coderScript.script).toContain("<id>local</id>");
expect(coderScript.script).toContain(
"<url>http://localhost:8081/artifactory/central</url>",
);
expect(coderScript.script).toContain(
"<url>http://localhost:8081/artifactory/snapshots</url>",
);
expect(coderScript.script).toContain(
"<url>http://localhost:8081/artifactory/local</url>",
);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured maven',
);
});
});
@@ -163,6 +163,13 @@ resource "coder_script" "jfrog" {
}
))
run_on_start = true
lifecycle {
precondition {
condition = data.coder_external_auth.jfrog.access_token != ""
error_message = "JFrog access token is empty. Please authenticate with JFrog using external auth."
}
}
}
resource "coder_env" "jfrog_ide_url" {
+1 -1
View File
@@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
module "kasmvnc" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kasmvnc/coder"
version = "1.2.6"
version = "1.2.7"
agent_id = coder_agent.example.id
desktop_environment = "xfce"
subdomain = true
+1 -1
View File
@@ -31,7 +31,7 @@ variable "desktop_environment" {
description = "Specifies the desktop environment of the workspace. This should be pre-installed on the workspace."
validation {
condition = contains(["xfce", "kde", "gnome", "lxde", "lxqt"], var.desktop_environment)
condition = contains(["cinnamon", "mate", "lxde", "lxqt", "kde", "gnome", "xfce", "manual"], var.desktop_environment)
error_message = "Invalid desktop environment. Please specify a valid desktop environment."
}
}
+4 -3
View File
@@ -18,7 +18,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "kiro" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kiro/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.main.id
}
```
@@ -31,7 +31,7 @@ module "kiro" {
module "kiro" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kiro/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
}
@@ -47,7 +47,7 @@ The following example configures Kiro to use the GitHub MCP server with authenti
module "kiro" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kiro/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
mcp = jsonencode({
@@ -60,6 +60,7 @@ module "kiro" {
"type" : "http"
}
}
})
}
@@ -17,11 +17,6 @@ run "default_output" {
condition = output.kiro_url == "kiro://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN"
error_message = "Default kiro_url must match expected value"
}
assert {
condition = coder_app.kiro.order == null
error_message = "coder_app order must be null by default"
}
}
run "adds_folder" {
@@ -53,54 +48,6 @@ run "folder_and_open_recent" {
}
}
run "custom_slug_display_name" {
command = plan
variables {
agent_id = "foo"
slug = "kiro-ai"
display_name = "Kiro AI IDE"
}
assert {
condition = coder_app.kiro.slug == "kiro-ai"
error_message = "coder_app slug must be set to kiro-ai"
}
assert {
condition = coder_app.kiro.display_name == "Kiro AI IDE"
error_message = "coder_app display_name must be set to Kiro AI IDE"
}
}
run "sets_order" {
command = plan
variables {
agent_id = "foo"
order = 5
}
assert {
condition = coder_app.kiro.order == 5
error_message = "coder_app order must be set to 5"
}
}
run "sets_group" {
command = plan
variables {
agent_id = "foo"
group = "AI IDEs"
}
assert {
condition = coder_app.kiro.group == "AI IDEs"
error_message = "coder_app group must be set to AI IDEs"
}
}
run "writes_mcp_json" {
command = plan
+4 -42
View File
@@ -26,7 +26,10 @@ describe("kiro", async () => {
);
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "kiro",
(res) =>
res.type === "coder_app" &&
res.module === "module.vscode-desktop-core" &&
res.name === "vscode-desktop",
);
expect(coder_app).not.toBeNull();
@@ -55,47 +58,6 @@ describe("kiro", async () => {
);
});
it("custom slug and display_name", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
slug: "kiro-ai",
display_name: "Kiro AI IDE",
});
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "kiro",
);
expect(coder_app?.instances[0].attributes.slug).toBe("kiro-ai");
expect(coder_app?.instances[0].attributes.display_name).toBe("Kiro AI IDE");
});
it("sets order", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
order: "5",
});
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "kiro",
);
expect(coder_app?.instances[0].attributes.order).toBe(5);
});
it("sets group", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
group: "AI IDEs",
});
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "kiro",
);
expect(coder_app?.instances[0].attributes.group).toBe("AI IDEs");
});
it("writes ~/.kiro/settings/mcp.json when mcp provided", async () => {
const id = await runContainer("alpine");
try {
+17 -34
View File
@@ -38,18 +38,6 @@ variable "group" {
default = null
}
variable "slug" {
type = string
description = "The slug of the app."
default = "kiro"
}
variable "display_name" {
type = string
description = "The display name of the app."
default = "Kiro IDE"
}
variable "mcp" {
type = string
description = "JSON-encoded string to configure MCP servers for Kiro. When set, writes ~/.kiro/settings/mcp.json."
@@ -63,26 +51,21 @@ locals {
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
}
resource "coder_app" "kiro" {
agent_id = var.agent_id
external = true
icon = "/icon/kiro.svg"
slug = var.slug
display_name = var.display_name
order = var.order
group = var.group
url = join("", [
"kiro://coder.coder-remote/open",
"?owner=",
data.coder_workspace_owner.me.name,
"&workspace=",
data.coder_workspace.me.name,
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
var.open_recent ? "&openRecent" : "",
"&url=",
data.coder_workspace.me.access_url,
"&token=$SESSION_TOKEN",
])
module "vscode-desktop-core" {
source = "registry.coder.com/coder/vscode-desktop-core/coder"
version = "1.0.0"
agent_id = var.agent_id
coder_app_icon = "/icon/kiro.svg"
coder_app_slug = "kiro-ai"
coder_app_display_name = "Kiro AI IDE"
coder_app_order = var.order
coder_app_group = var.group
folder = var.folder
open_recent = var.open_recent
protocol = "kiro"
}
resource "coder_script" "kiro_mcp" {
@@ -102,6 +85,6 @@ resource "coder_script" "kiro_mcp" {
}
output "kiro_url" {
value = coder_app.kiro.url
value = module.vscode-desktop-core.ide_uri
description = "Kiro IDE URL."
}
}
+10 -8
View File
@@ -2,23 +2,25 @@
display_name: mux
description: Coding Agent Multiplexer - Run multiple AI agents in parallel
icon: ../../../../.icons/mux.svg
verified: false
verified: true
tags: [ai, agents, development, multiplexer]
---
# mux
Automatically install and run mux in a Coder workspace. By default, the module installs `mux@next` from npm (with a fallback to downloading the npm tarball if npm is unavailable). mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces.
Automatically install and run [mux](https://github.com/coder/mux) in a Coder workspace. By default, the module installs `mux@next` from npm (with a fallback to downloading the npm tarball if npm is unavailable). mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces.
```tf
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.0.2"
version = "1.0.7"
agent_id = coder_agent.main.id
}
```
![mux](../../.images/mux-product-hero.webp)
## Features
- **Parallel Agent Execution**: Run multiple AI agents simultaneously on different tasks
@@ -35,7 +37,7 @@ module "mux" {
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.0.2"
version = "1.0.7"
agent_id = coder_agent.main.id
}
```
@@ -46,7 +48,7 @@ module "mux" {
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.0.2"
version = "1.0.7"
agent_id = coder_agent.main.id
# Default is "latest"; set to a specific version to pin
install_version = "0.4.0"
@@ -59,7 +61,7 @@ module "mux" {
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.0.2"
version = "1.0.7"
agent_id = coder_agent.main.id
port = 8080
}
@@ -73,7 +75,7 @@ Run an existing copy of mux if found, otherwise install from npm:
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.0.2"
version = "1.0.7"
agent_id = coder_agent.main.id
use_cached = true
}
@@ -87,7 +89,7 @@ Run without installing from the network (requires mux to be pre-installed):
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.0.2"
version = "1.0.7"
agent_id = coder_agent.main.id
install = false
}
+8 -5
View File
@@ -5,6 +5,9 @@ RESET='\033[0m'
MUX_BINARY="${INSTALL_PREFIX}/mux"
function run_mux() {
# Remove stale server lock if present
rm -f "$HOME/.mux/server.lock"
local port_value
port_value="${PORT}"
if [ -z "$port_value" ]; then
@@ -94,7 +97,7 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
fi
# sed-based fallback
if [ -z "$TARBALL_URL" ]; then
TARBALL_URL="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*\"tarball\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)"
TARBALL_URL="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*"tarball":"\([^"]*\)".*/\1/p' | head -n1)"
fi
# Fallback: resolve version then construct tarball URL
if [ -z "$TARBALL_URL" ]; then
@@ -103,10 +106,10 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
RESOLVED_VERSION="$(printf "%s" "$META_JSON" | node -e 'try{const fs=require("fs");const data=JSON.parse(fs.readFileSync(0,"utf8"));if(data&&data.version){console.log(data.version);}}catch(e){}')"
fi
if [ -z "$RESOLVED_VERSION" ]; then
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*\"version\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)"
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p' | head -n1)"
fi
if [ -z "$RESOLVED_VERSION" ]; then
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | grep -o '\"version\":\"[^\"]*\"' | head -n1 | cut -d '\"' -f4)"
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | grep -o '"version":"[^"]*"' | head -n1 | cut -d '"' -f4)"
fi
if [ -n "$RESOLVED_VERSION" ]; then
VERSION_TO_USE="$RESOLVED_VERSION"
@@ -138,9 +141,9 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
fi
if [ -z "$BIN_PATH" ]; then
# sed fallbacks (handle both string and object forms)
BIN_PATH=$(sed -n 's/.*\"bin\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' "$TMP_DIR/package/package.json" | head -n1)
BIN_PATH=$(sed -n 's/.*"bin"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$TMP_DIR/package/package.json" | head -n1)
if [ -z "$BIN_PATH" ]; then
BIN_PATH=$(sed -n '/\"bin\"[[:space:]]*:[[:space:]]*{/,/}/p' "$TMP_DIR/package/package.json" | sed -n 's/.*\"mux\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)
BIN_PATH=$(sed -n '/"bin"[[:space:]]*:[[:space:]]*{/,/}/p' "$TMP_DIR/package/package.json" | sed -n 's/.*"mux"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
fi
fi
if [ -n "$BIN_PATH" ] && [ -f "$TMP_DIR/package/$BIN_PATH" ]; then
+121
View File
@@ -0,0 +1,121 @@
---
display_name: Vault CLI
description: Installs the Hashicorp Vault CLI and optionally configures token authentication
icon: ../../../../.icons/vault.svg
verified: true
tags: [helper, integration, vault, cli]
---
# Vault CLI
Installs the [Vault](https://www.vaultproject.io/) CLI and optionally configures token authentication. This module focuses on CLI installation and can be used standalone or as a base for other authentication methods.
```tf
module "vault_cli" {
source = "registry.coder.com/coder/vault-cli/coder"
version = "1.1.1"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
}
```
## Prerequisites
The following tools are required in the workspace image:
- **HTTP client**: `curl`, `wget`, or `busybox` (at least one)
- **Archive utility**: `unzip` or `busybox` (at least one)
- **jq**: Optional but recommended for reliable JSON parsing (falls back to sed if not available)
## With Token Authentication
If you have a Vault token, you can provide it to automatically configure authentication:
```tf
module "vault_cli" {
source = "registry.coder.com/coder/vault-cli/coder"
version = "1.1.1"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_token = var.vault_token # Optional
}
```
## Examples
### Basic Installation (CLI Only)
Install the Vault CLI without any authentication:
```tf
module "vault_cli" {
source = "registry.coder.com/coder/vault-cli/coder"
version = "1.1.1"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
}
```
### With Specific Version
```tf
module "vault_cli" {
source = "registry.coder.com/coder/vault-cli/coder"
version = "1.1.1"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_cli_version = "1.15.0"
}
```
### Custom Installation Directory
```tf
module "vault_cli" {
source = "registry.coder.com/coder/vault-cli/coder"
version = "1.1.1"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
install_dir = "/home/coder/bin"
}
```
### With Vault Enterprise Namespace
For Vault Enterprise users who need to specify a namespace:
```tf
module "vault_cli" {
source = "registry.coder.com/coder/vault-cli/coder"
version = "1.1.1"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_token = var.vault_token
vault_namespace = "admin/my-namespace"
}
```
### Vault Enterprise Binary
Install the Vault Enterprise binary. This is required if using SAML authentication to Vault:
```tf
module "vault_cli" {
source = "registry.coder.com/coder/vault-cli/coder"
version = "1.1.1"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
enterprise = true
}
```
## Related Modules
For more advanced authentication methods, see:
- [vault-github](https://registry.coder.com/modules/coder/vault-github) - Authenticate with Vault using GitHub tokens
- [vault-jwt](https://registry.coder.com/modules/coder/vault-jwt) - Authenticate with Vault using OIDC/JWT
For simple token-based authentication, see:
- [vault-token](https://registry.coder.com/modules/coder/vault-token) - Authenticate with Vault using a token
+97
View File
@@ -0,0 +1,97 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.17"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "vault_addr" {
type = string
description = "The address of the Vault server."
}
variable "vault_token" {
type = string
description = "The Vault token to use for authentication. If not provided, only the CLI will be installed."
default = ""
sensitive = true
}
variable "install_dir" {
type = string
description = "The directory to install the Vault CLI to."
default = "/usr/local/bin"
}
variable "vault_cli_version" {
type = string
description = "The version of the Vault CLI to install."
default = "latest"
validation {
condition = var.vault_cli_version == "latest" || can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+$", var.vault_cli_version))
error_message = "vault_cli_version must be either 'latest' or a semantic version (e.g., '1.15.0')."
}
}
variable "vault_namespace" {
type = string
description = "The Vault Enterprise namespace to use. If not provided, no namespace will be configured."
default = null
}
variable "enterprise" {
type = bool
description = "Whether to install the enterprise version of the Vault CLI. Required if using SAML authentication to Vault."
default = false
}
data "coder_workspace" "me" {}
resource "coder_script" "vault_cli" {
agent_id = var.agent_id
display_name = "Vault CLI"
icon = "/icon/vault.svg"
script = templatefile("${path.module}/run.sh", {
VAULT_ADDR = var.vault_addr
VAULT_TOKEN = var.vault_token
INSTALL_DIR = var.install_dir
VAULT_CLI_VERSION = var.vault_cli_version
ENTERPRISE = var.enterprise
})
run_on_start = true
start_blocks_login = true
}
resource "coder_env" "vault_addr" {
agent_id = var.agent_id
name = "VAULT_ADDR"
value = var.vault_addr
}
resource "coder_env" "vault_token" {
count = var.vault_token != "" ? 1 : 0
agent_id = var.agent_id
name = "VAULT_TOKEN"
value = var.vault_token
}
resource "coder_env" "vault_namespace" {
count = var.vault_namespace != null ? 1 : 0
agent_id = var.agent_id
name = "VAULT_NAMESPACE"
value = var.vault_namespace
}
output "vault_cli_version" {
description = "The version of the Vault CLI that was installed."
value = var.vault_cli_version
}
@@ -0,0 +1,176 @@
mock_provider "coder" {}
variables {
agent_id = "test-agent-id"
vault_addr = "https://vault.example.com"
}
run "test_vault_cli_without_token" {
assert {
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
error_message = "Display name should be 'Vault CLI'"
}
assert {
condition = resource.coder_env.vault_addr.name == "VAULT_ADDR"
error_message = "VAULT_ADDR environment variable should be set"
}
assert {
condition = resource.coder_env.vault_addr.value == "https://vault.example.com"
error_message = "VAULT_ADDR should match the provided vault_addr"
}
assert {
condition = length(resource.coder_env.vault_token) == 0
error_message = "VAULT_TOKEN should not be set when vault_token is not provided"
}
assert {
condition = length(resource.coder_env.vault_namespace) == 0
error_message = "VAULT_NAMESPACE should not be set when vault_namespace is not provided"
}
}
run "test_vault_cli_with_token" {
variables {
vault_token = "test-vault-token"
}
assert {
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
error_message = "Display name should be 'Vault CLI'"
}
assert {
condition = resource.coder_env.vault_addr.name == "VAULT_ADDR"
error_message = "VAULT_ADDR environment variable should be set"
}
assert {
condition = length(resource.coder_env.vault_token) == 1
error_message = "VAULT_TOKEN should be set when vault_token is provided"
}
assert {
condition = resource.coder_env.vault_token[0].name == "VAULT_TOKEN"
error_message = "VAULT_TOKEN environment variable name should be correct"
}
assert {
condition = resource.coder_env.vault_token[0].value == "test-vault-token"
error_message = "VAULT_TOKEN should match the provided vault_token"
}
}
run "test_vault_cli_custom_version" {
variables {
vault_cli_version = "1.15.0"
}
assert {
condition = output.vault_cli_version == "1.15.0"
error_message = "Vault CLI version output should match the provided version"
}
}
run "test_vault_cli_custom_install_dir" {
variables {
install_dir = "/custom/install/dir"
}
assert {
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
error_message = "Display name should be 'Vault CLI'"
}
}
run "test_vault_cli_invalid_version" {
command = plan
variables {
vault_cli_version = "invalid-version"
}
expect_failures = [var.vault_cli_version]
}
run "test_vault_cli_valid_semver" {
variables {
vault_cli_version = "1.18.3"
}
assert {
condition = output.vault_cli_version == "1.18.3"
error_message = "Vault CLI version output should match the provided version"
}
}
run "test_vault_cli_rejects_v_prefix" {
command = plan
variables {
vault_cli_version = "v1.18.3"
}
expect_failures = [var.vault_cli_version]
}
run "test_vault_cli_with_namespace" {
variables {
vault_namespace = "admin/my-namespace"
}
assert {
condition = length(resource.coder_env.vault_namespace) == 1
error_message = "VAULT_NAMESPACE should be set when vault_namespace is provided"
}
assert {
condition = resource.coder_env.vault_namespace[0].name == "VAULT_NAMESPACE"
error_message = "VAULT_NAMESPACE environment variable name should be correct"
}
assert {
condition = resource.coder_env.vault_namespace[0].value == "admin/my-namespace"
error_message = "VAULT_NAMESPACE should match the provided vault_namespace"
}
}
run "test_vault_cli_with_token_and_namespace" {
variables {
vault_token = "test-vault-token"
vault_namespace = "admin/my-namespace"
}
assert {
condition = length(resource.coder_env.vault_token) == 1
error_message = "VAULT_TOKEN should be set when vault_token is provided"
}
assert {
condition = length(resource.coder_env.vault_namespace) == 1
error_message = "VAULT_NAMESPACE should be set when vault_namespace is provided"
}
assert {
condition = resource.coder_env.vault_token[0].value == "test-vault-token"
error_message = "VAULT_TOKEN should match the provided vault_token"
}
assert {
condition = resource.coder_env.vault_namespace[0].value == "admin/my-namespace"
error_message = "VAULT_NAMESPACE should match the provided vault_namespace"
}
}
run "test_vault_cli_enterprise" {
variables {
enterprise = true
}
assert {
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
error_message = "Display name should be 'Vault CLI'"
}
}
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env bash
# Convert all templated variables to shell variables
VAULT_ADDR=${VAULT_ADDR}
VAULT_TOKEN=${VAULT_TOKEN}
INSTALL_DIR=${INSTALL_DIR}
VAULT_CLI_VERSION=${VAULT_CLI_VERSION}
ENTERPRISE=${ENTERPRISE}
# Fetch URL content to stdout
fetch() {
url="$1"
if command -v curl > /dev/null 2>&1; then
curl -sSL --fail "$${url}"
elif command -v wget > /dev/null 2>&1; then
wget -qO- "$${url}"
elif command -v busybox > /dev/null 2>&1; then
busybox wget -qO- "$${url}"
else
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
return 1
fi
}
# Download URL to a file
fetch_to_file() {
dest="$1"
url="$2"
if command -v curl > /dev/null 2>&1; then
curl -sSL --fail "$${url}" -o "$${dest}"
elif command -v wget > /dev/null 2>&1; then
wget -O "$${dest}" "$${url}"
elif command -v busybox > /dev/null 2>&1; then
busybox wget -O "$${dest}" "$${url}"
else
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
return 1
fi
}
unzip_safe() {
if command -v unzip > /dev/null 2>&1; then
command unzip "$@"
elif command -v busybox > /dev/null 2>&1; then
busybox unzip "$@"
else
printf "unzip or busybox is not installed. Please install unzip in your image.\n"
return 1
fi
}
install() {
# Get the architecture of the system
ARCH=$(uname -m)
if [ "$${ARCH}" = "x86_64" ]; then
ARCH="amd64"
elif [ "$${ARCH}" = "aarch64" ]; then
ARCH="arm64"
else
printf "Unsupported architecture: %s\n" "$${ARCH}"
return 1
fi
# Determine OS and validate
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
if [ "$${OS}" != "linux" ] && [ "$${OS}" != "darwin" ]; then
printf "Unsupported OS: %s. Only linux and darwin are supported.\n" "$${OS}"
return 1
fi
# Fetch release information from HashiCorp API
if [ "$${VAULT_CLI_VERSION}" = "latest" ]; then
if [ "$${ENTERPRISE}" = "true" ]; then
API_URL="https://api.releases.hashicorp.com/v1/releases/vault/latest?license_class=enterprise"
else
API_URL="https://api.releases.hashicorp.com/v1/releases/vault/latest"
fi
else
# For specific version, append +ent suffix for enterprise
if [ "$${ENTERPRISE}" = "true" ]; then
API_URL="https://api.releases.hashicorp.com/v1/releases/vault/$${VAULT_CLI_VERSION}+ent"
else
API_URL="https://api.releases.hashicorp.com/v1/releases/vault/$${VAULT_CLI_VERSION}"
fi
fi
API_RESPONSE=$(fetch "$${API_URL}")
if [ -z "$${API_RESPONSE}" ]; then
printf "Failed to fetch release information from HashiCorp API.\n"
return 1
fi
# Parse version and download URL from API response
if command -v jq > /dev/null 2>&1; then
VAULT_CLI_VERSION=$(printf '%s' "$${API_RESPONSE}" | jq -r '.version')
DOWNLOAD_URL=$(printf '%s' "$${API_RESPONSE}" | jq -r --arg os "$${OS}" --arg arch "$${ARCH}" '.builds[] | select(.os == $os and .arch == $arch) | .url')
else
VAULT_CLI_VERSION=$(printf '%s' "$${API_RESPONSE}" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
# Fallback: construct URL manually if jq not available
DOWNLOAD_URL="https://releases.hashicorp.com/vault/$${VAULT_CLI_VERSION}/vault_$${VAULT_CLI_VERSION}_$${OS}_$${ARCH}.zip"
fi
if [ -z "$${VAULT_CLI_VERSION}" ]; then
printf "Failed to determine Vault version.\n"
return 1
fi
if [ -z "$${DOWNLOAD_URL}" ]; then
printf "Failed to determine download URL for Vault %s (%s/%s).\n" "$${VAULT_CLI_VERSION}" "$${OS}" "$${ARCH}"
return 1
fi
printf "Vault version: %s\n" "$${VAULT_CLI_VERSION}"
# Check if the vault CLI is installed and has the correct version
installation_needed=1
if command -v vault > /dev/null 2>&1; then
CURRENT_VERSION=$(vault version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
if [ "$${CURRENT_VERSION}" = "$${VAULT_CLI_VERSION}" ]; then
printf "Vault version %s is already installed and up-to-date.\n\n" "$${CURRENT_VERSION}"
installation_needed=0
fi
fi
if [ "$${installation_needed}" = "1" ]; then
# Download and install Vault
if [ -z "$${CURRENT_VERSION}" ]; then
printf "Installing Vault CLI ...\n\n"
else
printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "$${VAULT_CLI_VERSION}"
fi
# Create temporary directory for download
TEMP_DIR=$(mktemp -d)
cd "$${TEMP_DIR}" || return 1
printf "Downloading from %s\n" "$${DOWNLOAD_URL}"
if ! fetch_to_file vault.zip "$${DOWNLOAD_URL}"; then
printf "Failed to download Vault.\n"
rm -rf "$${TEMP_DIR}"
return 1
fi
if ! unzip_safe vault.zip; then
printf "Failed to unzip Vault.\n"
rm -rf "$${TEMP_DIR}"
return 1
fi
# Install to the specified directory
if [ -n "$${INSTALL_DIR}" ] && [ -w "$${INSTALL_DIR}" ]; then
mv vault "$${INSTALL_DIR}/vault"
printf "Vault installed to %s successfully!\n\n" "$${INSTALL_DIR}"
elif [ -n "$${INSTALL_DIR}" ] && [ ! -w "$${INSTALL_DIR}" ]; then
# Try with sudo if install dir specified but not writable
if sudo mv vault "$${INSTALL_DIR}/vault" 2> /dev/null; then
printf "Vault installed to %s successfully!\n\n" "$${INSTALL_DIR}"
else
printf "Warning: Cannot write to %s. " "$${INSTALL_DIR}"
mkdir -p ~/.local/bin
if mv vault ~/.local/bin/vault; then
printf "Installed to ~/.local/bin instead.\n"
printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
else
printf "Failed to install Vault.\n"
rm -rf "$${TEMP_DIR}"
return 1
fi
fi
elif sudo mv vault /usr/local/bin/vault 2> /dev/null; then
printf "Vault installed successfully!\n\n"
else
mkdir -p ~/.local/bin
if ! mv vault ~/.local/bin/vault; then
printf "Failed to move Vault to local bin.\n"
rm -rf "$${TEMP_DIR}"
return 1
fi
printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
fi
# Clean up temp directory
rm -rf "$${TEMP_DIR}"
fi
return 0
}
# Run installation
if ! install; then
printf "Failed to install Vault CLI.\n"
exit 1
fi
# Indicate token configuration status
if [ -n "$${VAULT_TOKEN}" ]; then
printf "Vault token has been configured via VAULT_TOKEN environment variable.\n"
else
printf "No Vault token provided. Use 'vault login' or set VAULT_TOKEN to authenticate.\n"
fi
@@ -1,5 +1,5 @@
---
display_name: VSCode Desktop Core
display_name: Coder VSCode Desktop Core
description: Building block for modules that need to link to an external VSCode-based IDE
icon: ../../../../.icons/coder.svg
verified: true
@@ -11,20 +11,20 @@ tags: [internal, library]
> [!CAUTION]
> We do not recommend using this module directly. Instead, please consider using one of our [Desktop IDE modules](https://registry.coder.com/modules?search=tag%3Aide).
The VSCode Desktop Core module is a building block for modules that need to expose access to VSCode-based IDEs. It is intended primarily to be used as a library to create modules for VSCode-based IDEs.
The VSCode Desktop Core module is a building block for modules that need to expose access to VSCode-based IDEs. It is intended primarily for internal use by Coder to create modules for VSCode-based IDEs.
```tf
module "vscode-desktop-core" {
source = "registry.coder.com/coder/vscode-desktop-core/coder"
version = "1.0.0"
version = "1.0.1"
agent_id = var.agent_id
coder_app_icon = "/icon/code.svg"
coder_app_slug = "vscode"
coder_app_display_name = "VS Code Desktop"
coder_app_order = var.order
coder_app_group = var.group
web_app_icon = "/icon/code.svg"
web_app_slug = "vscode"
web_app_display_name = "VS Code Desktop"
web_app_order = var.order
web_app_group = var.group
folder = var.folder
open_recent = var.open_recent
@@ -10,9 +10,11 @@ const appName = "vscode-desktop";
const defaultVariables = {
agent_id: "foo",
coder_app_icon: "/icon/code.svg",
coder_app_slug: "vscode",
coder_app_display_name: "VS Code Desktop",
web_app_icon: "/icon/code.svg",
web_app_slug: "vscode",
web_app_display_name: "VS Code Desktop",
protocol: "vscode",
};
@@ -21,80 +23,115 @@ describe("vscode-desktop-core", async () => {
testRequiredVariables(import.meta.dir, defaultVariables);
it("default output", async () => {
const state = await runTerraformApply(import.meta.dir, defaultVariables);
expect(state.outputs.ide_uri.value).toBe(
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
);
describe("coder_app", () => {
describe("IDE URI attributes", () => {
it("default output", async () => {
const state = await runTerraformApply(
import.meta.dir,
defaultVariables,
);
expect(state.outputs.ide_uri.value).toBe(
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
);
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === appName,
);
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === appName,
);
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBeNull();
});
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBeNull();
});
it("adds folder", async () => {
const state = await runTerraformApply(import.meta.dir, {
folder: "/foo/bar",
it("adds folder", async () => {
const state = await runTerraformApply(import.meta.dir, {
folder: "/foo/bar",
...defaultVariables,
...defaultVariables,
});
expect(state.outputs.ide_uri.value).toBe(
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
);
});
it("adds folder and open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
folder: "/foo/bar",
open_recent: "true",
...defaultVariables,
});
expect(state.outputs.ide_uri.value).toBe(
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
);
});
it("adds folder but not open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
folder: "/foo/bar",
openRecent: "false",
...defaultVariables,
});
expect(state.outputs.ide_uri.value).toBe(
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
);
});
it("adds open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
open_recent: "true",
...defaultVariables,
});
expect(state.outputs.ide_uri.value).toBe(
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
);
});
});
expect(state.outputs.ide_uri.value).toBe(
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
);
});
it("sets custom slug and display_name", async () => {
const state = await runTerraformApply(import.meta.dir, defaultVariables);
it("adds folder and open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
folder: "/foo/bar",
open_recent: "true",
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === appName,
);
...defaultVariables,
});
expect(state.outputs.ide_uri.value).toBe(
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
);
});
it("adds folder but not open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
folder: "/foo/bar",
openRecent: "false",
...defaultVariables,
});
expect(state.outputs.ide_uri.value).toBe(
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
);
});
it("adds open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
open_recent: "true",
...defaultVariables,
});
expect(state.outputs.ide_uri.value).toBe(
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
);
});
it("expect order to be set", async () => {
const state = await runTerraformApply(import.meta.dir, {
coder_app_order: "22",
...defaultVariables,
expect(coder_app?.instances[0].attributes.slug).toBe(
defaultVariables.web_app_slug,
);
expect(coder_app?.instances[0].attributes.display_name).toBe(
defaultVariables.web_app_display_name,
);
});
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === appName,
);
it("sets order", async () => {
const state = await runTerraformApply(import.meta.dir, {
web_app_order: "5",
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBe(22);
...defaultVariables,
});
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === appName,
);
expect(coder_app?.instances[0].attributes.order).toBe(5);
});
it("sets group", async () => {
const state = await runTerraformApply(import.meta.dir, {
web_app_group: "web-app-group",
...defaultVariables,
});
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === appName,
);
expect(coder_app?.instances[0].attributes.group).toBe("web-app-group");
});
});
});
@@ -28,31 +28,31 @@ variable "open_recent" {
variable "protocol" {
type = string
description = "The URI protocol for the IDE."
description = "The URI protocol the IDE."
}
variable "coder_app_icon" {
variable "web_app_icon" {
type = string
description = "The icon of the coder_app."
}
variable "coder_app_slug" {
variable "web_app_slug" {
type = string
description = "The slug of the coder_app."
}
variable "coder_app_display_name" {
variable "web_app_display_name" {
type = string
description = "The display name of the coder_app."
}
variable "coder_app_order" {
variable "web_app_order" {
type = number
description = "The order of the coder_app."
default = null
}
variable "coder_app_group" {
variable "web_app_group" {
type = string
description = "The group of the coder_app."
default = null
@@ -65,25 +65,38 @@ resource "coder_app" "vscode-desktop" {
agent_id = var.agent_id
external = true
icon = var.coder_app_icon
slug = var.coder_app_slug
display_name = var.coder_app_display_name
icon = var.web_app_icon
slug = var.web_app_slug
display_name = var.web_app_display_name
order = var.coder_app_order
group = var.coder_app_group
order = var.web_app_order
group = var.web_app_group
# While the call to "join" is not strictly necessary, it makes the URL more readable.
url = join("", [
"${var.protocol}://coder.coder-remote/open",
var.protocol,
"://coder.coder-remote/open",
"?owner=",
data.coder_workspace_owner.me.name,
"&workspace=",
data.coder_workspace.me.name,
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
var.open_recent ? "&openRecent" : "",
"&url=",
data.coder_workspace.me.access_url,
"&token=$SESSION_TOKEN",
])
/*
url = join("", [
"vscode://coder.coder-remote/open",
"?owner=${data.coder_workspace_owner.me.name}",
"&workspace=${data.coder_workspace.me.name}",
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
var.open_recent ? "&openRecent" : "",
"&url=${data.coder_workspace.me.access_url}",
# NOTE: There is a protocol whitelist for the token replacement, so this will only work with the protocols hardcoded in the front-end.
# (https://github.com/coder/coder/blob/6ba4b5bbc95e2e528d7f5b1e31fffa200ae1a6db/site/src/modules/apps/apps.ts#L18)
"&token=$SESSION_TOKEN",
])
*/
}
output "ide_uri" {
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "vscode" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-desktop/coder"
version = "1.1.2"
version = "1.2.0"
agent_id = coder_agent.main.id
}
```
@@ -29,7 +29,7 @@ module "vscode" {
module "vscode" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-desktop/coder"
version = "1.1.2"
version = "1.2.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
}
@@ -22,7 +22,10 @@ describe("vscode-desktop", async () => {
);
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "vscode",
(res) =>
res.type === "coder_app" &&
res.module === "module.vscode-desktop-core" &&
res.name === "vscode-desktop",
);
expect(coder_app).not.toBeNull();
@@ -71,19 +74,4 @@ describe("vscode-desktop", async () => {
"vscode://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
it("expect order to be set", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
order: "22",
});
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "vscode",
);
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBe(22);
});
});
+15 -24
View File
@@ -38,33 +38,24 @@ variable "group" {
default = null
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
module "vscode-desktop-core" {
source = "registry.coder.com/coder/vscode-desktop-core/coder"
version = "1.0.0"
resource "coder_app" "vscode" {
agent_id = var.agent_id
external = true
icon = "/icon/code.svg"
slug = "vscode"
display_name = "VS Code Desktop"
order = var.order
group = var.group
agent_id = var.agent_id
url = join("", [
"vscode://coder.coder-remote/open",
"?owner=",
data.coder_workspace_owner.me.name,
"&workspace=",
data.coder_workspace.me.name,
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
var.open_recent ? "&openRecent" : "",
"&url=",
data.coder_workspace.me.access_url,
"&token=$SESSION_TOKEN",
])
coder_app_icon = "/icon/code.svg"
coder_app_slug = "vscode"
coder_app_display_name = "VS Code Desktop"
coder_app_order = var.order
coder_app_group = var.group
folder = var.folder
open_recent = var.open_recent
protocol = "vscode"
}
output "vscode_url" {
value = coder_app.vscode.url
value = module.vscode-desktop-core.ide_uri
description = "VS Code Desktop URL."
}
}
+4 -3
View File
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "windsurf" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder"
version = "1.2.1"
version = "1.3.0"
agent_id = coder_agent.main.id
}
```
@@ -29,7 +29,7 @@ module "windsurf" {
module "windsurf" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder"
version = "1.2.1"
version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
}
@@ -45,7 +45,7 @@ The following example configures Windsurf to use the GitHub MCP server with auth
module "windsurf" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder"
version = "1.2.1"
version = "1.3.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
mcp = jsonencode({
@@ -58,6 +58,7 @@ module "windsurf" {
"type" : "http"
}
}
})
}
+4 -16
View File
@@ -26,7 +26,10 @@ describe("windsurf", async () => {
);
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "windsurf",
(res) =>
res.type === "coder_app" &&
res.module === "module.vscode-desktop-core" &&
res.name === "vscode-desktop",
);
expect(coder_app).not.toBeNull();
@@ -76,21 +79,6 @@ describe("windsurf", async () => {
);
});
it("expect order to be set", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
order: 22,
});
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "windsurf",
);
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBe(22);
});
it("writes ~/.codeium/windsurf/mcp_config.json when mcp provided", async () => {
const id = await runContainer("alpine");
try {

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