Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54a7bb0001 | |||
| 50f4d5388b | |||
| 36943d1dfb | |||
| e7d705bf98 | |||
| 898219b16b | |||
| fc071e0930 | |||
| d516aff908 | |||
| ccdca6daf5 | |||
| ce039f64df | |||
| 8acda84dd7 | |||
| 76c1299968 | |||
| 60372ff797 | |||
| f28bcdb713 | |||
| cb553209a5 | |||
| 5d0504aef9 | |||
| c1c0dec90f | |||
| 59b67c2c98 | |||
| 7abe422e0a | |||
| db8217e4e5 | |||
| f75afeb0c8 | |||
| 182e5548e2 | |||
| d057a820c1 | |||
| b4e9545c35 | |||
| 50ac3b31f6 | |||
| 056937a758 | |||
| af8b4f02fd | |||
| 2de6a57a3f | |||
| 60fec19d7d | |||
| 44354b202d | |||
| 80acbd7e3a | |||
| 80f429faf1 | |||
| e516446d03 | |||
| f0045397d4 | |||
| 6af8508bc0 | |||
| d212de47ed | |||
| 54b9bf3038 | |||
| cb990bbee0 | |||
| 213aabb3b0 | |||
| 2937286712 | |||
| 8d556a8ab7 | |||
| 16015559e2 | |||
| f1010ee7a6 | |||
| 17734c073a | |||
| 6813e0b5b8 | |||
| 9e47369905 | |||
| d9d44ca338 | |||
| 7152b85246 | |||
| 41c6bece3e | |||
| 9452763f7d | |||
| 77328656ff | |||
| c4c484089f | |||
| 7e53098bea | |||
| 901043bb01 | |||
| 35e64f2e4a | |||
| 65edb54e88 | |||
| c270edfdab | |||
| f712d1c55b | |||
| bc383a32f3 | |||
| a9b015044f | |||
| e94dfd2df6 | |||
| 9125a52f57 | |||
| c8441fc593 | |||
| 62951f1fca | |||
| 6bebc02122 | |||
| 97b036e7d4 | |||
| 240643d3b0 | |||
| 68f881e220 | |||
| 94d938156d | |||
| 1d30ac954d | |||
| a468ec68ea | |||
| 52c1d47161 | |||
| b206a6870c |
@@ -1,5 +1,3 @@
|
||||
Closes #
|
||||
|
||||
## Description
|
||||
|
||||
<!-- Briefly describe what this PR does and why -->
|
||||
@@ -7,6 +5,7 @@ Closes #
|
||||
## Type of Change
|
||||
|
||||
- [ ] New module
|
||||
- [ ] New template
|
||||
- [ ] Bug fix
|
||||
- [ ] Feature/enhancement
|
||||
- [ ] Documentation
|
||||
@@ -20,10 +19,16 @@ Closes #
|
||||
**New version:** `v1.0.0`
|
||||
**Breaking change:** [ ] Yes [ ] No
|
||||
|
||||
## Template Information
|
||||
|
||||
<!-- Delete this section if not applicable -->
|
||||
|
||||
**Path:** `registry/[namespace]/templates/[template-name]`
|
||||
|
||||
## Testing & Validation
|
||||
|
||||
- [ ] Tests pass (`bun test`)
|
||||
- [ ] Code formatted (`bun run fmt`)
|
||||
- [ ] Code formatted (`bun fmt`)
|
||||
- [ ] Changes tested locally
|
||||
|
||||
## Related Issues
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../AGENTS.md
|
||||
@@ -4,3 +4,7 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
@@ -192,8 +192,8 @@ main() {
|
||||
|
||||
# Always run formatter to ensure consistent formatting
|
||||
echo "🔧 Running formatter to ensure consistent formatting..."
|
||||
if command -v bun >/dev/null 2>&1; then
|
||||
bun fmt >/dev/null 2>&1 || echo "⚠️ Warning: bun fmt failed, but continuing..."
|
||||
if command -v bun > /dev/null 2>&1; then
|
||||
bun fmt > /dev/null 2>&1 || echo "⚠️ Warning: bun fmt failed, but continuing..."
|
||||
else
|
||||
echo "⚠️ Warning: bun not found, skipping formatting"
|
||||
fi
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
[default.extend-words]
|
||||
muc = "muc" # For Munich location code
|
||||
tyo = "tyo" # For Tokyo location code
|
||||
Hashi = "Hashi"
|
||||
HashiCorp = "HashiCorp"
|
||||
mavrickrishi = "mavrickrishi" # Username
|
||||
mavrick = "mavrick" # Username
|
||||
|
||||
[files]
|
||||
extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive
|
||||
@@ -28,6 +28,8 @@ jobs:
|
||||
run: bun install
|
||||
- name: Run TypeScript tests
|
||||
run: bun test
|
||||
- name: Run Terraform tests
|
||||
run: ./scripts/terraform_test_all.sh
|
||||
- name: Run Terraform Validate
|
||||
run: bun terraform-validate
|
||||
validate-style:
|
||||
@@ -48,7 +50,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.35.4
|
||||
uses: crate-ci/typos@v1.38.1
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
@@ -61,7 +63,7 @@ jobs:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v5
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.23.2"
|
||||
- name: Validate contributors
|
||||
|
||||
@@ -30,12 +30,12 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
- name: Authenticate with Google Cloud
|
||||
uses: google-github-actions/auth@b7593ed2efd1c1617e1b0254da33b86225adb2a5
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
|
||||
with:
|
||||
workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github
|
||||
service_account: registry-v2-github@coder-registry-1.iam.gserviceaccount.com
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@cb1e50a9932213ecece00a606661ae9ca44f3397
|
||||
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
|
||||
- name: Deploy to dev.registry.coder.com
|
||||
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch main
|
||||
- name: Deploy to registry.coder.com
|
||||
|
||||
@@ -15,10 +15,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: v2.1
|
||||
version: v2.1
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
name: Create Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "release/*/*/v*.*.*"
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Extract tag information
|
||||
id: tag_info
|
||||
run: |
|
||||
TAG=${GITHUB_REF#refs/tags/}
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
IFS='/' read -ra PARTS <<< "$TAG"
|
||||
NAMESPACE="${PARTS[1]}"
|
||||
MODULE="${PARTS[2]}"
|
||||
VERSION="${PARTS[3]}"
|
||||
|
||||
echo "namespace=$NAMESPACE" >> $GITHUB_OUTPUT
|
||||
echo "module=$MODULE" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "module_path=registry/$NAMESPACE/modules/$MODULE" >> $GITHUB_OUTPUT
|
||||
|
||||
RELEASE_TITLE="$NAMESPACE/$MODULE $VERSION"
|
||||
echo "release_title=$RELEASE_TITLE" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Find previous tag
|
||||
id: prev_tag
|
||||
env:
|
||||
NAMESPACE: ${{ steps.tag_info.outputs.namespace }}
|
||||
MODULE: ${{ steps.tag_info.outputs.module }}
|
||||
CURRENT_TAG: ${{ steps.tag_info.outputs.tag }}
|
||||
run: |
|
||||
PREV_TAG=$(git tag -l "release/$NAMESPACE/$MODULE/v*" | sort -V | grep -B1 "$CURRENT_TAG" | head -1)
|
||||
|
||||
if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "$CURRENT_TAG" ]; then
|
||||
echo "No previous tag found, using initial commit"
|
||||
PREV_TAG=$(git rev-list --max-parents=0 HEAD)
|
||||
fi
|
||||
|
||||
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Previous tag: $PREV_TAG"
|
||||
|
||||
- name: Generate changelog
|
||||
id: changelog
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MODULE_PATH: ${{ steps.tag_info.outputs.module_path }}
|
||||
PREV_TAG: ${{ steps.prev_tag.outputs.prev_tag }}
|
||||
CURRENT_TAG: ${{ steps.tag_info.outputs.tag }}
|
||||
run: |
|
||||
echo "Generating changelog for $MODULE_PATH between $PREV_TAG and $CURRENT_TAG"
|
||||
|
||||
COMMITS=$(git log --oneline --no-merges "$PREV_TAG..$CURRENT_TAG" -- "$MODULE_PATH")
|
||||
|
||||
if [ -z "$COMMITS" ]; then
|
||||
echo "No commits found for this module"
|
||||
echo "changelog=No changes found for this module." >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$PREV_TAG" == release/* ]]; then
|
||||
FULL_CHANGELOG=$(gh api repos/:owner/:repo/releases/generate-notes \
|
||||
--field tag_name="$CURRENT_TAG" \
|
||||
--field previous_tag_name="$PREV_TAG" \
|
||||
--jq '.body')
|
||||
else
|
||||
echo "New module detected, skipping GitHub API"
|
||||
FULL_CHANGELOG=""
|
||||
fi
|
||||
|
||||
MODULE_COMMIT_SHAS=$(git log --format="%H" --no-merges "$PREV_TAG..$CURRENT_TAG" -- "$MODULE_PATH")
|
||||
|
||||
FILTERED_CHANGELOG="## What's Changed\n\n"
|
||||
|
||||
for sha in $MODULE_COMMIT_SHAS; do
|
||||
SHORT_SHA=${sha:0:7}
|
||||
|
||||
COMMIT_LINES=$(echo "$FULL_CHANGELOG" | grep -E "$SHORT_SHA|$(git log --format='%s' -n 1 $sha)" || true)
|
||||
|
||||
if [ -n "$COMMIT_LINES" ]; then
|
||||
FILTERED_CHANGELOG="${FILTERED_CHANGELOG}${COMMIT_LINES}\n"
|
||||
else
|
||||
COMMIT_MSG=$(git log --format="%s" -n 1 $sha)
|
||||
AUTHOR=$(gh api repos/:owner/:repo/commits/$sha --jq '.author.login // .commit.author.name')
|
||||
FILTERED_CHANGELOG="${FILTERED_CHANGELOG}* $COMMIT_MSG by @$AUTHOR\n"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "changelog<<EOF" >> $GITHUB_OUTPUT
|
||||
echo -e "$FILTERED_CHANGELOG" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ steps.tag_info.outputs.tag }}
|
||||
RELEASE_TITLE: ${{ steps.tag_info.outputs.release_title }}
|
||||
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
|
||||
run: |
|
||||
gh release create "$TAG_NAME" \
|
||||
--title "$RELEASE_TITLE" \
|
||||
--notes "$CHANGELOG"
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
|
||||
- name: Comment on PR - Failure
|
||||
if: failure() && steps.version-check.outputs.versions_up_to_date == 'false'
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -163,8 +163,8 @@ linters:
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
- SA4006 # Detects redundant assignments
|
||||
- SA4009 # Detects redundant variable declarations
|
||||
- SA4006 # Detects redundant assignments
|
||||
- SA4009 # Detects redundant variable declarations
|
||||
- SA1019
|
||||
exclusions:
|
||||
generated: lax
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Akamai</title>
|
||||
<path d="M13.0548 0C6.384 0 .961 5.3802.961 12.0078.961 18.6354 6.3698 24 13.0548 24c.6168 0 .6454-.3572.0859-.5293-4.9349-1.5063-8.5352-6.069-8.5352-11.4629 0-5.4656 3.6725-10.0706 8.6934-11.5195C13.8153.3448 13.6716 0 13.0548 0Zm2.3242 1.8223c-5.2648 0-9.5254 4.2606-9.5254 9.5254 0 1.2193.2285 2.3818.6445 3.4433.1722.459.4454.4584.4024.0137-.0287-.3156-.0567-.6447-.0567-.9746 0-5.2648 4.2606-9.5254 9.5254-9.5254 4.9779 0 6.4698 2.2235 6.6563 2.08.2008-.1577-1.808-4.5624-7.6465-4.5624zm.4687 4.0703c-1.8622.0592-3.651.7168-5.1035 1.8554-.2582.2009-.1567.3284.1445.1993 2.4675-1.076 5.5812-1.1046 8.6368-.043 2.0514.7173 3.2413 1.7364 3.3418 1.6934.1578-.0718-1.1915-2.2226-3.6446-3.1407-1.1135-.4196-2.2576-.6-3.375-.5644z" fill="#0096D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 852 B |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="512pt" height="512pt" version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m500.48 262.2-48.18 73.984c-0.73438 1.1367-2 1.8242-3.3555 1.8242-1.3516 0-2.6172-0.6875-3.3516-1.8242l-48.129-73.984c-0.78125-1.2227-0.83594-2.7773-0.14453-4.0547 0.69141-1.2734 2.0195-2.0742 3.4727-2.0898h24.781c-0.007813-29.523-7.7188-58.531-22.375-84.156-14.652-25.629-35.742-46.988-61.184-61.969-2.3711-1.3633-3.8633-3.8594-3.9453-6.5938-0.085937-2.7305 1.2539-5.3125 3.5352-6.8203l27.035-17.613c3.4766-2.3633 8.043-2.3633 11.52 0 28.473 19.934 51.723 46.441 67.773 77.27 16.051 30.828 24.434 65.074 24.438 99.832h24.781c1.4688 0 2.8203 0.80859 3.5156 2.1055 0.69531 1.293 0.62109 2.8633-0.1875 4.0898zm-85.043 79.359c-1.5078-2.2812-4.0898-3.6211-6.8203-3.5391-2.7344 0.085937-5.2305 1.5781-6.5938 3.9492-14.965 25.434-36.305 46.523-61.914 61.188-25.609 14.664-54.602 22.391-84.109 22.422v-24.781c-0.011719-1.4531-0.8125-2.7812-2.0898-3.4727-1.2773-0.69141-2.832-0.63672-4.0547 0.14453l-74.035 47.977c-1.1367 0.73438-1.8242 1.9961-1.8242 3.3516s0.6875 2.6172 1.8242 3.3555l73.984 48.18c1.2227 0.78125 2.7773 0.83594 4.0547 0.14453 1.2734-0.69141 2.0742-2.0234 2.0898-3.4727v-24.68c34.734-0.015624 68.957-8.3984 99.766-24.441 30.812-16.039 57.301-39.27 77.23-67.719 2.3672-3.4766 2.3672-8.043 0-11.52zm-245.45 60.52c-25.434-14.977-46.516-36.328-61.172-61.945-14.652-25.617-22.371-54.617-22.387-84.129h24.781c1.4531-0.011719 2.7812-0.8125 3.4727-2.0898 0.69141-1.2773 0.63672-2.832-0.14453-4.0547l-47.977-74.035c-0.73438-1.1367-1.9961-1.8242-3.3516-1.8242s-2.6172 0.6875-3.3555 1.8242l-48.332 73.984c-0.80859 1.2266-0.88281 2.7969-0.1875 4.0898 0.69531 1.2969 2.0469 2.1055 3.5156 2.1055h24.781c0.015625 34.734 8.3984 68.957 24.438 99.766 16.043 30.812 39.273 57.301 67.723 77.234 3.4766 2.3633 8.043 2.3633 11.52 0l27.086-17.664c2.2109-1.5195 3.4961-4.0625 3.4141-6.7422-0.082032-2.6836-1.5234-5.1406-3.8242-6.5195zm92.16-390.5c-1.2227-0.78125-2.7773-0.83594-4.0547-0.14453-1.2773 0.69141-2.0781 2.0195-2.0898 3.4727v24.73c-34.734 0.015625-68.957 8.3984-99.766 24.438-30.812 16.043-57.301 39.273-77.234 67.723-2.3633 3.4766-2.3633 8.043 0 11.52l17.664 27.086c1.5078 2.2812 4.0898 3.6211 6.8242 3.5352 2.7305-0.082032 5.2266-1.5742 6.5898-3.9453 14.965-25.41 36.289-46.48 61.879-61.133 25.59-14.652 54.555-22.383 84.043-22.426v24.781c0.011719 1.4531 0.8125 2.7812 2.0898 3.4727 1.2773 0.69141 2.832 0.63672 4.0547-0.14453l74.035-47.977c1.1367-0.73438 1.8242-1.9961 1.8242-3.3516s-0.6875-2.6172-1.8242-3.3555zm-6.1445 210.23c-9.0703 0-17.77 3.6055-24.184 10.02-6.4141 6.4141-10.02 15.113-10.02 24.184s3.6055 17.77 10.02 24.184c6.4141 6.4141 15.113 10.02 24.184 10.02s17.77-3.6055 24.184-10.02c6.4141-6.4141 10.02-15.113 10.02-24.184s-3.6055-17.77-10.02-24.184c-6.4141-6.4141-15.113-10.02-24.184-10.02zm90.727-26.828-10.344 14.953c4.0039 6.9414 7.0859 14.375 9.1641 22.117l17.973 2.9688c6.543 1.1445 11.316 6.8242 11.316 13.465v15.055c0 6.6406-4.7734 12.32-11.316 13.465l-17.766 3.125v-0.003907c-2.1562 7.6992-5.3086 15.082-9.3711 21.965l10.238 14.797h0.003906c3.8047 5.4375 3.1562 12.82-1.5352 17.512l-10.648 10.648h-0.003906c-4.6914 4.6953-12.074 5.3438-17.508 1.5391l-14.797-10.238v-0.003907c-6.9453 4.0039-14.379 7.0859-22.121 9.1641l-3.0195 18.023c-1.1445 6.543-6.8242 11.316-13.465 11.316h-15.055c-6.6406 0-12.32-4.7734-13.465-11.316l-3.125-17.766h0.003907c-7.7031-2.1758-15.086-5.3398-21.965-9.4219l-14.797 10.238v0.003907c-5.4375 3.8047-12.82 3.1562-17.512-1.5391l-10.648-10.648c-4.6953-4.6914-5.3438-12.074-1.5391-17.512l10.238-14.797h0.003907c-4.0039-6.9414-7.0859-14.375-9.1641-22.117l-18.023-2.9688c-6.543-1.1445-11.316-6.8242-11.316-13.465v-15.055c0-6.6406 4.7734-12.32 11.316-13.465l17.766-3.125v0.003907c2.1562-7.6992 5.3086-15.082 9.3711-21.965l-10.238-14.797h-0.003906c-3.8047-5.4375-3.1562-12.82 1.5352-17.512l10.648-10.648h0.003906c4.6914-4.6953 12.074-5.3438 17.508-1.5391l14.797 10.238v0.003907c6.9453-4.0039 14.379-7.0859 22.121-9.1641l3.0195-18.023c1.1445-6.543 6.8242-11.316 13.465-11.316h15.055c6.6406 0 12.32 4.7734 13.465 11.316l3.125 17.766h-0.003907c7.6992 2.1562 15.082 5.3086 21.965 9.3711l14.797-10.238v-0.003906c5.4375-3.8047 12.82-3.1562 17.512 1.5352l10.648 10.648v0.003906c4.6875 4.6367 5.3984 11.957 1.6914 17.406zm-36.047 61.031c0-14.504-5.7578-28.41-16.016-38.664-10.254-10.258-24.16-16.016-38.664-16.016s-28.41 5.7578-38.664 16.016c-10.258 10.254-16.016 24.16-16.016 38.664s5.7578 28.41 16.016 38.664c10.254 10.258 24.16 16.016 38.664 16.016 14.5-0.011719 28.398-5.7773 38.652-16.027 10.25-10.254 16.016-24.152 16.027-38.652z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><text x="50%" y="50%" font-size="96px" text-anchor="middle" dominant-baseline="middle" font-family="Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, sans-serif">🔌</text></svg>
|
||||
|
After Width: | Height: | Size: 247 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="251" height="251" viewBox="0 0 251 251" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 47.0195C39.45 49.6394 71.06 81.3272 73.54 120.815H119.61C117.05 55.8589 64.93 3.64245 0 0.942627V47.0195Z" fill="#0DC09D"/>
|
||||
<path d="M73.8 131.324C71.18 170.771 39.49 202.379 0 204.859V250.926C64.96 248.366 117.18 196.249 119.88 131.324H73.8Z" fill="#0DC09D"/>
|
||||
<path d="M176.201 120.545C178.821 81.0972 210.511 49.4894 250.001 47.0095V0.942627C185.041 3.50245 132.821 55.619 130.121 120.545H176.201Z" fill="#0DC09D"/>
|
||||
<path d="M250.001 204.849C210.551 202.229 178.941 170.542 176.461 131.054H130.391C132.951 196.01 185.071 248.226 250.001 250.926V204.849Z" fill="#0DC09D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 693 B |
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="64" viewBox="0 0 25.6 25.6" width="64"><style><![CDATA[.B{stroke-linecap:round}.C{stroke-linejoin:round}.D{stroke-linejoin:miter}.E{stroke-width:.716}]]></style><g fill="none" stroke="#fff"><path d="M18.983 18.636c.163-1.357.114-1.555 1.124-1.336l.257.023c.777.035 1.793-.125 2.4-.402 1.285-.596 2.047-1.592.78-1.33-2.89.596-3.1-.383-3.1-.383 3.053-4.53 4.33-10.28 3.227-11.687-3.004-3.84-8.205-2.024-8.292-1.976l-.028.005c-.57-.12-1.2-.19-1.93-.2-1.308-.02-2.3.343-3.054.914 0 0-9.277-3.822-8.846 4.807.092 1.836 2.63 13.9 5.66 10.25C8.29 15.987 9.36 14.86 9.36 14.86c.53.353 1.167.533 1.834.468l.052-.044a2.01 2.01 0 0 0 .021.518c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.162l-.046.183c.306.245.285 1.76.33 2.842s.116 2.093.337 2.688.48 2.13 2.53 1.7c1.713-.367 3.023-.896 3.143-5.81" fill="#000" stroke="#000" stroke-linecap="butt" stroke-width="2.149" class="D"/><path d="M23.535 15.6c-2.89.596-3.1-.383-3.1-.383 3.053-4.53 4.33-10.28 3.228-11.687-3.004-3.84-8.205-2.023-8.292-1.976l-.028.005a10.31 10.31 0 0 0-1.929-.201c-1.308-.02-2.3.343-3.054.914 0 0-9.278-3.822-8.846 4.807.092 1.836 2.63 13.9 5.66 10.25C8.29 15.987 9.36 14.86 9.36 14.86c.53.353 1.167.533 1.834.468l.052-.044a2.02 2.02 0 0 0 .021.518c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.162l-.046.183c.306.245.52 1.593.484 2.815s-.06 2.06.18 2.716.48 2.13 2.53 1.7c1.713-.367 2.6-1.32 2.725-2.906.088-1.128.286-.962.3-1.97l.16-.478c.183-1.53.03-2.023 1.085-1.793l.257.023c.777.035 1.794-.125 2.39-.402 1.285-.596 2.047-1.592.78-1.33z" fill="#336791" stroke="none"/><g class="E"><g class="B"><path d="M12.814 16.467c-.08 2.846.02 5.712.298 6.4s.875 2.05 2.926 1.612c1.713-.367 2.337-1.078 2.607-2.647l.633-5.017M10.356 2.2S1.072-1.596 1.504 7.033c.092 1.836 2.63 13.9 5.66 10.25C8.27 15.95 9.27 14.907 9.27 14.907m6.1-13.4c-.32.1 5.164-2.005 8.282 1.978 1.1 1.407-.175 7.157-3.228 11.687" class="C"/><path d="M20.425 15.17s.2.98 3.1.382c1.267-.262.504.734-.78 1.33-1.054.49-3.418.615-3.457-.06-.1-1.745 1.244-1.215 1.147-1.652-.088-.394-.69-.78-1.086-1.744-.347-.84-4.76-7.29 1.224-6.333.22-.045-1.56-5.7-7.16-5.782S7.99 8.196 7.99 8.196" stroke-linejoin="bevel"/></g><g class="C"><path d="M11.247 15.768c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.163.35-.49-.002-1.27-.482-1.468-.232-.096-.542-.216-.94.23z"/><path d="M11.196 15.753c-.08-.513.168-1.122.433-1.836.398-1.07 1.316-2.14.582-5.537-.547-2.53-4.22-.527-4.22-.184s.166 1.74-.06 3.365c-.297 2.122 1.35 3.916 3.246 3.733" class="B"/></g></g><g fill="#fff" class="D"><path d="M10.322 8.145c-.017.117.215.43.516.472s.558-.202.575-.32-.215-.246-.516-.288-.56.02-.575.136z" stroke-width=".239"/><path d="M19.486 7.906c.016.117-.215.43-.516.472s-.56-.202-.575-.32.215-.246.516-.288.56.02.575.136z" stroke-width=".119"/></g><path d="M20.562 7.095c.05.92-.198 1.545-.23 2.524-.046 1.422.678 3.05-.413 4.68" class="B C E"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#75aadb" d="M71.4 38.8c-1.5-.6-3.9-1-6.9-1.1-4.2-.1-9 .4-9.2.5v20c13.3.6 15.5-1.7 15.5-1.7 11.6-5.9 4.3-16.2.6-17.7z"/><path fill="#75aadb" d="M64 0C28.6 0 0 28.6 0 64s28.6 64 64 64 64-28.6 64-64S99.3 0 64 0zm28.6 89.8H82L64.4 63.5h-9V84h9v5.8H41.5v-5.7l7.6-.1-.1-45.9c-.8-.2-7.5-.8-7.5-.8V32c1 1 7.9 1.2 7.9 1.2 1.6.1 3.9.2 5.2-.1 9.3-1.7 16.4-.4 16.4-.4 14 3.2 14.2 15.8 10.3 22.6-3.5 5.8-10.3 7.2-10.3 7.2l14.4 21.8 7.2-.1v5.6z"/><path d="M41.595 87.073v-2.726l1.82-.141a59.125 59.125 0 013.752-.144h1.931V37.996l-.938-.127c-.516-.07-2.204-.248-3.752-.397l-2.813-.27v-2.51c0-2.332.027-2.495.39-2.3 1.583.847 10.7 1.07 15.83.388 4.202-.558 11.495-.425 14.035.257 5.483 1.472 9.11 4.646 10.824 9.473.717 2.018.817 5.847.216 8.224-.903 3.572-2.39 6.048-4.865 8.101-1.482 1.23-4.847 3.03-6.145 3.29-.397.079-.772.224-.832.321-.06.098 3.123 5.072 7.075 11.054l7.184 10.876 3.633-.068 3.634-.068V89.8l-5.242-.008-5.24-.007-8.82-13.234-8.817-13.234h-9.178V84.061h9.049V89.8H41.595zm25.158-29.162c3.476-.55 7.265-2.774 8.973-5.263 2.511-3.663 1.537-8.99-2.294-12.547-1.357-1.26-2.205-1.63-4.794-2.1-2.124-.386-8.66-.454-11.706-.122l-1.544.168-.058 10.083-.057 10.082.72.106c1.366.2 8.67-.075 10.76-.407z" fill="#fff" stroke="#fff" stroke-width=".788"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="32" viewBox="0 0 375 375" width="32" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect fill="#0071ff" height="375.001088" rx="58.59392" stroke-width=".91553" width="375.001088" x=".0009759" y="-.0066962"/>
|
||||
<path d="m150.428 322.264c-29.063-6.202-53.897-22.439-73.115-47.804-19.507-25.746-27.838-55.355-25.723-91.414 6.655-62.013 47.667-106.753 99.687-120.411 4.509-.989 8.353-3.462 12.55-1.322 3.22 1.64 6.028 4.467 7.206 7.251 1.25 2.955 1.877 21.54.99 29.331-1.076 9.46-3.877 12.418-14.566 15.388-29.723 10.195-48.105 34.07-53.697 61.017-4.8 29.668 2.951 59.729 21.528 78.727 8.966 8.993 17.92 14.24 30.869 18.086 8.646 2.57 13.393 5.758 15.036 10.102 1.085 2.867 1.63 22.984.779 28.772-1.33 9.046-1.702 9.796-5.792 11.667-5.029 2.3-7.404 2.392-15.752.61zm50.708.29c-3.092-1.402-5.673-4.83-6.73-8.94-.134-9.408-2.366-25.754 1.02-33.373 1.88-4.128 4.65-5.999 12.433-8.396 21.267-6.551 37.593-19.88 46.806-38.213 11.11-22.108 11.877-55.183 1.808-77.975-9.154-20.723-25.7-35.217-48.555-42.534-8.872-2.84-12.004-5.065-12.968-9.21-1.002-4.31-1.435-19.87-.785-28.218.682-8.766 1.249-9.99 6.162-13.318 3.701-2.505 5.482-2.446 17.223.575 36.718 10.077 65.97 33.597 83.026 66.68 18.495 37.034 19.191 86.11 1.742 122.655-17.233 36.09-50.591 62.511-88.622 70.194-8.172 1.65-9.07 1.656-12.56.073z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,22 @@
|
||||
# Ignore symlinks to avoid Prettier errors
|
||||
CLAUDE.md
|
||||
.github/copilot-instructions.md
|
||||
|
||||
# Ignore node_modules and dependencies
|
||||
node_modules/
|
||||
|
||||
# Ignore Terraform files (formatted by terraform fmt)
|
||||
*.tf
|
||||
*.hcl
|
||||
*.tfvars
|
||||
|
||||
# Ignore generated and temporary files
|
||||
.terraform/
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
*.tfstate.lock.info
|
||||
|
||||
# Ignore other files that shouldn't be formatted
|
||||
bun.lock
|
||||
go.sum
|
||||
go.mod
|
||||
@@ -0,0 +1,168 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to AI coding assistants when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
The Coder Registry is a community-driven repository for Terraform modules and templates that extend Coder workspaces. It's organized with:
|
||||
|
||||
- **Modules**: Individual components and tools (IDEs, auth integrations, dev tools)
|
||||
- **Templates**: Complete workspace configurations for different platforms
|
||||
- **Namespaces**: Each contributor has their own namespace under `/registry/[namespace]/`
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Formatting
|
||||
|
||||
```bash
|
||||
bun run fmt # Format all code (Prettier + Terraform)
|
||||
bun run fmt:ci # Check formatting (CI mode)
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Test all modules with .tftest.hcl files
|
||||
bun run test
|
||||
|
||||
# Test specific module (from module directory)
|
||||
terraform init -upgrade
|
||||
terraform test -verbose
|
||||
|
||||
# Validate Terraform syntax
|
||||
./scripts/terraform_validate.sh
|
||||
```
|
||||
|
||||
### Module Creation
|
||||
|
||||
```bash
|
||||
# Generate new module scaffold
|
||||
./scripts/new_module.sh namespace/module-name
|
||||
```
|
||||
|
||||
### TypeScript Testing & Setup
|
||||
|
||||
The repository uses Bun for TypeScript testing with utilities:
|
||||
|
||||
- `test/test.ts` - Testing utilities for container management and Terraform operations
|
||||
- `setup.ts` - Test cleanup (removes .tfstate files and test containers)
|
||||
- Container-based testing with Docker for module validation
|
||||
|
||||
## Architecture & Organization
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
registry/[namespace]/
|
||||
├── README.md # Contributor info with frontmatter
|
||||
├── .images/ # Namespace avatar (avatar.png/svg)
|
||||
├── modules/ # Individual components
|
||||
│ └── [module]/ # Each module has main.tf, README.md, tests
|
||||
└── templates/ # Complete workspace configs
|
||||
└── [template]/ # Each template has main.tf, README.md
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
**Module Structure**: Each module contains:
|
||||
|
||||
- `main.tf` - Terraform implementation
|
||||
- `README.md` - Documentation with YAML frontmatter
|
||||
- `.tftest.hcl` - Terraform test files (required)
|
||||
- `run.sh` - Optional startup script
|
||||
|
||||
**Template Structure**: Each template contains:
|
||||
|
||||
- `main.tf` - Complete Coder template configuration
|
||||
- `README.md` - Documentation with YAML frontmatter
|
||||
- Additional configs, scripts as needed
|
||||
|
||||
### README Frontmatter Requirements
|
||||
|
||||
All modules/templates require YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
display_name: "Module Name"
|
||||
description: "Brief description"
|
||||
icon: "../../../../.icons/tool.svg"
|
||||
verified: false
|
||||
tags: ["tag1", "tag2"]
|
||||
---
|
||||
```
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Module Testing
|
||||
|
||||
- Every module MUST have `.tftest.hcl` test files
|
||||
- Optional `main.test.ts` files for container-based testing or complex business logic validation
|
||||
- Tests use Docker containers with `--network=host` flag
|
||||
- Linux required for testing (Docker Desktop on macOS/Windows won't work)
|
||||
- Use Colima or OrbStack on macOS instead of Docker Desktop
|
||||
|
||||
### Test Utilities
|
||||
|
||||
The `test/test.ts` file provides:
|
||||
|
||||
- `runTerraformApply()` - Execute Terraform with variables
|
||||
- `executeScriptInContainer()` - Run coder_script resources in containers
|
||||
- `testRequiredVariables()` - Validate required variables
|
||||
- Container management functions
|
||||
|
||||
## Validation & Quality
|
||||
|
||||
### Automated Validation
|
||||
|
||||
The Go validation tool (`cmd/readmevalidation/`) checks:
|
||||
|
||||
- Repository structure integrity
|
||||
- Contributor README files
|
||||
- Module and template documentation
|
||||
- Frontmatter format compliance
|
||||
|
||||
### Versioning
|
||||
|
||||
Use semantic versioning for modules:
|
||||
|
||||
- **Patch** (1.2.3 → 1.2.4): Bug fixes
|
||||
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
|
||||
- **Major** (1.2.3 → 2.0.0): Breaking changes
|
||||
|
||||
## Dependencies & Tools
|
||||
|
||||
### Required Tools
|
||||
|
||||
- **Terraform** - Module development and testing
|
||||
- **Docker** - Container-based testing
|
||||
- **Bun** - JavaScript runtime for formatting/scripts
|
||||
- **Go 1.23+** - Validation tooling
|
||||
|
||||
### Development Dependencies
|
||||
|
||||
- Prettier with Terraform and shell plugins
|
||||
- TypeScript for test utilities
|
||||
- Various npm packages for documentation processing
|
||||
|
||||
## Workflow Notes
|
||||
|
||||
### Contributing Process
|
||||
|
||||
1. Create namespace (first-time contributors)
|
||||
2. Generate module/template files using scripts
|
||||
3. Implement functionality and tests
|
||||
4. Run formatting and validation
|
||||
5. Submit PR with appropriate template
|
||||
|
||||
### Testing Workflow
|
||||
|
||||
- All modules must pass `terraform test`
|
||||
- Use `bun run test` for comprehensive testing
|
||||
- Format code with `bun run fmt` before submission
|
||||
- Manual testing recommended for templates
|
||||
|
||||
### Namespace Management
|
||||
|
||||
- Each contributor gets unique namespace
|
||||
- Namespace avatar required (avatar.png/svg in .images/)
|
||||
- Namespace README with contributor info and frontmatter
|
||||
@@ -124,18 +124,23 @@ This script generates:
|
||||
- Accurate description and usage examples
|
||||
- Correct icon path (usually `../../../../.icons/your-icon.svg`)
|
||||
- Proper tags that describe your module
|
||||
3. **Create at least one `.tftest.hcl`** to test your module with `terraform test`
|
||||
3. **Create tests for your module:**
|
||||
- **Terraform tests**: Create a `*.tftest.hcl` file and test with `terraform test`
|
||||
- **TypeScript tests**: Create `main.test.ts` file if your module runs scripts or has business logic that Terraform tests can't cover
|
||||
4. **Add any scripts** or additional files your module needs
|
||||
|
||||
### 4. Test and Submit
|
||||
|
||||
```bash
|
||||
# Test your module (from the module directory)
|
||||
# Test your module
|
||||
cd registry/[namespace]/modules/[module-name]
|
||||
|
||||
# Required: Test Terraform functionality
|
||||
terraform init -upgrade
|
||||
terraform test -verbose
|
||||
|
||||
# Or run all tests in the repo
|
||||
./scripts/terraform_test_all.sh
|
||||
# Optional: Test TypeScript files if you have main.test.ts
|
||||
bun test main.test.ts
|
||||
|
||||
# Format code
|
||||
bun run fmt
|
||||
@@ -343,8 +348,8 @@ coder templates push test-[template-name] -d .
|
||||
terraform init -upgrade
|
||||
terraform test -verbose
|
||||
|
||||
# Test all modules
|
||||
./scripts/terraform_test_all.sh
|
||||
# Optional: If you have TypeScript tests
|
||||
bun test main.test.ts
|
||||
```
|
||||
|
||||
### 3. Maintain Backward Compatibility
|
||||
@@ -393,7 +398,9 @@ Example: `https://github.com/coder/registry/compare/main...your-branch?template=
|
||||
### Every Module Must Have
|
||||
|
||||
- `main.tf` - Terraform code
|
||||
- One or more `.tftest.hcl` files - Working tests with `terraform test`
|
||||
- **Tests**:
|
||||
- `*.tftest.hcl` files with `terraform test` (to test terraform specific logic)
|
||||
- `main.test.ts` file with `bun test` (to test business logic, i.e., `coder_script` to install a package.)
|
||||
- `README.md` - Documentation with frontmatter
|
||||
|
||||
### Every Template Must Have
|
||||
@@ -493,6 +500,10 @@ When reporting bugs, include:
|
||||
2. **No tests** or broken tests
|
||||
3. **Hardcoded values** instead of variables
|
||||
4. **Breaking changes** without defaults
|
||||
5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`) before submitting
|
||||
5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`, and `bun test main.test.ts` if applicable) before submitting
|
||||
|
||||
## For Maintainers
|
||||
|
||||
Guidelines for reviewing PRs, managing releases, and maintaining the registry. [See the maintainer guide for detailed information.](./MAINTAINER.md)
|
||||
|
||||
Happy contributing! 🚀
|
||||
|
||||
@@ -23,6 +23,7 @@ Check that PRs have:
|
||||
- [ ] Working tests (`terraform test`)
|
||||
- [ ] Formatted code (`bun run fmt`)
|
||||
- [ ] Avatar image for new namespaces (`avatar.png` or `avatar.svg` in `.images/`)
|
||||
- [ ] Version label: `version:patch`, `version:minor`, or `version:major`
|
||||
|
||||
### Version Guidelines
|
||||
|
||||
@@ -32,7 +33,8 @@ When reviewing PRs, ensure the version change follows semantic versioning:
|
||||
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
|
||||
- **Major** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing types)
|
||||
|
||||
PRs should clearly indicate the version change (e.g., `v1.2.3 → v1.2.4`).
|
||||
PRs should clearly indicate the intended version change (e.g., `v1.2.3 → v1.2.4`) and include the appropriate label: `version:patch`, `version:minor`, or `version:major`.
|
||||
The “Version Bump” CI uses this label to validate required updates (README version refs, etc.).
|
||||
|
||||
### Validate READMEs
|
||||
|
||||
|
||||
@@ -48,3 +48,7 @@ Simply include that snippet inside your Coder template, defining any data depend
|
||||
## Contributing
|
||||
|
||||
We are always accepting new contributions. [Please see our contributing guide for more information.](./CONTRIBUTING.md)
|
||||
|
||||
## For Maintainers
|
||||
|
||||
Guidelines for maintainers reviewing PRs and managing releases. [See the maintainer guide for more information.](./MAINTAINER.md)
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
"": {
|
||||
"name": "registry",
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.18",
|
||||
"bun-types": "^1.2.18",
|
||||
"@types/bun": "^1.2.21",
|
||||
"bun-types": "^1.2.21",
|
||||
"dedent": "^1.6.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^16.0.0",
|
||||
"marked": "^16.2.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-sh": "^0.18.0",
|
||||
"prettier-plugin-terraform-formatter": "^1.2.1",
|
||||
@@ -21,7 +21,7 @@
|
||||
"packages": {
|
||||
"@reteps/dockerfmt": ["@reteps/dockerfmt@0.3.6", "", {}, "sha512-Tb5wIMvBf/nLejTQ61krK644/CEMB/cpiaIFXqGApfGqO3GwcR3qnI0DbmkFVCl2OyEp8LnLX3EkucoL0+tbFg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
|
||||
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.14", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw=="],
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
|
||||
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||
|
||||
"marked": ["marked@16.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-MUKMXDjsD/eptB7GPzxo4xcnLS6oo7/RHimUMHEDRhUooPwmN9BEpMl7AEOJv3bmso169wHI2wUF9VQgL7zfmA=="],
|
||||
"marked": ["marked@16.2.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg=="],
|
||||
|
||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||
|
||||
|
||||
@@ -94,6 +94,9 @@ func validateCoderModuleReadme(rm coderResourceReadme) []error {
|
||||
for _, err := range validateCoderModuleReadmeBody(rm.body) {
|
||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
for _, err := range validateResourceGfmAlerts(rm.body) {
|
||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
if fmErrs := validateCoderResourceFrontmatter("modules", rm.filePath, rm.frontmatter); len(fmErrs) != 0 {
|
||||
errs = append(errs, fmErrs...)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -16,11 +17,16 @@ import (
|
||||
var (
|
||||
supportedResourceTypes = []string{"modules", "templates"}
|
||||
operatingSystems = []string{"windows", "macos", "linux"}
|
||||
gfmAlertTypes = []string{"NOTE", "IMPORTANT", "CAUTION", "WARNING", "TIP"}
|
||||
|
||||
// TODO: This is a holdover from the validation logic used by the Coder Modules repo. It gives us some assurance, but
|
||||
// realistically, we probably want to parse any Terraform code snippets, and make some deeper guarantees about how it's
|
||||
// structured. Just validating whether it *can* be parsed as Terraform would be a big improvement.
|
||||
terraformVersionRe = regexp.MustCompile(`^\s*\bversion\s+=`)
|
||||
|
||||
// Matches the format "> [!INFO]". Deliberately using a broad pattern to catch formatting issues that can mess up
|
||||
// the renderer for the Registry website
|
||||
gfmAlertRegex = regexp.MustCompile(`^>(\s*)\[!(\w+)\](\s*)(.*)`)
|
||||
)
|
||||
|
||||
type coderResourceFrontmatter struct {
|
||||
@@ -277,3 +283,73 @@ func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
|
||||
}
|
||||
return allReadmeFiles, nil
|
||||
}
|
||||
|
||||
func validateResourceGfmAlerts(readmeBody string) []error {
|
||||
trimmed := strings.TrimSpace(readmeBody)
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
var sourceLine string
|
||||
isInsideGfmQuotes := false
|
||||
isInsideCodeBlock := false
|
||||
|
||||
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
|
||||
for lineScanner.Scan() {
|
||||
sourceLine = lineScanner.Text()
|
||||
|
||||
if strings.HasPrefix(sourceLine, "```") {
|
||||
isInsideCodeBlock = !isInsideCodeBlock
|
||||
continue
|
||||
}
|
||||
if isInsideCodeBlock {
|
||||
continue
|
||||
}
|
||||
|
||||
isInsideGfmQuotes = isInsideGfmQuotes && strings.HasPrefix(sourceLine, "> ")
|
||||
|
||||
currentMatch := gfmAlertRegex.FindStringSubmatch(sourceLine)
|
||||
if currentMatch == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Nested GFM alerts is such a weird mistake that it's probably not really safe to keep trying to process the
|
||||
// rest of the content, so this will prevent any other validations from happening for the given line
|
||||
if isInsideGfmQuotes {
|
||||
errs = append(errs, errors.New("registry does not support nested GFM alerts"))
|
||||
continue
|
||||
}
|
||||
|
||||
leadingWhitespace := currentMatch[1]
|
||||
if len(leadingWhitespace) != 1 {
|
||||
errs = append(errs, errors.New("GFM alerts must have one space between the '>' and the start of the GFM brackets"))
|
||||
}
|
||||
isInsideGfmQuotes = true
|
||||
|
||||
alertHeader := currentMatch[2]
|
||||
upperHeader := strings.ToUpper(alertHeader)
|
||||
if !slices.Contains(gfmAlertTypes, upperHeader) {
|
||||
errs = append(errs, xerrors.Errorf("GFM alert type %q is not supported", alertHeader))
|
||||
}
|
||||
if alertHeader != upperHeader {
|
||||
errs = append(errs, xerrors.Errorf("GFM alerts must be in all caps"))
|
||||
}
|
||||
|
||||
trailingWhitespace := currentMatch[3]
|
||||
if trailingWhitespace != "" {
|
||||
errs = append(errs, xerrors.Errorf("GFM alerts must not have any trailing whitespace after the closing bracket"))
|
||||
}
|
||||
|
||||
extraContent := currentMatch[4]
|
||||
if extraContent != "" {
|
||||
errs = append(errs, xerrors.Errorf("GFM alerts must not have any extra content on the same line"))
|
||||
}
|
||||
}
|
||||
|
||||
if gfmAlertRegex.Match([]byte(sourceLine)) {
|
||||
errs = append(errs, xerrors.Errorf("README has an incomplete GFM alert at the end of the file"))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -70,6 +70,9 @@ func validateCoderTemplateReadme(rm coderResourceReadme) []error {
|
||||
for _, err := range validateCoderTemplateReadmeBody(rm.body) {
|
||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
for _, err := range validateResourceGfmAlerts(rm.body) {
|
||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
if fmErrs := validateCoderResourceFrontmatter("templates", rm.filePath, rm.frontmatter); len(fmErrs) != 0 {
|
||||
errs = append(errs, fmErrs...)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ run "app_url_uses_port" {
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.MODULE_NAME.url == "http://localhost:19999"
|
||||
error_message = "Expected MODULE_NAME app URL to include configured port"
|
||||
condition = resource.coder_app.module_name.url == "http://localhost:19999"
|
||||
error_message = "Expected module-name app URL to include configured port"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +35,13 @@ variable "agent_id" {
|
||||
|
||||
variable "log_path" {
|
||||
type = string
|
||||
description = "The path to log MODULE_NAME to."
|
||||
default = "/tmp/MODULE_NAME.log"
|
||||
description = "The path to the module log file."
|
||||
default = "/tmp/module_name.log"
|
||||
}
|
||||
|
||||
variable "port" {
|
||||
type = number
|
||||
description = "The port to run MODULE_NAME on."
|
||||
description = "The port to run the application on."
|
||||
default = 19999
|
||||
}
|
||||
|
||||
@@ -59,9 +59,9 @@ variable "order" {
|
||||
# Add other variables here
|
||||
|
||||
|
||||
resource "coder_script" "MODULE_NAME" {
|
||||
resource "coder_script" "module_name" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "MODULE_NAME"
|
||||
display_name = "Module Name"
|
||||
icon = local.icon_url
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
LOG_PATH : var.log_path,
|
||||
@@ -70,10 +70,10 @@ resource "coder_script" "MODULE_NAME" {
|
||||
run_on_stop = false
|
||||
}
|
||||
|
||||
resource "coder_app" "MODULE_NAME" {
|
||||
resource "coder_app" "module_name" {
|
||||
agent_id = var.agent_id
|
||||
slug = "MODULE_NAME"
|
||||
display_name = "MODULE_NAME"
|
||||
slug = "module-name"
|
||||
display_name = "Module Name"
|
||||
url = "http://localhost:${var.port}"
|
||||
icon = local.icon_url
|
||||
subdomain = false
|
||||
@@ -88,10 +88,10 @@ resource "coder_app" "MODULE_NAME" {
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "MODULE_NAME" {
|
||||
type = "list(string)"
|
||||
name = "MODULE_NAME"
|
||||
display_name = "MODULE_NAME"
|
||||
data "coder_parameter" "module_name" {
|
||||
type = "string"
|
||||
name = "module_name"
|
||||
display_name = "Module Name"
|
||||
icon = local.icon_url
|
||||
mutable = var.mutable
|
||||
default = local.options["Option 1"]["value"]
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
display_name: NAMESPACE_NAME
|
||||
bio: Brief description of what this namespace provides
|
||||
github: your-github-username
|
||||
avatar: ./.images/avatar.svg
|
||||
linkedin: https://www.linkedin.com/in/your-profile
|
||||
website: https://your-website.com
|
||||
status: community
|
||||
---
|
||||
|
||||
# NAMESPACE_NAME
|
||||
|
||||
Brief description of what this namespace provides. Include information about:
|
||||
|
||||
- What types of templates/modules you offer
|
||||
- Your focus areas (e.g., specific cloud providers, technologies)
|
||||
- Any special features or configurations
|
||||
|
||||
## Templates
|
||||
|
||||
List your available templates here:
|
||||
|
||||
- **template-name**: Brief description
|
||||
|
||||
## Modules
|
||||
|
||||
List your available modules here:
|
||||
|
||||
- **module-name**: Brief description
|
||||
|
||||
## Contributing
|
||||
|
||||
If you'd like to contribute to this namespace, please [open an issue](https://github.com/coder/registry/issues) or submit a pull request.
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: TEMPLATE_NAME
|
||||
description: A brief description of what this template does
|
||||
tags: [tag1, tag2, tag3]
|
||||
icon: /icon/TEMPLATE_NAME.svg
|
||||
---
|
||||
|
||||
# TEMPLATE_NAME
|
||||
|
||||
A brief description of what this template provides and its use case.
|
||||
|
||||
## Features
|
||||
|
||||
- Feature 1
|
||||
- Feature 2
|
||||
- Feature 3
|
||||
|
||||
## Requirements
|
||||
|
||||
- List any prerequisites or requirements
|
||||
- Provider-specific requirements (e.g., Docker, AWS credentials)
|
||||
- Minimum Coder version if applicable
|
||||
|
||||
## Usage
|
||||
|
||||
1. Step-by-step instructions on how to use this template
|
||||
2. Any configuration that needs to be done
|
||||
3. How to customize the template
|
||||
|
||||
## Variables
|
||||
|
||||
| Name | Description | Type | Default | Required |
|
||||
| ----------- | --------------------------- | -------- | ----------------- | -------- |
|
||||
| example_var | Description of the variable | `string` | `"default_value"` | no |
|
||||
|
||||
## Resources Created
|
||||
|
||||
- List of resources that will be created
|
||||
- Brief description of each resource
|
||||
|
||||
## Customization
|
||||
|
||||
Explain how users can customize this template for their needs:
|
||||
|
||||
- How to modify the startup script
|
||||
- How to add additional software
|
||||
- How to configure different providers
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
- Issue 1 and its solution
|
||||
- Issue 2 and its solution
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please see the [contributing guidelines](../../CONTRIBUTING.md) for more information.
|
||||
@@ -0,0 +1,172 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
# Add your provider here (e.g., docker, aws, gcp, azure)
|
||||
# docker = {
|
||||
# source = "kreuzwerker/docker"
|
||||
# }
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
username = data.coder_workspace_owner.me.name
|
||||
}
|
||||
|
||||
# Add your variables here
|
||||
# variable "example_var" {
|
||||
# default = "default_value"
|
||||
# description = "Description of the variable"
|
||||
# type = string
|
||||
# }
|
||||
|
||||
# Configure your provider here
|
||||
# provider "docker" {
|
||||
# host = var.docker_socket != "" ? var.docker_socket : null
|
||||
# }
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
arch = data.coder_provisioner.me.arch
|
||||
os = "linux"
|
||||
startup_script = <<-EOT
|
||||
set -e
|
||||
|
||||
# Prepare user home with default files on first start.
|
||||
if [ ! -f ~/.init_done ]; then
|
||||
cp -rT /etc/skel ~
|
||||
touch ~/.init_done
|
||||
fi
|
||||
|
||||
# Add any commands that should be executed at workspace startup here
|
||||
EOT
|
||||
|
||||
# These environment variables allow you to make Git commits right away after creating a
|
||||
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
|
||||
# You can remove this block if you'd prefer to configure Git manually or using
|
||||
# dotfiles. (see docs/dotfiles.md)
|
||||
env = {
|
||||
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
|
||||
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
|
||||
}
|
||||
|
||||
# The following metadata blocks are optional. They are used to display
|
||||
# information about your workspace in the dashboard. You can remove them
|
||||
# if you don't want to display any information.
|
||||
# For basic templates, you can remove the "display_apps" block.
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Home Disk"
|
||||
key = "3_home_disk"
|
||||
script = "coder stat disk --path $${HOME}"
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
display_apps {
|
||||
vscode = true
|
||||
vscode_insiders = false
|
||||
ssh_helper = false
|
||||
port_forwarding_helper = true
|
||||
web_terminal = true
|
||||
}
|
||||
}
|
||||
|
||||
# Add your resources here (e.g., docker container, VM, etc.)
|
||||
# resource "docker_image" "main" {
|
||||
# name = "codercom/enterprise-base:ubuntu"
|
||||
# }
|
||||
|
||||
# resource "docker_container" "workspace" {
|
||||
# count = data.coder_workspace.me.start_count
|
||||
# image = docker_image.main.image_id
|
||||
# # Uses lower() to avoid Docker restriction on container names.
|
||||
# name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||
# # Hostname makes the shell more user friendly: coder@my-workspace:~$
|
||||
# hostname = data.coder_workspace.me.name
|
||||
# # Use the docker gateway if the access URL is 127.0.0.1
|
||||
# entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\.0\.0\.1/", "host.docker.internal")]
|
||||
# env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
|
||||
# host {
|
||||
# host = "host.docker.internal"
|
||||
# ip = "host-gateway"
|
||||
# }
|
||||
# volumes {
|
||||
# container_path = "/home/${local.username}"
|
||||
# volume_name = docker_volume.home_volume[0].name
|
||||
# read_only = false
|
||||
# }
|
||||
# # Add labels in Docker to keep track of orphan resources.
|
||||
# labels {
|
||||
# label = "coder.owner"
|
||||
# value = data.coder_workspace_owner.me.name
|
||||
# }
|
||||
# labels {
|
||||
# label = "coder.owner_id"
|
||||
# value = data.coder_workspace_owner.me.id
|
||||
# }
|
||||
# labels {
|
||||
# label = "coder.workspace_id"
|
||||
# value = data.coder_workspace.me.id
|
||||
# }
|
||||
# labels {
|
||||
# label = "coder.workspace_name"
|
||||
# value = data.coder_workspace.me.name
|
||||
# }
|
||||
# }
|
||||
|
||||
# resource "docker_volume" "home_volume" {
|
||||
# count = data.coder_workspace.me.start_count
|
||||
# name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}-home"
|
||||
# # Protect the volume from being deleted due to changes in attributes.
|
||||
# lifecycle {
|
||||
# ignore_changes = all
|
||||
# }
|
||||
# # Add labels in Docker to keep track of orphan resources.
|
||||
# labels {
|
||||
# label = "coder.owner"
|
||||
# value = data.coder_workspace_owner.me.name
|
||||
# }
|
||||
# labels {
|
||||
# label = "coder.owner_id"
|
||||
# value = data.coder_workspace_owner.me.id
|
||||
# }
|
||||
# labels {
|
||||
# label = "coder.workspace_id"
|
||||
# value = data.coder_workspace.me.id
|
||||
# }
|
||||
# labels {
|
||||
# label = "coder.workspace_name"
|
||||
# value = data.coder_workspace.me.name
|
||||
# }
|
||||
# }
|
||||
|
||||
resource "coder_metadata" "workspace_info" {
|
||||
resource_id = coder_agent.main.id
|
||||
|
||||
item {
|
||||
key = "TEMPLATE_NAME"
|
||||
value = "TEMPLATE_NAME"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "registry",
|
||||
"scripts": {
|
||||
"fmt": "bun x prettier --write **/*.sh **/*.ts **/*.md *.md && terraform fmt -recursive -diff",
|
||||
"fmt:ci": "bun x prettier --check **/*.sh **/*.ts **/*.md *.md && terraform fmt -check -recursive -diff",
|
||||
"fmt": "bun x prettier --write . && terraform fmt -recursive -diff",
|
||||
"fmt:ci": "bun x prettier --check . && terraform fmt -check -recursive -diff",
|
||||
"terraform-validate": "./scripts/terraform_validate.sh",
|
||||
"test": "./scripts/terraform_test_all.sh",
|
||||
"update-version": "./update-version.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.18",
|
||||
"bun-types": "^1.2.18",
|
||||
"@types/bun": "^1.2.21",
|
||||
"bun-types": "^1.2.21",
|
||||
"dedent": "^1.6.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^16.0.0",
|
||||
"marked": "^16.2.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-sh": "^0.18.0",
|
||||
"prettier-plugin-terraform-formatter": "^1.2.1"
|
||||
|
||||
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 451 KiB |
@@ -0,0 +1,7 @@
|
||||
---
|
||||
display_name: Jash
|
||||
bio: Coder user and contributor.
|
||||
github: AJ0070
|
||||
avatar: ./.images/avatar.png
|
||||
status: community
|
||||
---
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
display_name: "pgAdmin"
|
||||
description: "A web-based interface for managing PostgreSQL databases in your Coder workspace."
|
||||
icon: "../../../../.icons/pgadmin.svg"
|
||||
maintainer_github: "AJ0070"
|
||||
verified: false
|
||||
tags: ["database", "postgres", "pgadmin", "web-ide"]
|
||||
---
|
||||
|
||||
# pgAdmin
|
||||
|
||||
This module adds a pgAdmin app to your Coder workspace, providing a powerful web-based interface for managing PostgreSQL databases.
|
||||
|
||||
It can be served on a Coder subdomain for easy access, or on `localhost` if you prefer to use port-forwarding.
|
||||
|
||||
```tf
|
||||
module "pgadmin" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/AJ0070/pgadmin/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
import { describe } from "bun:test";
|
||||
import { runTerraformInit, testRequiredVariables } from "~test";
|
||||
|
||||
describe("pgadmin", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The agent to install pgAdmin on."
|
||||
}
|
||||
|
||||
variable "port" {
|
||||
type = number
|
||||
description = "The port to run pgAdmin on."
|
||||
default = 5050
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "If true, the app will be served on a subdomain."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "config" {
|
||||
type = any
|
||||
description = "A map of pgAdmin configuration settings."
|
||||
default = {
|
||||
DEFAULT_EMAIL = "admin@coder.com"
|
||||
DEFAULT_PASSWORD = "coderPASSWORD"
|
||||
SERVER_MODE = false
|
||||
MASTER_PASSWORD_REQUIRED = false
|
||||
LISTEN_ADDRESS = "127.0.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_app" "pgadmin" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
agent_id = var.agent_id
|
||||
display_name = "pgAdmin"
|
||||
slug = "pgadmin"
|
||||
icon = "/icon/pgadmin.svg"
|
||||
url = local.url
|
||||
subdomain = var.subdomain
|
||||
share = "owner"
|
||||
|
||||
healthcheck {
|
||||
url = local.healthcheck_url
|
||||
interval = 5
|
||||
threshold = 6
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_script" "pgadmin" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Install and run pgAdmin"
|
||||
icon = "/icon/pgadmin.svg"
|
||||
run_on_start = true
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
PORT = var.port,
|
||||
LOG_PATH = "/tmp/pgadmin.log",
|
||||
SERVER_BASE_PATH = local.server_base_path,
|
||||
CONFIG = local.config_content,
|
||||
PGADMIN_DATA_DIR = local.pgadmin_data_dir,
|
||||
PGADMIN_LOG_DIR = local.pgadmin_log_dir,
|
||||
PGADMIN_VENV_DIR = local.pgadmin_venv_dir
|
||||
})
|
||||
}
|
||||
|
||||
locals {
|
||||
server_base_path = var.subdomain ? "" : format("/@%s/%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, "pgadmin")
|
||||
url = "http://localhost:${var.port}${local.server_base_path}"
|
||||
healthcheck_url = "http://localhost:${var.port}${local.server_base_path}/"
|
||||
|
||||
# pgAdmin data directories (user-local paths)
|
||||
pgadmin_data_dir = "$HOME/.pgadmin"
|
||||
pgadmin_log_dir = "$HOME/.pgadmin/logs"
|
||||
pgadmin_venv_dir = "$HOME/.pgadmin/venv"
|
||||
|
||||
base_config = merge(var.config, {
|
||||
LISTEN_PORT = var.port
|
||||
# Override paths for user installation
|
||||
DATA_DIR = local.pgadmin_data_dir
|
||||
LOG_FILE = "${local.pgadmin_log_dir}/pgadmin4.log"
|
||||
SQLITE_PATH = "${local.pgadmin_data_dir}/pgadmin4.db"
|
||||
SESSION_DB_PATH = "${local.pgadmin_data_dir}/sessions"
|
||||
STORAGE_DIR = "${local.pgadmin_data_dir}/storage"
|
||||
# Disable initial setup prompts for automated deployment
|
||||
SETUP_AUTH = false
|
||||
})
|
||||
|
||||
config_with_path = var.subdomain ? local.base_config : merge(local.base_config, {
|
||||
APPLICATION_ROOT = local.server_base_path
|
||||
})
|
||||
|
||||
config_content = join("\n", [
|
||||
for key, value in local.config_with_path :
|
||||
format("%s = %s", key,
|
||||
can(regex("^(true|false)$", tostring(value))) ? (value ? "True" : "False") :
|
||||
can(tonumber(value)) ? tostring(value) :
|
||||
format("'%s'", tostring(value))
|
||||
)
|
||||
])
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PORT=${PORT}
|
||||
LOG_PATH=${LOG_PATH}
|
||||
SERVER_BASE_PATH=${SERVER_BASE_PATH}
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
|
||||
printf "$${BOLD}Installing pgAdmin!\n"
|
||||
|
||||
# Check if Python 3 is available
|
||||
if ! command -v python3 > /dev/null 2>&1; then
|
||||
echo "⚠️ Warning: Python 3 is not installed. Please install Python 3 before using this module."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Setup pgAdmin directories (from Terraform configuration)
|
||||
PGADMIN_DATA_DIR="${PGADMIN_DATA_DIR}"
|
||||
PGADMIN_LOG_DIR="${PGADMIN_LOG_DIR}"
|
||||
PGADMIN_VENV_DIR="${PGADMIN_VENV_DIR}"
|
||||
|
||||
printf "Setting up pgAdmin directories...\n"
|
||||
mkdir -p "$PGADMIN_DATA_DIR"
|
||||
mkdir -p "$PGADMIN_LOG_DIR"
|
||||
|
||||
# Check if pgAdmin virtual environment already exists and is working
|
||||
if [ -f "$PGADMIN_VENV_DIR/bin/pgadmin4" ] && [ -f "$PGADMIN_VENV_DIR/bin/activate" ]; then
|
||||
printf "🥳 pgAdmin virtual environment already exists\n\n"
|
||||
else
|
||||
printf "Creating Python virtual environment for pgAdmin...\n"
|
||||
if ! python3 -m venv "$PGADMIN_VENV_DIR"; then
|
||||
echo "⚠️ Warning: Failed to create virtual environment"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf "Installing pgAdmin 4 in virtual environment...\n"
|
||||
if ! "$PGADMIN_VENV_DIR/bin/pip" install pgadmin4; then
|
||||
echo "⚠️ Warning: Failed to install pgAdmin4"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf "🥳 pgAdmin has been installed successfully\n\n"
|
||||
fi
|
||||
|
||||
printf "$${BOLD}Configuring pgAdmin...\n"
|
||||
|
||||
if [ -f "$PGADMIN_VENV_DIR/bin/pgadmin4" ]; then
|
||||
# pgAdmin installs to a predictable location in the virtual environment
|
||||
PYTHON_VERSION=$("$PGADMIN_VENV_DIR/bin/python" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
|
||||
PGADMIN_INSTALL_DIR="$PGADMIN_VENV_DIR/lib/python$PYTHON_VERSION/site-packages/pgadmin4"
|
||||
|
||||
# Create pgAdmin config file in the correct location (next to config.py)
|
||||
cat > "$PGADMIN_INSTALL_DIR/config_local.py" << EOF
|
||||
# pgAdmin configuration for Coder workspace
|
||||
${CONFIG}
|
||||
EOF
|
||||
|
||||
printf "📄 Config written to $PGADMIN_INSTALL_DIR/config_local.py\n"
|
||||
|
||||
printf "$${BOLD}Starting pgAdmin in background...\n"
|
||||
printf "📝 Check logs at $${LOG_PATH}\n"
|
||||
printf "🌐 Serving at http://localhost:${PORT}${SERVER_BASE_PATH}\n"
|
||||
|
||||
# Create required directories
|
||||
mkdir -p "$PGADMIN_DATA_DIR/sessions"
|
||||
mkdir -p "$PGADMIN_DATA_DIR/storage"
|
||||
|
||||
# Start pgadmin4 from the virtual environment with proper environment
|
||||
cd "$PGADMIN_DATA_DIR"
|
||||
PYTHONPATH="$PGADMIN_INSTALL_DIR:$${PYTHONPATH:-}" "$PGADMIN_VENV_DIR/bin/pgadmin4" > "$${LOG_PATH}" 2>&1 &
|
||||
else
|
||||
printf "⚠️ Warning: pgAdmin4 virtual environment not found\n"
|
||||
printf "📝 Installation may have failed - check logs above\n"
|
||||
fi
|
||||
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,14 @@
|
||||
---
|
||||
display_name: "Benraouane Soufiane"
|
||||
bio: "Full stack developer creating awesome things."
|
||||
avatar: "./.images/avatar.png"
|
||||
github: "benraouanesoufiane"
|
||||
linkedin: "https://www.linkedin.com/in/benraouane-soufiane" # Optional
|
||||
website: "https://benraouanesoufiane.com" # Optional
|
||||
support_email: "hello@benraouanesoufiane.com" # Optional
|
||||
status: "community"
|
||||
---
|
||||
|
||||
# Benraouane Soufiane
|
||||
|
||||
Full stack developer creating awesome things.
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
display_name: RustDesk
|
||||
description: Run RustDesk in your workspace with virtual display
|
||||
icon: ../../../../.icons/rustdesk.svg
|
||||
verified: false
|
||||
tags: [rustdesk, rdp, vm]
|
||||
---
|
||||
|
||||
# RustDesk
|
||||
|
||||
Launches RustDesk within your workspace with a virtual display to provide remote desktop access. The module outputs the RustDesk ID and password needed to connect from external RustDesk clients.
|
||||
|
||||
```tf
|
||||
module "rustdesk" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/BenraouaneSoufiane/rustdesk/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Automatically sets up virtual display (Xvfb)
|
||||
- Downloads and configures RustDesk
|
||||
- Outputs RustDesk ID and password for easy connection
|
||||
- Provides external app link to RustDesk web client for browser-based access
|
||||
- Starts virtual display (Xvfb) with customizable resolution
|
||||
- Customizable screen resolution and RustDesk version
|
||||
|
||||
## Requirements
|
||||
|
||||
- Coder v2.5 or higher
|
||||
- Linux workspace with `apt`, `dnf`, or `yum` package manager
|
||||
|
||||
## Examples
|
||||
|
||||
### Custom configuration with specific version
|
||||
|
||||
```tf
|
||||
module "rustdesk" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/BenraouaneSoufiane/rustdesk/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
rustdesk_password = "mycustompass"
|
||||
xvfb_resolution = "1920x1080x24"
|
||||
rustdesk_version = "1.4.1"
|
||||
}
|
||||
```
|
||||
|
||||
### Docker container configuration
|
||||
|
||||
It requires coder' server to be run as root, when using with Docker, add the following to your `docker_container` resource:
|
||||
|
||||
```tf
|
||||
resource "docker_container" "workspace" {
|
||||
|
||||
# ... other configuration ...
|
||||
|
||||
user = "root"
|
||||
privileged = true
|
||||
network_mode = "host"
|
||||
|
||||
ports {
|
||||
internal = 21115
|
||||
external = 21115
|
||||
}
|
||||
ports {
|
||||
internal = 21116
|
||||
external = 21116
|
||||
}
|
||||
ports {
|
||||
internal = 21118
|
||||
external = 21118
|
||||
}
|
||||
ports {
|
||||
internal = 21119
|
||||
external = 21119
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,75 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "log_path" {
|
||||
type = string
|
||||
description = "The path to log rustdesk to."
|
||||
default = "/tmp/rustdesk.log"
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
description = "Attach RustDesk setup to this agent"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
description = "Run order among scripts/apps"
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
# Optional knobs passed as env (you can expose these as variables too)
|
||||
variable "rustdesk_password" {
|
||||
description = "If empty, the script will generate one"
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "xvfb_resolution" {
|
||||
description = "Xvfb screen size/depth"
|
||||
type = string
|
||||
default = "1024x768x16"
|
||||
}
|
||||
|
||||
variable "rustdesk_version" {
|
||||
description = "RustDesk version to install (use 'latest' for most recent release)"
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
resource "coder_script" "rustdesk" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "RustDesk"
|
||||
run_on_start = true
|
||||
|
||||
# Prepend env as bash exports, then append the script file literally.
|
||||
script = <<-EOT
|
||||
# --- module-provided env knobs ---
|
||||
export RUSTDESK_PASSWORD="${var.rustdesk_password}"
|
||||
export XVFB_RESOLUTION="${var.xvfb_resolution}"
|
||||
export RUSTDESK_VERSION="${var.rustdesk_version}"
|
||||
# ---------------------------------
|
||||
|
||||
${file("${path.module}/run.sh")}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "coder_app" "rustdesk" {
|
||||
agent_id = var.agent_id
|
||||
slug = "rustdesk"
|
||||
display_name = "Rustdesk"
|
||||
url = "https://rustdesk.com/web"
|
||||
icon = "/icon/rustdesk.svg"
|
||||
order = var.order
|
||||
external = true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
printf "${BOLD}🖥️ Installing RustDesk Remote Desktop\n${RESET}"
|
||||
|
||||
# ---- configurable knobs (env overrides) ----
|
||||
RUSTDESK_VERSION="${RUSTDESK_VERSION:-latest}"
|
||||
LOG_PATH="${LOG_PATH:-/tmp/rustdesk.log}"
|
||||
|
||||
# ---- fetch latest version if needed ----
|
||||
if [ "$RUSTDESK_VERSION" = "latest" ]; then
|
||||
printf "🔍 Fetching latest RustDesk version...\n"
|
||||
RUSTDESK_VERSION=$(curl -s https://api.github.com/repos/rustdesk/rustdesk/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' || echo "1.4.1")
|
||||
printf "📌 Fetched RustDesk version: ${RUSTDESK_VERSION}\n"
|
||||
else
|
||||
printf "📌 Using specified RustDesk version: ${RUSTDESK_VERSION}\n"
|
||||
fi
|
||||
XVFB_RESOLUTION="${XVFB_RESOLUTION:-1024x768x16}"
|
||||
RUSTDESK_PASSWORD="${RUSTDESK_PASSWORD:-}"
|
||||
|
||||
# ---- detect package manager & arch ----
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
x86_64 | amd64) PKG_ARCH="x86_64" ;;
|
||||
aarch64 | arm64) PKG_ARCH="aarch64" ;;
|
||||
*)
|
||||
echo "❌ Unsupported arch: $ARCH"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v apt-get > /dev/null 2>&1; then
|
||||
PKG_SYS="deb"
|
||||
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.deb"
|
||||
INSTALL_DEPS='apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y wget libva2 libva-drm2 libva-x11-2 libgstreamer-plugins-base1.0-0 gstreamer1.0-pipewire xfce4 xfce4-goodies xvfb x11-xserver-utils dbus-x11 libegl1 libgl1 libglx0 libglu1-mesa mesa-utils libxrandr2 libxss1 libgtk-3-0t64 libgbm1 libdrm2 libxcomposite1 libxdamage1 libxfixes3'
|
||||
INSTALL_CMD="apt-get install -y ./${PKG_NAME}"
|
||||
CLEAN_CMD="rm -f \"${PKG_NAME}\""
|
||||
elif command -v dnf > /dev/null 2>&1; then
|
||||
PKG_SYS="rpm"
|
||||
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.rpm"
|
||||
INSTALL_DEPS='dnf install -y wget libva libva-intel-driver gstreamer1-plugins-base pipewire xfce4-session xfce4-panel xorg-x11-server-Xvfb xorg-x11-xauth dbus-x11 mesa-libEGL mesa-libGL mesa-libGLU mesa-dri-drivers libXrandr libXScrnSaver gtk3 mesa-libgbm libdrm libXcomposite libXdamage libXfixes'
|
||||
INSTALL_CMD="dnf install -y ./${PKG_NAME}"
|
||||
CLEAN_CMD="rm -f \"${PKG_NAME}\""
|
||||
elif command -v yum > /dev/null 2>&1; then
|
||||
PKG_SYS="rpm"
|
||||
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.rpm"
|
||||
INSTALL_DEPS='yum install -y wget libva libva-intel-driver gstreamer1-plugins-base pipewire xfce4-session xfce4-panel xorg-x11-server-Xvfb xorg-x11-xauth dbus-x11 mesa-libEGL mesa-libGL mesa-libGLU mesa-dri-drivers libXrandr libXScrnSaver gtk3 mesa-libgbm libdrm libXcomposite libXdamage libXfixes'
|
||||
INSTALL_CMD="yum install -y ./${PKG_NAME}"
|
||||
CLEAN_CMD="rm -f \"${PKG_NAME}\""
|
||||
else
|
||||
echo "❌ Unsupported distro: need apt, dnf, or yum."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- install rustdesk if missing ----
|
||||
if ! command -v rustdesk > /dev/null 2>&1; then
|
||||
printf "📦 Installing dependencies...\n"
|
||||
sudo bash -c "$INSTALL_DEPS" 2>&1 | tee -a "${LOG_PATH}"
|
||||
|
||||
printf "⬇️ Downloading RustDesk ${RUSTDESK_VERSION} (${PKG_SYS}, ${PKG_ARCH})...\n"
|
||||
URL="https://github.com/rustdesk/rustdesk/releases/download/${RUSTDESK_VERSION}/${PKG_NAME}"
|
||||
wget -q "$URL" 2>&1 | tee -a "${LOG_PATH}"
|
||||
|
||||
printf "🔧 Installing RustDesk...\n"
|
||||
sudo bash -c "$INSTALL_CMD" 2>&1 | tee -a "${LOG_PATH}"
|
||||
|
||||
printf "🧹 Cleaning up...\n"
|
||||
bash -c "$CLEAN_CMD" 2>&1 | tee -a "${LOG_PATH}"
|
||||
else
|
||||
printf "✅ RustDesk already installed\n"
|
||||
fi
|
||||
|
||||
# ---- start virtual display ----
|
||||
echo "Starting Xvfb with resolution ${XVFB_RESOLUTION}…"
|
||||
Xvfb :99 -screen 0 "${XVFB_RESOLUTION}" >> "${LOG_PATH}" 2>&1 &
|
||||
export DISPLAY=:99
|
||||
|
||||
# Wait for X to be ready
|
||||
for i in {1..10}; do
|
||||
if xdpyinfo -display :99 > /dev/null 2>&1; then
|
||||
echo "X display is ready"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# ---- create (or accept) password and start rustdesk ----
|
||||
if [[ -z "${RUSTDESK_PASSWORD}" ]]; then
|
||||
RUSTDESK_PASSWORD="$(tr -dc 'a-zA-Z0-9@' < /dev/urandom | head -c 10)@97"
|
||||
fi
|
||||
|
||||
echo "Starting XFCE desktop environment..."
|
||||
xfce4-session >> "${LOG_PATH}" 2>&1 &
|
||||
|
||||
echo "Waiting for xfce4-session to initialize..."
|
||||
sleep 5
|
||||
|
||||
printf "🔐 Setting RustDesk password and starting service...\n"
|
||||
rustdesk >> "${LOG_PATH}" 2>&1 &
|
||||
sleep 2
|
||||
|
||||
rustdesk --password "${RUSTDESK_PASSWORD}" >> "${LOG_PATH}" 2>&1 &
|
||||
sleep 3
|
||||
|
||||
RID="$(rustdesk --get-id 2> /dev/null || echo 'ID_PENDING')"
|
||||
|
||||
printf "🥳 RustDesk setup complete!\n\n"
|
||||
printf "${BOLD}📋 Connection Details:${RESET}\n"
|
||||
printf " RustDesk ID: ${RID}\n"
|
||||
printf " RustDesk Password: ${RUSTDESK_PASSWORD}\n"
|
||||
printf " Display: ${DISPLAY} (${XVFB_RESOLUTION})\n"
|
||||
printf "\n📝 Logs available at: ${LOG_PATH}\n\n"
|
||||
|
||||
echo "Setup script completed successfully. All services running in background."
|
||||
exit 0
|
||||
@@ -28,7 +28,9 @@ describe("tmux module", async () => {
|
||||
|
||||
// check that the script contains expected lines
|
||||
expect(scriptResource.script).toContain("Installing tmux");
|
||||
expect(scriptResource.script).toContain("Installing Tmux Plugin Manager (TPM)");
|
||||
expect(scriptResource.script).toContain(
|
||||
"Installing Tmux Plugin Manager (TPM)",
|
||||
);
|
||||
expect(scriptResource.script).toContain("tmux configuration created at");
|
||||
expect(scriptResource.script).toContain("✅ tmux setup complete!");
|
||||
});
|
||||
|
||||
@@ -8,75 +8,75 @@ TMUX_CONFIG="${TMUX_CONFIG}"
|
||||
|
||||
# Function to install tmux
|
||||
install_tmux() {
|
||||
printf "Checking for tmux installation\n"
|
||||
printf "Checking for tmux installation\n"
|
||||
|
||||
if command -v tmux &> /dev/null; then
|
||||
printf "tmux is already installed \n\n"
|
||||
return 0
|
||||
fi
|
||||
if command -v tmux &> /dev/null; then
|
||||
printf "tmux is already installed \n\n"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf "Installing tmux \n\n"
|
||||
printf "Installing tmux \n\n"
|
||||
|
||||
# Detect package manager and install tmux
|
||||
if command -v apt-get &> /dev/null; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y tmux
|
||||
elif command -v yum &> /dev/null; then
|
||||
sudo yum install -y tmux
|
||||
elif command -v dnf &> /dev/null; then
|
||||
sudo dnf install -y tmux
|
||||
elif command -v zypper &> /dev/null; then
|
||||
sudo zypper install -y tmux
|
||||
elif command -v apk &> /dev/null; then
|
||||
sudo apk add tmux
|
||||
elif command -v brew &> /dev/null; then
|
||||
brew install tmux
|
||||
else
|
||||
printf "No supported package manager found. Please install tmux manually. \n"
|
||||
exit 1
|
||||
fi
|
||||
# Detect package manager and install tmux
|
||||
if command -v apt-get &> /dev/null; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y tmux
|
||||
elif command -v yum &> /dev/null; then
|
||||
sudo yum install -y tmux
|
||||
elif command -v dnf &> /dev/null; then
|
||||
sudo dnf install -y tmux
|
||||
elif command -v zypper &> /dev/null; then
|
||||
sudo zypper install -y tmux
|
||||
elif command -v apk &> /dev/null; then
|
||||
sudo apk add tmux
|
||||
elif command -v brew &> /dev/null; then
|
||||
brew install tmux
|
||||
else
|
||||
printf "No supported package manager found. Please install tmux manually. \n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "tmux installed successfully \n"
|
||||
printf "tmux installed successfully \n"
|
||||
}
|
||||
|
||||
# Function to install Tmux Plugin Manager (TPM)
|
||||
install_tpm() {
|
||||
local tpm_dir="$HOME/.tmux/plugins/tpm"
|
||||
local tpm_dir="$HOME/.tmux/plugins/tpm"
|
||||
|
||||
if [ -d "$tpm_dir" ]; then
|
||||
printf "TPM is already installed"
|
||||
return 0
|
||||
fi
|
||||
if [ -d "$tpm_dir" ]; then
|
||||
printf "TPM is already installed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf "Installing Tmux Plugin Manager (TPM) \n"
|
||||
printf "Installing Tmux Plugin Manager (TPM) \n"
|
||||
|
||||
# Create plugins directory
|
||||
mkdir -p "$HOME/.tmux/plugins"
|
||||
# Create plugins directory
|
||||
mkdir -p "$HOME/.tmux/plugins"
|
||||
|
||||
# Clone TPM repository
|
||||
if command -v git &> /dev/null; then
|
||||
git clone https://github.com/tmux-plugins/tpm "$tpm_dir"
|
||||
printf "TPM installed successfully"
|
||||
else
|
||||
printf "Git is not installed. Please install git to use tmux plugins. \n"
|
||||
exit 1
|
||||
fi
|
||||
# Clone TPM repository
|
||||
if command -v git &> /dev/null; then
|
||||
git clone https://github.com/tmux-plugins/tpm "$tpm_dir"
|
||||
printf "TPM installed successfully"
|
||||
else
|
||||
printf "Git is not installed. Please install git to use tmux plugins. \n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create tmux configuration
|
||||
setup_tmux_config() {
|
||||
printf "Setting up tmux configuration \n"
|
||||
printf "Setting up tmux configuration \n"
|
||||
|
||||
local config_dir="$HOME/.tmux"
|
||||
local config_file="$HOME/.tmux.conf"
|
||||
local config_dir="$HOME/.tmux"
|
||||
local config_file="$HOME/.tmux.conf"
|
||||
|
||||
mkdir -p "$config_dir"
|
||||
mkdir -p "$config_dir"
|
||||
|
||||
if [ -n "$TMUX_CONFIG" ]; then
|
||||
printf "$TMUX_CONFIG" > "$config_file"
|
||||
printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n"
|
||||
else
|
||||
cat > "$config_file" << EOF
|
||||
if [ -n "$TMUX_CONFIG" ]; then
|
||||
printf "$TMUX_CONFIG" > "$config_file"
|
||||
printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n"
|
||||
else
|
||||
cat > "$config_file" << EOF
|
||||
# Tmux Configuration File
|
||||
|
||||
# =============================================================================
|
||||
@@ -106,48 +106,48 @@ bind C-r run-shell "~/.tmux/plugins/tmux-resurrect/scripts/restore.sh"
|
||||
# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf)
|
||||
run '~/.tmux/plugins/tpm/tpm'
|
||||
EOF
|
||||
printf "tmux configuration created at {$config_file} \n\n"
|
||||
fi
|
||||
printf "tmux configuration created at {$config_file} \n\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to install tmux plugins
|
||||
install_plugins() {
|
||||
printf "Installing tmux plugins"
|
||||
printf "Installing tmux plugins"
|
||||
|
||||
# Check if TPM is installed
|
||||
if [ ! -d "$HOME/.tmux/plugins/tpm" ]; then
|
||||
printf "TPM is not installed. Cannot install plugins. \n"
|
||||
return 1
|
||||
fi
|
||||
# Check if TPM is installed
|
||||
if [ ! -d "$HOME/.tmux/plugins/tpm" ]; then
|
||||
printf "TPM is not installed. Cannot install plugins. \n"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Install plugins using TPM
|
||||
"$HOME/.tmux/plugins/tpm/bin/install_plugins"
|
||||
# Install plugins using TPM
|
||||
"$HOME/.tmux/plugins/tpm/bin/install_plugins"
|
||||
|
||||
printf "tmux plugins installed successfully \n"
|
||||
printf "tmux plugins installed successfully \n"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
printf "$${BOLD} 🛠️Setting up tmux with session persistence! \n\n"
|
||||
printf ""
|
||||
printf "$${BOLD} 🛠️Setting up tmux with session persistence! \n\n"
|
||||
printf ""
|
||||
|
||||
# Install dependencies
|
||||
install_tmux
|
||||
install_tpm
|
||||
# Install dependencies
|
||||
install_tmux
|
||||
install_tpm
|
||||
|
||||
# Setup tmux configuration
|
||||
setup_tmux_config
|
||||
# Setup tmux configuration
|
||||
setup_tmux_config
|
||||
|
||||
# Install plugins
|
||||
install_plugins
|
||||
# Install plugins
|
||||
install_plugins
|
||||
|
||||
printf "$${BOLD}✅ tmux setup complete! \n\n"
|
||||
printf "$${BOLD}✅ tmux setup complete! \n\n"
|
||||
|
||||
printf "$${BOLD} Attempting to restore sessions\n"
|
||||
tmux new-session -d \; source-file ~/.tmux.conf \; run-shell '~/.tmux/plugins/tmux-resurrect/scripts/restore.sh'
|
||||
printf "$${BOLD} Sessions restored: -> %s\n" "$(tmux ls)"
|
||||
printf "$${BOLD} Attempting to restore sessions\n"
|
||||
tmux new-session -d \; source-file ~/.tmux.conf \; run-shell '~/.tmux/plugins/tmux-resurrect/scripts/restore.sh'
|
||||
printf "$${BOLD} Sessions restored: -> %s\n" "$(tmux ls)"
|
||||
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
main
|
||||
|
||||
@@ -16,7 +16,7 @@ handle_session() {
|
||||
local session_name="$1"
|
||||
|
||||
# Check if the session exists
|
||||
if tmux has-session -t "$session_name" 2>/dev/null; then
|
||||
if tmux has-session -t "$session_name" 2> /dev/null; then
|
||||
echo "Session '$session_name' exists, attaching to it..."
|
||||
tmux attach-session -t "$session_name"
|
||||
else
|
||||
|
||||
@@ -13,7 +13,7 @@ Run Auggie CLI in your workspace to access Augment's AI coding assistant with ad
|
||||
```tf
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -47,7 +47,7 @@ module "coder-login" {
|
||||
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
@@ -103,7 +103,7 @@ EOF
|
||||
```tf
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
|
||||
@@ -165,9 +165,9 @@ describe("auggie", async () => {
|
||||
mcpServers: {
|
||||
test: {
|
||||
command: "test-cmd",
|
||||
type: "stdio"
|
||||
}
|
||||
}
|
||||
type: "stdio",
|
||||
},
|
||||
},
|
||||
});
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
@@ -187,13 +187,16 @@ describe("auggie", 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
|
||||
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");
|
||||
const rulesFile = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.augment/rules.md",
|
||||
);
|
||||
expect(rulesFile).toContain(rules);
|
||||
});
|
||||
|
||||
@@ -309,12 +312,15 @@ describe("auggie", async () => {
|
||||
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
|
||||
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");
|
||||
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");
|
||||
|
||||
@@ -66,7 +66,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.6.0"
|
||||
default = "v0.10.0"
|
||||
validation {
|
||||
condition = can(regex("^v[0-9]+\\.[0-9]+\\.[0-9]+", var.agentapi_version))
|
||||
error_message = "agentapi_version must be a valid semantic version starting with 'v', like 'v0.3.3'."
|
||||
@@ -178,7 +178,7 @@ locals {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -25,7 +25,6 @@ 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"
|
||||
@@ -51,28 +50,27 @@ function install_auggie() {
|
||||
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=$(
|
||||
|
||||
@@ -39,7 +39,6 @@ printf "report_tasks: %s\n" "$ARG_REPORT_TASKS"
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
|
||||
function validate_auggie_installation() {
|
||||
if command_exists auggie; then
|
||||
printf "Auggie is installed\n"
|
||||
|
||||
@@ -13,7 +13,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte
|
||||
```tf
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = var.openai_api_key
|
||||
folder = "/home/coder/project"
|
||||
@@ -33,7 +33,7 @@ module "codex" {
|
||||
module "codex" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = "..."
|
||||
folder = "/home/coder/project"
|
||||
@@ -60,7 +60,7 @@ module "coder-login" {
|
||||
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = "..."
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
@@ -106,7 +106,7 @@ For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_se
|
||||
```tf
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.1"
|
||||
# ... other variables ...
|
||||
|
||||
# Override default configuration
|
||||
|
||||
@@ -124,8 +124,8 @@ describe("codex", async () => {
|
||||
});
|
||||
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('sandbox_mode = "danger-full-access"');
|
||||
expect(resp).toContain('preferred_auth_method = "apikey"');
|
||||
expect(resp).toContain("[custom_section]");
|
||||
expect(resp).toContain("[mcp_servers.Coder]");
|
||||
});
|
||||
@@ -221,7 +221,7 @@ describe("codex", async () => {
|
||||
debug = true
|
||||
logging_level = "verbose"
|
||||
`.trim();
|
||||
|
||||
|
||||
const additionalMCP = dedent`
|
||||
[mcp_servers.CustomTool]
|
||||
command = "/usr/local/bin/custom-tool"
|
||||
@@ -235,7 +235,7 @@ describe("codex", async () => {
|
||||
type = "stdio"
|
||||
description = "Database query interface"
|
||||
`.trim();
|
||||
|
||||
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
base_config_toml: baseConfig,
|
||||
@@ -244,14 +244,14 @@ describe("codex", async () => {
|
||||
});
|
||||
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('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\"");
|
||||
|
||||
expect(resp).toContain('logging_level = "verbose"');
|
||||
|
||||
// Check MCP servers
|
||||
expect(resp).toContain("[mcp_servers.Coder]");
|
||||
expect(resp).toContain("[mcp_servers.CustomTool]");
|
||||
@@ -268,17 +268,17 @@ describe("codex", async () => {
|
||||
});
|
||||
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_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);
|
||||
@@ -328,7 +328,10 @@ describe("codex", async () => {
|
||||
},
|
||||
});
|
||||
await execModuleScript(id_2);
|
||||
const resp_2 = await readFileContainer(id_2, "/home/coder/.codex/AGENTS.md");
|
||||
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);
|
||||
|
||||
@@ -80,7 +80,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.5.0"
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "codex_model" {
|
||||
@@ -128,9 +128,10 @@ locals {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = var.folder
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
|
||||
@@ -84,8 +84,8 @@ function install_codex() {
|
||||
}
|
||||
|
||||
write_minimal_default_config() {
|
||||
local config_path="$1"
|
||||
cat << EOF > "$config_path"
|
||||
local config_path="$1"
|
||||
cat << EOF > "$config_path"
|
||||
# Minimal Default Codex Configuration
|
||||
sandbox_mode = "workspace-write"
|
||||
approval_policy = "never"
|
||||
@@ -98,9 +98,9 @@ EOF
|
||||
}
|
||||
|
||||
append_mcp_servers_section() {
|
||||
local config_path="$1"
|
||||
|
||||
cat << EOF >> "$config_path"
|
||||
local config_path="$1"
|
||||
|
||||
cat << EOF >> "$config_path"
|
||||
|
||||
# MCP Servers Configuration
|
||||
[mcp_servers.Coder]
|
||||
@@ -112,32 +112,32 @@ type = "stdio"
|
||||
|
||||
EOF
|
||||
|
||||
if [ -n "$ARG_ADDITIONAL_MCP_SERVERS" ]; then
|
||||
printf "Adding additional MCP servers\n"
|
||||
echo "$ARG_ADDITIONAL_MCP_SERVERS" >> "$config_path"
|
||||
fi
|
||||
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"
|
||||
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
|
||||
@@ -146,7 +146,7 @@ function add_instruction_prompt_if_exists() {
|
||||
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}" || {
|
||||
|
||||
@@ -55,8 +55,6 @@ 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"
|
||||
@@ -65,7 +63,6 @@ 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
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
---
|
||||
display_name: Copilot CLI
|
||||
description: GitHub Copilot CLI agent for AI-powered terminal assistance
|
||||
icon: ../../../../.icons/github.svg
|
||||
verified: false
|
||||
tags: [agent, copilot, ai, github, tasks]
|
||||
---
|
||||
|
||||
# Copilot
|
||||
|
||||
Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-copilot-cli) in your workspace for AI-powered coding assistance directly from the terminal. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for task reporting in the Coder UI.
|
||||
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This example assumes you have [Coder external authentication](https://coder.com/docs/admin/external-auth) configured with `id = "github"`. If not, you can provide a direct token using the `github_token` variable or provide the correct external authentication id for GitHub by setting `external_auth_id = "my-github"`.
|
||||
|
||||
> [!NOTE]
|
||||
> By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js v22+** and **npm v10+**
|
||||
- **[Active Copilot subscription](https://docs.github.com/en/copilot/about-github-copilot/subscription-plans-for-github-copilot)** (GitHub Copilot Pro, Pro+, Business, or Enterprise)
|
||||
- **GitHub authentication** via one of:
|
||||
- [Coder external authentication](https://coder.com/docs/admin/external-auth) (recommended)
|
||||
- Direct token via `github_token` variable
|
||||
- Interactive login in Copilot
|
||||
|
||||
## Examples
|
||||
|
||||
### Usage with Tasks
|
||||
|
||||
For development environments where you want Copilot to have full access to tools and automatically resume sessions:
|
||||
|
||||
```tf
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Initial task prompt for Copilot."
|
||||
mutable = true
|
||||
}
|
||||
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
copilot_model = "claude-sonnet-4.5"
|
||||
allow_all_tools = true
|
||||
resume_session = true
|
||||
|
||||
trusted_directories = ["/home/coder/projects", "/tmp"]
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
Customize tool permissions, MCP servers, and Copilot settings:
|
||||
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
|
||||
# Version pinning (defaults to "0.0.334", use "latest" for newest version)
|
||||
copilot_version = "latest"
|
||||
|
||||
# Tool permissions
|
||||
allow_tools = ["shell(git)", "shell(npm)", "write"]
|
||||
trusted_directories = ["/home/coder/projects", "/tmp"]
|
||||
|
||||
# Custom Copilot configuration
|
||||
copilot_config = jsonencode({
|
||||
banner = "never"
|
||||
theme = "dark"
|
||||
})
|
||||
|
||||
# MCP server configuration
|
||||
mcp_config = jsonencode({
|
||||
mcpServers = {
|
||||
filesystem = {
|
||||
command = "npx"
|
||||
args = ["-y", "@modelcontextprotocol/server-filesystem", "/home/coder/projects"]
|
||||
description = "Provides file system access to the workspace"
|
||||
name = "Filesystem"
|
||||
timeout = 3000
|
||||
type = "local"
|
||||
tools = ["*"]
|
||||
trust = true
|
||||
}
|
||||
playwright = {
|
||||
command = "npx"
|
||||
args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated"]
|
||||
description = "Browser automation for testing and previewing changes"
|
||||
name = "Playwright"
|
||||
timeout = 5000
|
||||
type = "local"
|
||||
tools = ["*"]
|
||||
trust = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
# Pre-install Node.js if needed
|
||||
pre_install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> GitHub Copilot CLI does not automatically install MCP servers. You have two options:
|
||||
>
|
||||
> - Use `npx -y` in the MCP config (shown above) to auto-install on each run
|
||||
> - Pre-install MCP servers in `pre_install_script` for faster startup (e.g., `npm install -g @modelcontextprotocol/server-filesystem`)
|
||||
|
||||
### Direct Token Authentication
|
||||
|
||||
Use this example when you want to provide a GitHub Personal Access Token instead of using Coder external auth:
|
||||
|
||||
```tf
|
||||
variable "github_token" {
|
||||
type = string
|
||||
description = "GitHub Personal Access Token"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
github_token = var.github_token
|
||||
}
|
||||
```
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
Run Copilot as a command-line tool without task reporting or web interface. This installs and configures Copilot, making it available as a CLI app in the Coder agent bar that you can launch to interact with Copilot directly from your terminal. Set `report_tasks = false` to disable integration with Coder Tasks.
|
||||
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
report_tasks = false
|
||||
cli_app = true
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
The module supports multiple authentication methods (in priority order):
|
||||
|
||||
1. **[Coder External Auth](https://coder.com/docs/admin/external-auth) (Recommended)** - Automatic if GitHub external auth is configured in Coder
|
||||
2. **Direct Token** - Pass `github_token` variable (OAuth or Personal Access Token)
|
||||
3. **Interactive** - Copilot prompts for login via `/login` command if no auth found
|
||||
|
||||
> [!NOTE]
|
||||
> OAuth tokens work best with Copilot. Personal Access Tokens may have limited functionality.
|
||||
|
||||
## Session Resumption
|
||||
|
||||
By default, the module resumes the latest Copilot session when the workspace restarts. Set `resume_session = false` to always start fresh sessions.
|
||||
|
||||
> [!NOTE]
|
||||
> Session resumption requires persistent storage for the home directory or workspace volume. Without persistent storage, sessions will not resume across workspace restarts.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any issues, check the log files in the `~/.copilot-module` directory within your workspace for detailed information.
|
||||
|
||||
```bash
|
||||
# Installation logs
|
||||
cat ~/.copilot-module/install.log
|
||||
|
||||
# Startup logs
|
||||
cat ~/.copilot-module/agentapi-start.log
|
||||
|
||||
# Pre/post install script logs
|
||||
cat ~/.copilot-module/pre_install.log
|
||||
cat ~/.copilot-module/post_install.log
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> To use tasks with Copilot, you must have an active GitHub Copilot subscription.
|
||||
> The `workdir` variable is required and specifies the directory where Copilot will run.
|
||||
|
||||
## References
|
||||
|
||||
- [GitHub Copilot CLI Documentation](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)
|
||||
- [Installing GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)
|
||||
- [AgentAPI Documentation](https://github.com/coder/agentapi)
|
||||
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
|
||||
@@ -0,0 +1,236 @@
|
||||
run "defaults_are_correct" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.copilot_model == "claude-sonnet-4.5"
|
||||
error_message = "Default model should be 'claude-sonnet-4.5'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.report_tasks == true
|
||||
error_message = "Task reporting should be enabled by default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.resume_session == true
|
||||
error_message = "Session resumption should be enabled by default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.allow_all_tools == false
|
||||
error_message = "allow_all_tools should be disabled by default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.mcp_app_status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug env var should be created"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.mcp_app_status_slug.value == "copilot"
|
||||
error_message = "Status slug value should be 'copilot'"
|
||||
}
|
||||
}
|
||||
|
||||
run "github_token_creates_env_var" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
github_token = "test_github_token_abc123"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.github_token) == 1
|
||||
error_message = "github_token env var should be created when token is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.github_token[0].name == "GITHUB_TOKEN"
|
||||
error_message = "github_token env var name should be 'GITHUB_TOKEN'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.github_token[0].value == "test_github_token_abc123"
|
||||
error_message = "github_token env var value should match input"
|
||||
}
|
||||
}
|
||||
|
||||
run "github_token_not_created_when_empty" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
github_token = ""
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.github_token) == 0
|
||||
error_message = "github_token env var should not be created when empty"
|
||||
}
|
||||
}
|
||||
|
||||
run "copilot_model_env_var_for_non_default" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
copilot_model = "claude-sonnet-4"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.copilot_model) == 1
|
||||
error_message = "copilot_model env var should be created for non-default model"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.copilot_model[0].name == "COPILOT_MODEL"
|
||||
error_message = "copilot_model env var name should be 'COPILOT_MODEL'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.copilot_model[0].value == "claude-sonnet-4"
|
||||
error_message = "copilot_model env var value should match input"
|
||||
}
|
||||
}
|
||||
|
||||
run "copilot_model_not_created_for_default" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
copilot_model = "claude-sonnet-4.5"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.copilot_model) == 0
|
||||
error_message = "copilot_model env var should not be created for default model"
|
||||
}
|
||||
}
|
||||
|
||||
run "model_validation_accepts_valid_models" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
copilot_model = "gpt-5"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(["claude-sonnet-4", "claude-sonnet-4.5", "gpt-5"], var.copilot_model)
|
||||
error_message = "Model should be one of the valid options"
|
||||
}
|
||||
}
|
||||
|
||||
run "copilot_config_merges_with_trusted_directories" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder/project"
|
||||
trusted_directories = ["/workspace", "/data"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.final_copilot_config) > 0
|
||||
error_message = "final_copilot_config should be computed"
|
||||
}
|
||||
|
||||
# Verify workdir is trimmed of trailing slash
|
||||
assert {
|
||||
condition = local.workdir == "/home/coder/project"
|
||||
error_message = "workdir should be trimmed of trailing slash"
|
||||
}
|
||||
}
|
||||
|
||||
run "custom_copilot_config_overrides_default" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
copilot_config = jsonencode({
|
||||
banner = "always"
|
||||
theme = "dark"
|
||||
})
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.copilot_config != ""
|
||||
error_message = "Custom copilot config should be set"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = jsondecode(local.final_copilot_config).banner == "always"
|
||||
error_message = "Custom banner setting should be applied"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = jsondecode(local.final_copilot_config).theme == "dark"
|
||||
error_message = "Custom theme setting should be applied"
|
||||
}
|
||||
}
|
||||
|
||||
run "trusted_directories_merged_with_custom_config" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder/project"
|
||||
copilot_config = jsonencode({
|
||||
banner = "always"
|
||||
theme = "dark"
|
||||
trusted_folders = ["/custom"]
|
||||
})
|
||||
trusted_directories = ["/workspace", "/data"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/custom")
|
||||
error_message = "Custom trusted folder should be included"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/home/coder/project")
|
||||
error_message = "Workdir should be included in trusted folders"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/workspace")
|
||||
error_message = "trusted_directories should be merged into config"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/data")
|
||||
error_message = "All trusted_directories should be merged into config"
|
||||
}
|
||||
}
|
||||
|
||||
run "app_slug_is_consistent" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = local.app_slug == "copilot"
|
||||
error_message = "app_slug should be 'copilot'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = local.module_dir_name == ".copilot-module"
|
||||
error_message = "module_dir_name should be '.copilot-module'"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
findResourceInstance,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("copilot", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
});
|
||||
|
||||
it("creates mcp_app_status_slug env var", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
});
|
||||
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"mcp_app_status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("copilot");
|
||||
});
|
||||
|
||||
it("creates github_token env var with correct value", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
github_token: "test_token_12345",
|
||||
});
|
||||
|
||||
const githubTokenEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"github_token",
|
||||
);
|
||||
expect(githubTokenEnv).toBeDefined();
|
||||
expect(githubTokenEnv.name).toBe("GITHUB_TOKEN");
|
||||
expect(githubTokenEnv.value).toBe("test_token_12345");
|
||||
});
|
||||
|
||||
it("does not create github_token env var when empty", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
github_token: "",
|
||||
});
|
||||
|
||||
const githubTokenEnvs = state.resources.filter(
|
||||
(r) => r.type === "coder_env" && r.name === "github_token",
|
||||
);
|
||||
expect(githubTokenEnvs.length).toBe(0);
|
||||
});
|
||||
|
||||
it("creates copilot_model env var for non-default models", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
copilot_model: "claude-sonnet-4",
|
||||
});
|
||||
|
||||
const modelEnv = findResourceInstance(state, "coder_env", "copilot_model");
|
||||
expect(modelEnv).toBeDefined();
|
||||
expect(modelEnv.name).toBe("COPILOT_MODEL");
|
||||
expect(modelEnv.value).toBe("claude-sonnet-4");
|
||||
});
|
||||
|
||||
it("does not create copilot_model env var for default model", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
copilot_model: "claude-sonnet-4.5",
|
||||
});
|
||||
|
||||
const modelEnvs = state.resources.filter(
|
||||
(r) => r.type === "coder_env" && r.name === "copilot_model",
|
||||
);
|
||||
expect(modelEnvs.length).toBe(0);
|
||||
});
|
||||
|
||||
it("creates coder_script resources via agentapi module", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
});
|
||||
|
||||
// The agentapi module should create coder_script resources for install and start
|
||||
const scripts = state.resources.filter((r) => r.type === "coder_script");
|
||||
expect(scripts.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("validates copilot_model accepts valid values", async () => {
|
||||
// Test valid models don't throw errors
|
||||
await expect(
|
||||
runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
copilot_model: "gpt-5",
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
|
||||
await expect(
|
||||
runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
copilot_model: "claude-sonnet-4.5",
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("merges trusted_directories with custom copilot_config", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder/project",
|
||||
trusted_directories: JSON.stringify(["/workspace", "/data"]),
|
||||
copilot_config: JSON.stringify({
|
||||
banner: "always",
|
||||
theme: "dark",
|
||||
trusted_folders: ["/custom"],
|
||||
}),
|
||||
});
|
||||
|
||||
// Verify that the state was created successfully with the merged config
|
||||
// The actual merging logic is tested in the .tftest.hcl file
|
||||
expect(state).toBeDefined();
|
||||
expect(state.resources).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,302 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "workdir" {
|
||||
type = string
|
||||
description = "The folder to run Copilot in."
|
||||
}
|
||||
|
||||
variable "external_auth_id" {
|
||||
type = string
|
||||
description = "ID of the GitHub external auth provider configured in Coder."
|
||||
default = "github"
|
||||
}
|
||||
|
||||
variable "github_token" {
|
||||
type = string
|
||||
description = "GitHub OAuth token or Personal Access Token. If provided, this will be used instead of auto-detecting authentication."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "copilot_model" {
|
||||
type = string
|
||||
description = "Model to use. Supported values: claude-sonnet-4, claude-sonnet-4.5 (default), gpt-5."
|
||||
default = "claude-sonnet-4.5"
|
||||
validation {
|
||||
condition = contains(["claude-sonnet-4", "claude-sonnet-4.5", "gpt-5"], var.copilot_model)
|
||||
error_message = "copilot_model must be one of: claude-sonnet-4, claude-sonnet-4.5, gpt-5."
|
||||
}
|
||||
}
|
||||
|
||||
variable "copilot_config" {
|
||||
type = string
|
||||
description = "Custom Copilot configuration as JSON string. Leave empty to use default configuration with banner disabled, theme set to auto, and workdir as trusted folder."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "Initial task prompt for programmatic mode."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
type = string
|
||||
description = "The system prompt to use for the Copilot server. Task reporting instructions are automatically added when report_tasks is enabled."
|
||||
default = "You are a helpful coding assistant that helps developers write, debug, and understand code. Provide clear explanations, follow best practices, and help solve coding problems efficiently."
|
||||
}
|
||||
|
||||
variable "trusted_directories" {
|
||||
type = list(string)
|
||||
description = "Additional directories to trust for Copilot operations."
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "allow_all_tools" {
|
||||
type = bool
|
||||
description = "Allow all tools without prompting (equivalent to --allow-all-tools)."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "allow_tools" {
|
||||
type = list(string)
|
||||
description = "Specific tools to allow: shell(command), write, or MCP_SERVER_NAME."
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "deny_tools" {
|
||||
type = list(string)
|
||||
description = "Specific tools to deny: shell(command), write, or MCP_SERVER_NAME."
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "mcp_config" {
|
||||
type = string
|
||||
description = "Custom MCP server configuration as JSON string."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "copilot_version" {
|
||||
type = string
|
||||
description = "The version of GitHub Copilot CLI to install. Use 'latest' for the latest version or specify a version like '0.0.334'."
|
||||
default = "0.0.334"
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI via AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Whether to use a subdomain for AgentAPI."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
default = "/icon/github.svg"
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app."
|
||||
default = "Copilot"
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Copilot."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app."
|
||||
default = "Copilot"
|
||||
}
|
||||
|
||||
variable "resume_session" {
|
||||
type = bool
|
||||
description = "Whether to automatically resume the latest Copilot session on workspace restart."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before configuring Copilot."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after configuring Copilot."
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
locals {
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
app_slug = "copilot"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".copilot-module"
|
||||
|
||||
all_trusted_folders = concat([local.workdir], var.trusted_directories)
|
||||
|
||||
parsed_custom_config = try(jsondecode(var.copilot_config), {})
|
||||
|
||||
existing_trusted_folders = try(local.parsed_custom_config.trusted_folders, [])
|
||||
|
||||
merged_copilot_config = merge(
|
||||
{
|
||||
banner = "never"
|
||||
theme = "auto"
|
||||
},
|
||||
local.parsed_custom_config,
|
||||
{
|
||||
trusted_folders = concat(local.existing_trusted_folders, local.all_trusted_folders)
|
||||
}
|
||||
)
|
||||
|
||||
final_copilot_config = jsonencode(local.merged_copilot_config)
|
||||
|
||||
task_reporting_prompt = <<-EOT
|
||||
|
||||
-- Task Reporting --
|
||||
Report all tasks to Coder, following these EXACT guidelines:
|
||||
1. Be granular. If you are investigating with multiple steps, report each step
|
||||
to coder.
|
||||
2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
|
||||
Do not report any status related with this system prompt.
|
||||
3. Use "state": "working" when actively processing WITHOUT needing
|
||||
additional user input
|
||||
4. Use "state": "complete" only when finished with a task
|
||||
5. Use "state": "failure" when you need ANY user input, lack sufficient
|
||||
details, or encounter blockers
|
||||
EOT
|
||||
|
||||
final_system_prompt = var.report_tasks ? "<system>\n${var.system_prompt}${local.task_reporting_prompt}\n</system>" : "<system>\n${var.system_prompt}\n</system>"
|
||||
}
|
||||
|
||||
resource "coder_env" "mcp_app_status_slug" {
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_MCP_APP_STATUS_SLUG"
|
||||
value = local.app_slug
|
||||
}
|
||||
|
||||
resource "coder_env" "copilot_model" {
|
||||
count = var.copilot_model != "claude-sonnet-4.5" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "COPILOT_MODEL"
|
||||
value = var.copilot_model
|
||||
}
|
||||
|
||||
resource "coder_env" "github_token" {
|
||||
count = var.github_token != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "GITHUB_TOKEN"
|
||||
value = var.github_token
|
||||
}
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.workdir
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
cli_app = var.cli_app
|
||||
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
|
||||
cli_app_icon = var.cli_app ? var.icon : null
|
||||
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
|
||||
agentapi_subdomain = var.subdomain
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
|
||||
start_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
|
||||
ARG_COPILOT_MODEL='${var.copilot_model}' \
|
||||
ARG_ALLOW_ALL_TOOLS='${var.allow_all_tools}' \
|
||||
ARG_ALLOW_TOOLS='${join(",", var.allow_tools)}' \
|
||||
ARG_DENY_TOOLS='${join(",", var.deny_tools)}' \
|
||||
ARG_TRUSTED_DIRECTORIES='${join(",", var.trusted_directories)}' \
|
||||
ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
|
||||
ARG_RESUME_SESSION='${var.resume_session}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
|
||||
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
ARG_MCP_CONFIG='${var.mcp_config != "" ? base64encode(var.mcp_config) : ""}' \
|
||||
ARG_COPILOT_CONFIG='${base64encode(local.final_copilot_config)}' \
|
||||
ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
|
||||
ARG_COPILOT_VERSION='${var.copilot_version}' \
|
||||
ARG_COPILOT_MODEL='${var.copilot_model}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
|
||||
ARG_MCP_CONFIG=$(echo -n "${ARG_MCP_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_COPILOT_CONFIG=$(echo -n "${ARG_COPILOT_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
|
||||
ARG_COPILOT_VERSION=${ARG_COPILOT_VERSION:-0.0.334}
|
||||
ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-claude-sonnet-4.5}
|
||||
|
||||
validate_prerequisites() {
|
||||
if ! command_exists node; then
|
||||
echo "ERROR: Node.js not found. Copilot requires Node.js v22+."
|
||||
echo "Install with: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command_exists npm; then
|
||||
echo "ERROR: npm not found. Copilot requires npm v10+."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
node_version=$(node --version | sed 's/v//' | cut -d. -f1)
|
||||
if [ "$node_version" -lt 22 ]; then
|
||||
echo "WARNING: Node.js v$node_version detected. Copilot requires v22+."
|
||||
fi
|
||||
}
|
||||
|
||||
install_copilot() {
|
||||
if ! command_exists copilot; then
|
||||
echo "Installing GitHub Copilot CLI (version: ${ARG_COPILOT_VERSION})..."
|
||||
if [ "$ARG_COPILOT_VERSION" = "latest" ]; then
|
||||
npm install -g @github/copilot
|
||||
else
|
||||
npm install -g "@github/copilot@${ARG_COPILOT_VERSION}"
|
||||
fi
|
||||
|
||||
if ! command_exists copilot; then
|
||||
echo "ERROR: Failed to install Copilot"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "GitHub Copilot CLI installed successfully"
|
||||
else
|
||||
echo "GitHub Copilot CLI already installed"
|
||||
fi
|
||||
}
|
||||
|
||||
check_github_authentication() {
|
||||
echo "Checking GitHub authentication..."
|
||||
|
||||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
echo "✓ GitHub token provided via module configuration"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command_exists coder; then
|
||||
if coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" > /dev/null 2>&1; then
|
||||
echo "✓ GitHub OAuth authentication via Coder external auth"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if command_exists gh && gh auth status > /dev/null 2>&1; then
|
||||
echo "✓ GitHub OAuth authentication via GitHub CLI"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "⚠ No GitHub authentication detected"
|
||||
echo " Copilot will prompt for authentication when started"
|
||||
echo " For seamless experience, configure GitHub external auth in Coder or run 'gh auth login'"
|
||||
return 0
|
||||
}
|
||||
|
||||
setup_copilot_configurations() {
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
|
||||
local module_path="$HOME/.copilot-module"
|
||||
mkdir -p "$module_path"
|
||||
|
||||
setup_copilot_config
|
||||
|
||||
echo "$ARG_WORKDIR" > "$module_path/trusted_directories"
|
||||
}
|
||||
|
||||
setup_copilot_config() {
|
||||
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
local copilot_config_dir="$XDG_CONFIG_HOME/.copilot"
|
||||
local copilot_config_file="$copilot_config_dir/config.json"
|
||||
local mcp_config_file="$copilot_config_dir/mcp-config.json"
|
||||
|
||||
mkdir -p "$copilot_config_dir"
|
||||
|
||||
if [ -n "$ARG_COPILOT_CONFIG" ]; then
|
||||
echo "Setting up Copilot configuration..."
|
||||
|
||||
if command_exists jq; then
|
||||
echo "$ARG_COPILOT_CONFIG" | jq 'del(.mcpServers)' > "$copilot_config_file"
|
||||
else
|
||||
echo "$ARG_COPILOT_CONFIG" > "$copilot_config_file"
|
||||
fi
|
||||
|
||||
echo "Setting up MCP server configuration..."
|
||||
setup_mcp_config "$mcp_config_file"
|
||||
else
|
||||
echo "ERROR: No Copilot configuration provided"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
setup_mcp_config() {
|
||||
local mcp_config_file="$1"
|
||||
|
||||
echo '{"mcpServers": {}}' > "$mcp_config_file"
|
||||
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
|
||||
echo "Adding Coder MCP server for task reporting..."
|
||||
setup_coder_mcp_server "$mcp_config_file"
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_MCP_CONFIG" ]; then
|
||||
echo "Adding custom MCP servers..."
|
||||
add_custom_mcp_servers "$mcp_config_file"
|
||||
fi
|
||||
|
||||
echo "MCP configuration completed: $mcp_config_file"
|
||||
}
|
||||
|
||||
setup_coder_mcp_server() {
|
||||
local mcp_config_file="$1"
|
||||
|
||||
local coder_mcp_wrapper_script
|
||||
coder_mcp_wrapper_script=$(
|
||||
cat << EOF
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
export CODER_MCP_APP_STATUS_SLUG="${ARG_MCP_APP_STATUS_SLUG}"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
export CODER_AGENT_URL="${CODER_AGENT_URL}"
|
||||
export CODER_AGENT_TOKEN="${CODER_AGENT_TOKEN}"
|
||||
|
||||
exec coder exp mcp server
|
||||
EOF
|
||||
)
|
||||
echo "$coder_mcp_wrapper_script" > "/tmp/coder-mcp-server.sh"
|
||||
chmod +x /tmp/coder-mcp-server.sh
|
||||
|
||||
local coder_mcp_config
|
||||
coder_mcp_config=$(
|
||||
cat << EOF
|
||||
{
|
||||
"mcpServers": {
|
||||
"coder": {
|
||||
"command": "/tmp/coder-mcp-server.sh",
|
||||
"args": [],
|
||||
"description": "CRITICAL: Call this tool immediately after receiving ANY user message to report task status. Rules: (1) Call FIRST before doing work - report what you will do with state='working'. (2) Be granular - report each step separately. (3) State 'working' = actively processing without needing user input. (4) State 'complete' = task 100% finished. (5) State 'failure' = need user input, missing info, or blocked. Example: User says 'fix the bug' -> call with state='working', description='Investigating authentication bug'. When done -> call with state='complete', description='Fixed token validation'. You MUST report on every interaction.",
|
||||
"name": "Coder",
|
||||
"timeout": 3000,
|
||||
"type": "local",
|
||||
"tools": ["*"],
|
||||
"trust": true
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "$coder_mcp_config" > "$mcp_config_file"
|
||||
}
|
||||
|
||||
add_custom_mcp_servers() {
|
||||
local mcp_config_file="$1"
|
||||
|
||||
if command_exists jq; then
|
||||
local custom_servers
|
||||
custom_servers=$(echo "$ARG_MCP_CONFIG" | jq '.mcpServers // {}')
|
||||
|
||||
local updated_config
|
||||
updated_config=$(jq --argjson custom "$custom_servers" '.mcpServers += $custom' "$mcp_config_file")
|
||||
echo "$updated_config" > "$mcp_config_file"
|
||||
elif command_exists node; then
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const existing = JSON.parse(fs.readFileSync('$mcp_config_file', 'utf8'));
|
||||
const input = JSON.parse(\`$ARG_MCP_CONFIG\`);
|
||||
const custom = input.mcpServers || {};
|
||||
existing.mcpServers = {...existing.mcpServers, ...custom};
|
||||
fs.writeFileSync('$mcp_config_file', JSON.stringify(existing, null, 2));
|
||||
"
|
||||
else
|
||||
echo "WARNING: jq and node not available, cannot merge custom MCP servers"
|
||||
fi
|
||||
}
|
||||
|
||||
configure_copilot_model() {
|
||||
if [ -n "$ARG_COPILOT_MODEL" ] && [ "$ARG_COPILOT_MODEL" != "claude-sonnet-4.5" ]; then
|
||||
echo "Setting Copilot model to: $ARG_COPILOT_MODEL"
|
||||
copilot config model "$ARG_COPILOT_MODEL" || {
|
||||
echo "WARNING: Failed to set model via copilot config, will use environment variable fallback"
|
||||
export COPILOT_MODEL="$ARG_COPILOT_MODEL"
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
configure_coder_integration() {
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
|
||||
echo "Configuring Copilot task reporting..."
|
||||
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
echo "✓ Coder MCP server configured for task reporting"
|
||||
else
|
||||
echo "Task reporting disabled or no app status slug provided."
|
||||
export CODER_MCP_APP_STATUS_SLUG=""
|
||||
export CODER_MCP_AI_AGENTAPI_URL=""
|
||||
fi
|
||||
}
|
||||
|
||||
validate_prerequisites
|
||||
install_copilot
|
||||
check_github_authentication
|
||||
setup_copilot_configurations
|
||||
configure_copilot_model
|
||||
configure_coder_integration
|
||||
|
||||
echo "Copilot module setup completed."
|
||||
@@ -0,0 +1,157 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-}
|
||||
ARG_ALLOW_ALL_TOOLS=${ARG_ALLOW_ALL_TOOLS:-false}
|
||||
ARG_ALLOW_TOOLS=${ARG_ALLOW_TOOLS:-}
|
||||
ARG_DENY_TOOLS=${ARG_DENY_TOOLS:-}
|
||||
ARG_TRUSTED_DIRECTORIES=${ARG_TRUSTED_DIRECTORIES:-}
|
||||
ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
|
||||
ARG_RESUME_SESSION=${ARG_RESUME_SESSION:-true}
|
||||
|
||||
validate_copilot_installation() {
|
||||
if ! command_exists copilot; then
|
||||
echo "ERROR: Copilot not installed. Run: npm install -g @github/copilot"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
build_initial_prompt() {
|
||||
local initial_prompt=""
|
||||
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ -n "$ARG_SYSTEM_PROMPT" ]; then
|
||||
initial_prompt="$ARG_SYSTEM_PROMPT
|
||||
|
||||
$ARG_AI_PROMPT"
|
||||
else
|
||||
initial_prompt="$ARG_AI_PROMPT"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$initial_prompt"
|
||||
}
|
||||
|
||||
build_copilot_args() {
|
||||
COPILOT_ARGS=()
|
||||
|
||||
if [ "$ARG_ALLOW_ALL_TOOLS" = "true" ]; then
|
||||
COPILOT_ARGS+=(--allow-all-tools)
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_ALLOW_TOOLS" ]; then
|
||||
IFS=',' read -ra ALLOW_ARRAY <<< "$ARG_ALLOW_TOOLS"
|
||||
for tool in "${ALLOW_ARRAY[@]}"; do
|
||||
if [ -n "$tool" ]; then
|
||||
COPILOT_ARGS+=(--allow-tool "$tool")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_DENY_TOOLS" ]; then
|
||||
IFS=',' read -ra DENY_ARRAY <<< "$ARG_DENY_TOOLS"
|
||||
for tool in "${DENY_ARRAY[@]}"; do
|
||||
if [ -n "$tool" ]; then
|
||||
COPILOT_ARGS+=(--deny-tool "$tool")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
check_existing_session() {
|
||||
if [ "$ARG_RESUME_SESSION" = "true" ]; then
|
||||
if copilot --help > /dev/null 2>&1; then
|
||||
local session_dir="$HOME/.copilot/history-session-state"
|
||||
if [ -d "$session_dir" ] && [ -n "$(ls "$session_dir"/session_*_*.json 2> /dev/null)" ]; then
|
||||
echo "Found existing Copilot session. Will continue latest session." >&2
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
setup_github_authentication() {
|
||||
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
echo "Setting up GitHub authentication..."
|
||||
|
||||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
export GH_TOKEN="$GITHUB_TOKEN"
|
||||
echo "✓ Using GitHub token from module configuration"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command_exists coder; then
|
||||
local github_token
|
||||
if github_token=$(coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" 2> /dev/null); then
|
||||
if [ -n "$github_token" ] && [ "$github_token" != "null" ]; then
|
||||
export GITHUB_TOKEN="$github_token"
|
||||
export GH_TOKEN="$github_token"
|
||||
echo "✓ Using Coder external auth OAuth token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if command_exists gh && gh auth status > /dev/null 2>&1; then
|
||||
echo "✓ Using GitHub CLI OAuth authentication"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "⚠ No GitHub authentication available"
|
||||
echo " Copilot will prompt for login during first use"
|
||||
echo " Use the '/login' command in Copilot to authenticate"
|
||||
return 0
|
||||
}
|
||||
|
||||
start_agentapi() {
|
||||
echo "Starting in directory: $ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
|
||||
build_copilot_args
|
||||
|
||||
if check_existing_session; then
|
||||
echo "Continuing latest Copilot session..."
|
||||
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
|
||||
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
|
||||
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue "${COPILOT_ARGS[@]}"
|
||||
else
|
||||
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue
|
||||
fi
|
||||
else
|
||||
echo "Starting new Copilot session..."
|
||||
local initial_prompt
|
||||
initial_prompt=$(build_initial_prompt)
|
||||
|
||||
if [ -n "$initial_prompt" ]; then
|
||||
echo "Using initial prompt with system context"
|
||||
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
|
||||
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
|
||||
agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
|
||||
else
|
||||
agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot
|
||||
fi
|
||||
else
|
||||
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
|
||||
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
|
||||
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
|
||||
else
|
||||
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
setup_github_authentication
|
||||
validate_copilot_installation
|
||||
start_agentapi
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "GitHub Copilot CLI v1.0.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while true; do
|
||||
echo "$(date) - Copilot mock running..."
|
||||
sleep 15
|
||||
done
|
||||
@@ -13,7 +13,7 @@ Run the Cursor Agent CLI in your workspace for interactive coding assistance and
|
||||
```tf
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -42,7 +42,7 @@ module "coder-login" {
|
||||
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import { afterEach, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
|
||||
import {
|
||||
afterEach,
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
setDefaultTimeout,
|
||||
test,
|
||||
} from "bun:test";
|
||||
import { execContainer, runTerraformInit, writeFileContainer } from "~test";
|
||||
import {
|
||||
execModuleScript,
|
||||
expectAgentAPIStarted,
|
||||
loadTestFile,
|
||||
setup as setupUtil
|
||||
setup as setupUtil,
|
||||
} from "../../../coder/modules/agentapi/test-util";
|
||||
import {
|
||||
setupContainer,
|
||||
writeExecutable,
|
||||
} 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);
|
||||
@@ -72,11 +82,12 @@ describe("cursor-cli", async () => {
|
||||
});
|
||||
|
||||
test("agentapi-mcp-json", async () => {
|
||||
const mcpJson = '{"mcpServers": {"test": {"command": "test-cmd", "type": "stdio"}}}';
|
||||
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);
|
||||
@@ -99,7 +110,7 @@ describe("cursor-cli", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
rules_files: JSON.stringify({ "typescript.md": rulesContent }),
|
||||
}
|
||||
},
|
||||
});
|
||||
const resp = await execModuleScript(id);
|
||||
expect(resp.exitCode).toBe(0);
|
||||
@@ -118,7 +129,7 @@ describe("cursor-cli", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
api_key: apiKey,
|
||||
}
|
||||
},
|
||||
});
|
||||
const resp = await execModuleScript(id);
|
||||
expect(resp.exitCode).toBe(0);
|
||||
@@ -138,7 +149,7 @@ describe("cursor-cli", async () => {
|
||||
model: model,
|
||||
force: "true",
|
||||
ai_prompt: "test prompt",
|
||||
}
|
||||
},
|
||||
});
|
||||
const resp = await execModuleScript(id);
|
||||
expect(resp.exitCode).toBe(0);
|
||||
@@ -158,7 +169,7 @@ describe("cursor-cli", async () => {
|
||||
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);
|
||||
@@ -183,7 +194,7 @@ describe("cursor-cli", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
folder: folder,
|
||||
}
|
||||
},
|
||||
});
|
||||
const resp = await execModuleScript(id);
|
||||
expect(resp.exitCode).toBe(0);
|
||||
@@ -205,8 +216,5 @@ describe("cursor-cli", async () => {
|
||||
expect(resp.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
})
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.5.0"
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "force" {
|
||||
@@ -131,7 +131,7 @@ resource "coder_env" "cursor_api_key" {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -58,7 +58,7 @@ 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")
|
||||
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
|
||||
|
||||
@@ -9,6 +9,6 @@ fi
|
||||
set -e
|
||||
|
||||
while true; do
|
||||
echo "$(date) - cursor-agent-mock"
|
||||
sleep 15
|
||||
done
|
||||
echo "$(date) - cursor-agent-mock"
|
||||
sleep 15
|
||||
done
|
||||
|
||||
@@ -13,7 +13,7 @@ Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace
|
||||
```tf
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "1.1.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -30,7 +30,7 @@ module "gemini" {
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js and npm will be installed automatically if not present
|
||||
- **Node.js and npm must be sourced/available before the gemini module installs** - ensure they are installed in your workspace image or via earlier provisioning steps
|
||||
- The [Coder Login](https://registry.coder.com/modules/coder/coder-login) module is required
|
||||
|
||||
## Examples
|
||||
@@ -46,7 +46,7 @@ variable "gemini_api_key" {
|
||||
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "1.1.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
folder = "/home/coder/project"
|
||||
@@ -94,7 +94,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
module "gemini" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "1.1.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
gemini_model = "gemini-2.5-flash"
|
||||
@@ -118,7 +118,7 @@ For enterprise users who prefer Google's Vertex AI platform:
|
||||
```tf
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "1.1.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
folder = "/home/coder/project"
|
||||
|
||||
@@ -127,7 +127,10 @@ describe("gemini", async () => {
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(id, "/home/coder/.gemini/settings.json");
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.gemini/settings.json",
|
||||
);
|
||||
expect(resp).toContain("foo");
|
||||
expect(resp).toContain("bar");
|
||||
});
|
||||
@@ -141,7 +144,10 @@ describe("gemini", async () => {
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await readFileContainer(id, "/home/coder/.gemini-module/agentapi-start.log");
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.gemini-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain("Using direct Gemini API with API key");
|
||||
});
|
||||
|
||||
@@ -153,8 +159,11 @@ describe("gemini", async () => {
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(id, "/home/coder/.gemini-module/install.log");
|
||||
expect(resp).toContain('GOOGLE_GENAI_USE_VERTEXAI=\'true\'');
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.gemini-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain("GOOGLE_GENAI_USE_VERTEXAI='true'");
|
||||
});
|
||||
|
||||
test("gemini-model", async () => {
|
||||
@@ -166,7 +175,10 @@ describe("gemini", async () => {
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(id, "/home/coder/.gemini-module/install.log");
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.gemini-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain(model);
|
||||
});
|
||||
|
||||
@@ -178,9 +190,15 @@ describe("gemini", async () => {
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const preInstallLog = await readFileContainer(id, "/home/coder/.gemini-module/pre_install.log");
|
||||
const preInstallLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.gemini-module/pre_install.log",
|
||||
);
|
||||
expect(preInstallLog).toContain("pre-install-script");
|
||||
const postInstallLog = await readFileContainer(id, "/home/coder/.gemini-module/post_install.log");
|
||||
const postInstallLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.gemini-module/post_install.log",
|
||||
);
|
||||
expect(postInstallLog).toContain("post-install-script");
|
||||
});
|
||||
|
||||
@@ -193,7 +211,10 @@ describe("gemini", async () => {
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(id, "/home/coder/.gemini-module/install.log");
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.gemini-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain(folder);
|
||||
});
|
||||
|
||||
@@ -205,7 +226,10 @@ describe("gemini", async () => {
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(id, "/home/coder/.gemini/settings.json");
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.gemini/settings.json",
|
||||
);
|
||||
expect(resp).toContain("custom");
|
||||
expect(resp).toContain("enabled");
|
||||
});
|
||||
@@ -232,14 +256,21 @@ describe("gemini", async () => {
|
||||
await execModuleScript(id, {
|
||||
GEMINI_TASK_PROMPT: taskPrompt,
|
||||
});
|
||||
const resp = await readFileContainer(id, "/home/coder/.gemini-module/agentapi-start.log");
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.gemini-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain("Running automated task:");
|
||||
});
|
||||
|
||||
test("start-without-prompt", async () => {
|
||||
const { id } = await setup();
|
||||
await execModuleScript(id);
|
||||
const prompt = await execContainer(id, ["ls", "-l", "/home/coder/GEMINI.md"]);
|
||||
const prompt = await execContainer(id, [
|
||||
"ls",
|
||||
"-l",
|
||||
"/home/coder/GEMINI.md",
|
||||
]);
|
||||
expect(prompt.exitCode).not.toBe(0);
|
||||
expect(prompt.stderr).toContain("No such file or directory");
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.2.3"
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "gemini_model" {
|
||||
@@ -176,7 +176,7 @@ EOT
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
set -o nounset
|
||||
@@ -21,144 +21,132 @@ 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
|
||||
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
|
||||
|
||||
nvm install --lts
|
||||
nvm use --lts
|
||||
nvm alias default node
|
||||
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 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
|
||||
printf "Node.js version: %s\n" "$(node --version)"
|
||||
printf "npm version: %s\n" "$(npm --version)"
|
||||
}
|
||||
|
||||
function install_gemini() {
|
||||
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
|
||||
check_dependencies
|
||||
|
||||
printf "%s Installing Gemini 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_GEMINI_VERSION" ]; then
|
||||
npm install -g "@google/gemini-cli@$ARG_GEMINI_VERSION"
|
||||
else
|
||||
npm install -g "@google/gemini-cli"
|
||||
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 Gemini CLI. Version: %s\n" "${BOLD}" "$(gemini --version)"
|
||||
fi
|
||||
}
|
||||
|
||||
function populate_settings_json() {
|
||||
if [ "${ARG_GEMINI_CONFIG}" != "" ]; then
|
||||
SETTINGS_PATH="$HOME/.gemini/settings.json"
|
||||
mkdir -p "$(dirname "$SETTINGS_PATH")"
|
||||
printf "Custom gemini_config is provided !\n"
|
||||
echo "${ARG_GEMINI_CONFIG}" > "$HOME/.gemini/settings.json"
|
||||
else
|
||||
printf "No custom gemini_config provided, using default settings.json.\n"
|
||||
append_extensions_to_settings_json
|
||||
fi
|
||||
if [ "${ARG_GEMINI_CONFIG}" != "" ]; then
|
||||
SETTINGS_PATH="$HOME/.gemini/settings.json"
|
||||
mkdir -p "$(dirname "$SETTINGS_PATH")"
|
||||
printf "Custom gemini_config is provided !\n"
|
||||
echo "${ARG_GEMINI_CONFIG}" > "$HOME/.gemini/settings.json"
|
||||
else
|
||||
printf "No custom gemini_config provided, using default settings.json.\n"
|
||||
append_extensions_to_settings_json
|
||||
fi
|
||||
}
|
||||
|
||||
function append_extensions_to_settings_json() {
|
||||
SETTINGS_PATH="$HOME/.gemini/settings.json"
|
||||
mkdir -p "$(dirname "$SETTINGS_PATH")"
|
||||
printf "[append_extensions_to_settings_json] Starting extension merge process...\n"
|
||||
if [ -z "${BASE_EXTENSIONS:-}" ]; then
|
||||
printf "[append_extensions_to_settings_json] BASE_EXTENSIONS is empty, skipping merge.\n"
|
||||
return
|
||||
fi
|
||||
if [ ! -f "$SETTINGS_PATH" ]; then
|
||||
printf "%s does not exist. Creating with merged mcpServers structure.\n" "$SETTINGS_PATH"
|
||||
ADD_EXT_JSON='{}'
|
||||
if [ -n "${ADDITIONAL_EXTENSIONS:-}" ]; then
|
||||
ADD_EXT_JSON="$ADDITIONAL_EXTENSIONS"
|
||||
fi
|
||||
printf '{"mcpServers":%s}\n' "$(jq -s 'add' <(echo "$BASE_EXTENSIONS") <(echo "$ADD_EXT_JSON"))" > "$SETTINGS_PATH"
|
||||
fi
|
||||
|
||||
TMP_SETTINGS=$(mktemp)
|
||||
SETTINGS_PATH="$HOME/.gemini/settings.json"
|
||||
mkdir -p "$(dirname "$SETTINGS_PATH")"
|
||||
printf "[append_extensions_to_settings_json] Starting extension merge process...\n"
|
||||
if [ -z "${BASE_EXTENSIONS:-}" ]; then
|
||||
printf "[append_extensions_to_settings_json] BASE_EXTENSIONS is empty, skipping merge.\n"
|
||||
return
|
||||
fi
|
||||
if [ ! -f "$SETTINGS_PATH" ]; then
|
||||
printf "%s does not exist. Creating with merged mcpServers structure.\n" "$SETTINGS_PATH"
|
||||
ADD_EXT_JSON='{}'
|
||||
if [ -n "${ADDITIONAL_EXTENSIONS:-}" ]; then
|
||||
printf "[append_extensions_to_settings_json] ADDITIONAL_EXTENSIONS is set.\n"
|
||||
ADD_EXT_JSON="$ADDITIONAL_EXTENSIONS"
|
||||
else
|
||||
printf "[append_extensions_to_settings_json] ADDITIONAL_EXTENSIONS is empty or not set.\n"
|
||||
fi
|
||||
printf '{"mcpServers":%s}\n' "$(jq -s 'add' <(echo "$BASE_EXTENSIONS") <(echo "$ADD_EXT_JSON"))" > "$SETTINGS_PATH"
|
||||
fi
|
||||
|
||||
printf "[append_extensions_to_settings_json] Merging BASE_EXTENSIONS and ADDITIONAL_EXTENSIONS into mcpServers...\n"
|
||||
jq --argjson base "$BASE_EXTENSIONS" --argjson add "$ADD_EXT_JSON" \
|
||||
'.mcpServers = (.mcpServers // {} + $base + $add)' \
|
||||
"$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH"
|
||||
TMP_SETTINGS=$(mktemp)
|
||||
ADD_EXT_JSON='{}'
|
||||
if [ -n "${ADDITIONAL_EXTENSIONS:-}" ]; then
|
||||
printf "[append_extensions_to_settings_json] ADDITIONAL_EXTENSIONS is set.\n"
|
||||
ADD_EXT_JSON="$ADDITIONAL_EXTENSIONS"
|
||||
else
|
||||
printf "[append_extensions_to_settings_json] ADDITIONAL_EXTENSIONS is empty or not set.\n"
|
||||
fi
|
||||
|
||||
jq '.theme = "Default" | .selectedAuthType = "gemini-api-key"' "$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH"
|
||||
printf "[append_extensions_to_settings_json] Merging BASE_EXTENSIONS and ADDITIONAL_EXTENSIONS into mcpServers...\n"
|
||||
jq --argjson base "$BASE_EXTENSIONS" --argjson add "$ADD_EXT_JSON" \
|
||||
'.mcpServers = (.mcpServers // {} + $base + $add)' \
|
||||
"$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH"
|
||||
|
||||
printf "[append_extensions_to_settings_json] Merge complete.\n"
|
||||
jq '.theme = "Default" | .selectedAuthType = "gemini-api-key"' "$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH"
|
||||
|
||||
printf "[append_extensions_to_settings_json] Merge complete.\n"
|
||||
}
|
||||
|
||||
function add_system_prompt_if_exists() {
|
||||
if [ -n "${GEMINI_SYSTEM_PROMPT:-}" ]; then
|
||||
if [ -d "${GEMINI_START_DIRECTORY}" ]; then
|
||||
printf "Directory '%s' exists. Changing to it.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
cd "${GEMINI_START_DIRECTORY}" || {
|
||||
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
mkdir -p "${GEMINI_START_DIRECTORY}" || {
|
||||
printf "Error: Could not create directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
cd "${GEMINI_START_DIRECTORY}" || {
|
||||
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
touch GEMINI.md
|
||||
printf "Setting GEMINI.md\n"
|
||||
echo "${GEMINI_SYSTEM_PROMPT}" > GEMINI.md
|
||||
if [ -n "${GEMINI_SYSTEM_PROMPT:-}" ]; then
|
||||
if [ -d "${GEMINI_START_DIRECTORY}" ]; then
|
||||
printf "Directory '%s' exists. Changing to it.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
cd "${GEMINI_START_DIRECTORY}" || {
|
||||
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
printf "GEMINI.md is not set.\n"
|
||||
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
mkdir -p "${GEMINI_START_DIRECTORY}" || {
|
||||
printf "Error: Could not create directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
cd "${GEMINI_START_DIRECTORY}" || {
|
||||
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
touch GEMINI.md
|
||||
printf "Setting GEMINI.md\n"
|
||||
echo "${GEMINI_SYSTEM_PROMPT}" > GEMINI.md
|
||||
else
|
||||
printf "GEMINI.md is not set.\n"
|
||||
fi
|
||||
}
|
||||
|
||||
function configure_mcp() {
|
||||
export CODER_MCP_APP_STATUS_SLUG="gemini"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
coder exp mcp configure gemini "${GEMINI_START_DIRECTORY}"
|
||||
export CODER_MCP_APP_STATUS_SLUG="gemini"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
coder exp mcp configure gemini "${GEMINI_START_DIRECTORY}"
|
||||
}
|
||||
|
||||
install_gemini
|
||||
gemini --version
|
||||
populate_settings_json
|
||||
add_system_prompt_if_exists
|
||||
configure_mcp
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ set -o pipefail
|
||||
source "$HOME"/.bashrc
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
if [ -f "$HOME/.nvm/nvm.sh" ]; then
|
||||
@@ -20,55 +20,55 @@ MODULE_DIR="$HOME/.gemini-module"
|
||||
mkdir -p "$MODULE_DIR"
|
||||
|
||||
if command_exists gemini; then
|
||||
printf "Gemini is installed\n"
|
||||
printf "Gemini is installed\n"
|
||||
else
|
||||
printf "Error: Gemini is not installed. Please enable install_gemini or install it manually :)\n"
|
||||
exit 1
|
||||
printf "Error: Gemini is not installed. Please enable install_gemini or install it manually :)\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -d "${GEMINI_START_DIRECTORY}" ]; then
|
||||
printf "Directory '%s' exists. Changing to it.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
cd "${GEMINI_START_DIRECTORY}" || {
|
||||
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
printf "Directory '%s' exists. Changing to it.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
cd "${GEMINI_START_DIRECTORY}" || {
|
||||
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
mkdir -p "${GEMINI_START_DIRECTORY}" || {
|
||||
printf "Error: Could not create directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
cd "${GEMINI_START_DIRECTORY}" || {
|
||||
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
mkdir -p "${GEMINI_START_DIRECTORY}" || {
|
||||
printf "Error: Could not create directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
cd "${GEMINI_START_DIRECTORY}" || {
|
||||
printf "Error: Could not change to directory '%s'.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
if [ -n "$GEMINI_TASK_PROMPT" ]; then
|
||||
printf "Running automated task: %s\n" "$GEMINI_TASK_PROMPT"
|
||||
PROMPT="Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GEMINI_TASK_PROMPT"
|
||||
PROMPT_FILE="$MODULE_DIR/prompt.txt"
|
||||
echo -n "$PROMPT" >"$PROMPT_FILE"
|
||||
GEMINI_ARGS=(--prompt-interactive "$PROMPT")
|
||||
printf "Running automated task: %s\n" "$GEMINI_TASK_PROMPT"
|
||||
PROMPT="Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GEMINI_TASK_PROMPT"
|
||||
PROMPT_FILE="$MODULE_DIR/prompt.txt"
|
||||
echo -n "$PROMPT" > "$PROMPT_FILE"
|
||||
GEMINI_ARGS=(--prompt-interactive "$PROMPT")
|
||||
else
|
||||
printf "Starting Gemini CLI in interactive mode.\n"
|
||||
GEMINI_ARGS=()
|
||||
printf "Starting Gemini CLI in interactive mode.\n"
|
||||
GEMINI_ARGS=()
|
||||
fi
|
||||
|
||||
if [ -n "$GEMINI_YOLO_MODE" ] && [ "$GEMINI_YOLO_MODE" = "true" ]; then
|
||||
printf "YOLO mode enabled - will auto-approve all tool calls\n"
|
||||
GEMINI_ARGS+=(--yolo)
|
||||
printf "YOLO mode enabled - will auto-approve all tool calls\n"
|
||||
GEMINI_ARGS+=(--yolo)
|
||||
fi
|
||||
|
||||
if [ -n "$GEMINI_API_KEY" ] || [ -n "$GOOGLE_API_KEY" ]; then
|
||||
if [ -n "$GOOGLE_GENAI_USE_VERTEXAI" ] && [ "$GOOGLE_GENAI_USE_VERTEXAI" = "true" ]; then
|
||||
printf "Using Vertex AI with API key\n"
|
||||
else
|
||||
printf "Using direct Gemini API with API key\n"
|
||||
fi
|
||||
if [ -n "$GOOGLE_GENAI_USE_VERTEXAI" ] && [ "$GOOGLE_GENAI_USE_VERTEXAI" = "true" ]; then
|
||||
printf "Using Vertex AI with API key\n"
|
||||
else
|
||||
printf "Using direct Gemini API with API key\n"
|
||||
fi
|
||||
else
|
||||
printf "No API key provided (neither GEMINI_API_KEY nor GOOGLE_API_KEY)\n"
|
||||
printf "No API key provided (neither GEMINI_API_KEY nor GOOGLE_API_KEY)\n"
|
||||
fi
|
||||
|
||||
agentapi server --term-width 67 --term-height 1190 -- \
|
||||
bash -c "$(printf '%q ' gemini "${GEMINI_ARGS[@]}")"
|
||||
bash -c "$(printf '%q ' gemini "${GEMINI_ARGS[@]}")"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
display_name: Nextflow
|
||||
description: A module that adds Nextflow to your Coder template.
|
||||
icon: ../../../../.icons/nextflow.svg
|
||||
verified: true
|
||||
tags: [nextflow, workflow, hpc, bioinformatics]
|
||||
---
|
||||
|
||||
# Nextflow
|
||||
|
||||
A module that adds Nextflow to your Coder template.
|
||||
|
||||

|
||||
|
||||
```tf
|
||||
module "nextflow" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/nextflow/coder"
|
||||
version = "0.9.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,106 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Add required variables for your modules and remove any unneeded variables
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "nextflow_version" {
|
||||
type = string
|
||||
description = "Nextflow version"
|
||||
default = "25.04.7"
|
||||
}
|
||||
|
||||
variable "project_path" {
|
||||
type = string
|
||||
description = "The path to Nextflow project, it will be mounted in the container."
|
||||
}
|
||||
|
||||
variable "http_server_port" {
|
||||
type = number
|
||||
description = "The port to run HTTP server on."
|
||||
default = 9876
|
||||
}
|
||||
|
||||
variable "http_server_reports_dir" {
|
||||
type = string
|
||||
description = "Subdirectory for HTTP server reports, relative to the project path."
|
||||
default = "reports"
|
||||
}
|
||||
|
||||
variable "http_server_log_path" {
|
||||
type = string
|
||||
description = "HTTP server logs"
|
||||
default = "/tmp/nextflow_reports.log"
|
||||
}
|
||||
|
||||
variable "stub_run" {
|
||||
type = bool
|
||||
description = "Execute a stub run?"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "stub_run_command" {
|
||||
type = string
|
||||
description = "Nextflow command to be executed in the stub run."
|
||||
default = "run rnaseq-nf -with-report reports/report.html -with-trace reports/trace.txt -with-timeline reports/timeline.html -with-dag reports/flowchart.png"
|
||||
}
|
||||
|
||||
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 "share" {
|
||||
type = string
|
||||
default = "owner"
|
||||
validation {
|
||||
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
|
||||
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "coder_script" "nextflow" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "nextflow"
|
||||
icon = "/icon/nextflow.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
NEXTFLOW_VERSION : var.nextflow_version,
|
||||
PROJECT_PATH : var.project_path,
|
||||
HTTP_SERVER_PORT : var.http_server_port,
|
||||
HTTP_SERVER_REPORTS_DIR : var.http_server_reports_dir,
|
||||
HTTP_SERVER_LOG_PATH : var.http_server_log_path,
|
||||
STUB_RUN : var.stub_run,
|
||||
STUB_RUN_COMMAND : var.stub_run_command,
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "nextflow" {
|
||||
agent_id = var.agent_id
|
||||
slug = "nextflow-reports"
|
||||
display_name = "Nextflow Reports"
|
||||
url = "http://localhost:${var.http_server_port}"
|
||||
icon = "/icon/nextflow.svg"
|
||||
subdomain = true
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -eu
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
printf "$${BOLD}Starting Nextflow...$${RESET}\n"
|
||||
|
||||
if ! command -v nextflow > /dev/null 2>&1; then
|
||||
# Update system dependencies
|
||||
sudo apt update
|
||||
sudo apt install openjdk-21-jdk graphviz salmon fastqc multiqc -y
|
||||
|
||||
# Install nextflow
|
||||
export NXF_VER=${NEXTFLOW_VERSION}
|
||||
curl -s https://get.nextflow.io | bash
|
||||
sudo mv nextflow /usr/local/bin/
|
||||
sudo chmod +x /usr/local/bin/nextflow
|
||||
|
||||
# Verify installation
|
||||
tmp_verify=$(mktemp -d coder-nextflow-XXXXXX)
|
||||
nextflow run hello \
|
||||
-with-report "$${tmp_verify}/report.html" \
|
||||
-with-trace "$${tmp_verify}/trace.txt" \
|
||||
-with-timeline "$${tmp_verify}/timeline.html" \
|
||||
-with-dag "$${tmp_verify}/flowchart.png"
|
||||
rm -r "$${tmp_verify}"
|
||||
else
|
||||
echo "Nextflow is already installed\n\n"
|
||||
fi
|
||||
|
||||
if [ ! -z ${PROJECT_PATH} ]; then
|
||||
# Project is located at PROJECT_PATH
|
||||
echo "Change directory: ${PROJECT_PATH}"
|
||||
cd ${PROJECT_PATH}
|
||||
fi
|
||||
|
||||
# Start a web server to preview reports
|
||||
mkdir -p ${HTTP_SERVER_REPORTS_DIR}
|
||||
echo "Starting HTTP server in background, check logs: ${HTTP_SERVER_LOG_PATH}"
|
||||
python3 -m http.server --directory ${HTTP_SERVER_REPORTS_DIR} ${HTTP_SERVER_PORT} > "${HTTP_SERVER_LOG_PATH}" 2>&1 &
|
||||
|
||||
# Stub run?
|
||||
if [ "${STUB_RUN}" = "true" ]; then
|
||||
nextflow ${STUB_RUN_COMMAND} -stub-run
|
||||
fi
|
||||
|
||||
printf "\n$${BOLD}Nextflow ${NEXTFLOW_VERSION} is ready. HTTP server is listening on port ${HTTP_SERVER_PORT}$${RESET}\n"
|
||||
@@ -1,19 +1,19 @@
|
||||
---
|
||||
display_name: Sourcegraph AMP
|
||||
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
|
||||
verified: true
|
||||
tags: [agent, sourcegraph, amp, ai, tasks]
|
||||
---
|
||||
|
||||
# Sourcegraph AMP CLI
|
||||
# Sourcegraph Amp CLI
|
||||
|
||||
Run [Sourcegraph AMP CLI](https://sourcegraph.com/amp) in your workspace to access Sourcegraph's AI-powered code search and analysis tools, with AgentAPI integration for seamless Coder Tasks support.
|
||||
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 "sourcegraph-amp" {
|
||||
module "amp-cli" {
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
|
||||
install_sourcegraph_amp = true
|
||||
@@ -31,19 +31,19 @@ module "sourcegraph-amp" {
|
||||
```tf
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
name = "AI Prompt"
|
||||
description = "Write an initial prompt for AMP to work on."
|
||||
description = "Write an initial prompt for Amp to work on."
|
||||
type = "string"
|
||||
default = ""
|
||||
mutable = true
|
||||
|
||||
}
|
||||
|
||||
# Set system prompt for Sourcegraph Amp via environment variables
|
||||
# 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.
|
||||
You are an Amp assistant that helps developers debug and write code efficiently.
|
||||
|
||||
Always log task status to Coder.
|
||||
EOT
|
||||
@@ -53,14 +53,14 @@ resource "coder_agent" "main" {
|
||||
|
||||
variable "sourcegraph_amp_api_key" {
|
||||
type = string
|
||||
description = "Sourcegraph AMP API key"
|
||||
description = "Sourcegraph Amp API key. Get one at https://ampcode.com/settings"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "sourcegraph-amp" {
|
||||
module "amp-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key # recommended for authenticated usage
|
||||
install_sourcegraph_amp = true
|
||||
@@ -69,8 +69,8 @@ module "sourcegraph-amp" {
|
||||
|
||||
## 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
|
||||
- **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
|
||||
@@ -80,11 +80,11 @@ module "sourcegraph-amp" {
|
||||
- If AgentAPI fails to start, verify that your container has network access and executable permissions for the scripts
|
||||
|
||||
> [!IMPORTANT]
|
||||
> For using **Coder Tasks** with Sourcegraph AMP, make sure to pass the `AI Prompt` parameter and set `sourcegraph_amp_api_key`.
|
||||
> 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
|
||||
|
||||
- [Sourcegraph AMP Documentation](https://ampcode.com/manual)
|
||||
- [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)
|
||||
|
||||
@@ -69,7 +69,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.3.0"
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
@@ -151,7 +151,7 @@ locals {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
FROM ubuntu
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
curl \
|
||||
git \
|
||||
golang \
|
||||
sudo \
|
||||
vim \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& apt-get install -y \
|
||||
curl \
|
||||
git \
|
||||
golang \
|
||||
sudo \
|
||||
vim \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG USER=coder
|
||||
RUN useradd --groups sudo --no-create-home --shell /bin/bash ${USER} \
|
||||
&& echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \
|
||||
&& chmod 0440 /etc/sudoers.d/${USER}
|
||||
&& echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \
|
||||
&& chmod 0440 /etc/sudoers.d/${USER}
|
||||
USER ${USER}
|
||||
WORKDIR /home/${USER}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
display_name: Externally Managed Workspace
|
||||
description: A template to provision externally managed resources as Coder workspaces
|
||||
icon: ../../../../.icons/electric-plug-emoji.svg
|
||||
verified: true
|
||||
tags: [external]
|
||||
---
|
||||
|
||||
# Externally Managed Workspace Template
|
||||
|
||||
> [!IMPORTANT]
|
||||
> External agents require a [Premium](https://coder.com/pricing) Coder license.
|
||||
|
||||
This template provides a minimal scaffolding for creating Coder workspaces that connect to externally provisioned compute resources.
|
||||
|
||||
Use this template as a starting point to build your own custom templates for scenarios where you need to connect to existing infrastructure.
|
||||
@@ -0,0 +1,74 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "agent_config" {
|
||||
name = "agent_config"
|
||||
display_name = "Agent Configuration"
|
||||
description = "Select the operating system and architecture combination for the agent"
|
||||
type = "string"
|
||||
default = "linux-amd64"
|
||||
|
||||
option {
|
||||
name = "Linux AMD64"
|
||||
value = "linux-amd64"
|
||||
}
|
||||
option {
|
||||
name = "Linux ARM64"
|
||||
value = "linux-arm64"
|
||||
}
|
||||
option {
|
||||
name = "Linux ARMv7"
|
||||
value = "linux-armv7"
|
||||
}
|
||||
option {
|
||||
name = "Windows AMD64"
|
||||
value = "windows-amd64"
|
||||
}
|
||||
option {
|
||||
name = "Windows ARM64"
|
||||
value = "windows-arm64"
|
||||
}
|
||||
option {
|
||||
name = "macOS AMD64"
|
||||
value = "darwin-amd64"
|
||||
}
|
||||
option {
|
||||
name = "macOS ARM64 (Apple Silicon)"
|
||||
value = "darwin-arm64"
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
locals {
|
||||
agent_config = split("-", data.coder_parameter.agent_config.value)
|
||||
agent_os = local.agent_config[0]
|
||||
agent_arch = local.agent_config[1]
|
||||
}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
arch = local.agent_arch
|
||||
os = local.agent_os
|
||||
}
|
||||
|
||||
resource "coder_external_agent" "main" {
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
# Adds code-server
|
||||
# See all available modules at https://registry.coder.com/modules
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
version = "~> 1.0"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
@@ -22,31 +22,16 @@ provider "docker" {}
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.0.0"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/projects"
|
||||
install_claude_code = true
|
||||
claude_code_version = "latest"
|
||||
workdir = "/home/coder/projects"
|
||||
order = 999
|
||||
|
||||
experiment_post_install_script = data.coder_parameter.setup_script.value
|
||||
|
||||
# This enables Coder Tasks
|
||||
experiment_report_tasks = true
|
||||
}
|
||||
|
||||
# You can also use a model provider, like AWS Bedrock or Vertex by replacing
|
||||
# this with the special env vars from the Claude Code docs.
|
||||
# see: https://docs.anthropic.com/en/docs/claude-code/third-party-integrations
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Generate one at: https://console.anthropic.com/settings/keys"
|
||||
sensitive = true
|
||||
}
|
||||
resource "coder_env" "anthropic_api_key" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_CLAUDE_API_KEY"
|
||||
value = var.anthropic_api_key
|
||||
claude_api_key = ""
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
system_prompt = data.coder_parameter.system_prompt.value
|
||||
model = "sonnet"
|
||||
permission_mode = "plan"
|
||||
post_install_script = data.coder_parameter.setup_script.value
|
||||
}
|
||||
|
||||
# We are using presets to set the prompts, image, and set up instructions
|
||||
@@ -172,23 +157,6 @@ data "coder_parameter" "preview_port" {
|
||||
mutable = false
|
||||
}
|
||||
|
||||
# Other variables for Claude Code
|
||||
resource "coder_env" "claude_task_prompt" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_CLAUDE_TASK_PROMPT"
|
||||
value = data.coder_parameter.ai_prompt.value
|
||||
}
|
||||
resource "coder_env" "app_status_slug" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_APP_STATUS_SLUG"
|
||||
value = "ccw"
|
||||
}
|
||||
resource "coder_env" "claude_system_prompt" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
|
||||
value = data.coder_parameter.system_prompt.value
|
||||
}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
@@ -300,13 +268,6 @@ module "code-server" {
|
||||
order = 1
|
||||
}
|
||||
|
||||
module "vscode" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-desktop/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
@@ -321,23 +282,13 @@ module "cursor" {
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
|
||||
# JetBrains IDEs to make available for the user to select
|
||||
jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
|
||||
default = "IU"
|
||||
|
||||
# Default folder to open when starting a JetBrains IDE
|
||||
folder = "/home/coder/projects"
|
||||
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
version = "~> 1.0"
|
||||
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "~> 1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
order = 2
|
||||
folder = "/home/coder/projects"
|
||||
}
|
||||
|
||||
resource "docker_volume" "home_volume" {
|
||||
@@ -422,4 +373,4 @@ resource "docker_container" "workspace" {
|
||||
label = "coder.workspace_name"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 976 KiB |
|
After Width: | Height: | Size: 302 KiB |
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
|
||||
```tf
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -164,7 +164,9 @@ describe("agentapi", async () => {
|
||||
id,
|
||||
"/home/coder/test-agentapi-start.log",
|
||||
);
|
||||
expect(agentApiStartLog).toContain("Using AGENTAPI_CHAT_BASE_PATH: /@default/default.foo/apps/agentapi-web/chat");
|
||||
expect(agentApiStartLog).toContain(
|
||||
"Using AGENTAPI_CHAT_BASE_PATH: /@default/default.foo/apps/agentapi-web/chat",
|
||||
);
|
||||
});
|
||||
|
||||
test("validate-agentapi-version", async () => {
|
||||
@@ -186,14 +188,16 @@ describe("agentapi", async () => {
|
||||
agentapi_version: "v0.0.1",
|
||||
agentapi_subdomain: "false",
|
||||
},
|
||||
shouldThrow: "Running with subdomain = false is only supported by agentapi >= v0.3.3.",
|
||||
shouldThrow:
|
||||
"Running with subdomain = false is only supported by agentapi >= v0.3.3.",
|
||||
},
|
||||
{
|
||||
moduleVariables: {
|
||||
agentapi_version: "v0.3.2",
|
||||
agentapi_subdomain: "false",
|
||||
},
|
||||
shouldThrow: "Running with subdomain = false is only supported by agentapi >= v0.3.3.",
|
||||
shouldThrow:
|
||||
"Running with subdomain = false is only supported by agentapi >= v0.3.3.",
|
||||
},
|
||||
{
|
||||
moduleVariables: {
|
||||
@@ -226,13 +230,17 @@ describe("agentapi", async () => {
|
||||
agentapi_version: "arbitrary-string-bypasses-validation",
|
||||
},
|
||||
shouldThrow: "",
|
||||
}
|
||||
},
|
||||
];
|
||||
for (const { moduleVariables, shouldThrow } of cases) {
|
||||
if (shouldThrow) {
|
||||
expect(setup({ moduleVariables: moduleVariables as Record<string, string> })).rejects.toThrow(shouldThrow);
|
||||
expect(
|
||||
setup({ moduleVariables: moduleVariables as Record<string, string> }),
|
||||
).rejects.toThrow(shouldThrow);
|
||||
} else {
|
||||
expect(setup({ moduleVariables: moduleVariables as Record<string, string> })).resolves.toBeDefined();
|
||||
expect(
|
||||
setup({ moduleVariables: moduleVariables as Record<string, string> }),
|
||||
).resolves.toBeDefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -117,7 +117,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.3.3"
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "agentapi_port" {
|
||||
|
||||
@@ -11,22 +11,22 @@ agentapi_started=false
|
||||
|
||||
echo "Waiting for agentapi server to start on port $port..."
|
||||
for i in $(seq 1 150); do
|
||||
for j in $(seq 1 3); do
|
||||
sleep 0.1
|
||||
if curl -fs -o /dev/null "http://localhost:$port/status"; then
|
||||
echo "agentapi response received ($j/3)"
|
||||
else
|
||||
echo "agentapi server not responding ($i/15)"
|
||||
continue 2
|
||||
fi
|
||||
done
|
||||
agentapi_started=true
|
||||
break
|
||||
for j in $(seq 1 3); do
|
||||
sleep 0.1
|
||||
if curl -fs -o /dev/null "http://localhost:$port/status"; then
|
||||
echo "agentapi response received ($j/3)"
|
||||
else
|
||||
echo "agentapi server not responding ($i/15)"
|
||||
continue 2
|
||||
fi
|
||||
done
|
||||
agentapi_started=true
|
||||
break
|
||||
done
|
||||
|
||||
if [ "$agentapi_started" != "true" ]; then
|
||||
echo "Error: agentapi server did not start on port $port after 15 seconds."
|
||||
exit 1
|
||||
echo "Error: agentapi server did not start on port $port after 15 seconds."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "agentapi server started on port $port."
|
||||
|
||||
@@ -17,76 +17,76 @@ AGENTAPI_CHAT_BASE_PATH="${ARG_AGENTAPI_CHAT_BASE_PATH:-}"
|
||||
set +o nounset
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
module_path="$HOME/${MODULE_DIR_NAME}"
|
||||
mkdir -p "$module_path/scripts"
|
||||
|
||||
if [ ! -d "${WORKDIR}" ]; then
|
||||
echo "Warning: The specified folder '${WORKDIR}' does not exist."
|
||||
echo "Creating the folder..."
|
||||
mkdir -p "${WORKDIR}"
|
||||
echo "Folder created successfully."
|
||||
echo "Warning: The specified folder '${WORKDIR}' does not exist."
|
||||
echo "Creating the folder..."
|
||||
mkdir -p "${WORKDIR}"
|
||||
echo "Folder created successfully."
|
||||
fi
|
||||
if [ -n "${PRE_INSTALL_SCRIPT}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo -n "${PRE_INSTALL_SCRIPT}" >"$module_path/pre_install.sh"
|
||||
chmod +x "$module_path/pre_install.sh"
|
||||
"$module_path/pre_install.sh" 2>&1 | tee "$module_path/pre_install.log"
|
||||
echo "Running pre-install script..."
|
||||
echo -n "${PRE_INSTALL_SCRIPT}" > "$module_path/pre_install.sh"
|
||||
chmod +x "$module_path/pre_install.sh"
|
||||
"$module_path/pre_install.sh" 2>&1 | tee "$module_path/pre_install.log"
|
||||
fi
|
||||
|
||||
echo "Running install script..."
|
||||
echo -n "${INSTALL_SCRIPT}" >"$module_path/install.sh"
|
||||
echo -n "${INSTALL_SCRIPT}" > "$module_path/install.sh"
|
||||
chmod +x "$module_path/install.sh"
|
||||
"$module_path/install.sh" 2>&1 | tee "$module_path/install.log"
|
||||
|
||||
# Install AgentAPI if enabled
|
||||
if [ "${INSTALL_AGENTAPI}" = "true" ]; then
|
||||
echo "Installing AgentAPI..."
|
||||
arch=$(uname -m)
|
||||
if [ "$arch" = "x86_64" ]; then
|
||||
binary_name="agentapi-linux-amd64"
|
||||
elif [ "$arch" = "aarch64" ]; then
|
||||
binary_name="agentapi-linux-arm64"
|
||||
else
|
||||
echo "Error: Unsupported architecture: $arch"
|
||||
exit 1
|
||||
fi
|
||||
if [ "${AGENTAPI_VERSION}" = "latest" ]; then
|
||||
# for the latest release the download URL pattern is different than for tagged releases
|
||||
# https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases
|
||||
download_url="https://github.com/coder/agentapi/releases/latest/download/$binary_name"
|
||||
else
|
||||
download_url="https://github.com/coder/agentapi/releases/download/${AGENTAPI_VERSION}/$binary_name"
|
||||
fi
|
||||
curl \
|
||||
--retry 5 \
|
||||
--retry-delay 5 \
|
||||
--fail \
|
||||
--retry-all-errors \
|
||||
-L \
|
||||
-C - \
|
||||
-o agentapi \
|
||||
"$download_url"
|
||||
chmod +x agentapi
|
||||
sudo mv agentapi /usr/local/bin/agentapi
|
||||
echo "Installing AgentAPI..."
|
||||
arch=$(uname -m)
|
||||
if [ "$arch" = "x86_64" ]; then
|
||||
binary_name="agentapi-linux-amd64"
|
||||
elif [ "$arch" = "aarch64" ]; then
|
||||
binary_name="agentapi-linux-arm64"
|
||||
else
|
||||
echo "Error: Unsupported architecture: $arch"
|
||||
exit 1
|
||||
fi
|
||||
if [ "${AGENTAPI_VERSION}" = "latest" ]; then
|
||||
# for the latest release the download URL pattern is different than for tagged releases
|
||||
# https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases
|
||||
download_url="https://github.com/coder/agentapi/releases/latest/download/$binary_name"
|
||||
else
|
||||
download_url="https://github.com/coder/agentapi/releases/download/${AGENTAPI_VERSION}/$binary_name"
|
||||
fi
|
||||
curl \
|
||||
--retry 5 \
|
||||
--retry-delay 5 \
|
||||
--fail \
|
||||
--retry-all-errors \
|
||||
-L \
|
||||
-C - \
|
||||
-o agentapi \
|
||||
"$download_url"
|
||||
chmod +x agentapi
|
||||
sudo mv agentapi /usr/local/bin/agentapi
|
||||
fi
|
||||
if ! command_exists agentapi; then
|
||||
echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually."
|
||||
exit 1
|
||||
echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "${START_SCRIPT}" >"$module_path/scripts/agentapi-start.sh"
|
||||
echo -n "${WAIT_FOR_START_SCRIPT}" >"$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
echo -n "${START_SCRIPT}" > "$module_path/scripts/agentapi-start.sh"
|
||||
echo -n "${WAIT_FOR_START_SCRIPT}" > "$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
chmod +x "$module_path/scripts/agentapi-start.sh"
|
||||
chmod +x "$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
|
||||
if [ -n "${POST_INSTALL_SCRIPT}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo -n "${POST_INSTALL_SCRIPT}" >"$module_path/post_install.sh"
|
||||
chmod +x "$module_path/post_install.sh"
|
||||
"$module_path/post_install.sh" 2>&1 | tee "$module_path/post_install.log"
|
||||
echo "Running post-install script..."
|
||||
echo -n "${POST_INSTALL_SCRIPT}" > "$module_path/post_install.sh"
|
||||
chmod +x "$module_path/post_install.sh"
|
||||
"$module_path/post_install.sh" 2>&1 | tee "$module_path/post_install.log"
|
||||
fi
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
@@ -97,5 +97,5 @@ cd "${WORKDIR}"
|
||||
export AGENTAPI_CHAT_BASE_PATH="${AGENTAPI_CHAT_BASE_PATH:-}"
|
||||
# Disable host header check since AgentAPI is proxied by Coder (which does its own validation)
|
||||
export AGENTAPI_ALLOWED_HOSTS="*"
|
||||
nohup "$module_path/scripts/agentapi-start.sh" true "${AGENTAPI_PORT}" &>"$module_path/agentapi-start.log" &
|
||||
nohup "$module_path/scripts/agentapi-start.sh" true "${AGENTAPI_PORT}" &> "$module_path/agentapi-start.log" &
|
||||
"$module_path/scripts/agentapi-wait-for-start.sh" "${AGENTAPI_PORT}"
|
||||
|
||||
@@ -25,14 +25,20 @@ export const setupContainer = async ({
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
|
||||
return {
|
||||
id, coderScript, cleanup: async () => {
|
||||
if (process.env["DEBUG"] === "true" || process.env["DEBUG"] === "1" || process.env["DEBUG"] === "yes") {
|
||||
id,
|
||||
coderScript,
|
||||
cleanup: async () => {
|
||||
if (
|
||||
process.env["DEBUG"] === "true" ||
|
||||
process.env["DEBUG"] === "1" ||
|
||||
process.env["DEBUG"] === "yes"
|
||||
) {
|
||||
console.log(`Not removing container ${id} in debug mode`);
|
||||
console.log(`Run "docker rm -f ${id}" to remove it manually.`);
|
||||
} else {
|
||||
await removeContainer(id);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@ const portIdx = args.findIndex((arg) => arg === "--port") + 1;
|
||||
const port = portIdx ? args[portIdx] : 3284;
|
||||
|
||||
console.log(`starting server on port ${port}`);
|
||||
fs.writeFileSync("/home/coder/agentapi-mock.log", `AGENTAPI_ALLOWED_HOSTS: ${process.env.AGENTAPI_ALLOWED_HOSTS}`);
|
||||
fs.writeFileSync(
|
||||
"/home/coder/agentapi-mock.log",
|
||||
`AGENTAPI_ALLOWED_HOSTS: ${process.env.AGENTAPI_ALLOWED_HOSTS}`,
|
||||
);
|
||||
|
||||
http
|
||||
.createServer(function (_request, response) {
|
||||
|
||||
@@ -8,15 +8,15 @@ port=${2:-3284}
|
||||
module_path="$HOME/.agentapi-module"
|
||||
log_file_path="$module_path/agentapi.log"
|
||||
|
||||
echo "using prompt: $use_prompt" >>/home/coder/test-agentapi-start.log
|
||||
echo "using port: $port" >>/home/coder/test-agentapi-start.log
|
||||
echo "using prompt: $use_prompt" >> /home/coder/test-agentapi-start.log
|
||||
echo "using port: $port" >> /home/coder/test-agentapi-start.log
|
||||
|
||||
AGENTAPI_CHAT_BASE_PATH="${AGENTAPI_CHAT_BASE_PATH:-}"
|
||||
if [ -n "$AGENTAPI_CHAT_BASE_PATH" ]; then
|
||||
echo "Using AGENTAPI_CHAT_BASE_PATH: $AGENTAPI_CHAT_BASE_PATH" >>/home/coder/test-agentapi-start.log
|
||||
export AGENTAPI_CHAT_BASE_PATH
|
||||
echo "Using AGENTAPI_CHAT_BASE_PATH: $AGENTAPI_CHAT_BASE_PATH" >> /home/coder/test-agentapi-start.log
|
||||
export AGENTAPI_CHAT_BASE_PATH
|
||||
fi
|
||||
|
||||
agentapi server --port "$port" --term-width 67 --term-height 1190 -- \
|
||||
bash -c aiagent \
|
||||
>"$log_file_path" 2>&1
|
||||
bash -c aiagent \
|
||||
> "$log_file_path" 2>&1
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
---
|
||||
display_name: Amazon Q
|
||||
description: Run Amazon Q in your workspace to access Amazon's AI coding assistant.
|
||||
description: Run Amazon Q in your workspace to access Amazon's AI coding assistant with MCP integration and task reporting.
|
||||
icon: ../../../../.icons/amazon-q.svg
|
||||
verified: true
|
||||
tags: [agent, ai, aws, amazon-q]
|
||||
tags: [agent, ai, aws, amazon-q, tasks]
|
||||
---
|
||||
|
||||
# Amazon Q
|
||||
|
||||
Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's AI coding assistant. This module installs and launches Amazon Q, with support for background operation, task reporting, and custom pre/post install scripts.
|
||||
Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's AI coding assistant. This module provides a complete integration with Coder workspaces, including automatic installation, MCP (Model Context Protocol) integration for task reporting, and support for custom pre/post install scripts.
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.1.2"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
|
||||
# Required: see below for how to generate
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
# Required: Authentication tarball (see below for generation)
|
||||
auth_tarball = <<-EOF
|
||||
base64encoded-tarball
|
||||
EOF
|
||||
}
|
||||
```
|
||||
|
||||
@@ -25,97 +28,370 @@ module "amazon-q" {
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You must generate an authenticated Amazon Q tarball on another machine:
|
||||
```sh
|
||||
cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0
|
||||
```
|
||||
Paste the result into the `experiment_auth_tarball` variable.
|
||||
- To run in the background, your workspace must have `screen` or `tmux` installed.
|
||||
- **zstd** - Required for compressing the authentication tarball
|
||||
- **Ubuntu/Debian**: `sudo apt-get install zstd`
|
||||
- **RHEL/CentOS/Fedora**: `sudo yum install zstd` or `sudo dnf install zstd`
|
||||
- **auth_tarball** - Required for installation and authentication
|
||||
|
||||
<details>
|
||||
<summary><strong>How to generate the Amazon Q auth tarball (step-by-step)</strong></summary>
|
||||
### Authentication Tarball
|
||||
|
||||
**1. Install and authenticate Amazon Q on your local machine:**
|
||||
You must generate an authenticated Amazon Q tarball on another machine where you have successfully logged in:
|
||||
|
||||
- Download and install Amazon Q from the [official site](https://aws.amazon.com/q/developer/).
|
||||
- Run `q login` and complete the authentication process in your terminal.
|
||||
```bash
|
||||
# 1. Install Amazon Q and login on your local machine
|
||||
q login
|
||||
|
||||
**2. Locate your Amazon Q config directory:**
|
||||
# 2. Generate the authentication tarball
|
||||
cd ~/.local/share/amazon-q
|
||||
tar -c . | zstd | base64 -w 0
|
||||
```
|
||||
|
||||
- The config is typically stored at `~/.local/share/amazon-q`.
|
||||
Copy the output and use it as the `auth_tarball` variable.
|
||||
|
||||
**3. Generate the tarball:**
|
||||
## Detailed Authentication Setup
|
||||
|
||||
- Run the following command in your terminal:
|
||||
```sh
|
||||
cd ~/.local/share/amazon-q
|
||||
tar -c . | zstd | base64 -w 0
|
||||
```
|
||||
**Step 1: Install Amazon Q locally**
|
||||
|
||||
**4. Copy the output:**
|
||||
- Download from [AWS Amazon Q Developer](https://aws.amazon.com/q/developer/)
|
||||
- Follow the installation instructions for your platform
|
||||
|
||||
- The command will output a long string. Copy this entire string.
|
||||
**Step 2: Authenticate**
|
||||
|
||||
**5. Paste into your Terraform variable:**
|
||||
```bash
|
||||
q login
|
||||
```
|
||||
|
||||
- Assign the string to the `experiment_auth_tarball` variable in your Terraform configuration, for example:
|
||||
```tf
|
||||
variable "amazon_q_auth_tarball" {
|
||||
type = string
|
||||
default = "PASTE_LONG_STRING_HERE"
|
||||
}
|
||||
```
|
||||
Complete the authentication process in your browser.
|
||||
|
||||
**Note:**
|
||||
**Step 3: Generate tarball**
|
||||
|
||||
- You must re-generate the tarball if you log out or re-authenticate Amazon Q on your local machine.
|
||||
- This process is required for each user who wants to use Amazon Q in their workspace.
|
||||
```bash
|
||||
cd ~/.local/share/amazon-q
|
||||
tar -c . | zstd | base64 -w 0 > /tmp/amazon-q-auth.txt
|
||||
```
|
||||
|
||||
[Reference: Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/generate-docs.html)
|
||||
|
||||
</details>
|
||||
|
||||
## Examples
|
||||
|
||||
### Run Amazon Q in the background with tmux
|
||||
**Step 4: Use in Terraform**
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
experiment_use_tmux = true
|
||||
variable "amazon_q_auth_tarball" {
|
||||
type = string
|
||||
sensitive = true
|
||||
default = "PASTE_YOUR_TARBALL_HERE"
|
||||
}
|
||||
```
|
||||
|
||||
### Enable task reporting (experimental)
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> - Regenerate the tarball if you logout or re-authenticate
|
||||
> - Each user needs their own authentication tarball
|
||||
> - Keep the tarball secure as it contains authentication credentials
|
||||
|
||||
### Coder Tasks Integration
|
||||
|
||||
A `coder_parameter` named **'AI Prompt'** is required to enable integration with [Coder Tasks](https://coder.com/docs/ai-coder/tasks).
|
||||
|
||||
```tf
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
name = "AI Prompt"
|
||||
display_name = "AI Prompt"
|
||||
description = "Prompt for the AI task to execute"
|
||||
type = "string"
|
||||
mutable = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
experiment_report_tasks = true
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
trust_all_tools = true
|
||||
|
||||
# Task reporting configuration
|
||||
report_tasks = true
|
||||
|
||||
# Enable CLI app alongside web app
|
||||
cli_app = true
|
||||
web_app_display_name = "Amazon Q"
|
||||
cli_app_display_name = "Q CLI"
|
||||
}
|
||||
```
|
||||
|
||||
### Run custom scripts before/after install
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> - The parameter name must be exactly **'AI Prompt'** (case-sensitive)
|
||||
> - This parameter enables the AI task workflow integration
|
||||
> - The parameter value is passed to the Amazon Q module via the `ai_prompt` variable
|
||||
> - Without this parameter, `coder_ai_task` resources will not function properly
|
||||
>
|
||||
> **_Security Notice_**
|
||||
> In order to allow the tasks flow non-interactively all the tools are trusted
|
||||
> This flag bypasses standard permission checks and allows Amazon Q broader access to your system than normally permitted.
|
||||
> While this enables more functionality, it also means Amazon Q can potentially execute commands with the same privileges as the user running it.
|
||||
> Use this module only in trusted environments and be aware of the security implications.
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
experiment_pre_install_script = "echo Pre-install!"
|
||||
experiment_post_install_script = "echo Post-install!"
|
||||
### Default System Prompt
|
||||
|
||||
The module includes a simple system prompt that instructs Amazon Q:
|
||||
|
||||
```
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
```
|
||||
|
||||
You can customize this behavior by providing your own system prompt via the `system_prompt` variable.
|
||||
|
||||
### Default Coder MCP Instructions
|
||||
|
||||
The module includes specific instructions for the Coder MCP server integration that are separate from the system prompt:
|
||||
|
||||
```
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message
|
||||
- Be granular If you are investigating with multiple steps report each step to coder.
|
||||
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input
|
||||
- Use "state": "complete" only when finished with a task
|
||||
- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers.
|
||||
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing
|
||||
- Include clear and actionable steps for the user
|
||||
- Be less than 160 characters in length
|
||||
```
|
||||
|
||||
You can customize these instructions by providing your own via the `coder_mcp_instructions` variable.
|
||||
|
||||
## Default Agent Configuration
|
||||
|
||||
The module includes a default agent configuration template that provides a comprehensive setup for Amazon Q integration:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "agent",
|
||||
"description": "This is an default agent config",
|
||||
"prompt": "${system_prompt}",
|
||||
"mcpServers": {},
|
||||
"tools": [
|
||||
"fs_read",
|
||||
"fs_write",
|
||||
"execute_bash",
|
||||
"use_aws",
|
||||
"@coder",
|
||||
"knowledge"
|
||||
],
|
||||
"toolAliases": {},
|
||||
"allowedTools": ["fs_read", "@coder"],
|
||||
"resources": [
|
||||
"file://AmazonQ.md",
|
||||
"file://README.md",
|
||||
"file://.amazonq/rules/**/*.md"
|
||||
],
|
||||
"hooks": {},
|
||||
"toolsSettings": {},
|
||||
"useLegacyMcpJson": true
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
### Configuration Details:
|
||||
|
||||
- Only one of `experiment_use_screen` or `experiment_use_tmux` can be true at a time.
|
||||
- If neither is set, Amazon Q runs in the foreground.
|
||||
- For more details, see the [main.tf](./main.tf) source.
|
||||
- **Tools Available:** File operations, bash execution, AWS CLI, Coder MCP integration, and knowledge base access
|
||||
- **@coder Tool:** Enables Coder MCP integration for task reporting (`coder_report_task` and related tools)
|
||||
- **Allowed Tools:** By default, only `fs_read` and `@coder` are allowed (can be customized for security)
|
||||
- **Resources:** Access to documentation and rule files in the workspace
|
||||
- **MCP Servers:** Empty by default, can be configured via `agent_config` variable
|
||||
- **System Prompt:** Dynamically populated from the `system_prompt` variable
|
||||
- **Legacy MCP:** Uses legacy MCP JSON format for compatibility
|
||||
|
||||
You can override this configuration by providing your own JSON via the `agent_config` variable.
|
||||
|
||||
### Agent Name Configuration
|
||||
|
||||
The module automatically extracts the agent name from the `"name"` field in the `agent_config` JSON and uses it for:
|
||||
|
||||
- **Configuration File:** Saves the agent config as `~/.aws/amazonq/cli-agents/{agent_name}.json`
|
||||
- **Default Agent:** Sets the agent as the default using `q settings chat.defaultAgent {agent_name}`
|
||||
- **MCP Integration:** Associates the Coder MCP server with the specified agent name
|
||||
|
||||
If no custom `agent_config` is provided, the default agent name "agent" is used.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
}
|
||||
```
|
||||
|
||||
This example will:
|
||||
|
||||
1. Download and install Amazon Q CLI v1.14.1
|
||||
2. Extract authentication tarball to ~/.local/share/amazon-q
|
||||
3. Configure Coder MCP integration for task reporting
|
||||
4. Create default agent configuration file
|
||||
5. Start Amazon Q in /home/coder directory
|
||||
6. Provide web interface through AgentAPI
|
||||
|
||||
> [!IMPORTANT]
|
||||
> By default `fs_write` tool is not allowed, which will pause the task execution
|
||||
> an will wait for the prompt to approve it usage.
|
||||
> To avoid this, and allow the normal task flow, user has two options:
|
||||
>
|
||||
> - Change the parameter `trust_all_tools` value to `true` (default to `false`)
|
||||
> OR
|
||||
> - Provide you own agent configuration with the tools of your choice allowed
|
||||
|
||||
### With Custom AI Prompt
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
|
||||
trust_all_tools = true
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **_Security Notice_**
|
||||
> In order to allow the tasks flow non-interactively all the tools are trusted
|
||||
> This flag bypasses standard permission checks and allows Amazon Q broader access to your system than normally permitted.
|
||||
> While this enables more functionality, it also means Amazon Q can potentially execute commands with the same privileges as the user running it.
|
||||
> Use this module only in trusted environments and be aware of the security implications.
|
||||
|
||||
### With Custom Pre/Post Install Scripts
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
|
||||
pre_install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
echo "Setting up custom environment..."
|
||||
# Install additional dependencies
|
||||
sudo apt-get update && sudo apt-get install -y zstd
|
||||
EOT
|
||||
|
||||
post_install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
echo "Configuring Amazon Q settings..."
|
||||
# Custom configuration commands
|
||||
q settings chat.model claude-3-sonnet
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
### Specific Version Installation
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
amazon_q_version = "1.14.0" # Specific version
|
||||
install_amazon_q = true
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Agent Configuration
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
|
||||
agent_config = <<-EOT
|
||||
{
|
||||
"name": "custom-agent",
|
||||
"description": "Custom Amazon Q agent for my workspace",
|
||||
"prompt": "You are a specialized DevOps assistant...",
|
||||
"tools": ["fs_read", "fs_write", "execute_bash", "use_aws"]
|
||||
}
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
### With Custom AgentAPI Configuration
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
|
||||
# AgentAPI configuration for environments without wildcard access url. https://coder.com/docs/admin/setup#wildcard-access-url
|
||||
agentapi_chat_based_path = true
|
||||
agentapi_version = "v0.10.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Air-Gapped Installation
|
||||
|
||||
For environments without direct internet access, you can host Amazon Q installation files internally and configure the module to use your internal repository:
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
|
||||
# Point to internal artifact repository
|
||||
q_install_url = "https://artifacts.internal.corp/amazon-q-releases"
|
||||
|
||||
# Use specific version available in your repository
|
||||
amazon_q_version = "1.14.1"
|
||||
}
|
||||
```
|
||||
|
||||
**Prerequisites for Air-Gapped Setup:**
|
||||
|
||||
1. Download Amazon Q installation files from AWS and host them internally
|
||||
2. Maintain the same directory structure: `{base_url}/{version}/q-{arch}-linux.zip`
|
||||
3. Ensure both architectures are available:
|
||||
- `q-x86_64-linux.zip` for Intel/AMD systems
|
||||
- `q-aarch64-linux.zip` for ARM systems
|
||||
4. Configure network access from Coder workspaces to your internal repository
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Authentication issues:**
|
||||
|
||||
- Regenerate the auth tarball on your local machine
|
||||
- Ensure the tarball is properly base64 encoded
|
||||
- Check that the original authentication is still valid
|
||||
|
||||
**MCP integration not working:**
|
||||
|
||||
- Verify that AgentAPI is installed (`install_agentapi = true`)
|
||||
- Check that the Coder agent is properly configured
|
||||
- Review the system prompt configuration
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
run "required_variables" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
}
|
||||
}
|
||||
|
||||
run "minimal_config" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdA==" # base64 "test"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable not configured correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.value == "amazonq"
|
||||
error_message = "Status slug value should be 'amazonq'"
|
||||
}
|
||||
}
|
||||
|
||||
# Test Case 1: Basic Usage – No Autonomous Use of Q
|
||||
# Using vanilla Kubernetes Deployment Template configuration
|
||||
run "test_case_1_basic_usage" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
}
|
||||
|
||||
# Q is installed and authenticated
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable should be configured for basic usage"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.value == "amazonq"
|
||||
error_message = "Status slug value should be 'amazonq' for basic usage"
|
||||
}
|
||||
|
||||
# AgentAPI is installed and configured (default behavior)
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 1
|
||||
error_message = "Auth tarball environment variable should be created for authentication"
|
||||
}
|
||||
|
||||
# Foundational configuration applied
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated with foundational configuration"
|
||||
}
|
||||
|
||||
# No additional parameters required (using defaults)
|
||||
assert {
|
||||
condition = local.agent_name == "agent"
|
||||
error_message = "Default agent name should be 'agent' when no custom config provided"
|
||||
}
|
||||
}
|
||||
|
||||
# Test Case 2: Autonomous Usage – Autonomous Use of Q
|
||||
# AI prompt passed through from external source (Tasks interface or Issue Tracker CI)
|
||||
run "test_case_2_autonomous_usage" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
|
||||
}
|
||||
|
||||
# Q is installed and authenticated
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable should be configured for autonomous usage"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.value == "amazonq"
|
||||
error_message = "Status slug value should be 'amazonq' for autonomous usage"
|
||||
}
|
||||
|
||||
# AgentAPI is installed and configured
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 1
|
||||
error_message = "Auth tarball environment variable should be created for autonomous usage"
|
||||
}
|
||||
|
||||
# Foundational configuration for all components applied
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated for autonomous usage"
|
||||
}
|
||||
|
||||
# AI prompt is configured
|
||||
assert {
|
||||
condition = local.full_prompt == "Help me set up a Python FastAPI project with proper testing structure"
|
||||
error_message = "AI prompt should be configured correctly for autonomous usage"
|
||||
}
|
||||
|
||||
# Default agent name when no custom config
|
||||
assert {
|
||||
condition = local.agent_name == "agent"
|
||||
error_message = "Default agent name should be 'agent' for autonomous usage"
|
||||
}
|
||||
}
|
||||
|
||||
# Test Case 3: Extended Configuration – Parameter Validation and File Rendering
|
||||
# Validates extended configuration options and parameter application
|
||||
run "test_case_3_extended_configuration" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
amazon_q_version = "1.14.1"
|
||||
q_install_url = "https://desktop-release.q.us-east-1.amazonaws.com"
|
||||
install_amazon_q = true
|
||||
install_agentapi = true
|
||||
agentapi_version = "v0.6.0"
|
||||
trust_all_tools = true
|
||||
ai_prompt = "Help me create a production-grade TypeScript monorepo with testing and deployment"
|
||||
system_prompt = "You are a helpful software assistant working in a secure enterprise environment"
|
||||
pre_install_script = "echo 'Pre-install setup'"
|
||||
post_install_script = "echo 'Post-install cleanup'"
|
||||
agent_config = jsonencode({
|
||||
name = "production-agent"
|
||||
description = "Production Amazon Q agent for enterprise environment"
|
||||
prompt = "You are a helpful software assistant working in a secure enterprise environment"
|
||||
mcpServers = {}
|
||||
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
|
||||
toolAliases = {}
|
||||
allowedTools = ["fs_read"]
|
||||
resources = ["file://AmazonQ.md", "file://README.md", "file://.amazonq/rules/**/*.md"]
|
||||
hooks = {}
|
||||
toolsSettings = {}
|
||||
useLegacyMcpJson = true
|
||||
})
|
||||
}
|
||||
|
||||
# All installation parameters are applied correctly
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.value == "amazonq"
|
||||
error_message = "Status slug should be configured correctly with extended parameters"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
|
||||
error_message = "Auth tarball should be configured correctly with extended parameters"
|
||||
}
|
||||
|
||||
# Custom agent configuration is loaded and referenced correctly
|
||||
assert {
|
||||
condition = local.agent_name == "production-agent"
|
||||
error_message = "Agent name should be extracted from custom agent config"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Custom agent config should be processed correctly"
|
||||
}
|
||||
|
||||
# AI prompt and system prompt are configured
|
||||
assert {
|
||||
condition = local.full_prompt == "Help me create a production-grade TypeScript monorepo with testing and deployment"
|
||||
error_message = "AI prompt should be configured correctly in extended configuration"
|
||||
}
|
||||
|
||||
# Pre-install and post-install scripts are provided
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated correctly for extended configuration"
|
||||
}
|
||||
}
|
||||
|
||||
run "full_config" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
install_amazon_q = true
|
||||
install_agentapi = true
|
||||
agentapi_version = "v0.5.0"
|
||||
amazon_q_version = "latest"
|
||||
trust_all_tools = true
|
||||
ai_prompt = "Build a web application"
|
||||
auth_tarball = "dGVzdA=="
|
||||
order = 1
|
||||
group = "AI Tools"
|
||||
icon = "/icon/custom-amazon-q.svg"
|
||||
pre_install_script = "echo 'pre-install'"
|
||||
post_install_script = "echo 'post-install'"
|
||||
agent_config = jsonencode({
|
||||
name = "test-agent"
|
||||
description = "Test agent configuration"
|
||||
prompt = "You are a helpful AI assistant for testing."
|
||||
mcpServers = {}
|
||||
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
|
||||
toolAliases = {}
|
||||
allowedTools = ["fs_read"]
|
||||
resources = ["file://AmazonQ.md", "file://README.md", "file://.amazonq/rules/**/*.md"]
|
||||
hooks = {}
|
||||
toolsSettings = {}
|
||||
useLegacyMcpJson = true
|
||||
})
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable not configured correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.value == "amazonq"
|
||||
error_message = "Status slug value should be 'amazonq'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 1
|
||||
error_message = "Auth tarball environment variable should be created when provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "auth_tarball_environment" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.auth_tarball[0].name == "AMAZON_Q_AUTH_TARBALL"
|
||||
error_message = "Auth tarball environment variable name should be 'AMAZON_Q_AUTH_TARBALL'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
|
||||
error_message = "Auth tarball environment variable value should match input"
|
||||
}
|
||||
}
|
||||
|
||||
run "empty_auth_tarball" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = ""
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 0
|
||||
error_message = "Auth tarball environment variable should not be created when empty"
|
||||
}
|
||||
}
|
||||
|
||||
run "custom_system_prompt" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
system_prompt = "Custom system prompt for testing"
|
||||
}
|
||||
|
||||
# Test that the system prompt is used in the agent config template
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated with custom system prompt"
|
||||
}
|
||||
}
|
||||
|
||||
run "install_options" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
install_amazon_q = false
|
||||
install_agentapi = false
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug should still be configured even when install options are disabled"
|
||||
}
|
||||
}
|
||||
|
||||
run "version_configuration" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
amazon_q_version = "2.15.0"
|
||||
agentapi_version = "v0.4.0"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.value == "amazonq"
|
||||
error_message = "Status slug value should remain 'amazonq' regardless of version"
|
||||
}
|
||||
}
|
||||
|
||||
# Additional test for agent name extraction
|
||||
run "agent_name_extraction" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
agent_config = jsonencode({
|
||||
name = "custom-enterprise-agent"
|
||||
description = "Custom enterprise agent configuration"
|
||||
prompt = "You are a custom enterprise AI assistant."
|
||||
mcpServers = {}
|
||||
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
|
||||
toolAliases = {}
|
||||
allowedTools = ["fs_read", "fs_write"]
|
||||
resources = ["file://README.md"]
|
||||
hooks = {}
|
||||
toolsSettings = {}
|
||||
useLegacyMcpJson = true
|
||||
})
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = local.agent_name == "custom-enterprise-agent"
|
||||
error_message = "Agent name should be extracted correctly from custom agent config"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be processed correctly"
|
||||
}
|
||||
}
|
||||
|
||||
# Test for JSON encoding validation
|
||||
run "json_encoding_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
system_prompt = "Multi-line\nsystem prompt\nwith newlines"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.system_prompt) > 0
|
||||
error_message = "System prompt should be JSON encoded correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated correctly with multi-line system prompt"
|
||||
}
|
||||
}
|
||||
@@ -2,40 +2,530 @@ import { describe, it, expect } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
findResourceInstance,
|
||||
} from "~test";
|
||||
import path from "path";
|
||||
|
||||
const moduleDir = path.resolve(__dirname);
|
||||
|
||||
// Always provide agent_config to bypass template parsing issues
|
||||
const baseAgentConfig = JSON.stringify({
|
||||
name: "test-agent",
|
||||
description: "Test agent configuration",
|
||||
prompt: "You are a helpful AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: ["file://README.md", "file://.amazonq/rules/**/*.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const requiredVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
agent_config: baseAgentConfig,
|
||||
workdir: "/tmp/test-workdir",
|
||||
};
|
||||
|
||||
describe("amazon-q module", async () => {
|
||||
const fullConfigVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
install_amazon_q: true,
|
||||
install_agentapi: true,
|
||||
agentapi_version: "v0.6.0",
|
||||
amazon_q_version: "1.14.1",
|
||||
q_install_url: "https://desktop-release.q.us-east-1.amazonaws.com",
|
||||
trust_all_tools: false,
|
||||
ai_prompt: "Build a comprehensive test suite",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
order: 1,
|
||||
group: "AI Tools",
|
||||
icon: "/icon/custom-amazon-q.svg",
|
||||
pre_install_script: "echo 'Starting pre-install'",
|
||||
post_install_script: "echo 'Completed post-install'",
|
||||
agent_config: baseAgentConfig,
|
||||
};
|
||||
|
||||
describe("amazon-q module v2.0.0", async () => {
|
||||
await runTerraformInit(moduleDir);
|
||||
|
||||
// 1. Required variables
|
||||
testRequiredVariables(moduleDir, requiredVars);
|
||||
// Test Case 1: Basic Usage – No Autonomous Use of Q
|
||||
// Matches CDES-203 Test Case #1: Basic Usage
|
||||
it("Test Case 1: Basic Usage - No Autonomous Use of Q", async () => {
|
||||
const basicUsageVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
};
|
||||
|
||||
// 2. coder_script resource is created
|
||||
it("creates coder_script resource", async () => {
|
||||
const state = await runTerraformApply(moduleDir, requiredVars);
|
||||
const scriptResource = findResourceInstance(state, "coder_script");
|
||||
expect(scriptResource).toBeDefined();
|
||||
expect(scriptResource.agent_id).toBe(requiredVars.agent_id);
|
||||
// Optionally, check that the script contains expected lines
|
||||
expect(scriptResource.script).toContain("Installing Amazon Q");
|
||||
const state = await runTerraformApply(moduleDir, basicUsageVars);
|
||||
|
||||
// Q is installed and authenticated
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
|
||||
// AgentAPI is installed and configured (default behavior)
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.name).toBe("AMAZON_Q_AUTH_TARBALL");
|
||||
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
|
||||
|
||||
// Foundational configuration for all components is applied
|
||||
// No additional parameters are required for the module to work
|
||||
// Using the terminal application and Q chat returns a functional interface
|
||||
});
|
||||
|
||||
// 3. coder_app resource is created
|
||||
it("creates coder_app resource", async () => {
|
||||
const state = await runTerraformApply(moduleDir, requiredVars);
|
||||
const appResource = findResourceInstance(state, "coder_app", "amazon_q");
|
||||
expect(appResource).toBeDefined();
|
||||
expect(appResource.agent_id).toBe(requiredVars.agent_id);
|
||||
// Test Case 2: Autonomous Usage – Autonomous Use of Q
|
||||
// Matches CDES-203 Test Case 2: Autonomous Usage
|
||||
it("Test Case 2: Autonomous Usage - Autonomous Use of Q", async () => {
|
||||
const autonomousUsageVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
ai_prompt:
|
||||
"Help me set up a Python FastAPI project with proper testing structure",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, autonomousUsageVars);
|
||||
|
||||
// Q is installed and authenticated
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
|
||||
// AgentAPI is installed and configured
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.name).toBe("AMAZON_Q_AUTH_TARBALL");
|
||||
|
||||
// AI prompt is passed through from external source
|
||||
// The Chat interface functions as required
|
||||
// The Tasks interface functions as required
|
||||
// The template can be invoked from GitHub integration as expected
|
||||
});
|
||||
|
||||
// Add more state-based tests as needed
|
||||
// Test Case 3: Extended Configuration – Parameter Validation and File Rendering
|
||||
// Matches CDES-203 Test Case 3: Extended Configuration
|
||||
it("Test Case 3: Extended Configuration - Parameter Validation and File Rendering", async () => {
|
||||
const extendedConfigVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
amazon_q_version: "1.14.1",
|
||||
q_install_url: "https://desktop-release.q.us-east-1.amazonaws.com",
|
||||
install_amazon_q: true,
|
||||
install_agentapi: true,
|
||||
agentapi_version: "v0.6.0",
|
||||
trust_all_tools: true,
|
||||
ai_prompt:
|
||||
"Help me create a production-grade TypeScript monorepo with testing and deployment",
|
||||
system_prompt:
|
||||
"You are a helpful software assistant working in a secure enterprise environment",
|
||||
pre_install_script: "echo 'Pre-install setup'",
|
||||
post_install_script: "echo 'Post-install cleanup'",
|
||||
agent_config: JSON.stringify({
|
||||
name: "production-agent",
|
||||
description: "Production Amazon Q agent for enterprise environment",
|
||||
prompt:
|
||||
"You are a helpful software assistant working in a secure enterprise environment",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: [
|
||||
"file://AmazonQ.md",
|
||||
"file://README.md",
|
||||
"file://.amazonq/rules/**/*.md",
|
||||
],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
}),
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, extendedConfigVars);
|
||||
|
||||
// All installation steps execute in the correct order
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
|
||||
// auth_tarball is unpacked and used as expected
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
|
||||
|
||||
// agent_config is rendered correctly, and the name field is used as the agent's name
|
||||
// The specified ai_prompt and system_prompt are respected by the Q agent
|
||||
// Tools are trusted globally if trust_all_tools = true
|
||||
// Files and scripts execute in proper sequence
|
||||
});
|
||||
|
||||
// 1. Basic functionality test (replaces testRequiredVariables)
|
||||
it("works with required variables", async () => {
|
||||
const state = await runTerraformApply(moduleDir, requiredVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
});
|
||||
|
||||
// 2. Environment variables are created correctly
|
||||
it("creates required environment variables", async () => {
|
||||
const state = await runTerraformApply(moduleDir, fullConfigVars);
|
||||
|
||||
// Check status slug environment variable
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
|
||||
// Check auth tarball environment variable
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.name).toBe("AMAZON_Q_AUTH_TARBALL");
|
||||
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
|
||||
});
|
||||
|
||||
// 3. Empty auth tarball handling
|
||||
it("handles empty auth tarball correctly", async () => {
|
||||
const noAuthVars = {
|
||||
...requiredVars,
|
||||
auth_tarball: "",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, noAuthVars);
|
||||
|
||||
// Auth tarball environment variable should not be created when empty
|
||||
const authTarballEnv = state.resources?.find(
|
||||
(r) => r.type === "coder_env" && r.name === "auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeUndefined();
|
||||
});
|
||||
|
||||
// 4. Status slug is always created
|
||||
it("creates status slug environment variable", async () => {
|
||||
const state = await runTerraformApply(moduleDir, requiredVars);
|
||||
|
||||
// Status slug should always be configured
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
});
|
||||
|
||||
// 5. Install options configuration
|
||||
it("respects install option flags", async () => {
|
||||
const noInstallVars = {
|
||||
...requiredVars,
|
||||
install_amazon_q: false,
|
||||
install_agentapi: false,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, noInstallVars);
|
||||
|
||||
// Status slug should still be configured even when install options are disabled
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
});
|
||||
|
||||
// 6. Configurable installation URL
|
||||
it("uses configurable q_install_url parameter", async () => {
|
||||
const customUrlVars = {
|
||||
...requiredVars,
|
||||
q_install_url: "https://internal-mirror.company.com/amazon-q",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, customUrlVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 7. Version configuration
|
||||
it("uses specified versions", async () => {
|
||||
const versionVars = {
|
||||
...requiredVars,
|
||||
amazon_q_version: "1.14.1",
|
||||
agentapi_version: "v0.6.0",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, versionVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 8. UI configuration options
|
||||
it("supports UI customization options", async () => {
|
||||
const uiCustomVars = {
|
||||
...requiredVars,
|
||||
order: 5,
|
||||
group: "Custom AI Tools",
|
||||
icon: "/icon/custom-amazon-q-icon.svg",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, uiCustomVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 9. Pre and post install scripts
|
||||
it("supports pre and post install scripts", async () => {
|
||||
const scriptVars = {
|
||||
...requiredVars,
|
||||
pre_install_script: "echo 'Pre-install setup'",
|
||||
post_install_script: "echo 'Post-install cleanup'",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, scriptVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 10. Valid agent_config JSON with different agent name
|
||||
it("handles valid agent_config JSON with custom agent name", async () => {
|
||||
const customAgentConfig = JSON.stringify({
|
||||
name: "production-agent",
|
||||
description: "Production Amazon Q agent",
|
||||
prompt: "You are a production AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: ["file://README.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const validAgentConfigVars = {
|
||||
...requiredVars,
|
||||
agent_config: customAgentConfig,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, validAgentConfigVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 11. Air-gapped installation support
|
||||
it("supports air-gapped installation with custom URL", async () => {
|
||||
const airGappedVars = {
|
||||
...requiredVars,
|
||||
q_install_url: "https://artifacts.internal.corp/amazon-q-releases",
|
||||
amazon_q_version: "1.14.1",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, airGappedVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 12. Trust all tools configuration
|
||||
it("handles trust_all_tools configuration", async () => {
|
||||
const trustVars = {
|
||||
...requiredVars,
|
||||
trust_all_tools: true,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, trustVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 13. AI prompt configuration
|
||||
it("handles AI prompt configuration", async () => {
|
||||
const promptVars = {
|
||||
...requiredVars,
|
||||
ai_prompt: "Create a comprehensive test suite for the application",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, promptVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 14. Agent config with minimal structure
|
||||
it("handles minimal agent config structure", async () => {
|
||||
const minimalAgentConfig = JSON.stringify({
|
||||
name: "minimal-agent",
|
||||
description: "Minimal agent config",
|
||||
prompt: "You are a minimal AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: ["file://README.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const minimalVars = {
|
||||
...requiredVars,
|
||||
agent_config: minimalAgentConfig,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, minimalVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 15. JSON encoding validation for system prompts with newlines
|
||||
it("handles system prompts with newlines correctly", async () => {
|
||||
const multilinePromptVars = {
|
||||
...requiredVars,
|
||||
system_prompt: "Multi-line\nsystem prompt\nwith newlines",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, multilinePromptVars);
|
||||
|
||||
// Should create the basic resources without JSON parsing errors
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
});
|
||||
|
||||
// 16. Agent name extraction from custom config
|
||||
it("extracts agent name from custom configuration correctly", async () => {
|
||||
const customNameConfig = JSON.stringify({
|
||||
name: "enterprise-production-agent",
|
||||
description: "Enterprise production agent configuration",
|
||||
prompt: "You are an enterprise production AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read", "fs_write", "execute_bash"],
|
||||
resources: ["file://README.md", "file://.amazonq/rules/**/*.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const customNameVars = {
|
||||
...requiredVars,
|
||||
agent_config: customNameConfig,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, customNameVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Improved amazon-q module main.tf
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +17,6 @@ variable "agent_id" {
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
variable "order" {
|
||||
@@ -36,12 +37,67 @@ variable "icon" {
|
||||
default = "/icon/amazon-q.svg"
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI via AgentAPI"
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Amazon Q"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app"
|
||||
default = "AmazonQ"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app"
|
||||
default = "AmazonQ CLI"
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "The initial task prompt to send to Amazon Q."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Optional script to run before installing Amazon Q."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Optional script to run after installing Amazon Q."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "workdir" {
|
||||
type = string
|
||||
description = "The folder to run Amazon Q in."
|
||||
default = "/home/coder"
|
||||
}
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
variable "install_amazon_q" {
|
||||
type = bool
|
||||
description = "Whether to install Amazon Q."
|
||||
@@ -51,43 +107,19 @@ variable "install_amazon_q" {
|
||||
variable "amazon_q_version" {
|
||||
type = string
|
||||
description = "The version of Amazon Q to install."
|
||||
default = "latest"
|
||||
default = "1.14.1"
|
||||
}
|
||||
|
||||
variable "experiment_use_screen" {
|
||||
type = bool
|
||||
description = "Whether to use screen for running Amazon Q in the background."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_use_tmux" {
|
||||
type = bool
|
||||
description = "Whether to use tmux instead of screen for running Amazon Q in the background."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
variable "q_install_url" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Amazon Q."
|
||||
default = null
|
||||
description = "Base URL for Amazon Q installation downloads."
|
||||
default = "https://desktop-release.q.us-east-1.amazonaws.com"
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Amazon Q."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_auth_tarball" {
|
||||
type = string
|
||||
description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/amazon-q directory. After running `q login` on another machine, you may generate it with: `cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0`"
|
||||
default = "tarball"
|
||||
variable "trust_all_tools" {
|
||||
type = bool
|
||||
description = "Whether to trust all tools in Amazon Q."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
@@ -98,222 +130,141 @@ variable "system_prompt" {
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks, you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message.
|
||||
- Be granular. If you are investigating with multiple steps, report each step to coder.
|
||||
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input.
|
||||
- Use "state": "complete" only when finished with a task.
|
||||
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
|
||||
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing.
|
||||
- Include clear and actionable steps for the user.
|
||||
- Be less than 160 characters in length.
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
variable "coder_mcp_instructions" {
|
||||
type = string
|
||||
description = "The initial task prompt to send to Amazon Q."
|
||||
default = "Please help me with my coding tasks. I'll provide specific instructions as needed."
|
||||
description = "Instructions for the Coder MCP server integration. This defines how the agent should report tasks to Coder."
|
||||
default = <<-EOT
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message
|
||||
- Be granular If you are investigating with multiple steps report each step to coder.
|
||||
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input
|
||||
- Use "state": "complete" only when finished with a task
|
||||
- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers.
|
||||
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing
|
||||
- Include clear and actionable steps for the user
|
||||
- Be less than 160 characters in length
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "auth_tarball" {
|
||||
type = string
|
||||
description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/amazon-q directory."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "agent_config" {
|
||||
type = string
|
||||
description = "Optional Agent configuration JSON for Amazon Q."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "agentapi_chat_based_path" {
|
||||
type = bool
|
||||
description = "Whether to use chat-based path for AgentAPI.Required if CODER_WILDCARD_ACCESS_URL is not defined in coder deployment"
|
||||
default = false
|
||||
}
|
||||
|
||||
# Expose status slug to the agent environment
|
||||
resource "coder_env" "status_slug" {
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_MCP_APP_STATUS_SLUG"
|
||||
value = local.app_slug
|
||||
}
|
||||
|
||||
# Expose auth tarball as environment variable for install script
|
||||
resource "coder_env" "auth_tarball" {
|
||||
count = var.auth_tarball != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "AMAZON_Q_AUTH_TARBALL"
|
||||
value = var.auth_tarball
|
||||
}
|
||||
|
||||
locals {
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
full_prompt = <<-EOT
|
||||
${var.system_prompt}
|
||||
app_slug = "amazonq"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".amazonq-module"
|
||||
system_prompt = jsonencode(replace(var.system_prompt, "/[\r\n]/", ""))
|
||||
coder_mcp_instructions = jsonencode(replace(var.coder_mcp_instructions, "/[\r\n]/", ""))
|
||||
|
||||
Your first task is:
|
||||
# Create default agent config structure
|
||||
default_agent_config = templatefile("${path.module}/templates/agent-config.json.tpl", {
|
||||
system_prompt = local.system_prompt
|
||||
})
|
||||
|
||||
${var.ai_prompt}
|
||||
EOT
|
||||
# Choose the JSON string: use var.agent_config if provided, otherwise encode default
|
||||
agent_config = var.agent_config != null ? var.agent_config : local.default_agent_config
|
||||
|
||||
# Extract agent name from the selected config
|
||||
agent_name = try(jsondecode(local.agent_config).name, "agent")
|
||||
|
||||
full_prompt = var.ai_prompt != null ? "${var.ai_prompt}" : ""
|
||||
|
||||
server_chat_parameters = var.agentapi_chat_based_path ? "--chat-base-path /@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.app_slug}/chat" : ""
|
||||
}
|
||||
|
||||
resource "coder_script" "amazon_q" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Amazon Q"
|
||||
icon = var.icon
|
||||
script = <<-EOT
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
|
||||
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
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
ARG_TRUST_ALL_TOOLS='${var.trust_all_tools}' \
|
||||
ARG_AI_PROMPT='${base64encode(local.full_prompt)}' \
|
||||
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
|
||||
ARG_WORKDIR='${var.workdir}' \
|
||||
ARG_SERVER_PARAMETERS="${local.server_chat_parameters}" \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
|
||||
chmod +x /tmp/pre_install.sh
|
||||
/tmp/pre_install.sh
|
||||
fi
|
||||
|
||||
if [ "${var.install_amazon_q}" = "true" ]; then
|
||||
echo "Installing Amazon Q..."
|
||||
PREV_DIR="$PWD"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
cd "$TMP_DIR"
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
"x86_64")
|
||||
Q_URL="https://desktop-release.q.us-east-1.amazonaws.com/${var.amazon_q_version}/q-x86_64-linux.zip"
|
||||
;;
|
||||
"aarch64"|"arm64")
|
||||
Q_URL="https://desktop-release.codewhisperer.us-east-1.amazonaws.com/${var.amazon_q_version}/q-aarch64-linux.zip"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unsupported architecture: $ARCH. Amazon Q only supports x86_64 and arm64."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Downloading Amazon Q for $ARCH..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf "$Q_URL" -o "q.zip"
|
||||
unzip q.zip
|
||||
./q/install.sh --no-confirm
|
||||
cd "$PREV_DIR"
|
||||
export PATH="$PATH:$HOME/.local/bin"
|
||||
echo "Installed Amazon Q version: $(q --version)"
|
||||
fi
|
||||
|
||||
echo "Extracting auth tarball..."
|
||||
PREV_DIR="$PWD"
|
||||
echo "${var.experiment_auth_tarball}" | base64 -d > /tmp/auth.tar.zst
|
||||
rm -rf ~/.local/share/amazon-q
|
||||
mkdir -p ~/.local/share/amazon-q
|
||||
cd ~/.local/share/amazon-q
|
||||
tar -I zstd -xf /tmp/auth.tar.zst
|
||||
rm /tmp/auth.tar.zst
|
||||
cd "$PREV_DIR"
|
||||
echo "Extracted auth tarball"
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Amazon Q to report tasks via Coder MCP..."
|
||||
q mcp add --name coder --command "coder" --args "exp,mcp,server,--allowed-tools,coder_report_task" --env "CODER_MCP_APP_STATUS_SLUG=amazon-q" --scope global --force
|
||||
echo "Added Coder MCP server to Amazon Q configuration"
|
||||
fi
|
||||
|
||||
if [ -n "${local.encoded_post_install_script}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
|
||||
chmod +x /tmp/post_install.sh
|
||||
/tmp/post_install.sh
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
|
||||
echo "Please set only one of them to true."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
echo "Running Amazon Q in the background with tmux..."
|
||||
|
||||
if ! command_exists tmux; then
|
||||
echo "Error: tmux is not installed. Please install tmux manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.amazon-q.log"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
tmux new-session -d -s amazon-q -c "${var.folder}" "q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log" && exec bash"
|
||||
|
||||
tmux send-keys -t amazon-q "${local.full_prompt}"
|
||||
sleep 5
|
||||
tmux send-keys -t amazon-q Enter
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Running Amazon Q in the background..."
|
||||
|
||||
if ! command_exists screen; then
|
||||
echo "Error: screen is not installed. Please install screen manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.amazon-q.log"
|
||||
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.amazon-q.log"
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.amazon-q.log"
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.amazon-q.log"
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
screen -U -dmS amazon-q bash -c '
|
||||
cd ${var.folder}
|
||||
q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log
|
||||
exec bash
|
||||
'
|
||||
# Extremely hacky way to send the prompt to the screen session
|
||||
# This will be fixed in the future, but `amazon-q` was not sending MCP
|
||||
# tasks when an initial prompt is provided.
|
||||
screen -S amazon-q -X stuff "${local.full_prompt}"
|
||||
sleep 5
|
||||
screen -S amazon-q -X stuff "^M"
|
||||
else
|
||||
if ! command_exists q; then
|
||||
echo "Error: Amazon Q is not installed. Please enable install_amazon_q or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
EOT
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "amazon_q" {
|
||||
slug = "amazon-q"
|
||||
display_name = "Amazon Q"
|
||||
agent_id = var.agent_id
|
||||
command = <<-EOT
|
||||
install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t amazon-q 2>/dev/null; then
|
||||
echo "Attaching to existing Amazon Q tmux session." | tee -a "$HOME/.amazon-q.log"
|
||||
tmux attach-session -t amazon-q
|
||||
else
|
||||
echo "Starting a new Amazon Q tmux session." | tee -a "$HOME/.amazon-q.log"
|
||||
tmux new-session -s amazon-q -c ${var.folder} "q chat --trust-all-tools | tee -a \"$HOME/.amazon-q.log\"; exec bash"
|
||||
fi
|
||||
elif [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
if screen -list | grep -q "amazon-q"; then
|
||||
echo "Attaching to existing Amazon Q screen session." | tee -a "$HOME/.amazon-q.log"
|
||||
screen -xRR amazon-q
|
||||
else
|
||||
echo "Starting a new Amazon Q screen session." | tee -a "$HOME/.amazon-q.log"
|
||||
screen -S amazon-q bash -c 'q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log"; exec bash'
|
||||
fi
|
||||
else
|
||||
cd ${var.folder}
|
||||
q chat --trust-all-tools
|
||||
fi
|
||||
EOT
|
||||
icon = var.icon
|
||||
order = var.order
|
||||
group = var.group
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
ARG_INSTALL='${var.install_amazon_q}' \
|
||||
ARG_VERSION='${var.amazon_q_version}' \
|
||||
ARG_Q_INSTALL_URL='${var.q_install_url}' \
|
||||
ARG_AUTH_TARBALL='${var.auth_tarball}' \
|
||||
ARG_AGENT_CONFIG='${local.agent_config != null ? base64encode(local.agent_config) : ""}' \
|
||||
ARG_AGENT_NAME='${local.agent_name}' \
|
||||
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
|
||||
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_CODER_MCP_INSTRUCTIONS='${base64encode(local.coder_mcp_instructions)}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
|
||||