Compare commits

..

23 Commits

Author SHA1 Message Date
Zach Kipp 05d12203ea feat(claude-code): add boundary log socket configuration
- Add CODER_AGENT_BOUNDARY_LOG_SOCKET env var for agent
- Add boundary_log_socket_path variable (default: /tmp/coder-boundary-audit.sock)
- Update start.sh to pass --audit-socket to boundary when env var is set

This enables boundary audit log forwarding to coderd when boundary is enabled.
2025-12-10 23:30:50 -07:00
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
DevCats 70ca76e86d ci: add shellcheck step (#484) 2025-11-27 12:00:04 +05:00
111 changed files with 4934 additions and 1008 deletions
@@ -82,7 +82,8 @@ create_incident() {
# Function to check for existing unresolved incidents
check_existing_incident() {
# Fetch the latest incidents with status not equal to "RESOLVED"
local unresolved_incidents=$(curl -s -X GET "https://api.instatus.com/v1/$INSTATUS_PAGE_ID/incidents" \
local unresolved_incidents
unresolved_incidents=$(curl -s -X GET "https://api.instatus.com/v1/$INSTATUS_PAGE_ID/incidents" \
-H "Authorization: Bearer $INSTATUS_API_KEY" \
-H "Content-Type: application/json" | jq -r '.incidents[] | select(.status != "RESOLVED") | .id')
+12 -1
View File
@@ -29,8 +29,11 @@ jobs:
- 'scripts/ts_test_auto.sh'
- 'scripts/terraform_test_all.sh'
- 'scripts/terraform_validate.sh'
- 'scripts/shellcheck_validate.sh'
modules:
- 'registry/**/modules/**'
shell:
- '**/*.sh'
all:
- '**'
- name: Set up Terraform
@@ -64,6 +67,14 @@ jobs:
SHARED_CHANGED: ${{ steps.filter.outputs.shared }}
MODULE_CHANGED_FILES: ${{ steps.filter.outputs.modules_files }}
run: bun terraform-validate
- name: Run ShellCheck
env:
ALL_CHANGED_FILES: ${{ steps.filter.outputs.all_files }}
SHARED_CHANGED: ${{ steps.filter.outputs.shared }}
SHELL_CHANGED_FILES: ${{ steps.filter.outputs.shell_files }}
run: bun shellcheck
- name: Validate set -u ordering
run: ./scripts/validate_set_u_order.sh
validate-style:
name: Check for typos and unformatted code
runs-on: ubuntu-latest
@@ -82,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.40.0
with:
config: .github/typos.toml
validate-readme-files:
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.3 MiB

+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

+22
View File
@@ -0,0 +1,22 @@
# ShellCheck configuration for Coder Registry
# https://www.shellcheck.net/wiki/
# Set default shell dialect to bash (most scripts use bash)
shell=bash
# Disable checks that conflict with Terraform templating syntax
# Many scripts use Terraform's templatefile() function with $${VAR} escape syntax
disable=SC2154 # Variable is referenced but not assigned (injected by Terraform)
disable=SC2034 # Variable appears unused (used via $${VAR} syntax)
disable=SC1083 # Literal braces (Terraform's $${VAR} escape syntax)
disable=SC2193 # Comparison arguments never equal (Terraform interpolation)
disable=SC2125 # Brace expansion/globs in assignments (Terraform syntax)
disable=SC2157 # Argument to -n/-z is always true/false (Terraform $${VAR} syntax)
disable=SC2066 # Loop will only run once (Terraform $${VAR} array syntax)
# Disable checks that conflict with intentional patterns
disable=SC2076 # Quoted regex in =~ (intentional literal string match, not regex, for array membership checks)
# Enable all optional checks for thorough analysis
enable=all
+252 -12
View File
@@ -1,17 +1,19 @@
{
"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",
},
"peerDependencies": {
"typescript": "^5.8.3",
@@ -19,54 +21,292 @@
},
},
"packages": {
"@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
"@felipecrs/decompress-tarxz": ["@felipecrs/decompress-tarxz@5.0.4", "", { "dependencies": { "@xhmikosr/decompress-tar": "^8.1.0", "file-type": "^20.5.0", "is-stream": "^2.0.1", "xz-decompress": "^0.2.3" } }, "sha512-a+nAnDsiUA84Sy/a+FKYJtjOjFvNtW8Jcbi3NwE8kJKPpYAxINFLYsC9mev9/wngiNEBA3jfHn0qNFwICeZNJw=="],
"@reteps/dockerfmt": ["@reteps/dockerfmt@0.3.6", "", {}, "sha512-Tb5wIMvBf/nLejTQ61krK644/CEMB/cpiaIFXqGApfGqO3GwcR3qnI0DbmkFVCl2OyEp8LnLX3EkucoL0+tbFg=="],
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@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=="],
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="],
"dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="],
"bare-events": ["bare-events@2.8.0", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"bl": ["bl@1.2.3", "", { "dependencies": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" } }, "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww=="],
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-alloc": ["buffer-alloc@1.2.0", "", { "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" } }, "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow=="],
"buffer-alloc-unsafe": ["buffer-alloc-unsafe@1.1.0", "", {}, "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"buffer-fill": ["buffer-fill@1.0.0", "", {}, "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="],
"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=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"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=="],
"decompress-tar": ["decompress-tar@4.1.1", "", { "dependencies": { "file-type": "^5.2.0", "is-stream": "^1.1.0", "tar-stream": "^1.5.2" } }, "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ=="],
"decompress-tarbz2": ["decompress-tarbz2@4.1.1", "", { "dependencies": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", "is-stream": "^1.1.0", "seek-bzip": "^1.0.5", "unbzip2-stream": "^1.0.9" } }, "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A=="],
"decompress-targz": ["decompress-targz@4.1.1", "", { "dependencies": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", "is-stream": "^1.1.0" } }, "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w=="],
"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.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=="],
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"envalid": ["envalid@8.1.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-OT6+qVhKVyCidaGoXflb2iK1tC8pd0OV2Q+v9n33wNhUJ+lus+rJobUj4vJaQBPxPZ0vYrPGuxdrenyCAIJcow=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="],
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
"is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
"is-natural-number": ["is-natural-number@4.0.1", "", {}, "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"marked": ["marked@16.2.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg=="],
"make-dir": ["make-dir@1.3.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"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=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
"pinkie": ["pinkie@2.0.4", "", {}, "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg=="],
"pinkie-promise": ["pinkie-promise@2.0.1", "", { "dependencies": { "pinkie": "^2.0.0" } }, "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"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=="],
"prettier-plugin-terraform-formatter": ["prettier-plugin-terraform-formatter@1.2.1", "", { "peerDependencies": { "prettier": ">= 1.16.0" }, "optionalPeers": ["prettier"] }, "sha512-rdzV61Bs/Ecnn7uAS/vL5usTX8xUWM+nQejNLZxt3I1kJH5WSeLEmq7LYu1wCoEQF+y7Uv1xGvPRfl3lIe6+tA=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
"seek-bzip": ["seek-bzip@1.0.6", "", { "dependencies": { "commander": "^2.8.1" }, "bin": { "seek-bunzip": "bin/seek-bunzip", "seek-table": "bin/seek-bzip-table" } }, "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"sh-syntax": ["sh-syntax@0.5.8", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-JfVoxf4FxQI5qpsPbkHhZo+n6N9YMJobyl4oGEUBb/31oQYlgTjkXQD8PBiafS2UbWoxrTO0Z5PJUBXEPAG1Zw=="],
"shellcheck": ["shellcheck@4.1.0", "", { "dependencies": { "@felipecrs/decompress-tarxz": "5.0.4", "@xhmikosr/decompress-unzip": "7.1.0", "decompress": "4.2.1", "envalid": "8.1.0", "global-agent": "3.0.0" }, "bin": { "shellcheck": "bin/shellcheck.js" } }, "sha512-8143z6YGO4+Puwp9Ghn/g7+QxllSKlXaZSm3HXfvQXUfRXhM5P8TPORRHBBlyobl9BnniVne+d1Ff6RgNiccsQ=="],
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
"streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="],
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
"strip-dirs": ["strip-dirs@2.1.0", "", { "dependencies": { "is-natural-number": "^4.0.1" } }, "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g=="],
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
"text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="],
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
"to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="],
"token-types": ["token-types@6.1.1", "", { "dependencies": { "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
"unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"xz-decompress": ["xz-decompress@0.2.3", "", {}, "sha512-O8v6HG8T0PrKBcpyWA13GkSYWFvncwzuzcLx5A7++l3HsE3atmoetXjIxrZ/JV/nbvSZ7WS4+3XvREZuVn+rEA=="],
"yauzl": ["yauzl@3.2.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" } }, "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w=="],
"decompress-tar/file-type": ["file-type@5.2.0", "", {}, "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ=="],
"decompress-tar/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="],
"decompress-tar/tar-stream": ["tar-stream@1.6.2", "", { "dependencies": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", "end-of-stream": "^1.0.0", "fs-constants": "^1.0.0", "readable-stream": "^2.3.0", "to-buffer": "^1.1.1", "xtend": "^4.0.0" } }, "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A=="],
"decompress-tarbz2/file-type": ["file-type@6.2.0", "", {}, "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg=="],
"decompress-tarbz2/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="],
"decompress-targz/file-type": ["file-type@5.2.0", "", {}, "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ=="],
"decompress-targz/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="],
"decompress-unzip/file-type": ["file-type@3.9.0", "", {}, "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA=="],
"decompress-unzip/get-stream": ["get-stream@2.3.1", "", { "dependencies": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" } }, "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA=="],
"decompress-unzip/yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
"make-dir/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="],
"roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"to-buffer/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
}
}
+8 -6
View File
@@ -6,17 +6,19 @@
"terraform-validate": "./scripts/terraform_validate.sh",
"tftest": "./scripts/terraform_test_all.sh",
"tstest": "./scripts/ts_test_auto.sh",
"shellcheck": "./scripts/shellcheck_validate.sh",
"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"
"prettier-plugin-terraform-formatter": "^1.2.1",
"shellcheck": "^4.1.0"
},
"peerDependencies": {
"typescript": "^5.8.3"
+5 -5
View File
@@ -15,8 +15,8 @@ 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.2"
agent_id = coder_agent.main.id
version = "1.0.3"
agent_id = coder_agent.example.id
}
```
@@ -39,8 +39,8 @@ module "tmux" {
```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.2"
agent_id = coder_agent.main.id
version = "1.0.3"
agent_id = coder_agent.example.id
tmux_config = "" # Optional: custom tmux.conf content
save_interval = 1 # Optional: save interval in minutes
sessions = ["default", "dev", "ops"] # Optional: list of tmux sessions
@@ -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.2"
version = "1.0.3"
agent_id = var.agent_id
sessions = ["default", "dev", "anomaly"]
tmux_config = <<-EOT
+1 -1
View File
@@ -144,7 +144,7 @@ main() {
printf "$${BOLD}✅ tmux setup complete! \n\n"
printf "$${BOLD} Attempting to restore sessions\n"
tmux new-session -d \; source-file ~/.tmux.conf \; run-shell '~/.tmux/plugins/tmux-resurrect/scripts/restore.sh'
tmux new-session -d \; source-file ~/.tmux.conf \; run-shell "$HOME/.tmux/plugins/tmux-resurrect/scripts/restore.sh"
printf "$${BOLD} Sessions restored: -> %s\n" "$(tmux ls)"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

+10 -10
View File
@@ -14,8 +14,8 @@ This module installs small, robust scripts in your workspace to create and extra
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.2"
agent_id = coder_agent.main.id
version = "0.0.3"
agent_id = coder_agent.example.id
paths = ["./projects", "./code"]
}
@@ -43,8 +43,8 @@ Basic example:
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.2"
agent_id = coder_agent.main.id
version = "0.0.3"
agent_id = coder_agent.example.id
# Paths to include in the archive (files or directories).
directory = "~"
@@ -61,8 +61,8 @@ Customize compression and output:
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.2"
agent_id = coder_agent.main.id
version = "0.0.3"
agent_id = coder_agent.example.id
directory = "/"
paths = ["/etc", "/home"]
@@ -78,8 +78,8 @@ Enable auto-archive on stop:
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.2"
agent_id = coder_agent.main.id
version = "0.0.3"
agent_id = coder_agent.example.id
# Creates /tmp/coder-archive.tar.gz of the users home directory (defaults).
create_on_stop = true
@@ -92,8 +92,8 @@ Extract on start:
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.2"
agent_id = coder_agent.main.id
version = "0.0.3"
agent_id = coder_agent.example.id
# Where to look for the archive file to extract:
output_dir = "/tmp"
+3 -2
View File
@@ -6,8 +6,8 @@ EXTRACT_ON_START="${TF_EXTRACT_ON_START}"
EXTRACT_WAIT_TIMEOUT="${TF_EXTRACT_WAIT_TIMEOUT}"
# Set script defaults from Terraform.
DEFAULT_PATHS=(${TF_PATHS})
DEFAULT_EXCLUDE_PATTERNS=(${TF_EXCLUDE_PATTERNS})
IFS=' ' read -r -a DEFAULT_PATHS <<< "${TF_PATHS}"
IFS=' ' read -r -a DEFAULT_EXCLUDE_PATTERNS <<< "${TF_EXCLUDE_PATTERNS}"
DEFAULT_COMPRESSION="${TF_COMPRESSION}"
DEFAULT_ARCHIVE_PATH="${TF_ARCHIVE_PATH}"
DEFAULT_DIRECTORY="${TF_DIRECTORY}"
@@ -62,6 +62,7 @@ echo "Installed extract script to: $EXTRACT_WRAPPER_PATH"
# 3) Optionally wait for and extract an archive on start.
if [[ $EXTRACT_ON_START = true ]]; then
# shellcheck disable=SC1090
. "$LIB_PATH"
archive_wait_and_extract "$EXTRACT_WAIT_TIMEOUT" quiet || {
+6 -8
View File
@@ -14,7 +14,7 @@ Run Auggie CLI in your workspace to access Augment's AI coding assistant with ad
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.2.2"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
```
@@ -41,14 +41,14 @@ data "coder_parameter" "ai_prompt" {
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/coder-login/coder"
version = "1.0.31"
agent_id = coder_agent.main.id
version = "0.2.2"
agent_id = coder_agent.example.id
}
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.2.2"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
folder = "/home/coder/project"
# Authentication
@@ -57,7 +57,7 @@ module "auggie" {
EOF # Required for tasks
# Version
auggie_version = "0.3.0"
auggie_version = "0.2.2"
# Task configuration
ai_prompt = data.coder_parameter.ai_prompt.value
@@ -74,7 +74,6 @@ EOF # Required for tasks
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/coder/project"]
}
}
}
EOF
@@ -105,7 +104,7 @@ EOF
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.2.2"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
folder = "/home/coder/project"
# Multiple MCP configuration files
@@ -128,7 +127,6 @@ module "auggie" {
],
"timeout": 600
}
}
}
EOF
@@ -1,7 +1,10 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi
set -euo pipefail
BOLD='\033[0;1m'
@@ -1,7 +1,10 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi
set -euo pipefail
command_exists() {
command -v "$1" > /dev/null 2>&1
+5 -5
View File
@@ -14,7 +14,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "3.1.1"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
openai_api_key = var.openai_api_key
workdir = "/home/coder/project"
}
@@ -34,7 +34,7 @@ module "codex" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/codex/coder"
version = "3.1.1"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
openai_api_key = "..."
workdir = "/home/coder/project"
report_tasks = false
@@ -55,14 +55,14 @@ data "coder_parameter" "ai_prompt" {
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/coder-login/coder"
version = "1.0.31"
agent_id = coder_agent.main.id
version = "3.1.1"
agent_id = coder_agent.example.id
}
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "3.1.1"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
openai_api_key = "..."
ai_prompt = data.coder_parameter.ai_prompt.value
workdir = "/home/coder/project"
@@ -38,7 +38,8 @@ find_session_for_directory() {
return 1
fi
local session_id=$(grep "^$target_dir|" "$SESSION_TRACKING_FILE" | cut -d'|' -f2 | head -1)
local session_id
session_id=$(grep "^$target_dir|" "$SESSION_TRACKING_FILE" | cut -d'|' -f2 | head -1)
if [ -n "$session_id" ]; then
echo "$session_id"
@@ -74,9 +75,12 @@ find_recent_session_file() {
local latest_time=0
while IFS= read -r session_file; do
local file_time=$(stat -c %Y "$session_file" 2> /dev/null || stat -f %m "$session_file" 2> /dev/null || echo "0")
local first_line=$(head -n 1 "$session_file" 2> /dev/null)
local session_cwd=$(echo "$first_line" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4)
local file_time
file_time=$(stat -c %Y "$session_file" 2> /dev/null || stat -f %m "$session_file" 2> /dev/null || echo "0")
local first_line
first_line=$(head -n 1 "$session_file" 2> /dev/null)
local session_cwd
session_cwd=$(echo "$first_line" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4)
if [ "$session_cwd" = "$target_dir" ] && [ "$file_time" -gt "$latest_time" ]; then
latest_file="$session_file"
@@ -85,8 +89,10 @@ find_recent_session_file() {
done < <(find "$sessions_dir" -type f -name "*.jsonl" 2> /dev/null)
if [ -n "$latest_file" ]; then
local first_line=$(head -n 1 "$latest_file")
local session_id=$(echo "$first_line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
local first_line
first_line=$(head -n 1 "$latest_file")
local session_id
session_id=$(echo "$first_line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
if [ -n "$session_id" ]; then
echo "$session_id"
return 0
@@ -102,7 +108,8 @@ wait_for_session_file() {
local attempt=0
while [ $attempt -lt $max_attempts ]; do
local session_id=$(find_recent_session_file "$target_dir" 2> /dev/null || echo "")
local session_id
session_id=$(find_recent_session_file "$target_dir" 2> /dev/null || echo "")
if [ -n "$session_id" ]; then
echo "$session_id"
return 0
@@ -14,7 +14,7 @@ Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-c
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
}
```
@@ -52,7 +52,7 @@ data "coder_parameter" "ai_prompt" {
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
ai_prompt = data.coder_parameter.ai_prompt.value
@@ -72,11 +72,11 @@ Customize tool permissions, MCP servers, and Copilot settings:
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
# Version pinning (defaults to "latest", use specific version if desired)
copilot_version = "0.0.334"
copilot_version = "0.2.3"
# Tool permissions
allow_tools = ["shell(git)", "shell(npm)", "write"]
@@ -101,7 +101,6 @@ module "copilot" {
tools = ["*"]
trust = true
}
playwright = {
command = "npx"
args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated"]
@@ -144,7 +143,7 @@ variable "github_token" {
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
github_token = var.github_token
}
@@ -158,7 +157,7 @@ Run Copilot as a command-line tool without task reporting or web interface. This
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
workdir = "/home/coder"
report_tasks = false
cli_app = true
@@ -1,7 +1,10 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi
set -euo pipefail
command_exists() {
command -v "$1" > /dev/null 2>&1
@@ -1,7 +1,11 @@
#!/bin/bash
if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi
set -euo pipefail
source "$HOME"/.bashrc
export PATH="$HOME/.local/bin:$PATH"
command_exists() {
@@ -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}"
@@ -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.main.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
install_sourcegraph_amp = true
agentapi_version = "latest"
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "2.1.0"
agent_id = coder_agent.example.id
amp_api_key = var.amp_api_key
install_amp = true
agentapi_version = "latest"
}
```
@@ -48,8 +48,8 @@ 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.1"
agent_id = coder_agent.main.id
amp_version = "2.1.0"
agent_id = coder_agent.example.id
amp_api_key = var.amp_api_key # recommended for tasks usage
workdir = "/home/coder/project"
instruction_prompt = <<-EOT
@@ -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" {
@@ -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)
@@ -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
@@ -1,8 +1,6 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
# ANSI colors
BOLD='\033[1m'
GREEN='\033[0;32m'
@@ -1,14 +1,16 @@
#!/bin/bash
set -euo pipefail
# Load user environment
# shellcheck source=/dev/null
source "$HOME/.bashrc"
# shellcheck source=/dev/null
if [ -f "$HOME/.bashrc" ]; then
source "$HOME/.bashrc"
fi
if [ -f "$HOME/.nvm/nvm.sh" ]; then
source "$HOME/.nvm/nvm.sh"
fi
set -euo pipefail
export PATH="$HOME/.local/bin:$HOME/.amp/bin:$HOME/.npm-global/bin:$PATH"
function ensure_command() {
@@ -27,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
@@ -48,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"
@@ -56,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.

After

Width:  |  Height:  |  Size: 249 KiB

@@ -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."
}
+16 -17
View File
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.3"
version = "4.2.6"
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.6"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
enable_boundary = true
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.3"
}
```
@@ -70,7 +72,7 @@ data "coder_parameter" "ai_prompt" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.3"
version = "4.2.6"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
@@ -78,8 +80,8 @@ module "claude-code" {
# OR
claude_code_oauth_token = "xxxxx-xxxx-xxxx"
claude_code_version = "1.0.82" # Pin to a specific version
agentapi_version = "v0.10.0"
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"
@@ -89,11 +91,9 @@ module "claude-code" {
mcp = <<-EOF
{
"mcpServers": {
"memory": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-memory"],
"env": {}
"my-custom-tool": {
"command": "my-tool-server"
"args": ["--port", "8080"]
}
}
@@ -109,13 +109,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.3"
version = "4.2.6"
agent_id = coder_agent.main.id
workdir = "/home/coder"
workdir = "/home/coder/project"
install_claude_code = true
claude_code_version = "latest"
claude_code_version = "2.0.62"
report_tasks = false
cli_app = true
}
```
@@ -132,7 +131,7 @@ variable "claude_code_oauth_token" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.3"
version = "4.2.6"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
@@ -205,7 +204,7 @@ resource "coder_env" "bedrock_api_key" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.3"
version = "4.2.6"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
@@ -262,7 +261,7 @@ resource "coder_env" "google_application_credentials" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.3"
version = "4.2.6"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514"
+142 -38
View File
@@ -208,13 +208,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 +230,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 +328,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");
});
});
+20 -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.4"
}
variable "ai_prompt" {
@@ -252,6 +252,12 @@ variable "compile_boundary_from_source" {
default = false
}
variable "boundary_log_socket_path" {
type = string
description = "Path to the Unix socket for boundary audit logs. Both the agent and boundary use this path."
default = "/tmp/coder-boundary-audit.sock"
}
resource "coder_env" "claude_code_md_path" {
count = var.claude_md_path == "" ? 0 : 1
@@ -288,15 +294,22 @@ resource "coder_env" "disable_autoupdater" {
value = "1"
}
resource "coder_env" "boundary_log_socket" {
count = var.enable_boundary ? 1 : 0
agent_id = var.agent_id
name = "CODER_BOUNDARY_LOG_SOCKET"
value = var.boundary_log_socket_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 +370,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}' \
@@ -90,12 +90,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
@@ -51,19 +51,6 @@ 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
@@ -99,17 +86,85 @@ function validate_claude_installation() {
# This ensures all task sessions use a consistent, predictable ID
TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2"
get_project_dir() {
local workdir_normalized
workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
echo "$HOME/.claude/projects/${workdir_normalized}"
}
get_task_session_file() {
echo "$(get_project_dir)/${TASK_SESSION_ID}.jsonl"
}
task_session_exists() {
local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
local project_dir="$HOME/.claude/projects/${workdir_normalized}"
local session_file
session_file=$(get_task_session_file)
printf "PROJECT_DIR: %s, workdir_normalized: %s\n" "$project_dir" "$workdir_normalized"
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"
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
}
@@ -132,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[@]}")"
@@ -226,15 +247,20 @@ function start_agentapi() {
fi
# Set HTTP Proxy port used by Boundary
BOUNDARY_ARGS+=(--proxy-port $ARG_BOUNDARY_PROXY_PORT)
BOUNDARY_ARGS+=(--proxy-port "$ARG_BOUNDARY_PROXY_PORT")
# Pass audit socket path if CODER_AGENT_BOUNDARY_LOG_SOCKET is set
if [ -n "$CODER_AGENT_BOUNDARY_LOG_SOCKET" ]; then
BOUNDARY_ARGS+=(--audit-socket "$CODER_AGENT_BOUNDARY_LOG_SOCKET")
fi
# Set log level for boundary
BOUNDARY_ARGS+=(--log-level $ARG_BOUNDARY_LOG_LEVEL)
BOUNDARY_ARGS+=(--log-level "$ARG_BOUNDARY_LOG_LEVEL")
if [ "${ARG_ENABLE_BOUNDARY_PPROF:-false}" = "true" ]; then
# Enable boundary pprof server on specified port
BOUNDARY_ARGS+=(--pprof)
BOUNDARY_ARGS+=(--pprof-port ${ARG_BOUNDARY_PPROF_PORT})
BOUNDARY_ARGS+=(--pprof-port "$ARG_BOUNDARY_PPROF_PORT")
fi
agentapi server --type claude --term-width 67 --term-height 1190 -- \
+9 -10
View File
@@ -15,7 +15,7 @@ module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
}
```
@@ -30,8 +30,8 @@ module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
agent_id = coder_agent.main.id
install_version = "4.8.3"
agent_id = coder_agent.example.id
install_version = "1.4.1"
}
```
@@ -44,7 +44,7 @@ module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
extensions = [
"dracula-theme.theme-dracula"
]
@@ -62,12 +62,11 @@ module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
"workbench.colorTheme" = "Dracula"
}
}
```
@@ -80,7 +79,7 @@ module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
}
```
@@ -94,7 +93,7 @@ module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
additional_args = "--disable-workspace-trust"
}
```
@@ -110,7 +109,7 @@ module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
use_cached = true
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
}
@@ -123,7 +122,7 @@ module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
offline = true
}
```
@@ -88,6 +88,7 @@ function extension_installed() {
if [ "${USE_CACHED_EXTENSIONS}" != true ]; then
return 1
fi
# shellcheck disable=SC2066
for _extension in "$${EXTENSIONS_ARRAY[@]}"; do
if [ "$_extension" == "$1" ]; then
echo "Extension $1 was already installed."
@@ -99,6 +100,7 @@ function extension_installed() {
# Install each extension...
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
# shellcheck disable=SC2066
for extension in "$${EXTENSIONLIST[@]}"; do
if [ -z "$extension" ]; then
continue
+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."
}
}
@@ -15,7 +15,7 @@ The devcontainers-cli module provides an easy way to install [`@devcontainers/cl
```tf
module "devcontainers-cli" {
source = "registry.coder.com/coder/devcontainers-cli/coder"
version = "1.0.33"
agent_id = coder_agent.main.id
version = "1.0.34"
agent_id = coder_agent.example.id
}
```
@@ -4,7 +4,7 @@
# might contain a `package.json` with `packageManager` set to something
# other than the detected package manager. When this happens, it can
# cause the installation to fail.
cd "$CODER_SCRIPT_DATA_DIR"
cd "$CODER_SCRIPT_DATA_DIR" || exit
# If @devcontainers/cli is already installed, we can skip
if command -v devcontainer > /dev/null 2>&1; then
+12 -13
View File
@@ -18,8 +18,8 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.2.2"
agent_id = coder_agent.main.id
version = "1.2.3"
agent_id = coder_agent.example.id
}
```
@@ -31,8 +31,8 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.2.2"
agent_id = coder_agent.main.id
version = "1.2.3"
agent_id = coder_agent.example.id
}
```
@@ -42,8 +42,8 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.2.2"
agent_id = coder_agent.main.id
version = "1.2.3"
agent_id = coder_agent.example.id
user = "root"
}
```
@@ -54,16 +54,15 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.2.2"
agent_id = coder_agent.main.id
version = "1.2.3"
agent_id = coder_agent.example.id
}
module "dotfiles-root" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.2.2"
agent_id = coder_agent.main.id
version = "1.2.3"
agent_id = coder_agent.example.id
user = "root"
dotfiles_uri = module.dotfiles.dotfiles_uri
}
@@ -77,8 +76,8 @@ You can set a default dotfiles repository for all users by setting the `default_
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.2.2"
agent_id = coder_agent.main.id
version = "1.2.3"
agent_id = coder_agent.example.id
default_dotfiles_uri = "https://github.com/coder/dotfiles"
}
```
+1
View File
@@ -5,6 +5,7 @@ set -euo pipefail
DOTFILES_URI="${DOTFILES_URI}"
DOTFILES_USER="${DOTFILES_USER}"
# shellcheck disable=SC2157
if [ -n "$${DOTFILES_URI// }" ]; then
if [ -z "$DOTFILES_USER" ]; then
DOTFILES_USER="$USER"
+25 -29
View File
@@ -14,8 +14,8 @@ 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.1"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
```
@@ -28,8 +28,8 @@ module "git-clone" {
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.1"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
base_dir = "~/projects/coder"
}
@@ -43,12 +43,11 @@ 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.1"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
data "coder_external_auth" "github" {
id = "github"
}
@@ -70,18 +69,17 @@ 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.1"
agent_id = coder_agent.main.id
version = "1.2.2"
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
source = "registry.coder.com/coder/code-server/coder"
version = "1.0.18"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
order = 1
folder = "/home/${local.username}/${module.git_clone[count.index].folder_name}"
}
@@ -89,7 +87,7 @@ module "code-server" {
# Create a Coder app for the website
resource "coder_app" "website" {
count = data.coder_workspace.me.start_count
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
order = 2
slug = "website"
external = true
@@ -105,14 +103,13 @@ 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.1"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.example.com/coder/coder/tree/feat/example"
git_providers = {
"https://github.example.com/" = {
provider = "github"
}
}
}
```
@@ -125,8 +122,8 @@ 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.1"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
}
```
@@ -137,14 +134,13 @@ 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.1"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
git_providers = {
"https://gitlab.example.com/" = {
provider = "gitlab"
}
}
}
```
@@ -159,8 +155,8 @@ 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.1"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
branch_name = "feat/example"
}
@@ -177,8 +173,8 @@ 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.1"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
folder_name = "coder-dev"
base_dir = "~/projects/coder"
@@ -196,8 +192,8 @@ If not defined, the default, `0`, performs a full clone.
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-clone/coder"
version = "1.2.0"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
depth = 1
}
@@ -212,8 +208,8 @@ 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.1"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
post_clone_script = <<-EOT
#!/bin/bash
+1 -1
View File
@@ -60,7 +60,7 @@ if [ -n "$POST_CLONE_SCRIPT" ]; then
echo "Running post-clone script..."
echo "$POST_CLONE_SCRIPT" | base64 -d > /tmp/post_clone.sh
chmod +x /tmp/post_clone.sh
cd "$CLONE_PATH"
cd "$CLONE_PATH" || exit
/tmp/post_clone.sh
rm /tmp/post_clone.sh
fi
+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
@@ -15,7 +15,7 @@ module "kasmvnc" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kasmvnc/coder"
version = "1.2.6"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
desktop_environment = "xfce"
subdomain = true
}
+4 -2
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env bash
# Exit on error, undefined variables, and pipe failures
set -euo pipefail
set -eo pipefail
error() {
printf "💀 ERROR: %s\n" "$@"
@@ -121,6 +120,9 @@ fi
# shellcheck disable=SC1091
source /etc/os-release
set -u
distro="$ID"
distro_version="$VERSION_ID"
codename="$VERSION_CODENAME"
+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.5"
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.5"
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.5"
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.5"
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.5"
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.5"
agent_id = coder_agent.main.id
install = false
}
+3
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
+4 -4
View File
@@ -14,8 +14,8 @@ Add the `slackme` command to your workspace that DMs you on Slack when your comm
module "slackme" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/slackme/coder"
version = "1.0.32"
agent_id = coder_agent.main.id
version = "1.0.33"
agent_id = coder_agent.example.id
auth_provider_id = "slack"
}
```
@@ -74,8 +74,8 @@ slackme npm run long-build
module "slackme" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/slackme/coder"
version = "1.0.32"
agent_id = coder_agent.main.id
version = "1.0.33"
agent_id = coder_agent.example.id
auth_provider_id = "slack"
slack_message = <<EOF
👋 Hey there from Coder! $COMMAND took $DURATION to execute!
+2 -2
View File
@@ -73,13 +73,13 @@ fi
START=$(date +%s%N)
# Run all arguments as a command
$@
"$@"
END=$(date +%s%N)
DURATION_MS=$${DURATION_MS:-$(((END - START) / 1000000))}
PRETTY_DURATION=$(pretty_duration $DURATION_MS)
set -e
COMMAND=$(echo $@)
COMMAND=$(echo "$@")
SLACK_MESSAGE=$(echo "$SLACK_MESSAGE" | sed "s|\\$COMMAND|$COMMAND|g")
SLACK_MESSAGE=$(echo "$SLACK_MESSAGE" | sed "s|\\$DURATION|$PRETTY_DURATION|g")
+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.0"
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.0"
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.0"
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.0"
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.0"
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.0"
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.0"
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'"
}
}
+204
View File
@@ -0,0 +1,204 @@
#!/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. If dest is provided, write to file; otherwise output to stdout.
# Usage: fetch <url> [dest]
fetch() {
url="$1"
dest="$${2:-}"
# Detect HTTP client on first run
if [ -z "$${HTTP_CLIENT:-}" ]; then
if command -v curl > /dev/null 2>&1; then
HTTP_CLIENT="curl"
elif command -v wget > /dev/null 2>&1; then
HTTP_CLIENT="wget"
elif command -v busybox > /dev/null 2>&1; then
HTTP_CLIENT="busybox"
else
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
return 1
fi
fi
if [ -n "$${dest}" ]; then
# shellcheck disable=SC2195
case "$${HTTP_CLIENT}" in
curl) curl -sSL --fail "$${url}" -o "$${dest}" ;;
wget) wget -O "$${dest}" "$${url}" ;;
busybox) busybox wget -O "$${dest}" "$${url}" ;;
esac
else
# shellcheck disable=SC2195
case "$${HTTP_CLIENT}" in
curl) curl -sSL --fail "$${url}" ;;
wget) wget -qO- "$${url}" ;;
busybox) busybox wget -qO- "$${url}" ;;
esac
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 "$${DOWNLOAD_URL}" vault.zip; 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
@@ -14,8 +14,8 @@ This module lets you authenticate with [Hashicorp Vault](https://www.vaultprojec
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-github/coder"
version = "1.1.1"
agent_id = coder_agent.main.id
version = "1.1.2"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
}
```
@@ -46,8 +46,8 @@ To configure the Vault module, you must set up a Vault GitHub auth method. See t
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-github/coder"
version = "1.1.1"
agent_id = coder_agent.main.id
version = "1.1.2"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
coder_github_auth_id = "my-github-auth-id"
}
@@ -59,8 +59,8 @@ module "vault" {
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-github/coder"
version = "1.1.1"
agent_id = coder_agent.main.id
version = "1.1.2"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
coder_github_auth_id = "my-github-auth-id"
vault_github_auth_path = "my-github-auth-path"
@@ -73,9 +73,9 @@ module "vault" {
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-github/coder"
version = "1.1.1"
agent_id = coder_agent.main.id
version = "1.1.2"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_cli_version = "1.15.0"
vault_cli_version = "1.1.2"
}
```
@@ -47,6 +47,7 @@ install() {
if [ "$${INSTALL_VERSION}" = "latest" ]; then
LATEST_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -v 'rc' | grep -oE 'vault/[0-9]+\.[0-9]+\.[0-9]+' | sed 's/vault\///' | sort -V | tail -n 1)
printf "Latest version of Vault is %s.\n\n" "$${LATEST_VERSION}"
# shellcheck disable=SC2157
if [ -z "$${LATEST_VERSION}" ]; then
printf "Failed to determine the latest Vault version.\n"
return 1
@@ -64,8 +65,10 @@ install() {
fi
fi
# shellcheck disable=SC2170
if [ $${installation_needed} -eq 1 ]; then
# Download and install Vault
# shellcheck disable=SC2157
if [ -z "$${CURRENT_VERSION}" ]; then
printf "Installing Vault CLI ...\n\n"
else
+13 -13
View File
@@ -14,8 +14,8 @@ This module lets you authenticate with [Hashicorp Vault](https://www.vaultprojec
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-jwt/coder"
version = "1.2.1"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_jwt_role = "coder" # The Vault role to use for authentication
vault_jwt_token = "eyJhbGciOiJIUzI1N..." # optional, if not present, defaults to user's oidc authentication token
@@ -42,8 +42,8 @@ curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/d
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-jwt/coder"
version = "1.2.1"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_jwt_auth_path = "oidc"
vault_jwt_role = "coder" # The Vault role to use for authentication
@@ -58,8 +58,8 @@ data "coder_workspace_owner" "me" {}
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-jwt/coder"
version = "1.2.1"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_jwt_role = data.coder_workspace_owner.me.groups[0]
}
@@ -71,11 +71,11 @@ module "vault" {
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-jwt/coder"
version = "1.2.1"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_jwt_role = "coder" # The Vault role to use for authentication
vault_cli_version = "1.17.5"
vault_cli_version = "1.2.2"
}
```
@@ -87,11 +87,11 @@ terraform {
required_providers {
jwt = {
source = "geektheripper/jwt"
version = "1.1.4"
version = "1.2.2"
}
time = {
source = "hashicorp/time"
version = "0.11.1"
version = "1.2.2"
}
}
}
@@ -132,8 +132,8 @@ resource "jwt_signed_token" "vault" {
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vault-jwt/coder"
version = "1.2.1"
agent_id = coder_agent.main.id
version = "1.2.2"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_jwt_role = "coder" # The Vault role to use for authentication
vault_jwt_token = jwt_signed_token.vault[0].token
+3
View File
@@ -48,6 +48,7 @@ install() {
if [ "$${VAULT_CLI_VERSION}" = "latest" ]; then
LATEST_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -v 'rc' | grep -oE 'vault/[0-9]+\.[0-9]+\.[0-9]+' | sed 's/vault\///' | sort -V | tail -n 1)
printf "Latest version of Vault is %s.\n\n" "$${LATEST_VERSION}"
# shellcheck disable=SC2157
if [ -z "$${LATEST_VERSION}" ]; then
printf "Failed to determine the latest Vault version.\n"
return 1
@@ -65,8 +66,10 @@ install() {
fi
fi
# shellcheck disable=SC2170
if [ $${installation_needed} -eq 1 ]; then
# Download and install Vault
# shellcheck disable=SC2157
if [ -z "$${CURRENT_VERSION}" ]; then
printf "Installing Vault CLI ...\n\n"
else
+3 -3
View File
@@ -20,7 +20,7 @@ variable "vault_token" {
module "vault" {
source = "registry.coder.com/coder/vault-token/coder"
version = "1.3.1"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
vault_token = var.token # optional
vault_addr = "https://vault.example.com"
vault_namespace = "prod" # optional, vault enterprise only
@@ -74,9 +74,9 @@ variable "vault_token" {
module "vault" {
source = "registry.coder.com/coder/vault-token/coder"
version = "1.3.1"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_token = var.token
vault_cli_version = "1.19.0"
vault_cli_version = "1.3.1"
}
```
@@ -45,6 +45,7 @@ install() {
if [ "$${INSTALL_VERSION}" = "latest" ]; then
LATEST_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -v 'rc' | grep -oE 'vault/[0-9]+\.[0-9]+\.[0-9]+' | sed 's/vault\///' | sort -V | tail -n 1)
printf "Latest version of Vault is %s.\n\n" "$${LATEST_VERSION}"
# shellcheck disable=SC2157
if [ -z "$${LATEST_VERSION}" ]; then
printf "Failed to determine the latest Vault version.\n"
return 1
@@ -62,8 +63,10 @@ install() {
fi
fi
# shellcheck disable=SC2170
if [ $${installation_needed} -eq 1 ]; then
# Download and install Vault
# shellcheck disable=SC2157
if [ -z "$${CURRENT_VERSION}" ]; then
printf "Installing Vault CLI ...\n\n"
else
@@ -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."
}
}
+12 -13
View File
@@ -14,8 +14,8 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.4.2"
agent_id = coder_agent.main.id
version = "1.4.3"
agent_id = coder_agent.example.id
accept_license = true
}
```
@@ -30,8 +30,8 @@ module "vscode-web" {
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.4.2"
agent_id = coder_agent.main.id
version = "1.4.3"
agent_id = coder_agent.example.id
install_prefix = "/home/coder/.vscode-web"
folder = "/home/coder"
accept_license = true
@@ -44,8 +44,8 @@ module "vscode-web" {
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.4.2"
agent_id = coder_agent.main.id
version = "1.4.3"
agent_id = coder_agent.example.id
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
accept_license = true
}
@@ -59,13 +59,12 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.4.2"
agent_id = coder_agent.main.id
version = "1.4.3"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
"workbench.colorTheme" = "Dracula"
}
accept_license = true
}
```
@@ -78,8 +77,8 @@ By default, this module installs the latest. To pin a specific version, retrieve
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.4.2"
agent_id = coder_agent.main.id
version = "1.4.3"
agent_id = coder_agent.example.id
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
accept_license = true
}
@@ -94,8 +93,8 @@ Note: Either `workspace` or `folder` can be used, but not both simultaneously. T
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.4.2"
agent_id = coder_agent.main.id
version = "1.4.3"
agent_id = coder_agent.example.id
workspace = "/home/coder/coder.code-workspace"
}
```
+1
View File
@@ -94,6 +94,7 @@ printf "$${BOLD}VS Code Web has been installed.\n"
# Install each extension...
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
# shellcheck disable=SC2066
for extension in "$${EXTENSIONLIST[@]}"; do
if [ -z "$extension" ]; then
continue
+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 {
+18 -23
View File
@@ -16,7 +16,7 @@ variable "agent_id" {
variable "folder" {
type = string
description = "The folder to open in Cursor IDE."
description = "The folder to open in Windsurf Editor."
default = ""
}
@@ -63,26 +63,21 @@ locals {
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
}
resource "coder_app" "windsurf" {
agent_id = var.agent_id
external = true
icon = "/icon/windsurf.svg"
slug = var.slug
display_name = var.display_name
order = var.order
group = var.group
url = join("", [
"windsurf://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/windsurf.svg"
coder_app_slug = "windsurf"
coder_app_display_name = "Windsurf Editor"
coder_app_order = var.order
coder_app_group = var.group
folder = var.folder
open_recent = var.open_recent
protocol = "windsurf"
}
resource "coder_script" "windsurf_mcp" {
@@ -102,6 +97,6 @@ resource "coder_script" "windsurf_mcp" {
}
output "windsurf_url" {
value = coder_app.windsurf.url
value = module.vscode-desktop-core.ide_uri
description = "Windsurf Editor URL."
}
}
@@ -2,7 +2,7 @@
display_name: Positron Desktop
description: Add a one-click button to launch Positron Desktop
icon: ../../../../.icons/positron.svg
verified: true
verified: false
tags: [ide, positron]
---
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "positron" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/cytoshahar/positron/coder"
version = "1.0.0"
version = "1.0.1"
agent_id = coder_agent.main.id
}
```
@@ -21,7 +21,10 @@ describe("positron-desktop", async () => {
);
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "positron",
(res) =>
res.type === "coder_app" &&
res.module === "module.vscode-desktop-core" &&
res.name === "vscode-desktop",
);
expect(coder_app).not.toBeNull();
@@ -70,19 +73,4 @@ describe("positron-desktop", async () => {
"positron://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 === "positron",
);
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBe(22);
});
});
+27 -25
View File
@@ -9,10 +9,6 @@ terraform {
}
}
locals {
icon_url = "/icon/positron.svg"
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
@@ -42,33 +38,39 @@ variable "group" {
default = null
}
variable "slug" {
type = string
description = "The slug of the app."
default = "cursor"
}
variable "display_name" {
type = string
description = "The display name of the app."
default = "Cursor Desktop"
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_app" "positron" {
agent_id = var.agent_id
external = true
icon = local.icon_url
slug = "positron"
display_name = "Positron Desktop"
order = var.order
group = var.group
module "vscode-desktop-core" {
source = "registry.coder.com/coder/vscode-desktop-core/coder"
version = "1.0.0"
url = join("", [
"positron://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",
])
agent_id = var.agent_id
coder_app_icon = "/icon/positron.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 = "positron"
}
output "positron_url" {
value = coder_app.positron.url
value = module.vscode-desktop-core.ide_uri
description = "Positron Desktop URL."
}
+2 -2
View File
@@ -36,7 +36,7 @@ module "copyparty" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/djarbz/copyparty/coder"
version = "1.0.2"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
arguments = [
"-v", "/home/coder/:/home:r", # Share home directory (read-only)
"-v", "${local.repo_dir}:/repo:rw", # Share project directory (read-write)
@@ -52,7 +52,7 @@ module "copyparty" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/djarbz/copyparty/coder"
version = "1.0.2"
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
subdomain = true
arguments = [
"-v", "/tmp:/tmp:r", # Share tmp directory (read-only)
+1
View File
@@ -12,6 +12,7 @@ PINNED_VERSION="${PINNED_VERSION}"
# Custom CLI Arguments
# The variable from Terraform is a series of quoted and space separated strings.
# We need to parse it into a proper bash array.
# shellcheck disable=SC2206
ARGUMENTS=(${ARGUMENTS})
# VARIABLE appears unused. Verify use (or export if used externally).
Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

+22
View File
@@ -0,0 +1,22 @@
---
display_name: "Michael Orlov"
bio: "Platform Engineer specializing in cloud infrastructure, DevOps automation, and developer experience tools"
avatar: "./.images/avatar.png"
github: "harleylrn"
support_email: "michael.orlov@gmail.com"
status: "community"
---
# Michael Orlov
Platform Engineer specializing in cloud infrastructure, DevOps automation, and developer experience tools. Contributing modules and templates to enhance developer productivity in Coder workspaces.
## Modules
### kiro-cli
AI-powered coding assistant integration for Coder workspaces with MCP (Model Context Protocol) support and task reporting capabilities.
## About
I focus on creating tools and integrations that improve developer experience and productivity. My contributions to the Coder Registry aim to provide seamless integration of modern development tools and AI assistants into cloud development environments.
@@ -0,0 +1,396 @@
---
display_name: Kiro CLI
description: Run Kiro CLI in your workspace to access AI coding assistant with MCP integration and task reporting.
icon: ../../../../.icons/kiro.svg
verified: true
tags: [agent, ai, kiro, kiro-cli, tasks]
---
# Kiro CLI
Run [Kiro CLI](https://kiro.dev/) in your workspace to access AI coding assistant. This module provides a complete integration with Coder workspaces, including automatic installation, MCP (Model Context Protocol) integration for task reporting, and support for custom pre/post install scripts.
```tf
module "kiro-cli" {
source = "registry.coder.com/harleylrn/kiro-cli/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
# Required: Authentication tarball (see below for generation)
auth_tarball = <<-EOF
base64encoded-tarball
EOF
}
```
![Kiro CLI in action](../../.images/kiro-cli.png)
## Prerequisites
- **zstd** - Required for compressing the authentication tarball
- **Ubuntu/Debian**: `sudo apt-get install zstd`
- **RHEL/CentOS/Fedora**: `sudo yum install zstd` or `sudo dnf install zstd`
- **auth_tarball** - Required for installation and authentication
### Authentication Tarball
You must generate an authenticated Kiro CLI tarball on another machine where you have successfully logged in:
```bash
# 1. Install Kiro CLI and login on your local machine
kiro-cli login
# 2. Generate the authentication tarball
cd ~/.local/share/kiro-cli
tar -c . | zstd | base64 -w 0
```
Copy the output and use it as the `auth_tarball` variable.
## Detailed Authentication Setup
**Step 1: Install Kiro CLI locally**
- Download from [Kiro CLI](https://kiro.dev/)
- Follow the installation instructions for your platform
**Step 2: Authenticate**
```bash
kiro-cli login
```
Complete the authentication process in your browser.
**Step 3: Generate tarball**
```bash
cd ~/.local/share/kiro-cli
tar -c . | zstd | base64 -w 0 > /tmp/kiro-cli-auth.txt
```
**Step 4: Use in Terraform**
```tf
variable "kiro_cli_auth_tarball" {
type = string
sensitive = true
default = "PASTE_YOUR_TARBALL_HERE"
}
```
> [!IMPORTANT]
>
> - Regenerate the tarball if you logout or re-authenticate
> - Each user needs their own authentication tarball
> - Keep the tarball secure as it contains authentication credentials
### Coder Tasks Integration
To enable integration with [Coder Tasks](https://coder.com/docs/ai-coder/tasks), you need to define the `coder_task` data source, create the `coder_ai_task` resource, and configure the module with the task prompt.
```tf
data "coder_task" "me" {}
module "kiro-cli" {
count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0
source = "registry.coder.com/harleylrn/kiro-cli/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.kiro_cli_auth_tarball
ai_prompt = data.coder_task.me.prompt
trust_all_tools = true
# Task reporting configuration
report_tasks = true
# Enable CLI app alongside web app
cli_app = true
web_app_display_name = "Kiro CLI"
cli_app_display_name = "Kiro CLI"
}
resource "coder_ai_task" "task" {
count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0
app_id = module.kiro-cli[count.index].task_app_id
}
```
> [!IMPORTANT]
>
> - The `data "coder_task" "me" {}` data source provides the task prompt and enabled state
> - The module count is controlled by `data.coder_task.me.enabled` to only create when a task is active
> - The `coder_ai_task` resource links the module's task reporting to Coder's task system
> - The `ai_prompt` is passed from `data.coder_task.me.prompt`
> - Without this configuration, `coder_ai_task` resources will not function properly
>
> **_Security Notice_**
> In order to allow the tasks flow non-interactively all the tools are trusted
> This flag bypasses standard permission checks and allows Kiro CLI broader access to your system than normally permitted.
> While this enables more functionality, it also means Kiro CLI can potentially execute commands with the same privileges as the user running it.
> Use this module only in trusted environments and be aware of the security implications.
### Default System Prompt
The module includes a simple system prompt that instructs Kiro CLI:
```
You are a helpful Coding assistant. Aim to autonomously investigate
and solve issues the user gives you and test your work, whenever possible.
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
but opt for autonomy.
```
You can customize this behavior by providing your own system prompt via the `system_prompt` variable.
### Default Coder MCP Instructions
The module includes specific instructions for the Coder MCP server integration that are separate from the system prompt:
```
YOU MUST REPORT ALL TASKS TO CODER.
When reporting tasks you MUST follow these EXACT instructions:
- IMMEDIATELY report status after receiving ANY user message
- Be granular If you are investigating with multiple steps report each step to coder.
Task state MUST be one of the following:
- Use "state": "working" when actively processing WITHOUT needing additional user input
- Use "state": "complete" only when finished with a task
- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers.
Task summaries MUST:
- Include specifics about what you're doing
- Include clear and actionable steps for the user
- Be less than 160 characters in length
```
You can customize these instructions by providing your own via the `coder_mcp_instructions` variable.
## Default Agent Configuration
The module includes a default agent configuration template that provides a comprehensive setup for Kiro CLI integration:
```json
{
"name": "agent",
"description": "This is an default agent config",
"prompt": "${system_prompt}",
"mcpServers": {},
"tools": ["read", "write", "shell", "aws", "@coder", "knowledge"],
"toolAliases": {},
"allowedTools": ["read", "@coder"],
"resources": [
"file://KiroQ.md",
"file://README.md",
"file://.kiro/steering/**/*.md"
],
"hooks": {},
"toolsSettings": {},
"useLegacyMcpJson": true
}
```
### Configuration Details:
- **Tools Available:** File operations (`read`, `write`), shell execution (`shell`), AWS CLI (`aws`), Coder MCP integration (`@coder`), and knowledge base access (`knowledge`)
- **@coder Tool:** Enables Coder MCP integration for task reporting (`coder_report_task` and related tools)
- **Allowed Tools:** By default, only `read` and `@coder` are allowed (can be customized for security)
- **Resources:** Access to documentation and rule files in the workspace
- **MCP Servers:** Empty by default, can be configured via `agent_config` variable
- **System Prompt:** Dynamically populated from the `system_prompt` variable
- **Legacy MCP:** Uses legacy MCP JSON format for compatibility
You can override this configuration by providing your own JSON via the `agent_config` variable.
### Agent Name Configuration
The module automatically extracts the agent name from the `"name"` field in the `agent_config` JSON and uses it for:
- **Configuration File:** Saves the agent config as `~/.kiro/agents/{agent_name}.json`
- **Default Agent:** Sets the agent as the default using `q settings chat.defaultAgent {agent_name}`
- **MCP Integration:** Associates the Coder MCP server with the specified agent name
If no custom `agent_config` is provided, the default agent name "agent" is used.
## Usage Examples
### Basic Usage
```tf
module "kiro-cli" {
source = "registry.coder.com/harleylrn/kiro-cli/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.kiro_cli_auth_tarball
}
```
This example will:
1. Download and install Kiro CLI latest version
2. Extract authentication tarball to ~/.local/share/kiro-cli
3. Configure Coder MCP integration for task reporting
4. Create default agent configuration file
5. Start Kiro CLI in /home/coder directory
6. Provide web interface through AgentAPI
> [!IMPORTANT]
> By default `write` tool is not allowed, which will pause the task execution
> and will wait for the prompt to approve its usage.
> To avoid this, and allow the normal task flow, user has two options:
>
> - Change the parameter `trust_all_tools` value to `true` (default to `false`)
> OR
> - Provide your own agent configuration with the tools of your choice allowed
### With Custom AI Prompt
```tf
module "kiro-cli" {
source = "registry.coder.com/harleylrn/kiro-cli/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.kiro_cli_auth_tarball
ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
trust_all_tools = true
}
```
> [!IMPORTANT]
> **_Security Notice_**
> In order to allow the tasks flow non-interactively all the tools are trusted
> This flag bypasses standard permission checks and allows Kiro CLI broader access to your system than normally permitted.
> While this enables more functionality, it also means Kiro CLI can potentially execute commands with the same privileges as the user running it.
> Use this module only in trusted environments and be aware of the security implications.
### With Custom Pre/Post Install Scripts
```tf
module "kiro-cli" {
source = "registry.coder.com/harleylrn/kiro-cli/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.kiro_cli_auth_tarball
pre_install_script = <<-EOT
#!/bin/bash
echo "Setting up custom environment..."
# Install additional dependencies
sudo apt-get update && sudo apt-get install -y zstd
EOT
post_install_script = <<-EOT
#!/bin/bash
echo "Configuring Kiro CLI settings..."
# Custom configuration commands
kiro-cli settings chat.model claude-3-sonnet
EOT
}
```
### Specific Version Installation
```tf
module "kiro-cli" {
source = "registry.coder.com/harleylrn/kiro-cli/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.kiro_cli_auth_tarball
kiro_cli_version = "1.14.0" # Specific version
install_kiro_cli = true
}
```
### Custom Agent Configuration
```tf
module "kiro-cli" {
source = "registry.coder.com/harleylrn/kiro-cli/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.kiro_cli_auth_tarball
agent_config = <<-EOT
{
"name": "custom-agent",
"description": "Custom Kiro CLI agent for my workspace",
"prompt": "You are a specialized DevOps assistant...",
"tools": ["read", "write", "shell", "aws"]
}
EOT
}
```
### With Custom AgentAPI Configuration
```tf
module "kiro-cli" {
source = "registry.coder.com/harleylrn/kiro-cli/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.kiro_cli_auth_tarball
# AgentAPI configuration for environments without wildcard access url. https://coder.com/docs/admin/setup#wildcard-access-url
agentapi_chat_based_path = true
agentapi_version = "v0.10.0"
}
```
### Air-Gapped Installation
For environments without direct internet access, you can host Kiro CLI installation files internally and configure the module to use your internal repository:
```tf
module "kiro-cli" {
source = "registry.coder.com/harleylrn/kiro-cli/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.kiro_cli_auth_tarball
# Point to internal artifact repository
kiro_install_url = "https://artifacts.internal.corp/kiro-cli-releases"
# Use specific version available in your repository
kiro_cli_version = "latest"
}
```
**Prerequisites for Air-Gapped Setup:**
1. Download Kiro CLI installation files from the official source and host them internally
2. Maintain the same directory structure: `{base_url}/{version}/kirocli-{arch}-linux.zip`
3. Ensure both architectures are available:
- `kirocli-x86_64-linux.zip` for Intel/AMD systems
- `kirocli-aarch64-linux.zip` for ARM systems
4. Configure network access from Coder workspaces to your internal repository
## Troubleshooting
### Common Issues
**Authentication issues:**
- Regenerate the auth tarball on your local machine
- Ensure the tarball is properly base64 encoded
- Check that the original authentication is still valid
**MCP integration not working:**
- Verify that AgentAPI is installed (`install_agentapi = true`)
- Check that the Coder agent is properly configured
- Review the system prompt configuration
## Outputs
| Name | Description |
| -------------------------------------------------------------------- | ----------- |
| <a name="output_task_app_id"></a> [task_app_id](#output_task_app_id) | n/a |
@@ -0,0 +1,372 @@
run "required_variables" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
}
}
run "minimal_config" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdA==" # base64 "test"
}
assert {
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug environment variable not configured correctly"
}
assert {
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
error_message = "Status slug value should be 'kiro-cli'"
}
}
# Test Case 1: Basic Usage No Autonomous Use of Q
# Using vanilla Kubernetes Deployment Template configuration
run "test_case_1_basic_usage" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
}
# Q is installed and authenticated
assert {
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug environment variable should be configured for basic usage"
}
assert {
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
error_message = "Status slug value should be 'kiro-cli' for basic usage"
}
# AgentAPI is installed and configured (default behavior)
assert {
condition = length(resource.coder_env.auth_tarball) == 1
error_message = "Auth tarball environment variable should be created for authentication"
}
# Foundational configuration applied
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated with foundational configuration"
}
# No additional parameters required (using defaults)
assert {
condition = local.agent_name == "agent"
error_message = "Default agent name should be 'agent' when no custom config provided"
}
}
# Test Case 2: Autonomous Usage Autonomous Use of Q
# AI prompt passed through from external source (Tasks interface or Issue Tracker CI)
run "test_case_2_autonomous_usage" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
}
# Q is installed and authenticated
assert {
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug environment variable should be configured for autonomous usage"
}
assert {
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
error_message = "Status slug value should be 'kiro-cli' for autonomous usage"
}
# AgentAPI is installed and configured
assert {
condition = length(resource.coder_env.auth_tarball) == 1
error_message = "Auth tarball environment variable should be created for autonomous usage"
}
# Foundational configuration for all components applied
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated for autonomous usage"
}
# AI prompt is configured
assert {
condition = local.full_prompt == "Help me set up a Python FastAPI project with proper testing structure"
error_message = "AI prompt should be configured correctly for autonomous usage"
}
# Default agent name when no custom config
assert {
condition = local.agent_name == "agent"
error_message = "Default agent name should be 'agent' for autonomous usage"
}
}
# Test Case 3: Extended Configuration Parameter Validation and File Rendering
# Validates extended configuration options and parameter application
run "test_case_3_extended_configuration" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
kiro_cli_version = "1.14.1"
kiro_install_url = "https://desktop-release.q.us-east-1.amazonaws.com"
install_kiro_cli = true
install_agentapi = true
agentapi_version = "v0.6.0"
trust_all_tools = true
ai_prompt = "Help me create a production-grade TypeScript monorepo with testing and deployment"
system_prompt = "You are a helpful software assistant working in a secure enterprise environment"
pre_install_script = "echo 'Pre-install setup'"
post_install_script = "echo 'Post-install cleanup'"
agent_config = jsonencode({
name = "production-agent"
description = "Production Kiro CLI agent for enterprise environment"
prompt = "You are a helpful software assistant working in a secure enterprise environment"
mcpServers = {}
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
toolAliases = {}
allowedTools = ["fs_read"]
resources = ["file://KiroQ.md", "file://README.md", "file://.kiro/steering/**/*.md"]
hooks = {}
toolsSettings = {}
useLegacyMcpJson = true
})
}
# All installation parameters are applied correctly
assert {
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
error_message = "Status slug should be configured correctly with extended parameters"
}
assert {
condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
error_message = "Auth tarball should be configured correctly with extended parameters"
}
# Custom agent configuration is loaded and referenced correctly
assert {
condition = local.agent_name == "production-agent"
error_message = "Agent name should be extracted from custom agent config"
}
assert {
condition = length(local.agent_config) > 0
error_message = "Custom agent config should be processed correctly"
}
# AI prompt and system prompt are configured
assert {
condition = local.full_prompt == "Help me create a production-grade TypeScript monorepo with testing and deployment"
error_message = "AI prompt should be configured correctly in extended configuration"
}
# Pre-install and post-install scripts are provided
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated correctly for extended configuration"
}
}
run "full_config" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
install_kiro_cli = true
install_agentapi = true
agentapi_version = "v0.5.0"
kiro_cli_version = "latest"
trust_all_tools = true
ai_prompt = "Build a web application"
auth_tarball = "dGVzdA=="
order = 1
group = "AI Tools"
icon = "/icon/custom-kiro-cli.svg"
pre_install_script = "echo 'pre-install'"
post_install_script = "echo 'post-install'"
agent_config = jsonencode({
name = "test-agent"
description = "Test agent configuration"
prompt = "You are a helpful AI assistant for testing."
mcpServers = {}
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
toolAliases = {}
allowedTools = ["fs_read"]
resources = ["file://KiroQ.md", "file://README.md", "file://.kiro/steering/**/*.md"]
hooks = {}
toolsSettings = {}
useLegacyMcpJson = true
})
}
assert {
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug environment variable not configured correctly"
}
assert {
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
error_message = "Status slug value should be 'kiro-cli'"
}
assert {
condition = length(resource.coder_env.auth_tarball) == 1
error_message = "Auth tarball environment variable should be created when provided"
}
}
run "auth_tarball_environment" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
}
assert {
condition = resource.coder_env.auth_tarball[0].name == "KIRO_CLI_AUTH_TARBALL"
error_message = "Auth tarball environment variable name should be 'KIRO_CLI_AUTH_TARBALL'"
}
assert {
condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
error_message = "Auth tarball environment variable value should match input"
}
}
run "empty_auth_tarball" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = ""
}
assert {
condition = length(resource.coder_env.auth_tarball) == 0
error_message = "Auth tarball environment variable should not be created when empty"
}
}
run "custom_system_prompt" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
system_prompt = "Custom system prompt for testing"
}
# Test that the system prompt is used in the agent config template
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated with custom system prompt"
}
}
run "install_options" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
install_kiro_cli = false
install_agentapi = false
}
assert {
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug should still be configured even when install options are disabled"
}
}
run "version_configuration" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
kiro_cli_version = "2.15.0"
agentapi_version = "v0.4.0"
}
assert {
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
error_message = "Status slug value should remain 'kiro-cli' regardless of version"
}
}
# Additional test for agent name extraction
run "agent_name_extraction" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
agent_config = jsonencode({
name = "custom-enterprise-agent"
description = "Custom enterprise agent configuration"
prompt = "You are a custom enterprise AI assistant."
mcpServers = {}
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
toolAliases = {}
allowedTools = ["fs_read", "fs_write"]
resources = ["file://README.md"]
hooks = {}
toolsSettings = {}
useLegacyMcpJson = true
})
}
assert {
condition = local.agent_name == "custom-enterprise-agent"
error_message = "Agent name should be extracted correctly from custom agent config"
}
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be processed correctly"
}
}
# Test for JSON encoding validation
run "json_encoding_validation" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
system_prompt = "Multi-line\nsystem prompt\nwith newlines"
}
assert {
condition = length(local.system_prompt) > 0
error_message = "System prompt should be JSON encoded correctly"
}
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated correctly with multi-line system prompt"
}
}
@@ -0,0 +1,531 @@
import { describe, it, expect } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
findResourceInstance,
} from "~test";
import path from "path";
const moduleDir = path.resolve(__dirname);
// Always provide agent_config to bypass template parsing issues
const baseAgentConfig = JSON.stringify({
name: "test-agent",
description: "Test agent configuration",
prompt: "You are a helpful AI assistant.",
mcpServers: {},
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
toolAliases: {},
allowedTools: ["fs_read"],
resources: ["file://README.md", "file://.kiro/steering/**/*.md"],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
});
const requiredVars = {
agent_id: "dummy-agent-id",
agent_config: baseAgentConfig,
workdir: "/tmp/test-workdir",
};
const fullConfigVars = {
agent_id: "dummy-agent-id",
workdir: "/tmp/test-workdir",
install_kiro_cli: true,
install_agentapi: true,
agentapi_version: "v0.6.0",
kiro_cli_version: "1.14.1",
kiro_install_url: "https://desktop-release.q.us-east-1.amazonaws.com",
trust_all_tools: false,
ai_prompt: "Build a comprehensive test suite",
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
order: 1,
group: "AI Tools",
icon: "/icon/custom-kiro-cli.svg",
pre_install_script: "echo 'Starting pre-install'",
post_install_script: "echo 'Completed post-install'",
agent_config: baseAgentConfig,
};
describe("kiro-cli module v1.0.0", async () => {
await runTerraformInit(moduleDir);
// Test Case 1: Basic Usage No Autonomous Use of Q
// Matches CDES-203 Test Case #1: Basic Usage
it("Test Case 1: Basic Usage - No Autonomous Use of Q", async () => {
const basicUsageVars = {
agent_id: "dummy-agent-id",
workdir: "/tmp/test-workdir",
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
};
const state = await runTerraformApply(moduleDir, basicUsageVars);
// Q is installed and authenticated
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("kiro-cli");
// AgentAPI is installed and configured (default behavior)
const authTarballEnv = findResourceInstance(
state,
"coder_env",
"auth_tarball",
);
expect(authTarballEnv).toBeDefined();
expect(authTarballEnv.name).toBe("KIRO_CLI_AUTH_TARBALL");
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
// Foundational configuration for all components is applied
// No additional parameters are required for the module to work
// Using the terminal application and Q chat returns a functional interface
});
// Test Case 2: Autonomous Usage Autonomous Use of Q
// Matches CDES-203 Test Case 2: Autonomous Usage
it("Test Case 2: Autonomous Usage - Autonomous Use of Q", async () => {
const autonomousUsageVars = {
agent_id: "dummy-agent-id",
workdir: "/tmp/test-workdir",
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
ai_prompt:
"Help me set up a Python FastAPI project with proper testing structure",
};
const state = await runTerraformApply(moduleDir, autonomousUsageVars);
// Q is installed and authenticated
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("kiro-cli");
// AgentAPI is installed and configured
const authTarballEnv = findResourceInstance(
state,
"coder_env",
"auth_tarball",
);
expect(authTarballEnv).toBeDefined();
expect(authTarballEnv.name).toBe("KIRO_CLI_AUTH_TARBALL");
// AI prompt is passed through from external source
// The Chat interface functions as required
// The Tasks interface functions as required
// The template can be invoked from GitHub integration as expected
});
// Test Case 3: Extended Configuration Parameter Validation and File Rendering
// Matches CDES-203 Test Case 3: Extended Configuration
it("Test Case 3: Extended Configuration - Parameter Validation and File Rendering", async () => {
const extendedConfigVars = {
agent_id: "dummy-agent-id",
workdir: "/tmp/test-workdir",
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
kiro_cli_version: "1.14.1",
kiro_install_url: "https://desktop-release.q.us-east-1.amazonaws.com",
install_kiro_cli: true,
install_agentapi: true,
agentapi_version: "v0.6.0",
trust_all_tools: true,
ai_prompt:
"Help me create a production-grade TypeScript monorepo with testing and deployment",
system_prompt:
"You are a helpful software assistant working in a secure enterprise environment",
pre_install_script: "echo 'Pre-install setup'",
post_install_script: "echo 'Post-install cleanup'",
agent_config: JSON.stringify({
name: "production-agent",
description: "Production Kiro CLI agent for enterprise environment",
prompt:
"You are a helpful software assistant working in a secure enterprise environment",
mcpServers: {},
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
toolAliases: {},
allowedTools: ["fs_read"],
resources: [
"file://KiroQ.md",
"file://README.md",
"file://.kiro/steering/**/*.md",
],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
}),
};
const state = await runTerraformApply(moduleDir, extendedConfigVars);
// All installation steps execute in the correct order
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("kiro-cli");
// auth_tarball is unpacked and used as expected
const authTarballEnv = findResourceInstance(
state,
"coder_env",
"auth_tarball",
);
expect(authTarballEnv).toBeDefined();
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
// agent_config is rendered correctly, and the name field is used as the agent's name
// The specified ai_prompt and system_prompt are respected by the Q agent
// Tools are trusted globally if trust_all_tools = true
// Files and scripts execute in proper sequence
});
// 1. Basic functionality test (replaces testRequiredVariables)
it("works with required variables", async () => {
const state = await runTerraformApply(moduleDir, requiredVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("kiro-cli");
});
// 2. Environment variables are created correctly
it("creates required environment variables", async () => {
const state = await runTerraformApply(moduleDir, fullConfigVars);
// Check status slug environment variable
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("kiro-cli");
// Check auth tarball environment variable
const authTarballEnv = findResourceInstance(
state,
"coder_env",
"auth_tarball",
);
expect(authTarballEnv).toBeDefined();
expect(authTarballEnv.name).toBe("KIRO_CLI_AUTH_TARBALL");
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
});
// 3. Empty auth tarball handling
it("handles empty auth tarball correctly", async () => {
const noAuthVars = {
...requiredVars,
auth_tarball: "",
};
const state = await runTerraformApply(moduleDir, noAuthVars);
// Auth tarball environment variable should not be created when empty
const authTarballEnv = state.resources?.find(
(r) => r.type === "coder_env" && r.name === "auth_tarball",
);
expect(authTarballEnv).toBeUndefined();
});
// 4. Status slug is always created
it("creates status slug environment variable", async () => {
const state = await runTerraformApply(moduleDir, requiredVars);
// Status slug should always be configured
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("kiro-cli");
});
// 5. Install options configuration
it("respects install option flags", async () => {
const noInstallVars = {
...requiredVars,
install_kiro_cli: false,
install_agentapi: false,
};
const state = await runTerraformApply(moduleDir, noInstallVars);
// Status slug should still be configured even when install options are disabled
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.value).toBe("kiro-cli");
});
// 6. Configurable installation URL
it("uses configurable kiro_install_url parameter", async () => {
const customUrlVars = {
...requiredVars,
kiro_install_url: "https://internal-mirror.company.com/kiro-cli",
};
const state = await runTerraformApply(moduleDir, customUrlVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 7. Version configuration
it("uses specified versions", async () => {
const versionVars = {
...requiredVars,
kiro_cli_version: "1.14.1",
agentapi_version: "v0.6.0",
};
const state = await runTerraformApply(moduleDir, versionVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 8. UI configuration options
it("supports UI customization options", async () => {
const uiCustomVars = {
...requiredVars,
order: 5,
group: "Custom AI Tools",
icon: "/icon/custom-kiro-cli-icon.svg",
};
const state = await runTerraformApply(moduleDir, uiCustomVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 9. Pre and post install scripts
it("supports pre and post install scripts", async () => {
const scriptVars = {
...requiredVars,
pre_install_script: "echo 'Pre-install setup'",
post_install_script: "echo 'Post-install cleanup'",
};
const state = await runTerraformApply(moduleDir, scriptVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 10. Valid agent_config JSON with different agent name
it("handles valid agent_config JSON with custom agent name", async () => {
const customAgentConfig = JSON.stringify({
name: "production-agent",
description: "Production Kiro CLI agent",
prompt: "You are a production AI assistant.",
mcpServers: {},
tools: ["fs_read", "fs_write"],
toolAliases: {},
allowedTools: ["fs_read"],
resources: ["file://README.md"],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
});
const validAgentConfigVars = {
...requiredVars,
agent_config: customAgentConfig,
};
const state = await runTerraformApply(moduleDir, validAgentConfigVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 11. Air-gapped installation support
it("supports air-gapped installation with custom URL", async () => {
const airGappedVars = {
...requiredVars,
kiro_install_url: "https://artifacts.internal.corp/kiro-cli-releases",
kiro_cli_version: "1.14.1",
};
const state = await runTerraformApply(moduleDir, airGappedVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 12. Trust all tools configuration
it("handles trust_all_tools configuration", async () => {
const trustVars = {
...requiredVars,
trust_all_tools: true,
};
const state = await runTerraformApply(moduleDir, trustVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 13. AI prompt configuration
it("handles AI prompt configuration", async () => {
const promptVars = {
...requiredVars,
ai_prompt: "Create a comprehensive test suite for the application",
};
const state = await runTerraformApply(moduleDir, promptVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 14. Agent config with minimal structure
it("handles minimal agent config structure", async () => {
const minimalAgentConfig = JSON.stringify({
name: "minimal-agent",
description: "Minimal agent config",
prompt: "You are a minimal AI assistant.",
mcpServers: {},
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
toolAliases: {},
allowedTools: ["fs_read"],
resources: ["file://README.md"],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
});
const minimalVars = {
...requiredVars,
agent_config: minimalAgentConfig,
};
const state = await runTerraformApply(moduleDir, minimalVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 15. JSON encoding validation for system prompts with newlines
it("handles system prompts with newlines correctly", async () => {
const multilinePromptVars = {
...requiredVars,
system_prompt: "Multi-line\nsystem prompt\nwith newlines",
};
const state = await runTerraformApply(moduleDir, multilinePromptVars);
// Should create the basic resources without JSON parsing errors
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.value).toBe("kiro-cli");
});
// 16. Agent name extraction from custom config
it("extracts agent name from custom configuration correctly", async () => {
const customNameConfig = JSON.stringify({
name: "enterprise-production-agent",
description: "Enterprise production agent configuration",
prompt: "You are an enterprise production AI assistant.",
mcpServers: {},
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
toolAliases: {},
allowedTools: ["fs_read", "fs_write", "execute_bash"],
resources: ["file://README.md", "file://.kiro/steering/**/*.md"],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
});
const customNameVars = {
...requiredVars,
agent_config: customNameConfig,
};
const state = await runTerraformApply(moduleDir, customNameVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.value).toBe("kiro-cli");
});
});
+275
View File
@@ -0,0 +1,275 @@
# Improved kiro-cli module main.tf
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.12"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/kiro.svg"
}
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI via AgentAPI"
default = true
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for Kiro CLI"
default = false
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app"
default = "Kiro CLI"
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app"
default = "Kiro CLI"
}
variable "install_agentapi" {
type = bool
description = "Whether to install AgentAPI."
default = true
}
variable "ai_prompt" {
type = string
description = "The initial task prompt to send to Kiro CLI."
default = ""
}
variable "pre_install_script" {
type = string
description = "Optional script to run before installing Kiro CLI."
default = null
}
variable "post_install_script" {
type = string
description = "Optional script to run after installing Kiro CLI."
default = null
}
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.10.0"
}
variable "workdir" {
type = string
description = "The folder to run Kiro CLI in."
}
variable "install_kiro_cli" {
type = bool
description = "Whether to install Kiro CLI."
default = true
}
variable "kiro_cli_version" {
type = string
description = "The version of Kiro CLI to install."
default = "latest"
}
variable "kiro_install_url" {
type = string
description = "Base URL for Kiro CLI installation downloads."
default = "https://desktop-release.q.us-east-1.amazonaws.com"
}
variable "trust_all_tools" {
type = bool
description = "Whether to trust all tools in Kiro CLI."
default = false
}
variable "system_prompt" {
type = string
description = "The system prompt to use for Kiro CLI. This should instruct the agent how to do task reporting."
default = <<-EOT
You are a helpful Coding assistant. Aim to autonomously investigate
and solve issues the user gives you and test your work, whenever possible.
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
but opt for autonomy.
EOT
}
variable "coder_mcp_instructions" {
type = string
description = "Instructions for the Coder MCP server integration. This defines how the agent should report tasks to Coder."
default = <<-EOT
YOU MUST REPORT ALL TASKS TO CODER.
When reporting tasks you MUST follow these EXACT instructions:
- IMMEDIATELY report status after receiving ANY user message
- Be granular If you are investigating with multiple steps report each step to coder.
Task state MUST be one of the following:
- Use "state": "working" when actively processing WITHOUT needing additional user input
- Use "state": "complete" only when finished with a task
- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers.
Task summaries MUST:
- Include specifics about what you're doing
- Include clear and actionable steps for the user
- Be less than 160 characters in length
EOT
}
variable "auth_tarball" {
type = string
description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/kiro-cli directory."
default = ""
sensitive = true
}
variable "agent_config" {
type = string
description = "Optional Agent configuration JSON for Kiro CLI."
default = null
}
variable "agentapi_chat_based_path" {
type = bool
description = "Whether to use chat-based path for AgentAPI.Required if CODER_WILDCARD_ACCESS_URL is not defined in coder deployment"
default = false
}
# Expose status slug to the agent environment
resource "coder_env" "status_slug" {
agent_id = var.agent_id
name = "CODER_MCP_APP_STATUS_SLUG"
value = local.app_slug
count = var.report_tasks ? 1 : 0
}
# Expose auth tarball as environment variable for install script
resource "coder_env" "auth_tarball" {
count = var.auth_tarball != "" ? 1 : 0
agent_id = var.agent_id
name = "KIRO_CLI_AUTH_TARBALL"
value = var.auth_tarball
}
locals {
app_slug = "kiro-cli"
workdir = trimsuffix(var.workdir, "/")
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".kiro"
system_prompt = jsonencode(replace(var.system_prompt, "/[\r\n]/", ""))
coder_mcp_instructions = jsonencode(replace(var.coder_mcp_instructions, "/[\r\n]/", ""))
# Create default agent config structure
default_agent_config = templatefile("${path.module}/templates/agent-config.json.tpl", {
system_prompt = local.system_prompt
})
# Choose the JSON string: use var.agent_config if provided, otherwise encode default
agent_config = var.agent_config != null ? var.agent_config : local.default_agent_config
# Extract agent name from the selected config
agent_name = try(jsondecode(local.agent_config).name, "agent")
full_prompt = var.ai_prompt != null ? var.ai_prompt : ""
server_chat_parameters = var.agentapi_chat_based_path ? "--chat-base-path /@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.app_slug}/chat" : ""
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "2.0.0"
agent_id = var.agent_id
folder = local.workdir
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
ARG_TRUST_ALL_TOOLS='${var.trust_all_tools}' \
ARG_AI_PROMPT='${base64encode(local.full_prompt)}' \
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
ARG_WORKDIR='${var.workdir}' \
ARG_SERVER_PARAMETERS="${local.server_chat_parameters}" \
ARG_REPORT_TASKS='${var.report_tasks}' \
/tmp/start.sh
EOT
install_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_INSTALL='${var.install_kiro_cli}' \
ARG_VERSION='${var.kiro_cli_version}' \
ARG_KIRO_INSTALL_URL='${var.kiro_install_url}' \
ARG_AUTH_TARBALL='${var.auth_tarball}' \
ARG_AGENT_CONFIG='${local.agent_config != null ? base64encode(local.agent_config) : ""}' \
ARG_AGENT_NAME='${local.agent_name}' \
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_CODER_MCP_INSTRUCTIONS='${base64encode(local.coder_mcp_instructions)}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
/tmp/install.sh
EOT
}
output "task_app_id" {
value = module.agentapi.task_app_id
}
@@ -0,0 +1,159 @@
#!/bin/bash
# Install script for kiro-cli module
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" > /dev/null 2>&1
}
# Inputs
ARG_INSTALL=${ARG_INSTALL:-true}
ARG_VERSION=${ARG_VERSION:-latest}
ARG_KIRO_INSTALL_URL=${ARG_KIRO_INSTALL_URL:-https://desktop-release.q.us-east-1.amazonaws.com}
ARG_AUTH_TARBALL=${ARG_AUTH_TARBALL:-}
ARG_AGENT_CONFIG=${ARG_AGENT_CONFIG:-}
ARG_AGENT_NAME=${ARG_AGENT_NAME:-default-agent}
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.kiro}
ARG_CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG:-}
ARG_CODER_MCP_INSTRUCTIONS=${ARG_CODER_MCP_INSTRUCTIONS:-}
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
# Decode base64 inputs
ARG_AGENT_CONFIG_DECODED=""
if [ -n "$ARG_AGENT_CONFIG" ]; then
ARG_AGENT_CONFIG_DECODED=$(echo -n "$ARG_AGENT_CONFIG" | base64 -d)
fi
ARG_CODER_MCP_INSTRUCTIONS_DECODED=""
if [ -n "$ARG_CODER_MCP_INSTRUCTIONS" ]; then
ARG_CODER_MCP_INSTRUCTIONS_DECODED=$(echo -n "$ARG_CODER_MCP_INSTRUCTIONS" | base64 -d)
fi
echo "--------------------------------"
echo "install: $ARG_INSTALL"
echo "version: $ARG_VERSION"
echo "kiro_install_url: $ARG_KIRO_INSTALL_URL"
echo "agent_name: $ARG_AGENT_NAME"
echo "coder_mcp_app_status_slug: $ARG_CODER_MCP_APP_STATUS_SLUG"
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
echo "auth_tarball_provided: ${ARG_AUTH_TARBALL}"
echo "report_tasks: ${ARG_REPORT_TASKS}"
echo "--------------------------------"
# Install Kiro CLI if requested
function install_kiro_cli() {
if [ "$ARG_INSTALL" = "true" ]; then
echo "Installing Kiro CLI..."
PREV_DIR="$PWD"
TMP_DIR="$(mktemp -d)"
cd "$TMP_DIR"
ARCH="$(uname -m)"
case "$ARCH" in
"x86_64")
KIRO_URL="${ARG_KIRO_INSTALL_URL}/${ARG_VERSION}/kirocli-x86_64-linux.zip"
;;
"aarch64" | "arm64")
KIRO_URL="${ARG_KIRO_INSTALL_URL}/${ARG_VERSION}/kirocli-aarch64-linux.zip"
;;
*)
echo "Error: Unsupported architecture: $ARCH. Kiro CLI only supports x86_64 and arm64."
exit 1
;;
esac
echo "Downloading Kiro CLI for $ARCH from $KIRO_URL..."
curl --proto '=https' --tlsv1.2 -sSf "$KIRO_URL" -o "kirocli.zip"
unzip kirocli.zip
./kirocli/install.sh --no-confirm
cd "$PREV_DIR"
rm -rf "$TMP_DIR"
# Ensure binaries are discoverable; create stable symlink to kiro-cli
CANDIDATES=(
"$(command -v kiro-cli || true)"
"$HOME/.local/bin/kiro-cli"
)
FOUND_BIN=""
for c in "${CANDIDATES[@]}"; do
if [ -n "$c" ] && [ -x "$c" ]; then
FOUND_BIN="$c"
break
fi
done
export PATH="$PATH:$HOME/.local/bin"
echo "Installed Kiro CLI at: $(command -v kiro-cli || true) (resolved: $FOUND_BIN)"
fi
}
# Extract authentication tarball
function extract_auth_tarball() {
if [ -n "$ARG_AUTH_TARBALL" ]; then
echo "Extracting auth tarball..."
if ! command_exists zstd; then
echo "Error: zstd is required to extract the authentication tarball but is not installed."
echo "Please install zstd using the pre_install_script parameter."
exit 1
fi
PREV_DIR="$PWD"
echo "$ARG_AUTH_TARBALL" | base64 -d > /tmp/auth.tar.zst
rm -rf ~/.local/share/kiro-cli
mkdir -p ~/.local/share/kiro-cli
cd ~/.local/share/kiro-cli
tar -I zstd -xf /tmp/auth.tar.zst
rm /tmp/auth.tar.zst
cd "$PREV_DIR"
echo "Extracted auth tarball to ~/.local/share/kiro-cli"
else
echo "Warning: No auth tarball provided. Kiro CLI may require manual authentication."
fi
}
# Configure MCP integration and create agent
function configure_agent() {
# Create Kiro CLI agent configuration directory
AGENT_CONFIG_DIR="$HOME/.kiro/agents"
mkdir -p "$AGENT_CONFIG_DIR"
ALLOWED_TOOLS="coder_get_workspace\,coder_create_workspace\,coder_list_workspaces\,coder_list_templates\,coder_template_version_parameters\,coder_get_authenticated_user\,coder_create_workspace_build\,coder_create_template_version\,coder_get_workspace_agent_logs\,coder_get_workspace_build_logs\,coder_get_template_version_logs\,coder_update_template_active_version\,coder_upload_tar_file\,coder_create_template\,coder_delete_template\,coder_workspace_bash"
if [ -n "$ARG_AGENT_CONFIG_DECODED" ]; then
echo "Applying custom MCP configuration..."
# Use agent name as filename for the configuration
echo "$ARG_AGENT_CONFIG_DECODED" > "$AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json"
echo "Custom configuration saved to $AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json"
fi
if [ "$ARG_REPORT_TASKS" = "true" ]; then
echo "Configuring Kiro CLI to report tasks via Coder MCP..."
kiro-cli mcp add --name coder \
--command "coder" \
--agent "$ARG_AGENT_NAME" \
--args "exp,mcp,server,--allowed-tools,coder_report_task,--instructions,'$ARG_CODER_MCP_INSTRUCTIONS_DECODED'" \
--env "CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG}" \
--env "CODER_MCP_AI_AGENTAPI_URL=http://localhost:3284" \
--env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
--env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
--force || echo "Warning: Failed to add Coder MCP server"
else
kiro-cli mcp add --name coder \
--command "coder" \
--agent "$ARG_AGENT_NAME" \
--args "exp,mcp,server,--allowed-tools,coder_report_task" \
--env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
--env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
--force || echo "Warning: Failed to add Coder MCP server"
fi
echo "Added Coder MCP server into $ARG_AGENT_NAME in Kiro CLI configuration"
kiro-cli settings chat.defaultAgent "$ARG_AGENT_NAME"
}
# Main execution
install_kiro_cli
extract_auth_tarball
configure_agent
echo "Kiro CLI installation and configuration complete!"
@@ -0,0 +1,67 @@
#!/bin/bash
# Start script for kiro-cli module
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" > /dev/null 2>&1
}
# Decode inputs
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d)
ARG_TRUST_ALL_TOOLS=${ARG_TRUST_ALL_TOOLS:-true}
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.kiro}
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
ARG_SERVER_PARAMETERS=${ARG_SERVER_PARAMETERS:-""}
echo "--------------------------------"
echo "ai_prompt: $ARG_AI_PROMPT"
echo "trust_all_tools: $ARG_TRUST_ALL_TOOLS"
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
echo "workdir: $ARG_WORKDIR"
echo "report_tasks: ${ARG_REPORT_TASKS}"
echo "--------------------------------"
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
# Find Kiro CLI
if command_exists kiro-cli; then
KIRO_CMD=kiro-cli
elif [ -x "$HOME/.local/bin/kiro-cli" ]; then
KIRO_CMD="$HOME/.local/bin/kiro-cli"
else
echo "Error: Kiro CLI not found. Install it or set install_kiro_cli=true."
exit 1
fi
mkdir -p "$ARG_WORKDIR"
cd "$ARG_WORKDIR"
# Set up environment
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
# Build command arguments
ARGS=(chat)
if [ "$ARG_TRUST_ALL_TOOLS" = "true" ]; then
ARGS+=(--trust-all-tools)
fi
# Log and run with agentapi integration
printf "Running: %q %s\n" "$KIRO_CMD" "$(printf '%q ' "${ARGS[@]}")"
# If we have an AI prompt, we need to handle it specially
if [ -n "$ARG_AI_PROMPT" ]; then
if [ "$ARG_REPORT_TASKS" == "true" ]; then
PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AI_PROMPT"
else
PROMPT="$ARG_AI_PROMPT"
fi
ARGS+=("$PROMPT")
fi
# Use agentapi to manage the interactive session with initial prompt
agentapi server ${ARG_SERVER_PARAMETERS} --term-width 67 --term-height 1190 -- "$KIRO_CMD" "${ARGS[@]}"
@@ -0,0 +1,27 @@
{
"name": "agent",
"description": "This is an default agent config",
"prompt": ${system_prompt},
"mcpServers": {},
"tools": [
"read",
"write",
"shell",
"aws",
"@coder",
"knowledge"
],
"toolAliases": {},
"allowedTools": [
"read",
"@coder"
],
"resources": [
"file://KiroQ.md",
"file://README.md",
"file://.kiro/steering/**/*.md"
],
"hooks": {},
"toolsSettings": {},
"useLegacyMcpJson": true
}
@@ -13,7 +13,7 @@ Automatically detect and start development servers for various project types whe
```tf
module "auto_start_dev_servers" {
source = "registry.coder.com/mavrickrishi/auto-start-dev-server/coder"
version = "1.0.1"
version = "1.0.2"
agent_id = coder_agent.main.id
}
```
@@ -51,7 +51,7 @@ module "auto_start_dev_servers" {
```tf
module "auto_start" {
source = "./modules/auto-start-dev-server"
version = "1.0.1"
version = "1.0.2"
agent_id = coder_agent.main.id
}
```
@@ -61,7 +61,7 @@ module "auto_start" {
```tf
module "auto_start_dev_servers" {
source = "./modules/auto-start-dev-server"
version = "1.0.1"
version = "1.0.2"
agent_id = coder_agent.main.id
# Optional: Configure which project types to detect
@@ -100,7 +100,7 @@ module "auto_start_dev_servers" {
```tf
module "auto_start" {
source = "./modules/auto-start-dev-server"
version = "1.0.1"
version = "1.0.2"
agent_id = coder_agent.main.id
# Disable automatic preview app creation
@@ -113,7 +113,7 @@ module "auto_start" {
```tf
module "auto_start" {
source = "./modules/auto-start-dev-server"
version = "1.0.1"
version = "1.0.2"
agent_id = coder_agent.main.id
# Only enable web development projects
@@ -136,7 +136,7 @@ module "auto_start" {
```tf
module "auto_start" {
source = "./modules/auto-start-dev-server"
version = "1.0.1"
version = "1.0.2"
agent_id = coder_agent.main.id
workspace_directory = "/workspaces"

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