diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 87ed60be..133537e3 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,5 +1,3 @@
-Closes #
-
## Description
@@ -7,6 +5,7 @@ Closes #
## Type of Change
- [ ] New module
+- [ ] New template
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
@@ -20,10 +19,16 @@ Closes #
**New version:** `v1.0.0`
**Breaking change:** [ ] Yes [ ] No
+## Template Information
+
+
+
+**Path:** `registry/[namespace]/templates/[template-name]`
+
## Testing & Validation
- [ ] Tests pass (`bun test`)
-- [ ] Code formatted (`bun run fmt`)
+- [ ] Code formatted (`bun fmt`)
- [ ] Changes tested locally
## Related Issues
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 7e14213c..9c676ec1 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -28,6 +28,8 @@ jobs:
run: bun install
- name: Run TypeScript tests
run: bun test
+ - name: Run Terraform tests
+ run: ./scripts/terraform_test_all.sh
- name: Run Terraform Validate
run: bun terraform-validate
validate-style:
@@ -48,7 +50,7 @@ jobs:
- name: Validate formatting
run: bun fmt:ci
- name: Check for typos
- uses: crate-ci/typos@v1.37.2
+ uses: crate-ci/typos@v1.38.1
with:
config: .github/typos.toml
validate-readme-files:
diff --git a/.icons/auto-dev-server.svg b/.icons/auto-dev-server.svg
new file mode 100644
index 00000000..f043b56d
--- /dev/null
+++ b/.icons/auto-dev-server.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/.icons/nexus-repository.svg b/.icons/nexus-repository.svg
new file mode 100644
index 00000000..ca135cd5
--- /dev/null
+++ b/.icons/nexus-repository.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 97a33d9f..4f46eb67 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -124,18 +124,23 @@ This script generates:
- Accurate description and usage examples
- Correct icon path (usually `../../../../.icons/your-icon.svg`)
- Proper tags that describe your module
-3. **Create at least one `.tftest.hcl`** to test your module with `terraform test`
+3. **Create tests for your module:**
+ - **Terraform tests**: Create a `*.tftest.hcl` file and test with `terraform test`
+ - **TypeScript tests**: Create `main.test.ts` file if your module runs scripts or has business logic that Terraform tests can't cover
4. **Add any scripts** or additional files your module needs
### 4. Test and Submit
```bash
-# Test your module (from the module directory)
+# Test your module
+cd registry/[namespace]/modules/[module-name]
+
+# Required: Test Terraform functionality
terraform init -upgrade
terraform test -verbose
-# Or run all tests in the repo
-./scripts/terraform_test_all.sh
+# Optional: Test TypeScript files if you have main.test.ts
+bun test main.test.ts
# Format code
bun run fmt
@@ -343,8 +348,8 @@ coder templates push test-[template-name] -d .
terraform init -upgrade
terraform test -verbose
-# Test all modules
-./scripts/terraform_test_all.sh
+# Optional: If you have TypeScript tests
+bun test main.test.ts
```
### 3. Maintain Backward Compatibility
@@ -393,7 +398,9 @@ Example: `https://github.com/coder/registry/compare/main...your-branch?template=
### Every Module Must Have
- `main.tf` - Terraform code
-- One or more `.tftest.hcl` files - Working tests with `terraform test`
+- **Tests**:
+ - `*.tftest.hcl` files with `terraform test` (to test terraform specific logic)
+ - `main.test.ts` file with `bun test` (to test business logic, i.e., `coder_script` to install a package.)
- `README.md` - Documentation with frontmatter
### Every Template Must Have
@@ -493,6 +500,10 @@ When reporting bugs, include:
2. **No tests** or broken tests
3. **Hardcoded values** instead of variables
4. **Breaking changes** without defaults
-5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`) before submitting
+5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`, and `bun test main.test.ts` if applicable) before submitting
+
+## For Maintainers
+
+Guidelines for reviewing PRs, managing releases, and maintaining the registry. [See the maintainer guide for detailed information.](./MAINTAINER.md)
Happy contributing! π
diff --git a/MAINTAINER.md b/MAINTAINER.md
index 7d80d2fa..d37ea26d 100644
--- a/MAINTAINER.md
+++ b/MAINTAINER.md
@@ -23,6 +23,7 @@ Check that PRs have:
- [ ] Working tests (`terraform test`)
- [ ] Formatted code (`bun run fmt`)
- [ ] Avatar image for new namespaces (`avatar.png` or `avatar.svg` in `.images/`)
+- [ ] Version label: `version:patch`, `version:minor`, or `version:major`
### Version Guidelines
@@ -32,7 +33,8 @@ When reviewing PRs, ensure the version change follows semantic versioning:
- **Minor** (1.2.3 β 1.3.0): New features, adding inputs
- **Major** (1.2.3 β 2.0.0): Breaking changes (removing inputs, changing types)
-PRs should clearly indicate the version change (e.g., `v1.2.3 β v1.2.4`).
+PRs should clearly indicate the intended version change (e.g., `v1.2.3 β v1.2.4`) and include the appropriate label: `version:patch`, `version:minor`, or `version:major`.
+The βVersion Bumpβ CI uses this label to validate required updates (README version refs, etc.).
### Validate READMEs
diff --git a/README.md b/README.md
index 23746bd6..97f4677e 100644
--- a/README.md
+++ b/README.md
@@ -48,3 +48,7 @@ Simply include that snippet inside your Coder template, defining any data depend
## Contributing
We are always accepting new contributions. [Please see our contributing guide for more information.](./CONTRIBUTING.md)
+
+## For Maintainers
+
+Guidelines for maintainers reviewing PRs and managing releases. [See the maintainer guide for more information.](./MAINTAINER.md)
diff --git a/examples/modules/MODULE_NAME.tftest.hcl b/examples/modules/MODULE_NAME.tftest.hcl
index 6f11666b..a6ccc524 100644
--- a/examples/modules/MODULE_NAME.tftest.hcl
+++ b/examples/modules/MODULE_NAME.tftest.hcl
@@ -15,7 +15,7 @@ run "app_url_uses_port" {
}
assert {
- condition = resource.coder_app.MODULE_NAME.url == "http://localhost:19999"
- error_message = "Expected MODULE_NAME app URL to include configured port"
+ condition = resource.coder_app.module_name.url == "http://localhost:19999"
+ error_message = "Expected module-name app URL to include configured port"
}
}
diff --git a/examples/modules/main.tf b/examples/modules/main.tf
index 628eb1da..c0acfbc3 100644
--- a/examples/modules/main.tf
+++ b/examples/modules/main.tf
@@ -35,13 +35,13 @@ variable "agent_id" {
variable "log_path" {
type = string
- description = "The path to log MODULE_NAME to."
- default = "/tmp/MODULE_NAME.log"
+ description = "The path to the module log file."
+ default = "/tmp/module_name.log"
}
variable "port" {
type = number
- description = "The port to run MODULE_NAME on."
+ description = "The port to run the application on."
default = 19999
}
@@ -59,9 +59,9 @@ variable "order" {
# Add other variables here
-resource "coder_script" "MODULE_NAME" {
+resource "coder_script" "module_name" {
agent_id = var.agent_id
- display_name = "MODULE_NAME"
+ display_name = "Module Name"
icon = local.icon_url
script = templatefile("${path.module}/run.sh", {
LOG_PATH : var.log_path,
@@ -70,10 +70,10 @@ resource "coder_script" "MODULE_NAME" {
run_on_stop = false
}
-resource "coder_app" "MODULE_NAME" {
+resource "coder_app" "module_name" {
agent_id = var.agent_id
- slug = "MODULE_NAME"
- display_name = "MODULE_NAME"
+ slug = "module-name"
+ display_name = "Module Name"
url = "http://localhost:${var.port}"
icon = local.icon_url
subdomain = false
@@ -88,10 +88,10 @@ resource "coder_app" "MODULE_NAME" {
}
}
-data "coder_parameter" "MODULE_NAME" {
- type = "list(string)"
- name = "MODULE_NAME"
- display_name = "MODULE_NAME"
+data "coder_parameter" "module_name" {
+ type = "string"
+ name = "module_name"
+ display_name = "Module Name"
icon = local.icon_url
mutable = var.mutable
default = local.options["Option 1"]["value"]
diff --git a/registry/coder-labs/modules/auggie/README.md b/registry/coder-labs/modules/auggie/README.md
index 4b8e9315..6cb7102d 100644
--- a/registry/coder-labs/modules/auggie/README.md
+++ b/registry/coder-labs/modules/auggie/README.md
@@ -13,7 +13,7 @@ Run Auggie CLI in your workspace to access Augment's AI coding assistant with ad
```tf
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
- version = "0.1.0"
+ version = "0.2.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -47,7 +47,7 @@ module "coder-login" {
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
- version = "0.1.0"
+ version = "0.2.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
@@ -103,7 +103,7 @@ EOF
```tf
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
- version = "0.1.0"
+ version = "0.2.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
diff --git a/registry/coder-labs/modules/auggie/main.tf b/registry/coder-labs/modules/auggie/main.tf
index d6c326e0..5e268ab9 100644
--- a/registry/coder-labs/modules/auggie/main.tf
+++ b/registry/coder-labs/modules/auggie/main.tf
@@ -66,7 +66,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
- default = "v0.6.0"
+ default = "v0.10.0"
validation {
condition = can(regex("^v[0-9]+\\.[0-9]+\\.[0-9]+", var.agentapi_version))
error_message = "agentapi_version must be a valid semantic version starting with 'v', like 'v0.3.3'."
@@ -178,7 +178,7 @@ locals {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md
index 62c63a32..549721ec 100644
--- a/registry/coder-labs/modules/codex/README.md
+++ b/registry/coder-labs/modules/codex/README.md
@@ -13,7 +13,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
- version = "2.0.0"
+ version = "2.1.1"
agent_id = coder_agent.example.id
openai_api_key = var.openai_api_key
folder = "/home/coder/project"
@@ -33,7 +33,7 @@ module "codex" {
module "codex" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/codex/coder"
- version = "2.0.0"
+ version = "2.1.1"
agent_id = coder_agent.example.id
openai_api_key = "..."
folder = "/home/coder/project"
@@ -60,7 +60,7 @@ module "coder-login" {
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
- version = "2.0.0"
+ version = "2.1.1"
agent_id = coder_agent.example.id
openai_api_key = "..."
ai_prompt = data.coder_parameter.ai_prompt.value
@@ -106,7 +106,7 @@ For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_se
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
- version = "2.0.0"
+ version = "2.1.1"
# ... other variables ...
# Override default configuration
diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf
index 9fdec401..460d5af9 100644
--- a/registry/coder-labs/modules/codex/main.tf
+++ b/registry/coder-labs/modules/codex/main.tf
@@ -80,7 +80,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
- default = "v0.5.0"
+ default = "v0.10.0"
}
variable "codex_model" {
@@ -128,9 +128,10 @@ locals {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = var.agent_id
+ folder = var.folder
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
diff --git a/registry/coder-labs/modules/copilot/README.md b/registry/coder-labs/modules/copilot/README.md
new file mode 100644
index 00000000..83f59c7c
--- /dev/null
+++ b/registry/coder-labs/modules/copilot/README.md
@@ -0,0 +1,210 @@
+---
+display_name: Copilot CLI
+description: GitHub Copilot CLI agent for AI-powered terminal assistance
+icon: ../../../../.icons/github.svg
+verified: false
+tags: [agent, copilot, ai, github, tasks]
+---
+
+# Copilot
+
+Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-copilot-cli) in your workspace for AI-powered coding assistance directly from the terminal. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for task reporting in the Coder UI.
+
+```tf
+module "copilot" {
+ source = "registry.coder.com/coder-labs/copilot/coder"
+ version = "0.2.1"
+ agent_id = coder_agent.example.id
+ workdir = "/home/coder/projects"
+}
+```
+
+> [!IMPORTANT]
+> This example assumes you have [Coder external authentication](https://coder.com/docs/admin/external-auth) configured with `id = "github"`. If not, you can provide a direct token using the `github_token` variable or provide the correct external authentication id for GitHub by setting `external_auth_id = "my-github"`.
+
+> [!NOTE]
+> By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details.
+
+## Prerequisites
+
+- **Node.js v22+** and **npm v10+**
+- **[Active Copilot subscription](https://docs.github.com/en/copilot/about-github-copilot/subscription-plans-for-github-copilot)** (GitHub Copilot Pro, Pro+, Business, or Enterprise)
+- **GitHub authentication** via one of:
+ - [Coder external authentication](https://coder.com/docs/admin/external-auth) (recommended)
+ - Direct token via `github_token` variable
+ - Interactive login in Copilot
+
+## Examples
+
+### Usage with Tasks
+
+For development environments where you want Copilot to have full access to tools and automatically resume sessions:
+
+```tf
+data "coder_parameter" "ai_prompt" {
+ type = "string"
+ name = "AI Prompt"
+ default = ""
+ description = "Initial task prompt for Copilot."
+ mutable = true
+}
+
+module "copilot" {
+ source = "registry.coder.com/coder-labs/copilot/coder"
+ version = "0.2.1"
+ agent_id = coder_agent.example.id
+ workdir = "/home/coder/projects"
+
+ ai_prompt = data.coder_parameter.ai_prompt.value
+ copilot_model = "claude-sonnet-4.5"
+ allow_all_tools = true
+ resume_session = true
+
+ trusted_directories = ["/home/coder/projects", "/tmp"]
+}
+```
+
+### Advanced Configuration
+
+Customize tool permissions, MCP servers, and Copilot settings:
+
+```tf
+module "copilot" {
+ source = "registry.coder.com/coder-labs/copilot/coder"
+ version = "0.2.1"
+ agent_id = coder_agent.example.id
+ workdir = "/home/coder/projects"
+
+ # Version pinning (defaults to "0.0.334", use "latest" for newest version)
+ copilot_version = "latest"
+
+ # Tool permissions
+ allow_tools = ["shell(git)", "shell(npm)", "write"]
+ trusted_directories = ["/home/coder/projects", "/tmp"]
+
+ # Custom Copilot configuration
+ copilot_config = jsonencode({
+ banner = "never"
+ theme = "dark"
+ })
+
+ # MCP server configuration
+ mcp_config = jsonencode({
+ mcpServers = {
+ filesystem = {
+ command = "npx"
+ args = ["-y", "@modelcontextprotocol/server-filesystem", "/home/coder/projects"]
+ description = "Provides file system access to the workspace"
+ name = "Filesystem"
+ timeout = 3000
+ type = "local"
+ tools = ["*"]
+ trust = true
+ }
+ playwright = {
+ command = "npx"
+ args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated"]
+ description = "Browser automation for testing and previewing changes"
+ name = "Playwright"
+ timeout = 5000
+ type = "local"
+ tools = ["*"]
+ trust = false
+ }
+ }
+ })
+
+ # Pre-install Node.js if needed
+ pre_install_script = <<-EOT
+ #!/bin/bash
+ curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
+ sudo apt-get install -y nodejs
+ EOT
+}
+```
+
+> [!NOTE]
+> GitHub Copilot CLI does not automatically install MCP servers. You have two options:
+>
+> - Use `npx -y` in the MCP config (shown above) to auto-install on each run
+> - Pre-install MCP servers in `pre_install_script` for faster startup (e.g., `npm install -g @modelcontextprotocol/server-filesystem`)
+
+### Direct Token Authentication
+
+Use this example when you want to provide a GitHub Personal Access Token instead of using Coder external auth:
+
+```tf
+variable "github_token" {
+ type = string
+ description = "GitHub Personal Access Token"
+ sensitive = true
+}
+
+module "copilot" {
+ source = "registry.coder.com/coder-labs/copilot/coder"
+ version = "0.2.1"
+ agent_id = coder_agent.example.id
+ workdir = "/home/coder/projects"
+ github_token = var.github_token
+}
+```
+
+### Standalone Mode
+
+Run Copilot as a command-line tool without task reporting or web interface. This installs and configures Copilot, making it available as a CLI app in the Coder agent bar that you can launch to interact with Copilot directly from your terminal. Set `report_tasks = false` to disable integration with Coder Tasks.
+
+```tf
+module "copilot" {
+ source = "registry.coder.com/coder-labs/copilot/coder"
+ version = "0.2.1"
+ agent_id = coder_agent.example.id
+ workdir = "/home/coder"
+ report_tasks = false
+ cli_app = true
+}
+```
+
+## Authentication
+
+The module supports multiple authentication methods (in priority order):
+
+1. **[Coder External Auth](https://coder.com/docs/admin/external-auth) (Recommended)** - Automatic if GitHub external auth is configured in Coder
+2. **Direct Token** - Pass `github_token` variable (OAuth or Personal Access Token)
+3. **Interactive** - Copilot prompts for login via `/login` command if no auth found
+
+> [!NOTE]
+> OAuth tokens work best with Copilot. Personal Access Tokens may have limited functionality.
+
+## Session Resumption
+
+By default, the module resumes the latest Copilot session when the workspace restarts. Set `resume_session = false` to always start fresh sessions.
+
+> [!NOTE]
+> Session resumption requires persistent storage for the home directory or workspace volume. Without persistent storage, sessions will not resume across workspace restarts.
+
+## Troubleshooting
+
+If you encounter any issues, check the log files in the `~/.copilot-module` directory within your workspace for detailed information.
+
+```bash
+# Installation logs
+cat ~/.copilot-module/install.log
+
+# Startup logs
+cat ~/.copilot-module/agentapi-start.log
+
+# Pre/post install script logs
+cat ~/.copilot-module/pre_install.log
+cat ~/.copilot-module/post_install.log
+```
+
+> [!NOTE]
+> To use tasks with Copilot, you must have an active GitHub Copilot subscription.
+> The `workdir` variable is required and specifies the directory where Copilot will run.
+
+## References
+
+- [GitHub Copilot CLI Documentation](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)
+- [Installing GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)
+- [AgentAPI Documentation](https://github.com/coder/agentapi)
+- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
diff --git a/registry/coder-labs/modules/copilot/copilot.tftest.hcl b/registry/coder-labs/modules/copilot/copilot.tftest.hcl
new file mode 100644
index 00000000..185c019b
--- /dev/null
+++ b/registry/coder-labs/modules/copilot/copilot.tftest.hcl
@@ -0,0 +1,236 @@
+run "defaults_are_correct" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ }
+
+ assert {
+ condition = var.copilot_model == "claude-sonnet-4.5"
+ error_message = "Default model should be 'claude-sonnet-4.5'"
+ }
+
+ assert {
+ condition = var.report_tasks == true
+ error_message = "Task reporting should be enabled by default"
+ }
+
+ assert {
+ condition = var.resume_session == true
+ error_message = "Session resumption should be enabled by default"
+ }
+
+ assert {
+ condition = var.allow_all_tools == false
+ error_message = "allow_all_tools should be disabled by default"
+ }
+
+ assert {
+ condition = resource.coder_env.mcp_app_status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
+ error_message = "Status slug env var should be created"
+ }
+
+ assert {
+ condition = resource.coder_env.mcp_app_status_slug.value == "copilot"
+ error_message = "Status slug value should be 'copilot'"
+ }
+}
+
+run "github_token_creates_env_var" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ github_token = "test_github_token_abc123"
+ }
+
+ assert {
+ condition = length(resource.coder_env.github_token) == 1
+ error_message = "github_token env var should be created when token is provided"
+ }
+
+ assert {
+ condition = resource.coder_env.github_token[0].name == "GITHUB_TOKEN"
+ error_message = "github_token env var name should be 'GITHUB_TOKEN'"
+ }
+
+ assert {
+ condition = resource.coder_env.github_token[0].value == "test_github_token_abc123"
+ error_message = "github_token env var value should match input"
+ }
+}
+
+run "github_token_not_created_when_empty" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ github_token = ""
+ }
+
+ assert {
+ condition = length(resource.coder_env.github_token) == 0
+ error_message = "github_token env var should not be created when empty"
+ }
+}
+
+run "copilot_model_env_var_for_non_default" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ copilot_model = "claude-sonnet-4"
+ }
+
+ assert {
+ condition = length(resource.coder_env.copilot_model) == 1
+ error_message = "copilot_model env var should be created for non-default model"
+ }
+
+ assert {
+ condition = resource.coder_env.copilot_model[0].name == "COPILOT_MODEL"
+ error_message = "copilot_model env var name should be 'COPILOT_MODEL'"
+ }
+
+ assert {
+ condition = resource.coder_env.copilot_model[0].value == "claude-sonnet-4"
+ error_message = "copilot_model env var value should match input"
+ }
+}
+
+run "copilot_model_not_created_for_default" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ copilot_model = "claude-sonnet-4.5"
+ }
+
+ assert {
+ condition = length(resource.coder_env.copilot_model) == 0
+ error_message = "copilot_model env var should not be created for default model"
+ }
+}
+
+run "model_validation_accepts_valid_models" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ copilot_model = "gpt-5"
+ }
+
+ assert {
+ condition = contains(["claude-sonnet-4", "claude-sonnet-4.5", "gpt-5"], var.copilot_model)
+ error_message = "Model should be one of the valid options"
+ }
+}
+
+run "copilot_config_merges_with_trusted_directories" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ trusted_directories = ["/workspace", "/data"]
+ }
+
+ assert {
+ condition = length(local.final_copilot_config) > 0
+ error_message = "final_copilot_config should be computed"
+ }
+
+ # Verify workdir is trimmed of trailing slash
+ assert {
+ condition = local.workdir == "/home/coder/project"
+ error_message = "workdir should be trimmed of trailing slash"
+ }
+}
+
+run "custom_copilot_config_overrides_default" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ copilot_config = jsonencode({
+ banner = "always"
+ theme = "dark"
+ })
+ }
+
+ assert {
+ condition = var.copilot_config != ""
+ error_message = "Custom copilot config should be set"
+ }
+
+ assert {
+ condition = jsondecode(local.final_copilot_config).banner == "always"
+ error_message = "Custom banner setting should be applied"
+ }
+
+ assert {
+ condition = jsondecode(local.final_copilot_config).theme == "dark"
+ error_message = "Custom theme setting should be applied"
+ }
+}
+
+run "trusted_directories_merged_with_custom_config" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ copilot_config = jsonencode({
+ banner = "always"
+ theme = "dark"
+ trusted_folders = ["/custom"]
+ })
+ trusted_directories = ["/workspace", "/data"]
+ }
+
+ assert {
+ condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/custom")
+ error_message = "Custom trusted folder should be included"
+ }
+
+ assert {
+ condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/home/coder/project")
+ error_message = "Workdir should be included in trusted folders"
+ }
+
+ assert {
+ condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/workspace")
+ error_message = "trusted_directories should be merged into config"
+ }
+
+ assert {
+ condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/data")
+ error_message = "All trusted_directories should be merged into config"
+ }
+}
+
+run "app_slug_is_consistent" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ }
+
+ assert {
+ condition = local.app_slug == "copilot"
+ error_message = "app_slug should be 'copilot'"
+ }
+
+ assert {
+ condition = local.module_dir_name == ".copilot-module"
+ error_message = "module_dir_name should be '.copilot-module'"
+ }
+}
diff --git a/registry/coder-labs/modules/copilot/main.test.ts b/registry/coder-labs/modules/copilot/main.test.ts
new file mode 100644
index 00000000..1d438e33
--- /dev/null
+++ b/registry/coder-labs/modules/copilot/main.test.ts
@@ -0,0 +1,136 @@
+import { describe, expect, it } from "bun:test";
+import {
+ findResourceInstance,
+ runTerraformApply,
+ runTerraformInit,
+ testRequiredVariables,
+} from "~test";
+
+describe("copilot", async () => {
+ await runTerraformInit(import.meta.dir);
+
+ testRequiredVariables(import.meta.dir, {
+ agent_id: "test-agent",
+ workdir: "/home/coder",
+ });
+
+ it("creates mcp_app_status_slug env var", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ workdir: "/home/coder",
+ });
+
+ const statusSlugEnv = findResourceInstance(
+ state,
+ "coder_env",
+ "mcp_app_status_slug",
+ );
+ expect(statusSlugEnv).toBeDefined();
+ expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
+ expect(statusSlugEnv.value).toBe("copilot");
+ });
+
+ it("creates github_token env var with correct value", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ workdir: "/home/coder",
+ github_token: "test_token_12345",
+ });
+
+ const githubTokenEnv = findResourceInstance(
+ state,
+ "coder_env",
+ "github_token",
+ );
+ expect(githubTokenEnv).toBeDefined();
+ expect(githubTokenEnv.name).toBe("GITHUB_TOKEN");
+ expect(githubTokenEnv.value).toBe("test_token_12345");
+ });
+
+ it("does not create github_token env var when empty", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ workdir: "/home/coder",
+ github_token: "",
+ });
+
+ const githubTokenEnvs = state.resources.filter(
+ (r) => r.type === "coder_env" && r.name === "github_token",
+ );
+ expect(githubTokenEnvs.length).toBe(0);
+ });
+
+ it("creates copilot_model env var for non-default models", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ workdir: "/home/coder",
+ copilot_model: "claude-sonnet-4",
+ });
+
+ const modelEnv = findResourceInstance(state, "coder_env", "copilot_model");
+ expect(modelEnv).toBeDefined();
+ expect(modelEnv.name).toBe("COPILOT_MODEL");
+ expect(modelEnv.value).toBe("claude-sonnet-4");
+ });
+
+ it("does not create copilot_model env var for default model", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ workdir: "/home/coder",
+ copilot_model: "claude-sonnet-4.5",
+ });
+
+ const modelEnvs = state.resources.filter(
+ (r) => r.type === "coder_env" && r.name === "copilot_model",
+ );
+ expect(modelEnvs.length).toBe(0);
+ });
+
+ it("creates coder_script resources via agentapi module", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ workdir: "/home/coder",
+ });
+
+ // The agentapi module should create coder_script resources for install and start
+ const scripts = state.resources.filter((r) => r.type === "coder_script");
+ expect(scripts.length).toBeGreaterThan(0);
+ });
+
+ it("validates copilot_model accepts valid values", async () => {
+ // Test valid models don't throw errors
+ await expect(
+ runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ workdir: "/home/coder",
+ copilot_model: "gpt-5",
+ }),
+ ).resolves.toBeDefined();
+
+ await expect(
+ runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ workdir: "/home/coder",
+ copilot_model: "claude-sonnet-4.5",
+ }),
+ ).resolves.toBeDefined();
+ });
+
+ it("merges trusted_directories with custom copilot_config", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ workdir: "/home/coder/project",
+ trusted_directories: JSON.stringify(["/workspace", "/data"]),
+ copilot_config: JSON.stringify({
+ banner: "always",
+ theme: "dark",
+ trusted_folders: ["/custom"],
+ }),
+ });
+
+ // Verify that the state was created successfully with the merged config
+ // The actual merging logic is tested in the .tftest.hcl file
+ expect(state).toBeDefined();
+ expect(state.resources).toBeDefined();
+ });
+});
diff --git a/registry/coder-labs/modules/copilot/main.tf b/registry/coder-labs/modules/copilot/main.tf
new file mode 100644
index 00000000..eb9f78d4
--- /dev/null
+++ b/registry/coder-labs/modules/copilot/main.tf
@@ -0,0 +1,302 @@
+terraform {
+ required_version = ">= 1.0"
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = ">= 2.7"
+ }
+ }
+}
+
+variable "agent_id" {
+ type = string
+ description = "The ID of a Coder agent."
+}
+
+variable "workdir" {
+ type = string
+ description = "The folder to run Copilot in."
+}
+
+variable "external_auth_id" {
+ type = string
+ description = "ID of the GitHub external auth provider configured in Coder."
+ default = "github"
+}
+
+variable "github_token" {
+ type = string
+ description = "GitHub OAuth token or Personal Access Token. If provided, this will be used instead of auto-detecting authentication."
+ default = ""
+ sensitive = true
+}
+
+variable "copilot_model" {
+ type = string
+ description = "Model to use. Supported values: claude-sonnet-4, claude-sonnet-4.5 (default), gpt-5."
+ default = "claude-sonnet-4.5"
+ validation {
+ condition = contains(["claude-sonnet-4", "claude-sonnet-4.5", "gpt-5"], var.copilot_model)
+ error_message = "copilot_model must be one of: claude-sonnet-4, claude-sonnet-4.5, gpt-5."
+ }
+}
+
+variable "copilot_config" {
+ type = string
+ description = "Custom Copilot configuration as JSON string. Leave empty to use default configuration with banner disabled, theme set to auto, and workdir as trusted folder."
+ default = ""
+}
+
+variable "ai_prompt" {
+ type = string
+ description = "Initial task prompt for programmatic mode."
+ default = ""
+}
+
+variable "system_prompt" {
+ type = string
+ description = "The system prompt to use for the Copilot server. Task reporting instructions are automatically added when report_tasks is enabled."
+ default = "You are a helpful coding assistant that helps developers write, debug, and understand code. Provide clear explanations, follow best practices, and help solve coding problems efficiently."
+}
+
+variable "trusted_directories" {
+ type = list(string)
+ description = "Additional directories to trust for Copilot operations."
+ default = []
+}
+
+variable "allow_all_tools" {
+ type = bool
+ description = "Allow all tools without prompting (equivalent to --allow-all-tools)."
+ default = false
+}
+
+variable "allow_tools" {
+ type = list(string)
+ description = "Specific tools to allow: shell(command), write, or MCP_SERVER_NAME."
+ default = []
+}
+
+variable "deny_tools" {
+ type = list(string)
+ description = "Specific tools to deny: shell(command), write, or MCP_SERVER_NAME."
+ default = []
+}
+
+variable "mcp_config" {
+ type = string
+ description = "Custom MCP server configuration as JSON string."
+ default = ""
+}
+
+variable "install_agentapi" {
+ type = bool
+ description = "Whether to install AgentAPI."
+ default = true
+}
+
+variable "agentapi_version" {
+ type = string
+ description = "The version of AgentAPI to install."
+ default = "v0.10.0"
+}
+
+variable "copilot_version" {
+ type = string
+ description = "The version of GitHub Copilot CLI to install. Use 'latest' for the latest version or specify a version like '0.0.334'."
+ default = "0.0.334"
+}
+
+variable "report_tasks" {
+ type = bool
+ description = "Whether to enable task reporting to Coder UI via AgentAPI."
+ default = true
+}
+
+variable "subdomain" {
+ type = bool
+ description = "Whether to use a subdomain for AgentAPI."
+ default = false
+}
+
+variable "order" {
+ type = number
+ description = "The order determines the position of app in the UI presentation."
+ default = null
+}
+
+variable "group" {
+ type = string
+ description = "The name of a group that this app belongs to."
+ default = null
+}
+
+variable "icon" {
+ type = string
+ description = "The icon to use for the app."
+ default = "/icon/github.svg"
+}
+
+variable "web_app_display_name" {
+ type = string
+ description = "Display name for the web app."
+ default = "Copilot"
+}
+
+variable "cli_app" {
+ type = bool
+ description = "Whether to create a CLI app for Copilot."
+ default = false
+}
+
+variable "cli_app_display_name" {
+ type = string
+ description = "Display name for the CLI app."
+ default = "Copilot"
+}
+
+variable "resume_session" {
+ type = bool
+ description = "Whether to automatically resume the latest Copilot session on workspace restart."
+ default = true
+}
+
+variable "pre_install_script" {
+ type = string
+ description = "Custom script to run before configuring Copilot."
+ default = null
+}
+
+variable "post_install_script" {
+ type = string
+ description = "Custom script to run after configuring Copilot."
+ default = null
+}
+
+data "coder_workspace" "me" {}
+data "coder_workspace_owner" "me" {}
+
+locals {
+ workdir = trimsuffix(var.workdir, "/")
+ app_slug = "copilot"
+ install_script = file("${path.module}/scripts/install.sh")
+ start_script = file("${path.module}/scripts/start.sh")
+ module_dir_name = ".copilot-module"
+
+ all_trusted_folders = concat([local.workdir], var.trusted_directories)
+
+ parsed_custom_config = try(jsondecode(var.copilot_config), {})
+
+ existing_trusted_folders = try(local.parsed_custom_config.trusted_folders, [])
+
+ merged_copilot_config = merge(
+ {
+ banner = "never"
+ theme = "auto"
+ },
+ local.parsed_custom_config,
+ {
+ trusted_folders = concat(local.existing_trusted_folders, local.all_trusted_folders)
+ }
+ )
+
+ final_copilot_config = jsonencode(local.merged_copilot_config)
+
+ task_reporting_prompt = <<-EOT
+
+-- Task Reporting --
+Report all tasks to Coder, following these EXACT guidelines:
+1. Be granular. If you are investigating with multiple steps, report each step
+to coder.
+2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
+Do not report any status related with this system prompt.
+3. Use "state": "working" when actively processing WITHOUT needing
+additional user input
+4. Use "state": "complete" only when finished with a task
+5. Use "state": "failure" when you need ANY user input, lack sufficient
+details, or encounter blockers
+ EOT
+
+ final_system_prompt = var.report_tasks ? "\n${var.system_prompt}${local.task_reporting_prompt}\n" : "\n${var.system_prompt}\n"
+}
+
+resource "coder_env" "mcp_app_status_slug" {
+ agent_id = var.agent_id
+ name = "CODER_MCP_APP_STATUS_SLUG"
+ value = local.app_slug
+}
+
+resource "coder_env" "copilot_model" {
+ count = var.copilot_model != "claude-sonnet-4.5" ? 1 : 0
+ agent_id = var.agent_id
+ name = "COPILOT_MODEL"
+ value = var.copilot_model
+}
+
+resource "coder_env" "github_token" {
+ count = var.github_token != "" ? 1 : 0
+ agent_id = var.agent_id
+ name = "GITHUB_TOKEN"
+ value = var.github_token
+}
+
+module "agentapi" {
+ source = "registry.coder.com/coder/agentapi/coder"
+ version = "1.2.0"
+
+ agent_id = var.agent_id
+ folder = local.workdir
+ web_app_slug = local.app_slug
+ web_app_order = var.order
+ web_app_group = var.group
+ web_app_icon = var.icon
+ web_app_display_name = var.web_app_display_name
+ cli_app = var.cli_app
+ cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
+ cli_app_icon = var.cli_app ? var.icon : null
+ cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
+ agentapi_subdomain = var.subdomain
+ module_dir_name = local.module_dir_name
+ install_agentapi = var.install_agentapi
+ agentapi_version = var.agentapi_version
+ pre_install_script = var.pre_install_script
+ post_install_script = var.post_install_script
+
+ start_script = <<-EOT
+ #!/bin/bash
+ set -o errexit
+ set -o pipefail
+ echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
+ chmod +x /tmp/start.sh
+
+ ARG_WORKDIR='${local.workdir}' \
+ ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
+ ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
+ ARG_COPILOT_MODEL='${var.copilot_model}' \
+ ARG_ALLOW_ALL_TOOLS='${var.allow_all_tools}' \
+ ARG_ALLOW_TOOLS='${join(",", var.allow_tools)}' \
+ ARG_DENY_TOOLS='${join(",", var.deny_tools)}' \
+ ARG_TRUSTED_DIRECTORIES='${join(",", var.trusted_directories)}' \
+ ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
+ ARG_RESUME_SESSION='${var.resume_session}' \
+ /tmp/start.sh
+ EOT
+
+ install_script = <<-EOT
+ #!/bin/bash
+ set -o errexit
+ set -o pipefail
+ echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
+ chmod +x /tmp/install.sh
+
+ ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
+ ARG_REPORT_TASKS='${var.report_tasks}' \
+ ARG_WORKDIR='${local.workdir}' \
+ ARG_MCP_CONFIG='${var.mcp_config != "" ? base64encode(var.mcp_config) : ""}' \
+ ARG_COPILOT_CONFIG='${base64encode(local.final_copilot_config)}' \
+ ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
+ ARG_COPILOT_VERSION='${var.copilot_version}' \
+ ARG_COPILOT_MODEL='${var.copilot_model}' \
+ /tmp/install.sh
+ EOT
+}
\ No newline at end of file
diff --git a/registry/coder-labs/modules/copilot/scripts/install.sh b/registry/coder-labs/modules/copilot/scripts/install.sh
new file mode 100644
index 00000000..0aabd761
--- /dev/null
+++ b/registry/coder-labs/modules/copilot/scripts/install.sh
@@ -0,0 +1,234 @@
+#!/bin/bash
+set -euo pipefail
+
+source "$HOME"/.bashrc
+
+command_exists() {
+ command -v "$1" > /dev/null 2>&1
+}
+
+ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
+ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
+ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
+ARG_MCP_CONFIG=$(echo -n "${ARG_MCP_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
+ARG_COPILOT_CONFIG=$(echo -n "${ARG_COPILOT_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
+ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
+ARG_COPILOT_VERSION=${ARG_COPILOT_VERSION:-0.0.334}
+ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-claude-sonnet-4.5}
+
+validate_prerequisites() {
+ if ! command_exists node; then
+ echo "ERROR: Node.js not found. Copilot requires Node.js v22+."
+ echo "Install with: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs"
+ exit 1
+ fi
+
+ if ! command_exists npm; then
+ echo "ERROR: npm not found. Copilot requires npm v10+."
+ exit 1
+ fi
+
+ node_version=$(node --version | sed 's/v//' | cut -d. -f1)
+ if [ "$node_version" -lt 22 ]; then
+ echo "WARNING: Node.js v$node_version detected. Copilot requires v22+."
+ fi
+}
+
+install_copilot() {
+ if ! command_exists copilot; then
+ echo "Installing GitHub Copilot CLI (version: ${ARG_COPILOT_VERSION})..."
+ if [ "$ARG_COPILOT_VERSION" = "latest" ]; then
+ npm install -g @github/copilot
+ else
+ npm install -g "@github/copilot@${ARG_COPILOT_VERSION}"
+ fi
+
+ if ! command_exists copilot; then
+ echo "ERROR: Failed to install Copilot"
+ exit 1
+ fi
+
+ echo "GitHub Copilot CLI installed successfully"
+ else
+ echo "GitHub Copilot CLI already installed"
+ fi
+}
+
+check_github_authentication() {
+ echo "Checking GitHub authentication..."
+
+ if [ -n "${GITHUB_TOKEN:-}" ]; then
+ echo "β GitHub token provided via module configuration"
+ return 0
+ fi
+
+ if command_exists coder; then
+ if coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" > /dev/null 2>&1; then
+ echo "β GitHub OAuth authentication via Coder external auth"
+ return 0
+ fi
+ fi
+
+ if command_exists gh && gh auth status > /dev/null 2>&1; then
+ echo "β GitHub OAuth authentication via GitHub CLI"
+ return 0
+ fi
+
+ echo "β No GitHub authentication detected"
+ echo " Copilot will prompt for authentication when started"
+ echo " For seamless experience, configure GitHub external auth in Coder or run 'gh auth login'"
+ return 0
+}
+
+setup_copilot_configurations() {
+ mkdir -p "$ARG_WORKDIR"
+
+ local module_path="$HOME/.copilot-module"
+ mkdir -p "$module_path"
+
+ setup_copilot_config
+
+ echo "$ARG_WORKDIR" > "$module_path/trusted_directories"
+}
+
+setup_copilot_config() {
+ export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
+ local copilot_config_dir="$XDG_CONFIG_HOME/.copilot"
+ local copilot_config_file="$copilot_config_dir/config.json"
+ local mcp_config_file="$copilot_config_dir/mcp-config.json"
+
+ mkdir -p "$copilot_config_dir"
+
+ if [ -n "$ARG_COPILOT_CONFIG" ]; then
+ echo "Setting up Copilot configuration..."
+
+ if command_exists jq; then
+ echo "$ARG_COPILOT_CONFIG" | jq 'del(.mcpServers)' > "$copilot_config_file"
+ else
+ echo "$ARG_COPILOT_CONFIG" > "$copilot_config_file"
+ fi
+
+ echo "Setting up MCP server configuration..."
+ setup_mcp_config "$mcp_config_file"
+ else
+ echo "ERROR: No Copilot configuration provided"
+ exit 1
+ fi
+}
+
+setup_mcp_config() {
+ local mcp_config_file="$1"
+
+ echo '{"mcpServers": {}}' > "$mcp_config_file"
+
+ if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
+ echo "Adding Coder MCP server for task reporting..."
+ setup_coder_mcp_server "$mcp_config_file"
+ fi
+
+ if [ -n "$ARG_MCP_CONFIG" ]; then
+ echo "Adding custom MCP servers..."
+ add_custom_mcp_servers "$mcp_config_file"
+ fi
+
+ echo "MCP configuration completed: $mcp_config_file"
+}
+
+setup_coder_mcp_server() {
+ local mcp_config_file="$1"
+
+ local coder_mcp_wrapper_script
+ coder_mcp_wrapper_script=$(
+ cat << EOF
+#!/usr/bin/env bash
+set -e
+
+export CODER_MCP_APP_STATUS_SLUG="${ARG_MCP_APP_STATUS_SLUG}"
+export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
+export CODER_AGENT_URL="${CODER_AGENT_URL}"
+export CODER_AGENT_TOKEN="${CODER_AGENT_TOKEN}"
+
+exec coder exp mcp server
+EOF
+ )
+ echo "$coder_mcp_wrapper_script" > "/tmp/coder-mcp-server.sh"
+ chmod +x /tmp/coder-mcp-server.sh
+
+ local coder_mcp_config
+ coder_mcp_config=$(
+ cat << EOF
+{
+ "mcpServers": {
+ "coder": {
+ "command": "/tmp/coder-mcp-server.sh",
+ "args": [],
+ "description": "CRITICAL: Call this tool immediately after receiving ANY user message to report task status. Rules: (1) Call FIRST before doing work - report what you will do with state='working'. (2) Be granular - report each step separately. (3) State 'working' = actively processing without needing user input. (4) State 'complete' = task 100% finished. (5) State 'failure' = need user input, missing info, or blocked. Example: User says 'fix the bug' -> call with state='working', description='Investigating authentication bug'. When done -> call with state='complete', description='Fixed token validation'. You MUST report on every interaction.",
+ "name": "Coder",
+ "timeout": 3000,
+ "type": "local",
+ "tools": ["*"],
+ "trust": true
+ }
+ }
+}
+EOF
+ )
+
+ echo "$coder_mcp_config" > "$mcp_config_file"
+}
+
+add_custom_mcp_servers() {
+ local mcp_config_file="$1"
+
+ if command_exists jq; then
+ local custom_servers
+ custom_servers=$(echo "$ARG_MCP_CONFIG" | jq '.mcpServers // {}')
+
+ local updated_config
+ updated_config=$(jq --argjson custom "$custom_servers" '.mcpServers += $custom' "$mcp_config_file")
+ echo "$updated_config" > "$mcp_config_file"
+ elif command_exists node; then
+ node -e "
+ const fs = require('fs');
+ const existing = JSON.parse(fs.readFileSync('$mcp_config_file', 'utf8'));
+ const input = JSON.parse(\`$ARG_MCP_CONFIG\`);
+ const custom = input.mcpServers || {};
+ existing.mcpServers = {...existing.mcpServers, ...custom};
+ fs.writeFileSync('$mcp_config_file', JSON.stringify(existing, null, 2));
+ "
+ else
+ echo "WARNING: jq and node not available, cannot merge custom MCP servers"
+ fi
+}
+
+configure_copilot_model() {
+ if [ -n "$ARG_COPILOT_MODEL" ] && [ "$ARG_COPILOT_MODEL" != "claude-sonnet-4.5" ]; then
+ echo "Setting Copilot model to: $ARG_COPILOT_MODEL"
+ copilot config model "$ARG_COPILOT_MODEL" || {
+ echo "WARNING: Failed to set model via copilot config, will use environment variable fallback"
+ export COPILOT_MODEL="$ARG_COPILOT_MODEL"
+ }
+ fi
+}
+
+configure_coder_integration() {
+ if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
+ echo "Configuring Copilot task reporting..."
+ export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
+ export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
+ echo "β Coder MCP server configured for task reporting"
+ else
+ echo "Task reporting disabled or no app status slug provided."
+ export CODER_MCP_APP_STATUS_SLUG=""
+ export CODER_MCP_AI_AGENTAPI_URL=""
+ fi
+}
+
+validate_prerequisites
+install_copilot
+check_github_authentication
+setup_copilot_configurations
+configure_copilot_model
+configure_coder_integration
+
+echo "Copilot module setup completed."
diff --git a/registry/coder-labs/modules/copilot/scripts/start.sh b/registry/coder-labs/modules/copilot/scripts/start.sh
new file mode 100644
index 00000000..2653d593
--- /dev/null
+++ b/registry/coder-labs/modules/copilot/scripts/start.sh
@@ -0,0 +1,157 @@
+#!/bin/bash
+set -euo pipefail
+
+source "$HOME"/.bashrc
+export PATH="$HOME/.local/bin:$PATH"
+
+command_exists() {
+ command -v "$1" > /dev/null 2>&1
+}
+
+ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
+ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
+ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
+ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-}
+ARG_ALLOW_ALL_TOOLS=${ARG_ALLOW_ALL_TOOLS:-false}
+ARG_ALLOW_TOOLS=${ARG_ALLOW_TOOLS:-}
+ARG_DENY_TOOLS=${ARG_DENY_TOOLS:-}
+ARG_TRUSTED_DIRECTORIES=${ARG_TRUSTED_DIRECTORIES:-}
+ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
+ARG_RESUME_SESSION=${ARG_RESUME_SESSION:-true}
+
+validate_copilot_installation() {
+ if ! command_exists copilot; then
+ echo "ERROR: Copilot not installed. Run: npm install -g @github/copilot"
+ exit 1
+ fi
+}
+
+build_initial_prompt() {
+ local initial_prompt=""
+
+ if [ -n "$ARG_AI_PROMPT" ]; then
+ if [ -n "$ARG_SYSTEM_PROMPT" ]; then
+ initial_prompt="$ARG_SYSTEM_PROMPT
+
+$ARG_AI_PROMPT"
+ else
+ initial_prompt="$ARG_AI_PROMPT"
+ fi
+ fi
+
+ echo "$initial_prompt"
+}
+
+build_copilot_args() {
+ COPILOT_ARGS=()
+
+ if [ "$ARG_ALLOW_ALL_TOOLS" = "true" ]; then
+ COPILOT_ARGS+=(--allow-all-tools)
+ fi
+
+ if [ -n "$ARG_ALLOW_TOOLS" ]; then
+ IFS=',' read -ra ALLOW_ARRAY <<< "$ARG_ALLOW_TOOLS"
+ for tool in "${ALLOW_ARRAY[@]}"; do
+ if [ -n "$tool" ]; then
+ COPILOT_ARGS+=(--allow-tool "$tool")
+ fi
+ done
+ fi
+
+ if [ -n "$ARG_DENY_TOOLS" ]; then
+ IFS=',' read -ra DENY_ARRAY <<< "$ARG_DENY_TOOLS"
+ for tool in "${DENY_ARRAY[@]}"; do
+ if [ -n "$tool" ]; then
+ COPILOT_ARGS+=(--deny-tool "$tool")
+ fi
+ done
+ fi
+}
+
+check_existing_session() {
+ if [ "$ARG_RESUME_SESSION" = "true" ]; then
+ if copilot --help > /dev/null 2>&1; then
+ local session_dir="$HOME/.copilot/history-session-state"
+ if [ -d "$session_dir" ] && [ -n "$(ls "$session_dir"/session_*_*.json 2> /dev/null)" ]; then
+ echo "Found existing Copilot session. Will continue latest session." >&2
+ return 0
+ fi
+ fi
+ fi
+ return 1
+}
+
+setup_github_authentication() {
+ export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
+ echo "Setting up GitHub authentication..."
+
+ if [ -n "${GITHUB_TOKEN:-}" ]; then
+ export GH_TOKEN="$GITHUB_TOKEN"
+ echo "β Using GitHub token from module configuration"
+ return 0
+ fi
+
+ if command_exists coder; then
+ local github_token
+ if github_token=$(coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" 2> /dev/null); then
+ if [ -n "$github_token" ] && [ "$github_token" != "null" ]; then
+ export GITHUB_TOKEN="$github_token"
+ export GH_TOKEN="$github_token"
+ echo "β Using Coder external auth OAuth token"
+ return 0
+ fi
+ fi
+ fi
+
+ if command_exists gh && gh auth status > /dev/null 2>&1; then
+ echo "β Using GitHub CLI OAuth authentication"
+ return 0
+ fi
+
+ echo "β No GitHub authentication available"
+ echo " Copilot will prompt for login during first use"
+ echo " Use the '/login' command in Copilot to authenticate"
+ return 0
+}
+
+start_agentapi() {
+ echo "Starting in directory: $ARG_WORKDIR"
+ cd "$ARG_WORKDIR"
+
+ build_copilot_args
+
+ if check_existing_session; then
+ echo "Continuing latest Copilot session..."
+ if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
+ echo "Copilot arguments: ${COPILOT_ARGS[*]}"
+ agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue "${COPILOT_ARGS[@]}"
+ else
+ agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue
+ fi
+ else
+ echo "Starting new Copilot session..."
+ local initial_prompt
+ initial_prompt=$(build_initial_prompt)
+
+ if [ -n "$initial_prompt" ]; then
+ echo "Using initial prompt with system context"
+ if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
+ echo "Copilot arguments: ${COPILOT_ARGS[*]}"
+ agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
+ else
+ agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot
+ fi
+ else
+ if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
+ echo "Copilot arguments: ${COPILOT_ARGS[*]}"
+ agentapi server --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
+ else
+ agentapi server --type copilot --term-width 120 --term-height 40 -- copilot
+ fi
+ fi
+ fi
+}
+
+setup_github_authentication
+validate_copilot_installation
+start_agentapi
diff --git a/registry/coder-labs/modules/copilot/testdata/copilot-mock.sh b/registry/coder-labs/modules/copilot/testdata/copilot-mock.sh
new file mode 100644
index 00000000..f1daa15f
--- /dev/null
+++ b/registry/coder-labs/modules/copilot/testdata/copilot-mock.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+set -euo pipefail
+
+if [[ "$1" == "--version" ]]; then
+ echo "GitHub Copilot CLI v1.0.0"
+ exit 0
+fi
+
+while true; do
+ echo "$(date) - Copilot mock running..."
+ sleep 15
+done
diff --git a/registry/coder-labs/modules/cursor-cli/README.md b/registry/coder-labs/modules/cursor-cli/README.md
index 0d3cd753..92f045de 100644
--- a/registry/coder-labs/modules/cursor-cli/README.md
+++ b/registry/coder-labs/modules/cursor-cli/README.md
@@ -13,7 +13,7 @@ Run the Cursor Agent CLI in your workspace for interactive coding assistance and
```tf
module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder"
- version = "0.1.1"
+ version = "0.2.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -42,7 +42,7 @@ module "coder-login" {
module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder"
- version = "0.1.1"
+ version = "0.2.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
diff --git a/registry/coder-labs/modules/cursor-cli/main.tf b/registry/coder-labs/modules/cursor-cli/main.tf
index 48210371..c07193a6 100644
--- a/registry/coder-labs/modules/cursor-cli/main.tf
+++ b/registry/coder-labs/modules/cursor-cli/main.tf
@@ -56,7 +56,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
- default = "v0.5.0"
+ default = "v0.10.0"
}
variable "force" {
@@ -131,7 +131,7 @@ resource "coder_env" "cursor_api_key" {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
diff --git a/registry/coder-labs/modules/gemini/README.md b/registry/coder-labs/modules/gemini/README.md
index e661093e..1d499817 100644
--- a/registry/coder-labs/modules/gemini/README.md
+++ b/registry/coder-labs/modules/gemini/README.md
@@ -13,7 +13,7 @@ Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace
```tf
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
- version = "2.0.0"
+ version = "2.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -46,7 +46,7 @@ variable "gemini_api_key" {
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
- version = "2.0.0"
+ version = "2.1.0"
agent_id = coder_agent.example.id
gemini_api_key = var.gemini_api_key
folder = "/home/coder/project"
@@ -94,7 +94,7 @@ data "coder_parameter" "ai_prompt" {
module "gemini" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/gemini/coder"
- version = "2.0.0"
+ version = "2.1.0"
agent_id = coder_agent.example.id
gemini_api_key = var.gemini_api_key
gemini_model = "gemini-2.5-flash"
@@ -118,7 +118,7 @@ For enterprise users who prefer Google's Vertex AI platform:
```tf
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
- version = "2.0.0"
+ version = "2.1.0"
agent_id = coder_agent.example.id
gemini_api_key = var.gemini_api_key
folder = "/home/coder/project"
diff --git a/registry/coder-labs/modules/gemini/main.tf b/registry/coder-labs/modules/gemini/main.tf
index 889164b1..1dc25c6d 100644
--- a/registry/coder-labs/modules/gemini/main.tf
+++ b/registry/coder-labs/modules/gemini/main.tf
@@ -81,7 +81,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
- default = "v0.2.3"
+ default = "v0.10.0"
}
variable "gemini_model" {
@@ -176,7 +176,7 @@ EOT
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
diff --git a/registry/coder-labs/modules/sourcegraph-amp/README.md b/registry/coder-labs/modules/sourcegraph-amp/README.md
index d4486e65..61607d9d 100644
--- a/registry/coder-labs/modules/sourcegraph-amp/README.md
+++ b/registry/coder-labs/modules/sourcegraph-amp/README.md
@@ -13,7 +13,7 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
```tf
module "amp-cli" {
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
- version = "1.0.3"
+ version = "1.1.0"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
install_sourcegraph_amp = true
@@ -60,7 +60,7 @@ variable "sourcegraph_amp_api_key" {
module "amp-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
- version = "1.0.3"
+ version = "1.1.0"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key # recommended for authenticated usage
install_sourcegraph_amp = true
diff --git a/registry/coder-labs/modules/sourcegraph-amp/main.tf b/registry/coder-labs/modules/sourcegraph-amp/main.tf
index 033fc84e..b5566fef 100644
--- a/registry/coder-labs/modules/sourcegraph-amp/main.tf
+++ b/registry/coder-labs/modules/sourcegraph-amp/main.tf
@@ -69,7 +69,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
- default = "v0.3.0"
+ default = "v0.10.0"
}
variable "pre_install_script" {
@@ -151,7 +151,7 @@ locals {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
- version = "1.0.1"
+ version = "1.2.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
diff --git a/registry/coder/modules/agentapi/README.md b/registry/coder/modules/agentapi/README.md
index 0b5ed0ba..d68af511 100644
--- a/registry/coder/modules/agentapi/README.md
+++ b/registry/coder/modules/agentapi/README.md
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
```tf
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
diff --git a/registry/coder/modules/agentapi/main.tf b/registry/coder/modules/agentapi/main.tf
index 225a992d..e73f45f6 100644
--- a/registry/coder/modules/agentapi/main.tf
+++ b/registry/coder/modules/agentapi/main.tf
@@ -117,7 +117,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
- default = "v0.3.3"
+ default = "v0.10.0"
}
variable "agentapi_port" {
diff --git a/registry/coder/modules/amazon-q/README.md b/registry/coder/modules/amazon-q/README.md
index 03551487..3146f01e 100644
--- a/registry/coder/modules/amazon-q/README.md
+++ b/registry/coder/modules/amazon-q/README.md
@@ -13,7 +13,7 @@ Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's A
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
- version = "2.0.0"
+ version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
@@ -102,7 +102,7 @@ data "coder_parameter" "ai_prompt" {
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
- version = "2.0.0"
+ version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
@@ -228,7 +228,7 @@ If no custom `agent_config` is provided, the default agent name "agent" is used.
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
- version = "2.0.0"
+ version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
@@ -258,7 +258,7 @@ This example will:
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
- version = "2.0.0"
+ version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
@@ -279,7 +279,7 @@ module "amazon-q" {
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
- version = "2.0.0"
+ version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
@@ -305,7 +305,7 @@ module "amazon-q" {
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
- version = "2.0.0"
+ version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
@@ -319,7 +319,7 @@ module "amazon-q" {
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
- version = "2.0.0"
+ version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
@@ -340,14 +340,14 @@ module "amazon-q" {
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
- version = "2.0.0"
+ version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
# AgentAPI configuration for environments without wildcard access url. https://coder.com/docs/admin/setup#wildcard-access-url
agentapi_chat_based_path = true
- agentapi_version = "v0.6.1"
+ agentapi_version = "v0.10.0"
}
```
@@ -358,7 +358,7 @@ For environments without direct internet access, you can host Amazon Q installat
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
- version = "2.0.0"
+ version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
diff --git a/registry/coder/modules/amazon-q/main.tf b/registry/coder/modules/amazon-q/main.tf
index b845cedc..84ac3c03 100644
--- a/registry/coder/modules/amazon-q/main.tf
+++ b/registry/coder/modules/amazon-q/main.tf
@@ -88,7 +88,7 @@ variable "post_install_script" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
- default = "v0.6.1"
+ default = "v0.10.0"
}
variable "workdir" {
@@ -96,8 +96,6 @@ variable "workdir" {
description = "The folder to run Amazon Q in."
}
-# ---------------------------------------------
-
variable "install_amazon_q" {
type = bool
description = "Whether to install Amazon Q."
@@ -190,6 +188,7 @@ resource "coder_env" "auth_tarball" {
locals {
app_slug = "amazonq"
+ workdir = trimsuffix(var.workdir, "/")
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".amazonq-module"
@@ -215,9 +214,10 @@ locals {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = var.agent_id
+ folder = local.workdir
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
diff --git a/registry/coder/modules/amazon-q/scripts/install.sh b/registry/coder/modules/amazon-q/scripts/install.sh
index e3ae3361..f8e43e76 100644
--- a/registry/coder/modules/amazon-q/scripts/install.sh
+++ b/registry/coder/modules/amazon-q/scripts/install.sh
@@ -94,6 +94,13 @@ function install_amazon_q() {
function extract_auth_tarball() {
if [ -n "$ARG_AUTH_TARBALL" ]; then
echo "Extracting auth tarball..."
+
+ if ! command_exists zstd; then
+ echo "Error: zstd is required to extract the authentication tarball but is not installed."
+ echo "Please install zstd using the pre_install_script parameter."
+ exit 1
+ fi
+
PREV_DIR="$PWD"
echo "$ARG_AUTH_TARBALL" | base64 -d > /tmp/auth.tar.zst
rm -rf ~/.local/share/amazon-q
diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md
index 3c334e74..ed10e2a5 100644
--- a/registry/coder/modules/claude-code/README.md
+++ b/registry/coder/modules/claude-code/README.md
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "3.0.1"
+ version = "3.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
@@ -49,7 +49,7 @@ data "coder_parameter" "ai_prompt" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "3.0.1"
+ version = "3.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
@@ -58,7 +58,7 @@ module "claude-code" {
claude_code_oauth_token = "xxxxx-xxxx-xxxx"
claude_code_version = "1.0.82" # Pin to a specific version
- agentapi_version = "v0.6.1"
+ agentapi_version = "v0.10.0"
ai_prompt = data.coder_parameter.ai_prompt.value
model = "sonnet"
@@ -85,7 +85,7 @@ Run and configure Claude Code as a standalone CLI in your workspace.
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "3.0.1"
+ version = "3.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
install_claude_code = true
@@ -108,13 +108,168 @@ variable "claude_code_oauth_token" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "3.0.1"
+ version = "3.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
}
```
+### Usage with AWS Bedrock
+
+#### Prerequisites
+
+AWS account with Bedrock access, Claude models enabled in Bedrock console, appropriate IAM permissions.
+
+Configure Claude Code to use AWS Bedrock for accessing Claude models through your AWS infrastructure.
+
+```tf
+resource "coder_env" "bedrock_use" {
+ agent_id = coder_agent.example.id
+ name = "CLAUDE_CODE_USE_BEDROCK"
+ value = "1"
+}
+
+resource "coder_env" "aws_region" {
+ agent_id = coder_agent.example.id
+ name = "AWS_REGION"
+ value = "us-east-1" # Choose your preferred region
+}
+
+# Option 1: Using AWS credentials
+
+variable "aws_access_key_id" {
+ type = string
+ description = "Your AWS access key ID. Create this in the AWS IAM console under 'Security credentials'."
+ sensitive = true
+ value = "xxxx-xxx-xxxx"
+}
+
+variable "aws_secret_access_key" {
+ type = string
+ description = "Your AWS secret access key. This is shown once when you create an access key in the AWS IAM console."
+ sensitive = true
+ value = "xxxx-xxx-xxxx"
+}
+
+resource "coder_env" "aws_access_key_id" {
+ agent_id = coder_agent.example.id
+ name = "AWS_ACCESS_KEY_ID"
+ value = var.aws_access_key_id
+}
+
+resource "coder_env" "aws_secret_access_key" {
+ agent_id = coder_agent.example.id
+ name = "AWS_SECRET_ACCESS_KEY"
+ value = var.aws_secret_access_key
+}
+
+# Option 2: Using Bedrock API key (simpler)
+
+variable "aws_bearer_token_bedrock" {
+ type = string
+ description = "Your AWS Bedrock bearer token. This provides access to Bedrock without needing separate access key and secret key."
+ sensitive = true
+ value = "xxxx-xxx-xxxx"
+}
+
+resource "coder_env" "bedrock_api_key" {
+ agent_id = coder_agent.example.id
+ name = "AWS_BEARER_TOKEN_BEDROCK"
+ value = var.aws_bearer_token_bedrock
+}
+
+module "claude-code" {
+ source = "registry.coder.com/coder/claude-code/coder"
+ version = "3.1.1"
+ agent_id = coder_agent.example.id
+ workdir = "/home/coder/project"
+ model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
+}
+```
+
+> [!NOTE]
+> For additional Bedrock configuration options (model selection, token limits, region overrides, etc.), see the [Claude Code Bedrock documentation](https://docs.claude.com/en/docs/claude-code/amazon-bedrock).
+
+### Usage with Google Vertex AI
+
+#### Prerequisites
+
+GCP project with Vertex AI API enabled, Claude models enabled through Model Garden, service account with Vertex AI permissions, appropriate IAM permissions (Vertex AI User role).
+
+Configure Claude Code to use Google Vertex AI for accessing Claude models through Google Cloud Platform.
+
+```tf
+variable "vertex_sa_json" {
+ type = string
+ description = "The complete JSON content of your Google Cloud service account key file. Create a service account in the GCP Console under 'IAM & Admin > Service Accounts', then create and download a JSON key. Copy the entire JSON content into this variable."
+ sensitive = true
+}
+
+resource "coder_env" "vertex_use" {
+ agent_id = coder_agent.example.id
+ name = "CLAUDE_CODE_USE_VERTEX"
+ value = "1"
+}
+
+resource "coder_env" "vertex_project_id" {
+ agent_id = coder_agent.example.id
+ name = "ANTHROPIC_VERTEX_PROJECT_ID"
+ value = "your-gcp-project-id"
+}
+
+resource "coder_env" "cloud_ml_region" {
+ agent_id = coder_agent.example.id
+ name = "CLOUD_ML_REGION"
+ value = "global"
+}
+
+resource "coder_env" "vertex_sa_json" {
+ agent_id = coder_agent.example.id
+ name = "VERTEX_SA_JSON"
+ value = var.vertex_sa_json
+}
+
+resource "coder_env" "google_application_credentials" {
+ agent_id = coder_agent.example.id
+ name = "GOOGLE_APPLICATION_CREDENTIALS"
+ value = "/tmp/gcp-sa.json"
+}
+
+module "claude-code" {
+ source = "registry.coder.com/coder/claude-code/coder"
+ version = "3.1.1"
+ agent_id = coder_agent.example.id
+ workdir = "/home/coder/project"
+ model = "claude-sonnet-4@20250514"
+
+ pre_install_script = <<-EOT
+ #!/bin/bash
+ # Write the service account JSON to a file
+ echo "$VERTEX_SA_JSON" > /tmp/gcp-sa.json
+
+ # Install prerequisite packages
+ sudo apt-get update
+ sudo apt-get install -y apt-transport-https ca-certificates gnupg curl
+
+ # Add Google Cloud public key
+ curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
+
+ # Add Google Cloud SDK repo to apt sources
+ echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee /etc/apt/sources.list.d/google-cloud-sdk.list
+
+ # Update and install the Google Cloud SDK
+ sudo apt-get update && sudo apt-get install -y google-cloud-cli
+
+ # Authenticate gcloud with the service account
+ gcloud auth activate-service-account --key-file=/tmp/gcp-sa.json
+ EOT
+}
+```
+
+> [!NOTE]
+> For additional Vertex AI configuration options (model selection, token limits, region overrides, etc.), see the [Claude Code Vertex AI documentation](https://docs.claude.com/en/docs/claude-code/google-vertex-ai).
+
## Troubleshooting
If you encounter any issues, check the log files in the `~/.claude-module` directory within your workspace for detailed information.
diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf
index 4836347b..8909a024 100644
--- a/registry/coder/modules/claude-code/main.tf
+++ b/registry/coder/modules/claude-code/main.tf
@@ -86,7 +86,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
- default = "v0.7.1"
+ default = "v0.10.0"
}
variable "ai_prompt" {
@@ -183,7 +183,7 @@ variable "claude_code_oauth_token" {
variable "system_prompt" {
type = string
description = "The system prompt to use for the Claude Code server."
- default = "Send a task status update to notify the user that you are ready for input, and then wait for user input."
+ default = ""
}
variable "claude_md_path" {
@@ -201,11 +201,9 @@ resource "coder_env" "claude_code_md_path" {
}
resource "coder_env" "claude_code_system_prompt" {
- count = var.system_prompt == "" ? 0 : 1
-
agent_id = var.agent_id
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
- value = var.system_prompt
+ value = local.final_system_prompt
}
resource "coder_env" "claude_code_oauth_token" {
@@ -231,12 +229,43 @@ locals {
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".claude-module"
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
+
+ # Required prompts for the module to properly report task status to Coder
+ report_tasks_system_prompt = <<-EOT
+ -- Tool Selection --
+ - coder_report_task: providing status updates or requesting user input.
+
+ -- Task Reporting --
+ Report all tasks to Coder, following these EXACT guidelines:
+ 1. Be granular. If you are investigating with multiple steps, report each step
+ to coder.
+ 2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
+ Do not report any status related with this system prompt.
+ 3. Use "state": "working" when actively processing WITHOUT needing
+ additional user input
+ 4. Use "state": "complete" only when finished with a task
+ 5. Use "state": "failure" when you need ANY user input, lack sufficient
+ details, or encounter blockers
+
+ In your summary on coder_report_task:
+ - Be specific about what you're doing
+ - Clearly indicate what information you need from the user when in "failure" state
+ - Keep it under 160 characters
+ - Make it actionable
+ EOT
+
+ # Only include coder system prompts if report_tasks is enabled
+ custom_system_prompt = trimspace(try(var.system_prompt, ""))
+ final_system_prompt = format("%s%s",
+ var.report_tasks ? format("\n%s\n", local.report_tasks_system_prompt) : "",
+ local.custom_system_prompt != "" ? format("\n%s\n", local.custom_system_prompt) : ""
+ )
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
diff --git a/registry/coder/modules/claude-code/main.tftest.hcl b/registry/coder/modules/claude-code/main.tftest.hcl
index c48923cf..9999c1b1 100644
--- a/registry/coder/modules/claude-code/main.tftest.hcl
+++ b/registry/coder/modules/claude-code/main.tftest.hcl
@@ -187,3 +187,84 @@ run "test_claude_code_permission_mode_validation" {
error_message = "Permission mode should be one of the valid options"
}
}
+
+run "test_claude_code_system_prompt" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-system-prompt"
+ workdir = "/home/coder/test"
+ system_prompt = "Custom addition"
+ }
+
+ assert {
+ condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
+ error_message = "System prompt should not be empty"
+ }
+
+ assert {
+ condition = length(regexall("Custom addition", coder_env.claude_code_system_prompt.value)) > 0
+ error_message = "System prompt should have system_prompt variable value"
+ }
+}
+
+run "test_claude_report_tasks_default" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-report-tasks"
+ workdir = "/home/coder/test"
+ # report_tasks: default is true
+ }
+
+ assert {
+ condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
+ error_message = "System prompt should not be empty"
+ }
+
+ # Ensure system prompt is wrapped by
+ assert {
+ condition = startswith(trimspace(coder_env.claude_code_system_prompt.value), "")
+ error_message = "System prompt should start with "
+ }
+ assert {
+ condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "")
+ error_message = "System prompt should end with "
+ }
+
+ # Ensure Coder sections are injected when report_tasks=true (default)
+ assert {
+ condition = length(regexall("-- Tool Selection --", coder_env.claude_code_system_prompt.value)) > 0
+ error_message = "System prompt should have Tool Selection section"
+ }
+
+ assert {
+ condition = length(regexall("-- Task Reporting --", coder_env.claude_code_system_prompt.value)) > 0
+ error_message = "System prompt should have Task Reporting section"
+ }
+}
+
+run "test_claude_report_tasks_disabled" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-report-tasks"
+ workdir = "/home/coder/test"
+ report_tasks = false
+ }
+
+ assert {
+ condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
+ error_message = "System prompt should not be empty"
+ }
+
+ # Ensure system prompt is wrapped by
+ assert {
+ condition = startswith(trimspace(coder_env.claude_code_system_prompt.value), "")
+ error_message = "System prompt should start with "
+ }
+ assert {
+ condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "")
+ error_message = "System prompt should end with "
+ }
+}
\ No newline at end of file
diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh
index c3dcc22f..1285df90 100644
--- a/registry/coder/modules/claude-code/scripts/install.sh
+++ b/registry/coder/modules/claude-code/scripts/install.sh
@@ -1,7 +1,9 @@
#!/bin/bash
set -euo pipefail
-source "$HOME"/.bashrc
+if [ -f "$HOME/.bashrc" ]; then
+ source "$HOME"/.bashrc
+fi
BOLD='\033[0;1m'
diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh
index b5fca7a5..6eeb411b 100644
--- a/registry/coder/modules/claude-code/scripts/start.sh
+++ b/registry/coder/modules/claude-code/scripts/start.sh
@@ -1,7 +1,9 @@
#!/bin/bash
set -euo pipefail
-source "$HOME"/.bashrc
+if [ -f "$HOME/.bashrc" ]; then
+ source "$HOME"/.bashrc
+fi
export PATH="$HOME/.local/bin:$PATH"
command_exists() {
diff --git a/registry/coder/modules/git-clone/README.md b/registry/coder/modules/git-clone/README.md
index c12eb590..6ec2ccbe 100644
--- a/registry/coder/modules/git-clone/README.md
+++ b/registry/coder/modules/git-clone/README.md
@@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
@@ -28,7 +28,7 @@ module "git-clone" {
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
base_dir = "~/projects/coder"
@@ -43,12 +43,12 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
-data "coder_git_auth" "github" {
+data "coder_external_auth" "github" {
id = "github"
}
```
@@ -69,7 +69,7 @@ data "coder_parameter" "git_repo" {
module "git_clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = coder_agent.example.id
url = data.coder_parameter.git_repo.value
}
@@ -103,7 +103,7 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.example.com/coder/coder/tree/feat/example"
git_providers = {
@@ -122,7 +122,7 @@ To GitLab clone with a specific branch like `feat/example`
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
}
@@ -134,7 +134,7 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
git_providers = {
@@ -155,7 +155,7 @@ For example, to clone the `feat/example` branch:
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
branch_name = "feat/example"
@@ -173,7 +173,7 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder:
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
folder_name = "coder-dev"
@@ -192,9 +192,32 @@ If not defined, the default, `0`, performs a full clone.
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-clone/coder"
- version = "1.1.0"
+ version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
depth = 1
}
```
+
+## Post-clone script
+
+Run a custom script after cloning the repository by setting the `post_clone_script` variable.
+This is useful for running initialization tasks like installing dependencies or setting up the environment.
+
+```tf
+module "git-clone" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/git-clone/coder"
+ version = "1.2.0"
+ agent_id = coder_agent.example.id
+ url = "https://github.com/coder/coder"
+ post_clone_script = <<-EOT
+ #!/bin/bash
+ echo "Repository cloned successfully!"
+ # Install dependencies
+ npm install
+ # Run any other initialization tasks
+ make setup
+ EOT
+}
+```
diff --git a/registry/coder/modules/git-clone/main.test.ts b/registry/coder/modules/git-clone/main.test.ts
index 1e074fc0..0ae0a8db 100644
--- a/registry/coder/modules/git-clone/main.test.ts
+++ b/registry/coder/modules/git-clone/main.test.ts
@@ -30,11 +30,12 @@ describe("git-clone", async () => {
url: "fake-url",
});
const output = await executeScriptInContainer(state, "alpine/git");
- expect(output.exitCode).toBe(128);
expect(output.stdout).toEqual([
"Creating directory ~/fake-url...",
"Cloning fake-url to ~/fake-url...",
]);
+ expect(output.stderr.join(" ")).toContain("fatal");
+ expect(output.stderr.join(" ")).toContain("fake-url");
});
it("repo_dir should match repo name for https", async () => {
@@ -244,4 +245,20 @@ describe("git-clone", async () => {
"Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...",
]);
});
+
+ it("runs post-clone script", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ url: "fake-url",
+ post_clone_script: "echo 'Post-clone script executed'",
+ });
+ const output = await executeScriptInContainer(
+ state,
+ "alpine/git",
+ "sh",
+ "mkdir -p ~/fake-url && echo 'existing' > ~/fake-url/file.txt",
+ );
+ expect(output.stdout).toContain("Running post-clone script...");
+ expect(output.stdout).toContain("Post-clone script executed");
+ });
});
diff --git a/registry/coder/modules/git-clone/main.tf b/registry/coder/modules/git-clone/main.tf
index d03a59d7..2d547ad2 100644
--- a/registry/coder/modules/git-clone/main.tf
+++ b/registry/coder/modules/git-clone/main.tf
@@ -62,6 +62,12 @@ variable "depth" {
default = 0
}
+variable "post_clone_script" {
+ description = "Custom script to run after cloning the repository. Runs always after git clone, even if the repository already exists."
+ type = string
+ default = null
+}
+
locals {
# Remove query parameters and fragments from the URL
url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "")
@@ -81,6 +87,8 @@ locals {
clone_path = var.base_dir != "" ? join("/", [var.base_dir, local.folder_name]) : join("/", ["~", local.folder_name])
# Construct the web URL
web_url = startswith(local.clone_url, "git@") ? replace(replace(local.clone_url, ":", "/"), "git@", "https://") : local.clone_url
+ # Encode the post_clone_script for passing to the shell script
+ encoded_post_clone_script = var.post_clone_script != null ? base64encode(var.post_clone_script) : ""
}
output "repo_dir" {
@@ -120,6 +128,7 @@ resource "coder_script" "git_clone" {
REPO_URL : local.clone_url,
BRANCH_NAME : local.branch_name,
DEPTH = var.depth,
+ POST_CLONE_SCRIPT : local.encoded_post_clone_script,
})
display_name = "Git Clone"
icon = "/icon/git.svg"
diff --git a/registry/coder/modules/git-clone/run.sh b/registry/coder/modules/git-clone/run.sh
index 282c667a..07c970e9 100644
--- a/registry/coder/modules/git-clone/run.sh
+++ b/registry/coder/modules/git-clone/run.sh
@@ -6,6 +6,7 @@ BRANCH_NAME="${BRANCH_NAME}"
# Expand home if it's specified!
CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}"
DEPTH="${DEPTH}"
+POST_CLONE_SCRIPT="${POST_CLONE_SCRIPT}"
# Check if the variable is empty...
if [ -z "$REPO_URL" ]; then
@@ -52,5 +53,14 @@ if [ -z "$(ls -A "$CLONE_PATH")" ]; then
fi
else
echo "$CLONE_PATH already exists and isn't empty, skipping clone!"
- exit 0
+fi
+
+# Run post-clone script if provided
+if [ -n "$POST_CLONE_SCRIPT" ]; then
+ echo "Running post-clone script..."
+ echo "$POST_CLONE_SCRIPT" | base64 -d > /tmp/post_clone.sh
+ chmod +x /tmp/post_clone.sh
+ cd "$CLONE_PATH"
+ /tmp/post_clone.sh
+ rm /tmp/post_clone.sh
fi
diff --git a/registry/coder/modules/goose/README.md b/registry/coder/modules/goose/README.md
index a1dbfefe..f4f91ab5 100644
--- a/registry/coder/modules/goose/README.md
+++ b/registry/coder/modules/goose/README.md
@@ -13,7 +13,7 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener
```tf
module "goose" {
source = "registry.coder.com/coder/goose/coder"
- version = "2.1.2"
+ version = "2.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
@@ -79,7 +79,7 @@ resource "coder_agent" "main" {
module "goose" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/goose/coder"
- version = "2.1.2"
+ version = "2.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
diff --git a/registry/coder/modules/goose/main.tf b/registry/coder/modules/goose/main.tf
index 2e015e76..51f8b6d6 100644
--- a/registry/coder/modules/goose/main.tf
+++ b/registry/coder/modules/goose/main.tf
@@ -63,7 +63,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
- default = "v0.3.3"
+ default = "v0.10.0"
}
variable "subdomain" {
@@ -135,11 +135,12 @@ EOT
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".goose-module"
+ folder = trimsuffix(var.folder, "/")
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
- version = "1.1.1"
+ version = "1.2.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
@@ -156,6 +157,7 @@ module "agentapi" {
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = local.start_script
+ folder = local.folder
install_script = <<-EOT
#!/bin/bash
set -o errexit
diff --git a/registry/coder/modules/jetbrains-gateway/README.md b/registry/coder/modules/jetbrains-gateway/README.md
index 8cfe3b5e..0c5c8ff8 100644
--- a/registry/coder/modules/jetbrains-gateway/README.md
+++ b/registry/coder/modules/jetbrains-gateway/README.md
@@ -10,6 +10,7 @@ tags: [ide, jetbrains, parameter, gateway]
This module adds a JetBrains Gateway Button to open any workspace with a single click.
+> [!TIP]
> We recommend using the [Coder Toolbox module](https://registry.coder.com/modules/coder/jetbrains), which offers significant stability and connectivity benefits over Gateway. Reference our [documentation](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) for more information.
JetBrains recommends a minimum of 4 CPU cores and 8GB of RAM.
@@ -19,7 +20,7 @@ Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prereq
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
- version = "1.2.4"
+ version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
@@ -37,7 +38,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
- version = "1.2.4"
+ version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
@@ -51,7 +52,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
- version = "1.2.4"
+ version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["IU", "PY"]
@@ -66,7 +67,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
- version = "1.2.4"
+ version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["IU", "PY"]
@@ -91,7 +92,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
- version = "1.2.4"
+ version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
@@ -109,7 +110,7 @@ Due to the highest priority of the `ide_download_link` parameter in the `(jetbra
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
- version = "1.2.4"
+ version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
diff --git a/registry/coder/modules/kasmvnc/README.md b/registry/coder/modules/kasmvnc/README.md
index cb07e6e9..2bc862d4 100644
--- a/registry/coder/modules/kasmvnc/README.md
+++ b/registry/coder/modules/kasmvnc/README.md
@@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
module "kasmvnc" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kasmvnc/coder"
- version = "1.2.3"
+ version = "1.2.4"
agent_id = coder_agent.example.id
desktop_environment = "xfce"
subdomain = true
diff --git a/registry/coder/modules/kasmvnc/run.sh b/registry/coder/modules/kasmvnc/run.sh
index 8389ea14..04b8b9ee 100644
--- a/registry/coder/modules/kasmvnc/run.sh
+++ b/registry/coder/modules/kasmvnc/run.sh
@@ -60,6 +60,9 @@ install_deb() {
sudo apt-get -o DPkg::Lock::Timeout=300 -qq update
fi
+ echo "Installing required Perl DateTime module..."
+ DEBIAN_FRONTEND=noninteractive sudo apt-get -o DPkg::Lock::Timeout=300 install --yes -qq --no-install-recommends --no-install-suggests libdatetime-perl
+
DEBIAN_FRONTEND=noninteractive sudo apt-get -o DPkg::Lock::Timeout=300 install --yes -qq --no-install-recommends --no-install-suggests "$kasmdeb"
rm "$kasmdeb"
}
@@ -233,19 +236,17 @@ get_http_dir() {
# Check the system configuration path
if [[ -e /etc/kasmvnc/kasmvnc.yaml ]]; then
- d=($(grep -E "^\s*httpd_directory:.*$" /etc/kasmvnc/kasmvnc.yaml))
- # If this grep is successful, it will return:
- # httpd_directory: /usr/share/kasmvnc/www
- if [[ $${#d[@]} -eq 2 && -d "$${d[1]}" ]]; then
- httpd_directory="$${d[1]}"
+ d=$(grep -E '^\s*httpd_directory:.*$' "/etc/kasmvnc/kasmvnc.yaml" | awk '{print $$2}')
+ if [[ -n "$d" && -d "$d" ]]; then
+ httpd_directory=$d
fi
fi
# Check the home directory for overriding values
if [[ -e "$HOME/.vnc/kasmvnc.yaml" ]]; then
- d=($(grep -E "^\s*httpd_directory:.*$" "$HOME/.vnc/kasmvnc.yaml"))
- if [[ $${#d[@]} -eq 2 && -d "$${d[1]}" ]]; then
- httpd_directory="$${d[1]}"
+ d=$(grep -E '^\s*httpd_directory:.*$' "$HOME/.vnc/kasmvnc.yaml" | awk '{print $$2}')
+ if [[ -n "$d" && -d "$d" ]]; then
+ httpd_directory=$d
fi
fi
echo $httpd_directory
diff --git a/registry/coder/templates/kubernetes-devcontainer/main.tf b/registry/coder/templates/kubernetes-devcontainer/main.tf
index 07af6586..5e36226d 100644
--- a/registry/coder/templates/kubernetes-devcontainer/main.tf
+++ b/registry/coder/templates/kubernetes-devcontainer/main.tf
@@ -426,15 +426,14 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
- agent_id = coder_agent.main.id
- agent_name = "main"
- order = 1
+ agent_id = coder_agent.main.id
+ order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
- source = "registry.coder.com/modules/coder/jetbrains/coder"
+ source = "registry.coder.com/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
diff --git a/registry/mavrickrishi/README.md b/registry/mavrickrishi/README.md
index f4326ac3..eed6a5d9 100644
--- a/registry/mavrickrishi/README.md
+++ b/registry/mavrickrishi/README.md
@@ -19,3 +19,5 @@ participating in LFX CNCF programs, and helping the developer community grow.
## Modules
- **aws-ami-snapshot**: Create and manage AMI snapshots for Coder workspaces with restore capabilities
+- [nexus-repository](./modules/nexus-repository/) - Configure package managers to use Sonatype Nexus Repository
+- [auto-start-dev-server](modules/auto-start-dev-server/README.md) - Automatically detect and start development servers for various project types
diff --git a/registry/mavrickrishi/modules/auto-start-dev-server/README.md b/registry/mavrickrishi/modules/auto-start-dev-server/README.md
new file mode 100644
index 00000000..432b453f
--- /dev/null
+++ b/registry/mavrickrishi/modules/auto-start-dev-server/README.md
@@ -0,0 +1,151 @@
+---
+display_name: Auto-Start Dev Servers
+description: Automatically detect and start development servers for various project types
+icon: ../../../../.icons/auto-dev-server.svg
+verified: false
+tags: [development, automation, servers]
+---
+
+# Auto-Start Development Servers
+
+Automatically detect and start development servers for various project types when a workspace starts. This module scans your workspace for common project structures and starts the appropriate development servers in the background without manual intervention.
+
+```tf
+module "auto_start_dev_servers" {
+ source = "registry.coder.com/mavrickrishi/auto-start-dev-server/coder"
+ version = "1.0.1"
+ agent_id = coder_agent.main.id
+}
+```
+
+## Features
+
+- **Multi-language support**: Detects and starts servers for Node.js, Python (Django/Flask), Ruby (Rails), Java (Spring Boot), Go, PHP, Rust, and .NET projects
+- **Smart script prioritization**: Prioritizes `dev` scripts over `start` scripts for better development experience
+- **Intelligent frontend detection**: Automatically identifies frontend projects (React, Vue, Angular, Next.js, Nuxt, Svelte, Vite) and prioritizes them for preview apps
+- **Devcontainer integration**: Respects custom start commands defined in `.devcontainer/devcontainer.json`
+- **Configurable scanning**: Adjustable directory scan depth and project type toggles
+- **Non-blocking startup**: Servers start in the background with configurable startup delay
+- **Comprehensive logging**: All server output and detection results logged to a central file
+- **Smart detection**: Uses project-specific files and configurations to identify project types
+- **Integrated live preview**: Automatically creates a preview app for the primary frontend project
+
+## Supported Project Types
+
+| Framework/Language | Detection Files | Start Commands (in priority order) |
+| ------------------ | -------------------------------------------- | ----------------------------------------------------- |
+| **Node.js/npm** | `package.json` | `npm run dev`, `npm run serve`, `npm start` (or yarn) |
+| **Ruby on Rails** | `Gemfile` with rails gem | `bundle exec rails server` |
+| **Django** | `manage.py` | `python manage.py runserver` |
+| **Flask** | `requirements.txt` with Flask | `python app.py/main.py/run.py` |
+| **Spring Boot** | `pom.xml` or `build.gradle` with spring-boot | `mvn spring-boot:run`, `gradle bootRun` |
+| **Go** | `go.mod` | `go run main.go` |
+| **PHP** | `composer.json` | `php -S 0.0.0.0:8080` |
+| **Rust** | `Cargo.toml` | `cargo run` |
+| **.NET** | `*.csproj` | `dotnet run` |
+
+## Examples
+
+### Basic Usage
+
+```tf
+module "auto_start" {
+ source = "./modules/auto-start-dev-server"
+ version = "1.0.1"
+ agent_id = coder_agent.main.id
+}
+```
+
+### Advanced Usage
+
+```tf
+module "auto_start_dev_servers" {
+ source = "./modules/auto-start-dev-server"
+ version = "1.0.1"
+ agent_id = coder_agent.main.id
+
+ # Optional: Configure which project types to detect
+ enable_npm = true
+ enable_rails = true
+ enable_django = true
+ enable_flask = true
+ enable_spring_boot = true
+ enable_go = true
+ enable_php = true
+ enable_rust = true
+ enable_dotnet = true
+
+ # Optional: Enable devcontainer.json integration
+ enable_devcontainer = true
+
+ # Optional: Workspace directory to scan (supports environment variables)
+ workspace_directory = "$HOME"
+
+ # Optional: Directory scan depth (1-5)
+ scan_depth = 2
+
+ # Optional: Startup delay in seconds
+ startup_delay = 10
+
+ # Optional: Log file path
+ log_path = "/tmp/dev-servers.log"
+
+ # Optional: Enable automatic preview app (default: true)
+ enable_preview_app = true
+}
+```
+
+### Disable Preview App
+
+```tf
+module "auto_start" {
+ source = "./modules/auto-start-dev-server"
+ version = "1.0.1"
+ agent_id = coder_agent.main.id
+
+ # Disable automatic preview app creation
+ enable_preview_app = false
+}
+```
+
+### Selective Project Types
+
+```tf
+module "auto_start" {
+ source = "./modules/auto-start-dev-server"
+ version = "1.0.1"
+ agent_id = coder_agent.main.id
+
+ # Only enable web development projects
+ enable_npm = true
+ enable_rails = true
+ enable_django = true
+ enable_flask = true
+
+ # Disable other project types
+ enable_spring_boot = false
+ enable_go = false
+ enable_php = false
+ enable_rust = false
+ enable_dotnet = false
+}
+```
+
+### Deep Workspace Scanning
+
+```tf
+module "auto_start" {
+ source = "./modules/auto-start-dev-server"
+ version = "1.0.1"
+ agent_id = coder_agent.main.id
+
+ workspace_directory = "/workspaces"
+ scan_depth = 3
+ startup_delay = 5
+ log_path = "/var/log/dev-servers.log"
+}
+```
+
+## License
+
+This module is provided under the same license as the Coder Registry.
diff --git a/registry/mavrickrishi/modules/auto-start-dev-server/main.test.ts b/registry/mavrickrishi/modules/auto-start-dev-server/main.test.ts
new file mode 100644
index 00000000..05893194
--- /dev/null
+++ b/registry/mavrickrishi/modules/auto-start-dev-server/main.test.ts
@@ -0,0 +1,109 @@
+import { describe, expect, it } from "bun:test";
+import {
+ runTerraformApply,
+ runTerraformInit,
+ testRequiredVariables,
+} from "~test";
+
+describe("auto-start-dev-server", async () => {
+ await runTerraformInit(import.meta.dir);
+
+ testRequiredVariables(import.meta.dir, {
+ agent_id: "test-agent-123",
+ });
+
+ it("validates scan_depth range", () => {
+ const t1 = async () => {
+ await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent-123",
+ scan_depth: "0",
+ });
+ };
+ expect(t1).toThrow("Scan depth must be between 1 and 5");
+
+ const t2 = async () => {
+ await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent-123",
+ scan_depth: "6",
+ });
+ };
+ expect(t2).toThrow("Scan depth must be between 1 and 5");
+ });
+
+ it("applies successfully with default values", async () => {
+ await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent-123",
+ });
+ });
+
+ it("applies successfully with all project types enabled", async () => {
+ await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent-123",
+ enable_npm: "true",
+ enable_rails: "true",
+ enable_django: "true",
+ enable_flask: "true",
+ enable_spring_boot: "true",
+ enable_go: "true",
+ enable_php: "true",
+ enable_rust: "true",
+ enable_dotnet: "true",
+ enable_devcontainer: "true",
+ });
+ });
+
+ it("applies successfully with all project types disabled", async () => {
+ await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent-123",
+ enable_npm: "false",
+ enable_rails: "false",
+ enable_django: "false",
+ enable_flask: "false",
+ enable_spring_boot: "false",
+ enable_go: "false",
+ enable_php: "false",
+ enable_rust: "false",
+ enable_dotnet: "false",
+ enable_devcontainer: "false",
+ });
+ });
+
+ it("applies successfully with custom configuration", async () => {
+ await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent-123",
+ workspace_directory: "/custom/workspace",
+ scan_depth: "3",
+ startup_delay: "5",
+ log_path: "/var/log/custom-dev-servers.log",
+ display_name: "Custom Dev Server Startup",
+ });
+ });
+
+ it("validates scan_depth boundary values", async () => {
+ // Test valid boundary values
+ await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent-123",
+ scan_depth: "1",
+ });
+
+ await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent-123",
+ scan_depth: "5",
+ });
+ });
+
+ it("applies with selective project type configuration", async () => {
+ await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent-123",
+ enable_npm: "true",
+ enable_django: "true",
+ enable_go: "true",
+ enable_rails: "false",
+ enable_flask: "false",
+ enable_spring_boot: "false",
+ enable_php: "false",
+ enable_rust: "false",
+ enable_dotnet: "false",
+ });
+ });
+});
diff --git a/registry/mavrickrishi/modules/auto-start-dev-server/main.tf b/registry/mavrickrishi/modules/auto-start-dev-server/main.tf
new file mode 100644
index 00000000..f5d13941
--- /dev/null
+++ b/registry/mavrickrishi/modules/auto-start-dev-server/main.tf
@@ -0,0 +1,195 @@
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = ">= 2.5"
+ }
+ }
+}
+
+variable "agent_id" {
+ type = string
+ description = "The ID of a Coder agent."
+}
+
+variable "workspace_directory" {
+ type = string
+ description = "The directory to scan for development projects."
+ default = "$HOME"
+}
+
+variable "project_detection" {
+ type = bool
+ description = "Enable automatic project detection for all supported types. When true, all project types are detected unless individually disabled. When false, only explicitly enabled project types are detected."
+ default = true
+}
+
+variable "enable_npm" {
+ type = bool
+ description = "Enable auto-detection and startup of npm projects."
+ default = null
+}
+
+variable "enable_rails" {
+ type = bool
+ description = "Enable auto-detection and startup of Rails projects."
+ default = null
+}
+
+variable "enable_django" {
+ type = bool
+ description = "Enable auto-detection and startup of Django projects."
+ default = null
+}
+
+variable "enable_flask" {
+ type = bool
+ description = "Enable auto-detection and startup of Flask projects."
+ default = null
+}
+
+variable "enable_spring_boot" {
+ type = bool
+ description = "Enable auto-detection and startup of Spring Boot projects."
+ default = null
+}
+
+variable "enable_go" {
+ type = bool
+ description = "Enable auto-detection and startup of Go projects."
+ default = null
+}
+
+variable "enable_php" {
+ type = bool
+ description = "Enable auto-detection and startup of PHP projects."
+ default = null
+}
+
+variable "enable_rust" {
+ type = bool
+ description = "Enable auto-detection and startup of Rust projects."
+ default = null
+}
+
+variable "enable_dotnet" {
+ type = bool
+ description = "Enable auto-detection and startup of .NET projects."
+ default = null
+}
+
+variable "enable_devcontainer" {
+ type = bool
+ description = "Enable integration with devcontainer.json configuration."
+ default = null
+}
+
+variable "log_path" {
+ type = string
+ description = "The path to log development server output to."
+ default = "/tmp/dev-servers.log"
+}
+
+variable "scan_depth" {
+ type = number
+ description = "Maximum directory depth to scan for projects (1-5)."
+ default = 2
+ validation {
+ condition = var.scan_depth >= 1 && var.scan_depth <= 5
+ error_message = "Scan depth must be between 1 and 5."
+ }
+}
+
+variable "startup_delay" {
+ type = number
+ description = "Delay in seconds before starting dev servers (allows other setup to complete)."
+ default = 10
+}
+
+variable "display_name" {
+ type = string
+ description = "Display name for the auto-start dev server script."
+ default = "Auto-Start Dev Servers"
+}
+
+variable "enable_preview_app" {
+ type = bool
+ description = "Enable automatic creation of a preview app for the first detected project."
+ default = true
+}
+
+# Read the detected port from the file written by the script
+locals {
+ detected_port = var.enable_preview_app ? try(tonumber(trimspace(file("/tmp/detected-port.txt"))), 3000) : 3000
+ # Attempt to read project information for better preview naming
+ detected_projects = try(jsondecode(file("/tmp/detected-projects.json")), [])
+ preview_project = length(local.detected_projects) > 0 ? local.detected_projects[0] : null
+}
+
+resource "coder_script" "auto_start_dev_server" {
+ agent_id = var.agent_id
+ display_name = var.display_name
+ icon = "/icon/auto-dev-server.svg"
+ script = templatefile("${path.module}/run.sh", {
+ WORKSPACE_DIR = var.workspace_directory
+ ENABLE_NPM = coalesce(var.enable_npm, var.project_detection)
+ ENABLE_RAILS = coalesce(var.enable_rails, var.project_detection)
+ ENABLE_DJANGO = coalesce(var.enable_django, var.project_detection)
+ ENABLE_FLASK = coalesce(var.enable_flask, var.project_detection)
+ ENABLE_SPRING_BOOT = coalesce(var.enable_spring_boot, var.project_detection)
+ ENABLE_GO = coalesce(var.enable_go, var.project_detection)
+ ENABLE_PHP = coalesce(var.enable_php, var.project_detection)
+ ENABLE_RUST = coalesce(var.enable_rust, var.project_detection)
+ ENABLE_DOTNET = coalesce(var.enable_dotnet, var.project_detection)
+ ENABLE_DEVCONTAINER = coalesce(var.enable_devcontainer, var.project_detection)
+ LOG_PATH = var.log_path
+ SCAN_DEPTH = var.scan_depth
+ STARTUP_DELAY = var.startup_delay
+ })
+ run_on_start = true
+}
+
+# Create preview app for first detected project
+resource "coder_app" "preview" {
+ count = var.enable_preview_app ? 1 : 0
+ agent_id = var.agent_id
+ slug = "dev-preview"
+ display_name = "Live Preview"
+ url = "http://localhost:${local.detected_port}"
+ icon = "/icon/auto-dev-server.svg"
+ subdomain = true
+ share = "owner"
+}
+
+output "log_path" {
+ value = var.log_path
+ description = "Path to the log file for dev server output"
+}
+
+# Example output values for common port mappings
+output "common_ports" {
+ value = {
+ nodejs = 3000
+ rails = 3000
+ django = 8000
+ flask = 5000
+ spring = 8080
+ go = 8080
+ php = 8080
+ rust = 8000
+ dotnet = 5000
+ }
+ description = "Common default ports for different project types"
+}
+
+output "preview_url" {
+ value = var.enable_preview_app ? try(coder_app.preview[0].url, null) : null
+ description = "URL of the live preview app (if enabled)"
+}
+
+output "detected_port" {
+ value = local.detected_port
+ description = "Port of the first detected development server"
+}
diff --git a/registry/mavrickrishi/modules/auto-start-dev-server/run.sh b/registry/mavrickrishi/modules/auto-start-dev-server/run.sh
new file mode 100755
index 00000000..d0386e74
--- /dev/null
+++ b/registry/mavrickrishi/modules/auto-start-dev-server/run.sh
@@ -0,0 +1,468 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+# Color codes for output
+BOLD='\033[0;1m'
+GREEN='\033[0;32m'
+YELLOW='\033[0;33m'
+BLUE='\033[0;34m'
+RED='\033[0;31m'
+RESET='\033[0m'
+
+echo -e "$${BOLD}π Auto-Start Development Servers$${RESET}"
+echo "Workspace Directory: ${WORKSPACE_DIR}"
+echo "Log Path: ${LOG_PATH}"
+echo "Scan Depth: ${SCAN_DEPTH}"
+
+# Wait for startup delay to allow other setup to complete
+if [ "${STARTUP_DELAY}" -gt 0 ]; then
+ echo -e "$${YELLOW}β³ Waiting ${STARTUP_DELAY} seconds for system initialization...$${RESET}"
+ sleep "${STARTUP_DELAY}"
+fi
+
+# Initialize log file
+echo "=== Auto-Start Dev Servers Log ===" > "${LOG_PATH}"
+echo "Started at: $(date)" >> "${LOG_PATH}"
+
+# Initialize detected projects JSON file
+DETECTED_PROJECTS_FILE="/tmp/detected-projects.json"
+echo '[]' > "$DETECTED_PROJECTS_FILE"
+
+# Initialize detected port file for preview app
+DETECTED_PORT_FILE="/tmp/detected-port.txt"
+FIRST_PORT_DETECTED=false
+FRONTEND_PROJECT_DETECTED=false
+
+# Function to log messages
+log_message() {
+ echo -e "$1"
+ echo "$1" >> "${LOG_PATH}"
+}
+
+# Function to determine if a project is likely a frontend project
+is_frontend_project() {
+ local project_dir="$1"
+ local project_type="$2"
+
+ # Check for common frontend indicators
+ if [ "$project_type" = "nodejs" ]; then
+ # Check package.json for frontend dependencies
+ if [ -f "$project_dir/package.json" ] && command -v jq &> /dev/null; then
+ # Check for common frontend frameworks
+ local has_react=$(jq '.dependencies.react // .devDependencies.react // empty' "$project_dir/package.json")
+ local has_vue=$(jq '.dependencies.vue // .devDependencies.vue // empty' "$project_dir/package.json")
+ local has_angular=$(jq '.dependencies["@angular/core"] // .devDependencies["@angular/core"] // empty' "$project_dir/package.json")
+ local has_next=$(jq '.dependencies.next // .devDependencies.next // empty' "$project_dir/package.json")
+ local has_nuxt=$(jq '.dependencies.nuxt // .devDependencies.nuxt // empty' "$project_dir/package.json")
+ local has_svelte=$(jq '.dependencies.svelte // .devDependencies.svelte // empty' "$project_dir/package.json")
+ local has_vite=$(jq '.dependencies.vite // .devDependencies.vite // empty' "$project_dir/package.json")
+
+ if [ -n "$has_react" ] || [ -n "$has_vue" ] || [ -n "$has_angular" ] \
+ || [ -n "$has_next" ] || [ -n "$has_nuxt" ] || [ -n "$has_svelte" ] \
+ || [ -n "$has_vite" ]; then
+ return 0 # It's a frontend project
+ fi
+ fi
+
+ # Check for common frontend directory structures
+ if [ -d "$project_dir/src/components" ] || [ -d "$project_dir/components" ] \
+ || [ -d "$project_dir/pages" ] || [ -d "$project_dir/views" ] \
+ || [ -f "$project_dir/index.html" ] || [ -f "$project_dir/public/index.html" ]; then
+ return 0 # It's likely a frontend project
+ fi
+ fi
+
+ # Rails projects with webpack/webpacker are frontend-enabled
+ if [ "$project_type" = "rails" ]; then
+ if [ -f "$project_dir/config/webpacker.yml" ] || [ -f "$project_dir/webpack.config.js" ]; then
+ return 0
+ fi
+ fi
+
+ # Django projects with static/templates are frontend-enabled
+ if [ "$project_type" = "django" ]; then
+ if [ -d "$project_dir/static" ] || [ -d "$project_dir/templates" ]; then
+ return 0
+ fi
+ fi
+
+ return 1 # Not a frontend project
+}
+
+# Function to add detected project to JSON
+add_detected_project() {
+ local project_dir="$1"
+ local project_type="$2"
+ local port="$3"
+ local command="$4"
+
+ # Check if this is a frontend project
+ local is_frontend=false
+ if is_frontend_project "$project_dir" "$project_type"; then
+ is_frontend=true
+ log_message "$${BLUE}π¨ Detected frontend project at $project_dir$${RESET}"
+ fi
+
+ # Prioritize frontend projects for the preview app
+ # Set port if: 1) No port set yet, OR 2) This is frontend and no frontend detected yet
+ if [ "$FIRST_PORT_DETECTED" = false ] || ([ "$is_frontend" = true ] && [ "$FRONTEND_PROJECT_DETECTED" = false ]); then
+ echo "$port" > "$DETECTED_PORT_FILE"
+ FIRST_PORT_DETECTED=true
+ if [ "$is_frontend" = true ]; then
+ FRONTEND_PROJECT_DETECTED=true
+ log_message "$${BLUE}π― Frontend project detected - Preview app will be available on port $port$${RESET}"
+ else
+ log_message "$${BLUE}π― Project detected - Preview app will be available on port $port$${RESET}"
+ fi
+ fi
+
+ # Create JSON entry for this project
+ local project_json=$(jq -n \
+ --arg dir "$project_dir" \
+ --arg type "$project_type" \
+ --arg port "$port" \
+ --arg cmd "$command" \
+ --arg frontend "$is_frontend" \
+ '{"directory": $dir, "type": $type, "port": $port, "command": $cmd, "is_frontend": ($frontend == "true")}')
+
+ # Append to the detected projects file
+ jq ". += [$project_json]" "$DETECTED_PROJECTS_FILE" > "$DETECTED_PROJECTS_FILE.tmp" \
+ && mv "$DETECTED_PROJECTS_FILE.tmp" "$DETECTED_PROJECTS_FILE"
+}
+
+# Function to detect and start npm/yarn projects
+detect_npm_projects() {
+ if [ "${ENABLE_NPM}" != "true" ]; then
+ return
+ fi
+
+ log_message "$${BLUE}π Scanning for Node.js/npm projects...$${RESET}"
+
+ # Use find with maxdepth to respect scan depth
+ while IFS= read -r -d '' package_json; do
+ project_dir=$(dirname "$package_json")
+ log_message "$${GREEN}π¦ Found Node.js project: $project_dir$${RESET}"
+
+ cd "$project_dir"
+
+ # Check package.json for start script
+ if [ -f "package.json" ] && command -v jq &> /dev/null; then
+ start_script=$(jq -r '.scripts.start // empty' package.json)
+ dev_script=$(jq -r '.scripts.dev // empty' package.json)
+ serve_script=$(jq -r '.scripts.serve // empty' package.json)
+
+ # Determine port (check for common port configurations)
+ local project_port=3000
+ if [ -n "$dev_script" ] && echo "$dev_script" | grep -q "\-\-port"; then
+ project_port=$(echo "$dev_script" | grep -oE "\-\-port[[:space:]]+[0-9]+" | grep -oE "[0-9]+$" || echo "3000")
+ fi
+
+ # Use yarn if yarn.lock exists
+ local pkg_manager="npm"
+ local cmd_prefix=""
+ if [ -f "yarn.lock" ] && command -v yarn &> /dev/null; then
+ pkg_manager="yarn"
+ cmd_prefix=""
+ else
+ cmd_prefix="run "
+ fi
+
+ # Prioritize scripts: 'dev' > 'serve' > 'start' for development environments
+ if [ -n "$dev_script" ]; then
+ if [ "$pkg_manager" = "yarn" ]; then
+ log_message "$${GREEN}π’ Starting project with 'yarn dev' in $project_dir$${RESET}"
+ nohup yarn dev >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "nodejs" "$project_port" "yarn dev"
+ else
+ log_message "$${GREEN}π’ Starting project with 'npm run dev' in $project_dir$${RESET}"
+ nohup npm run dev >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "nodejs" "$project_port" "npm run dev"
+ fi
+ elif [ -n "$serve_script" ]; then
+ if [ "$pkg_manager" = "yarn" ]; then
+ log_message "$${GREEN}π’ Starting project with 'yarn serve' in $project_dir$${RESET}"
+ nohup yarn serve >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "nodejs" "$project_port" "yarn serve"
+ else
+ log_message "$${GREEN}π’ Starting project with 'npm run serve' in $project_dir$${RESET}"
+ nohup npm run serve >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "nodejs" "$project_port" "npm run serve"
+ fi
+ elif [ -n "$start_script" ]; then
+ if [ "$pkg_manager" = "yarn" ]; then
+ log_message "$${GREEN}π’ Starting project with 'yarn start' in $project_dir$${RESET}"
+ nohup yarn start >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "nodejs" "$project_port" "yarn start"
+ else
+ log_message "$${GREEN}π’ Starting project with 'npm start' in $project_dir$${RESET}"
+ nohup npm start >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "nodejs" "$project_port" "npm start"
+ fi
+ fi
+ fi
+
+ done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "package.json" -type f -print0)
+}
+
+# Function to detect and start Rails projects
+detect_rails_projects() {
+ if [ "${ENABLE_RAILS}" != "true" ]; then
+ return
+ fi
+
+ log_message "$${BLUE}π Scanning for Ruby on Rails projects...$${RESET}"
+
+ while IFS= read -r -d '' gemfile; do
+ project_dir=$(dirname "$gemfile")
+ log_message "$${GREEN}π Found Rails project: $project_dir$${RESET}"
+
+ cd "$project_dir"
+
+ # Check if it's actually a Rails project
+ if grep -q "gem ['\"]rails['\"]" Gemfile 2> /dev/null; then
+ log_message "$${GREEN}π’ Starting Rails server in $project_dir$${RESET}"
+ nohup bundle exec rails server >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "rails" "3000" "bundle exec rails server"
+ fi
+
+ done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "Gemfile" -type f -print0)
+}
+
+# Function to detect and start Django projects
+detect_django_projects() {
+ if [ "${ENABLE_DJANGO}" != "true" ]; then
+ return
+ fi
+
+ log_message "$${BLUE}π Scanning for Django projects...$${RESET}"
+
+ while IFS= read -r -d '' manage_py; do
+ project_dir=$(dirname "$manage_py")
+ log_message "$${GREEN}π Found Django project: $project_dir$${RESET}"
+
+ cd "$project_dir"
+ log_message "$${GREEN}π’ Starting Django development server in $project_dir$${RESET}"
+ nohup python manage.py runserver 0.0.0.0:8000 >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "django" "8000" "python manage.py runserver"
+
+ done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "manage.py" -type f -print0)
+}
+
+# Function to detect and start Flask projects
+detect_flask_projects() {
+ if [ "${ENABLE_FLASK}" != "true" ]; then
+ return
+ fi
+
+ log_message "$${BLUE}π Scanning for Flask projects...$${RESET}"
+
+ while IFS= read -r -d '' requirements_txt; do
+ project_dir=$(dirname "$requirements_txt")
+
+ # Check if Flask is in requirements
+ if grep -q -i "flask" "$requirements_txt" 2> /dev/null; then
+ log_message "$${GREEN}πΆοΈ Found Flask project: $project_dir$${RESET}"
+
+ cd "$project_dir"
+
+ # Look for common Flask app files
+ for app_file in app.py main.py run.py; do
+ if [ -f "$app_file" ]; then
+ log_message "$${GREEN}π’ Starting Flask application ($app_file) in $project_dir$${RESET}"
+ export FLASK_ENV=development
+ nohup python "$app_file" >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "flask" "5000" "python $app_file"
+ break
+ fi
+ done
+ fi
+
+ done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "requirements.txt" -type f -print0)
+}
+
+# Function to detect and start Spring Boot projects
+detect_spring_boot_projects() {
+ if [ "${ENABLE_SPRING_BOOT}" != "true" ]; then
+ return
+ fi
+
+ log_message "$${BLUE}π Scanning for Spring Boot projects...$${RESET}"
+
+ # Maven projects
+ while IFS= read -r -d '' pom_xml; do
+ project_dir=$(dirname "$pom_xml")
+
+ # Check if it's a Spring Boot project
+ if grep -q "spring-boot" "$pom_xml" 2> /dev/null; then
+ log_message "$${GREEN}π Found Spring Boot Maven project: $project_dir$${RESET}"
+
+ cd "$project_dir"
+ if command -v ./mvnw &> /dev/null; then
+ log_message "$${GREEN}π’ Starting Spring Boot application with Maven wrapper in $project_dir$${RESET}"
+ nohup ./mvnw spring-boot:run >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "spring-boot" "8080" "./mvnw spring-boot:run"
+ elif command -v mvn &> /dev/null; then
+ log_message "$${GREEN}π’ Starting Spring Boot application with Maven in $project_dir$${RESET}"
+ nohup mvn spring-boot:run >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "spring-boot" "8080" "mvn spring-boot:run"
+ fi
+ fi
+
+ done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "pom.xml" -type f -print0)
+
+ # Gradle projects
+ while IFS= read -r -d '' build_gradle; do
+ project_dir=$(dirname "$build_gradle")
+
+ # Check if it's a Spring Boot project
+ if grep -q "spring-boot" "$build_gradle" 2> /dev/null; then
+ log_message "$${GREEN}π Found Spring Boot Gradle project: $project_dir$${RESET}"
+
+ cd "$project_dir"
+ if command -v ./gradlew &> /dev/null; then
+ log_message "$${GREEN}π’ Starting Spring Boot application with Gradle wrapper in $project_dir$${RESET}"
+ nohup ./gradlew bootRun >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "spring-boot" "8080" "./gradlew bootRun"
+ elif command -v gradle &> /dev/null; then
+ log_message "$${GREEN}π’ Starting Spring Boot application with Gradle in $project_dir$${RESET}"
+ nohup gradle bootRun >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "spring-boot" "8080" "gradle bootRun"
+ fi
+ fi
+
+ done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "build.gradle" -type f -print0)
+}
+
+# Function to detect and start Go projects
+detect_go_projects() {
+ if [ "${ENABLE_GO}" != "true" ]; then
+ return
+ fi
+
+ log_message "$${BLUE}π Scanning for Go projects...$${RESET}"
+
+ while IFS= read -r -d '' go_mod; do
+ project_dir=$(dirname "$go_mod")
+ log_message "$${GREEN}πΉ Found Go project: $project_dir$${RESET}"
+
+ cd "$project_dir"
+
+ # Look for main.go or check if there's a main function
+ if [ -f "main.go" ]; then
+ log_message "$${GREEN}π’ Starting Go application in $project_dir$${RESET}"
+ nohup go run main.go >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "go" "8080" "go run main.go"
+ elif [ -f "cmd/main.go" ]; then
+ log_message "$${GREEN}π’ Starting Go application (cmd/main.go) in $project_dir$${RESET}"
+ nohup go run cmd/main.go >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "go" "8080" "go run cmd/main.go"
+ fi
+
+ done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "go.mod" -type f -print0)
+}
+
+# Function to detect and start PHP projects
+detect_php_projects() {
+ if [ "${ENABLE_PHP}" != "true" ]; then
+ return
+ fi
+
+ log_message "$${BLUE}π Scanning for PHP projects...$${RESET}"
+
+ while IFS= read -r -d '' composer_json; do
+ project_dir=$(dirname "$composer_json")
+ log_message "$${GREEN}π Found PHP project: $project_dir$${RESET}"
+
+ cd "$project_dir"
+
+ # Look for common PHP entry points
+ for entry_file in index.php public/index.php; do
+ if [ -f "$entry_file" ]; then
+ log_message "$${GREEN}π’ Starting PHP development server in $project_dir$${RESET}"
+ nohup php -S 0.0.0.0:8080 -t "$(dirname "$entry_file")" >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "php" "8080" "php -S 0.0.0.0:8080"
+ break
+ fi
+ done
+
+ done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "composer.json" -type f -print0)
+}
+
+# Function to detect and start Rust projects
+detect_rust_projects() {
+ if [ "${ENABLE_RUST}" != "true" ]; then
+ return
+ fi
+
+ log_message "$${BLUE}π Scanning for Rust projects...$${RESET}"
+
+ while IFS= read -r -d '' cargo_toml; do
+ project_dir=$(dirname "$cargo_toml")
+ log_message "$${GREEN}π¦ Found Rust project: $project_dir$${RESET}"
+
+ cd "$project_dir"
+
+ # Check if it's a binary project (has [[bin]] or default main.rs)
+ if grep -q "\[\[bin\]\]" Cargo.toml 2> /dev/null || [ -f "src/main.rs" ]; then
+ log_message "$${GREEN}π’ Starting Rust application in $project_dir$${RESET}"
+ nohup cargo run >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "rust" "8000" "cargo run"
+ fi
+
+ done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "Cargo.toml" -type f -print0)
+}
+
+# Function to detect and start .NET projects
+detect_dotnet_projects() {
+ if [ "${ENABLE_DOTNET}" != "true" ]; then
+ return
+ fi
+
+ log_message "$${BLUE}π Scanning for .NET projects...$${RESET}"
+
+ while IFS= read -r -d '' csproj; do
+ project_dir=$(dirname "$csproj")
+ log_message "$${GREEN}π· Found .NET project: $project_dir$${RESET}"
+
+ cd "$project_dir"
+ log_message "$${GREEN}π’ Starting .NET application in $project_dir$${RESET}"
+ nohup dotnet run >> "${LOG_PATH}" 2>&1 &
+ add_detected_project "$project_dir" "dotnet" "5000" "dotnet run"
+
+ done < <(find "${WORKSPACE_DIR}" -maxdepth "${SCAN_DEPTH}" -name "*.csproj" -type f -print0)
+}
+
+log_message "Starting auto-detection of development projects..."
+
+# Expand workspace directory if it contains variables
+WORKSPACE_DIR=$(eval echo "${WORKSPACE_DIR}")
+
+# Check if workspace directory exists
+if [ ! -d "$WORKSPACE_DIR" ]; then
+ log_message "$${RED}β Workspace directory does not exist: $WORKSPACE_DIR$${RESET}"
+ exit 1
+fi
+
+cd "$WORKSPACE_DIR"
+
+# Run all detection functions
+detect_npm_projects
+detect_rails_projects
+detect_django_projects
+detect_flask_projects
+detect_spring_boot_projects
+detect_go_projects
+detect_php_projects
+detect_rust_projects
+detect_dotnet_projects
+
+log_message "$${GREEN}β
Auto-start scan completed!$${RESET}"
+log_message "$${YELLOW}π‘ Check running processes with 'ps aux | grep -E \"(npm|rails|python|java|go|php|cargo|dotnet)\"'$${RESET}"
+log_message "$${YELLOW}π‘ View logs: tail -f ${LOG_PATH}$${RESET}"
+
+# Set default port if no projects were detected
+if [ "$FIRST_PORT_DETECTED" = false ]; then
+ echo "3000" > "$DETECTED_PORT_FILE"
+ log_message "$${YELLOW}β οΈ No projects detected - Preview app will default to port 3000$${RESET}"
+fi
diff --git a/registry/mavrickrishi/modules/nexus-repository/README.md b/registry/mavrickrishi/modules/nexus-repository/README.md
new file mode 100644
index 00000000..6bf5431c
--- /dev/null
+++ b/registry/mavrickrishi/modules/nexus-repository/README.md
@@ -0,0 +1,149 @@
+---
+display_name: Nexus Repository
+description: Configure package managers to use Sonatype Nexus Repository for Maven, npm, PyPI, and Docker registries.
+icon: ../../../../.icons/nexus-repository.svg
+verified: false
+tags: [integration, nexus-repository, maven, npm, pypi, docker]
+---
+
+# Sonatype Nexus Repository
+
+Configure package managers (Maven, npm, Go, PyPI, Docker) to use [Sonatype Nexus Repository](https://help.sonatype.com/en/sonatype-nexus-repository.html) with API token authentication. This module provides secure credential handling, multiple repository support per package manager, and flexible username configuration.
+
+```tf
+module "nexus_repository" {
+ source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
+ version = "1.0.1"
+ agent_id = coder_agent.example.id
+ nexus_url = "https://nexus.example.com"
+ nexus_password = var.nexus_api_token
+ package_managers = {
+ maven = ["maven-public", "maven-releases"]
+ npm = ["npm-public", "@scoped:npm-private"]
+ go = ["go-public", "go-private"]
+ pypi = ["pypi-public", "pypi-private"]
+ docker = ["docker-public", "docker-private"]
+ }
+}
+```
+
+## Requirements
+
+- Nexus Repository Manager 3.x
+- Valid API token or user credentials
+- Package managers installed on the workspace (Maven, npm, Go, pip, Docker as needed)
+
+> [!NOTE]
+> This module configures package managers but does not install them. You need to handle the installation of Maven, npm, Go, Python pip, and Docker yourself.
+
+## Examples
+
+### Configure Maven to use Nexus repositories
+
+```tf
+module "nexus_repository" {
+ source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
+ version = "1.0.1"
+ agent_id = coder_agent.example.id
+ nexus_url = "https://nexus.example.com"
+ nexus_password = var.nexus_api_token
+ package_managers = {
+ maven = ["maven-public", "maven-releases", "maven-snapshots"]
+ }
+}
+```
+
+### Configure npm with scoped packages
+
+```tf
+module "nexus_repository" {
+ source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
+ version = "1.0.1"
+ agent_id = coder_agent.example.id
+ nexus_url = "https://nexus.example.com"
+ nexus_password = var.nexus_api_token
+ package_managers = {
+ npm = ["npm-public", "@mycompany:npm-private"]
+ }
+}
+```
+
+### Configure Go module proxy
+
+```tf
+module "nexus_repository" {
+ source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
+ version = "1.0.1"
+ agent_id = coder_agent.example.id
+ nexus_url = "https://nexus.example.com"
+ nexus_password = var.nexus_api_token
+ package_managers = {
+ go = ["go-public", "go-private"]
+ }
+}
+```
+
+### Configure Python PyPI repositories
+
+```tf
+module "nexus_repository" {
+ source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
+ version = "1.0.1"
+ agent_id = coder_agent.example.id
+ nexus_url = "https://nexus.example.com"
+ nexus_password = var.nexus_api_token
+ package_managers = {
+ pypi = ["pypi-public", "pypi-private"]
+ }
+}
+```
+
+### Configure Docker registries
+
+```tf
+module "nexus_repository" {
+ source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
+ version = "1.0.1"
+ agent_id = coder_agent.example.id
+ nexus_url = "https://nexus.example.com"
+ nexus_password = var.nexus_api_token
+ package_managers = {
+ docker = ["docker-public", "docker-private"]
+ }
+}
+```
+
+### Use custom username
+
+```tf
+module "nexus_repository" {
+ source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
+ version = "1.0.1"
+ agent_id = coder_agent.example.id
+ nexus_url = "https://nexus.example.com"
+ nexus_username = "custom-user"
+ nexus_password = var.nexus_api_token
+ package_managers = {
+ maven = ["maven-public"]
+ }
+}
+```
+
+### Complete configuration for all package managers
+
+```tf
+module "nexus_repository" {
+ source = "registry.coder.com/mavrickrishi/nexus-repository/coder"
+ version = "1.0.1"
+ agent_id = coder_agent.example.id
+ nexus_url = "https://nexus.example.com"
+ nexus_password = var.nexus_api_token
+ package_managers = {
+ maven = ["maven-public", "maven-releases"]
+ npm = ["npm-public", "@company:npm-private"]
+ go = ["go-public", "go-private"]
+ pypi = ["pypi-public", "pypi-private"]
+ docker = ["docker-public", "docker-private"]
+ }
+}
+```
diff --git a/registry/mavrickrishi/modules/nexus-repository/main.test.ts b/registry/mavrickrishi/modules/nexus-repository/main.test.ts
new file mode 100644
index 00000000..1d5724bb
--- /dev/null
+++ b/registry/mavrickrishi/modules/nexus-repository/main.test.ts
@@ -0,0 +1,147 @@
+import { describe, expect, it } from "bun:test";
+import {
+ executeScriptInContainer,
+ runTerraformApply,
+ runTerraformInit,
+ testRequiredVariables,
+} from "~test";
+
+describe("nexus-repository", async () => {
+ await runTerraformInit(import.meta.dir);
+
+ testRequiredVariables(import.meta.dir, {
+ agent_id: "test-agent",
+ nexus_url: "https://nexus.example.com",
+ nexus_password: "test-password",
+ });
+
+ it("configures Maven settings", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ nexus_url: "https://nexus.example.com",
+ nexus_password: "test-token",
+ package_managers: JSON.stringify({
+ maven: ["maven-public"],
+ }),
+ });
+
+ const output = await executeScriptInContainer(state, "ubuntu:20.04");
+ expect(output.stdout.join("\n")).toContain("β Configuring Maven...");
+ expect(output.stdout.join("\n")).toContain("π₯³ Configuration complete!");
+ });
+
+ it("configures npm registry", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ nexus_url: "https://nexus.example.com",
+ nexus_password: "test-token",
+ package_managers: JSON.stringify({
+ npm: ["npm-public"],
+ }),
+ });
+
+ const output = await executeScriptInContainer(state, "ubuntu:20.04");
+ expect(output.stdout.join("\n")).toContain("π¦ Configuring npm...");
+ expect(output.stdout.join("\n")).toContain("π₯³ Configuration complete!");
+ });
+
+ it("configures PyPI repository", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ nexus_url: "https://nexus.example.com",
+ nexus_password: "test-token",
+ package_managers: JSON.stringify({
+ pypi: ["pypi-public"],
+ }),
+ });
+
+ const output = await executeScriptInContainer(state, "ubuntu:20.04");
+ expect(output.stdout.join("\n")).toContain("π Configuring pip...");
+ expect(output.stdout.join("\n")).toContain("π₯³ Configuration complete!");
+ });
+
+ it("configures multiple package managers", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ nexus_url: "https://nexus.example.com",
+ nexus_password: "test-token",
+ package_managers: JSON.stringify({
+ maven: ["maven-public"],
+ npm: ["npm-public"],
+ pypi: ["pypi-public"],
+ }),
+ });
+
+ const output = await executeScriptInContainer(state, "ubuntu:20.04");
+ expect(output.stdout.join("\n")).toContain("β Configuring Maven...");
+ expect(output.stdout.join("\n")).toContain("π¦ Configuring npm...");
+ expect(output.stdout.join("\n")).toContain("π Configuring pip...");
+ expect(output.stdout.join("\n")).toContain(
+ "β
Nexus repository configuration completed!",
+ );
+ });
+
+ it("handles empty package managers", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ nexus_url: "https://nexus.example.com",
+ nexus_password: "test-token",
+ package_managers: JSON.stringify({}),
+ });
+
+ const output = await executeScriptInContainer(state, "ubuntu:20.04");
+ expect(output.stdout.join("\n")).toContain(
+ "π€ no maven repository is set, skipping maven configuration.",
+ );
+ expect(output.stdout.join("\n")).toContain(
+ "π€ no npm repository is set, skipping npm configuration.",
+ );
+ expect(output.stdout.join("\n")).toContain(
+ "π€ no pypi repository is set, skipping pypi configuration.",
+ );
+ expect(output.stdout.join("\n")).toContain(
+ "π€ no docker repository is set, skipping docker configuration.",
+ );
+ });
+
+ it("configures Go module proxy", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ nexus_url: "https://nexus.example.com",
+ nexus_password: "test-token",
+ package_managers: JSON.stringify({
+ go: ["go-public", "go-private"],
+ }),
+ });
+
+ const output = await executeScriptInContainer(state, "ubuntu:20.04");
+ expect(output.stdout.join("\n")).toContain("πΉ Configuring Go...");
+ expect(output.stdout.join("\n")).toContain(
+ "Go proxy configured via GOPROXY environment variable",
+ );
+ expect(output.stdout.join("\n")).toContain("π₯³ Configuration complete!");
+ });
+
+ it("validates nexus_url format", async () => {
+ await expect(
+ runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ nexus_url: "invalid-url",
+ nexus_password: "test-token",
+ package_managers: JSON.stringify({}),
+ }),
+ ).rejects.toThrow();
+ });
+
+ it("validates username_field values", async () => {
+ await expect(
+ runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ nexus_url: "https://nexus.example.com",
+ nexus_password: "test-token",
+ username_field: "invalid",
+ package_managers: JSON.stringify({}),
+ }),
+ ).rejects.toThrow();
+ });
+});
diff --git a/registry/mavrickrishi/modules/nexus-repository/main.tf b/registry/mavrickrishi/modules/nexus-repository/main.tf
new file mode 100644
index 00000000..be573121
--- /dev/null
+++ b/registry/mavrickrishi/modules/nexus-repository/main.tf
@@ -0,0 +1,137 @@
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = ">= 2.5"
+ }
+ }
+}
+
+variable "nexus_url" {
+ type = string
+ description = "The base URL of your Nexus repository manager (e.g. https://nexus.example.com)"
+ validation {
+ condition = can(regex("^(https|http)://", var.nexus_url))
+ error_message = "nexus_url must be a valid URL starting with either 'https://' or 'http://'"
+ }
+}
+
+variable "nexus_username" {
+ type = string
+ description = "Custom username for Nexus authentication. If not provided, defaults to the Coder username based on the username_field setting"
+ default = null
+}
+
+variable "nexus_password" {
+ type = string
+ description = "API token or password for Nexus authentication. This value is sensitive and should be stored securely"
+ sensitive = true
+}
+
+variable "agent_id" {
+ type = string
+ description = "The ID of a Coder agent."
+}
+
+variable "package_managers" {
+ type = object({
+ maven = optional(list(string), [])
+ npm = optional(list(string), [])
+ go = optional(list(string), [])
+ pypi = optional(list(string), [])
+ docker = optional(list(string), [])
+ })
+ default = {
+ maven = []
+ npm = []
+ go = []
+ pypi = []
+ docker = []
+ }
+ description = <<-EOF
+ Configuration for package managers. Each key maps to a list of Nexus repository names:
+ - maven: List of Maven repository names
+ - npm: List of npm repository names (supports scoped packages with "@scope:repo-name")
+ - go: List of Go proxy repository names
+ - pypi: List of PyPI repository names
+ - docker: List of Docker registry names
+ Unused package managers can be omitted.
+ Example:
+ {
+ maven = ["maven-public", "maven-releases"]
+ npm = ["npm-public", "@scoped:npm-private"]
+ go = ["go-public", "go-private"]
+ pypi = ["pypi-public", "pypi-private"]
+ docker = ["docker-public", "docker-private"]
+ }
+ EOF
+}
+
+variable "username_field" {
+ type = string
+ description = "Field to use for username (\"username\" or \"email\"). Defaults to \"username\". Only used when nexus_username is not provided"
+ default = "username"
+ validation {
+ condition = can(regex("^(email|username)$", var.username_field))
+ error_message = "username_field must be either 'email' or 'username'"
+ }
+}
+
+data "coder_workspace" "me" {}
+data "coder_workspace_owner" "me" {}
+
+locals {
+ username = coalesce(var.nexus_username, var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name)
+ nexus_host = split("/", replace(replace(var.nexus_url, "https://", ""), "http://", ""))[0]
+}
+
+locals {
+ # Get first repository name or use default
+ maven_repo = length(var.package_managers.maven) > 0 ? var.package_managers.maven[0] : "maven-public"
+ npm_repo = length(var.package_managers.npm) > 0 ? var.package_managers.npm[0] : "npm-public"
+ go_repo = length(var.package_managers.go) > 0 ? var.package_managers.go[0] : "go-public"
+ pypi_repo = length(var.package_managers.pypi) > 0 ? var.package_managers.pypi[0] : "pypi-public"
+
+ npmrc = <<-EOF
+registry=${var.nexus_url}/repository/${local.npm_repo}/
+//${local.nexus_host}/repository/${local.npm_repo}/:username=${local.username}
+//${local.nexus_host}/repository/${local.npm_repo}/:_password=${base64encode(var.nexus_password)}
+//${local.nexus_host}/repository/${local.npm_repo}/:always-auth=true
+EOF
+}
+
+resource "coder_script" "nexus" {
+ agent_id = var.agent_id
+ display_name = "nexus-repository"
+ icon = "/icon/nexus-repository.svg"
+ script = templatefile("${path.module}/run.sh", {
+ NEXUS_URL = var.nexus_url
+ NEXUS_HOST = local.nexus_host
+ NEXUS_USERNAME = local.username
+ NEXUS_PASSWORD = var.nexus_password
+ HAS_MAVEN = length(var.package_managers.maven) == 0 ? "" : "YES"
+ MAVEN_REPO = local.maven_repo
+ HAS_NPM = length(var.package_managers.npm) == 0 ? "" : "YES"
+ NPMRC = local.npmrc
+ HAS_GO = length(var.package_managers.go) == 0 ? "" : "YES"
+ GO_REPO = local.go_repo
+ HAS_PYPI = length(var.package_managers.pypi) == 0 ? "" : "YES"
+ PYPI_REPO = local.pypi_repo
+ HAS_DOCKER = length(var.package_managers.docker) == 0 ? "" : "YES"
+ REGISTER_DOCKER = join("\n ", formatlist("register_docker \"%s\"", var.package_managers.docker))
+ })
+ run_on_start = true
+}
+
+resource "coder_env" "goproxy" {
+ count = length(var.package_managers.go) == 0 ? 0 : 1
+ agent_id = var.agent_id
+ name = "GOPROXY"
+ value = join(",", [
+ for repo in var.package_managers.go :
+ "https://${local.username}:${var.nexus_password}@${local.nexus_host}/repository/${repo}"
+ ])
+}
+
diff --git a/registry/mavrickrishi/modules/nexus-repository/run.sh b/registry/mavrickrishi/modules/nexus-repository/run.sh
new file mode 100644
index 00000000..b860c4bc
--- /dev/null
+++ b/registry/mavrickrishi/modules/nexus-repository/run.sh
@@ -0,0 +1,105 @@
+#!/usr/bin/env bash
+
+not_configured() {
+ type=$1
+ echo "π€ no $type repository is set, skipping $type configuration."
+}
+
+config_complete() {
+ echo "π₯³ Configuration complete!"
+}
+
+register_docker() {
+ repo=$1
+ echo -n "${NEXUS_PASSWORD}" | docker login "${NEXUS_HOST}/repository/$${repo}" --username "${NEXUS_USERNAME}" --password-stdin
+}
+
+echo "π Configuring Nexus repository access..."
+
+# Configure Maven
+if [ -n "${HAS_MAVEN}" ]; then
+ echo "β Configuring Maven..."
+ mkdir -p ~/.m2
+ cat > ~/.m2/settings.xml << 'EOF'
+
+
+
+
+ nexus
+ ${NEXUS_USERNAME}
+ ${NEXUS_PASSWORD}
+
+
+
+
+ nexus-mirror
+ *
+ ${NEXUS_URL}/repository/${MAVEN_REPO}
+
+
+
+EOF
+ config_complete
+else
+ not_configured maven
+fi
+
+# Configure npm
+if [ -n "${HAS_NPM}" ]; then
+ echo "π¦ Configuring npm..."
+ cat > ~/.npmrc << 'EOF'
+${NPMRC}
+EOF
+ config_complete
+else
+ not_configured npm
+fi
+
+# Configure Go
+if [ -n "${HAS_GO}" ]; then
+ echo "πΉ Configuring Go..."
+ # Go configuration is handled via GOPROXY environment variable
+ # which is set by the Terraform configuration
+ echo "Go proxy configured via GOPROXY environment variable"
+ config_complete
+else
+ not_configured go
+fi
+
+# Configure pip
+if [ -n "${HAS_PYPI}" ]; then
+ echo "π Configuring pip..."
+ mkdir -p ~/.pip
+ # Create .netrc file for secure credential storage
+ cat > ~/.netrc << EOF
+machine ${NEXUS_HOST}
+login ${NEXUS_USERNAME}
+password ${NEXUS_PASSWORD}
+EOF
+ chmod 600 ~/.netrc
+
+ # Update pip.conf to use index-url without embedded credentials
+ cat > ~/.pip/pip.conf << 'EOF'
+[global]
+index-url = https://${NEXUS_HOST}/repository/${PYPI_REPO}/simple
+EOF
+ config_complete
+else
+ not_configured pypi
+fi
+
+# Configure Docker
+if [ -n "${HAS_DOCKER}" ]; then
+ if command -v docker > /dev/null 2>&1; then
+ echo "π³ Configuring Docker credentials..."
+ mkdir -p ~/.docker
+ ${REGISTER_DOCKER}
+ config_complete
+ else
+ echo "π€ Docker is not installed, skipping Docker configuration."
+ fi
+else
+ not_configured docker
+fi
+
+echo "β
Nexus repository configuration completed!"