Compare commits

...

29 Commits

Author SHA1 Message Date
Atif Ali 68f881e220 chore: update Sourcegraph AMP to Amp CLI (#382)
## Description

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

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

## Type of Change

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

## Module Information

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

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

## Testing & Validation

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

## Related Issues

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

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

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

## Type of Change

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

## Module Information

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

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

## Testing & Validation

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

## Related Issues

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

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

## Type of Change

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

## Module Information

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

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

## Testing & Validation

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

## Related Issues

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

---------

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

## Description

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

## Type of Change

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

## Testing & Validation

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

---------

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

- Removed variables for hardcoded configuration options, and replaced
with variables for base config, and additional mcp servers.
- Set module defaults so that this will run with minimal module
configuration for tasks, while allowing further configuration if needed
by the user for codex through the base configuration.
- Updated tests for expected responses and new configuration options.
- Move all codex related files outside of project folder.
<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

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

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

## Testing & Validation

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

## Related Issues

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

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-08-21 10:56:09 -05:00
Atif Ali eca289be3a chore: set coder-labs publisher as official (#340)
@Parkreiner, please double-check if it will work and not fail any
validation at build time.
2025-08-21 09:14:14 -05:00
Ben Potter 7aa75f9451 fix: use correct app slug for status reporting (#344)
## Description

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

## Type of Change

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

## Description

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

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

## Type of Change

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

## Module Information

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

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

## Testing & Validation

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

## Related Issues

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

---------

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

## Description

Adds a Proxmox VM template 

## Type of Change

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

## Testing & Validation

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


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

---------

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

       🔍 Scanning all modules for missing release tags...

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

       📊 Summary: 0 of 54 modules need tagging

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

## After

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

🔍 Scanning all modules for missing release tags...

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

📊 Summary: 0 of 49 modules need tagging

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

## Description

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

## Type of Change

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

## Module Information

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

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

## Testing & Validation

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

## Related Issues

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

---------

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

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

## Type of Change

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

## Testing & Validation

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

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

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

---------

Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: 35C4n0r <work.jaykumar@gmail.com>
Co-authored-by: 35C4n0r <70096901+35C4n0r@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-18 13:08:48 -05:00
blink-so[bot] bb667d2209 fix(tag_release): improve macOS and Linux compatibility (#335)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2025-08-18 19:09:10 +05:00
dependabot[bot] f08bb30b53 chore(deps): bump actions/checkout from 4 to 5 (#334)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-18 12:25:16 +00:00
dependabot[bot] 32b039a838 chore(deps): bump crate-ci/typos from 1.35.3 to 1.35.4 (#333)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-18 12:22:19 +00:00
57 changed files with 4652 additions and 75 deletions
+4
View File
@@ -4,3 +4,7 @@ updates:
directory: "/"
schedule:
interval: "weekly"
groups:
github-actions:
patterns:
- "*"
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Run check.sh
run: |
+4 -4
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Set up Terraform
uses: coder/coder/.github/actions/setup-tf@main
- name: Set up Bun
@@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
@@ -48,7 +48,7 @@ jobs:
- name: Validate formatting
run: bun fmt:ci
- name: Check for typos
uses: crate-ci/typos@v1.35.3
uses: crate-ci/typos@v1.35.5
with:
config: .github/typos.toml
validate-readme-files:
@@ -59,7 +59,7 @@ jobs:
needs: validate-style
steps:
- name: Check out code
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v5
with:
+2 -2
View File
@@ -28,14 +28,14 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Authenticate with Google Cloud
uses: google-github-actions/auth@b7593ed2efd1c1617e1b0254da33b86225adb2a5
with:
workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github
service_account: registry-v2-github@coder-registry-1.iam.gserviceaccount.com
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@cb1e50a9932213ecece00a606661ae9ca44f3397
uses: google-github-actions/setup-gcloud@26f734c2779b00b7dda794207734c511110a4368
- name: Deploy to dev.registry.coder.com
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch main
- name: Deploy to registry.coder.com
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
with:
go-version: stable
+112
View File
@@ -0,0 +1,112 @@
name: Create Release
on:
push:
tags:
- "release/*/*/v*.*.*"
jobs:
create-release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
- name: Extract tag information
id: tag_info
run: |
TAG=${GITHUB_REF#refs/tags/}
echo "tag=$TAG" >> $GITHUB_OUTPUT
IFS='/' read -ra PARTS <<< "$TAG"
NAMESPACE="${PARTS[1]}"
MODULE="${PARTS[2]}"
VERSION="${PARTS[3]}"
echo "namespace=$NAMESPACE" >> $GITHUB_OUTPUT
echo "module=$MODULE" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "module_path=registry/$NAMESPACE/modules/$MODULE" >> $GITHUB_OUTPUT
RELEASE_TITLE="$NAMESPACE/$MODULE $VERSION"
echo "release_title=$RELEASE_TITLE" >> $GITHUB_OUTPUT
- name: Find previous tag
id: prev_tag
env:
NAMESPACE: ${{ steps.tag_info.outputs.namespace }}
MODULE: ${{ steps.tag_info.outputs.module }}
CURRENT_TAG: ${{ steps.tag_info.outputs.tag }}
run: |
PREV_TAG=$(git tag -l "release/$NAMESPACE/$MODULE/v*" | sort -V | grep -B1 "$CURRENT_TAG" | head -1)
if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "$CURRENT_TAG" ]; then
echo "No previous tag found, using initial commit"
PREV_TAG=$(git rev-list --max-parents=0 HEAD)
fi
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
echo "Previous tag: $PREV_TAG"
- name: Generate changelog
id: changelog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MODULE_PATH: ${{ steps.tag_info.outputs.module_path }}
PREV_TAG: ${{ steps.prev_tag.outputs.prev_tag }}
CURRENT_TAG: ${{ steps.tag_info.outputs.tag }}
run: |
echo "Generating changelog for $MODULE_PATH between $PREV_TAG and $CURRENT_TAG"
COMMITS=$(git log --oneline --no-merges "$PREV_TAG..$CURRENT_TAG" -- "$MODULE_PATH")
if [ -z "$COMMITS" ]; then
echo "No commits found for this module"
echo "changelog=No changes found for this module." >> $GITHUB_OUTPUT
exit 0
fi
FULL_CHANGELOG=$(gh api repos/:owner/:repo/releases/generate-notes \
--field tag_name="$CURRENT_TAG" \
--field previous_tag_name="$PREV_TAG" \
--jq '.body')
MODULE_COMMIT_SHAS=$(git log --format="%H" --no-merges "$PREV_TAG..$CURRENT_TAG" -- "$MODULE_PATH")
FILTERED_CHANGELOG="## What's Changed\n\n"
for sha in $MODULE_COMMIT_SHAS; do
SHORT_SHA=${sha:0:7}
COMMIT_LINES=$(echo "$FULL_CHANGELOG" | grep -E "$SHORT_SHA|$(git log --format='%s' -n 1 $sha)" || true)
if [ -n "$COMMIT_LINES" ]; then
FILTERED_CHANGELOG="${FILTERED_CHANGELOG}${COMMIT_LINES}\n"
else
COMMIT_MSG=$(git log --format="%s" -n 1 $sha)
AUTHOR=$(gh api repos/:owner/:repo/commits/$sha --jq '.author.login // .commit.author.name')
FILTERED_CHANGELOG="${FILTERED_CHANGELOG}* $COMMIT_MSG by @$AUTHOR\n"
fi
done
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo -e "$FILTERED_CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ steps.tag_info.outputs.tag }}
RELEASE_TITLE: ${{ steps.tag_info.outputs.release_title }}
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
run: |
gh release create "$TAG_NAME" \
--title "$RELEASE_TITLE" \
--notes "$CHANGELOG"
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
issues: write
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
+8
View File
@@ -0,0 +1,8 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="25" height="25"/>
<path d="M5.50694 21.1637C5.17323 21.1637 4.89218 21.1064 4.66378 20.9926C4.436 20.8787 4.26333 20.7052 4.1476 20.4763C4.03187 20.2473 3.97247 19.9672 3.97247 19.6382V16.2463C3.97247 15.8281 3.88859 15.5239 3.72265 15.3341C3.55549 15.1449 3.25912 15.0449 2.83418 15.0353C2.70191 15.0353 2.59598 14.9859 2.51577 14.8853C2.43433 14.7859 2.39453 14.6708 2.39453 14.5425C2.39453 14.4033 2.43433 14.2882 2.51577 14.1984C2.5966 14.1087 2.70375 14.0593 2.83418 14.0496C3.25912 14.0394 3.55549 13.94 3.72265 13.7508C3.88981 13.5616 3.97247 13.2622 3.97247 12.8537V9.46177C3.97247 8.96352 4.10474 8.58456 4.36742 8.3261C4.6301 8.06763 5.01035 7.9375 5.50694 7.9375H9.55926C9.71173 7.9375 9.83725 7.98269 9.9389 8.07185C10.0399 8.16162 10.0914 8.27669 10.0914 8.41466C10.0914 8.5448 10.0485 8.65626 9.96278 8.75145C9.87706 8.84664 9.76316 8.89423 9.62049 8.89423H5.8578C5.6441 8.89423 5.48245 8.94906 5.37162 9.05871C5.26018 9.16836 5.20446 9.33766 5.20446 9.5678V12.9754C5.20446 13.2742 5.14323 13.5454 5.02199 13.7894C4.90075 14.034 4.73848 14.2256 4.53581 14.3659C4.33313 14.5051 4.09616 14.575 3.82246 14.575V14.5147C4.09616 14.5147 4.33313 14.5846 4.53581 14.7238C4.73848 14.863 4.90075 15.0552 5.02199 15.3004C5.14323 15.5444 5.20446 15.8155 5.20446 16.1143V19.537C5.20446 19.7671 5.26018 19.9358 5.37162 20.0461C5.48306 20.1569 5.64533 20.2106 5.8578 20.2106H9.62049C9.76194 20.2106 9.87583 20.2582 9.96278 20.3527C10.0497 20.4479 10.0914 20.56 10.0914 20.6895C10.0914 20.8191 10.0412 20.9299 9.9389 21.0251C9.83725 21.1203 9.71112 21.1673 9.55926 21.1673H5.50694V21.1643V21.1637Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
<path d="M15.4423 21.1634C15.2898 21.1634 15.1643 21.1158 15.0626 21.0212C14.961 20.926 14.9102 20.8139 14.9102 20.6856C14.9102 20.5573 14.953 20.444 15.0387 20.3488C15.1245 20.2536 15.2384 20.2067 15.381 20.2067H19.1437C19.3574 20.2067 19.5191 20.153 19.6299 20.0422C19.7413 19.9325 19.7971 19.7632 19.7971 19.5331V16.1104C19.7971 15.8116 19.8583 15.5405 19.9795 15.2965C20.1008 15.0519 20.263 14.8603 20.4657 14.7199C20.6684 14.5807 20.9054 14.5108 21.1791 14.5108V14.5711C20.9054 14.5711 20.6684 14.5012 20.4657 14.362C20.263 14.2229 20.1008 14.0307 19.9795 13.7855C19.8583 13.5415 19.7971 13.2703 19.7971 12.9715V9.5639C19.7971 9.33496 19.7413 9.16566 19.6299 9.0548C19.5185 8.94515 19.3562 8.89033 19.1437 8.89033H15.381C15.2396 8.89033 15.1257 8.84273 15.0387 8.74754C14.953 8.65355 14.9102 8.54089 14.9102 8.41076C14.9102 8.27158 14.9604 8.15771 15.0626 8.06795C15.1637 7.97818 15.2898 7.93359 15.4423 7.93359H19.4946C19.9912 7.93359 20.3702 8.06373 20.6341 8.32219C20.898 8.58065 21.029 8.95961 21.029 9.45786V12.8498C21.029 13.2583 21.1129 13.5583 21.2789 13.7469C21.446 13.9361 21.7424 14.0361 22.1673 14.0457C22.2996 14.0554 22.4055 14.1048 22.4858 14.1945C22.5672 14.2843 22.607 14.3994 22.607 14.5385C22.607 14.6687 22.5672 14.7826 22.4858 14.8814C22.4055 14.9808 22.2978 15.0314 22.1673 15.0314C21.7424 15.041 21.4466 15.141 21.2789 15.3302C21.1117 15.5194 21.029 15.823 21.029 16.2424V19.6343C21.029 19.9639 20.9709 20.2422 20.8539 20.4723C20.737 20.7025 20.5655 20.8736 20.3377 20.9887C20.1093 21.1025 19.8283 21.1598 19.4946 21.1598H15.4423V21.1628V21.1634Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
<path d="M16.4845 15.8401C17.2224 15.8401 17.8206 15.2515 17.8206 14.5255C17.8206 13.7996 17.2224 13.2109 16.4845 13.2109C15.7467 13.2109 15.1484 13.7996 15.1484 14.5255C15.1484 15.2515 15.7467 15.8401 16.4845 15.8401Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
<path d="M9.00014 15.8401C9.73798 15.8401 10.3362 15.2515 10.3362 14.5255C10.3362 13.7996 9.73798 13.2109 9.00014 13.2109C8.2623 13.2109 7.66406 13.7996 7.66406 14.5255C7.66406 15.2515 8.2623 15.8401 9.00014 15.8401Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
<path d="M12.0442 4.13327L11.942 6.81971C11.942 6.97033 11.7974 7.04564 11.5084 7.04564C11.2194 7.04564 11.0749 6.97033 11.0749 6.81971C11.0492 6.15036 11.0284 5.63103 11.0112 5.26291C11.0027 4.88637 10.9941 4.61826 10.9855 4.45921C10.9769 4.30016 10.9727 4.20376 10.9727 4.17062V4.12062C10.9727 3.92843 11.1515 3.83203 11.5084 3.83203C11.8654 3.83203 12.0442 3.93264 12.0442 4.13327ZM14.213 4.13327L14.1108 6.81971C14.1108 6.97033 13.9663 7.04564 13.6773 7.04564C13.3883 7.04564 13.2437 6.97033 13.2437 6.81971C13.218 6.15036 13.1972 5.63103 13.1801 5.26291C13.1715 4.88637 13.1629 4.61826 13.1543 4.45921C13.1458 4.30016 13.1415 4.20376 13.1415 4.17062V4.12062C13.1415 3.92843 13.3203 3.83203 13.6773 3.83203C14.0342 3.83203 14.213 3.93264 14.213 4.13327Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

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

After

Width:  |  Height:  |  Size: 2.9 KiB

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

After

Width:  |  Height:  |  Size: 12 KiB

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

After

Width:  |  Height:  |  Size: 576 B

+17
View File
@@ -4,6 +4,7 @@ import (
"errors"
"os"
"path"
"regexp"
"slices"
"strings"
@@ -12,6 +13,10 @@ import (
var supportedUserNameSpaceDirectories = append(supportedResourceTypes, ".images")
// validNameRe validates that names contain only alphanumeric characters and hyphens
var validNameRe = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$`)
// validateCoderResourceSubdirectory validates that the structure of a module or template within a namespace follows all
// expected file conventions
func validateCoderResourceSubdirectory(dirPath string) []error {
@@ -42,6 +47,12 @@ func validateCoderResourceSubdirectory(dirPath string) []error {
continue
}
// Validate module/template name
if !validNameRe.MatchString(f.Name()) {
errs = append(errs, xerrors.Errorf("%q: name contains invalid characters (only alphanumeric characters and hyphens are allowed)", path.Join(dirPath, f.Name())))
continue
}
resourceReadmePath := path.Join(dirPath, f.Name(), "README.md")
if _, err := os.Stat(resourceReadmePath); err != nil {
if errors.Is(err, os.ErrNotExist) {
@@ -79,6 +90,12 @@ func validateRegistryDirectory() []error {
continue
}
// Validate namespace name
if !validNameRe.MatchString(nDir.Name()) {
allErrs = append(allErrs, xerrors.Errorf("%q: namespace name contains invalid characters (only alphanumeric characters and hyphens are allowed)", namespacePath))
continue
}
contributorReadmePath := path.Join(namespacePath, "README.md")
if _, err := os.Stat(contributorReadmePath); err != nil {
allErrs = append(allErrs, err)
+11 -13
View File
@@ -1,6 +1,6 @@
---
display_name: "Tmux"
description: "Tmux for coder agent :)"
display_name: "tmux"
description: "tmux with session persistence and plugins"
icon: "../../../../.icons/tmux.svg"
verified: false
tags: ["tmux", "terminal", "persistent"]
@@ -15,7 +15,7 @@ up a default or custom tmux configuration with session save/restore capabilities
```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.0"
version = "1.0.1"
agent_id = coder_agent.example.id
}
```
@@ -39,7 +39,7 @@ module "tmux" {
```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.0"
version = "1.0.1"
agent_id = coder_agent.example.id
tmux_config = "" # Optional: custom tmux.conf content
save_interval = 1 # Optional: save interval in minutes
@@ -78,7 +78,7 @@ This module can provision multiple tmux sessions, each as a separate app in the
```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.0"
version = "1.0.1"
agent_id = var.agent_id
sessions = ["default", "dev", "anomaly"]
tmux_config = <<-EOT
@@ -91,11 +91,9 @@ module "tmux" {
```
> [!IMPORTANT]
>
> - If you provide a custom `tmux_config`, it will completely replace the default configuration. Ensure you include plugin
> and TPM initialization lines if you want plugin support and session persistence.
> - The script will attempt to install dependencies using `sudo` where required.
> - If `git` is not installed, TPM installation will fail.
> - If you are using custom config, you'll be responsible for setting up persistence and plugins.
> - The `order`, `group`, and `icon` variables allow you to customize how tmux apps appear in the Coder UI.
> - In case of session restart or shh reconnection, the tmux session will be automatically restored :)
> If you provide a custom `tmux_config`, it will completely replace the default configuration. Ensure you include plugin and TPM initialization lines if you want plugin support and session persistence.
> The script will attempt to install dependencies using `sudo` where required.
> If `git` is not installed, TPM installation will fail.
> If you are using custom config, you'll be responsible for setting up persistence and plugins.
> The `order`, `group`, and `icon` variables allow you to customize how tmux apps appear in the Coder UI.
> In case of session restart or shh reconnection, the tmux session will be automatically restored :)
+1 -1
View File
@@ -5,7 +5,7 @@ github: coder
avatar: ./.images/avatar.svg
linkedin: https://www.linkedin.com/company/coderhq
website: https://discord.gg/coder
status: community
status: official
---
å
@@ -0,0 +1,161 @@
---
display_name: Auggie CLI
icon: ../../../../.icons/auggie.svg
description: Run Auggie CLI in your workspace for AI-powered coding assistance with AgentAPI integration
verified: true
tags: [agent, auggie, ai, tasks, augment]
---
# Auggie CLI
Run Auggie CLI in your workspace to access Augment's AI coding assistant with advanced context understanding and codebase integration. This module integrates with [AgentAPI](https://github.com/coder/agentapi).
```tf
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
```
## Prerequisites
- **Node.js and npm must be sourced/available before the auggie module installs** - ensure they are installed in your workspace image or via earlier provisioning steps
- You must add the [Coder Login](https://registry.coder.com/modules/coder/coder-login) module to your template
- **Augment session token for authentication (required for tasks). [Instructions](https://docs.augmentcode.com/cli/setup-auggie/authentication) to get the session token**
## Examples
### Usage with Tasks and Configuration
```tf
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Initial task prompt for Auggie CLI"
mutable = true
}
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/coder-login/coder"
version = "1.0.31"
agent_id = coder_agent.example.id
}
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
# Authentication
augment_session_token = <<-EOF
{"accessToken":"xxxx-yyyy-zzzz-jjjj","tenantURL":"https://d1.api.augmentcode.com/","scopes":["read","write"]}
EOF # Required for tasks
# Version
auggie_version = "0.3.0"
# Task configuration
ai_prompt = data.coder_parameter.ai_prompt.value
continue_previous_conversation = true
interaction_mode = "quiet"
auggie_model = "gpt5"
report_tasks = true
# MCP configuration for additional integrations
mcp = <<-EOF
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/coder/project"]
}
}
}
EOF
# Workspace guidelines
rules = <<-EOT
# Project Guidelines
## Code Style
- Use TypeScript for all new JavaScript files
- Follow consistent naming conventions
- Add comprehensive comments for complex logic
## Testing
- Write unit tests for all new functions
- Ensure test coverage above 80%
## Documentation
- Update README.md for any new features
- Document API changes in CHANGELOG.md
EOT
}
```
### Using Multiple MCP Configuration Files
```tf
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
# Multiple MCP configuration files
mcp_files = [
"/path/to/filesystem-mcp.json",
"/path/to/database-mcp.json",
"/path/to/api-mcp.json"
]
mcp = <<-EOF
{
"mcpServers": {
"Test MCP": {
"command": "uv",
"args": [
"--directory",
"/home/coder/test-mcp",
"run",
"server.py"
],
"timeout": 600
}
}
}
EOF
}
```
### Troubleshooting
If you have any issues, please take a look at the log files below.
```bash
# Installation logs
cat ~/.auggie-module/install.log
# Startup logs
cat ~/.auggie-module/agentapi-start.log
# Pre/post install script logs
cat ~/.auggie-module/pre_install.log
cat ~/.auggie-module/post_install.log
```
> [!NOTE]
> To use tasks with Auggie CLI, create a `coder_parameter` named `"AI Prompt"` and pass its value to the auggie module's `ai_prompt` variable. The `folder` variable is required for the module to function correctly.
## References
- [Auggie CLI Reference](https://docs.augmentcode.com/cli/reference)
- [Auggie CLI MCP Integration](https://docs.augmentcode.com/cli/integrations#mcp-integrations)
- [Augment Code Documentation](https://docs.augmentcode.com/)
- [AgentAPI Documentation](https://github.com/coder/agentapi)
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
@@ -0,0 +1,186 @@
run "test_auggie_basic" {
command = plan
variables {
agent_id = "test-agent-123"
folder = "/home/coder/projects"
}
assert {
condition = coder_env.auggie_session_auth.name == "AUGMENT_SESSION_AUTH"
error_message = "Auggie session auth environment variable should be set correctly"
}
assert {
condition = var.folder == "/home/coder/projects"
error_message = "Folder variable should be set correctly"
}
assert {
condition = var.agent_id == "test-agent-123"
error_message = "Agent ID variable should be set correctly"
}
assert {
condition = var.install_auggie == true
error_message = "Install auggie should default to true"
}
assert {
condition = var.install_agentapi == true
error_message = "Install agentapi should default to true"
}
}
run "test_auggie_with_session_token" {
command = plan
variables {
agent_id = "test-agent-456"
folder = "/home/coder/workspace"
augment_session_token = "test-session-token-123"
}
assert {
condition = coder_env.auggie_session_auth.value == "test-session-token-123"
error_message = "Auggie session token value should match the input"
}
}
run "test_auggie_with_custom_options" {
command = plan
variables {
agent_id = "test-agent-789"
folder = "/home/coder/custom"
order = 5
group = "development"
icon = "/icon/custom.svg"
auggie_model = "gpt-4"
ai_prompt = "Help me write better code"
interaction_mode = "compact"
continue_previous_conversation = true
install_auggie = false
install_agentapi = false
auggie_version = "1.0.0"
agentapi_version = "v0.6.0"
}
assert {
condition = var.order == 5
error_message = "Order variable should be set to 5"
}
assert {
condition = var.group == "development"
error_message = "Group variable should be set to 'development'"
}
assert {
condition = var.icon == "/icon/custom.svg"
error_message = "Icon variable should be set to custom icon"
}
assert {
condition = var.auggie_model == "gpt-4"
error_message = "Auggie model variable should be set to 'gpt-4'"
}
assert {
condition = var.ai_prompt == "Help me write better code"
error_message = "AI prompt variable should be set correctly"
}
assert {
condition = var.interaction_mode == "compact"
error_message = "Interaction mode should be set to 'compact'"
}
assert {
condition = var.continue_previous_conversation == true
error_message = "Continue previous conversation should be set to true"
}
assert {
condition = var.auggie_version == "1.0.0"
error_message = "Auggie version should be set to '1.0.0'"
}
assert {
condition = var.agentapi_version == "v0.6.0"
error_message = "AgentAPI version should be set to 'v0.6.0'"
}
}
run "test_auggie_with_mcp_and_rules" {
command = plan
variables {
agent_id = "test-agent-mcp"
folder = "/home/coder/mcp-test"
mcp = jsonencode({
mcpServers = {
test = {
command = "test-server"
args = ["--config", "test.json"]
}
}
})
mcp_files = [
"/path/to/mcp1.json",
"/path/to/mcp2.json"
]
rules = "# General coding rules\n- Write clean code\n- Add comments"
}
assert {
condition = var.mcp != ""
error_message = "MCP configuration should be provided"
}
assert {
condition = length(var.mcp_files) == 2
error_message = "Should have 2 MCP files"
}
assert {
condition = var.rules != ""
error_message = "Rules should be provided"
}
}
run "test_auggie_with_scripts" {
command = plan
variables {
agent_id = "test-agent-scripts"
folder = "/home/coder/scripts"
pre_install_script = "echo 'Pre-install script'"
post_install_script = "echo 'Post-install script'"
}
assert {
condition = var.pre_install_script == "echo 'Pre-install script'"
error_message = "Pre-install script should be set correctly"
}
assert {
condition = var.post_install_script == "echo 'Post-install script'"
error_message = "Post-install script should be set correctly"
}
}
run "test_auggie_interaction_mode_validation" {
command = plan
variables {
agent_id = "test-agent-validation"
folder = "/home/coder/test"
interaction_mode = "print"
}
assert {
condition = contains(["interactive", "print", "quiet", "compact"], var.interaction_mode)
error_message = "Interaction mode should be one of the valid options"
}
}
@@ -0,0 +1,342 @@
import {
test,
afterEach,
describe,
setDefaultTimeout,
beforeAll,
expect,
} from "bun:test";
import { execContainer, readFileContainer, runTerraformInit } from "~test";
import {
loadTestFile,
writeExecutable,
setup as setupUtil,
execModuleScript,
expectAgentAPIStarted,
} from "../../../coder/modules/agentapi/test-util";
import dedent from "dedent";
let cleanupFunctions: (() => Promise<void>)[] = [];
const registerCleanup = (cleanup: () => Promise<void>) => {
cleanupFunctions.push(cleanup);
};
afterEach(async () => {
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
cleanupFunctions = [];
for (const cleanup of cleanupFnsCopy) {
try {
await cleanup();
} catch (error) {
console.error("Error during cleanup:", error);
}
}
});
interface SetupProps {
skipAgentAPIMock?: boolean;
skipAuggieMock?: boolean;
moduleVariables?: Record<string, string>;
agentapiMockScript?: string;
}
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const projectDir = "/home/coder/project";
const { id } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
install_auggie: props?.skipAuggieMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
folder: projectDir,
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
if (!props?.skipAuggieMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/auggie",
content: await loadTestFile(import.meta.dir, "auggie-mock.sh"),
});
}
return { id };
};
setDefaultTimeout(60 * 1000);
describe("auggie", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
test("happy-path", async () => {
const { id } = await setup();
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("install-auggie-version", async () => {
const version_to_install = "0.3.0";
const { id } = await setup({
skipAuggieMock: true,
moduleVariables: {
install_auggie: "true",
auggie_version: version_to_install,
pre_install_script: dedent`
#!/usr/bin/env bash
set -euo pipefail
# Install Node.js and npm via system package manager
if ! command -v node >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y nodejs npm
fi
# Configure npm to use user directory (avoids permission issues)
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
# Persist npm user directory configuration
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
echo "prefix=$HOME/.npm-global" > ~/.npmrc
`,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`cat /home/coder/.auggie-module/install.log`,
]);
expect(resp.stdout).toContain(version_to_install);
});
test("check-latest-auggie-version-works", async () => {
const { id } = await setup({
skipAuggieMock: true,
skipAgentAPIMock: true,
moduleVariables: {
install_auggie: "true",
pre_install_script: dedent`
#!/usr/bin/env bash
set -euo pipefail
# Install Node.js and npm via system package manager
if ! command -v node >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y nodejs npm
fi
# Configure npm to use user directory (avoids permission issues)
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
# Persist npm user directory configuration
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
echo "prefix=$HOME/.npm-global" > ~/.npmrc
`,
},
});
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("auggie-session-token", async () => {
const sessionToken = "test-session-token-123";
const { id } = await setup({
moduleVariables: {
augment_session_token: sessionToken,
},
});
await execModuleScript(id);
const envCheck = await execContainer(id, [
"bash",
"-c",
`env | grep AUGMENT_SESSION_AUTH || echo "AUGMENT_SESSION_AUTH not found"`,
]);
expect(envCheck.stdout).toContain("AUGMENT_SESSION_AUTH");
});
test("auggie-mcp-config", async () => {
const mcpConfig = JSON.stringify({
mcpServers: {
test: {
command: "test-cmd",
type: "stdio"
}
}
});
const { id } = await setup({
moduleVariables: {
mcp: mcpConfig,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.auggie-module/agentapi-start.log",
);
expect(resp).toContain("--mcp-config");
});
test("auggie-rules", async () => {
const rules = "Always use TypeScript for new files";
const { id } = await setup({
moduleVariables: {
install_auggie: "false", // Don't need to install auggie to test rules file creation
rules: rules,
},
});
await execModuleScript(id);
const rulesFile = await readFileContainer(id, "/home/coder/.augment/rules.md");
expect(rulesFile).toContain(rules);
});
test("auggie-ai-task-prompt", async () => {
const prompt = "This is a task prompt for Auggie.";
const { id } = await setup({
moduleVariables: {
ai_prompt: prompt,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`cat /home/coder/.auggie-module/agentapi-start.log`,
]);
expect(resp.stdout).toContain(prompt);
});
test("auggie-interaction-mode", async () => {
const mode = "compact";
const { id } = await setup({
moduleVariables: {
interaction_mode: mode,
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.auggie-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain(`--${mode}`);
});
test("auggie-model", async () => {
const model = "gpt-4";
const { id } = await setup({
moduleVariables: {
auggie_model: model,
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.auggie-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain(`--model ${model}`);
});
test("auggie-continue-previous-conversation", async () => {
const { id } = await setup({
moduleVariables: {
continue_previous_conversation: "true",
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.auggie-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain("--continue");
});
test("pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'auggie-pre-install-script'",
post_install_script: "#!/bin/bash\necho 'auggie-post-install-script'",
},
});
await execModuleScript(id);
const preInstallLog = await readFileContainer(
id,
"/home/coder/.auggie-module/pre_install.log",
);
expect(preInstallLog).toContain("auggie-pre-install-script");
const postInstallLog = await readFileContainer(
id,
"/home/coder/.auggie-module/post_install.log",
);
expect(postInstallLog).toContain("auggie-post-install-script");
});
test("folder-variable", async () => {
const folder = "/home/coder/auggie-test-folder";
const { id } = await setup({
skipAuggieMock: false,
moduleVariables: {
folder,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.auggie-module/agentapi-start.log",
);
expect(resp).toContain(folder);
});
test("coder-mcp-config-created", async () => {
const { id } = await setup({
moduleVariables: {
install_auggie: "false", // Don't need to install auggie to test MCP config creation
},
});
await execModuleScript(id);
const mcpConfig = await readFileContainer(id, "/home/coder/.augment/coder_mcp.json");
expect(mcpConfig).toContain("mcpServers");
expect(mcpConfig).toContain("coder");
expect(mcpConfig).toContain("CODER_MCP_APP_STATUS_SLUG");
expect(mcpConfig).toContain("CODER_MCP_AI_AGENTAPI_URL");
});
test("mcp-files-array", async () => {
const mcpFiles = ["/path/to/mcp1.json", "/path/to/mcp2.json"];
const { id } = await setup({
moduleVariables: {
mcp_files: JSON.stringify(mcpFiles),
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.auggie-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain("mcp1.json");
expect(startLog.stdout).toContain("mcp2.json");
});
});
+230
View File
@@ -0,0 +1,230 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/auggie.svg"
}
variable "folder" {
type = string
description = "The folder to run Auggie in."
}
variable "install_auggie" {
type = bool
description = "Whether to install Auggie CLI."
default = true
}
variable "auggie_version" {
type = string
description = "The version of Auggie to install."
default = "" # empty string means the latest available version
validation {
condition = var.auggie_version == "" || can(regex("^v?[0-9]+\\.[0-9]+\\.[0-9]+", var.auggie_version))
error_message = "auggie_version must be empty (for latest) or a valid semantic version like 'v1.2.3' or '1.2.3'."
}
}
variable "install_agentapi" {
type = bool
description = "Whether to install AgentAPI."
default = true
}
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.6.0"
validation {
condition = can(regex("^v[0-9]+\\.[0-9]+\\.[0-9]+", var.agentapi_version))
error_message = "agentapi_version must be a valid semantic version starting with 'v', like 'v0.3.3'."
}
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing Auggie."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing Auggie."
default = null
}
# ----------------------------------------------
variable "ai_prompt" {
type = string
description = "Task prompt for the Auggie CLI"
default = ""
}
variable "mcp" {
type = string
description = "MCP configuration as a JSON string for the auggie cli, check https://docs.augmentcode.com/cli/integrations#mcp-integrations"
default = ""
}
variable "mcp_files" {
type = list(string)
description = "MCP configuration from a JSON file for the auggie cli, check https://docs.augmentcode.com/cli/integrations#mcp-integrations"
default = []
}
variable "rules" {
type = string
description = "Additional rules to append to workspace guidelines (markdown format)"
default = ""
}
variable "continue_previous_conversation" {
type = bool
description = "Whether to resume the previous conversation."
default = false
}
variable "interaction_mode" {
type = string
description = "Interaction mode with the Auggie CLI. Options: interactive, print, quiet, compact. https://docs.augmentcode.com/cli/reference#cli-flags"
default = "interactive"
validation {
condition = contains(["interactive", "print", "quiet", "compact"], var.interaction_mode)
error_message = "interaction_mode must be one of: interactive, print, quiet, compact."
}
}
variable "augment_session_token" {
type = string
description = "Auggie session token for authentication. https://docs.augmentcode.com/cli/setup-auggie/authentication"
default = ""
}
variable "auggie_model" {
type = string
description = "The model to use for Auggie, find available models using auggie --list-models"
default = ""
}
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI via AgentAPI"
default = false
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for Auggie"
default = false
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app"
default = "Auggie"
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app"
default = "Auggie CLI"
}
resource "coder_env" "auggie_session_auth" {
agent_id = var.agent_id
name = "AUGMENT_SESSION_AUTH"
value = var.augment_session_token
}
locals {
app_slug = "auggie"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".auggie-module"
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
agent_id = var.agent_id
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
ARG_AUGGIE_START_DIRECTORY='${var.folder}' \
ARG_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_MCP_FILES='${jsonencode(var.mcp_files)}' \
ARG_AUGGIE_RULES='${base64encode(var.rules)}' \
ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION='${var.continue_previous_conversation}' \
ARG_AUGGIE_INTERACTION_MODE='${var.interaction_mode}' \
ARG_AUGMENT_SESSION_AUTH='${var.augment_session_token}' \
ARG_AUGGIE_MODEL='${var.auggie_model}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
/tmp/start.sh
EOT
install_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_AUGGIE_INSTALL='${var.install_auggie}' \
ARG_AUGGIE_VERSION='${var.auggie_version}' \
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_AUGGIE_RULES='${base64encode(var.rules)}' \
ARG_MCP_CONFIG='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \
/tmp/install.sh
EOT
}
@@ -0,0 +1,127 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
BOLD='\033[0;1m'
# Function to check if a command exists
command_exists() {
command -v "$1" > /dev/null 2>&1
}
ARG_AUGGIE_INSTALL=${ARG_AUGGIE_INSTALL:-true}
ARG_AUGGIE_VERSION=${ARG_AUGGIE_VERSION:-}
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
ARG_AUGGIE_RULES=$(echo -n "${ARG_AUGGIE_RULES:-}" | base64 -d)
ARG_MCP_CONFIG=${ARG_MCP_CONFIG:-}
echo "--------------------------------"
printf "install auggie: %s\n" "$ARG_AUGGIE_INSTALL"
printf "auggie_version: %s\n" "$ARG_AUGGIE_VERSION"
printf "app_slug: %s\n" "$ARG_MCP_APP_STATUS_SLUG"
printf "rules: %s\n" "$ARG_AUGGIE_RULES"
echo "--------------------------------"
function check_dependencies() {
if ! command_exists node; then
printf "Error: Node.js is not installed. Please install Node.js manually or use the pre_install_script to install it.\n"
exit 1
fi
if ! command_exists npm; then
printf "Error: npm is not installed. Please install npm manually or use the pre_install_script to install it.\n"
exit 1
fi
printf "Node.js version: %s\n" "$(node --version)"
printf "npm version: %s\n" "$(npm --version)"
}
function install_auggie() {
if [ "${ARG_AUGGIE_INSTALL}" = "true" ]; then
check_dependencies
printf "%s Installing Auggie CLI\n" "${BOLD}"
NPM_GLOBAL_PREFIX="${HOME}/.npm-global"
if [ ! -d "$NPM_GLOBAL_PREFIX" ]; then
mkdir -p "$NPM_GLOBAL_PREFIX"
fi
npm config set prefix "$NPM_GLOBAL_PREFIX"
export PATH="$NPM_GLOBAL_PREFIX/bin:$PATH"
if [ -n "$ARG_AUGGIE_VERSION" ]; then
npm install -g "@augmentcode/auggie@$ARG_AUGGIE_VERSION"
else
npm install -g "@augmentcode/auggie"
fi
if ! grep -q "export PATH=\"\$HOME/.npm-global/bin:\$PATH\"" "$HOME/.bashrc"; then
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> "$HOME/.bashrc"
fi
printf "%s Successfully installed Auggie CLI. Version: %s\n" "${BOLD}" "$(auggie --version)"
else
printf "Skipping Auggie CLI installation (install_auggie=false)\n"
fi
}
function create_coder_mcp() {
AUGGIE_CODER_MCP_FILE="$HOME/.augment/coder_mcp.json"
CODER_MCP=$(
cat << EOF
{
"mcpServers":{
"coder": {
"args": ["exp", "mcp", "server"],
"command": "coder",
"env": {
"CODER_MCP_APP_STATUS_SLUG": "${ARG_MCP_APP_STATUS_SLUG}",
"CODER_MCP_AI_AGENTAPI_URL": "http://localhost:3284",
"CODER_AGENT_URL": "${CODER_AGENT_URL:-}",
"CODER_AGENT_TOKEN": "${CODER_AGENT_TOKEN:-}"
}
}
}
}
EOF
)
mkdir -p "$(dirname "$AUGGIE_CODER_MCP_FILE")"
echo "$CODER_MCP" > "$AUGGIE_CODER_MCP_FILE"
printf "Coder MCP config created at: %s\n" "$AUGGIE_CODER_MCP_FILE"
}
function create_user_mcp() {
if [ -n "$ARG_MCP_CONFIG" ]; then
USER_MCP_CONFIG_FILE="$HOME/.augment/user_mcp.json"
USER_MCP_CONTENT=$(echo -n "$ARG_MCP_CONFIG" | base64 -d)
mkdir -p "$(dirname "$USER_MCP_CONFIG_FILE")"
echo "$USER_MCP_CONTENT" > "$USER_MCP_CONFIG_FILE"
printf "User MCP config created at: %s\n" "$USER_MCP_CONFIG_FILE"
else
printf "No user MCP config provided, skipping user MCP config creation.\n"
fi
}
function create_rules_file() {
AUGGIE_RULES_FILE="$HOME/.augment/rules.md"
if [ -n "$ARG_AUGGIE_RULES" ]; then
mkdir -p "$(dirname "$AUGGIE_RULES_FILE")"
echo -n "$ARG_AUGGIE_RULES" > "$AUGGIE_RULES_FILE"
printf "Rules file created at: %s\n" "$AUGGIE_RULES_FILE"
else
printf "No rules provided, skipping rules file creation.\n"
fi
}
install_auggie
create_coder_mcp
create_user_mcp
create_rules_file
@@ -0,0 +1,104 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
command_exists() {
command -v "$1" > /dev/null 2>&1
}
if [ -f "$HOME/.nvm/nvm.sh" ]; then
source "$HOME"/.nvm/nvm.sh
else
export PATH="$HOME/.npm-global/bin:$PATH"
fi
ARG_AUGGIE_START_DIRECTORY=${ARG_AUGGIE_START_DIRECTORY:-"$HOME"}
ARG_TASK_PROMPT=$(echo -n "${ARG_TASK_PROMPT:-}" | base64 -d)
ARG_MCP_FILES=${ARG_MCP_FILES:-[]}
ARG_AUGGIE_RULES=${ARG_AUGGIE_RULES:-}
ARG_AUGMENT_SESSION_AUTH=${ARG_AUGMENT_SESSION_AUTH:-}
ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION=${ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION:-false}
ARG_AUGGIE_INTERACTION_MODE=${ARG_AUGGIE_INTERACTION_MODE:-"interactive"}
ARG_AUGGIE_MODEL=${ARG_AUGGIE_MODEL:-}
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-false}
ARGS=()
echo "--------------------------------"
printf "auggie_start_directory: %s\n" "$ARG_AUGGIE_START_DIRECTORY"
printf "task_prompt: %s\n" "$ARG_TASK_PROMPT"
printf "mcp_files: %s\n" "$ARG_MCP_FILES"
printf "auggie_rules: %s\n" "$ARG_AUGGIE_RULES"
printf "continue_previous_conversation: %s\n" "$ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION"
printf "auggie_interaction_mode: %s\n" "$ARG_AUGGIE_INTERACTION_MODE"
printf "augment_session_auth: %s\n" "$ARG_AUGMENT_SESSION_AUTH"
printf "auggie_model: %s\n" "$ARG_AUGGIE_MODEL"
printf "report_tasks: %s\n" "$ARG_REPORT_TASKS"
echo "--------------------------------"
function validate_auggie_installation() {
if command_exists auggie; then
printf "Auggie is installed\n"
else
printf "Error: Auggie is not installed. Please enable install_auggie or install it manually\n"
exit 1
fi
}
function build_auggie_args() {
if [ -n "$ARG_AUGGIE_INTERACTION_MODE" ]; then
if [ "$ARG_AUGGIE_INTERACTION_MODE" != "interactive" ]; then
ARGS+=(--"$ARG_AUGGIE_INTERACTION_MODE")
fi
fi
if [ -n "$ARG_AUGGIE_MODEL" ]; then
ARGS+=(--model "$ARG_AUGGIE_MODEL")
fi
if [ -f "$HOME/.augment/user_mcp.json" ]; then
ARGS+=(--mcp-config "$HOME/.augment/user_mcp.json")
fi
if [ -n "$ARG_MCP_FILES" ] && [ "$ARG_MCP_FILES" != "[]" ]; then
for file in $(echo "$ARG_MCP_FILES" | jq -r '.[]'); do
ARGS+=(--mcp-config "$file")
done
fi
ARGS+=(--mcp-config "$HOME/.augment/coder_mcp.json")
if [ -n "$ARG_AUGGIE_RULES" ]; then
AUGGIE_RULES_FILE="$HOME/.augment/rules.md"
ARGS+=(--rules "$AUGGIE_RULES_FILE")
fi
if [ "$ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION" == "true" ]; then
ARGS+=(--continue)
fi
if [ -n "$ARG_TASK_PROMPT" ]; then
if [ "$ARG_REPORT_TASKS" == "true" ]; then
PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_TASK_PROMPT"
else
PROMPT="$ARG_TASK_PROMPT"
fi
ARGS+=(--instruction "$PROMPT")
fi
}
function start_agentapi_server() {
mkdir -p "$ARG_AUGGIE_START_DIRECTORY"
cd "$ARG_AUGGIE_START_DIRECTORY"
ARGS+=(--workspace-root "$ARG_AUGGIE_START_DIRECTORY")
printf "Running auggie with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
agentapi server --term-width 67 --term-height 1190 -- auggie "${ARGS[@]}"
}
validate_auggie_installation
build_auggie_args
start_agentapi_server
@@ -0,0 +1,14 @@
#!/bin/bash
if [[ "$1" == "--version" ]]; then
echo "HELLO: $(bash -c env)"
echo "auggie version v1.0.0"
exit 0
fi
set -e
while true; do
echo "$(date) - auggie-mock"
sleep 15
done
+146
View File
@@ -0,0 +1,146 @@
---
display_name: Codex CLI
icon: ../../../../.icons/openai.svg
description: Run Codex CLI in your workspace with AgentAPI integration
verified: true
tags: [agent, codex, ai, openai, tasks]
---
# Codex CLI
Run Codex CLI in your workspace to access OpenAI's models through the Codex interface, with custom pre/post install scripts. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for Coder Tasks compatibility.
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "2.0.0"
agent_id = coder_agent.example.id
openai_api_key = var.openai_api_key
folder = "/home/coder/project"
}
```
## Prerequisites
- You must add the [Coder Login](https://registry.coder.com/modules/coder/coder-login) module to your template
- OpenAI API key for Codex access
## Examples
### Run standalone
```tf
module "codex" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/codex/coder"
version = "2.0.0"
agent_id = coder_agent.example.id
openai_api_key = "..."
folder = "/home/coder/project"
}
```
### Tasks integration
```tf
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Initial prompt for the Codex CLI"
mutable = true
}
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/coder-login/coder"
version = "1.0.31"
agent_id = coder_agent.example.id
}
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "2.0.0"
agent_id = coder_agent.example.id
openai_api_key = "..."
ai_prompt = data.coder_parameter.ai_prompt.value
folder = "/home/coder/project"
# Custom configuration for full auto mode
base_config_toml = <<-EOT
approval_policy = "never"
preferred_auth_method = "apikey"
EOT
}
```
> [!WARNING]
> This module configures Codex with a `workspace-write` sandbox that allows AI tasks to read/write files in the specified folder. While the sandbox provides security boundaries, Codex can still modify files within the workspace. Use this module _only_ in trusted environments and be aware of the security implications.
## How it Works
- **Install**: The module installs Codex CLI and sets up the environment
- **System Prompt**: If `codex_system_prompt` is set, writes the prompt to `AGENTS.md` in the `~/.codex/` directory
- **Start**: Launches Codex CLI in the specified directory, wrapped by AgentAPI
- **Configuration**: Sets `OPENAI_API_KEY` environment variable and passes `--model` flag to Codex CLI (if variables provided)
## Configuration
### Default Configuration
When no custom `base_config_toml` is provided, the module uses these secure defaults:
```toml
sandbox_mode = "workspace-write"
approval_policy = "never"
preferred_auth_method = "apikey"
[sandbox_workspace_write]
network_access = true
```
### Custom Configuration
For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_servers`:
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "2.0.0"
# ... other variables ...
# Override default configuration
base_config_toml = <<-EOT
sandbox_mode = "danger-full-access"
approval_policy = "never"
preferred_auth_method = "apikey"
EOT
# Add extra MCP servers
additional_mcp_servers = <<-EOT
[mcp_servers.GitHub]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
type = "stdio"
EOT
}
```
> [!NOTE]
> If no custom configuration is provided, the module uses secure defaults. The Coder MCP server is always included automatically. For containerized workspaces (Docker/Kubernetes), you may need `sandbox_mode = "danger-full-access"` to avoid permission issues. For advanced options, see [Codex config docs](https://github.com/openai/codex/blob/main/codex-rs/config.md).
## Troubleshooting
- Check installation and startup logs in `~/.codex-module/`
- Ensure your OpenAI API key has access to the specified model
> [!IMPORTANT]
> To use tasks with Codex CLI, ensure you have the `openai_api_key` variable set, and **you create a `coder_parameter` named `"AI Prompt"` and pass its value to the codex module's `ai_prompt` variable**. [Tasks Template Example](https://registry.coder.com/templates/coder-labs/tasks-docker).
> The module automatically configures Codex with your API key and model preferences.
> folder is a required variable for the module to function correctly.
## References
- [Codex CLI Documentation](https://github.com/openai/codex)
- [AgentAPI Documentation](https://github.com/coder/agentapi)
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
@@ -0,0 +1,368 @@
import {
test,
afterEach,
describe,
setDefaultTimeout,
beforeAll,
expect,
} from "bun:test";
import { execContainer, readFileContainer, runTerraformInit } from "~test";
import {
loadTestFile,
writeExecutable,
setup as setupUtil,
execModuleScript,
expectAgentAPIStarted,
} from "../../../coder/modules/agentapi/test-util";
import dedent from "dedent";
let cleanupFunctions: (() => Promise<void>)[] = [];
const registerCleanup = (cleanup: () => Promise<void>) => {
cleanupFunctions.push(cleanup);
};
afterEach(async () => {
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
cleanupFunctions = [];
for (const cleanup of cleanupFnsCopy) {
try {
await cleanup();
} catch (error) {
console.error("Error during cleanup:", error);
}
}
});
interface SetupProps {
skipAgentAPIMock?: boolean;
skipCodexMock?: boolean;
moduleVariables?: Record<string, string>;
agentapiMockScript?: string;
}
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const projectDir = "/home/coder/project";
const { id } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
install_codex: props?.skipCodexMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
codex_model: "gpt-4-turbo",
folder: "/home/coder",
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
if (!props?.skipCodexMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/codex",
content: await loadTestFile(import.meta.dir, "codex-mock.sh"),
});
}
return { id };
};
setDefaultTimeout(60 * 1000);
describe("codex", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
test("happy-path", async () => {
const { id } = await setup();
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("install-codex-version", async () => {
const version_to_install = "0.10.0";
const { id } = await setup({
skipCodexMock: true,
moduleVariables: {
install_codex: "true",
codex_version: version_to_install,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`cat /home/coder/.codex-module/install.log`,
]);
expect(resp.stdout).toContain(version_to_install);
});
test("check-latest-codex-version-works", async () => {
const { id } = await setup({
skipCodexMock: true,
skipAgentAPIMock: true,
moduleVariables: {
install_codex: "true",
},
});
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("base-config-toml", async () => {
const baseConfig = dedent`
sandbox_mode = "danger-full-access"
approval_policy = "never"
preferred_auth_method = "apikey"
[custom_section]
new_feature = true
`.trim();
const { id } = await setup({
moduleVariables: {
base_config_toml: baseConfig,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
expect(resp).toContain("sandbox_mode = \"danger-full-access\"");
expect(resp).toContain("preferred_auth_method = \"apikey\"");
expect(resp).toContain("[custom_section]");
expect(resp).toContain("[mcp_servers.Coder]");
});
test("codex-api-key", async () => {
const apiKey = "test-api-key-123";
const { id } = await setup({
moduleVariables: {
openai_api_key: apiKey,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.codex-module/agentapi-start.log",
);
expect(resp).toContain("OpenAI API Key: Provided");
});
test("pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
post_install_script: "#!/bin/bash\necho 'post-install-script'",
},
});
await execModuleScript(id);
const preInstallLog = await readFileContainer(
id,
"/home/coder/.codex-module/pre_install.log",
);
expect(preInstallLog).toContain("pre-install-script");
const postInstallLog = await readFileContainer(
id,
"/home/coder/.codex-module/post_install.log",
);
expect(postInstallLog).toContain("post-install-script");
});
test("folder-variable", async () => {
const folder = "/tmp/codex-test-folder";
const { id } = await setup({
skipCodexMock: false,
moduleVariables: {
folder,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.codex-module/install.log",
);
expect(resp).toContain(folder);
});
test("additional-mcp-servers", async () => {
const additional = dedent`
[mcp_servers.GitHub]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
type = "stdio"
description = "GitHub integration"
[mcp_servers.FileSystem]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
type = "stdio"
description = "File system access"
`.trim();
const { id } = await setup({
moduleVariables: {
additional_mcp_servers: additional,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
expect(resp).toContain("[mcp_servers.GitHub]");
expect(resp).toContain("[mcp_servers.FileSystem]");
expect(resp).toContain("[mcp_servers.Coder]");
expect(resp).toContain("GitHub integration");
});
test("full-custom-config", async () => {
const baseConfig = dedent`
sandbox_mode = "read-only"
approval_policy = "untrusted"
preferred_auth_method = "chatgpt"
custom_setting = "test-value"
[advanced_settings]
timeout = 30000
debug = true
logging_level = "verbose"
`.trim();
const additionalMCP = dedent`
[mcp_servers.CustomTool]
command = "/usr/local/bin/custom-tool"
args = ["--serve", "--port", "8080"]
type = "stdio"
description = "Custom development tool"
[mcp_servers.DatabaseMCP]
command = "python"
args = ["-m", "database_mcp_server"]
type = "stdio"
description = "Database query interface"
`.trim();
const { id } = await setup({
moduleVariables: {
base_config_toml: baseConfig,
additional_mcp_servers: additionalMCP,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
// Check base config
expect(resp).toContain("sandbox_mode = \"read-only\"");
expect(resp).toContain("preferred_auth_method = \"chatgpt\"");
expect(resp).toContain("custom_setting = \"test-value\"");
expect(resp).toContain("[advanced_settings]");
expect(resp).toContain("logging_level = \"verbose\"");
// Check MCP servers
expect(resp).toContain("[mcp_servers.Coder]");
expect(resp).toContain("[mcp_servers.CustomTool]");
expect(resp).toContain("[mcp_servers.DatabaseMCP]");
expect(resp).toContain("Custom development tool");
expect(resp).toContain("Database query interface");
});
test("minimal-default-config", async () => {
const { id } = await setup({
moduleVariables: {
// No base_config_toml or additional_mcp_servers - should use defaults
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
// Check default base config
expect(resp).toContain("sandbox_mode = \"workspace-write\"");
expect(resp).toContain("approval_policy = \"never\"");
expect(resp).toContain("[sandbox_workspace_write]");
expect(resp).toContain("network_access = true");
// Check only Coder MCP server is present
expect(resp).toContain("[mcp_servers.Coder]");
expect(resp).toContain("Report ALL tasks and statuses");
// Ensure no additional MCP servers
const mcpServerCount = (resp.match(/\[mcp_servers\./g) || []).length;
expect(mcpServerCount).toBe(1);
});
test("codex-system-prompt", async () => {
const prompt = "This is a system prompt for Codex.";
const { id } = await setup({
moduleVariables: {
codex_system_prompt: prompt,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/AGENTS.md");
expect(resp).toContain(prompt);
});
test("codex-system-prompt-skip-append-if-exists", async () => {
const prompt_1 = "This is a system prompt for Codex.";
const prompt_2 = "This is a system prompt for Goose.";
const prompt_3 = dedent`
This is a system prompt for Codex.
This is a system prompt for Gemini.
`.trim();
const pre_install_script = dedent`
#!/bin/bash
mkdir -p /home/coder/.codex
echo -e "${prompt_3}" >> /home/coder/.codex/AGENTS.md
`.trim();
const { id } = await setup({
moduleVariables: {
pre_install_script,
codex_system_prompt: prompt_2,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/AGENTS.md");
expect(resp).toContain(prompt_1);
expect(resp).toContain(prompt_2);
// Re-run with a prompt that already exists, it should not append again
const { id: id_2 } = await setup({
moduleVariables: {
pre_install_script,
codex_system_prompt: prompt_1,
},
});
await execModuleScript(id_2);
const resp_2 = await readFileContainer(id_2, "/home/coder/.codex/AGENTS.md");
expect(resp_2).toContain(prompt_1);
const count = (resp_2.match(new RegExp(prompt_1, "g")) || []).length;
expect(count).toBe(1);
});
test("codex-ai-task-prompt", async () => {
const prompt = "This is a system prompt for Codex.";
const { id } = await setup({
moduleVariables: {
ai_prompt: prompt,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`cat /home/coder/.codex-module/agentapi-start.log`,
]);
expect(resp.stdout).toContain(prompt);
});
test("start-without-prompt", async () => {
const { id } = await setup({
moduleVariables: {
codex_system_prompt: "", // Explicitly disable system prompt
},
});
await execModuleScript(id);
const prompt = await execContainer(id, [
"ls",
"-l",
"/home/coder/.codex/AGENTS.md",
]);
expect(prompt.exitCode).not.toBe(0);
expect(prompt.stderr).toContain("No such file or directory");
});
});
+176
View File
@@ -0,0 +1,176 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/openai.svg"
}
variable "folder" {
type = string
description = "The folder to run Codex in."
}
variable "install_codex" {
type = bool
description = "Whether to install Codex."
default = true
}
variable "codex_version" {
type = string
description = "The version of Codex to install."
default = "" # empty string means the latest available version
}
variable "base_config_toml" {
type = string
description = "Complete base TOML configuration for Codex (without mcp_servers section). If empty, uses minimal default configuration with workspace-write sandbox mode and never approval policy. For advanced options, see https://github.com/openai/codex/blob/main/codex-rs/config.md"
default = ""
}
variable "additional_mcp_servers" {
type = string
description = "Additional MCP servers configuration in TOML format. These will be merged with the required Coder MCP server in the [mcp_servers] section."
default = ""
}
variable "openai_api_key" {
type = string
description = "OpenAI API key for Codex CLI"
default = ""
}
variable "install_agentapi" {
type = bool
description = "Whether to install AgentAPI."
default = true
}
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.5.0"
}
variable "codex_model" {
type = string
description = "The model for Codex to use. Defaults to gpt-5."
default = ""
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing Codex."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing Codex."
default = null
}
variable "ai_prompt" {
type = string
description = "Initial task prompt for Codex CLI when launched via Tasks"
default = ""
}
variable "codex_system_prompt" {
type = string
description = "System instructions written to AGENTS.md in the ~/.codex directory"
default = "You are a helpful coding assistant. Start every response with `Codex says:`"
}
resource "coder_env" "openai_api_key" {
agent_id = var.agent_id
name = "OPENAI_API_KEY"
value = var.openai_api_key
}
locals {
app_slug = "codex"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".codex-module"
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
agent_id = var.agent_id
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 = "Codex"
cli_app_slug = "${local.app_slug}-cli"
cli_app_display_name = "Codex CLI"
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
ARG_OPENAI_API_KEY='${var.openai_api_key}' \
ARG_CODEX_MODEL='${var.codex_model}' \
ARG_CODEX_START_DIRECTORY='${var.folder}' \
ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
/tmp/start.sh
EOT
install_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_INSTALL='${var.install_codex}' \
ARG_CODEX_VERSION='${var.codex_version}' \
ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \
ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_CODEX_START_DIRECTORY='${var.folder}' \
ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \
/tmp/install.sh
EOT
}
@@ -0,0 +1,165 @@
#!/bin/bash
source "$HOME"/.bashrc
BOLD='\033[0;1m'
command_exists() {
command -v "$1" > /dev/null 2>&1
}
set -o errexit
set -o pipefail
set -o nounset
ARG_BASE_CONFIG_TOML=$(echo -n "$ARG_BASE_CONFIG_TOML" | base64 -d)
ARG_ADDITIONAL_MCP_SERVERS=$(echo -n "$ARG_ADDITIONAL_MCP_SERVERS" | base64 -d)
ARG_CODEX_INSTRUCTION_PROMPT=$(echo -n "$ARG_CODEX_INSTRUCTION_PROMPT" | base64 -d)
echo "=== Codex Module Configuration ==="
printf "Install Codex: %s\n" "$ARG_INSTALL"
printf "Codex Version: %s\n" "$ARG_CODEX_VERSION"
printf "App Slug: %s\n" "$ARG_CODER_MCP_APP_STATUS_SLUG"
printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
printf "Has Base Config: %s\n" "$([ -n "$ARG_BASE_CONFIG_TOML" ] && echo "Yes" || echo "No")"
printf "Has Additional MCP: %s\n" "$([ -n "$ARG_ADDITIONAL_MCP_SERVERS" ] && echo "Yes" || echo "No")"
printf "Has System Prompt: %s\n" "$([ -n "$ARG_CODEX_INSTRUCTION_PROMPT" ] && echo "Yes" || echo "No")"
echo "======================================"
set +o nounset
function install_node() {
if ! command_exists npm; then
printf "npm not found, checking for Node.js installation...\n"
if ! command_exists node; then
printf "Node.js not found, installing Node.js via NVM...\n"
export NVM_DIR="$HOME/.nvm"
if [ ! -d "$NVM_DIR" ]; then
mkdir -p "$NVM_DIR"
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
else
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
fi
nvm install --lts
nvm use --lts
nvm alias default node
printf "Node.js installed: %s\n" "$(node --version)"
printf "npm installed: %s\n" "$(npm --version)"
else
printf "Node.js is installed but npm is not available. Please install npm manually.\n"
exit 1
fi
fi
}
function install_codex() {
if [ "${ARG_INSTALL}" = "true" ]; then
install_node
if ! command_exists nvm; then
printf "which node: %s\n" "$(which node)"
printf "which npm: %s\n" "$(which npm)"
mkdir -p "$HOME"/.npm-global
npm config set prefix "$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then
echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc
fi
fi
printf "%s Installing Codex CLI\n" "${BOLD}"
if [ -n "$ARG_CODEX_VERSION" ]; then
npm install -g "@openai/codex@$ARG_CODEX_VERSION"
else
npm install -g "@openai/codex"
fi
printf "%s Successfully installed Codex CLI. Version: %s\n" "${BOLD}" "$(codex --version)"
fi
}
write_minimal_default_config() {
local config_path="$1"
cat << EOF > "$config_path"
# Minimal Default Codex Configuration
sandbox_mode = "workspace-write"
approval_policy = "never"
preferred_auth_method = "apikey"
[sandbox_workspace_write]
network_access = true
EOF
}
append_mcp_servers_section() {
local config_path="$1"
cat << EOF >> "$config_path"
# MCP Servers Configuration
[mcp_servers.Coder]
command = "coder"
args = ["exp", "mcp", "server"]
env = { "CODER_MCP_APP_STATUS_SLUG" = "${ARG_CODER_MCP_APP_STATUS_SLUG}", "CODER_MCP_AI_AGENTAPI_URL" = "http://localhost:3284", "CODER_AGENT_URL" = "${CODER_AGENT_URL}", "CODER_AGENT_TOKEN" = "${CODER_AGENT_TOKEN}" }
description = "Report ALL tasks and statuses (in progress, done, failed) you are working on."
type = "stdio"
EOF
if [ -n "$ARG_ADDITIONAL_MCP_SERVERS" ]; then
printf "Adding additional MCP servers\n"
echo "$ARG_ADDITIONAL_MCP_SERVERS" >> "$config_path"
fi
}
function populate_config_toml() {
CONFIG_PATH="$HOME/.codex/config.toml"
mkdir -p "$(dirname "$CONFIG_PATH")"
if [ -n "$ARG_BASE_CONFIG_TOML" ]; then
printf "Using provided base configuration\n"
echo "$ARG_BASE_CONFIG_TOML" > "$CONFIG_PATH"
else
printf "Using minimal default configuration\n"
write_minimal_default_config "$CONFIG_PATH"
fi
append_mcp_servers_section "$CONFIG_PATH"
}
function add_instruction_prompt_if_exists() {
if [ -n "${ARG_CODEX_INSTRUCTION_PROMPT:-}" ]; then
AGENTS_PATH="$HOME/.codex/AGENTS.md"
printf "Creating AGENTS.md in .codex directory: %s\\n" "${AGENTS_PATH}"
mkdir -p "$HOME/.codex"
if [ -f "${AGENTS_PATH}" ] && grep -Fq "${ARG_CODEX_INSTRUCTION_PROMPT}" "${AGENTS_PATH}"; then
printf "AGENTS.md already contains the instruction prompt. Skipping append.\n"
else
printf "Appending instruction prompt to AGENTS.md in .codex directory\n"
echo -e "\n${ARG_CODEX_INSTRUCTION_PROMPT}" >> "${AGENTS_PATH}"
fi
if [ ! -d "${ARG_CODEX_START_DIRECTORY}" ]; then
printf "Creating start directory '%s'\\n" "${ARG_CODEX_START_DIRECTORY}"
mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
exit 1
}
fi
else
printf "AGENTS.md instruction prompt is not set.\n"
fi
}
install_codex
codex --version
populate_config_toml
add_instruction_prompt_if_exists
@@ -0,0 +1,73 @@
#!/bin/bash
source "$HOME"/.bashrc
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" > /dev/null 2>&1
}
if [ -f "$HOME/.nvm/nvm.sh" ]; then
source "$HOME"/.nvm/nvm.sh
else
export PATH="$HOME/.npm-global/bin:$PATH"
fi
printf "Version: %s\n" "$(codex --version)"
set -o nounset
ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d)
echo "=== Codex Launch Configuration ==="
printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")"
printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}"
printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")"
echo "======================================"
set +o nounset
CODEX_ARGS=()
if command_exists codex; then
printf "Codex is installed\n"
else
printf "Error: Codex is not installed. Please enable install_codex or install it manually\n"
exit 1
fi
if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then
printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
cd "${ARG_CODEX_START_DIRECTORY}" || {
printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
exit 1
}
else
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
exit 1
}
cd "${ARG_CODEX_START_DIRECTORY}" || {
printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
exit 1
}
fi
if [ -n "$ARG_CODEX_MODEL" ]; then
CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL")
fi
if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
printf "Running the task prompt %s\n" "$ARG_CODEX_TASK_PROMPT"
PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
CODEX_ARGS+=("$PROMPT")
else
printf "No task prompt given.\n"
fi
# Terminal dimensions optimized for Coder Tasks UI sidebar:
# - Width 67: fits comfortably in sidebar
# - Height 1190: adjusted due to Codex terminal height bug
printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}"
agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}"
@@ -0,0 +1,14 @@
#!/bin/bash
if [[ "$1" == "--version" ]]; then
echo "HELLO: $(bash -c env)"
echo "codex version v1.0.0"
exit 0
fi
set -e
while true; do
echo "$(date) - codex-mock"
sleep 15
done
@@ -0,0 +1,135 @@
---
display_name: Cursor CLI
icon: ../../../../.icons/cursor.svg
description: Run Cursor Agent CLI in your workspace for AI pair programming
verified: true
tags: [agent, cursor, ai, tasks]
---
# Cursor CLI
Run the Cursor Agent CLI in your workspace for interactive coding assistance and automated task execution.
```tf
module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder"
version = "0.1.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
```
## Basic setup
A full example with MCP, rules, and pre/post install scripts:
```tf
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Build a Minesweeper in Python."
mutable = true
}
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/coder-login/coder"
version = "1.0.31"
agent_id = coder_agent.main.id
}
module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder"
version = "0.1.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
# Optional
install_cursor_cli = true
force = true
model = "gpt-5"
ai_prompt = data.coder_parameter.ai_prompt.value
api_key = "xxxx-xxxx-xxxx" # Required while using tasks, see note below
# Minimal MCP server (writes `folder/.cursor/mcp.json`):
mcp = jsonencode({
mcpServers = {
playwright = {
command = "npx"
args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated", "--no-sandbox"]
}
desktop-commander = {
command = "npx"
args = ["-y", "@wonderwhy-er/desktop-commander"]
}
}
})
# Use a pre_install_script to install the CLI
pre_install_script = <<-EOT
#!/usr/bin/env bash
set -euo pipefail
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
EOT
# Use post_install_script to wait for the repo to be ready
post_install_script = <<-EOT
#!/usr/bin/env bash
set -euo pipefail
TARGET="$${FOLDER}/.git/config"
echo "[cursor-cli] waiting for $${TARGET}..."
for i in $(seq 1 600); do
[ -f "$TARGET" ] && { echo "ready"; exit 0; }
sleep 1
done
echo "timeout waiting for $${TARGET}" >&2
EOT
# Provide a map of file name to content; files are written to `folder/.cursor/rules/<name>`.
rules_files = {
"python.mdc" = <<-EOT
---
description: RPC Service boilerplate
globs:
alwaysApply: false
---
- Use our internal RPC pattern when defining services
- Always use snake_case for service names.
@service-template.ts
EOT
"frontend.mdc" = <<-EOT
---
description: RPC Service boilerplate
globs:
alwaysApply: false
---
- Use our internal RPC pattern when defining services
- Always use snake_case for service names.
@service-template.ts
EOT
}
}
```
> [!NOTE]
> A `.cursor` directory will be created in the specified `folder`, containing the MCP configuration, rules.
> To use this module with tasks, please pass the API Key obtained from Cursor to the `api_key` variable. To obtain the api key follow the instructions [here](https://docs.cursor.com/en/cli/reference/authentication#step-1%3A-generate-an-api-key)
## References
- See Cursor CLI docs: `https://docs.cursor.com/en/cli/overview`
- For MCP project config, see `https://docs.cursor.com/en/context/mcp#using-mcp-json`. This module writes your `mcp_json` into `folder/.cursor/mcp.json`.
- For Rules, see `https://docs.cursor.com/en/context/rules#project-rules`. Provide `rules_files` (map of file name to content) to populate `folder/.cursor/rules/`.
## Troubleshooting
- Ensure the CLI is installed (enable `install_cursor_cli = true` or preinstall it in your image)
- Logs are written to `~/.cursor-cli-module/`
@@ -0,0 +1,152 @@
run "test_cursor_cli_basic" {
command = plan
variables {
agent_id = "test-agent-123"
folder = "/home/coder/projects"
}
assert {
condition = coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug environment variable should be set correctly"
}
assert {
condition = coder_env.status_slug.value == "cursorcli"
error_message = "Status slug value should be 'cursorcli'"
}
assert {
condition = var.folder == "/home/coder/projects"
error_message = "Folder variable should be set correctly"
}
assert {
condition = var.agent_id == "test-agent-123"
error_message = "Agent ID variable should be set correctly"
}
}
run "test_cursor_cli_with_api_key" {
command = plan
variables {
agent_id = "test-agent-456"
folder = "/home/coder/workspace"
api_key = "test-api-key-123"
}
assert {
condition = coder_env.cursor_api_key[0].name == "CURSOR_API_KEY"
error_message = "Cursor API key environment variable should be set correctly"
}
assert {
condition = coder_env.cursor_api_key[0].value == "test-api-key-123"
error_message = "Cursor API key value should match the input"
}
}
run "test_cursor_cli_with_custom_options" {
command = plan
variables {
agent_id = "test-agent-789"
folder = "/home/coder/custom"
order = 5
group = "development"
icon = "/icon/custom.svg"
model = "sonnet-4"
ai_prompt = "Help me write better code"
force = false
install_cursor_cli = false
install_agentapi = false
}
assert {
condition = var.order == 5
error_message = "Order variable should be set to 5"
}
assert {
condition = var.group == "development"
error_message = "Group variable should be set to 'development'"
}
assert {
condition = var.icon == "/icon/custom.svg"
error_message = "Icon variable should be set to custom icon"
}
assert {
condition = var.model == "sonnet-4"
error_message = "Model variable should be set to 'sonnet-4'"
}
assert {
condition = var.ai_prompt == "Help me write better code"
error_message = "AI prompt variable should be set correctly"
}
assert {
condition = var.force == false
error_message = "Force variable should be set to false"
}
}
run "test_cursor_cli_with_mcp_and_rules" {
command = plan
variables {
agent_id = "test-agent-mcp"
folder = "/home/coder/mcp-test"
mcp = jsonencode({
mcpServers = {
test = {
command = "test-server"
args = ["--config", "test.json"]
}
}
})
rules_files = {
"general.md" = "# General coding rules\n- Write clean code\n- Add comments"
"security.md" = "# Security rules\n- Never commit secrets\n- Validate inputs"
}
}
assert {
condition = var.mcp != null
error_message = "MCP configuration should be provided"
}
assert {
condition = var.rules_files != null
error_message = "Rules files should be provided"
}
assert {
condition = length(var.rules_files) == 2
error_message = "Should have 2 rules files"
}
}
run "test_cursor_cli_with_scripts" {
command = plan
variables {
agent_id = "test-agent-scripts"
folder = "/home/coder/scripts"
pre_install_script = "echo 'Pre-install script'"
post_install_script = "echo 'Post-install script'"
}
assert {
condition = var.pre_install_script == "echo 'Pre-install script'"
error_message = "Pre-install script should be set correctly"
}
assert {
condition = var.post_install_script == "echo 'Post-install script'"
error_message = "Post-install script should be set correctly"
}
}
@@ -0,0 +1,212 @@
import { afterEach, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
import { execContainer, runTerraformInit, writeFileContainer } from "~test";
import {
execModuleScript,
expectAgentAPIStarted,
loadTestFile,
setup as setupUtil
} from "../../../coder/modules/agentapi/test-util";
import { setupContainer, writeExecutable } from "../../../coder/modules/agentapi/test-util";
let cleanupFns: (() => Promise<void>)[] = [];
const registerCleanup = (fn: () => Promise<void>) => cleanupFns.push(fn);
afterEach(async () => {
const fns = cleanupFns.slice().reverse();
cleanupFns = [];
for (const fn of fns) {
try {
await fn();
} catch (err) {
console.error(err);
}
}
});
interface SetupProps {
skipAgentAPIMock?: boolean;
skipCursorCliMock?: boolean;
moduleVariables?: Record<string, string>;
agentapiMockScript?: string;
}
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const projectDir = "/home/coder/project";
const { id } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
enable_agentapi: "true",
install_cursor_cli: props?.skipCursorCliMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
folder: projectDir,
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
if (!props?.skipCursorCliMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/cursor-agent",
content: await loadTestFile(import.meta.dir, "cursor-cli-mock.sh"),
});
}
return { id };
};
setDefaultTimeout(180 * 1000);
describe("cursor-cli", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
test("agentapi-happy-path", async () => {
const { id } = await setup({});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
await expectAgentAPIStarted(id);
});
test("agentapi-mcp-json", async () => {
const mcpJson = '{"mcpServers": {"test": {"command": "test-cmd", "type": "stdio"}}}';
const { id } = await setup({
moduleVariables: {
mcp: mcpJson,
}
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
const mcpContent = await execContainer(id, [
"bash",
"-c",
`cat '/home/coder/project/.cursor/mcp.json'`,
]);
expect(mcpContent.exitCode).toBe(0);
expect(mcpContent.stdout).toContain("mcpServers");
expect(mcpContent.stdout).toContain("test");
expect(mcpContent.stdout).toContain("test-cmd");
expect(mcpContent.stdout).toContain("/tmp/mcp-hack.sh");
expect(mcpContent.stdout).toContain("coder");
});
test("agentapi-rules-files", async () => {
const rulesContent = "Always use TypeScript";
const { id } = await setup({
moduleVariables: {
rules_files: JSON.stringify({ "typescript.md": rulesContent }),
}
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
const rulesFile = await execContainer(id, [
"bash",
"-c",
`cat '/home/coder/project/.cursor/rules/typescript.md'`,
]);
expect(rulesFile.exitCode).toBe(0);
expect(rulesFile.stdout).toContain(rulesContent);
});
test("agentapi-api-key", async () => {
const apiKey = "test-cursor-api-key-123";
const { id } = await setup({
moduleVariables: {
api_key: apiKey,
}
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
const envCheck = await execContainer(id, [
"bash",
"-c",
`env | grep CURSOR_API_KEY || echo "CURSOR_API_KEY not found"`,
]);
expect(envCheck.stdout).toContain("CURSOR_API_KEY");
});
test("agentapi-model-and-force-flags", async () => {
const model = "sonnet-4";
const { id } = await setup({
moduleVariables: {
model: model,
force: "true",
ai_prompt: "test prompt",
}
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.cursor-cli-module/agentapi-start.log || cat /home/coder/.cursor-cli-module/start.log || true",
]);
expect(startLog.stdout).toContain(`-m ${model}`);
expect(startLog.stdout).toContain("-f");
expect(startLog.stdout).toContain("test prompt");
});
test("agentapi-pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'cursor-pre-install-script'",
post_install_script: "#!/bin/bash\necho 'cursor-post-install-script'",
}
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
const preInstallLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.cursor-cli-module/pre_install.log || true",
]);
expect(preInstallLog.stdout).toContain("cursor-pre-install-script");
const postInstallLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.cursor-cli-module/post_install.log || true",
]);
expect(postInstallLog.stdout).toContain("cursor-post-install-script");
});
test("agentapi-folder-variable", async () => {
const folder = "/tmp/cursor-test-folder";
const { id } = await setup({
moduleVariables: {
folder: folder,
}
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
const installLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.cursor-cli-module/install.log || true",
]);
expect(installLog.stdout).toContain(folder);
});
test("install-test-cursor-cli-latest", async () => {
const { id } = await setup({
skipCursorCliMock: true,
skipAgentAPIMock: true,
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
await expectAgentAPIStarted(id);
})
});
@@ -0,0 +1,179 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/cursor.svg"
}
variable "folder" {
type = string
description = "The folder to run Cursor CLI in."
}
variable "install_cursor_cli" {
type = bool
description = "Whether to install Cursor CLI."
default = true
}
variable "install_agentapi" {
type = bool
description = "Whether to install AgentAPI."
default = true
}
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.5.0"
}
variable "force" {
type = bool
description = "Force allow commands unless explicitly denied"
default = true
}
variable "model" {
type = string
description = "Model to use (e.g., sonnet-4, sonnet-4-thinking, gpt-5)"
default = ""
}
variable "ai_prompt" {
type = string
description = "AI prompt/task passed to cursor-agent."
default = ""
}
variable "api_key" {
type = string
description = "API key for Cursor CLI."
default = ""
sensitive = true
}
variable "mcp" {
type = string
description = "Workspace-specific MCP JSON to write to folder/.cursor/mcp.json. See https://docs.cursor.com/en/context/mcp#using-mcp-json"
default = null
}
variable "rules_files" {
type = map(string)
description = "Optional map of rule file name to content. Files will be written to folder/.cursor/rules/<name>. See https://docs.cursor.com/en/context/rules#project-rules"
default = null
}
variable "pre_install_script" {
type = string
description = "Optional script to run before installing Cursor CLI."
default = null
}
variable "post_install_script" {
type = string
description = "Optional script to run after installing Cursor CLI."
default = null
}
locals {
app_slug = "cursorcli"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".cursor-cli-module"
}
# Expose status slug and API key to the agent environment
resource "coder_env" "status_slug" {
agent_id = var.agent_id
name = "CODER_MCP_APP_STATUS_SLUG"
value = local.app_slug
}
resource "coder_env" "cursor_api_key" {
count = var.api_key != "" ? 1 : 0
agent_id = var.agent_id
name = "CURSOR_API_KEY"
value = var.api_key
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
agent_id = var.agent_id
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = "Cursor CLI"
cli_app_slug = local.app_slug
cli_app_display_name = "Cursor CLI"
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
ARG_FORCE='${var.force}' \
ARG_MODEL='${var.model}' \
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
ARG_FOLDER='${var.folder}' \
/tmp/start.sh
EOT
install_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_INSTALL='${var.install_cursor_cli}' \
ARG_WORKSPACE_MCP_JSON='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \
ARG_WORKSPACE_RULES_JSON='${var.rules_files != null ? base64encode(jsonencode(var.rules_files)) : ""}' \
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
ARG_FOLDER='${var.folder}' \
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
/tmp/install.sh
EOT
}
@@ -0,0 +1,122 @@
#!/bin/bash
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" > /dev/null 2>&1
}
# Inputs
ARG_INSTALL=${ARG_INSTALL:-true}
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.cursor-cli-module}
ARG_FOLDER=${ARG_FOLDER:-$HOME}
ARG_CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG:-}
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
ARG_WORKSPACE_MCP_JSON=$(echo -n "$ARG_WORKSPACE_MCP_JSON" | base64 -d)
ARG_WORKSPACE_RULES_JSON=$(echo -n "$ARG_WORKSPACE_RULES_JSON" | base64 -d)
echo "--------------------------------"
echo "install: $ARG_INSTALL"
echo "folder: $ARG_FOLDER"
echo "coder_mcp_app_status_slug: $ARG_CODER_MCP_APP_STATUS_SLUG"
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
echo "--------------------------------"
# Install Cursor via official installer if requested
function install_cursor_cli() {
if [ "$ARG_INSTALL" = "true" ]; then
echo "Installing Cursor via official installer..."
set +e
curl https://cursor.com/install -fsS | bash 2>&1
CURL_EXIT=${PIPESTATUS[0]}
set -e
if [ $CURL_EXIT -ne 0 ]; then
echo "Cursor installer failed with exit code $CURL_EXIT"
fi
# Ensure binaries are discoverable; create stable symlink to cursor-agent
CANDIDATES=(
"$(command -v cursor-agent || true)"
"$HOME/.cursor/bin/cursor-agent"
)
FOUND_BIN=""
for c in "${CANDIDATES[@]}"; do
if [ -n "$c" ] && [ -x "$c" ]; then
FOUND_BIN="$c"
break
fi
done
mkdir -p "$HOME/.local/bin"
if [ -n "$FOUND_BIN" ]; then
ln -sf "$FOUND_BIN" "$HOME/.local/bin/cursor-agent"
fi
echo "Installed cursor-agent at: $(command -v cursor-agent || true) (resolved: $FOUND_BIN)"
fi
}
# Write MCP config to user's home if provided (ARG_FOLDER/.cursor/mcp.json)
function write_mcp_config() {
TARGET_DIR="$ARG_FOLDER/.cursor"
TARGET_FILE="$TARGET_DIR/mcp.json"
mkdir -p "$TARGET_DIR"
CURSOR_MCP_HACK_SCRIPT=$(
cat << EOF
#!/usr/bin/env bash
set -e
# --- Set environment variables ---
export CODER_MCP_APP_STATUS_SLUG="${ARG_CODER_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}"
# --- Launch the MCP server ---
exec coder exp mcp server
EOF
)
echo "$CURSOR_MCP_HACK_SCRIPT" > "/tmp/mcp-hack.sh"
chmod +x /tmp/mcp-hack.sh
CODER_MCP=$(
cat << EOF
{
"coder": {
"args": [],
"command": "/tmp/mcp-hack.sh",
"description": "Report ALL tasks and statuses (in progress, done, failed) you are working on.",
"name": "Coder",
"timeout": 3000,
"type": "stdio",
"trust": true
}
}
EOF
)
echo "${ARG_WORKSPACE_MCP_JSON:-{}}" | jq --argjson base "$CODER_MCP" \
'.mcpServers = ((.mcpServers // {}) + $base)' > "$TARGET_FILE"
echo "Wrote workspace MCP to $TARGET_FILE"
}
# Write rules files to user's home (FOLDER/.cursor/rules)
function write_rules_file() {
if [ -n "$ARG_WORKSPACE_RULES_JSON" ]; then
RULES_DIR="$ARG_FOLDER/.cursor/rules"
mkdir -p "$RULES_DIR"
echo "$ARG_WORKSPACE_RULES_JSON" | jq -r 'to_entries[] | @base64' | while read -r entry; do
_jq() { echo "${entry}" | base64 -d | jq -r ${1}; }
NAME=$(_jq '.key')
CONTENT=$(_jq '.value')
echo "$CONTENT" > "$RULES_DIR/$NAME"
echo "Wrote rule: $RULES_DIR/$NAME"
done
fi
}
install_cursor_cli
write_mcp_config
write_rules_file
@@ -0,0 +1,67 @@
#!/bin/bash
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" > /dev/null 2>&1
}
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d)
ARG_FORCE=${ARG_FORCE:-false}
ARG_MODEL=${ARG_MODEL:-}
ARG_OUTPUT_FORMAT=${ARG_OUTPUT_FORMAT:-json}
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.cursor-cli-module}
ARG_FOLDER=${ARG_FOLDER:-$HOME}
echo "--------------------------------"
echo "install: $ARG_INSTALL"
echo "version: $ARG_VERSION"
echo "folder: $ARG_FOLDER"
echo "ai_prompt: $ARG_AI_PROMPT"
echo "force: $ARG_FORCE"
echo "model: $ARG_MODEL"
echo "output_format: $ARG_OUTPUT_FORMAT"
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
echo "folder: $ARG_FOLDER"
echo "--------------------------------"
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
# Find cursor agent cli
if command_exists cursor-agent; then
CURSOR_CMD=cursor-agent
elif [ -x "$HOME/.local/bin/cursor-agent" ]; then
CURSOR_CMD="$HOME/.local/bin/cursor-agent"
else
echo "Error: cursor-agent not found. Install it or set install_cursor_cli=true."
exit 1
fi
# Ensure working directory exists
if [ -d "$ARG_FOLDER" ]; then
cd "$ARG_FOLDER"
else
mkdir -p "$ARG_FOLDER"
cd "$ARG_FOLDER"
fi
ARGS=()
# global flags
if [ -n "$ARG_MODEL" ]; then
ARGS+=("-m" "$ARG_MODEL")
fi
if [ "$ARG_FORCE" = "true" ]; then
ARGS+=("-f")
fi
if [ -n "$ARG_AI_PROMPT" ]; then
printf "AI prompt provided\n"
ARGS+=("Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AI_PROMPT")
fi
# Log and run in background, redirecting all output to the log file
printf "Running: %q %s\n" "$CURSOR_CMD" "$(printf '%q ' "${ARGS[@]}")"
agentapi server --type cursor --term-width 67 --term-height 1190 -- "$CURSOR_CMD" "${ARGS[@]}"
@@ -0,0 +1,14 @@
#!/bin/bash
if [[ "$1" == "--version" ]]; then
echo "HELLO: $(bash -c env)"
echo "cursor-agent version v2.5.0"
exit 0
fi
set -e
while true; do
echo "$(date) - cursor-agent-mock"
sleep 15
done
@@ -107,6 +107,18 @@ describe("gemini", async () => {
expect(resp.stdout).toContain(version_to_install);
});
test("install-gemini-latest", async () => {
const { id } = await setup({
skipGeminiMock: true,
moduleVariables: {
install_gemini: "true",
gemini_version: "",
},
});
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("gemini-settings-json", async () => {
const settings = '{"foo": "bar"}';
const { id } = await setup({
@@ -0,0 +1,90 @@
---
display_name: Amp CLI
icon: ../../../../.icons/sourcegraph-amp.svg
description: Sourcegraph's AI coding agent with deep codebase understanding and intelligent code search capabilities
verified: false
tags: [agent, sourcegraph, amp, ai, tasks]
---
# Sourcegraph Amp CLI
Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI-powered code search and analysis tools, with AgentAPI integration for seamless Coder Tasks support.
```tf
module "amp-cli" {
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
install_sourcegraph_amp = true
agentapi_version = "latest"
}
```
## Prerequisites
- Include the [Coder Login](https://registry.coder.com/modules/coder-login/coder) module in your template
- Node.js and npm are automatically installed (via NVM) if not already available
## Usage Example
```tf
data "coder_parameter" "ai_prompt" {
name = "AI Prompt"
description = "Write an initial prompt for Amp to work on."
type = "string"
default = ""
mutable = true
}
# Set system prompt for Amp CLI via environment variables
resource "coder_agent" "main" {
# ...
env = {
SOURCEGRAPH_AMP_SYSTEM_PROMPT = <<-EOT
You are an Amp assistant that helps developers debug and write code efficiently.
Always log task status to Coder.
EOT
SOURCEGRAPH_AMP_TASK_PROMPT = data.coder_parameter.ai_prompt.value
}
}
variable "sourcegraph_amp_api_key" {
type = string
description = "Sourcegraph Amp API key. Get one at https://ampcode.com/settings"
sensitive = true
}
module "amp-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key # recommended for authenticated usage
install_sourcegraph_amp = true
}
```
## How it Works
- **Install**: Installs Sourcegraph Amp CLI using npm (installs Node.js via NVM if required)
- **Start**: Launches Amp CLI in the specified directory, wrapped with AgentAPI to enable tasks and AI interactions
- **Environment Variables**: Sets `SOURCEGRAPH_AMP_API_KEY` and `SOURCEGRAPH_AMP_START_DIRECTORY` for the CLI execution
## Troubleshooting
- If `amp` is not found, ensure `install_sourcegraph_amp = true` and your API key is valid
- Logs are written under `/home/coder/.sourcegraph-amp-module/` (`install.log`, `agentapi-start.log`) for debugging
- If AgentAPI fails to start, verify that your container has network access and executable permissions for the scripts
> [!IMPORTANT]
> For using **Coder Tasks** with Amp CLI, make sure to pass the `AI Prompt` parameter and set `sourcegraph_amp_api_key`.
> This ensures task reporting and status updates work seamlessly.
## References
- [Amp CLI Documentation](https://ampcode.com/manual)
- [AgentAPI Documentation](https://github.com/coder/agentapi)
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
@@ -0,0 +1,157 @@
import {
test,
afterEach,
describe,
setDefaultTimeout,
beforeAll,
expect,
} from "bun:test";
import { execContainer, readFileContainer, runTerraformInit } from "~test";
import {
loadTestFile,
writeExecutable,
setup as setupUtil,
execModuleScript,
expectAgentAPIStarted,
} from "../../../coder/modules/agentapi/test-util";
let cleanupFunctions: (() => Promise<void>)[] = [];
const registerCleanup = (cleanup: () => Promise<void>) => {
cleanupFunctions.push(cleanup);
};
afterEach(async () => {
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
cleanupFunctions = [];
for (const cleanup of cleanupFnsCopy) {
try {
await cleanup();
} catch (error) {
console.error("Error during cleanup:", error);
}
}
});
interface SetupProps {
skipAgentAPIMock?: boolean;
skipAmpMock?: boolean;
moduleVariables?: Record<string, string>;
agentapiMockScript?: string;
}
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const projectDir = "/home/coder/project";
const { id } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
install_sourcegraph_amp: props?.skipAmpMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
sourcegraph_amp_model: "test-model",
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
// Place the AMP mock CLI binary inside the container
if (!props?.skipAmpMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/amp",
content: await loadTestFile(`${import.meta.dir}`, "amp-mock.sh"),
});
}
return { id };
};
setDefaultTimeout(60 * 1000);
describe("sourcegraph-amp", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
test("happy-path", async () => {
const { id } = await setup();
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("api-key", async () => {
const apiKey = "test-api-key-123";
const { id } = await setup({
moduleVariables: {
sourcegraph_amp_api_key: apiKey,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/agentapi-start.log",
);
expect(resp).toContain("sourcegraph_amp_api_key provided !");
});
test("custom-folder", async () => {
const folder = "/tmp/sourcegraph-amp-test";
const { id } = await setup({
moduleVariables: {
folder,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/install.log",
);
expect(resp).toContain(folder);
});
test("pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
post_install_script: "#!/bin/bash\necho 'post-install-script'",
},
});
await execModuleScript(id);
const preLog = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/pre_install.log",
);
expect(preLog).toContain("pre-install-script");
const postLog = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/post_install.log",
);
expect(postLog).toContain("post-install-script");
});
test("system-prompt", async () => {
const prompt = "this is a system prompt for AMP";
const { id } = await setup();
await execModuleScript(id, {
SOURCEGRAPH_AMP_SYSTEM_PROMPT: prompt,
});
const resp = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/SYSTEM_PROMPT.md",
);
expect(resp).toContain(prompt);
});
test("task-prompt", async () => {
const prompt = "this is a task prompt for AMP";
const { id } = await setup();
await execModuleScript(id, {
SOURCEGRAPH_AMP_TASK_PROMPT: prompt,
});
const resp = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/agentapi-start.log",
);
expect(resp).toContain(`sourcegraph amp task prompt provided : ${prompt}`);
});
});
@@ -0,0 +1,195 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/sourcegraph-amp.svg"
}
variable "folder" {
type = string
description = "The folder to run sourcegraph_amp in."
default = "/home/coder"
}
variable "install_sourcegraph_amp" {
type = bool
description = "Whether to install sourcegraph-amp."
default = true
}
variable "sourcegraph_amp_api_key" {
type = string
description = "sourcegraph-amp API Key"
default = ""
}
resource "coder_env" "sourcegraph_amp_api_key" {
agent_id = var.agent_id
name = "SOURCEGRAPH_AMP_API_KEY"
value = var.sourcegraph_amp_api_key
}
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.3.0"
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing sourcegraph_amp"
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing sourcegraph_amp."
default = null
}
variable "base_amp_config" {
type = string
description = <<-EOT
Base AMP configuration in JSON format. Can be overridden to customize AMP settings.
If empty, defaults enable thinking and todos for autonomous operation. Additional options include:
- "amp.permissions": [] (tool permissions)
- "amp.tools.stopTimeout": 600 (extend timeout for long operations)
- "amp.terminal.commands.nodeSpawn.loadProfile": "daily" (environment loading)
- "amp.tools.disable": ["builtin:open"] (disable tools for containers)
- "amp.git.commit.ampThread.enabled": true (link commits to threads)
- "amp.git.commit.coauthor.enabled": true (add Amp as co-author)
Reference: https://ampcode.com/manual
EOT
default = ""
}
variable "additional_mcp_servers" {
type = string
description = "Additional MCP servers configuration in JSON format to append to amp.mcpServers."
default = null
}
locals {
app_slug = "amp"
default_base_config = {
"amp.anthropic.thinking.enabled" = true
"amp.todos.enabled" = true
}
# Use provided config or default, then extract base settings (excluding mcpServers)
user_config = var.base_amp_config != "" ? jsondecode(var.base_amp_config) : local.default_base_config
base_amp_settings = { for k, v in local.user_config : k => v if k != "amp.mcpServers" }
coder_mcp = {
"coder" = {
"command" = "coder"
"args" = ["exp", "mcp", "server"]
"env" = {
"CODER_MCP_APP_STATUS_SLUG" = local.app_slug
"CODER_MCP_AI_AGENTAPI_URL" = "http://localhost:3284"
}
"type" = "stdio"
}
}
additional_mcp = var.additional_mcp_servers != null ? jsondecode(var.additional_mcp_servers) : {}
merged_mcp_servers = merge(
lookup(local.user_config, "amp.mcpServers", {}),
local.coder_mcp,
local.additional_mcp
)
final_config = merge(local.base_amp_settings, {
"amp.mcpServers" = local.merged_mcp_servers
})
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".sourcegraph-amp-module"
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.0.1"
agent_id = var.agent_id
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = "Sourcegraph Amp"
cli_app_slug = "${local.app_slug}-cli"
cli_app_display_name = "Sourcegraph Amp CLI"
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
SOURCEGRAPH_AMP_API_KEY='${var.sourcegraph_amp_api_key}' \
SOURCEGRAPH_AMP_START_DIRECTORY='${var.folder}' \
/tmp/start.sh
EOT
install_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_INSTALL_SOURCEGRAPH_AMP='${var.install_sourcegraph_amp}' \
SOURCEGRAPH_AMP_START_DIRECTORY='${var.folder}' \
ARG_AMP_CONFIG="$(echo -n '${base64encode(jsonencode(local.final_config))}' | base64 -d)" \
/tmp/install.sh
EOT
}
@@ -0,0 +1,96 @@
#!/bin/bash
set -euo pipefail
# ANSI colors
BOLD='\033[1m'
echo "--------------------------------"
echo "Install flag: $ARG_INSTALL_SOURCEGRAPH_AMP"
echo "Workspace: $SOURCEGRAPH_AMP_START_DIRECTORY"
echo "--------------------------------"
# Helper function to check if a command exists
command_exists() {
command -v "$1" > /dev/null 2>&1
}
function install_node() {
if ! command_exists npm; then
printf "npm not found, checking for Node.js installation...\n"
if ! command_exists node; then
printf "Node.js not found, installing Node.js via NVM...\n"
export NVM_DIR="$HOME/.nvm"
if [ ! -d "$NVM_DIR" ]; then
mkdir -p "$NVM_DIR"
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
else
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
fi
# Temporarily disable nounset (-u) for nvm to avoid PROVIDED_VERSION error
set +u
nvm install --lts
nvm use --lts
nvm alias default node
set -u
printf "Node.js installed: %s\n" "$(node --version)"
printf "npm installed: %s\n" "$(npm --version)"
else
printf "Node.js is installed but npm is not available. Please install npm manually.\n"
exit 1
fi
fi
}
function install_sourcegraph_amp() {
if [ "${ARG_INSTALL_SOURCEGRAPH_AMP}" = "true" ]; then
install_node
# If nvm is not used, set up user npm global directory
if ! command_exists nvm; then
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then
echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc
fi
fi
printf "%s Installing Sourcegraph AMP CLI...\n" "${BOLD}"
npm install -g @sourcegraph/amp@0.0.1754179307-gba1f97
printf "%s Successfully installed Sourcegraph AMP CLI. Version: %s\n" "${BOLD}" "$(amp --version)"
fi
}
function setup_system_prompt() {
if [ -n "${SOURCEGRAPH_AMP_SYSTEM_PROMPT:-}" ]; then
echo "Setting Sourcegraph AMP system prompt..."
mkdir -p "$HOME/.sourcegraph-amp-module"
echo "$SOURCEGRAPH_AMP_SYSTEM_PROMPT" > "$HOME/.sourcegraph-amp-module/SYSTEM_PROMPT.md"
echo "System prompt saved to $HOME/.sourcegraph-amp-module/SYSTEM_PROMPT.md"
else
echo "No system prompt provided for Sourcegraph AMP."
fi
}
function configure_amp_settings() {
echo "Configuring AMP settings..."
SETTINGS_PATH="$HOME/.config/amp/settings.json"
mkdir -p "$(dirname "$SETTINGS_PATH")"
if [ -z "${ARG_AMP_CONFIG:-}" ]; then
echo "No AMP config provided, skipping configuration"
return
fi
echo "Writing AMP configuration to $SETTINGS_PATH"
printf '%s\n' "$ARG_AMP_CONFIG" > "$SETTINGS_PATH"
echo "AMP configuration complete"
}
install_sourcegraph_amp
setup_system_prompt
configure_amp_settings
@@ -0,0 +1,49 @@
#!/bin/bash
set -euo pipefail
# Load user environment
# shellcheck source=/dev/null
source "$HOME/.bashrc"
# shellcheck source=/dev/null
if [ -f "$HOME/.nvm/nvm.sh" ]; then
source "$HOME"/.nvm/nvm.sh
else
export PATH="$HOME/.npm-global/bin:$PATH"
fi
function ensure_command() {
command -v "$1" &> /dev/null || {
echo "Error: '$1' not found." >&2
exit 1
}
}
ensure_command amp
echo "AMP version: $(amp --version)"
dir="$SOURCEGRAPH_AMP_START_DIRECTORY"
if [[ -d "$dir" ]]; then
echo "Using existing directory: $dir"
else
echo "Creating directory: $dir"
mkdir -p "$dir"
fi
cd "$dir"
if [ -n "$SOURCEGRAPH_AMP_API_KEY" ]; then
printf "sourcegraph_amp_api_key provided !\n"
export AMP_API_KEY=$SOURCEGRAPH_AMP_API_KEY
else
printf "sourcegraph_amp_api_key not provided\n"
fi
if [ -n "${SOURCEGRAPH_AMP_TASK_PROMPT:-}" ]; then
printf "sourcegraph amp task prompt provided : $SOURCEGRAPH_AMP_TASK_PROMPT"
PROMPT="Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $SOURCEGRAPH_AMP_TASK_PROMPT"
# Pipe the prompt into amp, which will be run inside agentapi
agentapi server --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp"
else
printf "No task prompt given.\n"
agentapi server --term-width=67 --term-height=1190 -- amp
fi
@@ -0,0 +1,14 @@
#!/bin/bash
# Mock behavior of the AMP CLI
if [[ "$1" == "--version" ]]; then
echo "AMP CLI mock version v1.0.0"
exit 0
fi
# Simulate AMP running in a loop for AgentAPI to connect
set -e
while true; do
echo "$(date) - AMP mock is running..."
sleep 15
done
@@ -181,7 +181,7 @@ resource "coder_env" "claude_task_prompt" {
resource "coder_env" "app_status_slug" {
agent_id = coder_agent.main.id
name = "CODER_MCP_APP_STATUS_SLUG"
value = "claude-code"
value = "ccw"
}
resource "coder_env" "claude_system_prompt" {
agent_id = coder_agent.main.id
@@ -134,6 +134,7 @@ describe("goose", async () => {
console.log(resp.stderr);
}
expect(resp.exitCode).toBe(0);
await expectAgentAPIStarted(id);
});
test("config", async () => {
+26 -1
View File
@@ -16,7 +16,32 @@ A module that adds JupyterLab in your Coder template.
module "jupyterlab" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jupyterlab/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
}
```
## Configuration
JupyterLab is automatically configured to work with Coder's iframe embedding. For advanced configuration, you can use the `config` parameter to provide additional JupyterLab server settings according to the [JupyterLab configuration documentation](https://jupyter-server.readthedocs.io/en/latest/users/configuration.html).
```tf
module "jupyterlab" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jupyterlab/coder"
version = "1.2.0"
agent_id = coder_agent.example.id
config = {
ServerApp = {
# Required for Coder Tasks iFrame embedding - do not remove
tornado_settings = {
headers = {
"Content-Security-Policy" = "frame-ancestors 'self' ${data.coder_workspace.me.access_url}"
}
}
# Your additional configuration here
root_dir = "/workspace/notebooks"
}
}
}
```
@@ -3,6 +3,8 @@ import {
execContainer,
executeScriptInContainer,
findResourceInstance,
readFileContainer,
removeContainer,
runContainer,
runTerraformApply,
runTerraformInit,
@@ -104,4 +106,57 @@ describe("jupyterlab", async () => {
// const output = await executeScriptInContainerWithPip(state, "alpine");
// ...
// });
it("writes ~/.jupyter/jupyter_server_config.json when config provided", async () => {
const id = await runContainer("alpine");
try {
const config = {
ServerApp: {
port: 8888,
token: "test-token",
password: "",
allow_origin: "*"
}
};
const configJson = JSON.stringify(config);
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
config: configJson,
});
const script = findResourceInstance(state, "coder_script", "jupyterlab_config").script;
const resp = await execContainer(id, ["sh", "-c", script]);
if (resp.exitCode !== 0) {
console.log(resp.stdout);
console.log(resp.stderr);
}
expect(resp.exitCode).toBe(0);
const content = await readFileContainer(id, "/root/.jupyter/jupyter_server_config.json");
// Parse both JSON strings and compare objects to avoid key ordering issues
const actualConfig = JSON.parse(content);
expect(actualConfig).toEqual(config);
} finally {
await removeContainer(id);
}
});
it("creates config script with CSP fallback when config is empty", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
config: "{}",
});
const configScripts = state.resources.filter(
(res) => res.type === "coder_script" && res.name === "jupyterlab_config"
);
expect(configScripts.length).toBe(1);
});
it("creates config script with CSP fallback when config is not provided", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
const configScripts = state.resources.filter(
(res) => res.type === "coder_script" && res.name === "jupyterlab_config"
);
expect(configScripts.length).toBe(1);
});
});
+42
View File
@@ -12,6 +12,23 @@ terraform {
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
locals {
# Fallback config with CSP for Coder iframe embedding when user config is empty
csp_fallback_config = {
ServerApp = {
tornado_settings = {
headers = {
"Content-Security-Policy" = "frame-ancestors 'self' ${data.coder_workspace.me.access_url}"
}
}
}
}
# Use user config if provided, otherwise fallback to CSP config
config_json = var.config == "{}" ? jsonencode(local.csp_fallback_config) : var.config
config_b64 = base64encode(local.config_json)
}
# Add required variables for your modules and remove any unneeded variables
variable "agent_id" {
type = string
@@ -57,6 +74,26 @@ variable "group" {
default = null
}
variable "config" {
type = string
description = "A JSON string of JupyterLab server configuration settings. When set, writes ~/.jupyter/jupyter_server_config.json."
default = "{}"
}
resource "coder_script" "jupyterlab_config" {
agent_id = var.agent_id
display_name = "JupyterLab Config"
icon = "/icon/jupyter.svg"
run_on_start = true
start_blocks_login = false
script = <<-EOT
#!/bin/sh
set -eu
mkdir -p "$HOME/.jupyter"
echo -n "${local.config_b64}" | base64 -d > "$HOME/.jupyter/jupyter_server_config.json"
EOT
}
resource "coder_script" "jupyterlab" {
agent_id = var.agent_id
display_name = "jupyterlab"
@@ -79,4 +116,9 @@ resource "coder_app" "jupyterlab" {
share = var.share
order = var.order
group = var.group
healthcheck {
url = "http://localhost:${var.port}/api"
interval = 5
threshold = 6
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
module "kasmvnc" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kasmvnc/coder"
version = "1.2.1"
version = "1.2.2"
agent_id = coder_agent.example.id
desktop_environment = "xfce"
subdomain = true
+1 -1
View File
@@ -240,7 +240,7 @@ get_http_dir() {
# Check the home directory for overriding values
if [[ -e "$HOME/.vnc/kasmvnc.yaml" ]]; then
d=($(grep -E "^\s*httpd_directory:.*$" /etc/kasmvnc/kasmvnc.yaml))
d=($(grep -E "^\s*httpd_directory:.*$" "$HOME/.vnc/kasmvnc.yaml"))
if [[ $${#d[@]} -eq 2 && -d "$${d[1]}" ]]; then
httpd_directory="$${d[1]}"
fi
+20 -5
View File
@@ -14,7 +14,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.3.1"
version = "1.4.1"
agent_id = coder_agent.example.id
accept_license = true
}
@@ -30,7 +30,7 @@ module "vscode-web" {
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.3.1"
version = "1.4.1"
agent_id = coder_agent.example.id
install_prefix = "/home/coder/.vscode-web"
folder = "/home/coder"
@@ -44,7 +44,7 @@ module "vscode-web" {
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.3.1"
version = "1.4.1"
agent_id = coder_agent.example.id
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
accept_license = true
@@ -59,7 +59,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.3.1"
version = "1.4.1"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
@@ -77,9 +77,24 @@ By default, this module installs the latest. To pin a specific version, retrieve
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.3.1"
version = "1.4.1"
agent_id = coder_agent.example.id
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
accept_license = true
}
```
### Open an existing workspace on startup
To open an existing workspace on startup the `workspace` parameter can be used to represent a path on disk to a `code-workspace` file.
Note: Either `workspace` or `folder` can be used, but not both simultaneously. The `code-workspace` file must already be present on disk.
```tf
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.4.1"
agent_id = coder_agent.example.id
workspace = "/home/coder/coder.code-workspace"
}
```
+20 -2
View File
@@ -158,6 +158,12 @@ variable "platform" {
}
}
variable "workspace" {
type = string
description = "Path to a .code-workspace file to open in vscode-web."
default = ""
}
data "coder_workspace_owner" "me" {}
data "coder_workspace" "me" {}
@@ -178,6 +184,7 @@ resource "coder_script" "vscode-web" {
DISABLE_TRUST : var.disable_trust,
EXTENSIONS_DIR : var.extensions_dir,
FOLDER : var.folder,
WORKSPACE : var.workspace,
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
SERVER_BASE_PATH : local.server_base_path,
COMMIT_ID : var.commit_id,
@@ -195,6 +202,11 @@ resource "coder_script" "vscode-web" {
condition = !var.offline || !var.use_cached
error_message = "Offline and Use Cached can not be used together"
}
precondition {
condition = (var.workspace == "" || var.folder == "")
error_message = "Set only one of `workspace` or `folder`."
}
}
}
@@ -218,6 +230,12 @@ resource "coder_app" "vscode-web" {
locals {
server_base_path = var.subdomain ? "" : format("/@%s/%s/apps/%s/", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.slug)
url = var.folder == "" ? "http://localhost:${var.port}${local.server_base_path}" : "http://localhost:${var.port}${local.server_base_path}?folder=${var.folder}"
healthcheck_url = var.subdomain ? "http://localhost:${var.port}/healthz" : "http://localhost:${var.port}${local.server_base_path}/healthz"
url = (
var.workspace != "" ?
"http://localhost:${var.port}${local.server_base_path}?workspace=${urlencode(var.workspace)}" :
var.folder != "" ?
"http://localhost:${var.port}${local.server_base_path}?folder=${urlencode(var.folder)}" :
"http://localhost:${var.port}${local.server_base_path}"
)
healthcheck_url = var.subdomain ? "http://localhost:${var.port}/healthz" : "http://localhost:${var.port}${local.server_base_path}/healthz"
}
+18 -9
View File
@@ -109,18 +109,27 @@ if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
if ! command -v jq > /dev/null; then
echo "jq is required to install extensions from a workspace file."
else
WORKSPACE_DIR="$HOME"
if [ -n "${FOLDER}" ]; then
WORKSPACE_DIR="${FOLDER}"
fi
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
# Use sed to remove single-line comments before parsing with jq
extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR"/.vscode/extensions.json | jq -r '.recommendations[]')
# Prefer WORKSPACE if set and points to a file
if [ -n "${WORKSPACE}" ] && [ -f "${WORKSPACE}" ]; then
printf "🧩 Installing extensions from %s...\n" "${WORKSPACE}"
# Strip single-line comments then parse .extensions.recommendations[]
extensions=$(sed 's|//.*||g' "${WORKSPACE}" | jq -r '(.extensions.recommendations // [])[]')
for extension in $extensions; do
$VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
done
else
# Fallback to folder-based .vscode/extensions.json (existing behavior)
WORKSPACE_DIR="$HOME"
if [ -n "${FOLDER}" ]; then
WORKSPACE_DIR="${FOLDER}"
fi
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR/.vscode/extensions.json" | jq -r '.recommendations[]')
for extension in $extensions; do
$VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
done
fi
fi
fi
fi
Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

+13
View File
@@ -0,0 +1,13 @@
---
display_name: "Muhammad Uamir Ali"
bio: "Cloud Engineer | Infrastructure as code, Kubernetes | SRE"
github: "m4rrypro"
avatar: "./.images/avatar.jpeg"
linkedin: "https://www.linkedin.com/in/m4rry"
support_email: "m.umair.ali200@gmail.com"
status: "community"
---
# Muhammad Umair Ali
Cloud Engineer | Infrastructure as code, Kubernetes | SRE
@@ -0,0 +1,161 @@
---
display_name: Proxmox VM
description: Provision VMs on Proxmox VE as Coder workspaces
icon: ../../../../.icons/proxmox.svg
verified: false
tags: [proxmox, vm, cloud-init, qemu]
---
# Proxmox VM Template for Coder
Provision Linux VMs on Proxmox as [Coder workspaces](https://coder.com/docs/workspaces). The template clones a cloudinit base image, injects userdata via Snippets, and runs the Coder agent under the workspace owner's Linux user.
## Prerequisites
- Proxmox VE 8/9
- Proxmox API token with access to nodes and storages
- SSH access from Coder provisioner to Proxmox VE
- Storage with "Snippets" content enabled
- Ubuntu cloudinit image/template on Proxmox
- Latest images: https://cloud-images.ubuntu.com/ ([source](https://cloud-images.ubuntu.com/))
## Prepare a Proxmox CloudInit Template (once)
Run on the Proxmox node. This uses a RELEASE variable so you always pull a current image.
```bash
# Choose a release (e.g., jammy or noble)
RELEASE=jammy
IMG_URL="https://cloud-images.ubuntu.com/${RELEASE}/current/${RELEASE}-server-cloudimg-amd64.img"
IMG_PATH="/var/lib/vz/template/iso/${RELEASE}-server-cloudimg-amd64.img"
# Download cloud image
wget "$IMG_URL" -O "$IMG_PATH"
# Create base VM (example ID 999), enable QGA, correct boot order
NAME="ubuntu-${RELEASE}-cloudinit"
qm create 999 --name "$NAME" --memory 4096 --cores 2 \
--net0 virtio,bridge=vmbr0 --agent enabled=1
qm set 999 --scsihw virtio-scsi-pci
qm importdisk 999 "$IMG_PATH" local-lvm
qm set 999 --scsi0 local-lvm:vm-999-disk-0
qm set 999 --ide2 local-lvm:cloudinit
qm set 999 --serial0 socket --vga serial0
qm set 999 --boot 'order=scsi0;ide2;net0'
# Enable Snippets on storage 'local' (onetime)
pvesm set local --content snippets,vztmpl,backup,iso
# Convert to template
qm template 999
```
Verify:
```bash
qm config 999 | grep -E 'template:|agent:|boot:|ide2:|scsi0:'
```
### Enable Snippets via GUI
- Datacenter → Storage → select `local` → Edit → Content → check "Snippets" → OK
- Ensure `/var/lib/vz/snippets/` exists on the node for snippet files
- Template page → CloudInit → Snippet Storage: `local` → File: your yml → Apply
## Configure this template
Edit `terraform.tfvars` with your environment:
```hcl
# Proxmox API
proxmox_api_url = "https://<PVE_HOST>:8006/api2/json"
proxmox_api_token_id = "<USER@REALM>!<TOKEN>"
proxmox_api_token_secret = "<SECRET>"
# SSH to the node (for snippet upload)
proxmox_host = "<PVE_HOST>"
proxmox_password = "<NODE_ROOT_OR_SUDO_PASSWORD>"
proxmox_ssh_user = "root"
# Infra defaults
proxmox_node = "pve"
disk_storage = "local-lvm"
snippet_storage = "local"
bridge = "vmbr0"
vlan = 0
clone_template_vmid = 999
```
### Variables (terraform.tfvars)
- These values are standard Terraform variables that the template reads at apply time.
- Place secrets (e.g., `proxmox_api_token_secret`, `proxmox_password`) in `terraform.tfvars` or inject with environment variables using `TF_VAR_*` (e.g., `TF_VAR_proxmox_api_token_secret`).
- You can also override with `-var`/`-var-file` if you run Terraform directly. With Coder, the repo's `terraform.tfvars` is bundled when pushing the template.
Variables expected:
- `proxmox_api_url`, `proxmox_api_token_id`, `proxmox_api_token_secret` (sensitive)
- `proxmox_host`, `proxmox_password` (sensitive), `proxmox_ssh_user`
- `proxmox_node`, `disk_storage`, `snippet_storage`, `bridge`, `vlan`, `clone_template_vmid`
- Coder parameters: `cpu_cores`, `memory_mb`, `disk_size_gb`
## Proxmox API Token (GUI/CLI)
Docs: https://pve.proxmox.com/wiki/User_Management#pveum_tokens
GUI:
1. (Optional) Create automation user: Datacenter → Permissions → Users → Add (e.g., `terraform@pve`)
2. Permissions: Datacenter → Permissions → Add → User Permission
- Path: `/` (or narrower covering your nodes/storages)
- Role: `PVEVMAdmin` + `PVEStorageAdmin` (or `PVEAdmin` for simplicity)
3. Token: Datacenter → Permissions → API Tokens → Add → copy Token ID and Secret
4. Test:
```bash
curl -k -H "Authorization: PVEAPIToken=<USER@REALM>!<TOKEN>=<SECRET>" \
https:// < PVE_HOST > :8006/api2/json/version
```
CLI:
```bash
pveum user add terraform@pve --comment 'Terraform automation user'
pveum aclmod / -user terraform@pve -role PVEAdmin
pveum user token add terraform@pve terraform --privsep 0
```
## Use
```bash
# From this directory
coder templates push --yes proxmox-cloudinit --directory . | cat
```
Create a workspace from the template in the Coder UI. First boot usually takes 60120s while cloudinit runs.
## How it works
- Uploads rendered cloudinit userdata to `<storage>:snippets/<vm>.yml` via the provider's `proxmox_virtual_environment_file`
- VM config: `virtio-scsi-pci`, boot order `scsi0, ide2, net0`, QGA enabled
- Linux user equals Coder workspace owner (sanitized). To avoid collisions, reserved names (`admin`, `root`, etc.) get a suffix (e.g., `admin1`). User is created with `primary_group: adm`, `groups: [sudo]`, `no_user_group: true`
- systemd service runs as that user:
- `coder-agent.service`
## Troubleshooting quick hits
- iPXE boot loop: ensure template has bootable root disk and boot order `scsi0,ide2,net0`
- QGA not responding: install/enable QGA in template; allow 60120s on first boot
- Snippet upload errors: storage must include `Snippets`; token needs Datastore permissions; path format `<storage>:snippets/<file>` handled by provider
- Permissions errors: ensure the token's role covers the target node(s) and storages
- Verify snippet/QGA: `qm config <vmid> | egrep 'cicustom|ide2|ciuser'`
## References
- Ubuntu Cloud Images (latest): https://cloud-images.ubuntu.com/ ([source](https://cloud-images.ubuntu.com/))
- Proxmox qm(1) manual: https://pve.proxmox.com/pve-docs/qm.1.html
- Proxmox CloudInit Support: https://pve.proxmox.com/wiki/Cloud-Init_Support
- Terraform Proxmox provider (bpg): `bpg/proxmox` on the Terraform Registry
- Coder Best practices & templates:
- https://coder.com/docs/tutorials/best-practices/speed-up-templates
- https://coder.com/docs/tutorials/template-from-scratch
@@ -0,0 +1,53 @@
#cloud-config
hostname: ${hostname}
users:
- name: ${linux_user}
groups: [sudo]
shell: /bin/bash
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
package_update: false
package_upgrade: false
packages:
- curl
- ca-certificates
- git
- jq
write_files:
- path: /opt/coder/init.sh
permissions: "0755"
owner: root:root
encoding: b64
content: |
${coder_init_script_b64}
- path: /etc/systemd/system/coder-agent.service
permissions: "0644"
owner: root:root
content: |
[Unit]
Description=Coder Agent
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=${linux_user}
WorkingDirectory=/home/${linux_user}
Environment=HOME=/home/${linux_user}
Environment=CODER_AGENT_TOKEN=${coder_token}
ExecStart=/opt/coder/init.sh
OOMScoreAdjust=-1000
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
runcmd:
- systemctl daemon-reload
- systemctl enable --now coder-agent.service
final_message: "Cloud-init complete on ${hostname}"
+283
View File
@@ -0,0 +1,283 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
proxmox = {
source = "bpg/proxmox"
}
}
}
provider "coder" {}
provider "proxmox" {
endpoint = var.proxmox_api_url
api_token = "${var.proxmox_api_token_id}=${var.proxmox_api_token_secret}"
insecure = true
# SSH is needed for file uploads to Proxmox
ssh {
username = var.proxmox_ssh_user
password = var.proxmox_password
node {
name = var.proxmox_node
address = var.proxmox_host
}
}
}
variable "proxmox_api_url" {
type = string
}
variable "proxmox_api_token_id" {
type = string
sensitive = true
}
variable "proxmox_api_token_secret" {
type = string
sensitive = true
}
variable "proxmox_host" {
description = "Proxmox node IP or DNS for SSH"
type = string
}
variable "proxmox_password" {
description = "Proxmox password (used for SSH)"
type = string
sensitive = true
}
variable "proxmox_ssh_user" {
description = "SSH username on Proxmox node"
type = string
default = "root"
}
variable "proxmox_node" {
description = "Target Proxmox node"
type = string
default = "pve"
}
variable "disk_storage" {
description = "Disk storage (e.g., local-lvm)"
type = string
default = "local-lvm"
}
variable "snippet_storage" {
description = "Storage with Snippets content"
type = string
default = "local"
}
variable "bridge" {
description = "Bridge (e.g., vmbr0)"
type = string
default = "vmbr0"
}
variable "vlan" {
description = "VLAN tag (0 none)"
type = number
default = 0
}
variable "clone_template_vmid" {
description = "VMID of the cloud-init base template to clone"
type = number
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "cpu_cores" {
name = "cpu_cores"
display_name = "CPU Cores"
type = "number"
default = 2
mutable = true
}
data "coder_parameter" "memory_mb" {
name = "memory_mb"
display_name = "Memory (MB)"
type = "number"
default = 4096
mutable = true
}
data "coder_parameter" "disk_size_gb" {
name = "disk_size_gb"
display_name = "Disk Size (GB)"
type = "number"
default = 20
mutable = true
validation {
min = 10
max = 100
monotonic = "increasing"
}
}
resource "coder_agent" "dev" {
arch = "amd64"
os = "linux"
env = {
GIT_AUTHOR_NAME = data.coder_workspace_owner.me.name
GIT_AUTHOR_EMAIL = data.coder_workspace_owner.me.email
}
startup_script_behavior = "non-blocking"
startup_script = <<-EOT
set -e
# Add any startup scripts here
EOT
metadata {
display_name = "CPU Usage"
key = "cpu_usage"
script = "coder stat cpu"
interval = 10
timeout = 1
order = 1
}
metadata {
display_name = "RAM Usage"
key = "ram_usage"
script = "coder stat mem"
interval = 10
timeout = 1
order = 2
}
metadata {
display_name = "Disk Usage"
key = "disk_usage"
script = "coder stat disk"
interval = 600
timeout = 30
order = 3
}
}
locals {
hostname = lower(data.coder_workspace.me.name)
vm_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${local.hostname}"
snippet_filename = "${local.vm_name}.yml"
base_user = replace(replace(replace(lower(data.coder_workspace_owner.me.name), " ", "-"), "/", "-"), "@", "-") # to avoid special characters in the username
linux_user = contains(["root", "admin", "daemon", "bin", "sys"], local.base_user) ? "${local.base_user}1" : local.base_user # to avoid conflict with system users
rendered_user_data = templatefile("${path.module}/cloud-init/user-data.tftpl", {
coder_token = coder_agent.dev.token
coder_init_script_b64 = base64encode(coder_agent.dev.init_script)
hostname = local.vm_name
linux_user = local.linux_user
})
}
resource "proxmox_virtual_environment_file" "cloud_init_user_data" {
content_type = "snippets"
datastore_id = var.snippet_storage
node_name = var.proxmox_node
source_raw {
data = local.rendered_user_data
file_name = local.snippet_filename
}
}
resource "proxmox_virtual_environment_vm" "workspace" {
name = local.vm_name
node_name = var.proxmox_node
clone {
node_name = var.proxmox_node
vm_id = var.clone_template_vmid
full = false
retries = 5
}
agent {
enabled = true
}
on_boot = true
started = true
startup {
order = 1
}
scsi_hardware = "virtio-scsi-pci"
boot_order = ["scsi0", "ide2"]
memory {
dedicated = data.coder_parameter.memory_mb.value
}
cpu {
cores = data.coder_parameter.cpu_cores.value
sockets = 1
type = "host"
}
network_device {
bridge = var.bridge
model = "virtio"
vlan_id = var.vlan == 0 ? null : var.vlan
}
vga {
type = "serial0"
}
serial_device {
device = "socket"
}
disk {
interface = "scsi0"
datastore_id = var.disk_storage
size = data.coder_parameter.disk_size_gb.value
}
initialization {
type = "nocloud"
datastore_id = var.disk_storage
user_data_file_id = proxmox_virtual_environment_file.cloud_init_user_data.id
ip_config {
ipv4 {
address = "dhcp"
}
}
}
tags = ["coder", "workspace", local.vm_name]
depends_on = [proxmox_virtual_environment_file.cloud_init_user_data]
}
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.3.1"
agent_id = coder_agent.dev.id
}
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.3.0"
agent_id = coder_agent.dev.id
}
+38 -32
View File
@@ -28,7 +28,6 @@ JSON_OUTPUT='{
readonly EXIT_SUCCESS=0
readonly EXIT_ERROR=1
readonly EXIT_NO_ACTION_NEEDED=2
readonly EXIT_VALIDATION_FAILED=3
usage() {
cat << EOF
@@ -52,7 +51,7 @@ EXAMPLES:
$0 -m code-server -d # Target specific module
$0 -n coder -m code-server -d # Target module in namespace
Exit codes: 0=success, 1=error, 2=no action needed, 3=validation failed
Exit codes: 0=success, 1=error, 2=no action needed
EOF
exit 0
}
@@ -103,8 +102,7 @@ add_json_error() {
local details="${3:-}"
local exit_code="${4:-1}"
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg type "$type" --arg msg "$message" --arg details "$details" --argjson code "$exit_code" \
'.errors += [{"type": $type, "message": $msg, "details": $details, "exit_code": $code}]')
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg type "$type" --arg msg "$message" --arg details "$details" --argjson code "$exit_code" '.errors += [{"type": $type, "message": $msg, "details": $details, "exit_code": $code}]')
}
add_json_warning() {
@@ -112,8 +110,7 @@ add_json_warning() {
local message="$2"
local type="$3"
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg module "$module" --arg msg "$message" --arg type "$type" \
'.warnings += [{"module": $module, "message": $msg, "type": $type}]')
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg module "$module" --arg msg "$message" --arg type "$type" '.warnings += [{"module": $module, "message": $msg, "type": $type}]')
}
add_json_module() {
@@ -125,9 +122,7 @@ add_json_module() {
local status="$6"
local already_existed="$7"
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg ns "$namespace" --arg name "$module_name" --arg path "$path" \
--arg version "$version" --arg tag "$tag_name" --arg status "$status" --argjson existed "$already_existed" \
'.modules += [{"namespace": $ns, "module_name": $name, "path": $path, "version": $version, "tag_name": $tag, "status": $status, "already_existed": $existed}]')
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg ns "$namespace" --arg name "$module_name" --arg path "$path" --arg version "$version" --arg tag "$tag_name" --arg status "$status" --argjson existed "$already_existed" '.modules += [{"namespace": $ns, "module_name": $name, "path": $path, "version": $version, "tag_name": $tag, "status": $status, "already_existed": $existed}]')
}
parse_arguments() {
@@ -234,29 +229,38 @@ extract_version_from_readme() {
return 1
}
local version_line
version_line=$(grep -E "source\s*=\s*\"registry\.coder\.com/${namespace}/${module_name}" "$readme_path" | head -1 || echo "")
local version
version=$(extract_version_from_module_block "$readme_path" "$namespace" "$module_name")
if [ -n "$version_line" ]; then
local version
version=$(echo "$version_line" | sed -n 's/.*version\s*=\s*"\([^"]*\)".*/\1/p')
if [ -n "$version" ]; then
log "DEBUG" "Found version '$version' from source line: $version_line"
echo "$version"
return 0
fi
fi
local fallback_version
fallback_version=$(grep -E 'version\s*=\s*"[0-9]+\.[0-9]+\.[0-9]+"' "$readme_path" | head -1 | sed 's/.*version\s*=\s*"\([^"]*\)".*/\1/' || echo "")
if [ -n "$fallback_version" ]; then
log "DEBUG" "Found fallback version '$fallback_version'"
echo "$fallback_version"
if [ -n "$version" ]; then
log "DEBUG" "Found version '$version' from module block for $namespace/$module_name"
echo "$version"
return 0
fi
log "DEBUG" "No version found in $readme_path"
log "DEBUG" "No version found in module block for $namespace/$module_name in $readme_path"
return 1
}
extract_version_from_module_block() {
local readme_path="$1"
local namespace="$2"
local module_name="$3"
local version
version=$(grep -A 10 "source[[:space:]]*=[[:space:]]*\"registry\.coder\.com/${namespace}/${module_name}/coder" "$readme_path" \
| sed '/^[[:space:]]*}/q' \
| grep -E "version[[:space:]]*=[[:space:]]*\"[^\"]+\"" \
| head -1 \
| sed 's/.*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/')
if [ -n "$version" ]; then
log "DEBUG" "Found version '$version' for $namespace/$module_name"
echo "$version"
return 0
fi
log "DEBUG" "No version found within module block for $namespace/$module_name"
return 1
}
@@ -304,7 +308,9 @@ detect_modules_needing_tags() {
fi
local all_modules
all_modules=$(find registry -mindepth 3 -maxdepth 3 -type d -path "*/modules/*" | sort -u || echo "")
# Find all module directories, excluding hidden directories
# This works on both macOS and Linux
all_modules=$(find registry -mindepth 3 -maxdepth 3 -type d -path "*/modules/*" ! -name ".*" | sort -u || echo "")
[ -z "$all_modules" ] && {
log "ERROR" "No modules found to check"
@@ -550,7 +556,7 @@ create_and_push_tags() {
echo ""
fi
if [ $pushed_tags -gt 0 ]; then
if [ "$pushed_tags" -gt 0 ]; then
if [[ "$OUTPUT_FORMAT" != "json" ]]; then
log "SUCCESS" "🎉 Successfully created and pushed $pushed_tags release tags!"
echo ""
@@ -610,7 +616,7 @@ main() {
detect_exit_code=$?
case $detect_exit_code in
$EXIT_NO_ACTION_NEEDED)
"$EXIT_NO_ACTION_NEEDED")
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
finalize_json_output "$@"
else
@@ -618,7 +624,7 @@ main() {
fi
exit $EXIT_SUCCESS
;;
$EXIT_ERROR)
"$EXIT_ERROR")
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq '.summary.operation_status = "scan_failed"')
finalize_json_output "$@"