Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| ac48f0d166 | |||
| 49ef1203e4 | |||
| b5837a704d | |||
| 5764ff2fdc | |||
| df2f4321a1 | |||
| 8677e7d52b | |||
| d6ae51fad0 | |||
| eca289be3a | |||
| 7aa75f9451 | |||
| efac32ad9f | |||
| a69accbad5 | |||
| 29e5307121 | |||
| 545a245530 | |||
| c554463d4d | |||
| 4ea87a6e01 | |||
| f5a571679a | |||
| 0e1dcd3a80 | |||
| 4238f38353 | |||
| 858799ce20 |
@@ -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
|
||||
@@ -48,7 +48,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.37.2
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
@@ -61,7 +61,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,8 @@
|
||||
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="25" height="25"/>
|
||||
<path d="M5.50694 21.1637C5.17323 21.1637 4.89218 21.1064 4.66378 20.9926C4.436 20.8787 4.26333 20.7052 4.1476 20.4763C4.03187 20.2473 3.97247 19.9672 3.97247 19.6382V16.2463C3.97247 15.8281 3.88859 15.5239 3.72265 15.3341C3.55549 15.1449 3.25912 15.0449 2.83418 15.0353C2.70191 15.0353 2.59598 14.9859 2.51577 14.8853C2.43433 14.7859 2.39453 14.6708 2.39453 14.5425C2.39453 14.4033 2.43433 14.2882 2.51577 14.1984C2.5966 14.1087 2.70375 14.0593 2.83418 14.0496C3.25912 14.0394 3.55549 13.94 3.72265 13.7508C3.88981 13.5616 3.97247 13.2622 3.97247 12.8537V9.46177C3.97247 8.96352 4.10474 8.58456 4.36742 8.3261C4.6301 8.06763 5.01035 7.9375 5.50694 7.9375H9.55926C9.71173 7.9375 9.83725 7.98269 9.9389 8.07185C10.0399 8.16162 10.0914 8.27669 10.0914 8.41466C10.0914 8.5448 10.0485 8.65626 9.96278 8.75145C9.87706 8.84664 9.76316 8.89423 9.62049 8.89423H5.8578C5.6441 8.89423 5.48245 8.94906 5.37162 9.05871C5.26018 9.16836 5.20446 9.33766 5.20446 9.5678V12.9754C5.20446 13.2742 5.14323 13.5454 5.02199 13.7894C4.90075 14.034 4.73848 14.2256 4.53581 14.3659C4.33313 14.5051 4.09616 14.575 3.82246 14.575V14.5147C4.09616 14.5147 4.33313 14.5846 4.53581 14.7238C4.73848 14.863 4.90075 15.0552 5.02199 15.3004C5.14323 15.5444 5.20446 15.8155 5.20446 16.1143V19.537C5.20446 19.7671 5.26018 19.9358 5.37162 20.0461C5.48306 20.1569 5.64533 20.2106 5.8578 20.2106H9.62049C9.76194 20.2106 9.87583 20.2582 9.96278 20.3527C10.0497 20.4479 10.0914 20.56 10.0914 20.6895C10.0914 20.8191 10.0412 20.9299 9.9389 21.0251C9.83725 21.1203 9.71112 21.1673 9.55926 21.1673H5.50694V21.1643V21.1637Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
|
||||
<path d="M15.4423 21.1634C15.2898 21.1634 15.1643 21.1158 15.0626 21.0212C14.961 20.926 14.9102 20.8139 14.9102 20.6856C14.9102 20.5573 14.953 20.444 15.0387 20.3488C15.1245 20.2536 15.2384 20.2067 15.381 20.2067H19.1437C19.3574 20.2067 19.5191 20.153 19.6299 20.0422C19.7413 19.9325 19.7971 19.7632 19.7971 19.5331V16.1104C19.7971 15.8116 19.8583 15.5405 19.9795 15.2965C20.1008 15.0519 20.263 14.8603 20.4657 14.7199C20.6684 14.5807 20.9054 14.5108 21.1791 14.5108V14.5711C20.9054 14.5711 20.6684 14.5012 20.4657 14.362C20.263 14.2229 20.1008 14.0307 19.9795 13.7855C19.8583 13.5415 19.7971 13.2703 19.7971 12.9715V9.5639C19.7971 9.33496 19.7413 9.16566 19.6299 9.0548C19.5185 8.94515 19.3562 8.89033 19.1437 8.89033H15.381C15.2396 8.89033 15.1257 8.84273 15.0387 8.74754C14.953 8.65355 14.9102 8.54089 14.9102 8.41076C14.9102 8.27158 14.9604 8.15771 15.0626 8.06795C15.1637 7.97818 15.2898 7.93359 15.4423 7.93359H19.4946C19.9912 7.93359 20.3702 8.06373 20.6341 8.32219C20.898 8.58065 21.029 8.95961 21.029 9.45786V12.8498C21.029 13.2583 21.1129 13.5583 21.2789 13.7469C21.446 13.9361 21.7424 14.0361 22.1673 14.0457C22.2996 14.0554 22.4055 14.1048 22.4858 14.1945C22.5672 14.2843 22.607 14.3994 22.607 14.5385C22.607 14.6687 22.5672 14.7826 22.4858 14.8814C22.4055 14.9808 22.2978 15.0314 22.1673 15.0314C21.7424 15.041 21.4466 15.141 21.2789 15.3302C21.1117 15.5194 21.029 15.823 21.029 16.2424V19.6343C21.029 19.9639 20.9709 20.2422 20.8539 20.4723C20.737 20.7025 20.5655 20.8736 20.3377 20.9887C20.1093 21.1025 19.8283 21.1598 19.4946 21.1598H15.4423V21.1628V21.1634Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
|
||||
<path d="M16.4845 15.8401C17.2224 15.8401 17.8206 15.2515 17.8206 14.5255C17.8206 13.7996 17.2224 13.2109 16.4845 13.2109C15.7467 13.2109 15.1484 13.7996 15.1484 14.5255C15.1484 15.2515 15.7467 15.8401 16.4845 15.8401Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
|
||||
<path d="M9.00014 15.8401C9.73798 15.8401 10.3362 15.2515 10.3362 14.5255C10.3362 13.7996 9.73798 13.2109 9.00014 13.2109C8.2623 13.2109 7.66406 13.7996 7.66406 14.5255C7.66406 15.2515 8.2623 15.8401 9.00014 15.8401Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
|
||||
<path d="M12.0442 4.13327L11.942 6.81971C11.942 6.97033 11.7974 7.04564 11.5084 7.04564C11.2194 7.04564 11.0749 6.97033 11.0749 6.81971C11.0492 6.15036 11.0284 5.63103 11.0112 5.26291C11.0027 4.88637 10.9941 4.61826 10.9855 4.45921C10.9769 4.30016 10.9727 4.20376 10.9727 4.17062V4.12062C10.9727 3.92843 11.1515 3.83203 11.5084 3.83203C11.8654 3.83203 12.0442 3.93264 12.0442 4.13327ZM14.213 4.13327L14.1108 6.81971C14.1108 6.97033 13.9663 7.04564 13.6773 7.04564C13.3883 7.04564 13.2437 6.97033 13.2437 6.81971C13.218 6.15036 13.1972 5.63103 13.1801 5.26291C13.1715 4.88637 13.1629 4.61826 13.1543 4.45921C13.1458 4.30016 13.1415 4.20376 13.1415 4.17062V4.12062C13.1415 3.92843 13.3203 3.83203 13.6773 3.83203C14.0342 3.83203 14.213 3.93264 14.213 4.13327Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -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 |
@@ -0,0 +1,15 @@
|
||||
<svg width="721" height="721" viewBox="0 0 721 721" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1637_2935)">
|
||||
<g clip-path="url(#clip1_1637_2935)">
|
||||
<path d="M304.246 295.411V249.828C304.246 245.989 305.687 243.109 309.044 241.191L400.692 188.412C413.167 181.215 428.042 177.858 443.394 177.858C500.971 177.858 537.44 222.482 537.44 269.982C537.44 273.34 537.44 277.179 536.959 281.018L441.954 225.358C436.197 222 430.437 222 424.68 225.358L304.246 295.411ZM518.245 472.945V364.024C518.245 357.304 515.364 352.507 509.608 349.149L389.174 279.096L428.519 256.543C431.877 254.626 434.757 254.626 438.115 256.543L529.762 309.323C556.154 324.679 573.905 357.304 573.905 388.971C573.905 425.436 552.315 459.024 518.245 472.941V472.945ZM275.937 376.982L236.592 353.952C233.235 352.034 231.794 349.154 231.794 345.315V239.756C231.794 188.416 271.139 149.548 324.4 149.548C344.555 149.548 363.264 156.268 379.102 168.262L284.578 222.964C278.822 226.321 275.942 231.119 275.942 237.838V376.986L275.937 376.982ZM360.626 425.922L304.246 394.255V327.083L360.626 295.416L417.002 327.083V394.255L360.626 425.922ZM396.852 571.789C376.698 571.789 357.989 565.07 342.151 553.075L436.674 498.374C442.431 495.017 445.311 490.219 445.311 483.499V344.352L485.138 367.382C488.495 369.299 489.936 372.179 489.936 376.018V481.577C489.936 532.917 450.109 571.785 396.852 571.785V571.789ZM283.134 464.79L191.486 412.01C165.094 396.654 147.343 364.029 147.343 332.362C147.343 295.416 169.415 262.309 203.48 248.393V357.791C203.48 364.51 206.361 369.308 212.117 372.665L332.074 442.237L292.729 464.79C289.372 466.707 286.491 466.707 283.134 464.79ZM277.859 543.48C223.639 543.48 183.813 502.695 183.813 452.314C183.813 448.475 184.294 444.636 184.771 440.797L279.295 495.498C285.051 498.856 290.812 498.856 296.568 495.498L417.002 425.927V471.509C417.002 475.349 415.562 478.229 412.204 480.146L320.557 532.926C308.081 540.122 293.206 543.48 277.854 543.48H277.859ZM396.852 600.576C454.911 600.576 503.37 559.313 514.41 504.612C568.149 490.696 602.696 440.315 602.696 388.976C602.696 355.387 588.303 322.762 562.392 299.25C564.791 289.173 566.231 279.096 566.231 269.024C566.231 200.411 510.571 149.067 446.274 149.067C433.322 149.067 420.846 150.984 408.37 155.305C386.775 134.192 357.026 120.758 324.4 120.758C266.342 120.758 217.883 162.02 206.843 216.721C153.104 230.637 118.557 281.018 118.557 332.357C118.557 365.946 132.95 398.571 158.861 422.083C156.462 432.16 155.022 442.237 155.022 452.309C155.022 520.922 210.682 572.266 274.978 572.266C287.931 572.266 300.407 570.349 312.883 566.028C334.473 587.141 364.222 600.576 396.852 600.576Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1637_2935">
|
||||
<rect width="720" height="720" fill="white" transform="translate(0.606934 0.899902)"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_1637_2935">
|
||||
<rect width="484.139" height="479.818" fill="white" transform="translate(118.557 120.758)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -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,137 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="800"
|
||||
height="800"
|
||||
viewBox="0 0 211.66666 211.66666"
|
||||
version="1.1"
|
||||
id="svg924"
|
||||
inkscape:export-filename="/home/daniela/Documents/proxmox/Proxmox/Marketing/Logo/proxmox-logo/Screen/Full Lockup/stacked/proxmox-logo-color-stacked-bgblack.png"
|
||||
inkscape:export-xdpi="360"
|
||||
inkscape:export-ydpi="360"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
|
||||
sodipodi:docname="proxmox-logo-stacked-inverted-color-bgtrans.svg">
|
||||
<defs
|
||||
id="defs918" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.38489655"
|
||||
inkscape:cx="541.41545"
|
||||
inkscape:cy="189.70435"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
inkscape:pagecheckerboard="true"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="1720"
|
||||
inkscape:window-height="1343"
|
||||
inkscape:window-x="1720"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="0"
|
||||
units="px" />
|
||||
<metadata
|
||||
id="metadata921">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-11.346916,-31.368461)">
|
||||
<g
|
||||
id="g209">
|
||||
<g
|
||||
transform="matrix(0.84666672,0,0,0.84666672,544.05161,-814.30036)"
|
||||
id="g1288"
|
||||
style="fill:#ffffff">
|
||||
<g
|
||||
id="g1286"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:25.8336px;line-height:125%;font-family:Helion;-inkscape-font-specification:Helion;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
transform="matrix(1.2435137,0,0,1.2435137,-791.06481,553.75862)">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1272"
|
||||
d="m 168.85142,500.9595 h -11.85747 c -0.46554,0.0129 -0.85842,0.18085 -1.17864,0.50374 -0.32023,0.32294 -0.48707,0.72335 -0.50052,1.20125 v 16.3783 c 1.27229,-0.0318 2.33145,-0.472 3.17749,-1.32073 0.84604,-0.84873 1.2852,-1.91543 1.3175,-3.2001 h 9.04164 c 1.28573,-0.0317 2.35673,-0.47199 3.21302,-1.32072 0.85624,-0.84873 1.30079,-1.91543 1.33364,-3.2001 v -4.49499 c -0.0328,-1.28573 -0.4774,-2.35673 -1.33364,-3.21301 -0.85629,-0.85625 -1.92729,-1.3008 -3.21302,-1.33364 z m -9.04164,9.60997 v -5.06332 h 7.93081 c 0.0463,-0.0231 0.23141,0.0232 0.55542,0.13885 0.32398,0.11573 0.50912,0.43972 0.55541,0.97198 v 2.81583 c 0.0231,0.0474 -0.0231,0.23681 -0.13885,0.56833 -0.11573,0.33154 -0.43972,0.52098 -0.97198,0.56833 z"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1274"
|
||||
d="m 194.05931,508.89031 v -3.38416 c -0.0318,-1.28573 -0.47201,-2.35673 -1.32072,-3.21301 -0.84875,-0.85625 -1.91545,-1.3008 -3.2001,-1.33364 h -11.85747 c -0.47684,0.0129 -0.87295,0.18085 -1.18833,0.50374 -0.31538,0.32294 -0.47899,0.72335 -0.49083,1.20125 v 16.3783 c 1.27336,-0.0318 2.33683,-0.472 3.1904,-1.32073 0.85357,-0.84873 1.29705,-1.91543 1.33042,-3.2001 v -1.13666 h 5.14082 l 2.60916,3.71999 c 0.4187,0.60063 0.94398,1.07208 1.57583,1.41437 0.63182,0.34229 1.33793,0.51667 2.11833,0.52313 0.37618,-5.4e-4 0.74107,-0.0447 1.09468,-0.1324 0.35358,-0.0877 0.68618,-0.21582 0.99781,-0.38427 l -3.64249,-5.19249 c 1.05592,-0.22872 1.92133,-0.74647 2.59625,-1.55322 0.67487,-0.80675 1.02362,-1.77011 1.04624,-2.8901 z m -13.53663,0.5425 v -3.92666 h 7.87915 c 0.0474,-0.0231 0.23679,0.0232 0.56833,0.13885 0.33151,0.11573 0.52096,0.43972 0.56833,0.97198 v 1.705 c 0.0237,0.0463 -0.0237,0.23143 -0.14208,0.55541 -0.11842,0.324 -0.44995,0.50914 -0.99458,0.55542 z"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1276"
|
||||
d="m 210.1751,500.9595 h -9.01581 c -1.28467,0.0328 -2.35137,0.47739 -3.2001,1.33364 -0.84873,0.85628 -1.28897,1.92728 -1.32072,3.21301 v 9.01581 c 0.0317,1.28467 0.47199,2.35137 1.32072,3.2001 0.84873,0.84873 1.91543,1.28897 3.2001,1.32073 h 9.01581 c 1.28465,-0.0318 2.35135,-0.472 3.2001,-1.32073 0.84871,-0.84873 1.28895,-1.91543 1.32072,-3.2001 v -9.01581 c -0.0318,-1.28573 -0.47201,-2.35673 -1.32072,-3.21301 -0.84875,-0.85625 -1.91545,-1.3008 -3.2001,-1.33364 z m 0,12.4258 c 0.0237,0.0474 -0.0237,0.23681 -0.14208,0.56833 -0.11842,0.33153 -0.44995,0.52098 -0.99458,0.56833 h -6.74249 c -0.0474,0.0237 -0.23681,-0.0237 -0.56833,-0.14208 -0.33153,-0.1184 -0.52098,-0.44993 -0.56833,-0.99458 v -6.76832 c -0.0237,-0.0463 0.0237,-0.23141 0.14208,-0.55541 0.1184,-0.32398 0.44993,-0.50912 0.99458,-0.55542 h 6.74249 c 0.0473,-0.0231 0.23679,0.0232 0.56833,0.13885 0.33151,0.11573 0.52096,0.43972 0.56833,0.97198 z"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1278"
|
||||
d="m 237.4767,502.25116 c -0.39183,-0.39179 -0.84822,-0.69964 -1.36917,-0.92354 -0.52099,-0.22387 -1.08071,-0.33797 -1.67916,-0.34229 -0.6367,0.005 -1.22333,0.1308 -1.75989,0.37781 -0.53659,0.24705 -1.00052,0.58611 -1.39177,1.01719 l -3.90082,4.28832 -3.92666,-4.28832 c -0.4015,-0.44238 -0.86434,-0.78467 -1.38854,-1.02688 -0.5242,-0.24217 -1.1033,-0.36487 -1.73729,-0.36812 -0.59847,0.004 -1.15819,0.11842 -1.67916,0.34229 -0.52097,0.2239 -0.97736,0.53175 -1.36916,0.92354 l 7.05248,7.74998 -7.05248,7.74998 c 0.3918,0.40419 0.84819,0.71957 1.36916,0.94615 0.52097,0.22657 1.08069,0.34175 1.67916,0.34552 0.62538,-0.005 1.20878,-0.13079 1.75021,-0.37782 0.54142,-0.24703 1.00857,-0.58609 1.40145,-1.01718 l 3.90083,-4.28832 3.90082,4.28832 c 0.39125,0.43109 0.85518,0.77015 1.39177,1.01718 0.53656,0.24703 1.12319,0.37297 1.75989,0.37782 0.59845,-0.004 1.15817,-0.11895 1.67916,-0.34552 0.52096,-0.22658 0.97734,-0.54196 1.36917,-0.94615 l -7.05249,-7.74998 z"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1280"
|
||||
d="m 260.98042,500.9595 h -2.84166 c -0.92947,0.0129 -1.75721,0.2648 -2.48322,0.75562 -0.72604,0.49085 -1.27607,1.14314 -1.6501,1.95687 l 0.0258,-0.0517 -2.66082,5.83832 -2.635,-5.83832 v 0.0517 c -0.36275,-0.81373 -0.90955,-1.46602 -1.64041,-1.95687 -0.73087,-0.49082 -1.56184,-0.74269 -2.49291,-0.75562 h -2.81583 c -0.48922,0.0129 -0.89286,0.18085 -1.21093,0.50374 -0.31808,0.32294 -0.48276,0.72335 -0.49406,1.20125 v 16.3783 c 1.27336,-0.0318 2.33683,-0.472 3.19041,-1.32073 0.85357,-0.84873 1.29704,-1.91543 1.33041,-3.2001 v -8.65414 c 0.002,-0.11785 0.0371,-0.2115 0.10656,-0.28094 0.0694,-0.0694 0.16307,-0.10493 0.28094,-0.10656 0.0673,0.002 0.13293,0.0237 0.19698,0.0646 0.064,0.0409 0.11032,0.0883 0.13885,0.14208 l 5.01166,11.05664 c 0.0947,0.19752 0.23464,0.3579 0.41979,0.48115 0.18512,0.12325 0.38964,0.18675 0.61354,0.19052 0.22226,-0.003 0.42354,-0.0619 0.60385,-0.1776 0.18028,-0.11571 0.32344,-0.27179 0.42948,-0.46823 L 257.4154,505.687 c 0.0393,-0.0538 0.0899,-0.10116 0.15177,-0.14208 0.0619,-0.0409 0.13184,-0.0624 0.2099,-0.0646 0.10547,0.002 0.19158,0.0371 0.25833,0.10656 0.0667,0.0694 0.10116,0.16309 0.10333,0.28094 v 8.65414 c 0.0333,1.28467 0.47682,2.35137 1.33042,3.2001 0.85355,0.84873 1.91702,1.28897 3.19041,1.32073 v -16.3783 c -0.0119,-0.4779 -0.17548,-0.87831 -0.49084,-1.20125 -0.3154,-0.32289 -0.71151,-0.49081 -1.18833,-0.50374 z"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1282"
|
||||
d="m 278.79561,500.9595 h -9.01581 c -1.28467,0.0328 -2.35137,0.47739 -3.20009,1.33364 -0.84874,0.85628 -1.28898,1.92728 -1.32073,3.21301 v 9.01581 c 0.0317,1.28467 0.47199,2.35137 1.32073,3.2001 0.84872,0.84873 1.91542,1.28897 3.20009,1.32073 h 9.01581 c 1.28466,-0.0318 2.35135,-0.472 3.2001,-1.32073 0.84871,-0.84873 1.28896,-1.91543 1.32073,-3.2001 v -9.01581 c -0.0318,-1.28573 -0.47202,-2.35673 -1.32073,-3.21301 -0.84875,-0.85625 -1.91544,-1.3008 -3.2001,-1.33364 z m 0,12.4258 c 0.0237,0.0474 -0.0237,0.23681 -0.14208,0.56833 -0.11842,0.33153 -0.44994,0.52098 -0.99458,0.56833 h -6.74248 c -0.0474,0.0237 -0.23681,-0.0237 -0.56834,-0.14208 -0.33153,-0.1184 -0.52097,-0.44993 -0.56833,-0.99458 v -6.76832 c -0.0237,-0.0463 0.0237,-0.23141 0.14209,-0.55541 0.11839,-0.32398 0.44992,-0.50912 0.99458,-0.55542 h 6.74248 c 0.0473,-0.0231 0.23679,0.0232 0.56833,0.13885 0.33152,0.11573 0.52096,0.43972 0.56833,0.97198 z"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1284"
|
||||
d="m 306.0972,502.25116 c -0.39182,-0.39179 -0.84821,-0.69964 -1.36916,-0.92354 -0.52099,-0.22387 -1.08071,-0.33797 -1.67916,-0.34229 -0.6367,0.005 -1.22333,0.1308 -1.75989,0.37781 -0.5366,0.24705 -1.00052,0.58611 -1.39177,1.01719 l -3.90083,4.28832 -3.92665,-4.28832 c -0.4015,-0.44238 -0.86435,-0.78467 -1.38854,-1.02688 -0.52421,-0.24217 -1.1033,-0.36487 -1.73729,-0.36812 -0.59847,0.004 -1.15819,0.11842 -1.67916,0.34229 -0.52097,0.2239 -0.97736,0.53175 -1.36917,0.92354 l 7.05249,7.74998 -7.05249,7.74998 c 0.39181,0.40419 0.8482,0.71957 1.36917,0.94615 0.52097,0.22657 1.08069,0.34175 1.67916,0.34552 0.62538,-0.005 1.20878,-0.13079 1.7502,-0.37782 0.54142,-0.24703 1.00857,-0.58609 1.40146,-1.01718 l 3.90082,-4.28832 3.90083,4.28832 c 0.39125,0.43109 0.85517,0.77015 1.39177,1.01718 0.53656,0.24703 1.12319,0.37297 1.75989,0.37782 0.59845,-0.004 1.15817,-0.11895 1.67916,-0.34552 0.52095,-0.22658 0.97734,-0.54196 1.36916,-0.94615 l -7.05248,-7.74998 z"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1290"
|
||||
style="fill:#e57000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.730891"
|
||||
d="m 141.89758,116.88641 25.41237,27.92599 c -2.7927,2.88547 -6.70224,4.65495 -10.98527,4.65495 -4.56076,0 -8.56315,-1.95514 -11.35592,-5.02707 l -14.05575,-15.45172 -10.9846,-12.10215 10.9846,-12.00854 14.05575,-15.452532 c 2.79277,-3.071175 6.79516,-5.027074 11.35592,-5.027074 4.28303,0 8.19257,1.768759 10.98527,4.561525 z"
|
||||
sodipodi:nodetypes="ccscccccscc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1292"
|
||||
style="fill:#e57000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.730891"
|
||||
d="m 92.334245,116.8861 -25.41238,27.92603 c 2.7927,2.88547 6.702237,4.65495 10.985287,4.65495 4.560744,0 8.563138,-1.95514 11.355905,-5.02707 L 103.3188,128.98825 114.30338,116.8861 103.3188,104.87756 89.263057,89.425028 c -2.792767,-3.071215 -6.795161,-5.027074 -11.355905,-5.027074 -4.28305,0 -8.192587,1.768759 -10.985287,4.561526 z"
|
||||
sodipodi:nodetypes="ccscccccscc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1294"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.66712"
|
||||
d="m 127.12922,130.73289 -10.02585,-11.04587 -10.0263,11.04587 -23.195348,25.48982 c 2.548811,2.54902 6.118169,4.16327 10.025148,4.16327 4.16359,0 7.64778,-1.69975 10.28111,-4.58817 l 12.91539,-14.10405 12.82882,14.10405 c 2.5493,2.80293 6.20218,4.58817 10.36513,4.58817 3.9091,0 7.47768,-1.61425 10.02652,-4.16327 z"
|
||||
sodipodi:nodetypes="ccccscccscc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1296"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.66712"
|
||||
d="M 127.1286,103.03813 117.10275,114.084 107.07645,103.03813 83.881116,77.548321 c 2.548838,-2.548932 6.118156,-4.163226 10.025162,-4.163226 4.163603,0 7.647762,1.699732 10.281082,4.588143 l 12.91539,14.104094 12.82882,-14.104094 c 2.54934,-2.802999 6.20221,-4.588143 10.36513,-4.588143 3.90911,0 7.47772,1.614294 10.02653,4.163226 z"
|
||||
sodipodi:nodetypes="ccccscccscc" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -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,5 @@
|
||||
<svg width="400" height="400" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.41508 17.2983L7.88484 12.7653L9.51146 18.9412L11.8745 18.2949L9.52018 9.32758L0.69527 6.93747L0.066864 9.35199L6.13926 11.0015L1.68806 15.5279L3.41508 17.2983Z" fill="#F34E3F"/>
|
||||
<path d="M16.3044 12.0436L18.6675 11.3973L16.3132 2.43003L7.48824 0.0399246L6.85984 2.45444L14.312 4.47881L16.3044 12.0436Z" fill="#F34E3F"/>
|
||||
<path d="M12.9126 15.4902L15.2756 14.8439L12.9213 5.87659L4.09639 3.48648L3.46799 5.901L10.9201 7.92537L12.9126 15.4902Z" fill="#F34E3F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 576 B |
@@ -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
|
||||
@@ -495,4 +495,8 @@ When reporting bugs, include:
|
||||
4. **Breaking changes** without defaults
|
||||
5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`) 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! 🚀
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@@ -12,6 +13,10 @@ import (
|
||||
|
||||
var supportedUserNameSpaceDirectories = append(supportedResourceTypes, ".images")
|
||||
|
||||
// validNameRe validates that names contain only alphanumeric characters and hyphens
|
||||
var validNameRe = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$`)
|
||||
|
||||
|
||||
// validateCoderResourceSubdirectory validates that the structure of a module or template within a namespace follows all
|
||||
// expected file conventions
|
||||
func validateCoderResourceSubdirectory(dirPath string) []error {
|
||||
@@ -42,6 +47,12 @@ func validateCoderResourceSubdirectory(dirPath string) []error {
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate module/template name
|
||||
if !validNameRe.MatchString(f.Name()) {
|
||||
errs = append(errs, xerrors.Errorf("%q: name contains invalid characters (only alphanumeric characters and hyphens are allowed)", path.Join(dirPath, f.Name())))
|
||||
continue
|
||||
}
|
||||
|
||||
resourceReadmePath := path.Join(dirPath, f.Name(), "README.md")
|
||||
if _, err := os.Stat(resourceReadmePath); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
@@ -79,6 +90,12 @@ func validateRegistryDirectory() []error {
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate namespace name
|
||||
if !validNameRe.MatchString(nDir.Name()) {
|
||||
allErrs = append(allErrs, xerrors.Errorf("%q: namespace name contains invalid characters (only alphanumeric characters and hyphens are allowed)", namespacePath))
|
||||
continue
|
||||
}
|
||||
|
||||
contributorReadmePath := path.Join(namespacePath, "README.md")
|
||||
if _, err := os.Stat(contributorReadmePath); err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
|
||||
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
display_name: "Tmux"
|
||||
description: "Tmux for coder agent :)"
|
||||
display_name: "tmux"
|
||||
description: "tmux with session persistence and plugins"
|
||||
icon: "../../../../.icons/tmux.svg"
|
||||
verified: false
|
||||
tags: ["tmux", "terminal", "persistent"]
|
||||
@@ -15,7 +15,7 @@ up a default or custom tmux configuration with session save/restore capabilities
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -39,7 +39,7 @@ module "tmux" {
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
tmux_config = "" # Optional: custom tmux.conf content
|
||||
save_interval = 1 # Optional: save interval in minutes
|
||||
@@ -78,7 +78,7 @@ This module can provision multiple tmux sessions, each as a separate app in the
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
agent_id = var.agent_id
|
||||
sessions = ["default", "dev", "anomaly"]
|
||||
tmux_config = <<-EOT
|
||||
@@ -91,11 +91,9 @@ module "tmux" {
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> - If you provide a custom `tmux_config`, it will completely replace the default configuration. Ensure you include plugin
|
||||
> and TPM initialization lines if you want plugin support and session persistence.
|
||||
> - The script will attempt to install dependencies using `sudo` where required.
|
||||
> - If `git` is not installed, TPM installation will fail.
|
||||
> - If you are using custom config, you'll be responsible for setting up persistence and plugins.
|
||||
> - The `order`, `group`, and `icon` variables allow you to customize how tmux apps appear in the Coder UI.
|
||||
> - In case of session restart or shh reconnection, the tmux session will be automatically restored :)
|
||||
> If you provide a custom `tmux_config`, it will completely replace the default configuration. Ensure you include plugin and TPM initialization lines if you want plugin support and session persistence.
|
||||
> The script will attempt to install dependencies using `sudo` where required.
|
||||
> If `git` is not installed, TPM installation will fail.
|
||||
> If you are using custom config, you'll be responsible for setting up persistence and plugins.
|
||||
> The `order`, `group`, and `icon` variables allow you to customize how tmux apps appear in the Coder UI.
|
||||
> In case of session restart or shh reconnection, the tmux session will be automatically restored :)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@ github: coder
|
||||
avatar: ./.images/avatar.svg
|
||||
linkedin: https://www.linkedin.com/company/coderhq
|
||||
website: https://discord.gg/coder
|
||||
status: community
|
||||
status: official
|
||||
---
|
||||
|
||||
å
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
---
|
||||
display_name: Auggie CLI
|
||||
icon: ../../../../.icons/auggie.svg
|
||||
description: Run Auggie CLI in your workspace for AI-powered coding assistance with AgentAPI integration
|
||||
verified: true
|
||||
tags: [agent, auggie, ai, tasks, augment]
|
||||
---
|
||||
|
||||
# Auggie CLI
|
||||
|
||||
Run Auggie CLI in your workspace to access Augment's AI coding assistant with advanced context understanding and codebase integration. This module integrates with [AgentAPI](https://github.com/coder/agentapi).
|
||||
|
||||
```tf
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js and npm must be sourced/available before the auggie module installs** - ensure they are installed in your workspace image or via earlier provisioning steps
|
||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder/coder-login) module to your template
|
||||
- **Augment session token for authentication (required for tasks). [Instructions](https://docs.augmentcode.com/cli/setup-auggie/authentication) to get the session token**
|
||||
|
||||
## Examples
|
||||
|
||||
### Usage with Tasks and Configuration
|
||||
|
||||
```tf
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Initial task prompt for Auggie CLI"
|
||||
mutable = true
|
||||
}
|
||||
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/coder-login/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
# Authentication
|
||||
augment_session_token = <<-EOF
|
||||
{"accessToken":"xxxx-yyyy-zzzz-jjjj","tenantURL":"https://d1.api.augmentcode.com/","scopes":["read","write"]}
|
||||
EOF # Required for tasks
|
||||
|
||||
# Version
|
||||
auggie_version = "0.3.0"
|
||||
|
||||
# Task configuration
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
continue_previous_conversation = true
|
||||
interaction_mode = "quiet"
|
||||
auggie_model = "gpt5"
|
||||
report_tasks = true
|
||||
|
||||
# MCP configuration for additional integrations
|
||||
mcp = <<-EOF
|
||||
{
|
||||
"mcpServers": {
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/coder/project"]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Workspace guidelines
|
||||
rules = <<-EOT
|
||||
# Project Guidelines
|
||||
|
||||
## Code Style
|
||||
- Use TypeScript for all new JavaScript files
|
||||
- Follow consistent naming conventions
|
||||
- Add comprehensive comments for complex logic
|
||||
|
||||
## Testing
|
||||
- Write unit tests for all new functions
|
||||
- Ensure test coverage above 80%
|
||||
|
||||
## Documentation
|
||||
- Update README.md for any new features
|
||||
- Document API changes in CHANGELOG.md
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
### Using Multiple MCP Configuration Files
|
||||
|
||||
```tf
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
# Multiple MCP configuration files
|
||||
mcp_files = [
|
||||
"/path/to/filesystem-mcp.json",
|
||||
"/path/to/database-mcp.json",
|
||||
"/path/to/api-mcp.json"
|
||||
]
|
||||
|
||||
mcp = <<-EOF
|
||||
{
|
||||
"mcpServers": {
|
||||
"Test MCP": {
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"--directory",
|
||||
"/home/coder/test-mcp",
|
||||
"run",
|
||||
"server.py"
|
||||
],
|
||||
"timeout": 600
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If you have any issues, please take a look at the log files below.
|
||||
|
||||
```bash
|
||||
# Installation logs
|
||||
cat ~/.auggie-module/install.log
|
||||
|
||||
# Startup logs
|
||||
cat ~/.auggie-module/agentapi-start.log
|
||||
|
||||
# Pre/post install script logs
|
||||
cat ~/.auggie-module/pre_install.log
|
||||
cat ~/.auggie-module/post_install.log
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> To use tasks with Auggie CLI, create a `coder_parameter` named `"AI Prompt"` and pass its value to the auggie module's `ai_prompt` variable. The `folder` variable is required for the module to function correctly.
|
||||
|
||||
## References
|
||||
|
||||
- [Auggie CLI Reference](https://docs.augmentcode.com/cli/reference)
|
||||
- [Auggie CLI MCP Integration](https://docs.augmentcode.com/cli/integrations#mcp-integrations)
|
||||
- [Augment Code Documentation](https://docs.augmentcode.com/)
|
||||
- [AgentAPI Documentation](https://github.com/coder/agentapi)
|
||||
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
|
||||
@@ -0,0 +1,186 @@
|
||||
run "test_auggie_basic" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-123"
|
||||
folder = "/home/coder/projects"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_env.auggie_session_auth.name == "AUGMENT_SESSION_AUTH"
|
||||
error_message = "Auggie session auth environment variable should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.folder == "/home/coder/projects"
|
||||
error_message = "Folder variable should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.agent_id == "test-agent-123"
|
||||
error_message = "Agent ID variable should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_auggie == true
|
||||
error_message = "Install auggie should default to true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_agentapi == true
|
||||
error_message = "Install agentapi should default to true"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_auggie_with_session_token" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-456"
|
||||
folder = "/home/coder/workspace"
|
||||
augment_session_token = "test-session-token-123"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_env.auggie_session_auth.value == "test-session-token-123"
|
||||
error_message = "Auggie session token value should match the input"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_auggie_with_custom_options" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-789"
|
||||
folder = "/home/coder/custom"
|
||||
order = 5
|
||||
group = "development"
|
||||
icon = "/icon/custom.svg"
|
||||
auggie_model = "gpt-4"
|
||||
ai_prompt = "Help me write better code"
|
||||
interaction_mode = "compact"
|
||||
continue_previous_conversation = true
|
||||
install_auggie = false
|
||||
install_agentapi = false
|
||||
auggie_version = "1.0.0"
|
||||
agentapi_version = "v0.6.0"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.order == 5
|
||||
error_message = "Order variable should be set to 5"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.group == "development"
|
||||
error_message = "Group variable should be set to 'development'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.icon == "/icon/custom.svg"
|
||||
error_message = "Icon variable should be set to custom icon"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.auggie_model == "gpt-4"
|
||||
error_message = "Auggie model variable should be set to 'gpt-4'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.ai_prompt == "Help me write better code"
|
||||
error_message = "AI prompt variable should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.interaction_mode == "compact"
|
||||
error_message = "Interaction mode should be set to 'compact'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.continue_previous_conversation == true
|
||||
error_message = "Continue previous conversation should be set to true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.auggie_version == "1.0.0"
|
||||
error_message = "Auggie version should be set to '1.0.0'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.agentapi_version == "v0.6.0"
|
||||
error_message = "AgentAPI version should be set to 'v0.6.0'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_auggie_with_mcp_and_rules" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-mcp"
|
||||
folder = "/home/coder/mcp-test"
|
||||
mcp = jsonencode({
|
||||
mcpServers = {
|
||||
test = {
|
||||
command = "test-server"
|
||||
args = ["--config", "test.json"]
|
||||
}
|
||||
}
|
||||
})
|
||||
mcp_files = [
|
||||
"/path/to/mcp1.json",
|
||||
"/path/to/mcp2.json"
|
||||
]
|
||||
rules = "# General coding rules\n- Write clean code\n- Add comments"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.mcp != ""
|
||||
error_message = "MCP configuration should be provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(var.mcp_files) == 2
|
||||
error_message = "Should have 2 MCP files"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.rules != ""
|
||||
error_message = "Rules should be provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_auggie_with_scripts" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-scripts"
|
||||
folder = "/home/coder/scripts"
|
||||
pre_install_script = "echo 'Pre-install script'"
|
||||
post_install_script = "echo 'Post-install script'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.pre_install_script == "echo 'Pre-install script'"
|
||||
error_message = "Pre-install script should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.post_install_script == "echo 'Post-install script'"
|
||||
error_message = "Post-install script should be set correctly"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_auggie_interaction_mode_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-validation"
|
||||
folder = "/home/coder/test"
|
||||
interaction_mode = "print"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(["interactive", "print", "quiet", "compact"], var.interaction_mode)
|
||||
error_message = "Interaction mode should be one of the valid options"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
import {
|
||||
test,
|
||||
afterEach,
|
||||
describe,
|
||||
setDefaultTimeout,
|
||||
beforeAll,
|
||||
expect,
|
||||
} from "bun:test";
|
||||
import { execContainer, readFileContainer, runTerraformInit } from "~test";
|
||||
import {
|
||||
loadTestFile,
|
||||
writeExecutable,
|
||||
setup as setupUtil,
|
||||
execModuleScript,
|
||||
expectAgentAPIStarted,
|
||||
} from "../../../coder/modules/agentapi/test-util";
|
||||
import dedent from "dedent";
|
||||
|
||||
let cleanupFunctions: (() => Promise<void>)[] = [];
|
||||
const registerCleanup = (cleanup: () => Promise<void>) => {
|
||||
cleanupFunctions.push(cleanup);
|
||||
};
|
||||
afterEach(async () => {
|
||||
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
|
||||
cleanupFunctions = [];
|
||||
for (const cleanup of cleanupFnsCopy) {
|
||||
try {
|
||||
await cleanup();
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
interface SetupProps {
|
||||
skipAgentAPIMock?: boolean;
|
||||
skipAuggieMock?: boolean;
|
||||
moduleVariables?: Record<string, string>;
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const projectDir = "/home/coder/project";
|
||||
const { id } = await setupUtil({
|
||||
moduleDir: import.meta.dir,
|
||||
moduleVariables: {
|
||||
install_auggie: props?.skipAuggieMock ? "true" : "false",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
folder: projectDir,
|
||||
...props?.moduleVariables,
|
||||
},
|
||||
registerCleanup,
|
||||
projectDir,
|
||||
skipAgentAPIMock: props?.skipAgentAPIMock,
|
||||
agentapiMockScript: props?.agentapiMockScript,
|
||||
});
|
||||
if (!props?.skipAuggieMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/auggie",
|
||||
content: await loadTestFile(import.meta.dir, "auggie-mock.sh"),
|
||||
});
|
||||
}
|
||||
return { id };
|
||||
};
|
||||
|
||||
setDefaultTimeout(60 * 1000);
|
||||
|
||||
describe("auggie", async () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
test("happy-path", async () => {
|
||||
const { id } = await setup();
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("install-auggie-version", async () => {
|
||||
const version_to_install = "0.3.0";
|
||||
const { id } = await setup({
|
||||
skipAuggieMock: true,
|
||||
moduleVariables: {
|
||||
install_auggie: "true",
|
||||
auggie_version: version_to_install,
|
||||
pre_install_script: dedent`
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Install Node.js and npm via system package manager
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y nodejs npm
|
||||
fi
|
||||
|
||||
# Configure npm to use user directory (avoids permission issues)
|
||||
mkdir -p "$HOME/.npm-global"
|
||||
npm config set prefix "$HOME/.npm-global"
|
||||
|
||||
# Persist npm user directory configuration
|
||||
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
|
||||
echo "prefix=$HOME/.npm-global" > ~/.npmrc
|
||||
`,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat /home/coder/.auggie-module/install.log`,
|
||||
]);
|
||||
expect(resp.stdout).toContain(version_to_install);
|
||||
});
|
||||
|
||||
test("check-latest-auggie-version-works", async () => {
|
||||
const { id } = await setup({
|
||||
skipAuggieMock: true,
|
||||
skipAgentAPIMock: true,
|
||||
moduleVariables: {
|
||||
install_auggie: "true",
|
||||
pre_install_script: dedent`
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Install Node.js and npm via system package manager
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y nodejs npm
|
||||
fi
|
||||
|
||||
# Configure npm to use user directory (avoids permission issues)
|
||||
mkdir -p "$HOME/.npm-global"
|
||||
npm config set prefix "$HOME/.npm-global"
|
||||
|
||||
# Persist npm user directory configuration
|
||||
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
|
||||
echo "prefix=$HOME/.npm-global" > ~/.npmrc
|
||||
`,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("auggie-session-token", async () => {
|
||||
const sessionToken = "test-session-token-123";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
augment_session_token: sessionToken,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const envCheck = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`env | grep AUGMENT_SESSION_AUTH || echo "AUGMENT_SESSION_AUTH not found"`,
|
||||
]);
|
||||
expect(envCheck.stdout).toContain("AUGMENT_SESSION_AUTH");
|
||||
});
|
||||
|
||||
test("auggie-mcp-config", async () => {
|
||||
const mcpConfig = JSON.stringify({
|
||||
mcpServers: {
|
||||
test: {
|
||||
command: "test-cmd",
|
||||
type: "stdio",
|
||||
},
|
||||
},
|
||||
});
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
mcp: mcpConfig,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.auggie-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain("--mcp-config");
|
||||
});
|
||||
|
||||
test("auggie-rules", async () => {
|
||||
const rules = "Always use TypeScript for new files";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
install_auggie: "false", // Don't need to install auggie to test rules file creation
|
||||
rules: rules,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const rulesFile = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.augment/rules.md",
|
||||
);
|
||||
expect(rulesFile).toContain(rules);
|
||||
});
|
||||
|
||||
test("auggie-ai-task-prompt", async () => {
|
||||
const prompt = "This is a task prompt for Auggie.";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
ai_prompt: prompt,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat /home/coder/.auggie-module/agentapi-start.log`,
|
||||
]);
|
||||
expect(resp.stdout).toContain(prompt);
|
||||
});
|
||||
|
||||
test("auggie-interaction-mode", async () => {
|
||||
const mode = "compact";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
interaction_mode: mode,
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.auggie-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain(`--${mode}`);
|
||||
});
|
||||
|
||||
test("auggie-model", async () => {
|
||||
const model = "gpt-4";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
auggie_model: model,
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.auggie-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain(`--model ${model}`);
|
||||
});
|
||||
|
||||
test("auggie-continue-previous-conversation", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue_previous_conversation: "true",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.auggie-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain("--continue");
|
||||
});
|
||||
|
||||
test("pre-post-install-scripts", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
pre_install_script: "#!/bin/bash\necho 'auggie-pre-install-script'",
|
||||
post_install_script: "#!/bin/bash\necho 'auggie-post-install-script'",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const preInstallLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.auggie-module/pre_install.log",
|
||||
);
|
||||
expect(preInstallLog).toContain("auggie-pre-install-script");
|
||||
|
||||
const postInstallLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.auggie-module/post_install.log",
|
||||
);
|
||||
expect(postInstallLog).toContain("auggie-post-install-script");
|
||||
});
|
||||
|
||||
test("folder-variable", async () => {
|
||||
const folder = "/home/coder/auggie-test-folder";
|
||||
const { id } = await setup({
|
||||
skipAuggieMock: false,
|
||||
moduleVariables: {
|
||||
folder,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.auggie-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain(folder);
|
||||
});
|
||||
|
||||
test("coder-mcp-config-created", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
install_auggie: "false", // Don't need to install auggie to test MCP config creation
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const mcpConfig = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.augment/coder_mcp.json",
|
||||
);
|
||||
expect(mcpConfig).toContain("mcpServers");
|
||||
expect(mcpConfig).toContain("coder");
|
||||
expect(mcpConfig).toContain("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(mcpConfig).toContain("CODER_MCP_AI_AGENTAPI_URL");
|
||||
});
|
||||
|
||||
test("mcp-files-array", async () => {
|
||||
const mcpFiles = ["/path/to/mcp1.json", "/path/to/mcp2.json"];
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
mcp_files: JSON.stringify(mcpFiles),
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.auggie-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain("mcp1.json");
|
||||
expect(startLog.stdout).toContain("mcp2.json");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,230 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
default = "/icon/auggie.svg"
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to run Auggie in."
|
||||
}
|
||||
|
||||
variable "install_auggie" {
|
||||
type = bool
|
||||
description = "Whether to install Auggie CLI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "auggie_version" {
|
||||
type = string
|
||||
description = "The version of Auggie to install."
|
||||
default = "" # empty string means the latest available version
|
||||
validation {
|
||||
condition = var.auggie_version == "" || can(regex("^v?[0-9]+\\.[0-9]+\\.[0-9]+", var.auggie_version))
|
||||
error_message = "auggie_version must be empty (for latest) or a valid semantic version like 'v1.2.3' or '1.2.3'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.6.0"
|
||||
validation {
|
||||
condition = can(regex("^v[0-9]+\\.[0-9]+\\.[0-9]+", var.agentapi_version))
|
||||
error_message = "agentapi_version must be a valid semantic version starting with 'v', like 'v0.3.3'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Auggie."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Auggie."
|
||||
default = null
|
||||
}
|
||||
|
||||
# ----------------------------------------------
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "Task prompt for the Auggie CLI"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "mcp" {
|
||||
type = string
|
||||
description = "MCP configuration as a JSON string for the auggie cli, check https://docs.augmentcode.com/cli/integrations#mcp-integrations"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "mcp_files" {
|
||||
type = list(string)
|
||||
description = "MCP configuration from a JSON file for the auggie cli, check https://docs.augmentcode.com/cli/integrations#mcp-integrations"
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "rules" {
|
||||
type = string
|
||||
description = "Additional rules to append to workspace guidelines (markdown format)"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "continue_previous_conversation" {
|
||||
type = bool
|
||||
description = "Whether to resume the previous conversation."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "interaction_mode" {
|
||||
type = string
|
||||
description = "Interaction mode with the Auggie CLI. Options: interactive, print, quiet, compact. https://docs.augmentcode.com/cli/reference#cli-flags"
|
||||
default = "interactive"
|
||||
validation {
|
||||
condition = contains(["interactive", "print", "quiet", "compact"], var.interaction_mode)
|
||||
error_message = "interaction_mode must be one of: interactive, print, quiet, compact."
|
||||
}
|
||||
}
|
||||
|
||||
variable "augment_session_token" {
|
||||
type = string
|
||||
description = "Auggie session token for authentication. https://docs.augmentcode.com/cli/setup-auggie/authentication"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "auggie_model" {
|
||||
type = string
|
||||
description = "The model to use for Auggie, find available models using auggie --list-models"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI via AgentAPI"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Auggie"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app"
|
||||
default = "Auggie"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app"
|
||||
default = "Auggie CLI"
|
||||
}
|
||||
|
||||
resource "coder_env" "auggie_session_auth" {
|
||||
agent_id = var.agent_id
|
||||
name = "AUGMENT_SESSION_AUTH"
|
||||
value = var.augment_session_token
|
||||
}
|
||||
|
||||
locals {
|
||||
app_slug = "auggie"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".auggie-module"
|
||||
}
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
cli_app = var.cli_app
|
||||
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
|
||||
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
start_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
ARG_AUGGIE_START_DIRECTORY='${var.folder}' \
|
||||
ARG_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_MCP_FILES='${jsonencode(var.mcp_files)}' \
|
||||
ARG_AUGGIE_RULES='${base64encode(var.rules)}' \
|
||||
ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION='${var.continue_previous_conversation}' \
|
||||
ARG_AUGGIE_INTERACTION_MODE='${var.interaction_mode}' \
|
||||
ARG_AUGMENT_SESSION_AUTH='${var.augment_session_token}' \
|
||||
ARG_AUGGIE_MODEL='${var.auggie_model}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
ARG_AUGGIE_INSTALL='${var.install_auggie}' \
|
||||
ARG_AUGGIE_VERSION='${var.auggie_version}' \
|
||||
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_AUGGIE_RULES='${base64encode(var.rules)}' \
|
||||
ARG_MCP_CONFIG='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
ARG_AUGGIE_INSTALL=${ARG_AUGGIE_INSTALL:-true}
|
||||
ARG_AUGGIE_VERSION=${ARG_AUGGIE_VERSION:-}
|
||||
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
|
||||
ARG_AUGGIE_RULES=$(echo -n "${ARG_AUGGIE_RULES:-}" | base64 -d)
|
||||
ARG_MCP_CONFIG=${ARG_MCP_CONFIG:-}
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
printf "install auggie: %s\n" "$ARG_AUGGIE_INSTALL"
|
||||
printf "auggie_version: %s\n" "$ARG_AUGGIE_VERSION"
|
||||
printf "app_slug: %s\n" "$ARG_MCP_APP_STATUS_SLUG"
|
||||
printf "rules: %s\n" "$ARG_AUGGIE_RULES"
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
function check_dependencies() {
|
||||
if ! command_exists node; then
|
||||
printf "Error: Node.js is not installed. Please install Node.js manually or use the pre_install_script to install it.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command_exists npm; then
|
||||
printf "Error: npm is not installed. Please install npm manually or use the pre_install_script to install it.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "Node.js version: %s\n" "$(node --version)"
|
||||
printf "npm version: %s\n" "$(npm --version)"
|
||||
}
|
||||
|
||||
function install_auggie() {
|
||||
if [ "${ARG_AUGGIE_INSTALL}" = "true" ]; then
|
||||
check_dependencies
|
||||
|
||||
printf "%s Installing Auggie CLI\n" "${BOLD}"
|
||||
|
||||
NPM_GLOBAL_PREFIX="${HOME}/.npm-global"
|
||||
if [ ! -d "$NPM_GLOBAL_PREFIX" ]; then
|
||||
mkdir -p "$NPM_GLOBAL_PREFIX"
|
||||
fi
|
||||
|
||||
npm config set prefix "$NPM_GLOBAL_PREFIX"
|
||||
|
||||
export PATH="$NPM_GLOBAL_PREFIX/bin:$PATH"
|
||||
|
||||
if [ -n "$ARG_AUGGIE_VERSION" ]; then
|
||||
npm install -g "@augmentcode/auggie@$ARG_AUGGIE_VERSION"
|
||||
else
|
||||
npm install -g "@augmentcode/auggie"
|
||||
fi
|
||||
|
||||
if ! grep -q "export PATH=\"\$HOME/.npm-global/bin:\$PATH\"" "$HOME/.bashrc"; then
|
||||
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> "$HOME/.bashrc"
|
||||
fi
|
||||
|
||||
printf "%s Successfully installed Auggie CLI. Version: %s\n" "${BOLD}" "$(auggie --version)"
|
||||
else
|
||||
printf "Skipping Auggie CLI installation (install_auggie=false)\n"
|
||||
fi
|
||||
}
|
||||
|
||||
function create_coder_mcp() {
|
||||
AUGGIE_CODER_MCP_FILE="$HOME/.augment/coder_mcp.json"
|
||||
CODER_MCP=$(
|
||||
cat << EOF
|
||||
{
|
||||
"mcpServers":{
|
||||
"coder": {
|
||||
"args": ["exp", "mcp", "server"],
|
||||
"command": "coder",
|
||||
"env": {
|
||||
"CODER_MCP_APP_STATUS_SLUG": "${ARG_MCP_APP_STATUS_SLUG}",
|
||||
"CODER_MCP_AI_AGENTAPI_URL": "http://localhost:3284",
|
||||
"CODER_AGENT_URL": "${CODER_AGENT_URL:-}",
|
||||
"CODER_AGENT_TOKEN": "${CODER_AGENT_TOKEN:-}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
mkdir -p "$(dirname "$AUGGIE_CODER_MCP_FILE")"
|
||||
echo "$CODER_MCP" > "$AUGGIE_CODER_MCP_FILE"
|
||||
printf "Coder MCP config created at: %s\n" "$AUGGIE_CODER_MCP_FILE"
|
||||
}
|
||||
|
||||
function create_user_mcp() {
|
||||
if [ -n "$ARG_MCP_CONFIG" ]; then
|
||||
USER_MCP_CONFIG_FILE="$HOME/.augment/user_mcp.json"
|
||||
USER_MCP_CONTENT=$(echo -n "$ARG_MCP_CONFIG" | base64 -d)
|
||||
mkdir -p "$(dirname "$USER_MCP_CONFIG_FILE")"
|
||||
echo "$USER_MCP_CONTENT" > "$USER_MCP_CONFIG_FILE"
|
||||
printf "User MCP config created at: %s\n" "$USER_MCP_CONFIG_FILE"
|
||||
else
|
||||
printf "No user MCP config provided, skipping user MCP config creation.\n"
|
||||
fi
|
||||
}
|
||||
|
||||
function create_rules_file() {
|
||||
AUGGIE_RULES_FILE="$HOME/.augment/rules.md"
|
||||
if [ -n "$ARG_AUGGIE_RULES" ]; then
|
||||
mkdir -p "$(dirname "$AUGGIE_RULES_FILE")"
|
||||
echo -n "$ARG_AUGGIE_RULES" > "$AUGGIE_RULES_FILE"
|
||||
printf "Rules file created at: %s\n" "$AUGGIE_RULES_FILE"
|
||||
else
|
||||
printf "No rules provided, skipping rules file creation.\n"
|
||||
fi
|
||||
}
|
||||
|
||||
install_auggie
|
||||
create_coder_mcp
|
||||
create_user_mcp
|
||||
create_rules_file
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
if [ -f "$HOME/.nvm/nvm.sh" ]; then
|
||||
source "$HOME"/.nvm/nvm.sh
|
||||
else
|
||||
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||
fi
|
||||
|
||||
ARG_AUGGIE_START_DIRECTORY=${ARG_AUGGIE_START_DIRECTORY:-"$HOME"}
|
||||
ARG_TASK_PROMPT=$(echo -n "${ARG_TASK_PROMPT:-}" | base64 -d)
|
||||
ARG_MCP_FILES=${ARG_MCP_FILES:-[]}
|
||||
ARG_AUGGIE_RULES=${ARG_AUGGIE_RULES:-}
|
||||
ARG_AUGMENT_SESSION_AUTH=${ARG_AUGMENT_SESSION_AUTH:-}
|
||||
ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION=${ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION:-false}
|
||||
ARG_AUGGIE_INTERACTION_MODE=${ARG_AUGGIE_INTERACTION_MODE:-"interactive"}
|
||||
ARG_AUGGIE_MODEL=${ARG_AUGGIE_MODEL:-}
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-false}
|
||||
|
||||
ARGS=()
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
printf "auggie_start_directory: %s\n" "$ARG_AUGGIE_START_DIRECTORY"
|
||||
printf "task_prompt: %s\n" "$ARG_TASK_PROMPT"
|
||||
printf "mcp_files: %s\n" "$ARG_MCP_FILES"
|
||||
printf "auggie_rules: %s\n" "$ARG_AUGGIE_RULES"
|
||||
printf "continue_previous_conversation: %s\n" "$ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION"
|
||||
printf "auggie_interaction_mode: %s\n" "$ARG_AUGGIE_INTERACTION_MODE"
|
||||
printf "augment_session_auth: %s\n" "$ARG_AUGMENT_SESSION_AUTH"
|
||||
printf "auggie_model: %s\n" "$ARG_AUGGIE_MODEL"
|
||||
printf "report_tasks: %s\n" "$ARG_REPORT_TASKS"
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
function validate_auggie_installation() {
|
||||
if command_exists auggie; then
|
||||
printf "Auggie is installed\n"
|
||||
else
|
||||
printf "Error: Auggie is not installed. Please enable install_auggie or install it manually\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function build_auggie_args() {
|
||||
if [ -n "$ARG_AUGGIE_INTERACTION_MODE" ]; then
|
||||
if [ "$ARG_AUGGIE_INTERACTION_MODE" != "interactive" ]; then
|
||||
ARGS+=(--"$ARG_AUGGIE_INTERACTION_MODE")
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_AUGGIE_MODEL" ]; then
|
||||
ARGS+=(--model "$ARG_AUGGIE_MODEL")
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.augment/user_mcp.json" ]; then
|
||||
ARGS+=(--mcp-config "$HOME/.augment/user_mcp.json")
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_MCP_FILES" ] && [ "$ARG_MCP_FILES" != "[]" ]; then
|
||||
for file in $(echo "$ARG_MCP_FILES" | jq -r '.[]'); do
|
||||
ARGS+=(--mcp-config "$file")
|
||||
done
|
||||
fi
|
||||
|
||||
ARGS+=(--mcp-config "$HOME/.augment/coder_mcp.json")
|
||||
|
||||
if [ -n "$ARG_AUGGIE_RULES" ]; then
|
||||
AUGGIE_RULES_FILE="$HOME/.augment/rules.md"
|
||||
ARGS+=(--rules "$AUGGIE_RULES_FILE")
|
||||
fi
|
||||
|
||||
if [ "$ARG_AUGGIE_CONTINUE_PREVIOUS_CONVERSATION" == "true" ]; then
|
||||
ARGS+=(--continue)
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_TASK_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" == "true" ]; then
|
||||
PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_TASK_PROMPT"
|
||||
else
|
||||
PROMPT="$ARG_TASK_PROMPT"
|
||||
fi
|
||||
ARGS+=(--instruction "$PROMPT")
|
||||
fi
|
||||
}
|
||||
|
||||
function start_agentapi_server() {
|
||||
mkdir -p "$ARG_AUGGIE_START_DIRECTORY"
|
||||
cd "$ARG_AUGGIE_START_DIRECTORY"
|
||||
ARGS+=(--workspace-root "$ARG_AUGGIE_START_DIRECTORY")
|
||||
printf "Running auggie with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
|
||||
agentapi server --term-width 67 --term-height 1190 -- auggie "${ARGS[@]}"
|
||||
}
|
||||
|
||||
validate_auggie_installation
|
||||
build_auggie_args
|
||||
start_agentapi_server
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "HELLO: $(bash -c env)"
|
||||
echo "auggie version v1.0.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
while true; do
|
||||
echo "$(date) - auggie-mock"
|
||||
sleep 15
|
||||
done
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
display_name: Codex CLI
|
||||
icon: ../../../../.icons/openai.svg
|
||||
description: Run Codex CLI in your workspace with AgentAPI integration
|
||||
verified: true
|
||||
tags: [agent, codex, ai, openai, tasks]
|
||||
---
|
||||
|
||||
# Codex CLI
|
||||
|
||||
Run Codex CLI in your workspace to access OpenAI's models through the Codex interface, with custom pre/post install scripts. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for Coder Tasks compatibility.
|
||||
|
||||
```tf
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = var.openai_api_key
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder/coder-login) module to your template
|
||||
- OpenAI API key for Codex access
|
||||
|
||||
## Examples
|
||||
|
||||
### Run standalone
|
||||
|
||||
```tf
|
||||
module "codex" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = "..."
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
### Tasks integration
|
||||
|
||||
```tf
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Initial prompt for the Codex CLI"
|
||||
mutable = true
|
||||
}
|
||||
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/coder-login/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = "..."
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
folder = "/home/coder/project"
|
||||
|
||||
# Custom configuration for full auto mode
|
||||
base_config_toml = <<-EOT
|
||||
approval_policy = "never"
|
||||
preferred_auth_method = "apikey"
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> This module configures Codex with a `workspace-write` sandbox that allows AI tasks to read/write files in the specified folder. While the sandbox provides security boundaries, Codex can still modify files within the workspace. Use this module _only_ in trusted environments and be aware of the security implications.
|
||||
|
||||
## How it Works
|
||||
|
||||
- **Install**: The module installs Codex CLI and sets up the environment
|
||||
- **System Prompt**: If `codex_system_prompt` is set, writes the prompt to `AGENTS.md` in the `~/.codex/` directory
|
||||
- **Start**: Launches Codex CLI in the specified directory, wrapped by AgentAPI
|
||||
- **Configuration**: Sets `OPENAI_API_KEY` environment variable and passes `--model` flag to Codex CLI (if variables provided)
|
||||
|
||||
## Configuration
|
||||
|
||||
### Default Configuration
|
||||
|
||||
When no custom `base_config_toml` is provided, the module uses these secure defaults:
|
||||
|
||||
```toml
|
||||
sandbox_mode = "workspace-write"
|
||||
approval_policy = "never"
|
||||
preferred_auth_method = "apikey"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
network_access = true
|
||||
```
|
||||
|
||||
### Custom Configuration
|
||||
|
||||
For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_servers`:
|
||||
|
||||
```tf
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.0.0"
|
||||
# ... other variables ...
|
||||
|
||||
# Override default configuration
|
||||
base_config_toml = <<-EOT
|
||||
sandbox_mode = "danger-full-access"
|
||||
approval_policy = "never"
|
||||
preferred_auth_method = "apikey"
|
||||
EOT
|
||||
|
||||
# Add extra MCP servers
|
||||
additional_mcp_servers = <<-EOT
|
||||
[mcp_servers.GitHub]
|
||||
command = "npx"
|
||||
args = ["-y", "@modelcontextprotocol/server-github"]
|
||||
type = "stdio"
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If no custom configuration is provided, the module uses secure defaults. The Coder MCP server is always included automatically. For containerized workspaces (Docker/Kubernetes), you may need `sandbox_mode = "danger-full-access"` to avoid permission issues. For advanced options, see [Codex config docs](https://github.com/openai/codex/blob/main/codex-rs/config.md).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Check installation and startup logs in `~/.codex-module/`
|
||||
- Ensure your OpenAI API key has access to the specified model
|
||||
|
||||
> [!IMPORTANT]
|
||||
> To use tasks with Codex CLI, ensure you have the `openai_api_key` variable set, and **you create a `coder_parameter` named `"AI Prompt"` and pass its value to the codex module's `ai_prompt` variable**. [Tasks Template Example](https://registry.coder.com/templates/coder-labs/tasks-docker).
|
||||
> The module automatically configures Codex with your API key and model preferences.
|
||||
> folder is a required variable for the module to function correctly.
|
||||
|
||||
## References
|
||||
|
||||
- [Codex CLI Documentation](https://github.com/openai/codex)
|
||||
- [AgentAPI Documentation](https://github.com/coder/agentapi)
|
||||
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
|
||||
@@ -0,0 +1,371 @@
|
||||
import {
|
||||
test,
|
||||
afterEach,
|
||||
describe,
|
||||
setDefaultTimeout,
|
||||
beforeAll,
|
||||
expect,
|
||||
} from "bun:test";
|
||||
import { execContainer, readFileContainer, runTerraformInit } from "~test";
|
||||
import {
|
||||
loadTestFile,
|
||||
writeExecutable,
|
||||
setup as setupUtil,
|
||||
execModuleScript,
|
||||
expectAgentAPIStarted,
|
||||
} from "../../../coder/modules/agentapi/test-util";
|
||||
import dedent from "dedent";
|
||||
|
||||
let cleanupFunctions: (() => Promise<void>)[] = [];
|
||||
const registerCleanup = (cleanup: () => Promise<void>) => {
|
||||
cleanupFunctions.push(cleanup);
|
||||
};
|
||||
afterEach(async () => {
|
||||
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
|
||||
cleanupFunctions = [];
|
||||
for (const cleanup of cleanupFnsCopy) {
|
||||
try {
|
||||
await cleanup();
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
interface SetupProps {
|
||||
skipAgentAPIMock?: boolean;
|
||||
skipCodexMock?: boolean;
|
||||
moduleVariables?: Record<string, string>;
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const projectDir = "/home/coder/project";
|
||||
const { id } = await setupUtil({
|
||||
moduleDir: import.meta.dir,
|
||||
moduleVariables: {
|
||||
install_codex: props?.skipCodexMock ? "true" : "false",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
codex_model: "gpt-4-turbo",
|
||||
folder: "/home/coder",
|
||||
...props?.moduleVariables,
|
||||
},
|
||||
registerCleanup,
|
||||
projectDir,
|
||||
skipAgentAPIMock: props?.skipAgentAPIMock,
|
||||
agentapiMockScript: props?.agentapiMockScript,
|
||||
});
|
||||
if (!props?.skipCodexMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/codex",
|
||||
content: await loadTestFile(import.meta.dir, "codex-mock.sh"),
|
||||
});
|
||||
}
|
||||
return { id };
|
||||
};
|
||||
|
||||
setDefaultTimeout(60 * 1000);
|
||||
|
||||
describe("codex", async () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
test("happy-path", async () => {
|
||||
const { id } = await setup();
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("install-codex-version", async () => {
|
||||
const version_to_install = "0.10.0";
|
||||
const { id } = await setup({
|
||||
skipCodexMock: true,
|
||||
moduleVariables: {
|
||||
install_codex: "true",
|
||||
codex_version: version_to_install,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat /home/coder/.codex-module/install.log`,
|
||||
]);
|
||||
expect(resp.stdout).toContain(version_to_install);
|
||||
});
|
||||
|
||||
test("check-latest-codex-version-works", async () => {
|
||||
const { id } = await setup({
|
||||
skipCodexMock: true,
|
||||
skipAgentAPIMock: true,
|
||||
moduleVariables: {
|
||||
install_codex: "true",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("base-config-toml", async () => {
|
||||
const baseConfig = dedent`
|
||||
sandbox_mode = "danger-full-access"
|
||||
approval_policy = "never"
|
||||
preferred_auth_method = "apikey"
|
||||
|
||||
[custom_section]
|
||||
new_feature = true
|
||||
`.trim();
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
base_config_toml: baseConfig,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
|
||||
expect(resp).toContain('sandbox_mode = "danger-full-access"');
|
||||
expect(resp).toContain('preferred_auth_method = "apikey"');
|
||||
expect(resp).toContain("[custom_section]");
|
||||
expect(resp).toContain("[mcp_servers.Coder]");
|
||||
});
|
||||
|
||||
test("codex-api-key", async () => {
|
||||
const apiKey = "test-api-key-123";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
openai_api_key: apiKey,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.codex-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain("OpenAI API Key: Provided");
|
||||
});
|
||||
|
||||
test("pre-post-install-scripts", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
|
||||
post_install_script: "#!/bin/bash\necho 'post-install-script'",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const preInstallLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.codex-module/pre_install.log",
|
||||
);
|
||||
expect(preInstallLog).toContain("pre-install-script");
|
||||
const postInstallLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.codex-module/post_install.log",
|
||||
);
|
||||
expect(postInstallLog).toContain("post-install-script");
|
||||
});
|
||||
|
||||
test("folder-variable", async () => {
|
||||
const folder = "/tmp/codex-test-folder";
|
||||
const { id } = await setup({
|
||||
skipCodexMock: false,
|
||||
moduleVariables: {
|
||||
folder,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.codex-module/install.log",
|
||||
);
|
||||
expect(resp).toContain(folder);
|
||||
});
|
||||
|
||||
test("additional-mcp-servers", async () => {
|
||||
const additional = dedent`
|
||||
[mcp_servers.GitHub]
|
||||
command = "npx"
|
||||
args = ["-y", "@modelcontextprotocol/server-github"]
|
||||
type = "stdio"
|
||||
description = "GitHub integration"
|
||||
|
||||
[mcp_servers.FileSystem]
|
||||
command = "npx"
|
||||
args = ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
|
||||
type = "stdio"
|
||||
description = "File system access"
|
||||
`.trim();
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
additional_mcp_servers: additional,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
|
||||
expect(resp).toContain("[mcp_servers.GitHub]");
|
||||
expect(resp).toContain("[mcp_servers.FileSystem]");
|
||||
expect(resp).toContain("[mcp_servers.Coder]");
|
||||
expect(resp).toContain("GitHub integration");
|
||||
});
|
||||
|
||||
test("full-custom-config", async () => {
|
||||
const baseConfig = dedent`
|
||||
sandbox_mode = "read-only"
|
||||
approval_policy = "untrusted"
|
||||
preferred_auth_method = "chatgpt"
|
||||
custom_setting = "test-value"
|
||||
|
||||
[advanced_settings]
|
||||
timeout = 30000
|
||||
debug = true
|
||||
logging_level = "verbose"
|
||||
`.trim();
|
||||
|
||||
const additionalMCP = dedent`
|
||||
[mcp_servers.CustomTool]
|
||||
command = "/usr/local/bin/custom-tool"
|
||||
args = ["--serve", "--port", "8080"]
|
||||
type = "stdio"
|
||||
description = "Custom development tool"
|
||||
|
||||
[mcp_servers.DatabaseMCP]
|
||||
command = "python"
|
||||
args = ["-m", "database_mcp_server"]
|
||||
type = "stdio"
|
||||
description = "Database query interface"
|
||||
`.trim();
|
||||
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
base_config_toml: baseConfig,
|
||||
additional_mcp_servers: additionalMCP,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
|
||||
|
||||
// Check base config
|
||||
expect(resp).toContain('sandbox_mode = "read-only"');
|
||||
expect(resp).toContain('preferred_auth_method = "chatgpt"');
|
||||
expect(resp).toContain('custom_setting = "test-value"');
|
||||
expect(resp).toContain("[advanced_settings]");
|
||||
expect(resp).toContain('logging_level = "verbose"');
|
||||
|
||||
// Check MCP servers
|
||||
expect(resp).toContain("[mcp_servers.Coder]");
|
||||
expect(resp).toContain("[mcp_servers.CustomTool]");
|
||||
expect(resp).toContain("[mcp_servers.DatabaseMCP]");
|
||||
expect(resp).toContain("Custom development tool");
|
||||
expect(resp).toContain("Database query interface");
|
||||
});
|
||||
|
||||
test("minimal-default-config", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
// No base_config_toml or additional_mcp_servers - should use defaults
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
|
||||
|
||||
// Check default base config
|
||||
expect(resp).toContain('sandbox_mode = "workspace-write"');
|
||||
expect(resp).toContain('approval_policy = "never"');
|
||||
expect(resp).toContain("[sandbox_workspace_write]");
|
||||
expect(resp).toContain("network_access = true");
|
||||
|
||||
// Check only Coder MCP server is present
|
||||
expect(resp).toContain("[mcp_servers.Coder]");
|
||||
expect(resp).toContain("Report ALL tasks and statuses");
|
||||
|
||||
// Ensure no additional MCP servers
|
||||
const mcpServerCount = (resp.match(/\[mcp_servers\./g) || []).length;
|
||||
expect(mcpServerCount).toBe(1);
|
||||
});
|
||||
|
||||
test("codex-system-prompt", async () => {
|
||||
const prompt = "This is a system prompt for Codex.";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
codex_system_prompt: prompt,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(id, "/home/coder/.codex/AGENTS.md");
|
||||
expect(resp).toContain(prompt);
|
||||
});
|
||||
|
||||
test("codex-system-prompt-skip-append-if-exists", async () => {
|
||||
const prompt_1 = "This is a system prompt for Codex.";
|
||||
const prompt_2 = "This is a system prompt for Goose.";
|
||||
const prompt_3 = dedent`
|
||||
This is a system prompt for Codex.
|
||||
This is a system prompt for Gemini.
|
||||
`.trim();
|
||||
const pre_install_script = dedent`
|
||||
#!/bin/bash
|
||||
mkdir -p /home/coder/.codex
|
||||
echo -e "${prompt_3}" >> /home/coder/.codex/AGENTS.md
|
||||
`.trim();
|
||||
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
pre_install_script,
|
||||
codex_system_prompt: prompt_2,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(id, "/home/coder/.codex/AGENTS.md");
|
||||
expect(resp).toContain(prompt_1);
|
||||
expect(resp).toContain(prompt_2);
|
||||
|
||||
// Re-run with a prompt that already exists, it should not append again
|
||||
const { id: id_2 } = await setup({
|
||||
moduleVariables: {
|
||||
pre_install_script,
|
||||
codex_system_prompt: prompt_1,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id_2);
|
||||
const resp_2 = await readFileContainer(
|
||||
id_2,
|
||||
"/home/coder/.codex/AGENTS.md",
|
||||
);
|
||||
expect(resp_2).toContain(prompt_1);
|
||||
const count = (resp_2.match(new RegExp(prompt_1, "g")) || []).length;
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
test("codex-ai-task-prompt", async () => {
|
||||
const prompt = "This is a system prompt for Codex.";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
ai_prompt: prompt,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat /home/coder/.codex-module/agentapi-start.log`,
|
||||
]);
|
||||
expect(resp.stdout).toContain(prompt);
|
||||
});
|
||||
|
||||
test("start-without-prompt", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
codex_system_prompt: "", // Explicitly disable system prompt
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const prompt = await execContainer(id, [
|
||||
"ls",
|
||||
"-l",
|
||||
"/home/coder/.codex/AGENTS.md",
|
||||
]);
|
||||
expect(prompt.exitCode).not.toBe(0);
|
||||
expect(prompt.stderr).toContain("No such file or directory");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
default = "/icon/openai.svg"
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to run Codex in."
|
||||
}
|
||||
|
||||
variable "install_codex" {
|
||||
type = bool
|
||||
description = "Whether to install Codex."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "codex_version" {
|
||||
type = string
|
||||
description = "The version of Codex to install."
|
||||
default = "" # empty string means the latest available version
|
||||
}
|
||||
|
||||
variable "base_config_toml" {
|
||||
type = string
|
||||
description = "Complete base TOML configuration for Codex (without mcp_servers section). If empty, uses minimal default configuration with workspace-write sandbox mode and never approval policy. For advanced options, see https://github.com/openai/codex/blob/main/codex-rs/config.md"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "additional_mcp_servers" {
|
||||
type = string
|
||||
description = "Additional MCP servers configuration in TOML format. These will be merged with the required Coder MCP server in the [mcp_servers] section."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "openai_api_key" {
|
||||
type = string
|
||||
description = "OpenAI API key for Codex CLI"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.5.0"
|
||||
}
|
||||
|
||||
variable "codex_model" {
|
||||
type = string
|
||||
description = "The model for Codex to use. Defaults to gpt-5."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Codex."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Codex."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "Initial task prompt for Codex CLI when launched via Tasks"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "codex_system_prompt" {
|
||||
type = string
|
||||
description = "System instructions written to AGENTS.md in the ~/.codex directory"
|
||||
default = "You are a helpful coding assistant. Start every response with `Codex says:`"
|
||||
}
|
||||
|
||||
resource "coder_env" "openai_api_key" {
|
||||
agent_id = var.agent_id
|
||||
name = "OPENAI_API_KEY"
|
||||
value = var.openai_api_key
|
||||
}
|
||||
|
||||
locals {
|
||||
app_slug = "codex"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".codex-module"
|
||||
}
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = "Codex"
|
||||
cli_app_slug = "${local.app_slug}-cli"
|
||||
cli_app_display_name = "Codex CLI"
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
start_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
ARG_OPENAI_API_KEY='${var.openai_api_key}' \
|
||||
ARG_CODEX_MODEL='${var.codex_model}' \
|
||||
ARG_CODEX_START_DIRECTORY='${var.folder}' \
|
||||
ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
ARG_INSTALL='${var.install_codex}' \
|
||||
ARG_CODEX_VERSION='${var.codex_version}' \
|
||||
ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \
|
||||
ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \
|
||||
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_CODEX_START_DIRECTORY='${var.folder}' \
|
||||
ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/bin/bash
|
||||
source "$HOME"/.bashrc
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
ARG_BASE_CONFIG_TOML=$(echo -n "$ARG_BASE_CONFIG_TOML" | base64 -d)
|
||||
ARG_ADDITIONAL_MCP_SERVERS=$(echo -n "$ARG_ADDITIONAL_MCP_SERVERS" | base64 -d)
|
||||
ARG_CODEX_INSTRUCTION_PROMPT=$(echo -n "$ARG_CODEX_INSTRUCTION_PROMPT" | base64 -d)
|
||||
|
||||
echo "=== Codex Module Configuration ==="
|
||||
printf "Install Codex: %s\n" "$ARG_INSTALL"
|
||||
printf "Codex Version: %s\n" "$ARG_CODEX_VERSION"
|
||||
printf "App Slug: %s\n" "$ARG_CODER_MCP_APP_STATUS_SLUG"
|
||||
printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
|
||||
printf "Has Base Config: %s\n" "$([ -n "$ARG_BASE_CONFIG_TOML" ] && echo "Yes" || echo "No")"
|
||||
printf "Has Additional MCP: %s\n" "$([ -n "$ARG_ADDITIONAL_MCP_SERVERS" ] && echo "Yes" || echo "No")"
|
||||
printf "Has System Prompt: %s\n" "$([ -n "$ARG_CODEX_INSTRUCTION_PROMPT" ] && echo "Yes" || echo "No")"
|
||||
echo "======================================"
|
||||
|
||||
set +o nounset
|
||||
|
||||
function install_node() {
|
||||
if ! command_exists npm; then
|
||||
printf "npm not found, checking for Node.js installation...\n"
|
||||
if ! command_exists node; then
|
||||
printf "Node.js not found, installing Node.js via NVM...\n"
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
if [ ! -d "$NVM_DIR" ]; then
|
||||
mkdir -p "$NVM_DIR"
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
else
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
fi
|
||||
|
||||
nvm install --lts
|
||||
nvm use --lts
|
||||
nvm alias default node
|
||||
|
||||
printf "Node.js installed: %s\n" "$(node --version)"
|
||||
printf "npm installed: %s\n" "$(npm --version)"
|
||||
else
|
||||
printf "Node.js is installed but npm is not available. Please install npm manually.\n"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function install_codex() {
|
||||
if [ "${ARG_INSTALL}" = "true" ]; then
|
||||
install_node
|
||||
|
||||
if ! command_exists nvm; then
|
||||
printf "which node: %s\n" "$(which node)"
|
||||
printf "which npm: %s\n" "$(which npm)"
|
||||
|
||||
mkdir -p "$HOME"/.npm-global
|
||||
|
||||
npm config set prefix "$HOME/.npm-global"
|
||||
|
||||
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||
|
||||
if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then
|
||||
echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "%s Installing Codex CLI\n" "${BOLD}"
|
||||
|
||||
if [ -n "$ARG_CODEX_VERSION" ]; then
|
||||
npm install -g "@openai/codex@$ARG_CODEX_VERSION"
|
||||
else
|
||||
npm install -g "@openai/codex"
|
||||
fi
|
||||
printf "%s Successfully installed Codex CLI. Version: %s\n" "${BOLD}" "$(codex --version)"
|
||||
fi
|
||||
}
|
||||
|
||||
write_minimal_default_config() {
|
||||
local config_path="$1"
|
||||
cat << EOF > "$config_path"
|
||||
# Minimal Default Codex Configuration
|
||||
sandbox_mode = "workspace-write"
|
||||
approval_policy = "never"
|
||||
preferred_auth_method = "apikey"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
network_access = true
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
append_mcp_servers_section() {
|
||||
local config_path="$1"
|
||||
|
||||
cat << EOF >> "$config_path"
|
||||
|
||||
# MCP Servers Configuration
|
||||
[mcp_servers.Coder]
|
||||
command = "coder"
|
||||
args = ["exp", "mcp", "server"]
|
||||
env = { "CODER_MCP_APP_STATUS_SLUG" = "${ARG_CODER_MCP_APP_STATUS_SLUG}", "CODER_MCP_AI_AGENTAPI_URL" = "http://localhost:3284", "CODER_AGENT_URL" = "${CODER_AGENT_URL}", "CODER_AGENT_TOKEN" = "${CODER_AGENT_TOKEN}" }
|
||||
description = "Report ALL tasks and statuses (in progress, done, failed) you are working on."
|
||||
type = "stdio"
|
||||
|
||||
EOF
|
||||
|
||||
if [ -n "$ARG_ADDITIONAL_MCP_SERVERS" ]; then
|
||||
printf "Adding additional MCP servers\n"
|
||||
echo "$ARG_ADDITIONAL_MCP_SERVERS" >> "$config_path"
|
||||
fi
|
||||
}
|
||||
|
||||
function populate_config_toml() {
|
||||
CONFIG_PATH="$HOME/.codex/config.toml"
|
||||
mkdir -p "$(dirname "$CONFIG_PATH")"
|
||||
|
||||
if [ -n "$ARG_BASE_CONFIG_TOML" ]; then
|
||||
printf "Using provided base configuration\n"
|
||||
echo "$ARG_BASE_CONFIG_TOML" > "$CONFIG_PATH"
|
||||
else
|
||||
printf "Using minimal default configuration\n"
|
||||
write_minimal_default_config "$CONFIG_PATH"
|
||||
fi
|
||||
|
||||
append_mcp_servers_section "$CONFIG_PATH"
|
||||
}
|
||||
|
||||
function add_instruction_prompt_if_exists() {
|
||||
if [ -n "${ARG_CODEX_INSTRUCTION_PROMPT:-}" ]; then
|
||||
AGENTS_PATH="$HOME/.codex/AGENTS.md"
|
||||
printf "Creating AGENTS.md in .codex directory: %s\\n" "${AGENTS_PATH}"
|
||||
|
||||
mkdir -p "$HOME/.codex"
|
||||
|
||||
if [ -f "${AGENTS_PATH}" ] && grep -Fq "${ARG_CODEX_INSTRUCTION_PROMPT}" "${AGENTS_PATH}"; then
|
||||
printf "AGENTS.md already contains the instruction prompt. Skipping append.\n"
|
||||
else
|
||||
printf "Appending instruction prompt to AGENTS.md in .codex directory\n"
|
||||
echo -e "\n${ARG_CODEX_INSTRUCTION_PROMPT}" >> "${AGENTS_PATH}"
|
||||
fi
|
||||
|
||||
if [ ! -d "${ARG_CODEX_START_DIRECTORY}" ]; then
|
||||
printf "Creating start directory '%s'\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
|
||||
printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
else
|
||||
printf "AGENTS.md instruction prompt is not set.\n"
|
||||
fi
|
||||
}
|
||||
|
||||
install_codex
|
||||
codex --version
|
||||
populate_config_toml
|
||||
add_instruction_prompt_if_exists
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
if [ -f "$HOME/.nvm/nvm.sh" ]; then
|
||||
source "$HOME"/.nvm/nvm.sh
|
||||
else
|
||||
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||
fi
|
||||
|
||||
printf "Version: %s\n" "$(codex --version)"
|
||||
set -o nounset
|
||||
ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d)
|
||||
|
||||
echo "=== Codex Launch Configuration ==="
|
||||
printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")"
|
||||
printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}"
|
||||
printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
|
||||
printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")"
|
||||
echo "======================================"
|
||||
set +o nounset
|
||||
CODEX_ARGS=()
|
||||
|
||||
if command_exists codex; then
|
||||
printf "Codex is installed\n"
|
||||
else
|
||||
printf "Error: Codex is not installed. Please enable install_codex or install it manually\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then
|
||||
printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
cd "${ARG_CODEX_START_DIRECTORY}" || {
|
||||
printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
|
||||
printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
cd "${ARG_CODEX_START_DIRECTORY}" || {
|
||||
printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_CODEX_MODEL" ]; then
|
||||
CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL")
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
|
||||
printf "Running the task prompt %s\n" "$ARG_CODEX_TASK_PROMPT"
|
||||
PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
|
||||
CODEX_ARGS+=("$PROMPT")
|
||||
else
|
||||
printf "No task prompt given.\n"
|
||||
fi
|
||||
|
||||
# Terminal dimensions optimized for Coder Tasks UI sidebar:
|
||||
# - Width 67: fits comfortably in sidebar
|
||||
# - Height 1190: adjusted due to Codex terminal height bug
|
||||
printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}"
|
||||
agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}"
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "HELLO: $(bash -c env)"
|
||||
echo "codex version v1.0.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
while true; do
|
||||
echo "$(date) - codex-mock"
|
||||
sleep 15
|
||||
done
|
||||
@@ -1,14 +1,25 @@
|
||||
---
|
||||
display_name: Cursor CLI
|
||||
icon: ../../../../.icons/cursor.svg
|
||||
description: Run Cursor CLI agent in your workspace (no AgentAPI)
|
||||
description: Run Cursor Agent CLI in your workspace for AI pair programming
|
||||
verified: true
|
||||
tags: [agent, cursor, ai, cli]
|
||||
tags: [agent, cursor, ai, tasks]
|
||||
---
|
||||
|
||||
# Cursor CLI
|
||||
|
||||
Run the Cursor Coding Agent in your workspace using the Cursor CLI directly.
|
||||
Run the Cursor Agent CLI in your workspace for interactive coding assistance and automated task execution.
|
||||
|
||||
```tf
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
## Basic setup
|
||||
|
||||
A full example with MCP, rules, and pre/post install scripts:
|
||||
|
||||
@@ -31,7 +42,7 @@ module "coder-login" {
|
||||
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
@@ -40,6 +51,7 @@ module "cursor_cli" {
|
||||
force = true
|
||||
model = "gpt-5"
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
api_key = "xxxx-xxxx-xxxx" # Required while using tasks, see note below
|
||||
|
||||
# Minimal MCP server (writes `folder/.cursor/mcp.json`):
|
||||
mcp = jsonencode({
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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.0.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.0.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.0.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.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
folder = "/home/coder/project"
|
||||
|
||||
@@ -107,6 +107,18 @@ describe("gemini", async () => {
|
||||
expect(resp.stdout).toContain(version_to_install);
|
||||
});
|
||||
|
||||
test("install-gemini-latest", async () => {
|
||||
const { id } = await setup({
|
||||
skipGeminiMock: true,
|
||||
moduleVariables: {
|
||||
install_gemini: "true",
|
||||
gemini_version: "",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("gemini-settings-json", async () => {
|
||||
const settings = '{"foo": "bar"}';
|
||||
const { id } = await setup({
|
||||
@@ -115,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");
|
||||
});
|
||||
@@ -129,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");
|
||||
});
|
||||
|
||||
@@ -141,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 () => {
|
||||
@@ -154,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);
|
||||
});
|
||||
|
||||
@@ -166,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");
|
||||
});
|
||||
|
||||
@@ -181,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);
|
||||
});
|
||||
|
||||
@@ -193,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");
|
||||
});
|
||||
@@ -220,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");
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
display_name: Amp CLI
|
||||
icon: ../../../../.icons/sourcegraph-amp.svg
|
||||
description: Sourcegraph's AI coding agent with deep codebase understanding and intelligent code search capabilities
|
||||
verified: true
|
||||
tags: [agent, sourcegraph, amp, ai, tasks]
|
||||
---
|
||||
|
||||
# Sourcegraph Amp CLI
|
||||
|
||||
Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI-powered code search and analysis tools, with AgentAPI integration for seamless Coder Tasks support.
|
||||
|
||||
```tf
|
||||
module "amp-cli" {
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "1.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
|
||||
install_sourcegraph_amp = true
|
||||
agentapi_version = "latest"
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Include the [Coder Login](https://registry.coder.com/modules/coder-login/coder) module in your template
|
||||
- Node.js and npm are automatically installed (via NVM) if not already available
|
||||
|
||||
## Usage Example
|
||||
|
||||
```tf
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
name = "AI Prompt"
|
||||
description = "Write an initial prompt for Amp to work on."
|
||||
type = "string"
|
||||
default = ""
|
||||
mutable = true
|
||||
|
||||
}
|
||||
|
||||
# Set system prompt for Amp CLI via environment variables
|
||||
resource "coder_agent" "main" {
|
||||
# ...
|
||||
env = {
|
||||
SOURCEGRAPH_AMP_SYSTEM_PROMPT = <<-EOT
|
||||
You are an Amp assistant that helps developers debug and write code efficiently.
|
||||
|
||||
Always log task status to Coder.
|
||||
EOT
|
||||
SOURCEGRAPH_AMP_TASK_PROMPT = data.coder_parameter.ai_prompt.value
|
||||
}
|
||||
}
|
||||
|
||||
variable "sourcegraph_amp_api_key" {
|
||||
type = string
|
||||
description = "Sourcegraph Amp API key. Get one at https://ampcode.com/settings"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "amp-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "1.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key # recommended for authenticated usage
|
||||
install_sourcegraph_amp = true
|
||||
}
|
||||
```
|
||||
|
||||
## How it Works
|
||||
|
||||
- **Install**: Installs Sourcegraph Amp CLI using npm (installs Node.js via NVM if required)
|
||||
- **Start**: Launches Amp CLI in the specified directory, wrapped with AgentAPI to enable tasks and AI interactions
|
||||
- **Environment Variables**: Sets `SOURCEGRAPH_AMP_API_KEY` and `SOURCEGRAPH_AMP_START_DIRECTORY` for the CLI execution
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If `amp` is not found, ensure `install_sourcegraph_amp = true` and your API key is valid
|
||||
- Logs are written under `/home/coder/.sourcegraph-amp-module/` (`install.log`, `agentapi-start.log`) for debugging
|
||||
- If AgentAPI fails to start, verify that your container has network access and executable permissions for the scripts
|
||||
|
||||
> [!IMPORTANT]
|
||||
> For using **Coder Tasks** with Amp CLI, make sure to pass the `AI Prompt` parameter and set `sourcegraph_amp_api_key`.
|
||||
> This ensures task reporting and status updates work seamlessly.
|
||||
|
||||
## References
|
||||
|
||||
- [Amp CLI Documentation](https://ampcode.com/manual)
|
||||
- [AgentAPI Documentation](https://github.com/coder/agentapi)
|
||||
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
|
||||
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
test,
|
||||
afterEach,
|
||||
describe,
|
||||
setDefaultTimeout,
|
||||
beforeAll,
|
||||
expect,
|
||||
} from "bun:test";
|
||||
import { execContainer, readFileContainer, runTerraformInit } from "~test";
|
||||
import {
|
||||
loadTestFile,
|
||||
writeExecutable,
|
||||
setup as setupUtil,
|
||||
execModuleScript,
|
||||
expectAgentAPIStarted,
|
||||
} from "../../../coder/modules/agentapi/test-util";
|
||||
|
||||
let cleanupFunctions: (() => Promise<void>)[] = [];
|
||||
const registerCleanup = (cleanup: () => Promise<void>) => {
|
||||
cleanupFunctions.push(cleanup);
|
||||
};
|
||||
afterEach(async () => {
|
||||
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
|
||||
cleanupFunctions = [];
|
||||
for (const cleanup of cleanupFnsCopy) {
|
||||
try {
|
||||
await cleanup();
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
interface SetupProps {
|
||||
skipAgentAPIMock?: boolean;
|
||||
skipAmpMock?: boolean;
|
||||
moduleVariables?: Record<string, string>;
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const projectDir = "/home/coder/project";
|
||||
const { id } = await setupUtil({
|
||||
moduleDir: import.meta.dir,
|
||||
moduleVariables: {
|
||||
install_sourcegraph_amp: props?.skipAmpMock ? "true" : "false",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
sourcegraph_amp_model: "test-model",
|
||||
...props?.moduleVariables,
|
||||
},
|
||||
registerCleanup,
|
||||
projectDir,
|
||||
skipAgentAPIMock: props?.skipAgentAPIMock,
|
||||
agentapiMockScript: props?.agentapiMockScript,
|
||||
});
|
||||
|
||||
// Place the AMP mock CLI binary inside the container
|
||||
if (!props?.skipAmpMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/amp",
|
||||
content: await loadTestFile(`${import.meta.dir}`, "amp-mock.sh"),
|
||||
});
|
||||
}
|
||||
|
||||
return { id };
|
||||
};
|
||||
|
||||
setDefaultTimeout(60 * 1000);
|
||||
|
||||
describe("sourcegraph-amp", async () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
test("happy-path", async () => {
|
||||
const { id } = await setup();
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("api-key", async () => {
|
||||
const apiKey = "test-api-key-123";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
sourcegraph_amp_api_key: apiKey,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.sourcegraph-amp-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain("sourcegraph_amp_api_key provided !");
|
||||
});
|
||||
|
||||
test("custom-folder", async () => {
|
||||
const folder = "/tmp/sourcegraph-amp-test";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
folder,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.sourcegraph-amp-module/install.log",
|
||||
);
|
||||
expect(resp).toContain(folder);
|
||||
});
|
||||
|
||||
test("pre-post-install-scripts", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
|
||||
post_install_script: "#!/bin/bash\necho 'post-install-script'",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const preLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.sourcegraph-amp-module/pre_install.log",
|
||||
);
|
||||
expect(preLog).toContain("pre-install-script");
|
||||
const postLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.sourcegraph-amp-module/post_install.log",
|
||||
);
|
||||
expect(postLog).toContain("post-install-script");
|
||||
});
|
||||
|
||||
test("system-prompt", async () => {
|
||||
const prompt = "this is a system prompt for AMP";
|
||||
const { id } = await setup();
|
||||
await execModuleScript(id, {
|
||||
SOURCEGRAPH_AMP_SYSTEM_PROMPT: prompt,
|
||||
});
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.sourcegraph-amp-module/SYSTEM_PROMPT.md",
|
||||
);
|
||||
expect(resp).toContain(prompt);
|
||||
});
|
||||
|
||||
test("task-prompt", async () => {
|
||||
const prompt = "this is a task prompt for AMP";
|
||||
const { id } = await setup();
|
||||
await execModuleScript(id, {
|
||||
SOURCEGRAPH_AMP_TASK_PROMPT: prompt,
|
||||
});
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.sourcegraph-amp-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain(`sourcegraph amp task prompt provided : ${prompt}`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
default = "/icon/sourcegraph-amp.svg"
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to run sourcegraph_amp in."
|
||||
default = "/home/coder"
|
||||
}
|
||||
|
||||
variable "install_sourcegraph_amp" {
|
||||
type = bool
|
||||
description = "Whether to install sourcegraph-amp."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "sourcegraph_amp_api_key" {
|
||||
type = string
|
||||
description = "sourcegraph-amp API Key"
|
||||
default = ""
|
||||
}
|
||||
|
||||
resource "coder_env" "sourcegraph_amp_api_key" {
|
||||
agent_id = var.agent_id
|
||||
name = "SOURCEGRAPH_AMP_API_KEY"
|
||||
value = var.sourcegraph_amp_api_key
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.3.0"
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing sourcegraph_amp"
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing sourcegraph_amp."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "base_amp_config" {
|
||||
type = string
|
||||
description = <<-EOT
|
||||
Base AMP configuration in JSON format. Can be overridden to customize AMP settings.
|
||||
|
||||
If empty, defaults enable thinking and todos for autonomous operation. Additional options include:
|
||||
- "amp.permissions": [] (tool permissions)
|
||||
- "amp.tools.stopTimeout": 600 (extend timeout for long operations)
|
||||
- "amp.terminal.commands.nodeSpawn.loadProfile": "daily" (environment loading)
|
||||
- "amp.tools.disable": ["builtin:open"] (disable tools for containers)
|
||||
- "amp.git.commit.ampThread.enabled": true (link commits to threads)
|
||||
- "amp.git.commit.coauthor.enabled": true (add Amp as co-author)
|
||||
|
||||
Reference: https://ampcode.com/manual
|
||||
EOT
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "additional_mcp_servers" {
|
||||
type = string
|
||||
description = "Additional MCP servers configuration in JSON format to append to amp.mcpServers."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
app_slug = "amp"
|
||||
|
||||
default_base_config = {
|
||||
"amp.anthropic.thinking.enabled" = true
|
||||
"amp.todos.enabled" = true
|
||||
}
|
||||
|
||||
# Use provided config or default, then extract base settings (excluding mcpServers)
|
||||
user_config = var.base_amp_config != "" ? jsondecode(var.base_amp_config) : local.default_base_config
|
||||
base_amp_settings = { for k, v in local.user_config : k => v if k != "amp.mcpServers" }
|
||||
|
||||
coder_mcp = {
|
||||
"coder" = {
|
||||
"command" = "coder"
|
||||
"args" = ["exp", "mcp", "server"]
|
||||
"env" = {
|
||||
"CODER_MCP_APP_STATUS_SLUG" = local.app_slug
|
||||
"CODER_MCP_AI_AGENTAPI_URL" = "http://localhost:3284"
|
||||
}
|
||||
"type" = "stdio"
|
||||
}
|
||||
}
|
||||
|
||||
additional_mcp = var.additional_mcp_servers != null ? jsondecode(var.additional_mcp_servers) : {}
|
||||
|
||||
merged_mcp_servers = merge(
|
||||
lookup(local.user_config, "amp.mcpServers", {}),
|
||||
local.coder_mcp,
|
||||
local.additional_mcp
|
||||
)
|
||||
|
||||
final_config = merge(local.base_amp_settings, {
|
||||
"amp.mcpServers" = local.merged_mcp_servers
|
||||
})
|
||||
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".sourcegraph-amp-module"
|
||||
}
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.0.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = "Sourcegraph Amp"
|
||||
cli_app_slug = "${local.app_slug}-cli"
|
||||
cli_app_display_name = "Sourcegraph Amp CLI"
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
start_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
SOURCEGRAPH_AMP_API_KEY='${var.sourcegraph_amp_api_key}' \
|
||||
SOURCEGRAPH_AMP_START_DIRECTORY='${var.folder}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
ARG_INSTALL_SOURCEGRAPH_AMP='${var.install_sourcegraph_amp}' \
|
||||
SOURCEGRAPH_AMP_START_DIRECTORY='${var.folder}' \
|
||||
ARG_AMP_CONFIG="$(echo -n '${base64encode(jsonencode(local.final_config))}' | base64 -d)" \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# ANSI colors
|
||||
BOLD='\033[1m'
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "Install flag: $ARG_INSTALL_SOURCEGRAPH_AMP"
|
||||
echo "Workspace: $SOURCEGRAPH_AMP_START_DIRECTORY"
|
||||
echo "--------------------------------"
|
||||
|
||||
# Helper function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
function install_node() {
|
||||
if ! command_exists npm; then
|
||||
printf "npm not found, checking for Node.js installation...\n"
|
||||
if ! command_exists node; then
|
||||
printf "Node.js not found, installing Node.js via NVM...\n"
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
if [ ! -d "$NVM_DIR" ]; then
|
||||
mkdir -p "$NVM_DIR"
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
else
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
fi
|
||||
|
||||
# Temporarily disable nounset (-u) for nvm to avoid PROVIDED_VERSION error
|
||||
set +u
|
||||
nvm install --lts
|
||||
nvm use --lts
|
||||
nvm alias default node
|
||||
set -u
|
||||
|
||||
printf "Node.js installed: %s\n" "$(node --version)"
|
||||
printf "npm installed: %s\n" "$(npm --version)"
|
||||
else
|
||||
printf "Node.js is installed but npm is not available. Please install npm manually.\n"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function install_sourcegraph_amp() {
|
||||
if [ "${ARG_INSTALL_SOURCEGRAPH_AMP}" = "true" ]; then
|
||||
install_node
|
||||
|
||||
# If nvm is not used, set up user npm global directory
|
||||
if ! command_exists nvm; then
|
||||
mkdir -p "$HOME/.npm-global"
|
||||
npm config set prefix "$HOME/.npm-global"
|
||||
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||
if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then
|
||||
echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "%s Installing Sourcegraph AMP CLI...\n" "${BOLD}"
|
||||
npm install -g @sourcegraph/amp@0.0.1754179307-gba1f97
|
||||
printf "%s Successfully installed Sourcegraph AMP CLI. Version: %s\n" "${BOLD}" "$(amp --version)"
|
||||
fi
|
||||
}
|
||||
|
||||
function setup_system_prompt() {
|
||||
if [ -n "${SOURCEGRAPH_AMP_SYSTEM_PROMPT:-}" ]; then
|
||||
echo "Setting Sourcegraph AMP system prompt..."
|
||||
mkdir -p "$HOME/.sourcegraph-amp-module"
|
||||
echo "$SOURCEGRAPH_AMP_SYSTEM_PROMPT" > "$HOME/.sourcegraph-amp-module/SYSTEM_PROMPT.md"
|
||||
echo "System prompt saved to $HOME/.sourcegraph-amp-module/SYSTEM_PROMPT.md"
|
||||
else
|
||||
echo "No system prompt provided for Sourcegraph AMP."
|
||||
fi
|
||||
}
|
||||
|
||||
function configure_amp_settings() {
|
||||
echo "Configuring AMP settings..."
|
||||
SETTINGS_PATH="$HOME/.config/amp/settings.json"
|
||||
mkdir -p "$(dirname "$SETTINGS_PATH")"
|
||||
|
||||
if [ -z "${ARG_AMP_CONFIG:-}" ]; then
|
||||
echo "No AMP config provided, skipping configuration"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Writing AMP configuration to $SETTINGS_PATH"
|
||||
printf '%s\n' "$ARG_AMP_CONFIG" > "$SETTINGS_PATH"
|
||||
|
||||
echo "AMP configuration complete"
|
||||
}
|
||||
|
||||
install_sourcegraph_amp
|
||||
setup_system_prompt
|
||||
configure_amp_settings
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Load user environment
|
||||
# shellcheck source=/dev/null
|
||||
source "$HOME/.bashrc"
|
||||
# shellcheck source=/dev/null
|
||||
if [ -f "$HOME/.nvm/nvm.sh" ]; then
|
||||
source "$HOME"/.nvm/nvm.sh
|
||||
else
|
||||
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||
fi
|
||||
|
||||
function ensure_command() {
|
||||
command -v "$1" &> /dev/null || {
|
||||
echo "Error: '$1' not found." >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_command amp
|
||||
echo "AMP version: $(amp --version)"
|
||||
|
||||
dir="$SOURCEGRAPH_AMP_START_DIRECTORY"
|
||||
if [[ -d "$dir" ]]; then
|
||||
echo "Using existing directory: $dir"
|
||||
else
|
||||
echo "Creating directory: $dir"
|
||||
mkdir -p "$dir"
|
||||
fi
|
||||
cd "$dir"
|
||||
|
||||
if [ -n "$SOURCEGRAPH_AMP_API_KEY" ]; then
|
||||
printf "sourcegraph_amp_api_key provided !\n"
|
||||
export AMP_API_KEY=$SOURCEGRAPH_AMP_API_KEY
|
||||
else
|
||||
printf "sourcegraph_amp_api_key not provided\n"
|
||||
fi
|
||||
|
||||
if [ -n "${SOURCEGRAPH_AMP_TASK_PROMPT:-}" ]; then
|
||||
printf "sourcegraph amp task prompt provided : $SOURCEGRAPH_AMP_TASK_PROMPT"
|
||||
PROMPT="Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $SOURCEGRAPH_AMP_TASK_PROMPT"
|
||||
|
||||
# Pipe the prompt into amp, which will be run inside agentapi
|
||||
agentapi server --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp"
|
||||
else
|
||||
printf "No task prompt given.\n"
|
||||
agentapi server --term-width=67 --term-height=1190 -- amp
|
||||
fi
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Mock behavior of the AMP CLI
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "AMP CLI mock version v1.0.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Simulate AMP running in a loop for AgentAPI to connect
|
||||
set -e
|
||||
while true; do
|
||||
echo "$(date) - AMP mock is running..."
|
||||
sleep 15
|
||||
done
|
||||
@@ -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 = "claude-code"
|
||||
}
|
||||
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 |
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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.0.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.0.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.0.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.0.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.0.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.0.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.0.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.0.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.6.1"
|
||||
}
|
||||
```
|
||||
|
||||
### 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.0.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.6.1"
|
||||
}
|
||||
|
||||
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.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
cli_app = var.cli_app
|
||||
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
|
||||
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
|
||||
start_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/bin/bash
|
||||
# Install script for amazon-q module
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Inputs
|
||||
ARG_INSTALL=${ARG_INSTALL:-true}
|
||||
ARG_VERSION=${ARG_VERSION:-latest}
|
||||
ARG_Q_INSTALL_URL=${ARG_Q_INSTALL_URL:-https://desktop-release.q.us-east-1.amazonaws.com}
|
||||
ARG_AUTH_TARBALL=${ARG_AUTH_TARBALL:-}
|
||||
ARG_AGENT_CONFIG=${ARG_AGENT_CONFIG:-}
|
||||
ARG_AGENT_NAME=${ARG_AGENT_NAME:-default-agent}
|
||||
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.aws/.amazonq}
|
||||
ARG_CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG:-}
|
||||
ARG_CODER_MCP_INSTRUCTIONS=${ARG_CODER_MCP_INSTRUCTIONS:-}
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
|
||||
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
|
||||
|
||||
# Decode base64 inputs
|
||||
ARG_AGENT_CONFIG_DECODED=""
|
||||
if [ -n "$ARG_AGENT_CONFIG" ]; then
|
||||
ARG_AGENT_CONFIG_DECODED=$(echo -n "$ARG_AGENT_CONFIG" | base64 -d)
|
||||
fi
|
||||
|
||||
ARG_CODER_MCP_INSTRUCTIONS_DECODED=""
|
||||
if [ -n "$ARG_CODER_MCP_INSTRUCTIONS" ]; then
|
||||
ARG_CODER_MCP_INSTRUCTIONS_DECODED=$(echo -n "$ARG_CODER_MCP_INSTRUCTIONS" | base64 -d)
|
||||
fi
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "install: $ARG_INSTALL"
|
||||
echo "version: $ARG_VERSION"
|
||||
echo "q_install_url: $ARG_Q_INSTALL_URL"
|
||||
echo "agent_name: $ARG_AGENT_NAME"
|
||||
echo "coder_mcp_app_status_slug: $ARG_CODER_MCP_APP_STATUS_SLUG"
|
||||
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
|
||||
echo "auth_tarball_provided: ${ARG_AUTH_TARBALL}"
|
||||
echo "report_tasks: ${ARG_REPORT_TASKS}"
|
||||
echo "--------------------------------"
|
||||
|
||||
# Install Amazon Q if requested
|
||||
function install_amazon_q() {
|
||||
if [ "$ARG_INSTALL" = "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="${ARG_Q_INSTALL_URL}/${ARG_VERSION}/q-x86_64-linux.zip"
|
||||
;;
|
||||
"aarch64" | "arm64")
|
||||
Q_URL="${ARG_Q_INSTALL_URL}/${ARG_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 from $Q_URL..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf "$Q_URL" -o "q.zip"
|
||||
unzip q.zip
|
||||
./q/install.sh --no-confirm
|
||||
cd "$PREV_DIR"
|
||||
rm -rf "$TMP_DIR"
|
||||
|
||||
# Ensure binaries are discoverable; create stable symlink to q
|
||||
CANDIDATES=(
|
||||
"$(command -v q || true)"
|
||||
"$HOME/.local/bin/q"
|
||||
)
|
||||
FOUND_BIN=""
|
||||
for c in "${CANDIDATES[@]}"; do
|
||||
if [ -n "$c" ] && [ -x "$c" ]; then
|
||||
FOUND_BIN="$c"
|
||||
break
|
||||
fi
|
||||
done
|
||||
export PATH="$PATH:$HOME/.local/bin"
|
||||
echo "Installed Amazon Q at: $(command -v q || true) (resolved: $FOUND_BIN)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract authentication tarball
|
||||
function extract_auth_tarball() {
|
||||
if [ -n "$ARG_AUTH_TARBALL" ]; then
|
||||
echo "Extracting auth tarball..."
|
||||
PREV_DIR="$PWD"
|
||||
echo "$ARG_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 to ~/.local/share/amazon-q"
|
||||
else
|
||||
echo "Warning: No auth tarball provided. Amazon Q may require manual authentication."
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure MCP integration and create agent
|
||||
function configure_agent() {
|
||||
# Create Amazon Q agent configuration directory
|
||||
AGENT_CONFIG_DIR="$HOME/.aws/amazonq/cli-agents"
|
||||
mkdir -p "$AGENT_CONFIG_DIR"
|
||||
ALLOWED_TOOLS="coder_get_workspace\,coder_create_workspace\,coder_list_workspaces\,coder_list_templates\,coder_template_version_parameters\,coder_get_authenticated_user\,coder_create_workspace_build\,coder_create_template_version\,coder_get_workspace_agent_logs\,coder_get_workspace_build_logs\,coder_get_template_version_logs\,coder_update_template_active_version\,coder_upload_tar_file\,coder_create_template\,coder_delete_template\,coder_workspace_bash"
|
||||
if [ -n "$ARG_AGENT_CONFIG_DECODED" ]; then
|
||||
echo "Applying custom MCP configuration..."
|
||||
# Use agent name as filename for the configuration
|
||||
echo "$ARG_AGENT_CONFIG_DECODED" > "$AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json"
|
||||
echo "Custom configuration saved to $AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json"
|
||||
fi
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
echo "Configuring Amazon Q to report tasks via Coder MCP..."
|
||||
q mcp add --name coder \
|
||||
--command "coder" \
|
||||
--agent "$ARG_AGENT_NAME" \
|
||||
--args "exp,mcp,server,--allowed-tools,coder_report_task,--instructions,'$ARG_CODER_MCP_INSTRUCTIONS_DECODED'" \
|
||||
--env "CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG}" \
|
||||
--env "CODER_MCP_AI_AGENTAPI_URL=http://localhost:3284" \
|
||||
--env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
|
||||
--env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
|
||||
--force || echo "Warning: Failed to add Coder MCP server"
|
||||
else
|
||||
q mcp add --name coder \
|
||||
--command "coder" \
|
||||
--agent "$ARG_AGENT_NAME" \
|
||||
--args "exp,mcp,server,--allowed-tools,coder_report_task" \
|
||||
--env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
|
||||
--env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
|
||||
--force || echo "Warning: Failed to add Coder MCP server"
|
||||
fi
|
||||
echo "Added Coder MCP server into $ARG_AGENT_NAME in Amazon Q configuration"
|
||||
q settings chat.defaultAgent "$ARG_AGENT_NAME"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
install_amazon_q
|
||||
extract_auth_tarball
|
||||
configure_agent
|
||||
|
||||
echo "Amazon Q installation and configuration complete!"
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
# Start script for amazon-q module
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Decode inputs
|
||||
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d)
|
||||
ARG_TRUST_ALL_TOOLS=${ARG_TRUST_ALL_TOOLS:-true}
|
||||
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.aws/amazonq}
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
ARG_SERVER_PARAMETERS=${ARG_SERVER_PARAMETERS:-""}
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "ai_prompt: $ARG_AI_PROMPT"
|
||||
echo "trust_all_tools: $ARG_TRUST_ALL_TOOLS"
|
||||
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
|
||||
echo "workdir: $ARG_WORKDIR"
|
||||
echo "report_tasks: ${ARG_REPORT_TASKS}"
|
||||
echo "--------------------------------"
|
||||
|
||||
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
|
||||
|
||||
# Find Amazon Q CLI
|
||||
if command_exists q; then
|
||||
Q_CMD=q
|
||||
elif [ -x "$HOME/.local/bin/q" ]; then
|
||||
Q_CMD="$HOME/.local/bin/q"
|
||||
else
|
||||
echo "Error: Amazon Q CLI not found. Install it or set install_amazon_q=true."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
|
||||
# Set up environment
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
# Build command arguments
|
||||
ARGS=(chat)
|
||||
|
||||
if [ "$ARG_TRUST_ALL_TOOLS" = "true" ]; then
|
||||
ARGS+=(--trust-all-tools)
|
||||
fi
|
||||
|
||||
# Log and run with agentapi integration
|
||||
printf "Running: %q %s\n" "$Q_CMD" "$(printf '%q ' "${ARGS[@]}")"
|
||||
|
||||
# If we have an AI prompt, we need to handle it specially
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" == "true" ]; then
|
||||
PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AI_PROMPT"
|
||||
else
|
||||
PROMPT="$ARG_AI_PROMPT"
|
||||
fi
|
||||
ARGS+=("$PROMPT")
|
||||
fi
|
||||
|
||||
# Use agentapi to manage the interactive session with initial prompt
|
||||
agentapi server ${ARG_SERVER_PARAMETERS} --term-width 67 --term-height 1190 -- "$Q_CMD" "${ARGS[@]}"
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||