Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b14b05cb9 |
@@ -1,3 +1,5 @@
|
||||
Closes #
|
||||
|
||||
## Description
|
||||
|
||||
<!-- Briefly describe what this PR does and why -->
|
||||
@@ -5,7 +7,6 @@
|
||||
## Type of Change
|
||||
|
||||
- [ ] New module
|
||||
- [ ] New template
|
||||
- [ ] Bug fix
|
||||
- [ ] Feature/enhancement
|
||||
- [ ] Documentation
|
||||
@@ -19,16 +20,10 @@
|
||||
**New version:** `v1.0.0`
|
||||
**Breaking change:** [ ] Yes [ ] No
|
||||
|
||||
## Template Information
|
||||
|
||||
<!-- Delete this section if not applicable -->
|
||||
|
||||
**Path:** `registry/[namespace]/templates/[template-name]`
|
||||
|
||||
## Testing & Validation
|
||||
|
||||
- [ ] Tests pass (`bun test`)
|
||||
- [ ] Code formatted (`bun fmt`)
|
||||
- [ ] Code formatted (`bun run fmt`)
|
||||
- [ ] Changes tested locally
|
||||
|
||||
## Related Issues
|
||||
|
||||
@@ -70,43 +70,23 @@ update_readme_version() {
|
||||
if grep -q "source.*${module_source}" "$readme_path"; then
|
||||
echo "Updating version references for $namespace/$module_name in $readme_path"
|
||||
awk -v module_source="$module_source" -v new_version="$new_version" '
|
||||
/^[[:space:]]*module[[:space:]]/ {
|
||||
in_module_block = 1
|
||||
module_content = $0 "\n"
|
||||
module_has_target_source = 0
|
||||
next
|
||||
/source.*=.*/ {
|
||||
if ($0 ~ module_source) {
|
||||
in_target_module = 1
|
||||
} else {
|
||||
in_target_module = 0
|
||||
}
|
||||
}
|
||||
in_module_block {
|
||||
module_content = module_content $0 "\n"
|
||||
if ($0 ~ /source.*=/ && $0 ~ module_source) {
|
||||
module_has_target_source = 1
|
||||
/version.*=.*"/ {
|
||||
if (in_target_module) {
|
||||
gsub(/version[[:space:]]*=[[:space:]]*"[^"]*"/, "version = \"" new_version "\"")
|
||||
in_target_module = 0
|
||||
}
|
||||
if ($0 ~ /^[[:space:]]*}[[:space:]]*$/) {
|
||||
in_module_block = 0
|
||||
if (module_has_target_source) {
|
||||
num_lines = split(module_content, lines, "\n")
|
||||
for (i = 1; i <= num_lines; i++) {
|
||||
line = lines[i]
|
||||
if (line ~ /^[[:space:]]*version[[:space:]]*=/) {
|
||||
match(line, /^[[:space:]]*/)
|
||||
indent = substr(line, 1, RLENGTH)
|
||||
printf "%sversion = \"%s\"\n", indent, new_version
|
||||
} else {
|
||||
print line
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printf "%s", module_content
|
||||
}
|
||||
module_content = ""
|
||||
next
|
||||
}
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' "$readme_path" > "${readme_path}.tmp" && mv "${readme_path}.tmp" "$readme_path"
|
||||
return 0
|
||||
elif grep -q '^[[:space:]]*version[[:space:]]*=' "$readme_path"; then
|
||||
elif grep -q 'version\s*=\s*"' "$readme_path"; then
|
||||
echo "⚠️ Found version references but no module source match for $namespace/$module_name"
|
||||
return 1
|
||||
fi
|
||||
@@ -168,9 +148,9 @@ main() {
|
||||
local current_version
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
if [ -f "$readme_path" ] && grep -q '^[[:space:]]*version[[:space:]]*=' "$readme_path"; then
|
||||
if [ -f "$readme_path" ] && grep -q 'version\s*=\s*"' "$readme_path"; then
|
||||
local readme_version
|
||||
readme_version=$(awk '/^[[:space:]]*version[[:space:]]*=/ { match($0, /"[^"]*"/); print substr($0, RSTART+1, RLENGTH-2); exit }' "$readme_path")
|
||||
readme_version=$(grep 'version\s*=\s*"' "$readme_path" | head -1 | sed 's/.*version\s*=\s*"\([^"]*\)".*/\1/')
|
||||
echo "No git tag found, but README shows version: $readme_version"
|
||||
|
||||
if ! validate_version "$readme_version"; then
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
[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
|
||||
inh = "inh" # Option in setpriv command
|
||||
exportfs = "exportfs" # nfs related binary
|
||||
|
||||
[files]
|
||||
extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Run check.sh
|
||||
run: |
|
||||
|
||||
@@ -12,27 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
- name: Detect changed files
|
||||
uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
list-files: shell
|
||||
filters: |
|
||||
shared:
|
||||
- 'test/**'
|
||||
- 'package.json'
|
||||
- 'bun.lock'
|
||||
- 'bunfig.toml'
|
||||
- 'tsconfig.json'
|
||||
- '.github/workflows/ci.yaml'
|
||||
- 'scripts/ts_test_auto.sh'
|
||||
- 'scripts/terraform_test_all.sh'
|
||||
- 'scripts/terraform_validate.sh'
|
||||
modules:
|
||||
- 'registry/**/modules/**'
|
||||
all:
|
||||
- '**'
|
||||
uses: actions/checkout@v5
|
||||
- name: Set up Terraform
|
||||
uses: coder/coder/.github/actions/setup-tf@main
|
||||
- name: Set up Bun
|
||||
@@ -47,29 +27,15 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
- name: Run TypeScript tests
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.filter.outputs.all_files }}
|
||||
SHARED_CHANGED: ${{ steps.filter.outputs.shared }}
|
||||
MODULE_CHANGED_FILES: ${{ steps.filter.outputs.modules_files }}
|
||||
run: bun tstest
|
||||
- name: Run Terraform tests
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.filter.outputs.all_files }}
|
||||
SHARED_CHANGED: ${{ steps.filter.outputs.shared }}
|
||||
MODULE_CHANGED_FILES: ${{ steps.filter.outputs.modules_files }}
|
||||
run: bun tftest
|
||||
run: bun test
|
||||
- name: Run Terraform Validate
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.filter.outputs.all_files }}
|
||||
SHARED_CHANGED: ${{ steps.filter.outputs.shared }}
|
||||
MODULE_CHANGED_FILES: ${{ steps.filter.outputs.modules_files }}
|
||||
run: bun terraform-validate
|
||||
validate-style:
|
||||
name: Check for typos and unformatted code
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
- name: Install Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
@@ -82,7 +48,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.39.2
|
||||
uses: crate-ci/typos@v1.35.5
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
@@ -93,11 +59,11 @@ jobs:
|
||||
needs: validate-style
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24.0"
|
||||
go-version: "1.23.2"
|
||||
- name: Validate contributors
|
||||
run: go build ./cmd/readmevalidation && ./readmevalidation
|
||||
- name: Remove build file artifact
|
||||
|
||||
@@ -28,14 +28,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
- name: Authenticate with Google Cloud
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
|
||||
uses: google-github-actions/auth@b7593ed2efd1c1617e1b0254da33b86225adb2a5
|
||||
with:
|
||||
workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github
|
||||
service_account: registry-v2-github@coder-registry-1.iam.gserviceaccount.com
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
|
||||
uses: google-github-actions/setup-gcloud@26f734c2779b00b7dda794207734c511110a4368
|
||||
- name: Deploy to dev.registry.coder.com
|
||||
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch main
|
||||
- name: Deploy to registry.coder.com
|
||||
|
||||
@@ -14,11 +14,11 @@ jobs:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: v2.1
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -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@v8
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 852 B |
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="512pt" height="512pt" version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m500.48 262.2-48.18 73.984c-0.73438 1.1367-2 1.8242-3.3555 1.8242-1.3516 0-2.6172-0.6875-3.3516-1.8242l-48.129-73.984c-0.78125-1.2227-0.83594-2.7773-0.14453-4.0547 0.69141-1.2734 2.0195-2.0742 3.4727-2.0898h24.781c-0.007813-29.523-7.7188-58.531-22.375-84.156-14.652-25.629-35.742-46.988-61.184-61.969-2.3711-1.3633-3.8633-3.8594-3.9453-6.5938-0.085937-2.7305 1.2539-5.3125 3.5352-6.8203l27.035-17.613c3.4766-2.3633 8.043-2.3633 11.52 0 28.473 19.934 51.723 46.441 67.773 77.27 16.051 30.828 24.434 65.074 24.438 99.832h24.781c1.4688 0 2.8203 0.80859 3.5156 2.1055 0.69531 1.293 0.62109 2.8633-0.1875 4.0898zm-85.043 79.359c-1.5078-2.2812-4.0898-3.6211-6.8203-3.5391-2.7344 0.085937-5.2305 1.5781-6.5938 3.9492-14.965 25.434-36.305 46.523-61.914 61.188-25.609 14.664-54.602 22.391-84.109 22.422v-24.781c-0.011719-1.4531-0.8125-2.7812-2.0898-3.4727-1.2773-0.69141-2.832-0.63672-4.0547 0.14453l-74.035 47.977c-1.1367 0.73438-1.8242 1.9961-1.8242 3.3516s0.6875 2.6172 1.8242 3.3555l73.984 48.18c1.2227 0.78125 2.7773 0.83594 4.0547 0.14453 1.2734-0.69141 2.0742-2.0234 2.0898-3.4727v-24.68c34.734-0.015624 68.957-8.3984 99.766-24.441 30.812-16.039 57.301-39.27 77.23-67.719 2.3672-3.4766 2.3672-8.043 0-11.52zm-245.45 60.52c-25.434-14.977-46.516-36.328-61.172-61.945-14.652-25.617-22.371-54.617-22.387-84.129h24.781c1.4531-0.011719 2.7812-0.8125 3.4727-2.0898 0.69141-1.2773 0.63672-2.832-0.14453-4.0547l-47.977-74.035c-0.73438-1.1367-1.9961-1.8242-3.3516-1.8242s-2.6172 0.6875-3.3555 1.8242l-48.332 73.984c-0.80859 1.2266-0.88281 2.7969-0.1875 4.0898 0.69531 1.2969 2.0469 2.1055 3.5156 2.1055h24.781c0.015625 34.734 8.3984 68.957 24.438 99.766 16.043 30.812 39.273 57.301 67.723 77.234 3.4766 2.3633 8.043 2.3633 11.52 0l27.086-17.664c2.2109-1.5195 3.4961-4.0625 3.4141-6.7422-0.082032-2.6836-1.5234-5.1406-3.8242-6.5195zm92.16-390.5c-1.2227-0.78125-2.7773-0.83594-4.0547-0.14453-1.2773 0.69141-2.0781 2.0195-2.0898 3.4727v24.73c-34.734 0.015625-68.957 8.3984-99.766 24.438-30.812 16.043-57.301 39.273-77.234 67.723-2.3633 3.4766-2.3633 8.043 0 11.52l17.664 27.086c1.5078 2.2812 4.0898 3.6211 6.8242 3.5352 2.7305-0.082032 5.2266-1.5742 6.5898-3.9453 14.965-25.41 36.289-46.48 61.879-61.133 25.59-14.652 54.555-22.383 84.043-22.426v24.781c0.011719 1.4531 0.8125 2.7812 2.0898 3.4727 1.2773 0.69141 2.832 0.63672 4.0547-0.14453l74.035-47.977c1.1367-0.73438 1.8242-1.9961 1.8242-3.3516s-0.6875-2.6172-1.8242-3.3555zm-6.1445 210.23c-9.0703 0-17.77 3.6055-24.184 10.02-6.4141 6.4141-10.02 15.113-10.02 24.184s3.6055 17.77 10.02 24.184c6.4141 6.4141 15.113 10.02 24.184 10.02s17.77-3.6055 24.184-10.02c6.4141-6.4141 10.02-15.113 10.02-24.184s-3.6055-17.77-10.02-24.184c-6.4141-6.4141-15.113-10.02-24.184-10.02zm90.727-26.828-10.344 14.953c4.0039 6.9414 7.0859 14.375 9.1641 22.117l17.973 2.9688c6.543 1.1445 11.316 6.8242 11.316 13.465v15.055c0 6.6406-4.7734 12.32-11.316 13.465l-17.766 3.125v-0.003907c-2.1562 7.6992-5.3086 15.082-9.3711 21.965l10.238 14.797h0.003906c3.8047 5.4375 3.1562 12.82-1.5352 17.512l-10.648 10.648h-0.003906c-4.6914 4.6953-12.074 5.3438-17.508 1.5391l-14.797-10.238v-0.003907c-6.9453 4.0039-14.379 7.0859-22.121 9.1641l-3.0195 18.023c-1.1445 6.543-6.8242 11.316-13.465 11.316h-15.055c-6.6406 0-12.32-4.7734-13.465-11.316l-3.125-17.766h0.003907c-7.7031-2.1758-15.086-5.3398-21.965-9.4219l-14.797 10.238v0.003907c-5.4375 3.8047-12.82 3.1562-17.512-1.5391l-10.648-10.648c-4.6953-4.6914-5.3438-12.074-1.5391-17.512l10.238-14.797h0.003907c-4.0039-6.9414-7.0859-14.375-9.1641-22.117l-18.023-2.9688c-6.543-1.1445-11.316-6.8242-11.316-13.465v-15.055c0-6.6406 4.7734-12.32 11.316-13.465l17.766-3.125v0.003907c2.1562-7.6992 5.3086-15.082 9.3711-21.965l-10.238-14.797h-0.003906c-3.8047-5.4375-3.1562-12.82 1.5352-17.512l10.648-10.648h0.003906c4.6914-4.6953 12.074-5.3438 17.508-1.5391l14.797 10.238v0.003907c6.9453-4.0039 14.379-7.0859 22.121-9.1641l3.0195-18.023c1.1445-6.543 6.8242-11.316 13.465-11.316h15.055c6.6406 0 12.32 4.7734 13.465 11.316l3.125 17.766h-0.003907c7.6992 2.1562 15.082 5.3086 21.965 9.3711l14.797-10.238v-0.003906c5.4375-3.8047 12.82-3.1562 17.512 1.5352l10.648 10.648v0.003906c4.6875 4.6367 5.3984 11.957 1.6914 17.406zm-36.047 61.031c0-14.504-5.7578-28.41-16.016-38.664-10.254-10.258-24.16-16.016-38.664-16.016s-28.41 5.7578-38.664 16.016c-10.258 10.254-16.016 24.16-16.016 38.664s5.7578 28.41 16.016 38.664c10.254 10.258 24.16 16.016 38.664 16.016 14.5-0.011719 28.398-5.7773 38.652-16.027 10.25-10.254 16.016-24.152 16.027-38.652z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.5 KiB |
@@ -1,47 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="1024" height="1024">
|
||||
<path d="M0 0 C337.92 0 675.84 0 1024 0 C1024 337.92 1024 675.84 1024 1024 C686.08 1024 348.16 1024 0 1024 C0 686.08 0 348.16 0 0 Z " fill="#020708" transform="translate(0,0)"/>
|
||||
<path d="M0 0 C40.22460379 0 80.4466421 0.00304941 120.67057991 0.15463066 C125.95372492 0.17442425 131.23687063 0.19398844 136.52001762 0.21324348 C147.77834068 0.25435249 159.03666181 0.29592484 170.29497623 0.33933926 C170.99953161 0.34205531 171.70408698 0.34477135 172.42999252 0.34756971 C173.13533008 0.3502892 173.84066764 0.35300869 174.56737906 0.35581058 C175.99454502 0.36131272 177.42171098 0.36681401 178.84887695 0.37231445 C179.55666117 0.37504276 180.26444539 0.37777106 180.99367761 0.38058204 C192.50410753 0.42485496 204.01454726 0.46550406 215.52499563 0.50465261 C227.56090905 0.54564378 239.59680781 0.58979401 251.63269895 0.63687855 C258.30394122 0.66290814 264.97518119 0.68769341 271.64644051 0.70907402 C277.91414179 0.72924498 284.18181589 0.75352717 290.4494915 0.78049088 C292.69376491 0.78952735 294.93804399 0.79730645 297.18232727 0.80341911 C322.46351662 0.87417207 347.71815265 1.5042775 373 2 C373 96.05 373 190.1 373 287 C249.91 287 126.82 287 0 287 C0 192.29 0 97.58 0 0 Z " fill="#999F9E" transform="translate(326,395)"/>
|
||||
<path d="M0 0 C37.29 0 74.58 0 113 0 C113 46.86 113 93.72 113 142 C75.71 142 38.42 142 0 142 C0 95.14 0 48.28 0 0 Z " fill="#181E23" transform="translate(550,445)"/>
|
||||
<path d="M0 0 C37.29 0 74.58 0 113 0 C113 46.53 113 93.06 113 141 C95.613125 141.0309375 95.613125 141.0309375 77.875 141.0625 C74.3054248 141.071604 70.73584961 141.08070801 67.05810547 141.09008789 C62.46899414 141.09460449 62.46899414 141.09460449 60.27615356 141.09544373 C58.84198693 141.09691347 57.40782093 141.10027703 55.97366333 141.10557556 C37.3007381 141.17069509 18.67741441 140.49151091 0 140 C0 93.8 0 47.6 0 0 Z " fill="#181E23" transform="translate(362,446)"/>
|
||||
<path d="M0 0 C0 14.52 0 29.04 0 44 C-118.14 44 -236.28 44 -358 44 C-359.71802382 40.56395235 -359.1196618 36.61561104 -359.09765625 32.8359375 C-359.0962413 31.92872955 -359.09482635 31.02152161 -359.09336853 30.08682251 C-359.08776096 27.18284845 -359.07520718 24.27895085 -359.0625 21.375 C-359.05748598 19.40885528 -359.0529229 17.44270935 -359.04882812 15.4765625 C-359.03778906 10.6510121 -359.02051935 5.82551903 -359 1 C-319.04215869 0.87185681 -279.08431526 0.74439427 -239.12646896 0.61781773 C-234.3862332 0.60280127 -229.64599745 0.58777929 -224.90576172 0.57275391 C-223.49049118 0.56826798 -223.49049118 0.56826798 -222.04662931 0.56369144 C-206.8611473 0.51554559 -191.67566674 0.46695532 -176.49018669 0.41819459 C-160.85514345 0.36800044 -145.22009869 0.3182983 -129.58505249 0.26903296 C-120.82560112 0.24141872 -112.06615054 0.21359008 -103.30670166 0.18519592 C-68.87102254 0.07367311 -34.43592726 -0.03058253 0 0 Z " fill="#FEFEFE" transform="translate(692,838)"/>
|
||||
<path d="M0 0 C117.15 0 234.3 0 355 0 C355 13.86 355 27.72 355 42 C237.85 42 120.7 42 0 42 C0 28.14 0 14.28 0 0 Z " fill="#FEFEFE" transform="translate(334,135)"/>
|
||||
<path d="M0 0 C11.88 0 23.76 0 36 0 C36 19.8 36 39.6 36 60 C91.44 60 146.88 60 204 60 C204 71.55 204 83.1 204 95 C168.85263573 95.02994071 133.70661685 94.95253395 98.55956407 94.80645666 C91.61749391 94.77764681 84.67541903 94.75005412 77.7333438 94.72249681 C65.36625264 94.67337442 52.99916523 94.62336685 40.63208008 94.57275391 C28.66105336 94.52376386 16.69002497 94.47522104 4.71899414 94.42724609 C3.97369616 94.4242591 3.22839818 94.4212721 2.46051542 94.41819459 C-1.2806363 94.4032035 -5.02178805 94.38822085 -8.76293981 94.37324238 C-39.50863188 94.25013489 -70.25431782 94.12555501 -101 94 C-101 93.67 -101 93.34 -101 93 C-134.66 92.505 -134.66 92.505 -169 92 C-169 81.44 -169 70.88 -169 60 C-113.23 60 -57.46 60 0 60 C0 40.2 0 20.4 0 0 Z " fill="#191F24" transform="translate(495,302)"/>
|
||||
<path d="M0 0 C62.7 0 125.4 0 190 0 C190 11.88 190 23.76 190 36 C127.3 36 64.6 36 0 36 C0 24.12 0 12.24 0 0 Z " fill="#474B51" transform="translate(418,786)"/>
|
||||
<path d="M0 0 C49.5 0 99 0 150 0 C150 14.52 150 29.04 150 44 C100.5 44 51 44 0 44 C0 29.48 0 14.96 0 0 Z " fill="#FDFEFE" transform="translate(200,239)"/>
|
||||
<path d="M0 0 C48.18 0 96.36 0 146 0 C146 14.52 146 29.04 146 44 C131.979151 44.02742613 117.95836252 44.05097148 103.9375 44.0625 C102.90375118 44.06335602 101.87000237 44.06421204 100.80492783 44.06509399 C67.19004349 44.08969196 33.60574878 43.83234053 0 43 C0 28.81 0 14.62 0 0 Z " fill="#FEFEFE" transform="translate(677,239)"/>
|
||||
<path d="M0 0 C51.48 0 102.96 0 156 0 C156 13.53 156 27.06 156 41 C104.52 41 53.04 41 0 41 C0 27.47 0 13.94 0 0 Z " fill="#FEFEFE" transform="translate(259,188)"/>
|
||||
<path d="M0 0 C1.28729507 -0.00281982 2.57459015 -0.00563965 3.90089417 -0.00854492 C5.34078993 -0.00660032 6.78068557 -0.00455213 8.22058105 -0.00241089 C9.72647647 -0.00376487 11.23237154 -0.00554479 12.73826599 -0.00772095 C16.839391 -0.01229564 20.94049097 -0.01050558 25.04161644 -0.00734186 C29.32396896 -0.00481844 33.60631927 -0.00715575 37.88867188 -0.00872803 C45.08150621 -0.01054978 52.2743303 -0.00814327 59.46716309 -0.00338745 C67.79516627 0.00205518 76.12314497 0.00029253 84.45114756 -0.00521386 C91.58828139 -0.00974483 98.72540783 -0.01039561 105.86254263 -0.00777519 C110.13098648 -0.00621233 114.39941881 -0.00601889 118.66786194 -0.00931168 C122.67984227 -0.01219017 126.69179089 -0.01021511 130.70376778 -0.00441742 C132.18045054 -0.0030897 133.65713595 -0.00348413 135.13381767 -0.00568008 C137.1412128 -0.0083911 139.14861372 -0.0043972 141.15600586 0 C142.28205986 0.00037703 143.40811386 0.00075405 144.56829071 0.0011425 C147.07800293 0.12698364 147.07800293 0.12698364 148.07800293 1.12698364 C148.07800293 14.32698364 148.07800293 27.52698364 148.07800293 41.12698364 C96.92800293 41.12698364 45.77800293 41.12698364 -6.92199707 41.12698364 C-6.92199707 0.00231762 -6.92199707 0.00231762 0 0 Z " fill="#FEFEFE" transform="translate(616.9219970703125,187.87301635742188)"/>
|
||||
<path d="M0 0 C43.23 0 86.46 0 131 0 C131 15.84 131 31.68 131 48 C87.77 48 44.54 48 0 48 C0 32.16 0 16.32 0 0 Z " fill="#FEFEFE" transform="translate(128,353)"/>
|
||||
<path d="M0 0 C67.815 0.495 67.815 0.495 137 1 C137 15.19 137 29.38 137 44 C136.34 44 135.68 44 135 44 C135 44.66 135 45.32 135 46 C90.45 46 45.9 46 0 46 C0 30.82 0 15.64 0 0 Z " fill="#FDFEFE" transform="translate(156,294)"/>
|
||||
<path d="M0 0 C45.21 0 90.42 0 137 0 C137 14.19 137 28.38 137 43 C136.34 43 135.68 43 135 43 C135 43.66 135 44.32 135 45 C90.45 45 45.9 45 0 45 C0 30.15 0 15.3 0 0 Z " fill="#FBFCFC" transform="translate(732,295)"/>
|
||||
<path d="M0 0 C43.23 0 86.46 0 131 0 C131 15.18 131 30.36 131 46 C98.474264 46.72107728 65.97062256 47.09604795 33.4375 47.0625 C32.60141457 47.06164398 31.76532913 47.06078796 30.90390778 47.05990601 C20.60257228 47.04857042 10.30132061 47.02553343 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FEFEFE" transform="translate(765,354)"/>
|
||||
<path d="M0 0 C26.73 0 53.46 0 81 0 C82.19877676 50.94801223 82.19877676 50.94801223 82 74 C77.05167364 74.80762419 72.35065215 75.12200785 67.32055664 75.11352539 C66.57629897 75.11364593 65.8320413 75.11376646 65.06523037 75.11389065 C62.68017418 75.11315027 60.29519828 75.10556237 57.91015625 75.09765625 C56.42801959 75.09618411 54.9458824 75.09516508 53.46374512 75.09460449 C47.99665543 75.08938309 42.52957691 75.07542183 37.0625 75.0625 C24.831875 75.041875 12.60125 75.02125 0 75 C0 50.25 0 25.5 0 0 Z " fill="#FB4740" transform="translate(473,226)"/>
|
||||
<path d="M0 0 C36.96 0 73.92 0 112 0 C112.6261198 6.88731776 113.15084094 13.48218949 113.1328125 20.3515625 C113.13376923 21.17707611 113.13472595 22.00258972 113.13571167 22.8531189 C113.1363846 24.57216469 113.13459319 26.29121317 113.13037109 28.01025391 C113.12494037 30.65402778 113.13038111 33.29763707 113.13671875 35.94140625 C113.13605916 37.61979204 113.13478047 39.29817773 113.1328125 40.9765625 C113.13483673 41.76809723 113.13686096 42.55963196 113.13894653 43.37515259 C113.11499765 48.88500235 113.11499765 48.88500235 112 50 C110.54518505 50.0956161 109.08574514 50.12188351 107.62779236 50.12025452 C106.68403244 50.12162918 105.74027252 50.12300385 104.76791382 50.12442017 C103.72422638 50.12082489 102.68053894 50.11722961 101.60522461 50.11352539 C100.51267868 50.11367142 99.42013275 50.11381744 98.29447937 50.1139679 C94.66374767 50.11326822 91.03306654 50.10547329 87.40234375 50.09765625 C84.89270557 50.09579226 82.38306702 50.09436827 79.87342834 50.09336853 C73.93065407 50.08993348 67.98789616 50.08204273 62.04513031 50.07201904 C53.43224996 50.0578058 44.81936343 50.05254739 36.2064743 50.04621124 C24.13763437 50.03649856 12.06884226 50.01734306 0 50 C0 33.5 0 17 0 0 Z " fill="#FDFDFD" transform="translate(117,474)"/>
|
||||
<path d="M0 0 C36.63 0 73.26 0 111 0 C111 48 111 48 110 50 C73.7 50 37.4 50 0 50 C0 33.5 0 17 0 0 Z " fill="#FEFEFE" transform="translate(795,474)"/>
|
||||
<path d="M0 0 C54.12 0 108.24 0 164 0 C164 10.89 164 21.78 164 33 C109.88 33 55.76 33 0 33 C0 22.11 0 11.22 0 0 Z " fill="#181E23" transform="translate(431,622)"/>
|
||||
<path d="M0 0 C14.58571527 -0.0225479 29.17141907 -0.04091327 43.75714779 -0.05181217 C50.52910421 -0.05697491 57.30104905 -0.06401718 64.07299805 -0.07543945 C70.60224426 -0.08644714 77.13148294 -0.09227625 83.66073799 -0.09487724 C86.15793612 -0.09673199 88.65513355 -0.10035354 91.15232658 -0.10573006 C94.63664069 -0.11293684 98.12090431 -0.1139911 101.60522461 -0.11352539 C102.64891205 -0.11712067 103.69259949 -0.12071594 104.76791382 -0.12442017 C105.71167374 -0.1230455 106.65543365 -0.12167084 107.62779236 -0.12025452 C108.45279708 -0.12117631 109.2778018 -0.1220981 110.12780666 -0.12304783 C112 0 112 0 113 1 C113 16.18 113 31.36 113 47 C75.71 47 38.42 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FCFCFC" transform="translate(117,414)"/>
|
||||
<path d="M0 0 C0 15.84 0 31.68 0 48 C-36.96 48 -73.92 48 -112 48 C-112 32.49 -112 16.98 -112 1 C-92.15950898 0.75043408 -92.15950898 0.75043408 -83.5078125 0.64453125 C-77.67218771 0.57308961 -71.83656856 0.50132395 -66.00097656 0.42724609 C-43.99861261 0.14838814 -22.0044946 -0.06103882 0 0 Z " fill="#FAFAFA" transform="translate(906,537)"/>
|
||||
<path d="M0 0 C36.63 0 73.26 0 111 0 C111 15.51 111 31.02 111 47 C74.37 47 37.74 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FBFCFC" transform="translate(118,538)"/>
|
||||
<path d="M0 0 C36.3 0 72.6 0 110 0 C110 15.18 110 30.36 110 46 C86.98227311 46.96572558 64.03031552 47.12476904 40.99664307 47.06866455 C36.24190736 47.05823048 31.48716586 47.05382501 26.73242188 47.04882812 C17.48826669 47.03826689 8.24413623 47.02131845 -1 47 C-1.02529825 40.62793828 -1.04284496 34.25588946 -1.05493164 27.88378906 C-1.05996891 25.71483883 -1.06679891 23.545892 -1.07543945 21.37695312 C-1.08753151 18.26431437 -1.09323428 15.1517204 -1.09765625 12.0390625 C-1.10281754 11.065065 -1.10797882 10.0910675 -1.11329651 9.08755493 C-1.11337204 8.18677216 -1.11344757 7.28598938 -1.11352539 6.35791016 C-1.115746 5.56293121 -1.11796661 4.76795227 -1.12025452 3.94888306 C-1 2 -1 2 0 0 Z " fill="#FBFCFB" transform="translate(795,414)"/>
|
||||
<path d="M0 0 C34.98 0 69.96 0 106 0 C106 15.84 106 31.68 106 48 C71.02 48 36.04 48 0 48 C0 32.16 0 16.32 0 0 Z " fill="#FBFCFC" transform="translate(154,658)"/>
|
||||
<path d="M0 0 C34.32 0 68.64 0 104 0 C104 15.84 104 31.68 104 48 C69.35 48 34.7 48 -1 48 C-1 31.99652815 -0.6951464 15.9883671 0 0 Z " fill="#FBFBFB" transform="translate(765,658)"/>
|
||||
<path d="M0 0 C10.56252046 -0.02735617 21.1249617 -0.05092969 31.6875 -0.0625 C32.46348038 -0.06335602 33.23946075 -0.06421204 34.03895569 -0.06509399 C57.71503621 -0.08818645 81.33439347 0.16962784 105 1 C105 16.18 105 31.36 105 47 C70.35 47 35.7 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FCFDFD" transform="translate(125,598)"/>
|
||||
<path d="M0 0 C34.32 0 68.64 0 104 0 C104 15.51 104 31.02 104 47 C69.68 47 35.36 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FBFCFC" transform="translate(794,598)"/>
|
||||
<path d="M0 0 C37.62 0 75.24 0 114 0 C114 12.54 114 25.08 114 38 C76.38 38 38.76 38 0 38 C0 25.46 0 12.92 0 0 Z " fill="#474B51" transform="translate(456,716)"/>
|
||||
<path d="M0 0 C30.69 0 61.38 0 93 0 C93 15.18 93 30.36 93 46 C62.31 46 31.62 46 0 46 C0 30.82 0 15.64 0 0 Z " fill="#FCFDFD" transform="translate(732,719)"/>
|
||||
<path d="M0 0 C30.69 0 61.38 0 93 0 C93 14.85 93 29.7 93 45 C62.31 45 31.62 45 0 45 C0 30.15 0 15.3 0 0 Z " fill="#FEFEFE" transform="translate(200,720)"/>
|
||||
<path d="M0 0 C11.22 0 22.44 0 34 0 C34.6943434 30.12954393 35.09747148 60.23833134 35.0625 90.375 C35.06121597 91.48948643 35.06121597 91.48948643 35.05990601 92.62648773 C35.04860423 101.7510267 35.02558276 110.87548153 35 120 C23.45 120 11.9 120 0 120 C0 80.4 0 40.8 0 0 Z " fill="#474B50" transform="translate(258,470)"/>
|
||||
<path d="M0 0 C30.36 0 60.72 0 92 0 C92 14.19 92 28.38 92 43 C61.64 43 31.28 43 0 43 C0 28.81 0 14.62 0 0 Z " fill="#FCFDFD" transform="translate(259,781)"/>
|
||||
<path d="M0 0 C30.03 0 60.06 0 91 0 C91 14.19 91 28.38 91 43 C60.97 43 30.94 43 0 43 C0 28.81 0 14.62 0 0 Z " fill="#FCFDFD" transform="translate(674,781)"/>
|
||||
<path d="M0 0 C16.5 0 33 0 50 0 C50 25.41 50 50.82 50 77 C33.5 77 17 77 0 77 C0 51.59 0 26.18 0 0 Z " fill="#2CCDD4" transform="translate(582,478)"/>
|
||||
<path d="M0 0 C10.56 0 21.12 0 32 0 C32 39.27 32 78.54 32 119 C21.44 119 10.88 119 0 119 C0 79.73 0 40.46 0 0 Z " fill="#474B50" transform="translate(732,471)"/>
|
||||
<path d="M0 0 C16.17 0 32.34 0 49 0 C49 25.41 49 50.82 49 77 C32.83 77 16.66 77 0 77 C0 51.59 0 26.18 0 0 Z " fill="#2DCCD2" transform="translate(394,478)"/>
|
||||
<path d="M0 0 C15.84 0 31.68 0 48 0 C48 0.33 48 0.66 48 1 C32.49 1 16.98 1 1 1 C1 25.75 1 50.5 1 76 C28.06 75.67 55.12 75.34 83 75 C82.67 65.76 82.34 56.52 82 47 C81.93702338 38.74754429 81.90172306 30.50164687 81.9375 22.25 C81.94254243 20.16145945 81.94710032 18.07291767 81.95117188 15.984375 C81.96192066 10.98955155 81.97902162 5.99479052 82 1 C82.33 1 82.66 1 83 1 C83.57831229 19.95510799 84.11107346 38.91589618 84.23217773 57.88024902 C84.24403757 59.45720133 84.26110851 61.03412324 84.28344727 62.61096191 C84.31307026 64.8085447 84.32331211 67.00536665 84.328125 69.203125 C84.3374707 70.44739258 84.34681641 71.69166016 84.35644531 72.97363281 C84 76 84 76 82.73034668 77.68478394 C80.56608158 79.32981626 79.63802574 79.28920572 76.95800781 79.06982422 C75.71830231 78.98657394 75.71830231 78.98657394 74.45355225 78.90164185 C73.54591125 78.82839691 72.63827026 78.75515198 71.703125 78.6796875 C62.53186675 78.09888659 53.42327768 77.92843278 44.234375 78.0078125 C42.339151 78.0199881 42.339151 78.0199881 40.40563965 78.03240967 C35.17784805 78.06788344 29.95020985 78.1100683 24.72265625 78.17138672 C20.85162217 78.21602536 16.98061225 78.23702797 13.109375 78.2578125 C11.32536285 78.28574387 11.32536285 78.28574387 9.50531006 78.3142395 C7.87024506 78.31942093 7.87024506 78.31942093 6.20214844 78.32470703 C4.76279938 78.33922409 4.76279938 78.33922409 3.29437256 78.35403442 C2.15865814 78.17878738 2.15865814 78.17878738 1 78 C-1.11766624 74.82350064 -1.24885987 74.11349575 -1.23046875 70.48828125 C-1.22917969 69.52510986 -1.22789062 68.56193848 -1.2265625 67.56958008 C-1.21367187 66.49474365 -1.20078125 65.41990723 -1.1875 64.3125 C-1.18105469 63.18158936 -1.17460937 62.05067871 -1.16796875 60.88549805 C-1.00069226 40.5888483 -0.4546929 20.29067045 0 0 Z " fill="#4C0F0C" transform="translate(472,225)"/>
|
||||
<path d="M0 0 C8.45159814 -0.02350035 16.90318192 -0.04110214 25.35480499 -0.05181217 C29.28212158 -0.05696064 33.20941531 -0.06390567 37.13671875 -0.07543945 C53.56520732 -0.12238185 69.94755695 -0.05605073 86.36132812 0.74389648 C96.40041688 1.22706882 106.45208983 1.36454744 116.5 1.5625 C118.60677558 1.60589009 120.71354645 1.64950917 122.8203125 1.69335938 C127.88015843 1.79819282 132.94005639 1.8999973 138 2 C138 2.33 138 2.66 138 3 C92.79 3 47.58 3 1 3 C1 5.64 1 8.28 1 11 C0.67 11 0.34 11 0 11 C0 7.37 0 3.74 0 0 Z " fill="#6F7576" transform="translate(326,395)"/>
|
||||
<path d="M0 0 C0.33 0 0.66 0 1 0 C1 7.26 1 14.52 1 22 C37.3 22 73.6 22 111 22 C111.495 22.99 111.495 22.99 112 24 C74.71 24 37.42 24 -1 24 C-0.67 16.08 -0.34 8.16 0 0 Z " fill="#0E1418" transform="translate(551,563)"/>
|
||||
<path d="M0 0 C0.33 0 0.66 0 1 0 C1.57831229 18.95510799 2.11107346 37.91589618 2.23217773 56.88024902 C2.24403757 58.45720133 2.26110851 60.03412324 2.28344727 61.61096191 C2.31307026 63.8085447 2.32331211 66.00536665 2.328125 68.203125 C2.3374707 69.44739258 2.34681641 70.69166016 2.35644531 71.97363281 C2 75 2 75 0.79858398 76.92016602 C-1.65515594 78.39334257 -3.26106965 78.2550765 -6.10546875 78.07421875 C-7.08837891 78.01943359 -8.07128906 77.96464844 -9.08398438 77.90820312 C-10.62022461 77.79895508 -10.62022461 77.79895508 -12.1875 77.6875 C-13.22326172 77.62626953 -14.25902344 77.56503906 -15.32617188 77.50195312 C-17.88531957 77.34864013 -20.44267045 77.1807893 -23 77 C-23.33 76.34 -23.66 75.68 -24 75 C-11.625 74.505 -11.625 74.505 1 74 C0.835 70.32875 0.67 66.6575 0.5 62.875 C-0.33621628 41.9290255 -0.08802477 20.95802133 0 0 Z " fill="#390D0B" transform="translate(554,226)"/>
|
||||
<path d="M0 0 C0.66 0 1.32 0 2 0 C2 15.18 2 30.36 2 46 C0.68 45.67 -0.64 45.34 -2 45 C-1.88269497 38.75188831 -1.75785931 32.50396027 -1.62768555 26.25610352 C-1.58426577 24.12885993 -1.54259607 22.0015799 -1.50268555 19.87426758 C-1.44515293 16.82361849 -1.38141033 13.77315411 -1.31640625 10.72265625 C-1.29969376 9.76537125 -1.28298126 8.80808624 -1.26576233 7.8217926 C-1.24581711 6.94095505 -1.22587189 6.06011749 -1.20532227 5.15258789 C-1.18977798 4.37315323 -1.1742337 3.59371857 -1.15821838 2.79066467 C-1 1 -1 1 0 0 Z " fill="#D1D3D3" transform="translate(228,599)"/>
|
||||
<path d="M0 0 C35.64 0.33 71.28 0.66 108 1 C108 1.33 108 1.66 108 2 C54.54 2.495 54.54 2.495 0 3 C0 2.01 0 1.02 0 0 Z " fill="#F5F7F7" transform="translate(117,521)"/>
|
||||
<path d="M0 0 C6.93 0 13.86 0 21 0 C21 0.99 21 1.98 21 3 C14.07 3 7.14 3 0 3 C0 2.01 0 1.02 0 0 Z " fill="#272A2A" transform="translate(158,292)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,210 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="300mm"
|
||||
height="207mm"
|
||||
viewBox="0 0 300 207"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<title
|
||||
id="title1">copyparty_logo</title>
|
||||
<defs
|
||||
id="defs1">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient1">
|
||||
<stop
|
||||
style="stop-color:#ffcc55;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop1" />
|
||||
<stop
|
||||
style="stop-color:#ffcc00;stop-opacity:1"
|
||||
offset="0.2"
|
||||
id="stop2" />
|
||||
<stop
|
||||
style="stop-color:#ff8800;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1"
|
||||
id="linearGradient2"
|
||||
x1="15"
|
||||
y1="15"
|
||||
x2="15"
|
||||
y2="143"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<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>copyparty_logo</dc:title>
|
||||
<dc:source>github.com/9001/copyparty</dc:source>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
inkscape:label="kassett">
|
||||
<rect
|
||||
style="fill:#333333"
|
||||
id="rect1"
|
||||
width="300"
|
||||
height="205"
|
||||
x="0"
|
||||
y="0"
|
||||
rx="12"
|
||||
ry="12" />
|
||||
<rect
|
||||
style="fill:url(#linearGradient2)"
|
||||
id="rect2"
|
||||
width="270"
|
||||
height="128"
|
||||
x="15"
|
||||
y="15"
|
||||
rx="8"
|
||||
ry="8" />
|
||||
<rect
|
||||
style="fill:#333333"
|
||||
id="rect3"
|
||||
width="172"
|
||||
height="52"
|
||||
x="64"
|
||||
y="72"
|
||||
rx="26"
|
||||
ry="26" />
|
||||
<circle
|
||||
style="fill:#cccccc"
|
||||
id="circle1"
|
||||
cx="91"
|
||||
cy="98"
|
||||
r="18" />
|
||||
<circle
|
||||
style="fill:#cccccc"
|
||||
id="circle2"
|
||||
cx="209"
|
||||
cy="98"
|
||||
r="18" />
|
||||
<path
|
||||
style="fill:#737373;stroke-width:1px"
|
||||
d="m 48,207 10,-39 c 1.79,-6.2 5.6,-7.8 12,-8 60,-1 100,-1 160,0 6.4,0.2 10,1.8 12,8 l 10,39 z"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="ccccccc" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="tekst"
|
||||
style="display:none">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:38.8056px;line-height:1.25;font-family:Akbar;-inkscape-font-specification:Akbar;letter-spacing:3.70417px;word-spacing:0px;fill:#333333"
|
||||
x="47.153069"
|
||||
y="55.548954"
|
||||
id="text1"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan1"
|
||||
x="47.153069"
|
||||
y="55.548954"
|
||||
style="-inkscape-font-specification:Akbar"
|
||||
rotate="0 0">copyparty</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="stensatt">
|
||||
<path
|
||||
d="m 63.5,50.9 q -0.85,0.93 -4.73,2.3 -3.6,1.3 -4.4,1.3 -3.3,0 -5.1,-2.1 -1.75,-2 -1.75,-5.36 0,-4.6 3.76,-7.64 3.3,-2.7 7.3,-2.7 0.4,0 0.93,0.74 0.54,0.7 0.54,1.16 0,2.06 -2.2,2.7 -1.36,0.4 -4.04,1.16 -2.2,1.16 -2.2,4.4 0,3.2 2.9,3.2 0.85,0 0.85,0 0.54,0 1.44,-0.16 1.1,-0.23 2.9,-0.74 1.8,-0.54 2.13,-0.54 0.4,0 1.75,0.6 z"
|
||||
style="fill:#333333"
|
||||
id="path11" />
|
||||
<path
|
||||
d="m 87.6,45 q 0,4.2 -3.7,6.95 -3.2,2.3 -6.87,2.3 -3.4,0 -6,-2.6 -2.5,-2.6 -2.5,-6 0,-3.6 3.14,-6.64 3.2,-3 6.8,-3 3.5,0 6.3,2.76 2.83,2.76 2.83,6.25 z m -3.4,0.16 q 0,-2.25 -1.75,-3.7 -1.7,-1.5 -4,-1.5 -0.1,0 -1.6,1.6 -1.44,1.55 -2.44,1.55 -0.6,0 -0.8,-0.3 -1.16,2.3 -1.16,3 0,2.25 2.13,3.4 1.6,0.9 3.6,0.9 2,0 3.76,-1.1 2.25,-1.4 2.25,-3.84 z"
|
||||
style="fill:#333333"
|
||||
id="path12" />
|
||||
<path
|
||||
d="m 112.8,46.8 q 0,2.8 -1.9,4.4 -1.8,1.5 -4.7,1.5 -0.7,0 -2.7,-0.4 -1.9,-0.4 -2.6,-0.4 -2.1,0 -2.1,2.64 0,0.85 0.23,2.6 0.2,1.75 0.2,2.6 0,1.9 -0.77,2.83 -1.44,0 -3,-0.85 -1.46,-9.5 -1.46,-12 0,-3.65 1.75,-8.1 2.37,-6.05 6.45,-6.05 3.7,0 7.3,4.1 3.3,3.84 3.3,7.14 z m -3.8,0.2 q -0.6,-2.2 -2.6,-4.4 -2.3,-2.5 -4.3,-2.5 -1.3,0 -2.33,2.2 -0.9,1.8 -0.9,3.26 0,0.47 0.38,1.24 0.43,0.8 0.85,0.8 1.1,0 3.2,0.3 2.1,0.3 3.2,0.3 0.3,0 1.3,-0.4 1,-0.47 1.3,-0.74 z"
|
||||
style="fill:#333333"
|
||||
id="path13" />
|
||||
<path
|
||||
d="m 133,40 q -2.1,4.1 -3.2,7 -0.1,0.3 -1.6,4.5 -0.4,1.36 -1,4.2 -0.5,2.83 -1,4.2 -1,2.83 -2.3,2.64 -1.4,-0.2 -1.6,-1.6 0,-0.2 0,-0.5 0,-0.16 0.3,-1.5 1,-5.04 1,-6.44 0,-0.54 -0.1,-0.74 -1.4,-2.44 -4.1,-7.4 -2.7,-4.97 -2.4,-7.7 1.5,-1.36 2.1,-1.36 0.4,0 1.1,0.6 0.6,0.6 0.7,1.1 0.8,6.2 4.9,11.1 1,-1.8 1.8,-4.04 0.5,-1.4 1.6,-4.15 1.9,-4.46 3.4,-4.46 0.2,0 0.4,0.1 0.9,0.3 1.3,2.8 z"
|
||||
style="fill:#333333"
|
||||
id="path14" />
|
||||
<path
|
||||
d="m 157.5,48 q 0,2.8 -1.9,4.4 -1.8,1.5 -4.7,1.5 -0.7,0 -2.7,-0.4 -1.9,-0.4 -2.6,-0.4 -2,0 -2,2.64 0,0.85 0.2,2.6 0.2,1.75 0.2,2.6 0,1.9 -0.7,2.83 -1.5,0 -3,-0.85 -1.5,-9.5 -1.5,-11.95 0,-3.65 1.8,-8.1 2.3,-6.05 6.4,-6.05 3.7,0 7.2,4.1 3.3,3.84 3.3,7.14 z m -3.8,0.2 q -0.6,-2.2 -2.6,-4.4 -2.3,-2.5 -4.3,-2.5 -1.3,0 -2.3,2.2 -0.9,1.8 -0.9,3.26 0,0.47 0.4,1.24 0.4,0.8 0.8,0.8 1.1,0 3.2,0.3 2.1,0.3 3.2,0.3 0.3,0 1.3,-0.4 1,-0.47 1.3,-0.74 z"
|
||||
style="fill:#333333"
|
||||
id="path15" />
|
||||
<path
|
||||
d="m 182,53.3 q 0,0.9 -0.6,1.5 -0.6,0.6 -1.4,0.6 -1.6,0 -3,-0.9 -1.4,-0.93 -2.1,-2.3 -0.7,-0.1 -1.5,0.85 -0.9,1.16 -1.1,1.24 -1.2,0.54 -3.9,0.54 -2.2,0 -3.9,-2.44 -1.5,-2.13 -1.5,-4 0,-3.4 3.4,-6.4 3.2,-2.9 6.7,-2.9 0.9,0 1.7,0.6 0.8,0.6 0.8,1.44 0,0.54 -0.4,1.1 2.4,0.9 2.4,2.83 0,0.35 -0.1,1.05 -0.1,0.7 -0.1,1.05 0,0.4 0.1,0.6 0.5,1.3 2.5,3.4 1.9,1.9 1.9,2.2 z m -8.1,-10.1 q -0.4,0 -1.1,-0.1 -0.8,-0.16 -1.1,-0.16 -1.3,0 -3.2,1.94 -1.9,1.94 -1.9,3.3 0,0.8 0.7,1.8 0.9,1.3 2.2,1.3 2.6,0 3.5,-2.9 0.5,-2.6 1,-5.16 z"
|
||||
style="fill:#333333"
|
||||
id="path16" />
|
||||
<path
|
||||
d="m 203.8,42.4 q -0.4,0.4 -1.5,0.4 -0.9,0 -2.5,-0.3 -1.7,-0.3 -2.5,-0.3 -4.7,0 -5.5,6.9 -0.3,3.1 -0.4,3.3 -0.4,1 -1.7,2.3 h -1.1 q -0.7,-1.2 -1.3,-4.1 -0.6,-2.76 -0.6,-4.27 0,-1.16 0.1,-1.5 0.2,-0.54 1,-0.54 0.3,0 0.6,0.3 0.4,0.3 0.4,0.3 1.9,-3.53 3.1,-4.6 1.8,-1.7 5.1,-1.7 1.4,0 3.6,0.9 2.8,1.16 3.3,2.8 z"
|
||||
style="fill:#333333"
|
||||
id="path17" />
|
||||
<path
|
||||
d="m 229.5,37.16 q 0.3,0.8 0.3,1.44 0,1.86 -2.4,1.86 -1,0 -3.5,-0.5 -2.5,-0.54 -3.4,-0.54 -1.3,0 -1.5,0.1 -0.4,0.2 -0.4,1.2 0,2.2 0.6,6.9 0.7,5.86 1.6,6.13 -0.4,0.35 -0.4,1.1 -1.2,0.7 -2.6,0.7 -1.4,0 -2,-3.9 -0.2,-1.36 -0.5,-7.76 -0.2,-4.6 -0.8,-5.5 -0.3,-0.47 -4.3,-0.35 -1,0 -1.6,0.1 -0.5,0 -0.3,0 -0.8,0 -1.2,-0.7 -0.5,-1.3 -0.5,-1.4 0,-1.44 4.1,-2 1.6,-0.16 4.7,-0.5 0,-0.85 -0.1,-2.56 0,-1.75 0,-2.6 0,-4.35 2.1,-4.35 0.5,0 1.1,0.6 0.6,0.6 0.6,1.1 v 7.9 q 1.1,1.2 5,1.7 3.9,0.5 5.3,1.86 z"
|
||||
style="fill:#333333"
|
||||
id="path18" />
|
||||
<path
|
||||
d="m 251.2,40.2 q -2,4.1 -3.2,7 -0.1,0.3 -1.5,4.5 -0.5,1.36 -1,4.2 -0.5,2.83 -1,4.2 -1,2.83 -2.4,2.64 -1.4,-0.2 -1.5,-1.6 -0.1,-0.2 -0.1,-0.5 0,-0.16 0.3,-1.5 1.1,-5.04 1.1,-6.44 0,-0.54 -0.1,-0.74 -1.4,-2.44 -4.1,-7.4 -2.7,-4.97 -2.4,-7.7 1.4,-1.36 2.1,-1.36 0.4,0 1,0.6 0.6,0.6 0.7,1.1 0.9,6.2 4.9,11.1 1,-1.8 1.9,-4.04 0.5,-1.4 1.6,-4.15 1.8,-4.46 3.4,-4.46 0.2,0 0.4,0.1 0.8,0.3 1.2,2.8 z"
|
||||
style="fill:#333333"
|
||||
id="path19" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer5"
|
||||
inkscape:label="tagger">
|
||||
<g
|
||||
id="g1">
|
||||
<path
|
||||
id="path4"
|
||||
style="fill:#333333"
|
||||
d="m 111.4,83.335 -9.526,5.5 2.5,4.33 9.526,-5.5 z m -33.775,19.5 -9.526,5.5 2.5,4.33 9.526,-5.5 z"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
<path
|
||||
id="path5"
|
||||
style="fill:#333333"
|
||||
d="M 88.5,73 V 84 h 5 V 73 Z m 0,39 v 11 h 5 V 112 Z"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
<path
|
||||
id="path6"
|
||||
style="fill:#333333"
|
||||
d="m 68.1,87.665 9.526,5.5 2.5,-4.33 -9.526,-5.5 z m 33.775,19.5 9.527,5.5 2.5,-4.33 -9.527,-5.5 z"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
</g>
|
||||
<g
|
||||
id="g2"
|
||||
transform="rotate(30,150,318.19)">
|
||||
<path
|
||||
id="path7"
|
||||
style="fill:#333333"
|
||||
d="m 111.4,83.335 -9.526,5.5 2.5,4.33 9.526,-5.5 z m -33.775,19.5 -9.526,5.5 2.5,4.33 9.526,-5.5 z"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
<path
|
||||
id="path8"
|
||||
style="fill:#333333"
|
||||
d="M 88.5,73 V 84 h 5 V 73 Z m 0,39 v 11 h 5 V 112 Z"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
<path
|
||||
id="path9"
|
||||
style="fill:#333333"
|
||||
d="m 68.1,87.665 9.526,5.5 2.5,-4.33 -9.526,-5.5 z m 33.775,19.5 9.527,5.5 2.5,-4.33 -9.527,-5.5 z"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 8.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#FFF"><path d="M7.05 40q-1.2 0-2.1-.925-.9-.925-.9-2.075V11q0-1.15.9-2.075Q5.85 8 7.05 8h14l3 3h17q1.15 0 2.075.925.925.925.925 2.075v23q0 1.15-.925 2.075Q42.2 40 41.05 40Zm0-29v26h34V14H22.8l-3-3H7.05Zm0 0v26Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 289 B |
@@ -1,47 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="1024" height="1024">
|
||||
<path d="M0 0 C337.92 0 675.84 0 1024 0 C1024 337.92 1024 675.84 1024 1024 C686.08 1024 348.16 1024 0 1024 C0 686.08 0 348.16 0 0 Z " fill="#020708" transform="translate(0,0)"/>
|
||||
<path d="M0 0 C40.22460379 0 80.4466421 0.00304941 120.67057991 0.15463066 C125.95372492 0.17442425 131.23687063 0.19398844 136.52001762 0.21324348 C147.77834068 0.25435249 159.03666181 0.29592484 170.29497623 0.33933926 C170.99953161 0.34205531 171.70408698 0.34477135 172.42999252 0.34756971 C173.13533008 0.3502892 173.84066764 0.35300869 174.56737906 0.35581058 C175.99454502 0.36131272 177.42171098 0.36681401 178.84887695 0.37231445 C179.55666117 0.37504276 180.26444539 0.37777106 180.99367761 0.38058204 C192.50410753 0.42485496 204.01454726 0.46550406 215.52499563 0.50465261 C227.56090905 0.54564378 239.59680781 0.58979401 251.63269895 0.63687855 C258.30394122 0.66290814 264.97518119 0.68769341 271.64644051 0.70907402 C277.91414179 0.72924498 284.18181589 0.75352717 290.4494915 0.78049088 C292.69376491 0.78952735 294.93804399 0.79730645 297.18232727 0.80341911 C322.46351662 0.87417207 347.71815265 1.5042775 373 2 C373 96.05 373 190.1 373 287 C249.91 287 126.82 287 0 287 C0 192.29 0 97.58 0 0 Z " fill="#999F9E" transform="translate(326,395)"/>
|
||||
<path d="M0 0 C37.29 0 74.58 0 113 0 C113 46.86 113 93.72 113 142 C75.71 142 38.42 142 0 142 C0 95.14 0 48.28 0 0 Z " fill="#181E23" transform="translate(550,445)"/>
|
||||
<path d="M0 0 C37.29 0 74.58 0 113 0 C113 46.53 113 93.06 113 141 C95.613125 141.0309375 95.613125 141.0309375 77.875 141.0625 C74.3054248 141.071604 70.73584961 141.08070801 67.05810547 141.09008789 C62.46899414 141.09460449 62.46899414 141.09460449 60.27615356 141.09544373 C58.84198693 141.09691347 57.40782093 141.10027703 55.97366333 141.10557556 C37.3007381 141.17069509 18.67741441 140.49151091 0 140 C0 93.8 0 47.6 0 0 Z " fill="#181E23" transform="translate(362,446)"/>
|
||||
<path d="M0 0 C0 14.52 0 29.04 0 44 C-118.14 44 -236.28 44 -358 44 C-359.71802382 40.56395235 -359.1196618 36.61561104 -359.09765625 32.8359375 C-359.0962413 31.92872955 -359.09482635 31.02152161 -359.09336853 30.08682251 C-359.08776096 27.18284845 -359.07520718 24.27895085 -359.0625 21.375 C-359.05748598 19.40885528 -359.0529229 17.44270935 -359.04882812 15.4765625 C-359.03778906 10.6510121 -359.02051935 5.82551903 -359 1 C-319.04215869 0.87185681 -279.08431526 0.74439427 -239.12646896 0.61781773 C-234.3862332 0.60280127 -229.64599745 0.58777929 -224.90576172 0.57275391 C-223.49049118 0.56826798 -223.49049118 0.56826798 -222.04662931 0.56369144 C-206.8611473 0.51554559 -191.67566674 0.46695532 -176.49018669 0.41819459 C-160.85514345 0.36800044 -145.22009869 0.3182983 -129.58505249 0.26903296 C-120.82560112 0.24141872 -112.06615054 0.21359008 -103.30670166 0.18519592 C-68.87102254 0.07367311 -34.43592726 -0.03058253 0 0 Z " fill="#FEFEFE" transform="translate(692,838)"/>
|
||||
<path d="M0 0 C117.15 0 234.3 0 355 0 C355 13.86 355 27.72 355 42 C237.85 42 120.7 42 0 42 C0 28.14 0 14.28 0 0 Z " fill="#FEFEFE" transform="translate(334,135)"/>
|
||||
<path d="M0 0 C11.88 0 23.76 0 36 0 C36 19.8 36 39.6 36 60 C91.44 60 146.88 60 204 60 C204 71.55 204 83.1 204 95 C168.85263573 95.02994071 133.70661685 94.95253395 98.55956407 94.80645666 C91.61749391 94.77764681 84.67541903 94.75005412 77.7333438 94.72249681 C65.36625264 94.67337442 52.99916523 94.62336685 40.63208008 94.57275391 C28.66105336 94.52376386 16.69002497 94.47522104 4.71899414 94.42724609 C3.97369616 94.4242591 3.22839818 94.4212721 2.46051542 94.41819459 C-1.2806363 94.4032035 -5.02178805 94.38822085 -8.76293981 94.37324238 C-39.50863188 94.25013489 -70.25431782 94.12555501 -101 94 C-101 93.67 -101 93.34 -101 93 C-134.66 92.505 -134.66 92.505 -169 92 C-169 81.44 -169 70.88 -169 60 C-113.23 60 -57.46 60 0 60 C0 40.2 0 20.4 0 0 Z " fill="#191F24" transform="translate(495,302)"/>
|
||||
<path d="M0 0 C62.7 0 125.4 0 190 0 C190 11.88 190 23.76 190 36 C127.3 36 64.6 36 0 36 C0 24.12 0 12.24 0 0 Z " fill="#474B51" transform="translate(418,786)"/>
|
||||
<path d="M0 0 C49.5 0 99 0 150 0 C150 14.52 150 29.04 150 44 C100.5 44 51 44 0 44 C0 29.48 0 14.96 0 0 Z " fill="#FDFEFE" transform="translate(200,239)"/>
|
||||
<path d="M0 0 C48.18 0 96.36 0 146 0 C146 14.52 146 29.04 146 44 C131.979151 44.02742613 117.95836252 44.05097148 103.9375 44.0625 C102.90375118 44.06335602 101.87000237 44.06421204 100.80492783 44.06509399 C67.19004349 44.08969196 33.60574878 43.83234053 0 43 C0 28.81 0 14.62 0 0 Z " fill="#FEFEFE" transform="translate(677,239)"/>
|
||||
<path d="M0 0 C51.48 0 102.96 0 156 0 C156 13.53 156 27.06 156 41 C104.52 41 53.04 41 0 41 C0 27.47 0 13.94 0 0 Z " fill="#FEFEFE" transform="translate(259,188)"/>
|
||||
<path d="M0 0 C1.28729507 -0.00281982 2.57459015 -0.00563965 3.90089417 -0.00854492 C5.34078993 -0.00660032 6.78068557 -0.00455213 8.22058105 -0.00241089 C9.72647647 -0.00376487 11.23237154 -0.00554479 12.73826599 -0.00772095 C16.839391 -0.01229564 20.94049097 -0.01050558 25.04161644 -0.00734186 C29.32396896 -0.00481844 33.60631927 -0.00715575 37.88867188 -0.00872803 C45.08150621 -0.01054978 52.2743303 -0.00814327 59.46716309 -0.00338745 C67.79516627 0.00205518 76.12314497 0.00029253 84.45114756 -0.00521386 C91.58828139 -0.00974483 98.72540783 -0.01039561 105.86254263 -0.00777519 C110.13098648 -0.00621233 114.39941881 -0.00601889 118.66786194 -0.00931168 C122.67984227 -0.01219017 126.69179089 -0.01021511 130.70376778 -0.00441742 C132.18045054 -0.0030897 133.65713595 -0.00348413 135.13381767 -0.00568008 C137.1412128 -0.0083911 139.14861372 -0.0043972 141.15600586 0 C142.28205986 0.00037703 143.40811386 0.00075405 144.56829071 0.0011425 C147.07800293 0.12698364 147.07800293 0.12698364 148.07800293 1.12698364 C148.07800293 14.32698364 148.07800293 27.52698364 148.07800293 41.12698364 C96.92800293 41.12698364 45.77800293 41.12698364 -6.92199707 41.12698364 C-6.92199707 0.00231762 -6.92199707 0.00231762 0 0 Z " fill="#FEFEFE" transform="translate(616.9219970703125,187.87301635742188)"/>
|
||||
<path d="M0 0 C43.23 0 86.46 0 131 0 C131 15.84 131 31.68 131 48 C87.77 48 44.54 48 0 48 C0 32.16 0 16.32 0 0 Z " fill="#FEFEFE" transform="translate(128,353)"/>
|
||||
<path d="M0 0 C67.815 0.495 67.815 0.495 137 1 C137 15.19 137 29.38 137 44 C136.34 44 135.68 44 135 44 C135 44.66 135 45.32 135 46 C90.45 46 45.9 46 0 46 C0 30.82 0 15.64 0 0 Z " fill="#FDFEFE" transform="translate(156,294)"/>
|
||||
<path d="M0 0 C45.21 0 90.42 0 137 0 C137 14.19 137 28.38 137 43 C136.34 43 135.68 43 135 43 C135 43.66 135 44.32 135 45 C90.45 45 45.9 45 0 45 C0 30.15 0 15.3 0 0 Z " fill="#FBFCFC" transform="translate(732,295)"/>
|
||||
<path d="M0 0 C43.23 0 86.46 0 131 0 C131 15.18 131 30.36 131 46 C98.474264 46.72107728 65.97062256 47.09604795 33.4375 47.0625 C32.60141457 47.06164398 31.76532913 47.06078796 30.90390778 47.05990601 C20.60257228 47.04857042 10.30132061 47.02553343 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FEFEFE" transform="translate(765,354)"/>
|
||||
<path d="M0 0 C26.73 0 53.46 0 81 0 C82.19877676 50.94801223 82.19877676 50.94801223 82 74 C77.05167364 74.80762419 72.35065215 75.12200785 67.32055664 75.11352539 C66.57629897 75.11364593 65.8320413 75.11376646 65.06523037 75.11389065 C62.68017418 75.11315027 60.29519828 75.10556237 57.91015625 75.09765625 C56.42801959 75.09618411 54.9458824 75.09516508 53.46374512 75.09460449 C47.99665543 75.08938309 42.52957691 75.07542183 37.0625 75.0625 C24.831875 75.041875 12.60125 75.02125 0 75 C0 50.25 0 25.5 0 0 Z " fill="#FB4740" transform="translate(473,226)"/>
|
||||
<path d="M0 0 C36.96 0 73.92 0 112 0 C112.6261198 6.88731776 113.15084094 13.48218949 113.1328125 20.3515625 C113.13376923 21.17707611 113.13472595 22.00258972 113.13571167 22.8531189 C113.1363846 24.57216469 113.13459319 26.29121317 113.13037109 28.01025391 C113.12494037 30.65402778 113.13038111 33.29763707 113.13671875 35.94140625 C113.13605916 37.61979204 113.13478047 39.29817773 113.1328125 40.9765625 C113.13483673 41.76809723 113.13686096 42.55963196 113.13894653 43.37515259 C113.11499765 48.88500235 113.11499765 48.88500235 112 50 C110.54518505 50.0956161 109.08574514 50.12188351 107.62779236 50.12025452 C106.68403244 50.12162918 105.74027252 50.12300385 104.76791382 50.12442017 C103.72422638 50.12082489 102.68053894 50.11722961 101.60522461 50.11352539 C100.51267868 50.11367142 99.42013275 50.11381744 98.29447937 50.1139679 C94.66374767 50.11326822 91.03306654 50.10547329 87.40234375 50.09765625 C84.89270557 50.09579226 82.38306702 50.09436827 79.87342834 50.09336853 C73.93065407 50.08993348 67.98789616 50.08204273 62.04513031 50.07201904 C53.43224996 50.0578058 44.81936343 50.05254739 36.2064743 50.04621124 C24.13763437 50.03649856 12.06884226 50.01734306 0 50 C0 33.5 0 17 0 0 Z " fill="#FDFDFD" transform="translate(117,474)"/>
|
||||
<path d="M0 0 C36.63 0 73.26 0 111 0 C111 48 111 48 110 50 C73.7 50 37.4 50 0 50 C0 33.5 0 17 0 0 Z " fill="#FEFEFE" transform="translate(795,474)"/>
|
||||
<path d="M0 0 C54.12 0 108.24 0 164 0 C164 10.89 164 21.78 164 33 C109.88 33 55.76 33 0 33 C0 22.11 0 11.22 0 0 Z " fill="#181E23" transform="translate(431,622)"/>
|
||||
<path d="M0 0 C14.58571527 -0.0225479 29.17141907 -0.04091327 43.75714779 -0.05181217 C50.52910421 -0.05697491 57.30104905 -0.06401718 64.07299805 -0.07543945 C70.60224426 -0.08644714 77.13148294 -0.09227625 83.66073799 -0.09487724 C86.15793612 -0.09673199 88.65513355 -0.10035354 91.15232658 -0.10573006 C94.63664069 -0.11293684 98.12090431 -0.1139911 101.60522461 -0.11352539 C102.64891205 -0.11712067 103.69259949 -0.12071594 104.76791382 -0.12442017 C105.71167374 -0.1230455 106.65543365 -0.12167084 107.62779236 -0.12025452 C108.45279708 -0.12117631 109.2778018 -0.1220981 110.12780666 -0.12304783 C112 0 112 0 113 1 C113 16.18 113 31.36 113 47 C75.71 47 38.42 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FCFCFC" transform="translate(117,414)"/>
|
||||
<path d="M0 0 C0 15.84 0 31.68 0 48 C-36.96 48 -73.92 48 -112 48 C-112 32.49 -112 16.98 -112 1 C-92.15950898 0.75043408 -92.15950898 0.75043408 -83.5078125 0.64453125 C-77.67218771 0.57308961 -71.83656856 0.50132395 -66.00097656 0.42724609 C-43.99861261 0.14838814 -22.0044946 -0.06103882 0 0 Z " fill="#FAFAFA" transform="translate(906,537)"/>
|
||||
<path d="M0 0 C36.63 0 73.26 0 111 0 C111 15.51 111 31.02 111 47 C74.37 47 37.74 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FBFCFC" transform="translate(118,538)"/>
|
||||
<path d="M0 0 C36.3 0 72.6 0 110 0 C110 15.18 110 30.36 110 46 C86.98227311 46.96572558 64.03031552 47.12476904 40.99664307 47.06866455 C36.24190736 47.05823048 31.48716586 47.05382501 26.73242188 47.04882812 C17.48826669 47.03826689 8.24413623 47.02131845 -1 47 C-1.02529825 40.62793828 -1.04284496 34.25588946 -1.05493164 27.88378906 C-1.05996891 25.71483883 -1.06679891 23.545892 -1.07543945 21.37695312 C-1.08753151 18.26431437 -1.09323428 15.1517204 -1.09765625 12.0390625 C-1.10281754 11.065065 -1.10797882 10.0910675 -1.11329651 9.08755493 C-1.11337204 8.18677216 -1.11344757 7.28598938 -1.11352539 6.35791016 C-1.115746 5.56293121 -1.11796661 4.76795227 -1.12025452 3.94888306 C-1 2 -1 2 0 0 Z " fill="#FBFCFB" transform="translate(795,414)"/>
|
||||
<path d="M0 0 C34.98 0 69.96 0 106 0 C106 15.84 106 31.68 106 48 C71.02 48 36.04 48 0 48 C0 32.16 0 16.32 0 0 Z " fill="#FBFCFC" transform="translate(154,658)"/>
|
||||
<path d="M0 0 C34.32 0 68.64 0 104 0 C104 15.84 104 31.68 104 48 C69.35 48 34.7 48 -1 48 C-1 31.99652815 -0.6951464 15.9883671 0 0 Z " fill="#FBFBFB" transform="translate(765,658)"/>
|
||||
<path d="M0 0 C10.56252046 -0.02735617 21.1249617 -0.05092969 31.6875 -0.0625 C32.46348038 -0.06335602 33.23946075 -0.06421204 34.03895569 -0.06509399 C57.71503621 -0.08818645 81.33439347 0.16962784 105 1 C105 16.18 105 31.36 105 47 C70.35 47 35.7 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FCFDFD" transform="translate(125,598)"/>
|
||||
<path d="M0 0 C34.32 0 68.64 0 104 0 C104 15.51 104 31.02 104 47 C69.68 47 35.36 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FBFCFC" transform="translate(794,598)"/>
|
||||
<path d="M0 0 C37.62 0 75.24 0 114 0 C114 12.54 114 25.08 114 38 C76.38 38 38.76 38 0 38 C0 25.46 0 12.92 0 0 Z " fill="#474B51" transform="translate(456,716)"/>
|
||||
<path d="M0 0 C30.69 0 61.38 0 93 0 C93 15.18 93 30.36 93 46 C62.31 46 31.62 46 0 46 C0 30.82 0 15.64 0 0 Z " fill="#FCFDFD" transform="translate(732,719)"/>
|
||||
<path d="M0 0 C30.69 0 61.38 0 93 0 C93 14.85 93 29.7 93 45 C62.31 45 31.62 45 0 45 C0 30.15 0 15.3 0 0 Z " fill="#FEFEFE" transform="translate(200,720)"/>
|
||||
<path d="M0 0 C11.22 0 22.44 0 34 0 C34.6943434 30.12954393 35.09747148 60.23833134 35.0625 90.375 C35.06121597 91.48948643 35.06121597 91.48948643 35.05990601 92.62648773 C35.04860423 101.7510267 35.02558276 110.87548153 35 120 C23.45 120 11.9 120 0 120 C0 80.4 0 40.8 0 0 Z " fill="#474B50" transform="translate(258,470)"/>
|
||||
<path d="M0 0 C30.36 0 60.72 0 92 0 C92 14.19 92 28.38 92 43 C61.64 43 31.28 43 0 43 C0 28.81 0 14.62 0 0 Z " fill="#FCFDFD" transform="translate(259,781)"/>
|
||||
<path d="M0 0 C30.03 0 60.06 0 91 0 C91 14.19 91 28.38 91 43 C60.97 43 30.94 43 0 43 C0 28.81 0 14.62 0 0 Z " fill="#FCFDFD" transform="translate(674,781)"/>
|
||||
<path d="M0 0 C16.5 0 33 0 50 0 C50 25.41 50 50.82 50 77 C33.5 77 17 77 0 77 C0 51.59 0 26.18 0 0 Z " fill="#2CCDD4" transform="translate(582,478)"/>
|
||||
<path d="M0 0 C10.56 0 21.12 0 32 0 C32 39.27 32 78.54 32 119 C21.44 119 10.88 119 0 119 C0 79.73 0 40.46 0 0 Z " fill="#474B50" transform="translate(732,471)"/>
|
||||
<path d="M0 0 C16.17 0 32.34 0 49 0 C49 25.41 49 50.82 49 77 C32.83 77 16.66 77 0 77 C0 51.59 0 26.18 0 0 Z " fill="#2DCCD2" transform="translate(394,478)"/>
|
||||
<path d="M0 0 C15.84 0 31.68 0 48 0 C48 0.33 48 0.66 48 1 C32.49 1 16.98 1 1 1 C1 25.75 1 50.5 1 76 C28.06 75.67 55.12 75.34 83 75 C82.67 65.76 82.34 56.52 82 47 C81.93702338 38.74754429 81.90172306 30.50164687 81.9375 22.25 C81.94254243 20.16145945 81.94710032 18.07291767 81.95117188 15.984375 C81.96192066 10.98955155 81.97902162 5.99479052 82 1 C82.33 1 82.66 1 83 1 C83.57831229 19.95510799 84.11107346 38.91589618 84.23217773 57.88024902 C84.24403757 59.45720133 84.26110851 61.03412324 84.28344727 62.61096191 C84.31307026 64.8085447 84.32331211 67.00536665 84.328125 69.203125 C84.3374707 70.44739258 84.34681641 71.69166016 84.35644531 72.97363281 C84 76 84 76 82.73034668 77.68478394 C80.56608158 79.32981626 79.63802574 79.28920572 76.95800781 79.06982422 C75.71830231 78.98657394 75.71830231 78.98657394 74.45355225 78.90164185 C73.54591125 78.82839691 72.63827026 78.75515198 71.703125 78.6796875 C62.53186675 78.09888659 53.42327768 77.92843278 44.234375 78.0078125 C42.339151 78.0199881 42.339151 78.0199881 40.40563965 78.03240967 C35.17784805 78.06788344 29.95020985 78.1100683 24.72265625 78.17138672 C20.85162217 78.21602536 16.98061225 78.23702797 13.109375 78.2578125 C11.32536285 78.28574387 11.32536285 78.28574387 9.50531006 78.3142395 C7.87024506 78.31942093 7.87024506 78.31942093 6.20214844 78.32470703 C4.76279938 78.33922409 4.76279938 78.33922409 3.29437256 78.35403442 C2.15865814 78.17878738 2.15865814 78.17878738 1 78 C-1.11766624 74.82350064 -1.24885987 74.11349575 -1.23046875 70.48828125 C-1.22917969 69.52510986 -1.22789062 68.56193848 -1.2265625 67.56958008 C-1.21367187 66.49474365 -1.20078125 65.41990723 -1.1875 64.3125 C-1.18105469 63.18158936 -1.17460937 62.05067871 -1.16796875 60.88549805 C-1.00069226 40.5888483 -0.4546929 20.29067045 0 0 Z " fill="#4C0F0C" transform="translate(472,225)"/>
|
||||
<path d="M0 0 C8.45159814 -0.02350035 16.90318192 -0.04110214 25.35480499 -0.05181217 C29.28212158 -0.05696064 33.20941531 -0.06390567 37.13671875 -0.07543945 C53.56520732 -0.12238185 69.94755695 -0.05605073 86.36132812 0.74389648 C96.40041688 1.22706882 106.45208983 1.36454744 116.5 1.5625 C118.60677558 1.60589009 120.71354645 1.64950917 122.8203125 1.69335938 C127.88015843 1.79819282 132.94005639 1.8999973 138 2 C138 2.33 138 2.66 138 3 C92.79 3 47.58 3 1 3 C1 5.64 1 8.28 1 11 C0.67 11 0.34 11 0 11 C0 7.37 0 3.74 0 0 Z " fill="#6F7576" transform="translate(326,395)"/>
|
||||
<path d="M0 0 C0.33 0 0.66 0 1 0 C1 7.26 1 14.52 1 22 C37.3 22 73.6 22 111 22 C111.495 22.99 111.495 22.99 112 24 C74.71 24 37.42 24 -1 24 C-0.67 16.08 -0.34 8.16 0 0 Z " fill="#0E1418" transform="translate(551,563)"/>
|
||||
<path d="M0 0 C0.33 0 0.66 0 1 0 C1.57831229 18.95510799 2.11107346 37.91589618 2.23217773 56.88024902 C2.24403757 58.45720133 2.26110851 60.03412324 2.28344727 61.61096191 C2.31307026 63.8085447 2.32331211 66.00536665 2.328125 68.203125 C2.3374707 69.44739258 2.34681641 70.69166016 2.35644531 71.97363281 C2 75 2 75 0.79858398 76.92016602 C-1.65515594 78.39334257 -3.26106965 78.2550765 -6.10546875 78.07421875 C-7.08837891 78.01943359 -8.07128906 77.96464844 -9.08398438 77.90820312 C-10.62022461 77.79895508 -10.62022461 77.79895508 -12.1875 77.6875 C-13.22326172 77.62626953 -14.25902344 77.56503906 -15.32617188 77.50195312 C-17.88531957 77.34864013 -20.44267045 77.1807893 -23 77 C-23.33 76.34 -23.66 75.68 -24 75 C-11.625 74.505 -11.625 74.505 1 74 C0.835 70.32875 0.67 66.6575 0.5 62.875 C-0.33621628 41.9290255 -0.08802477 20.95802133 0 0 Z " fill="#390D0B" transform="translate(554,226)"/>
|
||||
<path d="M0 0 C0.66 0 1.32 0 2 0 C2 15.18 2 30.36 2 46 C0.68 45.67 -0.64 45.34 -2 45 C-1.88269497 38.75188831 -1.75785931 32.50396027 -1.62768555 26.25610352 C-1.58426577 24.12885993 -1.54259607 22.0015799 -1.50268555 19.87426758 C-1.44515293 16.82361849 -1.38141033 13.77315411 -1.31640625 10.72265625 C-1.29969376 9.76537125 -1.28298126 8.80808624 -1.26576233 7.8217926 C-1.24581711 6.94095505 -1.22587189 6.06011749 -1.20532227 5.15258789 C-1.18977798 4.37315323 -1.1742337 3.59371857 -1.15821838 2.79066467 C-1 1 -1 1 0 0 Z " fill="#D1D3D3" transform="translate(228,599)"/>
|
||||
<path d="M0 0 C35.64 0.33 71.28 0.66 108 1 C108 1.33 108 1.66 108 2 C54.54 2.495 54.54 2.495 0 3 C0 2.01 0 1.02 0 0 Z " fill="#F5F7F7" transform="translate(117,521)"/>
|
||||
<path d="M0 0 C6.93 0 13.86 0 21 0 C21 0.99 21 1.98 21 3 C14.07 3 7.14 3 0 3 C0 2.01 0 1.02 0 0 Z " fill="#272A2A" transform="translate(158,292)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,6 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 693 B |
|
Before Width: | Height: | Size: 27 KiB |
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 440 440">
|
||||
<g id="Layer_6" data-name="Layer 6">
|
||||
<rect x="12" y="12" width="416" height="416" rx="103" ry="103" fill="#3a78b1" stroke-width="0"/>
|
||||
</g>
|
||||
<g id="Layer_8" data-name="Layer 8">
|
||||
<path d="M83.6,373.6v-178.6c0-63,52.4-143.7,142.7-143.7s144.1,69.7,144.1,141.1c0,122.6-116.6,143.1-116.6,143.1,0,0,7.2-11.5,7.2-29.5s-8-28-8-28c0,0,60-16,60-87s-53-83-86-83c-57,0-88.3,48.5-88.3,84.3v181.9c0,25.9-17.5,23.9-17.5,23.9h-19.2s-18.4,0-18.4-24.4Z" fill="#fff" stroke-width="0"/>
|
||||
</g>
|
||||
<g id="Layer_9" data-name="Layer 9">
|
||||
<circle cx="204.9" cy="306.6" r="48" fill="#fff" stroke-width="0"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 727 B |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,5 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -124,23 +124,18 @@ This script generates:
|
||||
- Accurate description and usage examples
|
||||
- Correct icon path (usually `../../../../.icons/your-icon.svg`)
|
||||
- Proper tags that describe your module
|
||||
3. **Create tests for your module:**
|
||||
- **Terraform tests**: Create a `*.tftest.hcl` file and test with `terraform test`
|
||||
- **TypeScript tests**: Create `main.test.ts` file if your module runs scripts or has business logic that Terraform tests can't cover
|
||||
3. **Create at least one `.tftest.hcl`** to test your module with `terraform test`
|
||||
4. **Add any scripts** or additional files your module needs
|
||||
|
||||
### 4. Test and Submit
|
||||
|
||||
```bash
|
||||
# Test your module
|
||||
cd registry/[namespace]/modules/[module-name]
|
||||
|
||||
# Required: Test Terraform functionality
|
||||
# Test your module (from the module directory)
|
||||
terraform init -upgrade
|
||||
terraform test -verbose
|
||||
|
||||
# Optional: Test TypeScript files if you have main.test.ts
|
||||
bun test main.test.ts
|
||||
# Or run all tests in the repo
|
||||
./scripts/terraform_test_all.sh
|
||||
|
||||
# Format code
|
||||
bun run fmt
|
||||
@@ -348,8 +343,8 @@ coder templates push test-[template-name] -d .
|
||||
terraform init -upgrade
|
||||
terraform test -verbose
|
||||
|
||||
# Optional: If you have TypeScript tests
|
||||
bun test main.test.ts
|
||||
# Test all modules
|
||||
./scripts/terraform_test_all.sh
|
||||
```
|
||||
|
||||
### 3. Maintain Backward Compatibility
|
||||
@@ -398,9 +393,7 @@ Example: `https://github.com/coder/registry/compare/main...your-branch?template=
|
||||
### Every Module Must Have
|
||||
|
||||
- `main.tf` - Terraform code
|
||||
- **Tests**:
|
||||
- `*.tftest.hcl` files with `terraform test` (to test terraform specific logic)
|
||||
- `main.test.ts` file with `bun test` (to test business logic, i.e., `coder_script` to install a package.)
|
||||
- One or more `.tftest.hcl` files - Working tests with `terraform test`
|
||||
- `README.md` - Documentation with frontmatter
|
||||
|
||||
### Every Template Must Have
|
||||
@@ -500,10 +493,6 @@ When reporting bugs, include:
|
||||
2. **No tests** or broken tests
|
||||
3. **Hardcoded values** instead of variables
|
||||
4. **Breaking changes** without defaults
|
||||
5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`, and `bun test main.test.ts` if applicable) before submitting
|
||||
|
||||
## For Maintainers
|
||||
|
||||
Guidelines for reviewing PRs, managing releases, and maintaining the registry. [See the maintainer guide for detailed information.](./MAINTAINER.md)
|
||||
5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`) before submitting
|
||||
|
||||
Happy contributing! 🚀
|
||||
|
||||
@@ -23,7 +23,6 @@ Check that PRs have:
|
||||
- [ ] Working tests (`terraform test`)
|
||||
- [ ] Formatted code (`bun run fmt`)
|
||||
- [ ] Avatar image for new namespaces (`avatar.png` or `avatar.svg` in `.images/`)
|
||||
- [ ] Version label: `version:patch`, `version:minor`, or `version:major`
|
||||
|
||||
### Version Guidelines
|
||||
|
||||
@@ -33,8 +32,7 @@ When reviewing PRs, ensure the version change follows semantic versioning:
|
||||
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
|
||||
- **Major** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing types)
|
||||
|
||||
PRs should clearly indicate the intended version change (e.g., `v1.2.3 → v1.2.4`) and include the appropriate label: `version:patch`, `version:minor`, or `version:major`.
|
||||
The “Version Bump” CI uses this label to validate required updates (README version refs, etc.).
|
||||
PRs should clearly indicate the version change (e.g., `v1.2.3 → v1.2.4`).
|
||||
|
||||
### Validate READMEs
|
||||
|
||||
|
||||
@@ -48,7 +48,3 @@ 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)
|
||||
|
||||
@@ -15,7 +15,7 @@ run "app_url_uses_port" {
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.module_name.url == "http://localhost:19999"
|
||||
error_message = "Expected module-name app URL to include configured port"
|
||||
condition = resource.coder_app.MODULE_NAME.url == "http://localhost:19999"
|
||||
error_message = "Expected MODULE_NAME app URL to include configured port"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +35,13 @@ variable "agent_id" {
|
||||
|
||||
variable "log_path" {
|
||||
type = string
|
||||
description = "The path to the module log file."
|
||||
default = "/tmp/module_name.log"
|
||||
description = "The path to log MODULE_NAME to."
|
||||
default = "/tmp/MODULE_NAME.log"
|
||||
}
|
||||
|
||||
variable "port" {
|
||||
type = number
|
||||
description = "The port to run the application on."
|
||||
description = "The port to run MODULE_NAME on."
|
||||
default = 19999
|
||||
}
|
||||
|
||||
@@ -59,9 +59,9 @@ variable "order" {
|
||||
# Add other variables here
|
||||
|
||||
|
||||
resource "coder_script" "module_name" {
|
||||
resource "coder_script" "MODULE_NAME" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Module Name"
|
||||
display_name = "MODULE_NAME"
|
||||
icon = local.icon_url
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
LOG_PATH : var.log_path,
|
||||
@@ -70,10 +70,10 @@ resource "coder_script" "module_name" {
|
||||
run_on_stop = false
|
||||
}
|
||||
|
||||
resource "coder_app" "module_name" {
|
||||
resource "coder_app" "MODULE_NAME" {
|
||||
agent_id = var.agent_id
|
||||
slug = "module-name"
|
||||
display_name = "Module Name"
|
||||
slug = "MODULE_NAME"
|
||||
display_name = "MODULE_NAME"
|
||||
url = "http://localhost:${var.port}"
|
||||
icon = local.icon_url
|
||||
subdomain = false
|
||||
@@ -88,10 +88,10 @@ resource "coder_app" "module_name" {
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "module_name" {
|
||||
type = "string"
|
||||
name = "module_name"
|
||||
display_name = "Module Name"
|
||||
data "coder_parameter" "MODULE_NAME" {
|
||||
type = "list(string)"
|
||||
name = "MODULE_NAME"
|
||||
display_name = "MODULE_NAME"
|
||||
icon = local.icon_url
|
||||
mutable = var.mutable
|
||||
default = local.options["Option 1"]["value"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module coder.com/coder-registry
|
||||
|
||||
go 1.24.0
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
cdr.dev/slog v1.6.1
|
||||
@@ -20,7 +20,7 @@ require (
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
go.opentelemetry.io/otel v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.16.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
)
|
||||
|
||||
@@ -51,17 +51,17 @@ go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiM
|
||||
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
|
||||
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
|
||||
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg=
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
"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",
|
||||
"tftest": "./scripts/terraform_test_all.sh",
|
||||
"tstest": "./scripts/ts_test_auto.sh",
|
||||
"test": "./scripts/terraform_test_all.sh",
|
||||
"update-version": "./update-version.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
|
Before Width: | Height: | Size: 451 KiB |
@@ -16,7 +16,7 @@ It can be served on a Coder subdomain for easy access, or on `localhost` if you
|
||||
```tf
|
||||
module "pgadmin" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/AJ0070/pgadmin/coder"
|
||||
source = "registry.coder.com/aj0070/pgadmin/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 28 KiB |
@@ -1,14 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,75 +0,0 @@
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
#!/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,163 +0,0 @@
|
||||
---
|
||||
display_name: Archive
|
||||
description: Create automated and user-invocable scripts that archive and extract selected files/directories with optional compression (gzip or zstd).
|
||||
icon: ../../../../.icons/folder.svg
|
||||
verified: false
|
||||
tags: [backup, archive, tar, helper]
|
||||
---
|
||||
|
||||
# Archive
|
||||
|
||||
This module installs small, robust scripts in your workspace to create and extract tar archives from a list of files and directories. It supports optional compression (gzip or zstd). The create command prints only the resulting archive path to stdout; operational logs go to stderr. An optional stop hook can also create an archive automatically when the workspace stops, and an optional start hook can wait for an archive on-disk and extract it on start.
|
||||
|
||||
```tf
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
paths = ["./projects", "./code"]
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Installs two commands into the workspace `$PATH`: `coder-archive-create` and `coder-archive-extract`.
|
||||
- Creates a single `.tar`, `.tar.gz`, or `.tar.zst` containing selected paths (depends on `tar`).
|
||||
- Optional compression: `gzip`, `zstd` (depends on `gzip` or `zstd`).
|
||||
- Stores defaults so commands can be run without arguments (supports overriding via CLI flags).
|
||||
- Logs and status messages go to stderr, the create command prints only the final archive path to stdout.
|
||||
- Optional:
|
||||
- `create_on_stop` to create an archive automatically when the workspace stops.
|
||||
- `extract_on_start` to wait for an archive to appear and extract it on start.
|
||||
|
||||
> [!WARNING]
|
||||
> The `create_on_stop` feature uses the `coder_script` `run_on_stop` which may not work as expected on certain templates without additional provider configuration. The agent may be terminated before the script completes. See [coder/coder#6174](https://github.com/coder/coder/issues/6174) for provider-specific workarounds and [coder/coder#6175](https://github.com/coder/coder/issues/6175) for tracking a fix.
|
||||
|
||||
## Usage
|
||||
|
||||
Basic example:
|
||||
|
||||
```tf
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
# Paths to include in the archive (files or directories).
|
||||
directory = "~"
|
||||
paths = [
|
||||
"./projects",
|
||||
"./code",
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Customize compression and output:
|
||||
|
||||
```tf
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
directory = "/"
|
||||
paths = ["/etc", "/home"]
|
||||
compression = "zstd" # "gzip" | "zstd" | "none"
|
||||
output_dir = "/tmp/backup" # defaults to /tmp
|
||||
archive_name = "my-backup" # base name (extension is inferred from compression)
|
||||
}
|
||||
```
|
||||
|
||||
Enable auto-archive on stop:
|
||||
|
||||
```tf
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
# Creates /tmp/coder-archive.tar.gz of the users home directory (defaults).
|
||||
create_on_stop = true
|
||||
}
|
||||
```
|
||||
|
||||
Extract on start:
|
||||
|
||||
```tf
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
# Where to look for the archive file to extract:
|
||||
output_dir = "/tmp"
|
||||
archive_name = "my-archive"
|
||||
compression = "gzip"
|
||||
|
||||
# Waits up to 5 minutes for /tmp/my-archive.tar.gz to be present, note that
|
||||
# using a long timeout will delay every workspace start by this much until the
|
||||
# archive is present.
|
||||
extract_on_start = true
|
||||
extract_wait_timeout_seconds = 300
|
||||
}
|
||||
```
|
||||
|
||||
## Command usage
|
||||
|
||||
The installer writes the following files:
|
||||
|
||||
- `$CODER_SCRIPT_DATA_DIR/archive-lib.sh`
|
||||
- `$CODER_SCRIPT_BIN_DIR/coder-archive-create`
|
||||
- `$CODER_SCRIPT_BIN_DIR/coder-archive-extract`
|
||||
|
||||
Create usage:
|
||||
|
||||
```console
|
||||
coder-archive-create [OPTIONS] [PATHS...]
|
||||
-c, --compression <gzip|zstd|none> Compression algorithm (default from module)
|
||||
-C, --directory <DIRECTORY> Change to directory for archiving (default from module)
|
||||
-f, --file <ARCHIVE> Output archive file (default from module)
|
||||
-h, --help Show help
|
||||
```
|
||||
|
||||
Extract usage:
|
||||
|
||||
```console
|
||||
coder-archive-extract [OPTIONS]
|
||||
-c, --compression <gzip|zstd|none> Compression algorithm (default from module)
|
||||
-C, --directory <DIRECTORY> Extract into directory (default from module)
|
||||
-f, --file <ARCHIVE> Archive file to extract (default from module)
|
||||
-h, --help Show help
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
- Use Terraform defaults:
|
||||
|
||||
```
|
||||
coder-archive-create
|
||||
```
|
||||
|
||||
- Override compression and output file at runtime:
|
||||
|
||||
```
|
||||
coder-archive-create --compression zstd --file /tmp/backups/archive.tar.zst
|
||||
```
|
||||
|
||||
- Add extra paths on the fly (in addition to the Terraform defaults):
|
||||
|
||||
```
|
||||
coder-archive-create /etc/hosts
|
||||
```
|
||||
|
||||
- Extract an archive into a directory:
|
||||
|
||||
```
|
||||
coder-archive-extract --file /tmp/backups/archive.tar.gz --directory /tmp/restore
|
||||
```
|
||||
@@ -1,33 +0,0 @@
|
||||
mock_provider "coder" {}
|
||||
|
||||
run "apply_defaults" {
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "agent-123"
|
||||
paths = ["~/project", "/etc/hosts"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.archive_path == "/tmp/coder-archive.tar.gz"
|
||||
error_message = "archive_path should be empty when archive_name is not set"
|
||||
}
|
||||
}
|
||||
|
||||
run "apply_with_name" {
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "agent-123"
|
||||
paths = ["/etc/hosts"]
|
||||
archive_name = "nightly"
|
||||
output_dir = "/tmp/backups"
|
||||
compression = "zstd"
|
||||
create_archive_on_stop = true
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.archive_path == "/tmp/backups/nightly.tar.zst"
|
||||
error_message = "archive_path should be computed from archive_name + output_dir + extension"
|
||||
}
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
import { describe, expect, it, beforeAll } from "bun:test";
|
||||
import {
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
type TerraformState,
|
||||
} from "~test";
|
||||
|
||||
const USE_XTRACE =
|
||||
process.env.ARCHIVE_TEST_XTRACE === "1" || process.env.XTRACE === "1";
|
||||
|
||||
const IMAGE = "alpine";
|
||||
const BIN_DIR = "/tmp/coder-script-data/bin";
|
||||
const DATA_DIR = "/tmp/coder-script-data";
|
||||
|
||||
type ExecResult = {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
const ensureRunOk = (label: string, res: ExecResult) => {
|
||||
if (res.exitCode !== 0) {
|
||||
console.error(
|
||||
`[${label}] non-zero exit code: ${res.exitCode}\n--- stdout ---\n${res.stdout.trim()}\n--- stderr ---\n${res.stderr.trim()}\n--------------`,
|
||||
);
|
||||
}
|
||||
expect(res.exitCode).toBe(0);
|
||||
};
|
||||
|
||||
const sh = async (id: string, cmd: string): Promise<ExecResult> => {
|
||||
const res = await execContainer(id, ["sh", "-c", cmd]);
|
||||
return res;
|
||||
};
|
||||
|
||||
const bashRun = async (id: string, cmd: string): Promise<ExecResult> => {
|
||||
const injected = USE_XTRACE ? `/bin/bash -x ${cmd}` : cmd;
|
||||
return sh(id, injected);
|
||||
};
|
||||
|
||||
const prepareContainer = async (image = IMAGE) => {
|
||||
const id = await runContainer(image);
|
||||
// Prepare script dirs and deps.
|
||||
ensureRunOk(
|
||||
"mkdirs",
|
||||
await sh(id, `mkdir -p ${BIN_DIR} ${DATA_DIR} /tmp/backup`),
|
||||
);
|
||||
|
||||
// Install tools used by tests.
|
||||
ensureRunOk(
|
||||
"apk add",
|
||||
await sh(id, "apk add --no-cache bash tar gzip zstd coreutils"),
|
||||
);
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
const installArchive = async (
|
||||
state: TerraformState,
|
||||
opts?: { env?: string[] },
|
||||
) => {
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await prepareContainer();
|
||||
// Run installer script with correct env for CODER_SCRIPT paths.
|
||||
const args = ["bash"];
|
||||
if (USE_XTRACE) args.push("-x");
|
||||
args.push("-c", instance.script);
|
||||
|
||||
const resp = await execContainer(id, args, [
|
||||
"--env",
|
||||
`CODER_SCRIPT_BIN_DIR=${BIN_DIR}`,
|
||||
"--env",
|
||||
`CODER_SCRIPT_DATA_DIR=${DATA_DIR}`,
|
||||
...(opts?.env ?? []),
|
||||
]);
|
||||
|
||||
return {
|
||||
id,
|
||||
install: {
|
||||
exitCode: resp.exitCode,
|
||||
stdout: resp.stdout.trim(),
|
||||
stderr: resp.stderr.trim(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const fileExists = async (id: string, path: string) => {
|
||||
const res = await sh(id, `test -f ${path} && echo yes || echo no`);
|
||||
return res.stdout.trim() === "yes";
|
||||
};
|
||||
|
||||
const isExecutable = async (id: string, path: string) => {
|
||||
const res = await sh(id, `test -x ${path} && echo yes || echo no`);
|
||||
return res.stdout.trim() === "yes";
|
||||
};
|
||||
|
||||
const listTar = async (id: string, path: string) => {
|
||||
// Try to autodetect compression flags from extension.
|
||||
let cmd = "";
|
||||
if (path.endsWith(".tar.gz")) {
|
||||
cmd = `tar -tzf ${path}`;
|
||||
} else if (path.endsWith(".tar.zst")) {
|
||||
// validate with zstd and ask tar to list via --zstd.
|
||||
cmd = `zstd -t -q ${path} && tar --zstd -tf ${path}`;
|
||||
} else {
|
||||
cmd = `tar -tf ${path}`;
|
||||
}
|
||||
return sh(id, cmd);
|
||||
};
|
||||
|
||||
describe("archive", () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
// Ensure required variables are enforced.
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
});
|
||||
|
||||
it("installs wrapper scripts to BIN_DIR and library to DATA_DIR", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
});
|
||||
|
||||
// The Terraform output should reflect defaults from main.tf.
|
||||
expect(state.outputs.archive_path.value).toEqual(
|
||||
"/tmp/coder-archive.tar.gz",
|
||||
);
|
||||
|
||||
const { id, install } = await installArchive(state);
|
||||
ensureRunOk("install", install);
|
||||
|
||||
expect(install.stdout).toContain(
|
||||
`Installed archive library to: ${DATA_DIR}/archive-lib.sh`,
|
||||
);
|
||||
expect(install.stdout).toContain(
|
||||
`Installed create script to: ${BIN_DIR}/coder-archive-create`,
|
||||
);
|
||||
expect(install.stdout).toContain(
|
||||
`Installed extract script to: ${BIN_DIR}/coder-archive-extract`,
|
||||
);
|
||||
expect(await isExecutable(id, `${BIN_DIR}/coder-archive-create`)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(await isExecutable(id, `${BIN_DIR}/coder-archive-extract`)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses sane defaults: creates gzip archive at the default path and logs to stderr", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
// Keep defaults: compression=gzip, output_dir=/tmp, archive_name=coder-archive.
|
||||
});
|
||||
|
||||
const { id } = await installArchive(state);
|
||||
|
||||
const createTestdata = await bashRun(
|
||||
id,
|
||||
`mkdir ~/gzip; touch ~/gzip/defaults.txt`,
|
||||
);
|
||||
ensureRunOk("create testdata", createTestdata);
|
||||
|
||||
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create`);
|
||||
ensureRunOk("archive-create default run", run);
|
||||
|
||||
// Only the archive path should print to stdout.
|
||||
expect(run.stdout.trim()).toEqual("/tmp/coder-archive.tar.gz");
|
||||
expect(await fileExists(id, "/tmp/coder-archive.tar.gz")).toBe(true);
|
||||
|
||||
// Some useful diagnostics should be on stderr.
|
||||
expect(run.stderr).toContain("Creating archive:");
|
||||
expect(run.stderr).toContain("Compression: gzip");
|
||||
|
||||
const list = await listTar(id, "/tmp/coder-archive.tar.gz");
|
||||
ensureRunOk("list default archive", list);
|
||||
expect(list.stdout).toContain("gzip/defaults.txt");
|
||||
}, 20000);
|
||||
|
||||
it("creates a gzip archive with explicit -f and includes extra CLI paths", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
// Provide a simple default path so we can assert contents.
|
||||
paths: `["~/gzip"]`,
|
||||
compression: "gzip",
|
||||
});
|
||||
|
||||
const { id } = await installArchive(state);
|
||||
|
||||
const createTestdata = await bashRun(
|
||||
id,
|
||||
`mkdir ~/gzip; touch ~/gzip/test.txt; touch ~/gziptest.txt`,
|
||||
);
|
||||
ensureRunOk("create testdata", createTestdata);
|
||||
|
||||
const out = "/tmp/backup/test-archive.tar.gz";
|
||||
const run = await bashRun(
|
||||
id,
|
||||
`${BIN_DIR}/coder-archive-create -f ${out} ~/gziptest.txt`,
|
||||
);
|
||||
ensureRunOk("archive-create gzip explicit -f", run);
|
||||
|
||||
expect(run.stdout.trim()).toEqual(out);
|
||||
expect(await fileExists(id, out)).toBe(true);
|
||||
|
||||
const list = await sh(id, `tar -tzf ${out}`);
|
||||
ensureRunOk("tar -tzf contents (gzip)", list);
|
||||
expect(list.stdout).toContain("gzip/test.txt");
|
||||
expect(list.stdout).toContain("gziptest.txt");
|
||||
}, 20000);
|
||||
|
||||
it("creates a zstd-compressed archive when requested via CLI override", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
paths: `["/etc/hostname"]`,
|
||||
// Module default is gzip, override at runtime to zstd.
|
||||
});
|
||||
|
||||
const { id } = await installArchive(state);
|
||||
|
||||
const out = "/tmp/backup/zstd-archive.tar.zst";
|
||||
const run = await bashRun(
|
||||
id,
|
||||
`${BIN_DIR}/coder-archive-create --compression zstd -f ${out}`,
|
||||
);
|
||||
ensureRunOk("archive-create zstd", run);
|
||||
|
||||
expect(run.stdout.trim()).toEqual(out);
|
||||
|
||||
// Check integrity via zstd and that tar can list it.
|
||||
ensureRunOk("zstd -t", await sh(id, `test -f ${out} && zstd -t -q ${out}`));
|
||||
ensureRunOk("tar --zstd -tf", await sh(id, `tar --zstd -tf ${out}`));
|
||||
}, 30000);
|
||||
|
||||
it("creates an uncompressed tar when compression=none", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
// Keep module defaults but override at runtime.
|
||||
});
|
||||
|
||||
const { id } = await installArchive(state);
|
||||
|
||||
const out = "/tmp/backup/raw-archive.tar";
|
||||
const run = await bashRun(
|
||||
id,
|
||||
`${BIN_DIR}/coder-archive-create --compression none -f ${out}`,
|
||||
);
|
||||
ensureRunOk("archive-create none", run);
|
||||
|
||||
expect(run.stdout.trim()).toEqual(out);
|
||||
ensureRunOk("tar -tf (none)", await sh(id, `tar -tf ${out} >/dev/null`));
|
||||
}, 20000);
|
||||
|
||||
it("applies exclude patterns from Terraform", async () => {
|
||||
// Include a file, but also exclude it via Terraform defaults to ensure
|
||||
// exclusion flows through.
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
paths: `["/etc/hostname"]`,
|
||||
exclude_patterns: `["/etc/hostname"]`,
|
||||
});
|
||||
|
||||
const { id } = await installArchive(state);
|
||||
|
||||
const out = "/tmp/backup/excluded.tar.gz";
|
||||
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create -f ${out}`);
|
||||
ensureRunOk("archive-create with exclude_patterns", run);
|
||||
|
||||
const list = await sh(id, `tar -tzf ${out}`);
|
||||
ensureRunOk("tar -tzf contents (exclude)", list);
|
||||
expect(list.stdout).not.toContain("etc/hostname"); // Excluded by Terraform default.
|
||||
}, 20000);
|
||||
|
||||
it("adds a run_on_stop script when enabled", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
create_on_stop: true,
|
||||
});
|
||||
|
||||
const coderScripts = state.resources.filter(
|
||||
(r) => r.type === "coder_script",
|
||||
);
|
||||
// Installer (run_on_start) + run_on_stop.
|
||||
expect(coderScripts.length).toBe(2);
|
||||
});
|
||||
|
||||
it("extracts a previously created archive into a target directory", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
paths: `["/etc/hostname"]`,
|
||||
compression: "gzip",
|
||||
});
|
||||
|
||||
const { id } = await installArchive(state);
|
||||
|
||||
// Create archive.
|
||||
const out = "/tmp/backup/extract-test.tar.gz";
|
||||
const created = await bashRun(
|
||||
id,
|
||||
`${BIN_DIR}/coder-archive-create -f ${out} /etc/hosts`,
|
||||
);
|
||||
ensureRunOk("create for extract", created);
|
||||
|
||||
// Extract archive.
|
||||
const extractDir = "/tmp/extract";
|
||||
const extract = await bashRun(
|
||||
id,
|
||||
`${BIN_DIR}/coder-archive-extract -f ${out} -C ${extractDir}`,
|
||||
);
|
||||
ensureRunOk("archive-extract", extract);
|
||||
|
||||
// Verify a known file exists after extraction.
|
||||
const exists = await sh(
|
||||
id,
|
||||
`test -f ${extractDir}/etc/hosts && echo ok || echo no`,
|
||||
);
|
||||
expect(exists.stdout.trim()).toEqual("ok");
|
||||
}, 20000);
|
||||
|
||||
it("honors Terraform defaults without CLI args (compression, name, output_dir)", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
compression: "zstd",
|
||||
archive_name: "my-default",
|
||||
output_dir: "/tmp/defout",
|
||||
});
|
||||
|
||||
const { id } = await installArchive(state);
|
||||
|
||||
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create`);
|
||||
ensureRunOk("archive-create terraform defaults", run);
|
||||
expect(run.stdout.trim()).toEqual("/tmp/defout/my-default.tar.zst");
|
||||
expect(run.stderr).toContain("Creating archive:");
|
||||
expect(run.stderr).toContain("Compression: zstd");
|
||||
ensureRunOk(
|
||||
"zstd -t",
|
||||
await sh(id, "zstd -t -q /tmp/defout/my-default.tar.zst"),
|
||||
);
|
||||
ensureRunOk(
|
||||
"tar --zstd -tf",
|
||||
await sh(id, "tar --zstd -tf /tmp/defout/my-default.tar.zst"),
|
||||
);
|
||||
}, 30000);
|
||||
});
|
||||
@@ -1,134 +0,0 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
description = "The ID of a Coder agent."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "paths" {
|
||||
description = "List of files/directories to include in the archive. Defaults to the current directory."
|
||||
type = list(string)
|
||||
default = ["."]
|
||||
}
|
||||
|
||||
variable "exclude_patterns" {
|
||||
description = "Exclude patterns for the archive."
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "compression" {
|
||||
description = "Compression algorithm for the archive. Supported: gzip, zstd, none."
|
||||
type = string
|
||||
default = "gzip"
|
||||
validation {
|
||||
condition = contains(["gzip", "zstd", "none"], var.compression)
|
||||
error_message = "compression must be one of: gzip, zstd, none."
|
||||
}
|
||||
}
|
||||
|
||||
variable "archive_name" {
|
||||
description = "Optional archive base name without extension. If empty, defaults to \"coder-archive\"."
|
||||
type = string
|
||||
default = "coder-archive"
|
||||
}
|
||||
|
||||
variable "output_dir" {
|
||||
description = "Optional output directory where the archive will be written. Defaults to \"/tmp\"."
|
||||
type = string
|
||||
default = "/tmp"
|
||||
}
|
||||
|
||||
variable "directory" {
|
||||
description = "Change current directory to this path before creating or extracting the archive. Defaults to the user's home directory."
|
||||
type = string
|
||||
default = "~"
|
||||
}
|
||||
|
||||
variable "create_on_stop" {
|
||||
description = "If true, also create a run_on_stop script that creates the archive automatically on workspace stop."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "extract_on_start" {
|
||||
description = "If true, the installer will wait for an archive and extract it on start."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "extract_wait_timeout_seconds" {
|
||||
description = "Timeout (seconds) to wait for an archive when extract_on_start is true."
|
||||
type = number
|
||||
default = 5
|
||||
}
|
||||
|
||||
# Provide a stable script filename and sensible defaults.
|
||||
locals {
|
||||
extension = var.compression == "gzip" ? ".tar.gz" : var.compression == "zstd" ? ".tar.zst" : ".tar"
|
||||
|
||||
# Ensure ~ is expanded because it cannot be expanded inside quotes in a
|
||||
# templated shell script.
|
||||
paths = [for v in var.paths : replace(v, "/^~(\\/|$)/", "$$HOME$1")]
|
||||
exclude_patterns = [for v in var.exclude_patterns : replace(v, "/^~(\\/|$)/", "$$HOME$1")]
|
||||
directory = replace(var.directory, "/^~(\\/|$)/", "$$HOME$1")
|
||||
output_dir = replace(var.output_dir, "/^~(\\/|$)/", "$$HOME$1")
|
||||
|
||||
archive_path = "${local.output_dir}/${var.archive_name}${local.extension}"
|
||||
}
|
||||
|
||||
output "archive_path" {
|
||||
description = "Full path to the archive file that will be created, extracted, or both."
|
||||
value = local.archive_path
|
||||
}
|
||||
|
||||
# This script installs the user-facing archive script into $CODER_SCRIPT_BIN_DIR.
|
||||
# The installed script can be run manually by the user to create an archive.
|
||||
resource "coder_script" "archive_start_script" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Archive"
|
||||
icon = "/icon/folder.svg"
|
||||
run_on_start = true
|
||||
start_blocks_login = var.extract_on_start
|
||||
|
||||
# Render the user-facing archive script with Terraform defaults, then write it to $CODER_SCRIPT_BIN_DIR
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
TF_LIB_B64 = base64encode(file("${path.module}/scripts/archive-lib.sh")),
|
||||
TF_PATHS = join(" ", formatlist("%q", local.paths)),
|
||||
TF_EXCLUDE_PATTERNS = join(" ", formatlist("%q", local.exclude_patterns)),
|
||||
TF_COMPRESSION = var.compression,
|
||||
TF_ARCHIVE_PATH = local.archive_path,
|
||||
TF_DIRECTORY = local.directory,
|
||||
TF_EXTRACT_ON_START = var.extract_on_start,
|
||||
TF_EXTRACT_WAIT_TIMEOUT = var.extract_wait_timeout_seconds,
|
||||
})
|
||||
}
|
||||
|
||||
# Optionally, also register a run_on_stop script that creates the archive automatically
|
||||
# when the workspace stops. It simply invokes the installed archive script.
|
||||
resource "coder_script" "archive_stop_script" {
|
||||
count = var.create_on_stop ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
display_name = "Archive"
|
||||
icon = "/icon/folder.svg"
|
||||
run_on_stop = true
|
||||
start_blocks_login = false
|
||||
|
||||
# Call the installed script. It will log to stderr and print the archive path to stdout.
|
||||
# We redirect stdout to stderr to avoid surfacing the path in system logs if undesired.
|
||||
# Remove the redirection if you want the path to appear in stdout on stop as well.
|
||||
script = <<-EOT
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
"$CODER_SCRIPT_BIN_DIR/coder-archive-create"
|
||||
EOT
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
LIB_B64="${TF_LIB_B64}"
|
||||
EXTRACT_ON_START="${TF_EXTRACT_ON_START}"
|
||||
EXTRACT_WAIT_TIMEOUT="${TF_EXTRACT_WAIT_TIMEOUT}"
|
||||
|
||||
# Set script defaults from Terraform.
|
||||
DEFAULT_PATHS=(${TF_PATHS})
|
||||
DEFAULT_EXCLUDE_PATTERNS=(${TF_EXCLUDE_PATTERNS})
|
||||
DEFAULT_COMPRESSION="${TF_COMPRESSION}"
|
||||
DEFAULT_ARCHIVE_PATH="${TF_ARCHIVE_PATH}"
|
||||
DEFAULT_DIRECTORY="${TF_DIRECTORY}"
|
||||
|
||||
# 1) Decode the library into $CODER_SCRIPT_DATA_DIR/archive-lib.sh (static, sourceable).
|
||||
LIB_PATH="$CODER_SCRIPT_DATA_DIR/archive-lib.sh"
|
||||
lib_tmp="$(mktemp -t coder-module-archive.XXXXXX))"
|
||||
trap 'rm -f "$lib_tmp" 2>/dev/null || true' EXIT
|
||||
|
||||
# Decode the base64 content safely.
|
||||
if ! printf '%s' "$LIB_B64" | base64 -d > "$lib_tmp"; then
|
||||
echo "ERROR: Failed to decode archive library from base64." >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod 0644 "$lib_tmp"
|
||||
mv "$lib_tmp" "$LIB_PATH"
|
||||
|
||||
# 2) Generate the wrapper scripts (create and extract).
|
||||
create_wrapper() {
|
||||
tmp="$(mktemp -t coder-module-archive.XXXXXX)"
|
||||
trap 'rm -f "$tmp" 2>/dev/null || true' EXIT
|
||||
cat > "$tmp" << EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
. "$LIB_PATH"
|
||||
|
||||
# Set defaults from Terraform (through installer).
|
||||
$(
|
||||
declare -p \
|
||||
DEFAULT_PATHS \
|
||||
DEFAULT_EXCLUDE_PATTERNS \
|
||||
DEFAULT_COMPRESSION \
|
||||
DEFAULT_ARCHIVE_PATH \
|
||||
DEFAULT_DIRECTORY
|
||||
)
|
||||
|
||||
$1 "\$@"
|
||||
EOF
|
||||
chmod 0755 "$tmp"
|
||||
mv "$tmp" "$2"
|
||||
}
|
||||
|
||||
CREATE_WRAPPER_PATH="$CODER_SCRIPT_BIN_DIR/coder-archive-create"
|
||||
EXTRACT_WRAPPER_PATH="$CODER_SCRIPT_BIN_DIR/coder-archive-extract"
|
||||
create_wrapper archive_create "$CREATE_WRAPPER_PATH"
|
||||
create_wrapper archive_extract "$EXTRACT_WRAPPER_PATH"
|
||||
|
||||
echo "Installed archive library to: $LIB_PATH"
|
||||
echo "Installed create script to: $CREATE_WRAPPER_PATH"
|
||||
echo "Installed extract script to: $EXTRACT_WRAPPER_PATH"
|
||||
|
||||
# 3) Optionally wait for and extract an archive on start.
|
||||
if [[ $EXTRACT_ON_START = true ]]; then
|
||||
. "$LIB_PATH"
|
||||
|
||||
archive_wait_and_extract "$EXTRACT_WAIT_TIMEOUT" quiet || {
|
||||
exit_code=$?
|
||||
if [[ $exit_code -eq 2 ]]; then
|
||||
echo "WARNING: Archive not found in backup path (this is expected with new workspaces)."
|
||||
else
|
||||
exit $exit_code
|
||||
fi
|
||||
}
|
||||
fi
|
||||
@@ -1,279 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
log() {
|
||||
printf '%s\n' "$@" >&2
|
||||
}
|
||||
warn() {
|
||||
printf 'WARNING: %s\n' "$1" >&2
|
||||
}
|
||||
error() {
|
||||
printf 'ERROR: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
load_defaults() {
|
||||
DEFAULT_PATHS=("${DEFAULT_PATHS[@]:-.}")
|
||||
DEFAULT_EXCLUDE_PATTERNS=("${DEFAULT_EXCLUDE_PATTERNS[@]:-}")
|
||||
DEFAULT_COMPRESSION="${DEFAULT_COMPRESSION:-gzip}"
|
||||
DEFAULT_ARCHIVE_PATH="${DEFAULT_ARCHIVE_PATH:-/tmp/coder-archive.tar.gz}"
|
||||
DEFAULT_DIRECTORY="${DEFAULT_DIRECTORY:-$HOME}"
|
||||
}
|
||||
|
||||
ensure_tools() {
|
||||
command -v tar > /dev/null 2>&1 || error "tar is required"
|
||||
case "$1" in
|
||||
gzip)
|
||||
command -v gzip > /dev/null 2>&1 || error "gzip is required for gzip compression"
|
||||
;;
|
||||
zstd)
|
||||
command -v zstd > /dev/null 2>&1 || error "zstd is required for zstd compression"
|
||||
;;
|
||||
none) ;;
|
||||
*)
|
||||
error "Unsupported compression algorithm: $1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
usage_archive_create() {
|
||||
load_defaults
|
||||
|
||||
cat >&2 << USAGE
|
||||
Usage: coder-archive-create [OPTIONS] [[PATHS] ...]
|
||||
Options:
|
||||
-c, --compression <gzip|zstd|none> Compression algorithm (default "${DEFAULT_COMPRESSION}")
|
||||
-C, --directory <DIRECTORY> Change to directory (default "${DEFAULT_DIRECTORY}")
|
||||
-f, --file <ARCHIVE> Output archive file (default "${DEFAULT_ARCHIVE_PATH}")
|
||||
-h, --help Show this help
|
||||
USAGE
|
||||
}
|
||||
|
||||
archive_create() {
|
||||
load_defaults
|
||||
|
||||
local compression="${DEFAULT_COMPRESSION}"
|
||||
local directory="${DEFAULT_DIRECTORY}"
|
||||
local file="${DEFAULT_ARCHIVE_PATH}"
|
||||
local paths=("${DEFAULT_PATHS[@]}")
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-c | --compression)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage_archive_create
|
||||
error "Missing value for $1"
|
||||
fi
|
||||
compression="$2"
|
||||
shift 2
|
||||
;;
|
||||
-C | --directory)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage_archive_create
|
||||
error "Missing value for $1"
|
||||
fi
|
||||
directory="$2"
|
||||
shift 2
|
||||
;;
|
||||
-f | --file)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage_archive_create
|
||||
error "Missing value for $1"
|
||||
fi
|
||||
file="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage_archive_create
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
while [[ $# -gt 0 ]]; do
|
||||
paths+=("$1")
|
||||
shift
|
||||
done
|
||||
;;
|
||||
-*)
|
||||
usage_archive_create
|
||||
error "Unknown option: $1"
|
||||
;;
|
||||
*)
|
||||
paths+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
ensure_tools "$compression"
|
||||
|
||||
local -a tar_opts=(-c -f "$file" -C "$directory")
|
||||
case "$compression" in
|
||||
gzip)
|
||||
tar_opts+=(-z)
|
||||
;;
|
||||
zstd)
|
||||
tar_opts+=(--zstd)
|
||||
;;
|
||||
none) ;;
|
||||
*)
|
||||
error "Unsupported compression algorithm: $compression"
|
||||
;;
|
||||
esac
|
||||
|
||||
for path in "${DEFAULT_EXCLUDE_PATTERNS[@]}"; do
|
||||
if [[ -n $path ]]; then
|
||||
tar_opts+=(--exclude "$path")
|
||||
fi
|
||||
done
|
||||
|
||||
# Ensure destination directory exists.
|
||||
dest="$(dirname "$file")"
|
||||
mkdir -p "$dest" 2> /dev/null || error "Failed to create output dir: $dest"
|
||||
|
||||
log "Creating archive:"
|
||||
log " Compression: $compression"
|
||||
log " Directory: $directory"
|
||||
log " Archive: $file"
|
||||
log " Paths: ${paths[*]}"
|
||||
log " Exclude: ${DEFAULT_EXCLUDE_PATTERNS[*]}"
|
||||
|
||||
umask 077
|
||||
tar "${tar_opts[@]}" "${paths[@]}"
|
||||
|
||||
printf '%s\n' "$file"
|
||||
}
|
||||
|
||||
usage_archive_extract() {
|
||||
load_defaults
|
||||
|
||||
cat >&2 << USAGE
|
||||
Usage: coder-archive-extract [OPTIONS]
|
||||
Options:
|
||||
-c, --compression <gzip|zstd|none> Compression algorithm (default "${DEFAULT_COMPRESSION}")
|
||||
-C, --directory <DIRECTORY> Change to directory (default "${DEFAULT_DIRECTORY}")
|
||||
-f, --file <ARCHIVE> Output archive file (default "${DEFAULT_ARCHIVE_PATH}")
|
||||
-h, --help Show this help
|
||||
USAGE
|
||||
}
|
||||
|
||||
archive_extract() {
|
||||
load_defaults
|
||||
|
||||
local compression="${DEFAULT_COMPRESSION}"
|
||||
local directory="${DEFAULT_DIRECTORY}"
|
||||
local file="${DEFAULT_ARCHIVE_PATH}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-c | --compression)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage_archive_extract
|
||||
error "Missing value for $1"
|
||||
fi
|
||||
compression="$2"
|
||||
shift 2
|
||||
;;
|
||||
-C | --directory)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage_archive_extract
|
||||
error "Missing value for $1"
|
||||
fi
|
||||
directory="$2"
|
||||
shift 2
|
||||
;;
|
||||
-f | --file)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage_archive_extract
|
||||
error "Missing value for $1"
|
||||
fi
|
||||
file="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage_archive_extract
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
while [[ $# -gt 0 ]]; do
|
||||
shift
|
||||
done
|
||||
;;
|
||||
-*)
|
||||
usage_archive_extract
|
||||
error "Unknown option: $1"
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
ensure_tools "$compression"
|
||||
|
||||
local -a tar_opts=(-x -f "$file" -C "$directory")
|
||||
case "$compression" in
|
||||
gzip)
|
||||
tar_opts+=(-z)
|
||||
;;
|
||||
zstd)
|
||||
tar_opts+=(--zstd)
|
||||
;;
|
||||
none) ;;
|
||||
*)
|
||||
error "Unsupported compression algorithm: $compression"
|
||||
;;
|
||||
esac
|
||||
|
||||
for path in "${DEFAULT_EXCLUDE_PATTERNS[@]}"; do
|
||||
if [[ -n $path ]]; then
|
||||
tar_opts+=(--exclude "$path")
|
||||
fi
|
||||
done
|
||||
|
||||
# Ensure destination directory exists.
|
||||
mkdir -p "$directory" || error "Failed to create directory: $directory"
|
||||
|
||||
log "Extracting archive:"
|
||||
log " Compression: $compression"
|
||||
log " Directory: $directory"
|
||||
log " Archive: $file"
|
||||
log " Exclude: ${DEFAULT_EXCLUDE_PATTERNS[*]}"
|
||||
|
||||
umask 077
|
||||
tar "${tar_opts[@]}" "${paths[@]}"
|
||||
|
||||
printf 'Extracted %s into %s\n' "$file" "$directory"
|
||||
}
|
||||
|
||||
archive_wait_and_extract() {
|
||||
load_defaults
|
||||
|
||||
local timeout="${1:-300}"
|
||||
local quiet="${2:-}"
|
||||
local file="${DEFAULT_ARCHIVE_PATH}"
|
||||
|
||||
local start now
|
||||
start=$(date +%s)
|
||||
while true; do
|
||||
if [[ -f "$file" ]]; then
|
||||
archive_extract -f "$file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ((timeout <= 0)); then
|
||||
break
|
||||
fi
|
||||
now=$(date +%s)
|
||||
if ((now - start >= timeout)); then
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [[ -z $quiet ]]; then
|
||||
printf 'ERROR: Timed out waiting for archive: %s\n' "$file" >&2
|
||||
fi
|
||||
return 2
|
||||
}
|
||||
@@ -13,7 +13,7 @@ Run Auggie CLI in your workspace to access Augment's AI coding assistant with ad
|
||||
```tf
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.2.1"
|
||||
version = "0.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -47,7 +47,7 @@ module "coder-login" {
|
||||
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.2.1"
|
||||
version = "0.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
@@ -103,7 +103,7 @@ EOF
|
||||
```tf
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.2.1"
|
||||
version = "0.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
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'."
|
||||
@@ -174,15 +174,13 @@ locals {
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".auggie-module"
|
||||
folder = trimsuffix(var.folder, "/")
|
||||
}
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.folder
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
|
||||
@@ -13,10 +13,10 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte
|
||||
```tf
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "3.1.0"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = var.openai_api_key
|
||||
workdir = "/home/coder/project"
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -33,11 +33,10 @@ module "codex" {
|
||||
module "codex" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "3.1.0"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = "..."
|
||||
workdir = "/home/coder/project"
|
||||
report_tasks = false
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -61,11 +60,11 @@ module "coder-login" {
|
||||
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "3.1.0"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = "..."
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
workdir = "/home/coder/project"
|
||||
folder = "/home/coder/project"
|
||||
|
||||
# Custom configuration for full auto mode
|
||||
base_config_toml = <<-EOT
|
||||
@@ -76,7 +75,7 @@ module "codex" {
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> This module configures Codex with a `workspace-write` sandbox that allows AI tasks to read/write files in the specified workdir. 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.
|
||||
> 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
|
||||
|
||||
@@ -84,7 +83,6 @@ module "codex" {
|
||||
- **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)
|
||||
- **Session Continuity**: When `continue = true` (default), the module automatically tracks task sessions in `~/.codex-module/.codex-task-session`. On workspace restart, it resumes the existing session with full conversation history. Set `continue = false` to always start fresh sessions.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -108,7 +106,7 @@ For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_se
|
||||
```tf
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "3.1.0"
|
||||
version = "2.0.0"
|
||||
# ... other variables ...
|
||||
|
||||
# Override default configuration
|
||||
@@ -139,7 +137,7 @@ module "codex" {
|
||||
> [!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.
|
||||
> workdir is a required variable for the module to function correctly.
|
||||
> folder is a required variable for the module to function correctly.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
install_codex: props?.skipCodexMock ? "true" : "false",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
codex_model: "gpt-4-turbo",
|
||||
workdir: "/home/coder",
|
||||
folder: "/home/coder",
|
||||
...props?.moduleVariables,
|
||||
},
|
||||
registerCleanup,
|
||||
@@ -166,12 +166,12 @@ describe("codex", async () => {
|
||||
expect(postInstallLog).toContain("post-install-script");
|
||||
});
|
||||
|
||||
test("workdir-variable", async () => {
|
||||
const workdir = "/tmp/codex-test-workdir";
|
||||
test("folder-variable", async () => {
|
||||
const folder = "/tmp/codex-test-folder";
|
||||
const { id } = await setup({
|
||||
skipCodexMock: false,
|
||||
moduleVariables: {
|
||||
workdir,
|
||||
folder,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
@@ -179,7 +179,7 @@ describe("codex", async () => {
|
||||
id,
|
||||
"/home/coder/.codex-module/install.log",
|
||||
);
|
||||
expect(resp).toContain(workdir);
|
||||
expect(resp).toContain(folder);
|
||||
});
|
||||
|
||||
test("additional-mcp-servers", async () => {
|
||||
@@ -368,90 +368,4 @@ describe("codex", async () => {
|
||||
expect(prompt.exitCode).not.toBe(0);
|
||||
expect(prompt.stderr).toContain("No such file or directory");
|
||||
});
|
||||
|
||||
test("codex-continue-capture-new-session", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
ai_prompt: "test task",
|
||||
},
|
||||
});
|
||||
|
||||
const workdir = "/home/coder";
|
||||
const expectedSessionId = "019a1234-5678-9abc-def0-123456789012";
|
||||
const sessionsDir = "/home/coder/.codex/sessions";
|
||||
const sessionFile = `${sessionsDir}/${expectedSessionId}.jsonl`;
|
||||
|
||||
await execContainer(id, ["mkdir", "-p", sessionsDir]);
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`echo '{"id":"${expectedSessionId}","cwd":"${workdir}","created":"2024-10-24T20:00:00Z","model":"gpt-4-turbo"}' > ${sessionFile}`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
|
||||
const trackingFile = "/home/coder/.codex-module/.codex-task-session";
|
||||
const maxAttempts = 30;
|
||||
let trackingFileContents = "";
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
const result = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat ${trackingFile} 2>/dev/null || echo ""`,
|
||||
]);
|
||||
if (result.stdout.trim().length > 0) {
|
||||
trackingFileContents = result.stdout;
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
expect(trackingFileContents).toContain(`${workdir}|${expectedSessionId}`);
|
||||
|
||||
const startLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.codex-module/agentapi-start.log",
|
||||
);
|
||||
expect(startLog).toContain("Capturing new session ID");
|
||||
expect(startLog).toContain("Session tracked");
|
||||
expect(startLog).toContain(expectedSessionId);
|
||||
});
|
||||
|
||||
test("codex-continue-resume-existing-session", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
const workdir = "/home/coder";
|
||||
const mockSessionId = "019a1234-5678-9abc-def0-123456789012";
|
||||
const trackingFile = "/home/coder/.codex-module/.codex-task-session";
|
||||
|
||||
await execContainer(id, ["mkdir", "-p", "/home/coder/.codex-module"]);
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`echo "${workdir}|${mockSessionId}" > ${trackingFile}`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.codex-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain("Found existing task session");
|
||||
expect(startLog.stdout).toContain(mockSessionId);
|
||||
expect(startLog.stdout).toContain("Resuming existing session");
|
||||
expect(startLog.stdout).toContain(
|
||||
`Starting Codex with arguments: --model gpt-4-turbo resume ${mockSessionId}`,
|
||||
);
|
||||
expect(startLog.stdout).not.toContain("test prompt");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,41 +36,11 @@ variable "icon" {
|
||||
default = "/icon/openai.svg"
|
||||
}
|
||||
|
||||
variable "workdir" {
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to run Codex in."
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI via AgentAPI"
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Whether to use a subdomain for AgentAPI."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Codex"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app"
|
||||
default = "Codex"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app"
|
||||
default = "Codex CLI"
|
||||
}
|
||||
|
||||
variable "install_codex" {
|
||||
type = bool
|
||||
description = "Whether to install Codex."
|
||||
@@ -110,7 +80,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
default = "v0.5.0"
|
||||
}
|
||||
|
||||
variable "codex_model" {
|
||||
@@ -137,12 +107,6 @@ variable "ai_prompt" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "continue" {
|
||||
type = bool
|
||||
description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "codex_system_prompt" {
|
||||
type = string
|
||||
description = "System instructions written to AGENTS.md in the ~/.codex directory"
|
||||
@@ -156,7 +120,6 @@ resource "coder_env" "openai_api_key" {
|
||||
}
|
||||
|
||||
locals {
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
app_slug = "codex"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
@@ -165,21 +128,18 @@ locals {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.workdir
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
cli_app = var.cli_app
|
||||
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
|
||||
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
|
||||
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_subdomain = var.subdomain
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
@@ -191,11 +151,9 @@ module "agentapi" {
|
||||
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_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_CODEX_MODEL='${var.codex_model}' \
|
||||
ARG_CODEX_START_DIRECTORY='${local.workdir}' \
|
||||
ARG_CODEX_START_DIRECTORY='${var.folder}' \
|
||||
ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_CONTINUE='${var.continue}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
@@ -206,14 +164,12 @@ module "agentapi" {
|
||||
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
ARG_OPENAI_API_KEY='${var.openai_api_key}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
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='${local.workdir}' \
|
||||
ARG_CODEX_START_DIRECTORY='${var.folder}' \
|
||||
ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
|
||||
@@ -22,8 +22,6 @@ 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")"
|
||||
printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")"
|
||||
printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS"
|
||||
echo "======================================"
|
||||
|
||||
set +o nounset
|
||||
@@ -102,20 +100,13 @@ EOF
|
||||
append_mcp_servers_section() {
|
||||
local config_path="$1"
|
||||
|
||||
if [ "${ARG_REPORT_TASKS}" == "false" ]; then
|
||||
ARG_CODER_MCP_APP_STATUS_SLUG=""
|
||||
CODER_MCP_AI_AGENTAPI_URL=""
|
||||
else
|
||||
CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
fi
|
||||
|
||||
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" = "${CODER_MCP_AI_AGENTAPI_URL}" , "CODER_AGENT_URL" = "${CODER_AGENT_URL}", "CODER_AGENT_TOKEN" = "${CODER_AGENT_TOKEN}" }
|
||||
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"
|
||||
|
||||
@@ -168,21 +159,7 @@ function add_instruction_prompt_if_exists() {
|
||||
fi
|
||||
}
|
||||
|
||||
function add_auth_json() {
|
||||
AUTH_JSON_PATH="$HOME/.codex/auth.json"
|
||||
mkdir -p "$(dirname "$AUTH_JSON_PATH")"
|
||||
AUTH_JSON=$(
|
||||
cat << EOF
|
||||
{
|
||||
"OPENAI_API_KEY": "${ARG_OPENAI_API_KEY}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
echo "$AUTH_JSON" > "$AUTH_JSON_PATH"
|
||||
}
|
||||
|
||||
install_codex
|
||||
codex --version
|
||||
populate_config_toml
|
||||
add_instruction_prompt_if_exists
|
||||
add_auth_json
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
source "$HOME"/.bashrc
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
@@ -17,195 +16,55 @@ fi
|
||||
printf "Version: %s\n" "$(codex --version)"
|
||||
set -o nounset
|
||||
ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d)
|
||||
ARG_CONTINUE=${ARG_CONTINUE:-true}
|
||||
|
||||
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")"
|
||||
printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS"
|
||||
printf "Continue Sessions: %s\n" "$ARG_CONTINUE"
|
||||
echo "======================================"
|
||||
set +o nounset
|
||||
CODEX_ARGS=()
|
||||
|
||||
SESSION_TRACKING_FILE="$HOME/.codex-module/.codex-task-session"
|
||||
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
|
||||
|
||||
find_session_for_directory() {
|
||||
local target_dir="$1"
|
||||
|
||||
if [ ! -f "$SESSION_TRACKING_FILE" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local session_id=$(grep "^$target_dir|" "$SESSION_TRACKING_FILE" | cut -d'|' -f2 | head -1)
|
||||
|
||||
if [ -n "$session_id" ]; then
|
||||
echo "$session_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
store_session_mapping() {
|
||||
local dir="$1"
|
||||
local session_id="$2"
|
||||
|
||||
mkdir -p "$(dirname "$SESSION_TRACKING_FILE")"
|
||||
|
||||
if [ -f "$SESSION_TRACKING_FILE" ]; then
|
||||
grep -v "^$dir|" "$SESSION_TRACKING_FILE" > "$SESSION_TRACKING_FILE.tmp" 2> /dev/null || true
|
||||
mv "$SESSION_TRACKING_FILE.tmp" "$SESSION_TRACKING_FILE"
|
||||
fi
|
||||
|
||||
echo "$dir|$session_id" >> "$SESSION_TRACKING_FILE"
|
||||
}
|
||||
|
||||
find_recent_session_file() {
|
||||
local target_dir="$1"
|
||||
local sessions_dir="$HOME/.codex/sessions"
|
||||
|
||||
if [ ! -d "$sessions_dir" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local latest_file=""
|
||||
local latest_time=0
|
||||
|
||||
while IFS= read -r session_file; do
|
||||
local file_time=$(stat -c %Y "$session_file" 2> /dev/null || stat -f %m "$session_file" 2> /dev/null || echo "0")
|
||||
local first_line=$(head -n 1 "$session_file" 2> /dev/null)
|
||||
local session_cwd=$(echo "$first_line" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ "$session_cwd" = "$target_dir" ] && [ "$file_time" -gt "$latest_time" ]; then
|
||||
latest_file="$session_file"
|
||||
latest_time="$file_time"
|
||||
fi
|
||||
done < <(find "$sessions_dir" -type f -name "*.jsonl" 2> /dev/null)
|
||||
|
||||
if [ -n "$latest_file" ]; then
|
||||
local first_line=$(head -n 1 "$latest_file")
|
||||
local session_id=$(echo "$first_line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
|
||||
if [ -n "$session_id" ]; then
|
||||
echo "$session_id"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_session_file() {
|
||||
local target_dir="$1"
|
||||
local max_attempts=20
|
||||
local attempt=0
|
||||
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
local session_id=$(find_recent_session_file "$target_dir" 2> /dev/null || echo "")
|
||||
if [ -n "$session_id" ]; then
|
||||
echo "$session_id"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.5
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
validate_codex_installation() {
|
||||
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"
|
||||
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
|
||||
fi
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
setup_workdir() {
|
||||
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
|
||||
|
||||
build_codex_args() {
|
||||
CODEX_ARGS=()
|
||||
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
|
||||
|
||||
if [ -n "$ARG_CODEX_MODEL" ]; then
|
||||
CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL")
|
||||
fi
|
||||
|
||||
if [ "$ARG_CONTINUE" = "true" ]; then
|
||||
existing_session=$(find_session_for_directory "$ARG_CODEX_START_DIRECTORY" 2> /dev/null || echo "")
|
||||
|
||||
if [ -n "$existing_session" ]; then
|
||||
printf "Found existing task session for this directory: %s\n" "$existing_session"
|
||||
printf "Resuming existing session...\n"
|
||||
CODEX_ARGS+=("resume" "$existing_session")
|
||||
else
|
||||
printf "No existing task session found for this directory\n"
|
||||
printf "Starting new task session...\n"
|
||||
|
||||
if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
|
||||
if [ "${ARG_REPORT_TASKS}" == "true" ]; then
|
||||
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"
|
||||
else
|
||||
PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT"
|
||||
fi
|
||||
CODEX_ARGS+=("$PROMPT")
|
||||
fi
|
||||
fi
|
||||
else
|
||||
printf "Continue disabled, starting fresh session\n"
|
||||
|
||||
if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
|
||||
if [ "${ARG_REPORT_TASKS}" == "true" ]; then
|
||||
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"
|
||||
else
|
||||
PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT"
|
||||
fi
|
||||
CODEX_ARGS+=("$PROMPT")
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
capture_session_id() {
|
||||
if [ "$ARG_CONTINUE" = "true" ] && [ -z "$existing_session" ]; then
|
||||
printf "Capturing new session ID...\n"
|
||||
new_session=$(wait_for_session_file "$ARG_CODEX_START_DIRECTORY" || echo "")
|
||||
|
||||
if [ -n "$new_session" ]; then
|
||||
store_session_mapping "$ARG_CODEX_START_DIRECTORY" "$new_session"
|
||||
printf "✓ Session tracked: %s\n" "$new_session"
|
||||
printf "This session will be automatically resumed on next restart\n"
|
||||
else
|
||||
printf "⚠ Could not capture session ID after 10s timeout\n"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
start_codex() {
|
||||
printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}"
|
||||
agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}" &
|
||||
capture_session_id
|
||||
}
|
||||
|
||||
validate_codex_installation
|
||||
setup_workdir
|
||||
build_codex_args
|
||||
start_codex
|
||||
# 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[@]}"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Handle --version flag
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "HELLO: $(bash -c env)"
|
||||
echo "codex version v1.0.0"
|
||||
@@ -9,30 +8,7 @@ fi
|
||||
|
||||
set -e
|
||||
|
||||
SESSION_ID=""
|
||||
IS_RESUME=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
resume)
|
||||
IS_RESUME=true
|
||||
SESSION_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$IS_RESUME" = false ]; then
|
||||
SESSION_ID="019a1234-5678-9abc-def0-123456789012"
|
||||
echo "Created new session: $SESSION_ID"
|
||||
else
|
||||
echo "Resuming session: $SESSION_ID"
|
||||
fi
|
||||
|
||||
while true; do
|
||||
echo "$(date) - codex-mock (session: $SESSION_ID)"
|
||||
echo "$(date) - codex-mock"
|
||||
sleep 15
|
||||
done
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
---
|
||||
display_name: Copilot CLI
|
||||
description: GitHub Copilot CLI agent for AI-powered terminal assistance
|
||||
icon: ../../../../.icons/github.svg
|
||||
verified: false
|
||||
tags: [agent, copilot, ai, github, tasks]
|
||||
---
|
||||
|
||||
# Copilot
|
||||
|
||||
Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-copilot-cli) in your workspace for AI-powered coding assistance directly from the terminal. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for task reporting in the Coder UI.
|
||||
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This example assumes you have [Coder external authentication](https://coder.com/docs/admin/external-auth) configured with `id = "github"`. If not, you can provide a direct token using the `github_token` variable or provide the correct external authentication id for GitHub by setting `external_auth_id = "my-github"`.
|
||||
|
||||
> [!NOTE]
|
||||
> By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js v22+** and **npm v10+**
|
||||
- **[Active Copilot subscription](https://docs.github.com/en/copilot/about-github-copilot/subscription-plans-for-github-copilot)** (GitHub Copilot Pro, Pro+, Business, or Enterprise)
|
||||
- **GitHub authentication** via one of:
|
||||
- [Coder external authentication](https://coder.com/docs/admin/external-auth) (recommended)
|
||||
- Direct token via `github_token` variable
|
||||
- Interactive login in Copilot
|
||||
|
||||
## Examples
|
||||
|
||||
### Usage with Tasks
|
||||
|
||||
For development environments where you want Copilot to have full access to tools and automatically resume sessions:
|
||||
|
||||
```tf
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Initial task prompt for Copilot."
|
||||
mutable = true
|
||||
}
|
||||
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
copilot_model = "claude-sonnet-4.5"
|
||||
allow_all_tools = true
|
||||
resume_session = true
|
||||
|
||||
trusted_directories = ["/home/coder/projects", "/tmp"]
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
Customize tool permissions, MCP servers, and Copilot settings:
|
||||
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
|
||||
# Version pinning (defaults to "latest", use specific version if desired)
|
||||
copilot_version = "0.0.334"
|
||||
|
||||
# Tool permissions
|
||||
allow_tools = ["shell(git)", "shell(npm)", "write"]
|
||||
trusted_directories = ["/home/coder/projects", "/tmp"]
|
||||
|
||||
# Custom Copilot configuration
|
||||
copilot_config = jsonencode({
|
||||
banner = "never"
|
||||
theme = "dark"
|
||||
})
|
||||
|
||||
# MCP server configuration
|
||||
mcp_config = jsonencode({
|
||||
mcpServers = {
|
||||
filesystem = {
|
||||
command = "npx"
|
||||
args = ["-y", "@modelcontextprotocol/server-filesystem", "/home/coder/projects"]
|
||||
description = "Provides file system access to the workspace"
|
||||
name = "Filesystem"
|
||||
timeout = 3000
|
||||
type = "local"
|
||||
tools = ["*"]
|
||||
trust = true
|
||||
}
|
||||
playwright = {
|
||||
command = "npx"
|
||||
args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated"]
|
||||
description = "Browser automation for testing and previewing changes"
|
||||
name = "Playwright"
|
||||
timeout = 5000
|
||||
type = "local"
|
||||
tools = ["*"]
|
||||
trust = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
# Pre-install Node.js if needed
|
||||
pre_install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> GitHub Copilot CLI does not automatically install MCP servers. You have two options:
|
||||
>
|
||||
> - Use `npx -y` in the MCP config (shown above) to auto-install on each run
|
||||
> - Pre-install MCP servers in `pre_install_script` for faster startup (e.g., `npm install -g @modelcontextprotocol/server-filesystem`)
|
||||
|
||||
### Direct Token Authentication
|
||||
|
||||
Use this example when you want to provide a GitHub Personal Access Token instead of using Coder external auth:
|
||||
|
||||
```tf
|
||||
variable "github_token" {
|
||||
type = string
|
||||
description = "GitHub Personal Access Token"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
github_token = var.github_token
|
||||
}
|
||||
```
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
Run Copilot as a command-line tool without task reporting or web interface. This installs and configures Copilot, making it available as a CLI app in the Coder agent bar that you can launch to interact with Copilot directly from your terminal. Set `report_tasks = false` to disable integration with Coder Tasks.
|
||||
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
report_tasks = false
|
||||
cli_app = true
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
The module supports multiple authentication methods (in priority order):
|
||||
|
||||
1. **[Coder External Auth](https://coder.com/docs/admin/external-auth) (Recommended)** - Automatic if GitHub external auth is configured in Coder
|
||||
2. **Direct Token** - Pass `github_token` variable (OAuth or Personal Access Token)
|
||||
3. **Interactive** - Copilot prompts for login via `/login` command if no auth found
|
||||
|
||||
> [!NOTE]
|
||||
> OAuth tokens work best with Copilot. Personal Access Tokens may have limited functionality.
|
||||
|
||||
## Session Resumption
|
||||
|
||||
By default, the module resumes the latest Copilot session when the workspace restarts. Set `resume_session = false` to always start fresh sessions.
|
||||
|
||||
> [!NOTE]
|
||||
> Session resumption requires persistent storage for the home directory or workspace volume. Without persistent storage, sessions will not resume across workspace restarts.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any issues, check the log files in the `~/.copilot-module` directory within your workspace for detailed information.
|
||||
|
||||
```bash
|
||||
# Installation logs
|
||||
cat ~/.copilot-module/install.log
|
||||
|
||||
# Startup logs
|
||||
cat ~/.copilot-module/agentapi-start.log
|
||||
|
||||
# Pre/post install script logs
|
||||
cat ~/.copilot-module/pre_install.log
|
||||
cat ~/.copilot-module/post_install.log
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> To use tasks with Copilot, you must have an active GitHub Copilot subscription.
|
||||
> The `workdir` variable is required and specifies the directory where Copilot will run.
|
||||
|
||||
## References
|
||||
|
||||
- [GitHub Copilot CLI Documentation](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)
|
||||
- [Installing GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)
|
||||
- [AgentAPI Documentation](https://github.com/coder/agentapi)
|
||||
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
|
||||
@@ -1,236 +0,0 @@
|
||||
run "defaults_are_correct" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.copilot_model == "claude-sonnet-4.5"
|
||||
error_message = "Default model should be 'claude-sonnet-4.5'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.report_tasks == true
|
||||
error_message = "Task reporting should be enabled by default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.resume_session == true
|
||||
error_message = "Session resumption should be enabled by default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.allow_all_tools == false
|
||||
error_message = "allow_all_tools should be disabled by default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.mcp_app_status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug env var should be created"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.mcp_app_status_slug.value == "copilot"
|
||||
error_message = "Status slug value should be 'copilot'"
|
||||
}
|
||||
}
|
||||
|
||||
run "github_token_creates_env_var" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
github_token = "test_github_token_abc123"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.github_token) == 1
|
||||
error_message = "github_token env var should be created when token is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.github_token[0].name == "GITHUB_TOKEN"
|
||||
error_message = "github_token env var name should be 'GITHUB_TOKEN'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.github_token[0].value == "test_github_token_abc123"
|
||||
error_message = "github_token env var value should match input"
|
||||
}
|
||||
}
|
||||
|
||||
run "github_token_not_created_when_empty" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
github_token = ""
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.github_token) == 0
|
||||
error_message = "github_token env var should not be created when empty"
|
||||
}
|
||||
}
|
||||
|
||||
run "copilot_model_env_var_for_non_default" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
copilot_model = "claude-sonnet-4"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.copilot_model) == 1
|
||||
error_message = "copilot_model env var should be created for non-default model"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.copilot_model[0].name == "COPILOT_MODEL"
|
||||
error_message = "copilot_model env var name should be 'COPILOT_MODEL'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.copilot_model[0].value == "claude-sonnet-4"
|
||||
error_message = "copilot_model env var value should match input"
|
||||
}
|
||||
}
|
||||
|
||||
run "copilot_model_not_created_for_default" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
copilot_model = "claude-sonnet-4.5"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.copilot_model) == 0
|
||||
error_message = "copilot_model env var should not be created for default model"
|
||||
}
|
||||
}
|
||||
|
||||
run "model_validation_accepts_valid_models" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
copilot_model = "gpt-5"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(["claude-sonnet-4", "claude-sonnet-4.5", "gpt-5"], var.copilot_model)
|
||||
error_message = "Model should be one of the valid options"
|
||||
}
|
||||
}
|
||||
|
||||
run "copilot_config_merges_with_trusted_directories" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder/project"
|
||||
trusted_directories = ["/workspace", "/data"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.final_copilot_config) > 0
|
||||
error_message = "final_copilot_config should be computed"
|
||||
}
|
||||
|
||||
# Verify workdir is trimmed of trailing slash
|
||||
assert {
|
||||
condition = local.workdir == "/home/coder/project"
|
||||
error_message = "workdir should be trimmed of trailing slash"
|
||||
}
|
||||
}
|
||||
|
||||
run "custom_copilot_config_overrides_default" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
copilot_config = jsonencode({
|
||||
banner = "always"
|
||||
theme = "dark"
|
||||
})
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.copilot_config != ""
|
||||
error_message = "Custom copilot config should be set"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = jsondecode(local.final_copilot_config).banner == "always"
|
||||
error_message = "Custom banner setting should be applied"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = jsondecode(local.final_copilot_config).theme == "dark"
|
||||
error_message = "Custom theme setting should be applied"
|
||||
}
|
||||
}
|
||||
|
||||
run "trusted_directories_merged_with_custom_config" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder/project"
|
||||
copilot_config = jsonencode({
|
||||
banner = "always"
|
||||
theme = "dark"
|
||||
trusted_folders = ["/custom"]
|
||||
})
|
||||
trusted_directories = ["/workspace", "/data"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/custom")
|
||||
error_message = "Custom trusted folder should be included"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/home/coder/project")
|
||||
error_message = "Workdir should be included in trusted folders"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/workspace")
|
||||
error_message = "trusted_directories should be merged into config"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/data")
|
||||
error_message = "All trusted_directories should be merged into config"
|
||||
}
|
||||
}
|
||||
|
||||
run "app_slug_is_consistent" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = local.app_slug == "copilot"
|
||||
error_message = "app_slug should be 'copilot'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = local.module_dir_name == ".copilot-module"
|
||||
error_message = "module_dir_name should be '.copilot-module'"
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
findResourceInstance,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("copilot", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
});
|
||||
|
||||
it("creates mcp_app_status_slug env var", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
});
|
||||
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"mcp_app_status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("copilot");
|
||||
});
|
||||
|
||||
it("creates github_token env var with correct value", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
github_token: "test_token_12345",
|
||||
});
|
||||
|
||||
const githubTokenEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"github_token",
|
||||
);
|
||||
expect(githubTokenEnv).toBeDefined();
|
||||
expect(githubTokenEnv.name).toBe("GITHUB_TOKEN");
|
||||
expect(githubTokenEnv.value).toBe("test_token_12345");
|
||||
});
|
||||
|
||||
it("does not create github_token env var when empty", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
github_token: "",
|
||||
});
|
||||
|
||||
const githubTokenEnvs = state.resources.filter(
|
||||
(r) => r.type === "coder_env" && r.name === "github_token",
|
||||
);
|
||||
expect(githubTokenEnvs.length).toBe(0);
|
||||
});
|
||||
|
||||
it("creates copilot_model env var for non-default models", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
copilot_model: "claude-sonnet-4",
|
||||
});
|
||||
|
||||
const modelEnv = findResourceInstance(state, "coder_env", "copilot_model");
|
||||
expect(modelEnv).toBeDefined();
|
||||
expect(modelEnv.name).toBe("COPILOT_MODEL");
|
||||
expect(modelEnv.value).toBe("claude-sonnet-4");
|
||||
});
|
||||
|
||||
it("does not create copilot_model env var for default model", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
copilot_model: "claude-sonnet-4.5",
|
||||
});
|
||||
|
||||
const modelEnvs = state.resources.filter(
|
||||
(r) => r.type === "coder_env" && r.name === "copilot_model",
|
||||
);
|
||||
expect(modelEnvs.length).toBe(0);
|
||||
});
|
||||
|
||||
it("creates coder_script resources via agentapi module", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
});
|
||||
|
||||
// The agentapi module should create coder_script resources for install and start
|
||||
const scripts = state.resources.filter((r) => r.type === "coder_script");
|
||||
expect(scripts.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("validates copilot_model accepts valid values", async () => {
|
||||
// Test valid models don't throw errors
|
||||
await expect(
|
||||
runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
copilot_model: "gpt-5",
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
|
||||
await expect(
|
||||
runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
copilot_model: "claude-sonnet-4.5",
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("merges trusted_directories with custom copilot_config", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder/project",
|
||||
trusted_directories: JSON.stringify(["/workspace", "/data"]),
|
||||
copilot_config: JSON.stringify({
|
||||
banner: "always",
|
||||
theme: "dark",
|
||||
trusted_folders: ["/custom"],
|
||||
}),
|
||||
});
|
||||
|
||||
// Verify that the state was created successfully with the merged config
|
||||
// The actual merging logic is tested in the .tftest.hcl file
|
||||
expect(state).toBeDefined();
|
||||
expect(state.resources).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,302 +0,0 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "workdir" {
|
||||
type = string
|
||||
description = "The folder to run Copilot in."
|
||||
}
|
||||
|
||||
variable "external_auth_id" {
|
||||
type = string
|
||||
description = "ID of the GitHub external auth provider configured in Coder."
|
||||
default = "github"
|
||||
}
|
||||
|
||||
variable "github_token" {
|
||||
type = string
|
||||
description = "GitHub OAuth token or Personal Access Token. If provided, this will be used instead of auto-detecting authentication."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "copilot_model" {
|
||||
type = string
|
||||
description = "Model to use. Supported values: claude-sonnet-4, claude-sonnet-4.5 (default), gpt-5."
|
||||
default = "claude-sonnet-4.5"
|
||||
validation {
|
||||
condition = contains(["claude-sonnet-4", "claude-sonnet-4.5", "gpt-5"], var.copilot_model)
|
||||
error_message = "copilot_model must be one of: claude-sonnet-4, claude-sonnet-4.5, gpt-5."
|
||||
}
|
||||
}
|
||||
|
||||
variable "copilot_config" {
|
||||
type = string
|
||||
description = "Custom Copilot configuration as JSON string. Leave empty to use default configuration with banner disabled, theme set to auto, and workdir as trusted folder."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "Initial task prompt for programmatic mode."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
type = string
|
||||
description = "The system prompt to use for the Copilot server. Task reporting instructions are automatically added when report_tasks is enabled."
|
||||
default = "You are a helpful coding assistant that helps developers write, debug, and understand code. Provide clear explanations, follow best practices, and help solve coding problems efficiently."
|
||||
}
|
||||
|
||||
variable "trusted_directories" {
|
||||
type = list(string)
|
||||
description = "Additional directories to trust for Copilot operations."
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "allow_all_tools" {
|
||||
type = bool
|
||||
description = "Allow all tools without prompting (equivalent to --allow-all-tools)."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "allow_tools" {
|
||||
type = list(string)
|
||||
description = "Specific tools to allow: shell(command), write, or MCP_SERVER_NAME."
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "deny_tools" {
|
||||
type = list(string)
|
||||
description = "Specific tools to deny: shell(command), write, or MCP_SERVER_NAME."
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "mcp_config" {
|
||||
type = string
|
||||
description = "Custom MCP server configuration as JSON string."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "copilot_version" {
|
||||
type = string
|
||||
description = "The version of GitHub Copilot CLI to install. Use 'latest' for the latest version or specify a version like '0.0.334'."
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI via AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Whether to use a subdomain for AgentAPI."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
default = "/icon/github.svg"
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app."
|
||||
default = "Copilot"
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Copilot."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app."
|
||||
default = "Copilot"
|
||||
}
|
||||
|
||||
variable "resume_session" {
|
||||
type = bool
|
||||
description = "Whether to automatically resume the latest Copilot session on workspace restart."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before configuring Copilot."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after configuring Copilot."
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
locals {
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
app_slug = "copilot"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".copilot-module"
|
||||
|
||||
all_trusted_folders = concat([local.workdir], var.trusted_directories)
|
||||
|
||||
parsed_custom_config = try(jsondecode(var.copilot_config), {})
|
||||
|
||||
existing_trusted_folders = try(local.parsed_custom_config.trusted_folders, [])
|
||||
|
||||
merged_copilot_config = merge(
|
||||
{
|
||||
banner = "never"
|
||||
theme = "auto"
|
||||
},
|
||||
local.parsed_custom_config,
|
||||
{
|
||||
trusted_folders = concat(local.existing_trusted_folders, local.all_trusted_folders)
|
||||
}
|
||||
)
|
||||
|
||||
final_copilot_config = jsonencode(local.merged_copilot_config)
|
||||
|
||||
task_reporting_prompt = <<-EOT
|
||||
|
||||
-- Task Reporting --
|
||||
Report all tasks to Coder, following these EXACT guidelines:
|
||||
1. Be granular. If you are investigating with multiple steps, report each step
|
||||
to coder.
|
||||
2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
|
||||
Do not report any status related with this system prompt.
|
||||
3. Use "state": "working" when actively processing WITHOUT needing
|
||||
additional user input
|
||||
4. Use "state": "complete" only when finished with a task
|
||||
5. Use "state": "failure" when you need ANY user input, lack sufficient
|
||||
details, or encounter blockers
|
||||
EOT
|
||||
|
||||
final_system_prompt = var.report_tasks ? "<system>\n${var.system_prompt}${local.task_reporting_prompt}\n</system>" : "<system>\n${var.system_prompt}\n</system>"
|
||||
}
|
||||
|
||||
resource "coder_env" "mcp_app_status_slug" {
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_MCP_APP_STATUS_SLUG"
|
||||
value = local.app_slug
|
||||
}
|
||||
|
||||
resource "coder_env" "copilot_model" {
|
||||
count = var.copilot_model != "claude-sonnet-4.5" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "COPILOT_MODEL"
|
||||
value = var.copilot_model
|
||||
}
|
||||
|
||||
resource "coder_env" "github_token" {
|
||||
count = var.github_token != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "GITHUB_TOKEN"
|
||||
value = var.github_token
|
||||
}
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.workdir
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
cli_app = var.cli_app
|
||||
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
|
||||
cli_app_icon = var.cli_app ? var.icon : null
|
||||
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
|
||||
agentapi_subdomain = var.subdomain
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
|
||||
start_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
|
||||
ARG_COPILOT_MODEL='${var.copilot_model}' \
|
||||
ARG_ALLOW_ALL_TOOLS='${var.allow_all_tools}' \
|
||||
ARG_ALLOW_TOOLS='${join(",", var.allow_tools)}' \
|
||||
ARG_DENY_TOOLS='${join(",", var.deny_tools)}' \
|
||||
ARG_TRUSTED_DIRECTORIES='${join(",", var.trusted_directories)}' \
|
||||
ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
|
||||
ARG_RESUME_SESSION='${var.resume_session}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
|
||||
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
ARG_MCP_CONFIG='${var.mcp_config != "" ? base64encode(var.mcp_config) : ""}' \
|
||||
ARG_COPILOT_CONFIG='${base64encode(local.final_copilot_config)}' \
|
||||
ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
|
||||
ARG_COPILOT_VERSION='${var.copilot_version}' \
|
||||
ARG_COPILOT_MODEL='${var.copilot_model}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
|
||||
ARG_MCP_CONFIG=$(echo -n "${ARG_MCP_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_COPILOT_CONFIG=$(echo -n "${ARG_COPILOT_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
|
||||
ARG_COPILOT_VERSION=${ARG_COPILOT_VERSION:-0.0.334}
|
||||
ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-claude-sonnet-4.5}
|
||||
|
||||
validate_prerequisites() {
|
||||
if ! command_exists node; then
|
||||
echo "ERROR: Node.js not found. Copilot requires Node.js v22+."
|
||||
echo "Install with: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command_exists npm; then
|
||||
echo "ERROR: npm not found. Copilot requires npm v10+."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
node_version=$(node --version | sed 's/v//' | cut -d. -f1)
|
||||
if [ "$node_version" -lt 22 ]; then
|
||||
echo "WARNING: Node.js v$node_version detected. Copilot requires v22+."
|
||||
fi
|
||||
}
|
||||
|
||||
install_copilot() {
|
||||
if ! command_exists copilot; then
|
||||
echo "Installing GitHub Copilot CLI (version: ${ARG_COPILOT_VERSION})..."
|
||||
if [ "$ARG_COPILOT_VERSION" = "latest" ]; then
|
||||
npm install -g @github/copilot
|
||||
else
|
||||
npm install -g "@github/copilot@${ARG_COPILOT_VERSION}"
|
||||
fi
|
||||
|
||||
if ! command_exists copilot; then
|
||||
echo "ERROR: Failed to install Copilot"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "GitHub Copilot CLI installed successfully"
|
||||
else
|
||||
echo "GitHub Copilot CLI already installed"
|
||||
fi
|
||||
}
|
||||
|
||||
check_github_authentication() {
|
||||
echo "Checking GitHub authentication..."
|
||||
|
||||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
echo "✓ GitHub token provided via module configuration"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command_exists coder; then
|
||||
if coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" > /dev/null 2>&1; then
|
||||
echo "✓ GitHub OAuth authentication via Coder external auth"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if command_exists gh && gh auth status > /dev/null 2>&1; then
|
||||
echo "✓ GitHub OAuth authentication via GitHub CLI"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "⚠ No GitHub authentication detected"
|
||||
echo " Copilot will prompt for authentication when started"
|
||||
echo " For seamless experience, configure GitHub external auth in Coder or run 'gh auth login'"
|
||||
return 0
|
||||
}
|
||||
|
||||
setup_copilot_configurations() {
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
|
||||
local module_path="$HOME/.copilot-module"
|
||||
mkdir -p "$module_path"
|
||||
|
||||
setup_copilot_config
|
||||
|
||||
echo "$ARG_WORKDIR" > "$module_path/trusted_directories"
|
||||
}
|
||||
|
||||
setup_copilot_config() {
|
||||
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
local copilot_config_dir="$XDG_CONFIG_HOME/.copilot"
|
||||
local copilot_config_file="$copilot_config_dir/config.json"
|
||||
local mcp_config_file="$copilot_config_dir/mcp-config.json"
|
||||
|
||||
mkdir -p "$copilot_config_dir"
|
||||
|
||||
if [ -n "$ARG_COPILOT_CONFIG" ]; then
|
||||
echo "Setting up Copilot configuration..."
|
||||
|
||||
if command_exists jq; then
|
||||
echo "$ARG_COPILOT_CONFIG" | jq 'del(.mcpServers)' > "$copilot_config_file"
|
||||
else
|
||||
echo "$ARG_COPILOT_CONFIG" > "$copilot_config_file"
|
||||
fi
|
||||
|
||||
echo "Setting up MCP server configuration..."
|
||||
setup_mcp_config "$mcp_config_file"
|
||||
else
|
||||
echo "ERROR: No Copilot configuration provided"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
setup_mcp_config() {
|
||||
local mcp_config_file="$1"
|
||||
|
||||
echo '{"mcpServers": {}}' > "$mcp_config_file"
|
||||
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
|
||||
echo "Adding Coder MCP server for task reporting..."
|
||||
setup_coder_mcp_server "$mcp_config_file"
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_MCP_CONFIG" ]; then
|
||||
echo "Adding custom MCP servers..."
|
||||
add_custom_mcp_servers "$mcp_config_file"
|
||||
fi
|
||||
|
||||
echo "MCP configuration completed: $mcp_config_file"
|
||||
}
|
||||
|
||||
setup_coder_mcp_server() {
|
||||
local mcp_config_file="$1"
|
||||
|
||||
local coder_mcp_wrapper_script
|
||||
coder_mcp_wrapper_script=$(
|
||||
cat << EOF
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
export CODER_MCP_APP_STATUS_SLUG="${ARG_MCP_APP_STATUS_SLUG}"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
export CODER_AGENT_URL="${CODER_AGENT_URL}"
|
||||
export CODER_AGENT_TOKEN="${CODER_AGENT_TOKEN}"
|
||||
|
||||
exec coder exp mcp server
|
||||
EOF
|
||||
)
|
||||
echo "$coder_mcp_wrapper_script" > "/tmp/coder-mcp-server.sh"
|
||||
chmod +x /tmp/coder-mcp-server.sh
|
||||
|
||||
local coder_mcp_config
|
||||
coder_mcp_config=$(
|
||||
cat << EOF
|
||||
{
|
||||
"mcpServers": {
|
||||
"coder": {
|
||||
"command": "/tmp/coder-mcp-server.sh",
|
||||
"args": [],
|
||||
"description": "CRITICAL: Call this tool immediately after receiving ANY user message to report task status. Rules: (1) Call FIRST before doing work - report what you will do with state='working'. (2) Be granular - report each step separately. (3) State 'working' = actively processing without needing user input. (4) State 'complete' = task 100% finished. (5) State 'failure' = need user input, missing info, or blocked. Example: User says 'fix the bug' -> call with state='working', description='Investigating authentication bug'. When done -> call with state='complete', description='Fixed token validation'. You MUST report on every interaction.",
|
||||
"name": "Coder",
|
||||
"timeout": 3000,
|
||||
"type": "local",
|
||||
"tools": ["*"],
|
||||
"trust": true
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "$coder_mcp_config" > "$mcp_config_file"
|
||||
}
|
||||
|
||||
add_custom_mcp_servers() {
|
||||
local mcp_config_file="$1"
|
||||
|
||||
if command_exists jq; then
|
||||
local custom_servers
|
||||
custom_servers=$(echo "$ARG_MCP_CONFIG" | jq '.mcpServers // {}')
|
||||
|
||||
local updated_config
|
||||
updated_config=$(jq --argjson custom "$custom_servers" '.mcpServers += $custom' "$mcp_config_file")
|
||||
echo "$updated_config" > "$mcp_config_file"
|
||||
elif command_exists node; then
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const existing = JSON.parse(fs.readFileSync('$mcp_config_file', 'utf8'));
|
||||
const input = JSON.parse(\`$ARG_MCP_CONFIG\`);
|
||||
const custom = input.mcpServers || {};
|
||||
existing.mcpServers = {...existing.mcpServers, ...custom};
|
||||
fs.writeFileSync('$mcp_config_file', JSON.stringify(existing, null, 2));
|
||||
"
|
||||
else
|
||||
echo "WARNING: jq and node not available, cannot merge custom MCP servers"
|
||||
fi
|
||||
}
|
||||
|
||||
configure_copilot_model() {
|
||||
if [ -n "$ARG_COPILOT_MODEL" ] && [ "$ARG_COPILOT_MODEL" != "claude-sonnet-4.5" ]; then
|
||||
echo "Setting Copilot model to: $ARG_COPILOT_MODEL"
|
||||
copilot config model "$ARG_COPILOT_MODEL" || {
|
||||
echo "WARNING: Failed to set model via copilot config, will use environment variable fallback"
|
||||
export COPILOT_MODEL="$ARG_COPILOT_MODEL"
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
configure_coder_integration() {
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
|
||||
echo "Configuring Copilot task reporting..."
|
||||
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
echo "✓ Coder MCP server configured for task reporting"
|
||||
else
|
||||
echo "Task reporting disabled or no app status slug provided."
|
||||
export CODER_MCP_APP_STATUS_SLUG=""
|
||||
export CODER_MCP_AI_AGENTAPI_URL=""
|
||||
fi
|
||||
}
|
||||
|
||||
validate_prerequisites
|
||||
install_copilot
|
||||
check_github_authentication
|
||||
setup_copilot_configurations
|
||||
configure_copilot_model
|
||||
configure_coder_integration
|
||||
|
||||
echo "Copilot module setup completed."
|
||||
@@ -1,157 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-}
|
||||
ARG_ALLOW_ALL_TOOLS=${ARG_ALLOW_ALL_TOOLS:-false}
|
||||
ARG_ALLOW_TOOLS=${ARG_ALLOW_TOOLS:-}
|
||||
ARG_DENY_TOOLS=${ARG_DENY_TOOLS:-}
|
||||
ARG_TRUSTED_DIRECTORIES=${ARG_TRUSTED_DIRECTORIES:-}
|
||||
ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
|
||||
ARG_RESUME_SESSION=${ARG_RESUME_SESSION:-true}
|
||||
|
||||
validate_copilot_installation() {
|
||||
if ! command_exists copilot; then
|
||||
echo "ERROR: Copilot not installed. Run: npm install -g @github/copilot"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
build_initial_prompt() {
|
||||
local initial_prompt=""
|
||||
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ -n "$ARG_SYSTEM_PROMPT" ]; then
|
||||
initial_prompt="$ARG_SYSTEM_PROMPT
|
||||
|
||||
$ARG_AI_PROMPT"
|
||||
else
|
||||
initial_prompt="$ARG_AI_PROMPT"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$initial_prompt"
|
||||
}
|
||||
|
||||
build_copilot_args() {
|
||||
COPILOT_ARGS=()
|
||||
|
||||
if [ "$ARG_ALLOW_ALL_TOOLS" = "true" ]; then
|
||||
COPILOT_ARGS+=(--allow-all-tools)
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_ALLOW_TOOLS" ]; then
|
||||
IFS=',' read -ra ALLOW_ARRAY <<< "$ARG_ALLOW_TOOLS"
|
||||
for tool in "${ALLOW_ARRAY[@]}"; do
|
||||
if [ -n "$tool" ]; then
|
||||
COPILOT_ARGS+=(--allow-tool "$tool")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_DENY_TOOLS" ]; then
|
||||
IFS=',' read -ra DENY_ARRAY <<< "$ARG_DENY_TOOLS"
|
||||
for tool in "${DENY_ARRAY[@]}"; do
|
||||
if [ -n "$tool" ]; then
|
||||
COPILOT_ARGS+=(--deny-tool "$tool")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
check_existing_session() {
|
||||
if [ "$ARG_RESUME_SESSION" = "true" ]; then
|
||||
if copilot --help > /dev/null 2>&1; then
|
||||
local session_dir="$HOME/.copilot/history-session-state"
|
||||
if [ -d "$session_dir" ] && [ -n "$(ls "$session_dir"/session_*_*.json 2> /dev/null)" ]; then
|
||||
echo "Found existing Copilot session. Will continue latest session." >&2
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
setup_github_authentication() {
|
||||
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
echo "Setting up GitHub authentication..."
|
||||
|
||||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
export GH_TOKEN="$GITHUB_TOKEN"
|
||||
echo "✓ Using GitHub token from module configuration"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command_exists coder; then
|
||||
local github_token
|
||||
if github_token=$(coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" 2> /dev/null); then
|
||||
if [ -n "$github_token" ] && [ "$github_token" != "null" ]; then
|
||||
export GITHUB_TOKEN="$github_token"
|
||||
export GH_TOKEN="$github_token"
|
||||
echo "✓ Using Coder external auth OAuth token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if command_exists gh && gh auth status > /dev/null 2>&1; then
|
||||
echo "✓ Using GitHub CLI OAuth authentication"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "⚠ No GitHub authentication available"
|
||||
echo " Copilot will prompt for login during first use"
|
||||
echo " Use the '/login' command in Copilot to authenticate"
|
||||
return 0
|
||||
}
|
||||
|
||||
start_agentapi() {
|
||||
echo "Starting in directory: $ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
|
||||
build_copilot_args
|
||||
|
||||
if check_existing_session; then
|
||||
echo "Continuing latest Copilot session..."
|
||||
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
|
||||
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
|
||||
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue "${COPILOT_ARGS[@]}"
|
||||
else
|
||||
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue
|
||||
fi
|
||||
else
|
||||
echo "Starting new Copilot session..."
|
||||
local initial_prompt
|
||||
initial_prompt=$(build_initial_prompt)
|
||||
|
||||
if [ -n "$initial_prompt" ]; then
|
||||
echo "Using initial prompt with system context"
|
||||
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
|
||||
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
|
||||
agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
|
||||
else
|
||||
agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot
|
||||
fi
|
||||
else
|
||||
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
|
||||
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
|
||||
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
|
||||
else
|
||||
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
setup_github_authentication
|
||||
validate_copilot_installation
|
||||
start_agentapi
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "GitHub Copilot CLI v1.0.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while true; do
|
||||
echo "$(date) - Copilot mock running..."
|
||||
sleep 15
|
||||
done
|
||||
@@ -13,7 +13,7 @@ Run the Cursor Agent CLI in your workspace for interactive coding assistance and
|
||||
```tf
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.2.1"
|
||||
version = "0.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -42,7 +42,7 @@ module "coder-login" {
|
||||
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.2.1"
|
||||
version = "0.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
default = "v0.5.0"
|
||||
}
|
||||
|
||||
variable "force" {
|
||||
@@ -113,7 +113,6 @@ locals {
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".cursor-cli-module"
|
||||
folder = trimsuffix(var.folder, "/")
|
||||
}
|
||||
|
||||
# Expose status slug and API key to the agent environment
|
||||
@@ -132,10 +131,9 @@ resource "coder_env" "cursor_api_key" {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.folder
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
|
||||
@@ -13,7 +13,7 @@ Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace
|
||||
```tf
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.1.1"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -46,7 +46,7 @@ variable "gemini_api_key" {
|
||||
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.1.1"
|
||||
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 = "2.1.1"
|
||||
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 = "2.1.1"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
folder = "/home/coder/project"
|
||||
|
||||
@@ -81,7 +81,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
default = "v0.2.3"
|
||||
}
|
||||
|
||||
variable "gemini_model" {
|
||||
@@ -172,15 +172,13 @@ EOT
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".gemini-module"
|
||||
folder = trimsuffix(var.folder, "/")
|
||||
}
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.folder
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
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
|
||||
}
|
||||
```
|
||||
@@ -1,106 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -eu
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
printf "$${BOLD}Starting Nextflow...$${RESET}\n"
|
||||
|
||||
if ! command -v nextflow > /dev/null 2>&1; then
|
||||
# Update system dependencies
|
||||
sudo apt update
|
||||
sudo apt install openjdk-21-jdk graphviz salmon fastqc multiqc -y
|
||||
|
||||
# Install nextflow
|
||||
export NXF_VER=${NEXTFLOW_VERSION}
|
||||
curl -s https://get.nextflow.io | bash
|
||||
sudo mv nextflow /usr/local/bin/
|
||||
sudo chmod +x /usr/local/bin/nextflow
|
||||
|
||||
# Verify installation
|
||||
tmp_verify=$(mktemp -d coder-nextflow-XXXXXX)
|
||||
nextflow run hello \
|
||||
-with-report "$${tmp_verify}/report.html" \
|
||||
-with-trace "$${tmp_verify}/trace.txt" \
|
||||
-with-timeline "$${tmp_verify}/timeline.html" \
|
||||
-with-dag "$${tmp_verify}/flowchart.png"
|
||||
rm -r "$${tmp_verify}"
|
||||
else
|
||||
echo "Nextflow is already installed\n\n"
|
||||
fi
|
||||
|
||||
if [ ! -z ${PROJECT_PATH} ]; then
|
||||
# Project is located at PROJECT_PATH
|
||||
echo "Change directory: ${PROJECT_PATH}"
|
||||
cd ${PROJECT_PATH}
|
||||
fi
|
||||
|
||||
# Start a web server to preview reports
|
||||
mkdir -p ${HTTP_SERVER_REPORTS_DIR}
|
||||
echo "Starting HTTP server in background, check logs: ${HTTP_SERVER_LOG_PATH}"
|
||||
python3 -m http.server --directory ${HTTP_SERVER_REPORTS_DIR} ${HTTP_SERVER_PORT} > "${HTTP_SERVER_LOG_PATH}" 2>&1 &
|
||||
|
||||
# Stub run?
|
||||
if [ "${STUB_RUN}" = "true" ]; then
|
||||
nextflow ${STUB_RUN_COMMAND} -stub-run
|
||||
fi
|
||||
|
||||
printf "\n$${BOLD}Nextflow ${NEXTFLOW_VERSION} is ready. HTTP server is listening on port ${HTTP_SERVER_PORT}$${RESET}\n"
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
display_name: Amp
|
||||
display_name: Amp CLI
|
||||
icon: ../../../../.icons/sourcegraph-amp.svg
|
||||
description: Sourcegraph's AI coding agent with deep codebase understanding and intelligent code search capabilities
|
||||
verified: true
|
||||
verified: false
|
||||
tags: [agent, sourcegraph, amp, ai, tasks]
|
||||
---
|
||||
|
||||
@@ -13,7 +13,7 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
|
||||
```tf
|
||||
module "amp-cli" {
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "2.0.1"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
|
||||
install_sourcegraph_amp = true
|
||||
@@ -23,10 +23,8 @@ module "amp-cli" {
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Default (official installer)**: No prerequisites - the official installer includes its own runtime (Bun)
|
||||
- **npm installation (`install_via_npm = true`)**: Requires Node.js and npm to be installed before Amp installation
|
||||
- Required for Alpine Linux or other musl-based systems
|
||||
- Ensure Node.js and npm are available in your workspace image or via earlier provisioning steps
|
||||
- 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
|
||||
|
||||
@@ -37,55 +35,52 @@ data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
default = ""
|
||||
mutable = true
|
||||
|
||||
}
|
||||
|
||||
variable "amp_api_key" {
|
||||
# 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"
|
||||
amp_version = "2.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
amp_api_key = var.amp_api_key # recommended for tasks usage
|
||||
workdir = "/home/coder/project"
|
||||
instruction_prompt = <<-EOT
|
||||
# Instructions
|
||||
- Start every response with `amp > `
|
||||
EOT
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
base_amp_config = jsonencode({
|
||||
"amp.anthropic.thinking.enabled" = true
|
||||
"amp.todos.enabled" = true
|
||||
"amp.tools.stopTimeout" = 600
|
||||
"amp.git.commit.ampThread.enabled" = true
|
||||
"amp.git.commit.coauthor.enabled" = true
|
||||
"amp.terminal.commands.nodeSpawn.loadProfile" = "daily"
|
||||
"amp.permissions" = [
|
||||
{ "tool" : "mcp__coder__*", "action" : "allow" },
|
||||
{ "tool" : "Bash", "action" : "allow", "context" : "thread" },
|
||||
{ "tool" : "Bash", "matches" : { "cmd" : ["rm -rf /*", "rm -rf ~/*"] }, "action" : "reject", "context" : "subagent" },
|
||||
{ "tool" : "edit_file", "action" : "allow" },
|
||||
{ "tool" : "write_file", "action" : "allow" },
|
||||
{ "tool" : "read_file", "action" : "allow" },
|
||||
{ "tool" : "Grep", "action" : "allow" }
|
||||
]
|
||||
})
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key # recommended for authenticated usage
|
||||
install_sourcegraph_amp = true
|
||||
}
|
||||
```
|
||||
|
||||
## How it Works
|
||||
|
||||
- **Install**: Installs Sourcegraph Amp CLI using npm (installs Node.js via NVM if required)
|
||||
- **Start**: Launches Amp CLI in the specified directory, wrapped with AgentAPI to enable tasks and AI interactions
|
||||
- **Environment Variables**: Sets `SOURCEGRAPH_AMP_API_KEY` and `SOURCEGRAPH_AMP_START_DIRECTORY` for the CLI execution
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If `amp` is not found, ensure `install_amp = true` and your API key is valid
|
||||
- Logs are written under `/home/coder/.amp-module/` (`install.log`, `agentapi-start.log`) for debugging
|
||||
- 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]
|
||||
> To use tasks with Amp CLI, create a `coder_parameter` named `"AI Prompt"` and pass its value to the amp-cli module's `ai_prompt` variable. The `folder` variable is required for the module to function correctly.
|
||||
> For using **Coder Tasks** with Amp CLI, make sure to set `amp_api_key`.
|
||||
> For using **Coder Tasks** with Amp CLI, make sure to pass the `AI Prompt` parameter and set `sourcegraph_amp_api_key`.
|
||||
> This ensures task reporting and status updates work seamlessly.
|
||||
|
||||
## References
|
||||
|
||||
@@ -43,9 +43,9 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const { id } = await setupUtil({
|
||||
moduleDir: import.meta.dir,
|
||||
moduleVariables: {
|
||||
workdir: "/home/coder",
|
||||
install_amp: props?.skipAmpMock ? "true" : "false",
|
||||
install_sourcegraph_amp: props?.skipAmpMock ? "true" : "false",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
sourcegraph_amp_model: "test-model",
|
||||
...props?.moduleVariables,
|
||||
},
|
||||
registerCleanup,
|
||||
@@ -68,94 +68,45 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
|
||||
setDefaultTimeout(60 * 1000);
|
||||
|
||||
describe("amp", async () => {
|
||||
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: {
|
||||
// amp_api_key: apiKey,
|
||||
// },
|
||||
// });
|
||||
// await execModuleScript(id);
|
||||
// const resp = await readFileContainer(
|
||||
// id,
|
||||
// "/home/coder/.amp-module/agentapi-start.log",
|
||||
// );
|
||||
// expect(resp).toContain("amp_api_key provided !");
|
||||
// });
|
||||
//
|
||||
test("install-latest-version", async () => {
|
||||
const { id } = await setup({
|
||||
skipAmpMock: true,
|
||||
skipAgentAPIMock: true,
|
||||
moduleVariables: {
|
||||
amp_version: "",
|
||||
},
|
||||
});
|
||||
test("happy-path", async () => {
|
||||
const { id } = await setup();
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("install-specific-version", async () => {
|
||||
test("api-key", async () => {
|
||||
const apiKey = "test-api-key-123";
|
||||
const { id } = await setup({
|
||||
skipAmpMock: true,
|
||||
moduleVariables: {
|
||||
amp_version: "0.0.1755964909-g31e083",
|
||||
sourcegraph_amp_api_key: apiKey,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.amp-module/agentapi-start.log",
|
||||
"/home/coder/.sourcegraph-amp-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain("0.0.1755964909-g31e08");
|
||||
expect(resp).toContain("sourcegraph_amp_api_key provided !");
|
||||
});
|
||||
|
||||
test("install-via-npm", async () => {
|
||||
const { id } = await setup({
|
||||
skipAmpMock: true,
|
||||
moduleVariables: {
|
||||
install_via_npm: "true",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const installLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.amp-module/install.log",
|
||||
);
|
||||
expect(installLog).toContain("Installing Amp via npm");
|
||||
|
||||
const startLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.amp-module/agentapi-start.log",
|
||||
);
|
||||
expect(startLog).toContain("AMP version:");
|
||||
});
|
||||
|
||||
test("custom-workdir", async () => {
|
||||
const workdir = "/tmp/amp-test";
|
||||
test("custom-folder", async () => {
|
||||
const folder = "/tmp/sourcegraph-amp-test";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
workdir,
|
||||
folder,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.amp-module/agentapi-start.log",
|
||||
"/home/coder/.sourcegraph-amp-module/install.log",
|
||||
);
|
||||
expect(resp).toContain(workdir);
|
||||
expect(resp).toContain(folder);
|
||||
});
|
||||
|
||||
test("pre-post-install-scripts", async () => {
|
||||
@@ -168,104 +119,39 @@ describe("amp", async () => {
|
||||
await execModuleScript(id);
|
||||
const preLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.amp-module/pre_install.log",
|
||||
"/home/coder/.sourcegraph-amp-module/pre_install.log",
|
||||
);
|
||||
expect(preLog).toContain("pre-install-script");
|
||||
const postLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.amp-module/post_install.log",
|
||||
"/home/coder/.sourcegraph-amp-module/post_install.log",
|
||||
);
|
||||
expect(postLog).toContain("post-install-script");
|
||||
});
|
||||
|
||||
test("instruction-prompt", async () => {
|
||||
const prompt = "this is a instruction prompt for AMP";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
instruction_prompt: prompt,
|
||||
},
|
||||
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,
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(id, "/home/coder/.config/AGENTS.md");
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.sourcegraph-amp-module/SYSTEM_PROMPT.md",
|
||||
);
|
||||
expect(resp).toContain(prompt);
|
||||
});
|
||||
|
||||
test("ai-prompt", async () => {
|
||||
test("task-prompt", async () => {
|
||||
const prompt = "this is a task prompt for AMP";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
ai_prompt: prompt,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.amp-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain(`amp task prompt provided : ${prompt}`);
|
||||
});
|
||||
|
||||
test("custom-base-config", async () => {
|
||||
const customConfig = JSON.stringify({
|
||||
"amp.anthropic.thinking.enabled": false,
|
||||
"amp.todos.enabled": false,
|
||||
"amp.tools.stopTimeout": 900,
|
||||
"amp.git.commit.ampThread.enabled": true,
|
||||
});
|
||||
const customMcp = JSON.stringify({
|
||||
"test-server": {
|
||||
command: "/usr/bin/test-mcp",
|
||||
args: ["--test-arg"],
|
||||
type: "stdio",
|
||||
},
|
||||
});
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
base_amp_config: customConfig,
|
||||
mcp: customMcp,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id, {
|
||||
CODER_AGENT_TOKEN: "test-token",
|
||||
CODER_AGENT_URL: "http://test-url:3000",
|
||||
});
|
||||
const settingsContent = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.config/amp/settings.json",
|
||||
);
|
||||
const settings = JSON.parse(settingsContent);
|
||||
|
||||
expect(settings["amp.anthropic.thinking.enabled"]).toBe(false);
|
||||
expect(settings["amp.todos.enabled"]).toBe(false);
|
||||
expect(settings["amp.tools.stopTimeout"]).toBe(900);
|
||||
expect(settings["amp.git.commit.ampThread.enabled"]).toBe(true);
|
||||
expect(settings["amp.mcpServers"]).toBeDefined();
|
||||
expect(settings["amp.mcpServers"].coder).toBeDefined();
|
||||
expect(settings["amp.mcpServers"]["test-server"]).toBeDefined();
|
||||
expect(settings["amp.mcpServers"]["test-server"].command).toBe(
|
||||
"/usr/bin/test-mcp",
|
||||
);
|
||||
expect(settings["amp.mcpServers"]["test-server"].args).toEqual([
|
||||
"--test-arg",
|
||||
]);
|
||||
});
|
||||
|
||||
test("default-base-config", async () => {
|
||||
const { id } = await setup();
|
||||
await execModuleScript(id, {
|
||||
CODER_AGENT_TOKEN: "test-token",
|
||||
CODER_AGENT_URL: "http://test-url:3000",
|
||||
SOURCEGRAPH_AMP_TASK_PROMPT: prompt,
|
||||
});
|
||||
const settingsContent = await readFileContainer(
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.config/amp/settings.json",
|
||||
"/home/coder/.sourcegraph-amp-module/agentapi-start.log",
|
||||
);
|
||||
const settings = JSON.parse(settingsContent);
|
||||
|
||||
expect(settings["amp.anthropic.thinking.enabled"]).toBe(true);
|
||||
expect(settings["amp.todos.enabled"]).toBe(true);
|
||||
expect(settings["amp.mcpServers"]).toBeDefined();
|
||||
expect(settings["amp.mcpServers"].coder).toBeDefined();
|
||||
expect(settings["amp.mcpServers"].coder.command).toBe("coder");
|
||||
expect(resp).toContain(`sourcegraph amp task prompt provided : ${prompt}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,12 +6,7 @@ terraform {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
external = {
|
||||
source = "hashicorp/external"
|
||||
version = "2.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
@@ -41,9 +36,28 @@ variable "icon" {
|
||||
default = "/icon/sourcegraph-amp.svg"
|
||||
}
|
||||
|
||||
variable "workdir" {
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to run AMP CLI in."
|
||||
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" {
|
||||
@@ -55,87 +69,21 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Claude Code"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app"
|
||||
default = "Amp"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app"
|
||||
default = "Amp CLI"
|
||||
default = "v0.3.0"
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing amp cli"
|
||||
description = "Custom script to run before installing sourcegraph_amp"
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing amp cli."
|
||||
description = "Custom script to run after installing sourcegraph_amp."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI"
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "install_amp" {
|
||||
type = bool
|
||||
description = "Whether to install amp cli."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "install_via_npm" {
|
||||
type = bool
|
||||
description = "Install Amp via npm instead of the official installer."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "amp_api_key" {
|
||||
type = string
|
||||
description = "amp cli API Key"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "amp_version" {
|
||||
type = string
|
||||
description = "The version of amp cli to install."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "Task prompt for the Amp CLI"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "instruction_prompt" {
|
||||
type = string
|
||||
description = "Instruction prompt for the Amp CLI. https://ampcode.com/manual#AGENTS.md"
|
||||
default = ""
|
||||
}
|
||||
|
||||
resource "coder_env" "amp_api_key" {
|
||||
agent_id = var.agent_id
|
||||
name = "AMP_API_KEY"
|
||||
value = var.amp_api_key
|
||||
}
|
||||
|
||||
variable "base_amp_config" {
|
||||
type = string
|
||||
description = <<-EOT
|
||||
@@ -154,25 +102,22 @@ variable "base_amp_config" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "mcp" {
|
||||
variable "additional_mcp_servers" {
|
||||
type = string
|
||||
description = "Additional MCP servers configuration in JSON format to append to amp.mcpServers."
|
||||
default = null
|
||||
}
|
||||
|
||||
data "external" "env" {
|
||||
program = ["sh", "-c", "echo '{\"CODER_AGENT_TOKEN\":\"'$CODER_AGENT_TOKEN'\",\"CODER_AGENT_URL\":\"'$CODER_AGENT_URL'\"}'"]
|
||||
}
|
||||
|
||||
locals {
|
||||
app_slug = "amp"
|
||||
|
||||
default_base_config = jsonencode({
|
||||
default_base_config = {
|
||||
"amp.anthropic.thinking.enabled" = true
|
||||
"amp.todos.enabled" = true
|
||||
})
|
||||
}
|
||||
|
||||
user_config = jsondecode(var.base_amp_config != "" ? var.base_amp_config : local.default_base_config)
|
||||
# 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 = {
|
||||
@@ -180,16 +125,14 @@ locals {
|
||||
"command" = "coder"
|
||||
"args" = ["exp", "mcp", "server"]
|
||||
"env" = {
|
||||
"CODER_MCP_APP_STATUS_SLUG" = var.report_tasks == true ? local.app_slug : ""
|
||||
"CODER_MCP_AI_AGENTAPI_URL" = var.report_tasks == true ? "http://localhost:3284" : ""
|
||||
"CODER_AGENT_TOKEN" = data.external.env.result.CODER_AGENT_TOKEN
|
||||
"CODER_AGENT_URL" = data.external.env.result.CODER_AGENT_URL
|
||||
"CODER_MCP_APP_STATUS_SLUG" = local.app_slug
|
||||
"CODER_MCP_AI_AGENTAPI_URL" = "http://localhost:3284"
|
||||
}
|
||||
"type" = "stdio"
|
||||
}
|
||||
}
|
||||
|
||||
additional_mcp = var.mcp != null ? jsondecode(var.mcp) : {}
|
||||
additional_mcp = var.additional_mcp_servers != null ? jsondecode(var.additional_mcp_servers) : {}
|
||||
|
||||
merged_mcp_servers = merge(
|
||||
lookup(local.user_config, "amp.mcpServers", {}),
|
||||
@@ -203,24 +146,21 @@ locals {
|
||||
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".amp-module"
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
module_dir_name = ".sourcegraph-amp-module"
|
||||
}
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.0.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.workdir
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
cli_app = var.cli_app
|
||||
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
|
||||
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
|
||||
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
|
||||
@@ -233,10 +173,8 @@ module "agentapi" {
|
||||
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
ARG_AMP_API_KEY='${var.amp_api_key}' \
|
||||
ARG_AMP_START_DIRECTORY='${var.workdir}' \
|
||||
ARG_AMP_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
SOURCEGRAPH_AMP_API_KEY='${var.sourcegraph_amp_api_key}' \
|
||||
SOURCEGRAPH_AMP_START_DIRECTORY='${var.folder}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
@@ -247,11 +185,9 @@ module "agentapi" {
|
||||
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
ARG_INSTALL_AMP='${var.install_amp}' \
|
||||
ARG_INSTALL_VIA_NPM='${var.install_via_npm}' \
|
||||
ARG_AMP_CONFIG="${base64encode(jsonencode(local.final_config))}" \
|
||||
ARG_AMP_VERSION='${var.amp_version}' \
|
||||
ARG_AMP_INSTRUCTION_PROMPT='${base64encode(var.instruction_prompt)}' \
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,119 +1,77 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
|
||||
# ANSI colors
|
||||
BOLD='\033[1m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
ARG_INSTALL_AMP=${ARG_INSTALL_AMP:-true}
|
||||
ARG_INSTALL_VIA_NPM=${ARG_INSTALL_VIA_NPM:-false}
|
||||
ARG_AMP_VERSION=${ARG_AMP_VERSION:-}
|
||||
ARG_AMP_INSTRUCTION_PROMPT=$(echo -n "${ARG_AMP_INSTRUCTION_PROMPT:-}" | base64 -d)
|
||||
ARG_AMP_CONFIG=$(echo -n "${ARG_AMP_CONFIG:-}" | base64 -d)
|
||||
|
||||
echo "--------------------------------"
|
||||
printf "Install flag: %s\n" "$ARG_INSTALL_AMP"
|
||||
printf "Install via npm: %s\n" "$ARG_INSTALL_VIA_NPM"
|
||||
printf "Amp Version: %s\n" "$ARG_AMP_VERSION"
|
||||
printf "AMP Config: %s\n" "$ARG_AMP_CONFIG"
|
||||
printf "Instruction Prompt: %s\n" "$ARG_AMP_INSTRUCTION_PROMPT"
|
||||
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
|
||||
}
|
||||
|
||||
install_amp_npm() {
|
||||
printf "%s${YELLOW}Installing Amp via npm${NC}\n" "${BOLD}"
|
||||
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
|
||||
|
||||
# Load nvm if available
|
||||
# shellcheck source=/dev/null
|
||||
if [ -f "$HOME/.nvm/nvm.sh" ]; then
|
||||
source "$HOME/.nvm/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
|
||||
|
||||
if ! command_exists node || ! command_exists npm; then
|
||||
printf "${YELLOW}Warning: Node.js/npm not found. Skipping Amp installation.${NC}\n"
|
||||
printf "To install Amp via npm, please install Node.js and npm first.\n"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "Node.js version: %s\n" "$(node --version)"
|
||||
printf "npm version: %s\n" "$(npm --version)"
|
||||
|
||||
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_AMP_VERSION" ]; then
|
||||
npm install -g "@sourcegraph/amp@$ARG_AMP_VERSION"
|
||||
else
|
||||
npm install -g "@sourcegraph/amp"
|
||||
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
|
||||
}
|
||||
|
||||
install_amp_official() {
|
||||
printf "%s Installing Amp using official installer\n" "${BOLD}"
|
||||
|
||||
if [ -n "$ARG_AMP_VERSION" ]; then
|
||||
export AMP_VERSION="$ARG_AMP_VERSION"
|
||||
printf "Installing Amp version: %s\n" "$AMP_VERSION"
|
||||
fi
|
||||
|
||||
if curl -fsSL https://ampcode.com/install.sh | bash; then
|
||||
export PATH="$HOME/.local/bin:$HOME/.amp/bin:$PATH"
|
||||
|
||||
if ! grep -q 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc"; then
|
||||
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc"
|
||||
fi
|
||||
else
|
||||
printf "${YELLOW}Warning: Official installer failed. Installation skipped.${NC}\n"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
function install_amp() {
|
||||
if [ "${ARG_INSTALL_AMP}" = "true" ]; then
|
||||
if [ "${ARG_INSTALL_VIA_NPM}" = "true" ]; then
|
||||
install_amp_npm || {
|
||||
printf "${YELLOW}Amp installation via npm failed.${NC}\n"
|
||||
return 0
|
||||
}
|
||||
printf "Node.js installed: %s\n" "$(node --version)"
|
||||
printf "npm installed: %s\n" "$(npm --version)"
|
||||
else
|
||||
install_amp_official || {
|
||||
printf "${YELLOW}Amp installation via official installer failed.${NC}\n"
|
||||
return 0
|
||||
}
|
||||
printf "Node.js is installed but npm is not available. Please install npm manually.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command_exists amp; then
|
||||
printf "%s${GREEN}Successfully installed Sourcegraph Amp CLI. Version: %s${NC}\n" "${BOLD}" "$(amp --version)"
|
||||
fi
|
||||
else
|
||||
printf "Skipping Sourcegraph Amp CLI installation (install_amp=false)\n"
|
||||
fi
|
||||
}
|
||||
|
||||
function setup_instruction_prompt() {
|
||||
if [ -n "${ARG_AMP_INSTRUCTION_PROMPT:-}" ]; then
|
||||
echo "Setting AMP instruction prompt..."
|
||||
mkdir -p "$HOME/.config"
|
||||
echo "$ARG_AMP_INSTRUCTION_PROMPT" > "$HOME/.config/AGENTS.md"
|
||||
echo "Instruction prompt saved to $HOME/.config/AGENTS.md"
|
||||
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 instruction prompt provided for Sourcegraph AMP."
|
||||
echo "No system prompt provided for Sourcegraph AMP."
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -128,17 +86,11 @@ function configure_amp_settings() {
|
||||
fi
|
||||
|
||||
echo "Writing AMP configuration to $SETTINGS_PATH"
|
||||
UPDATED_CONFIG=$(echo "$ARG_AMP_CONFIG" | jq --arg token "$CODER_AGENT_TOKEN" --arg url "$CODER_AGENT_URL" \
|
||||
".[\"amp.mcpServers\"].coder.env += {
|
||||
\"CODER_AGENT_TOKEN\": \"$CODER_AGENT_TOKEN\",
|
||||
\"CODER_AGENT_URL\": \"$CODER_AGENT_URL\"
|
||||
}")
|
||||
printf "UPDATED_CONFIG: %s\n" "$UPDATED_CONFIG"
|
||||
printf '%s\n' "$UPDATED_CONFIG" > "$SETTINGS_PATH"
|
||||
printf '%s\n' "$ARG_AMP_CONFIG" > "$SETTINGS_PATH"
|
||||
|
||||
echo "AMP configuration complete"
|
||||
}
|
||||
|
||||
install_amp
|
||||
setup_instruction_prompt
|
||||
install_sourcegraph_amp
|
||||
setup_system_prompt
|
||||
configure_amp_settings
|
||||
|
||||
@@ -6,11 +6,11 @@ set -euo pipefail
|
||||
source "$HOME/.bashrc"
|
||||
# shellcheck source=/dev/null
|
||||
if [ -f "$HOME/.nvm/nvm.sh" ]; then
|
||||
source "$HOME/.nvm/nvm.sh"
|
||||
source "$HOME"/.nvm/nvm.sh
|
||||
else
|
||||
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||
fi
|
||||
|
||||
export PATH="$HOME/.local/bin:$HOME/.amp/bin:$HOME/.npm-global/bin:$PATH"
|
||||
|
||||
function ensure_command() {
|
||||
command -v "$1" &> /dev/null || {
|
||||
echo "Error: '$1' not found." >&2
|
||||
@@ -18,21 +18,10 @@ function ensure_command() {
|
||||
}
|
||||
}
|
||||
|
||||
ARG_AMP_START_DIRECTORY=${ARG_AMP_START_DIRECTORY:-"$HOME"}
|
||||
ARG_AMP_API_KEY=${ARG_AMP_API_KEY:-}
|
||||
ARG_AMP_TASK_PROMPT=$(echo -n "${ARG_AMP_TASK_PROMPT:-}" | base64 -d)
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
|
||||
echo "--------------------------------"
|
||||
printf "Workspace: %s\n" "$ARG_AMP_START_DIRECTORY"
|
||||
printf "Task Prompt: %s\n" "$ARG_AMP_TASK_PROMPT"
|
||||
printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
|
||||
echo "--------------------------------"
|
||||
|
||||
ensure_command amp
|
||||
echo "AMP version: $(amp --version)"
|
||||
|
||||
dir="$ARG_AMP_START_DIRECTORY"
|
||||
dir="$SOURCEGRAPH_AMP_START_DIRECTORY"
|
||||
if [[ -d "$dir" ]]; then
|
||||
echo "Using existing directory: $dir"
|
||||
else
|
||||
@@ -41,23 +30,20 @@ else
|
||||
fi
|
||||
cd "$dir"
|
||||
|
||||
if [ -n "$ARG_AMP_API_KEY" ]; then
|
||||
printf "amp_api_key provided !\n"
|
||||
export AMP_API_KEY=$ARG_AMP_API_KEY
|
||||
if [ -n "$SOURCEGRAPH_AMP_API_KEY" ]; then
|
||||
printf "sourcegraph_amp_api_key provided !\n"
|
||||
export AMP_API_KEY=$SOURCEGRAPH_AMP_API_KEY
|
||||
else
|
||||
printf "amp_api_key not provided\n"
|
||||
printf "sourcegraph_amp_api_key not provided\n"
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_AMP_TASK_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" == "true" ]; then
|
||||
printf "amp task prompt provided : %s" "$ARG_AMP_TASK_PROMPT\n"
|
||||
PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AMP_TASK_PROMPT"
|
||||
else
|
||||
PROMPT="$ARG_AMP_TASK_PROMPT"
|
||||
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 --type amp --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp"
|
||||
agentapi server --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp"
|
||||
else
|
||||
printf "No task prompt given.\n"
|
||||
agentapi server --type amp --term-width=67 --term-height=1190 -- amp
|
||||
agentapi server --term-width=67 --term-height=1190 -- amp
|
||||
fi
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.13"
|
||||
source = "coder/coder"
|
||||
}
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
@@ -13,36 +12,41 @@ terraform {
|
||||
# This template requires a valid Docker socket
|
||||
# However, you can reference our Kubernetes/VM
|
||||
# example templates and adapt the Claude Code module
|
||||
#
|
||||
# see: https://registry.coder.com/templates
|
||||
#
|
||||
# see: https://registry.coder.com/templates
|
||||
provider "docker" {}
|
||||
|
||||
# A `coder_ai_task` resource enables Tasks and associates
|
||||
# the task with the coder_app that will act as an AI agent.
|
||||
resource "coder_ai_task" "task" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
app_id = module.claude-code[count.index].task_app_id
|
||||
}
|
||||
|
||||
# You can read the task prompt from the `coder_task` data source.
|
||||
data "coder_task" "me" {}
|
||||
|
||||
# The Claude Code module does the automatic task reporting
|
||||
# Other agent modules: https://registry.coder.com/modules?search=agent
|
||||
# Or use a custom agent:
|
||||
# Or use a custom agent:
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.0.0"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/projects"
|
||||
folder = "/home/coder/projects"
|
||||
install_claude_code = true
|
||||
claude_code_version = "latest"
|
||||
order = 999
|
||||
claude_api_key = ""
|
||||
ai_prompt = data.coder_task.me.prompt
|
||||
system_prompt = data.coder_parameter.system_prompt.value
|
||||
model = "sonnet"
|
||||
permission_mode = "plan"
|
||||
post_install_script = data.coder_parameter.setup_script.value
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# We are using presets to set the prompts, image, and set up instructions
|
||||
@@ -62,13 +66,13 @@ data "coder_workspace_preset" "default" {
|
||||
(servers, dev watchers, GUI apps).
|
||||
- Built-in tools - use for everything else:
|
||||
(file operations, git commands, builds & installs, one-off shell commands)
|
||||
|
||||
|
||||
Remember this decision rule:
|
||||
- Stays running? → desktop-commander
|
||||
- Finishes immediately? → built-in tools
|
||||
|
||||
|
||||
-- Context --
|
||||
There is an existing app and tmux dev server running on port 8000. Be sure to read it's CLAUDE.md (./realworld-django-rest-framework-angular/CLAUDE.md) to learn more about it.
|
||||
There is an existing app and tmux dev server running on port 8000. Be sure to read it's CLAUDE.md (./realworld-django-rest-framework-angular/CLAUDE.md) to learn more about it.
|
||||
|
||||
Since this app is for demo purposes and the user is previewing the homepage and subsequent pages, aim to make the first visual change/prototype very quickly so the user can preview it, then focus on backend or logic which can be a more involved, long-running architecture plan.
|
||||
|
||||
@@ -118,7 +122,7 @@ data "coder_workspace_preset" "default" {
|
||||
|
||||
# Pre-builds is a Coder Premium
|
||||
# feature to speed up workspace creation
|
||||
#
|
||||
#
|
||||
# see https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces
|
||||
# prebuilds {
|
||||
# instances = 1
|
||||
@@ -137,6 +141,13 @@ data "coder_parameter" "system_prompt" {
|
||||
description = "System prompt for the agent with generalized instructions"
|
||||
mutable = false
|
||||
}
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Write a prompt for Claude Code"
|
||||
mutable = true
|
||||
}
|
||||
data "coder_parameter" "setup_script" {
|
||||
name = "setup_script"
|
||||
display_name = "Setup Script"
|
||||
@@ -161,6 +172,23 @@ data "coder_parameter" "preview_port" {
|
||||
mutable = false
|
||||
}
|
||||
|
||||
# Other variables for Claude Code
|
||||
resource "coder_env" "claude_task_prompt" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_CLAUDE_TASK_PROMPT"
|
||||
value = data.coder_parameter.ai_prompt.value
|
||||
}
|
||||
resource "coder_env" "app_status_slug" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_APP_STATUS_SLUG"
|
||||
value = "ccw"
|
||||
}
|
||||
resource "coder_env" "claude_system_prompt" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
|
||||
value = data.coder_parameter.system_prompt.value
|
||||
}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
@@ -272,6 +300,13 @@ 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"
|
||||
@@ -286,13 +321,23 @@ module "cursor" {
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "~> 1.0"
|
||||
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"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
folder = "/home/coder/projects"
|
||||
order = 2
|
||||
}
|
||||
|
||||
resource "docker_volume" "home_volume" {
|
||||
|
||||
|
Before Width: | Height: | Size: 976 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 302 KiB |
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
|
||||
```tf
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "2.0.0"
|
||||
version = "1.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.12"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
default = "v0.3.3"
|
||||
}
|
||||
|
||||
variable "agentapi_port" {
|
||||
@@ -239,6 +239,8 @@ resource "coder_app" "agentapi_cli" {
|
||||
group = var.cli_app_group
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = coder_app.agentapi_web.id
|
||||
resource "coder_ai_task" "agentapi" {
|
||||
sidebar_app {
|
||||
id = coder_app.agentapi_web.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,58 +8,76 @@ tags: [agent, ai, aider]
|
||||
|
||||
# Aider
|
||||
|
||||
Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider with AgentAPI for seamless Coder Tasks Support.
|
||||
Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider and provides a persistent session using screen or tmux.
|
||||
|
||||
```tf
|
||||
variable "api_key" {
|
||||
type = string
|
||||
description = "API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
api_key = var.api_key
|
||||
ai_provider = "google"
|
||||
model = "gemini"
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
## Features
|
||||
|
||||
- pipx is automatically installed if not already available
|
||||
- **Interactive Parameter Selection**: Choose your AI provider, model, and configuration options when creating the workspace
|
||||
- **Multiple AI Providers**: Supports Anthropic (Claude), OpenAI, DeepSeek, GROQ, and OpenRouter
|
||||
- **Persistent Sessions**: Uses screen (default) or tmux to keep Aider running in the background
|
||||
- **Optional Dependencies**: Install Playwright for web page scraping and PortAudio for voice coding
|
||||
- **Project Integration**: Works with any project directory, including Git repositories
|
||||
- **Browser UI**: Use Aider in your browser with a modern web interface instead of the terminal
|
||||
- **Non-Interactive Mode**: Automatically processes tasks when provided via the `task_prompt` variable
|
||||
|
||||
## Usage Example
|
||||
## Module Parameters
|
||||
|
||||
> [!NOTE]
|
||||
> The `use_screen` and `use_tmux` parameters cannot both be enabled at the same time. By default, `use_screen` is set to `true` and `use_tmux` is set to `false`.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic setup with API key
|
||||
|
||||
```tf
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
name = "AI Prompt"
|
||||
description = "Write an initial prompt for Aider to work on."
|
||||
type = "string"
|
||||
default = ""
|
||||
mutable = true
|
||||
}
|
||||
|
||||
variable "gemini_api_key" {
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Gemini API key"
|
||||
description = "Anthropic API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
api_key = var.gemini_api_key
|
||||
install_aider = true
|
||||
workdir = "/home/coder"
|
||||
ai_provider = "google"
|
||||
model = "gemini"
|
||||
install_agentapi = true
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
system_prompt = "..."
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
}
|
||||
```
|
||||
|
||||
This basic setup will:
|
||||
|
||||
- Install Aider in the workspace
|
||||
- Create a persistent screen session named "aider"
|
||||
- Configure Aider to use Anthropic Claude 3.7 Sonnet model
|
||||
- Enable task reporting (configures Aider to report tasks to Coder MCP)
|
||||
|
||||
### Using OpenAI with tmux
|
||||
|
||||
```tf
|
||||
variable "openai_api_key" {
|
||||
type = string
|
||||
description = "OpenAI API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
use_tmux = true
|
||||
ai_provider = "openai"
|
||||
ai_model = "4o" # Uses Aider's built-in alias for gpt-4o
|
||||
ai_api_key = var.openai_api_key
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,16 +93,174 @@ variable "custom_api_key" {
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "2.0.0"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
ai_provider = "custom"
|
||||
custom_env_var_name = "MY_CUSTOM_API_KEY"
|
||||
model = "custom-model"
|
||||
api_key = var.custom_api_key
|
||||
ai_model = "custom-model"
|
||||
ai_api_key = var.custom_api_key
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Custom Extensions (Experimental)
|
||||
|
||||
You can extend Aider's capabilities by adding custom extensions:
|
||||
|
||||
```tf
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
|
||||
experiment_pre_install_script = <<-EOT
|
||||
pip install some-custom-dependency
|
||||
EOT
|
||||
|
||||
experiment_additional_extensions = <<-EOT
|
||||
custom-extension:
|
||||
args: []
|
||||
cmd: custom-extension-command
|
||||
description: A custom extension for Aider
|
||||
enabled: true
|
||||
envs: {}
|
||||
name: custom-extension
|
||||
timeout: 300
|
||||
type: stdio
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
Note: The indentation in the heredoc is preserved, so you can write the YAML naturally.
|
||||
|
||||
## Task Reporting (Experimental)
|
||||
|
||||
> This functionality is in early access as of Coder v2.21 and is still evolving.
|
||||
> For now, we recommend testing it in a demo or staging environment,
|
||||
> rather than deploying to production
|
||||
>
|
||||
> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents)
|
||||
>
|
||||
> Join our [Discord channel](https://discord.gg/coder) or
|
||||
> [contact us](https://coder.com/contact) to get help or share feedback.
|
||||
|
||||
Your workspace must have either `screen` or `tmux` installed to use this.
|
||||
|
||||
Task reporting is **enabled by default** in this module, allowing you to:
|
||||
|
||||
- Send an initial prompt to Aider during workspace creation
|
||||
- Monitor task progress in the Coder UI
|
||||
- Use the `coder_parameter` resource to collect prompts from users
|
||||
|
||||
### Setting up Task Reporting
|
||||
|
||||
To use task reporting effectively:
|
||||
|
||||
1. Add the Coder Login module to your template
|
||||
2. Configure the necessary variables to pass the task prompt
|
||||
3. Optionally add a coder_parameter to collect prompts from users
|
||||
|
||||
Here's a complete example:
|
||||
|
||||
```tf
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/coder-login/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Anthropic API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Write a prompt for Aider"
|
||||
mutable = true
|
||||
ephemeral = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
task_prompt = data.coder_parameter.ai_prompt.value
|
||||
|
||||
# Optionally customize the system prompt
|
||||
system_prompt = <<-EOT
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
When a task prompt is provided via the `task_prompt` variable, the module automatically:
|
||||
|
||||
1. Combines the system prompt with the task prompt into a single message in the format:
|
||||
|
||||
```
|
||||
SYSTEM PROMPT:
|
||||
[system_prompt content]
|
||||
|
||||
This is your current task: [task_prompt]
|
||||
```
|
||||
|
||||
2. Executes the task during workspace creation using the `--message` and `--yes-always` flags
|
||||
3. Logs task output to `$HOME/.aider.log` for reference
|
||||
|
||||
If you want to disable task reporting, set `experiment_report_tasks = false` in your module configuration.
|
||||
|
||||
## Using Aider in Your Workspace
|
||||
|
||||
After the workspace starts, Aider will be installed and configured according to your parameters. A persistent session will automatically be started during workspace creation.
|
||||
|
||||
### Session Options
|
||||
|
||||
You can run Aider in three different ways:
|
||||
|
||||
1. **Direct Mode**: Aider starts directly in the specified folder when you click the app button
|
||||
|
||||
- Simple setup without persistent context
|
||||
- Suitable for quick coding sessions
|
||||
|
||||
2. **Screen Mode** (Default): Run Aider in a screen session that persists across connections
|
||||
|
||||
- Session name: "aider" (or configured via `session_name`)
|
||||
|
||||
3. **Tmux Mode**: Run Aider in a tmux session instead of screen
|
||||
|
||||
- Set `use_tmux = true` to enable
|
||||
- Session name: "aider" (or configured via `session_name`)
|
||||
- Configures tmux with mouse support for shared sessions
|
||||
|
||||
Persistent sessions (screen/tmux) allow you to:
|
||||
|
||||
- Disconnect and reconnect without losing context
|
||||
- Run Aider in the background while doing other work
|
||||
- Switch between terminal and browser interfaces
|
||||
|
||||
### Available AI Providers and Models
|
||||
|
||||
Aider supports various providers and models, and this module integrates directly with Aider's built-in model aliases:
|
||||
@@ -104,12 +280,10 @@ For a complete and up-to-date list of supported aliases and models, please refer
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If `aider` is not found, ensure `install_aider = true` and your API key is valid
|
||||
- Logs are written under `/home/coder/.aider-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
|
||||
If you encounter issues:
|
||||
|
||||
## References
|
||||
1. **Screen/Tmux issues**: If you can't reconnect to your session, check if the session exists with `screen -list` or `tmux list-sessions`
|
||||
2. **API key issues**: Ensure you've entered the correct API key for your selected provider
|
||||
3. **Browser mode issues**: If the browser interface doesn't open, check that you're accessing it from a machine that can reach your Coder workspace
|
||||
|
||||
- [Aider Documentation](https://aider.chat/docs)
|
||||
- [AgentAPI Documentation](https://github.com/coder/agentapi)
|
||||
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
|
||||
For more information on using Aider, see the [Aider documentation](https://aider.chat/docs/).
|
||||
|
||||
@@ -1,138 +1,107 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
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";
|
||||
findResourceInstance,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
describe("aider", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
interface SetupProps {
|
||||
skipAgentAPIMock?: boolean;
|
||||
skipAiderMock?: 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_aider: props?.skipAiderMock ? "true" : "false",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
aider_model: "test-model",
|
||||
...props?.moduleVariables,
|
||||
},
|
||||
registerCleanup,
|
||||
projectDir,
|
||||
skipAgentAPIMock: props?.skipAgentAPIMock,
|
||||
agentapiMockScript: props?.agentapiMockScript,
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
// Place the Aider mock CLI binary inside the container
|
||||
if (!props?.skipAiderMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/aider",
|
||||
content: await loadTestFile(`${import.meta.dir}`, "aider-mock.sh"),
|
||||
it("configures task prompt correctly", async () => {
|
||||
const testPrompt = "Add a hello world function";
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
task_prompt: testPrompt,
|
||||
});
|
||||
}
|
||||
|
||||
return { id };
|
||||
};
|
||||
|
||||
setDefaultTimeout(60 * 1000);
|
||||
|
||||
describe("Aider", async () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
test("happy-path", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
model: "gemini",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("api-key", async () => {
|
||||
const apiKey = "test-api-key-123";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
api_key: apiKey,
|
||||
model: "gemini",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.aider-module/agentapi-start.log",
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(
|
||||
`This is your current task: ${testPrompt}`,
|
||||
);
|
||||
expect(resp).toContain("API key provided!");
|
||||
expect(instance.script).toContain("aider --architect --yes-always");
|
||||
});
|
||||
|
||||
test("custom-folder", async () => {
|
||||
const workdir = "/tmp/aider-test";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
workdir,
|
||||
model: "gemini",
|
||||
},
|
||||
it("handles custom system prompt", async () => {
|
||||
const customPrompt = "Report all tasks with state: working";
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
system_prompt: customPrompt,
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.aider-module/install.log",
|
||||
);
|
||||
expect(resp).toContain(workdir);
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(customPrompt);
|
||||
});
|
||||
|
||||
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'",
|
||||
model: "gemini",
|
||||
},
|
||||
it("handles pre and post install scripts", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
experiment_pre_install_script: "echo 'Pre-install script executed'",
|
||||
experiment_post_install_script: "echo 'Post-install script executed'",
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const preLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.aider-module/pre_install.log",
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
|
||||
expect(instance.script).toContain("Running pre-install script");
|
||||
expect(instance.script).toContain("Running post-install script");
|
||||
expect(instance.script).toContain("base64 -d > /tmp/pre_install.sh");
|
||||
expect(instance.script).toContain("base64 -d > /tmp/post_install.sh");
|
||||
});
|
||||
|
||||
it("validates that use_screen and use_tmux cannot both be true", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
use_screen: true,
|
||||
use_tmux: true,
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
|
||||
expect(instance.script).toContain(
|
||||
"Error: Both use_screen and use_tmux cannot be enabled at the same time",
|
||||
);
|
||||
expect(preLog).toContain("pre-install-script");
|
||||
const postLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.aider-module/post_install.log",
|
||||
expect(instance.script).toContain("exit 1");
|
||||
});
|
||||
|
||||
it("configures Aider with known provider and model", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
ai_provider: "anthropic",
|
||||
ai_model: "sonnet",
|
||||
ai_api_key: "test-anthropic-key",
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(
|
||||
'export ANTHROPIC_API_KEY=\\"test-anthropic-key\\"',
|
||||
);
|
||||
expect(instance.script).toContain("--model sonnet");
|
||||
expect(instance.script).toContain(
|
||||
"Starting Aider using anthropic provider and model: sonnet",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles custom provider with custom env var and API key", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
ai_provider: "custom",
|
||||
custom_env_var_name: "MY_CUSTOM_API_KEY",
|
||||
ai_model: "custom-model",
|
||||
ai_api_key: "test-custom-key",
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(
|
||||
'export MY_CUSTOM_API_KEY=\\"test-custom-key\\"',
|
||||
);
|
||||
expect(instance.script).toContain("--model custom-model");
|
||||
expect(instance.script).toContain(
|
||||
"Starting Aider using custom provider and model: custom-model",
|
||||
);
|
||||
expect(postLog).toContain("post-install-script");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,84 +36,87 @@ variable "icon" {
|
||||
default = "/icon/aider.svg"
|
||||
}
|
||||
|
||||
variable "workdir" {
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to run Aider in."
|
||||
default = "/home/coder"
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI via AgentAPI"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Whether to use a subdomain for AgentAPI."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Aider"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app"
|
||||
default = "Aider"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app"
|
||||
default = "Aider CLI"
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Aider."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Aider."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "Initial task prompt for Aider."
|
||||
default = ""
|
||||
}
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
variable "install_aider" {
|
||||
type = bool
|
||||
description = "Whether to install Aider."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "aider_version" {
|
||||
type = string
|
||||
description = "The version of Aider to install."
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "use_screen" {
|
||||
type = bool
|
||||
description = "Whether to use screen for running Aider in the background"
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "use_tmux" {
|
||||
type = bool
|
||||
description = "Whether to use tmux instead of screen for running Aider in the background"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "session_name" {
|
||||
type = string
|
||||
description = "Name for the persistent session (screen or tmux)"
|
||||
default = "aider"
|
||||
}
|
||||
|
||||
variable "experiment_report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
type = string
|
||||
description = "System prompt for instructing Aider on task reporting and behavior"
|
||||
default = "You are a helpful coding assistant that helps developers write, debug, and understand code. Provide clear explanations, follow best practices, and help solve coding problems efficiently."
|
||||
default = <<-EOT
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
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 "task_prompt" {
|
||||
type = string
|
||||
description = "Task prompt to use with Aider"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Aider."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Aider."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_additional_extensions" {
|
||||
@@ -125,19 +128,20 @@ variable "experiment_additional_extensions" {
|
||||
variable "ai_provider" {
|
||||
type = string
|
||||
description = "AI provider to use with Aider (openai, anthropic, azure, google, etc.)"
|
||||
default = "google"
|
||||
default = "anthropic"
|
||||
validation {
|
||||
condition = contains(["openai", "anthropic", "azure", "google", "cohere", "mistral", "ollama", "custom"], var.ai_provider)
|
||||
error_message = "provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom"
|
||||
error_message = "ai_provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom"
|
||||
}
|
||||
}
|
||||
|
||||
variable "model" {
|
||||
variable "ai_model" {
|
||||
type = string
|
||||
description = "AI model to use with Aider. Can use Aider's built-in aliases like '4o' (gpt-4o), 'sonnet' (claude-3-7-sonnet), 'opus' (claude-3-opus), etc."
|
||||
default = "sonnet"
|
||||
}
|
||||
|
||||
variable "api_key" {
|
||||
variable "ai_api_key" {
|
||||
type = string
|
||||
description = "API key for the selected AI provider. This will be set as the appropriate environment variable based on the provider."
|
||||
default = ""
|
||||
@@ -150,66 +154,55 @@ variable "custom_env_var_name" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "base_aider_config" {
|
||||
type = string
|
||||
description = <<-EOT
|
||||
Base Aider configuration in yaml format. Will be stored in .aider.conf.yml file.
|
||||
|
||||
options include:
|
||||
read:
|
||||
- CONVENTIONS.md
|
||||
- anotherfile.txt
|
||||
- thirdfile.py
|
||||
model: xxx
|
||||
##Specify the OpenAI API key
|
||||
openai-api-key: xxx
|
||||
## (deprecated, use --set-env OPENAI_API_TYPE=<value>)
|
||||
openai-api-type: xxx
|
||||
## (deprecated, use --set-env OPENAI_API_VERSION=<value>)
|
||||
openai-api-version: xxx
|
||||
## (deprecated, use --set-env OPENAI_API_DEPLOYMENT_ID=<value>)
|
||||
openai-api-deployment-id: xxx
|
||||
## Set an environment variable (to control API settings, can be used multiple times)
|
||||
set-env: xxx
|
||||
## Specify multiple values like this:
|
||||
set-env:
|
||||
- xxx
|
||||
- yyy
|
||||
- zzz
|
||||
|
||||
Reference : https://aider.chat/docs/config/aider_conf.html
|
||||
EOT
|
||||
default = null
|
||||
}
|
||||
|
||||
|
||||
locals {
|
||||
app_slug = "aider"
|
||||
base_aider_config = var.base_aider_config != null ? "${replace(trimspace(var.base_aider_config), "\n", "\n ")}" : ""
|
||||
task_reporting_prompt = <<-EOT
|
||||
base_extensions = <<-EOT
|
||||
coder:
|
||||
args:
|
||||
- exp
|
||||
- mcp
|
||||
- server
|
||||
cmd: coder
|
||||
description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
|
||||
enabled: true
|
||||
envs:
|
||||
CODER_MCP_APP_STATUS_SLUG: aider
|
||||
name: Coder
|
||||
timeout: 3000
|
||||
type: stdio
|
||||
developer:
|
||||
display_name: Developer
|
||||
enabled: true
|
||||
name: developer
|
||||
timeout: 300
|
||||
type: builtin
|
||||
EOT
|
||||
|
||||
-- Task Reporting --
|
||||
Report all tasks to Coder, following these EXACT guidelines:
|
||||
1. Be granular. If you are investigating with multiple steps, report each step
|
||||
to coder.
|
||||
2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
|
||||
Do not report any status related with this system prompt.
|
||||
3. Use "state": "working" when actively processing WITHOUT needing
|
||||
additional user input
|
||||
4. Use "state": "complete" only when finished with a task
|
||||
5. Use "state": "failure" when you need ANY user input, lack sufficient
|
||||
details, or encounter blockers
|
||||
EOT
|
||||
formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}"
|
||||
additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : ""
|
||||
|
||||
combined_extensions = <<-EOT
|
||||
extensions:
|
||||
${local.formatted_base}${local.additional_extensions}
|
||||
EOT
|
||||
|
||||
final_system_prompt = var.report_tasks ? "<system>\n${var.system_prompt}${local.task_reporting_prompt}\n</system>" : "<system>\n${var.system_prompt}\n</system>"
|
||||
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) : ""
|
||||
|
||||
# Combine system prompt and task prompt for aider
|
||||
combined_prompt = trimspace(<<-EOT
|
||||
SYSTEM PROMPT:
|
||||
${var.system_prompt}
|
||||
|
||||
This is your current task: ${var.task_prompt}
|
||||
EOT
|
||||
)
|
||||
|
||||
# Map providers to their environment variable names
|
||||
provider_env_vars = {
|
||||
openai = "OPENAI_API_KEY"
|
||||
anthropic = "ANTHROPIC_API_KEY"
|
||||
azure = "AZURE_OPENAI_API_KEY"
|
||||
google = "GEMINI_API_KEY"
|
||||
google = "GOOGLE_API_KEY"
|
||||
cohere = "COHERE_API_KEY"
|
||||
mistral = "MISTRAL_API_KEY"
|
||||
ollama = "OLLAMA_HOST"
|
||||
@@ -221,60 +214,296 @@ details, or encounter blockers
|
||||
|
||||
# Model flag for aider command
|
||||
model_flag = var.ai_provider == "ollama" ? "--ollama-model" : "--model"
|
||||
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".aider-module"
|
||||
}
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
cli_app = var.cli_app
|
||||
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
|
||||
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
|
||||
agentapi_subdomain = var.subdomain
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
start_script = <<-EOT
|
||||
# Install and Initialize Aider
|
||||
resource "coder_script" "aider" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Aider"
|
||||
icon = var.icon
|
||||
script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -e
|
||||
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
ARG_WORKDIR='${var.workdir}' \
|
||||
ARG_API_KEY='${base64encode(var.api_key)}' \
|
||||
ARG_MODEL='${var.model}' \
|
||||
ARG_PROVIDER='${var.ai_provider}' \
|
||||
ARG_ENV_API_NAME_HOLDER='${local.env_var_name}' \
|
||||
ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
|
||||
ARG_AI_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_WORKDIR='${var.workdir}' \
|
||||
ARG_INSTALL_AIDER='${var.install_aider}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_AIDER_CONFIG="$(echo -n '${base64encode(local.base_aider_config)}' | base64 -d)" \
|
||||
/tmp/install.sh
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
echo "Setting up Aider AI pair programming..."
|
||||
|
||||
if [ "${var.use_screen}" = "true" ] && [ "${var.use_tmux}" = "true" ]; then
|
||||
echo "Error: Both use_screen and use_tmux cannot be enabled at the same time."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${var.folder}"
|
||||
|
||||
if [ "$(uname)" = "Linux" ]; then
|
||||
echo "Checking dependencies for Linux..."
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if ! command_exists tmux; then
|
||||
echo "Installing tmux for persistent sessions..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq tmux
|
||||
else
|
||||
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
|
||||
apt-get install -y -qq tmux || echo "Warning: Cannot install tmux without sudo privileges"
|
||||
fi
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo dnf install -y -q tmux
|
||||
else
|
||||
dnf install -y -q tmux || echo "Warning: Cannot install tmux without sudo privileges"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Unable to install tmux on this system. Neither apt-get nor dnf found."
|
||||
fi
|
||||
else
|
||||
echo "tmux is already installed, skipping installation."
|
||||
fi
|
||||
elif [ "${var.use_screen}" = "true" ]; then
|
||||
if ! command_exists screen; then
|
||||
echo "Installing screen for persistent sessions..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq screen
|
||||
else
|
||||
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
|
||||
apt-get install -y -qq screen || echo "Warning: Cannot install screen without sudo privileges"
|
||||
fi
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo dnf install -y -q screen
|
||||
else
|
||||
dnf install -y -q screen || echo "Warning: Cannot install screen without sudo privileges"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Unable to install screen on this system. Neither apt-get nor dnf found."
|
||||
fi
|
||||
else
|
||||
echo "screen is already installed, skipping installation."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "This module currently only supports Linux workspaces."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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_aider}" = "true" ]; then
|
||||
echo "Installing Aider..."
|
||||
|
||||
if ! command_exists python3 || ! command_exists pip3; then
|
||||
echo "Installing Python dependencies required for Aider..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq python3-pip python3-venv
|
||||
else
|
||||
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
|
||||
apt-get install -y -qq python3-pip python3-venv || echo "Warning: Cannot install Python packages without sudo privileges"
|
||||
fi
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo dnf install -y -q python3-pip python3-virtualenv
|
||||
else
|
||||
dnf install -y -q python3-pip python3-virtualenv || echo "Warning: Cannot install Python packages without sudo privileges"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Unable to install Python on this system. Neither apt-get nor dnf found."
|
||||
fi
|
||||
else
|
||||
echo "Python is already installed, skipping installation."
|
||||
fi
|
||||
|
||||
if ! command_exists aider; then
|
||||
curl -LsSf https://aider.chat/install.sh | sh
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.bashrc"; then
|
||||
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.bashrc"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.zshrc" ]; then
|
||||
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.zshrc"; then
|
||||
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.zshrc"
|
||||
fi
|
||||
fi
|
||||
|
||||
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_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Aider to report tasks via Coder MCP..."
|
||||
|
||||
mkdir -p "$HOME/.config/aider"
|
||||
|
||||
cat > "$HOME/.config/aider/config.yml" << EOL
|
||||
${trimspace(local.combined_extensions)}
|
||||
EOL
|
||||
echo "Added Coder MCP extension to Aider config.yml"
|
||||
fi
|
||||
|
||||
echo "Starting persistent Aider session..."
|
||||
|
||||
touch "$HOME/.aider.log"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
export PATH="$HOME/bin:$PATH"
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if [ -n "${var.task_prompt}" ]; then
|
||||
echo "Running Aider with message in tmux session..."
|
||||
|
||||
# Configure tmux for shared sessions
|
||||
if [ ! -f "$HOME/.tmux.conf" ]; then
|
||||
echo "Creating ~/.tmux.conf with shared session settings..."
|
||||
echo "set -g mouse on" > "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
|
||||
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
|
||||
echo "set -g mouse on" >> "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\""
|
||||
echo "Aider task started in tmux session '${var.session_name}'. Check the UI for progress."
|
||||
else
|
||||
# Configure tmux for shared sessions
|
||||
if [ ! -f "$HOME/.tmux.conf" ]; then
|
||||
echo "Creating ~/.tmux.conf with shared session settings..."
|
||||
echo "set -g mouse on" > "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
|
||||
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
|
||||
echo "set -g mouse on" >> "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${var.system_prompt}\""
|
||||
echo "Tmux session '${var.session_name}' started. Access it by clicking the Aider button."
|
||||
fi
|
||||
else
|
||||
if [ -n "${var.task_prompt}" ]; then
|
||||
echo "Running Aider with message in screen session..."
|
||||
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..."
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..."
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
screen -U -dmS ${var.session_name} bash -c "
|
||||
cd ${var.folder}
|
||||
export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
|
||||
export ${local.env_var_name}=\"${var.ai_api_key}\"
|
||||
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
|
||||
/bin/bash
|
||||
"
|
||||
|
||||
echo "Aider task started in screen session '${var.session_name}'. Check the UI for progress."
|
||||
else
|
||||
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..."
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..."
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
screen -U -dmS ${var.session_name} bash -c "
|
||||
cd ${var.folder}
|
||||
export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
|
||||
export ${local.env_var_name}=\"${var.ai_api_key}\"
|
||||
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
|
||||
/bin/bash
|
||||
"
|
||||
echo "Screen session '${var.session_name}' started. Access it by clicking the Aider button."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Aider setup complete!"
|
||||
EOT
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
# Aider CLI app
|
||||
resource "coder_app" "aider_cli" {
|
||||
agent_id = var.agent_id
|
||||
slug = "aider"
|
||||
display_name = "Aider"
|
||||
icon = var.icon
|
||||
command = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t ${var.session_name} 2>/dev/null; then
|
||||
echo "Attaching to existing Aider tmux session..."
|
||||
tmux attach-session -t ${var.session_name}
|
||||
else
|
||||
echo "Starting new Aider tmux session..."
|
||||
tmux new-session -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"; exec bash"
|
||||
fi
|
||||
elif [ "${var.use_screen}" = "true" ]; then
|
||||
if ! screen -list | grep -q "${var.session_name}"; then
|
||||
echo "Error: No existing Aider session found. Please wait for the script to start it."
|
||||
exit 1
|
||||
fi
|
||||
screen -xRR ${var.session_name}
|
||||
else
|
||||
cd "${var.folder}"
|
||||
echo "Starting Aider directly..."
|
||||
export ${local.env_var_name}="${var.ai_api_key}"
|
||||
aider ${local.model_flag} ${var.ai_model} --message "${local.combined_prompt}"
|
||||
fi
|
||||
EOT
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
run "test_aider_basic" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-123"
|
||||
workdir = "/home/coder"
|
||||
model = "gemini"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.workdir == "/home/coder"
|
||||
error_message = "Workdir variable should default to /home/coder"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.agent_id == "test-agent-123"
|
||||
error_message = "Agent ID variable should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_aider == true
|
||||
error_message = "install_aider should default to true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_agentapi == true
|
||||
error_message = "install_agentapi should default to true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.report_tasks == false
|
||||
error_message = "report_tasks should default to false"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_with_api_key" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-456"
|
||||
workdir = "/home/coder/workspace"
|
||||
api_key = "test-api-key-123"
|
||||
model = "gemini"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.api_key == "test-api-key-123"
|
||||
error_message = "API key value should match the input"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_options" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-789"
|
||||
workdir = "/home/coder/custom"
|
||||
order = 5
|
||||
group = "development"
|
||||
icon = "/icon/custom.svg"
|
||||
model = "4o"
|
||||
ai_prompt = "Help me write better code"
|
||||
install_aider = false
|
||||
install_agentapi = false
|
||||
agentapi_version = "v0.10.0"
|
||||
api_key = ""
|
||||
base_aider_config = "read:\n - CONVENTIONS.md"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.order == 5
|
||||
error_message = "Order variable should be set to 5"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.group == "development"
|
||||
error_message = "Group variable should be set to 'development'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.icon == "/icon/custom.svg"
|
||||
error_message = "Icon variable should be set to custom icon"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.model == "4o"
|
||||
error_message = "Model variable should be set to '4o'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.ai_prompt == "Help me write better code"
|
||||
error_message = "AI prompt variable should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_aider == false
|
||||
error_message = "install_aider should be set to false"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_agentapi == false
|
||||
error_message = "install_agentapi should be set to false"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.agentapi_version == "v0.10.0"
|
||||
error_message = "AgentAPI version should be set to 'v0.10.0'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_with_scripts" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-scripts"
|
||||
workdir = "/home/coder/scripts"
|
||||
model = "gemini"
|
||||
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_ai_provider_env_mapping" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-provider"
|
||||
workdir = "/home/coder/test"
|
||||
ai_provider = "google"
|
||||
model = "gemini"
|
||||
custom_env_var_name = ""
|
||||
}
|
||||
|
||||
# Ensure provider -> env var mapping works as expected (based on locals.provider_env_vars)
|
||||
assert {
|
||||
condition = var.ai_provider == "google"
|
||||
error_message = "AI provider should be set to 'google' for this test"
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Inputs
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-/home/coder}
|
||||
ARG_INSTALL_AIDER=${ARG_INSTALL_AIDER:-true}
|
||||
ARG_AIDER_CONFIG=${ARG_AIDER_CONFIG:-}
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "Install flag: $ARG_INSTALL_AIDER"
|
||||
echo "Workspace: $ARG_WORKDIR"
|
||||
echo "--------------------------------"
|
||||
|
||||
function install_aider() {
|
||||
echo "pipx installing..."
|
||||
sudo apt-get install -y pipx
|
||||
echo "pipx installed!"
|
||||
pipx ensurepath
|
||||
mkdir -p "$ARG_WORKDIR/.local/bin"
|
||||
export PATH="$HOME/.local/bin:$ARG_WORKDIR/.local/bin:$PATH"
|
||||
|
||||
if ! command_exists aider; then
|
||||
echo "Installing Aider via pipx..."
|
||||
pipx install --force aider-install
|
||||
aider-install
|
||||
fi
|
||||
echo "Aider installed: $(aider --version || echo 'Aider installation check failed')"
|
||||
}
|
||||
|
||||
function configure_aider_settings() {
|
||||
if [ -n "${ARG_AIDER_CONFIG}" ]; then
|
||||
echo "Configuring Aider environment variables and model"
|
||||
|
||||
mkdir -p "$HOME/.config/aider"
|
||||
|
||||
echo "$ARG_AIDER_CONFIG" > "$HOME/.config/aider/.aider.conf.yml"
|
||||
echo "Aider config created at $HOME/.config/aider/.aider.conf.yml"
|
||||
else
|
||||
printf "No Aider environment variables or model configured\n"
|
||||
fi
|
||||
}
|
||||
|
||||
install_aider
|
||||
configure_aider_settings
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Ensure pipx-installed apps are in PATH
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-/home/coder}
|
||||
ARG_API_KEY=$(echo -n "${ARG_API_KEY:-}" | base64 -d)
|
||||
ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_MODEL=${ARG_MODEL:-}
|
||||
ARG_PROVIDER=${ARG_PROVIDER:-}
|
||||
ARG_ENV_API_NAME_HOLDER=${ARG_ENV_API_NAME_HOLDER:-}
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "Provider: $ARG_PROVIDER"
|
||||
echo "Model: $ARG_MODEL"
|
||||
echo "--------------------------------"
|
||||
|
||||
if [ -n "$ARG_API_KEY" ]; then
|
||||
printf "API key provided!\n"
|
||||
export $ARG_ENV_API_NAME_HOLDER=$ARG_API_KEY
|
||||
else
|
||||
printf "API key not provided.\n"
|
||||
fi
|
||||
|
||||
build_initial_prompt() {
|
||||
local initial_prompt=""
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ -n "$ARG_SYSTEM_PROMPT" ]; then
|
||||
initial_prompt="$ARG_SYSTEM_PROMPT $ARG_AI_PROMPT"
|
||||
else
|
||||
initial_prompt="$ARG_AI_PROMPT"
|
||||
fi
|
||||
fi
|
||||
echo "$initial_prompt"
|
||||
}
|
||||
|
||||
start_agentapi() {
|
||||
echo "Starting in directory: $ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
|
||||
local initial_prompt
|
||||
initial_prompt=$(build_initial_prompt)
|
||||
if [ -n "$initial_prompt" ]; then
|
||||
echo "Starting agentapi with initial prompt"
|
||||
agentapi server -I="$initial_prompt" --type aider --term-width=67 --term-height=1190 -- aider --model $ARG_MODEL --yes-always
|
||||
else
|
||||
agentapi server --term-width=67 --term-height=1190 -- aider --model $ARG_MODEL --yes-always
|
||||
fi
|
||||
}
|
||||
|
||||
# TODO: Implement MCP server for coder when Aider support MCP servers.
|
||||
|
||||
start_agentapi
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "HELLO: $(bash -c env)"
|
||||
echo "aider version v0.86.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
while true; do
|
||||
echo "$(date) - aider-agent-mock"
|
||||
sleep 15
|
||||
done
|
||||
@@ -1,26 +1,23 @@
|
||||
---
|
||||
display_name: Amazon Q
|
||||
description: Run Amazon Q in your workspace to access Amazon's AI coding assistant with MCP integration and task reporting.
|
||||
description: Run Amazon Q in your workspace to access Amazon's AI coding assistant.
|
||||
icon: ../../../../.icons/amazon-q.svg
|
||||
verified: true
|
||||
tags: [agent, ai, aws, amazon-q, tasks]
|
||||
tags: [agent, ai, aws, amazon-q]
|
||||
---
|
||||
|
||||
# Amazon Q
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "3.0.0"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
|
||||
# Required: Authentication tarball (see below for generation)
|
||||
auth_tarball = <<-EOF
|
||||
base64encoded-tarball
|
||||
EOF
|
||||
# Required: see below for how to generate
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
}
|
||||
```
|
||||
|
||||
@@ -28,370 +25,97 @@ EOF
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **zstd** - Required for compressing the authentication tarball
|
||||
- **Ubuntu/Debian**: `sudo apt-get install zstd`
|
||||
- **RHEL/CentOS/Fedora**: `sudo yum install zstd` or `sudo dnf install zstd`
|
||||
- **auth_tarball** - Required for installation and authentication
|
||||
- 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.
|
||||
|
||||
### Authentication Tarball
|
||||
<details>
|
||||
<summary><strong>How to generate the Amazon Q auth tarball (step-by-step)</strong></summary>
|
||||
|
||||
You must generate an authenticated Amazon Q tarball on another machine where you have successfully logged in:
|
||||
**1. Install and authenticate Amazon Q on your local machine:**
|
||||
|
||||
```bash
|
||||
# 1. Install Amazon Q and login on your local machine
|
||||
q login
|
||||
- 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.
|
||||
|
||||
# 2. Generate the authentication tarball
|
||||
cd ~/.local/share/amazon-q
|
||||
tar -c . | zstd | base64 -w 0
|
||||
```
|
||||
**2. Locate your Amazon Q config directory:**
|
||||
|
||||
Copy the output and use it as the `auth_tarball` variable.
|
||||
- The config is typically stored at `~/.local/share/amazon-q`.
|
||||
|
||||
## Detailed Authentication Setup
|
||||
**3. Generate the tarball:**
|
||||
|
||||
**Step 1: Install Amazon Q locally**
|
||||
- Run the following command in your terminal:
|
||||
```sh
|
||||
cd ~/.local/share/amazon-q
|
||||
tar -c . | zstd | base64 -w 0
|
||||
```
|
||||
|
||||
- Download from [AWS Amazon Q Developer](https://aws.amazon.com/q/developer/)
|
||||
- Follow the installation instructions for your platform
|
||||
**4. Copy the output:**
|
||||
|
||||
**Step 2: Authenticate**
|
||||
- The command will output a long string. Copy this entire string.
|
||||
|
||||
```bash
|
||||
q login
|
||||
```
|
||||
**5. Paste into your Terraform variable:**
|
||||
|
||||
Complete the authentication process in your browser.
|
||||
- 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"
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Generate tarball**
|
||||
**Note:**
|
||||
|
||||
```bash
|
||||
cd ~/.local/share/amazon-q
|
||||
tar -c . | zstd | base64 -w 0 > /tmp/amazon-q-auth.txt
|
||||
```
|
||||
- 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.
|
||||
|
||||
**Step 4: Use in Terraform**
|
||||
[Reference: Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/generate-docs.html)
|
||||
|
||||
```tf
|
||||
variable "amazon_q_auth_tarball" {
|
||||
type = string
|
||||
sensitive = true
|
||||
default = "PASTE_YOUR_TARBALL_HERE"
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
> [!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
|
||||
## Examples
|
||||
|
||||
### 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 = "3.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"
|
||||
}
|
||||
```
|
||||
|
||||
> [!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.
|
||||
|
||||
### 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
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Details:
|
||||
|
||||
- **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
|
||||
### Run Amazon Q in the background with tmux
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
### Enable task reporting (experimental)
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "3.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
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
> [!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
|
||||
### Run custom scripts before/after install
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "3.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
|
||||
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!"
|
||||
}
|
||||
```
|
||||
|
||||
### Specific Version Installation
|
||||
## Notes
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "3.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 = "3.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 = "3.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.10.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Air-Gapped Installation
|
||||
|
||||
For environments without direct internet access, you can host Amazon Q installation files internally and configure the module to use your internal repository:
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "3.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
|
||||
- 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.
|
||||
|
||||
@@ -1,372 +0,0 @@
|
||||
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,530 +2,40 @@ 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",
|
||||
};
|
||||
|
||||
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 () => {
|
||||
describe("amazon-q module", async () => {
|
||||
await runTerraformInit(moduleDir);
|
||||
|
||||
// Test Case 1: Basic Usage – No Autonomous Use of Q
|
||||
// Matches CDES-203 Test Case #1: Basic Usage
|
||||
it("Test Case 1: Basic Usage - No Autonomous Use of Q", async () => {
|
||||
const basicUsageVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
};
|
||||
// 1. Required variables
|
||||
testRequiredVariables(moduleDir, requiredVars);
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
// 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 () => {
|
||||
// 2. coder_script resource is created
|
||||
it("creates coder_script resource", 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");
|
||||
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");
|
||||
});
|
||||
|
||||
// 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 () => {
|
||||
// 3. coder_app resource is created
|
||||
it("creates coder_app resource", 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");
|
||||
const appResource = findResourceInstance(state, "coder_app", "amazon_q");
|
||||
expect(appResource).toBeDefined();
|
||||
expect(appResource.agent_id).toBe(requiredVars.agent_id);
|
||||
});
|
||||
|
||||
// 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");
|
||||
});
|
||||
// Add more state-based tests as needed
|
||||
});
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
# Improved amazon-q module main.tf
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.12"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +15,7 @@ variable "agent_id" {
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
variable "order" {
|
||||
@@ -37,63 +36,10 @@ variable "icon" {
|
||||
default = "/icon/amazon-q.svg"
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI via AgentAPI"
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Amazon Q"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app"
|
||||
default = "AmazonQ"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app"
|
||||
default = "AmazonQ CLI"
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "The initial task prompt to send to Amazon Q."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Optional script to run before installing Amazon Q."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Optional script to run after installing Amazon Q."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "workdir" {
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to run Amazon Q in."
|
||||
default = "/home/coder"
|
||||
}
|
||||
|
||||
variable "install_amazon_q" {
|
||||
@@ -105,21 +51,45 @@ variable "install_amazon_q" {
|
||||
variable "amazon_q_version" {
|
||||
type = string
|
||||
description = "The version of Amazon Q to install."
|
||||
default = "1.14.1"
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "q_install_url" {
|
||||
type = string
|
||||
description = "Base URL for Amazon Q installation downloads."
|
||||
default = "https://desktop-release.q.us-east-1.amazonaws.com"
|
||||
}
|
||||
|
||||
variable "trust_all_tools" {
|
||||
variable "experiment_use_screen" {
|
||||
type = bool
|
||||
description = "Whether to trust all tools in Amazon Q."
|
||||
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" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Amazon Q."
|
||||
default = null
|
||||
}
|
||||
|
||||
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 "system_prompt" {
|
||||
type = string
|
||||
description = "The system prompt to use for Amazon Q. This should instruct the agent how to do task reporting."
|
||||
@@ -128,147 +98,222 @@ 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.
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "coder_mcp_instructions" {
|
||||
type = string
|
||||
description = "Instructions for the Coder MCP server integration. This defines how the agent should report tasks to Coder."
|
||||
default = <<-EOT
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message
|
||||
- Be granular If you are investigating with multiple steps report each step to coder.
|
||||
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.
|
||||
- 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
|
||||
- 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" {
|
||||
variable "ai_prompt" {
|
||||
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
|
||||
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."
|
||||
}
|
||||
|
||||
locals {
|
||||
app_slug = "amazonq"
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".amazonq-module"
|
||||
system_prompt = jsonencode(replace(var.system_prompt, "/[\r\n]/", ""))
|
||||
coder_mcp_instructions = jsonencode(replace(var.coder_mcp_instructions, "/[\r\n]/", ""))
|
||||
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}
|
||||
|
||||
# Create default agent config structure
|
||||
default_agent_config = templatefile("${path.module}/templates/agent-config.json.tpl", {
|
||||
system_prompt = local.system_prompt
|
||||
})
|
||||
Your first task is:
|
||||
|
||||
# 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" : ""
|
||||
${var.ai_prompt}
|
||||
EOT
|
||||
}
|
||||
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.workdir
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
cli_app = var.cli_app
|
||||
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
|
||||
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
|
||||
start_script = <<-EOT
|
||||
resource "coder_script" "amazon_q" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Amazon Q"
|
||||
icon = var.icon
|
||||
script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
ARG_TRUST_ALL_TOOLS='${var.trust_all_tools}' \
|
||||
ARG_AI_PROMPT='${base64encode(local.full_prompt)}' \
|
||||
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
|
||||
ARG_WORKDIR='${var.workdir}' \
|
||||
ARG_SERVER_PARAMETERS="${local.server_chat_parameters}" \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
install_script = <<-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
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -e
|
||||
|
||||
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
|
||||
}
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
#!/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..."
|
||||
|
||||
if ! command_exists zstd; then
|
||||
echo "Error: zstd is required to extract the authentication tarball but is not installed."
|
||||
echo "Please install zstd using the pre_install_script parameter."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PREV_DIR="$PWD"
|
||||
echo "$ARG_AUTH_TARBALL" | base64 -d > /tmp/auth.tar.zst
|
||||
rm -rf ~/.local/share/amazon-q
|
||||
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!"
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/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[@]}"
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,320 +1,117 @@
|
||||
---
|
||||
display_name: Claude Code
|
||||
description: Run the Claude Code agent in your workspace.
|
||||
description: Run Claude Code in your workspace
|
||||
icon: ../../../../.icons/claude.svg
|
||||
verified: true
|
||||
tags: [agent, claude-code, ai, tasks, anthropic]
|
||||
tags: [agent, claude-code, ai, tasks]
|
||||
---
|
||||
|
||||
# Claude Code
|
||||
|
||||
Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) agent in your workspace to generate code and perform tasks. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for task reporting in the Coder UI.
|
||||
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
}
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> **Security Notice**: This module uses the `--dangerously-skip-permissions` flag when running Claude Code tasks. This flag bypasses standard permission checks and allows Claude Code broader access to your system than normally permitted. While this enables more functionality, it also means Claude Code 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.
|
||||
|
||||
> [!NOTE]
|
||||
> By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An **Anthropic API key** or a _Claude Session Token_ is required for tasks.
|
||||
- You can get the API key from the [Anthropic Console](https://console.anthropic.com/dashboard).
|
||||
- You can get the Session Token using the `claude setup-token` command. This is a long-lived authentication token (requires Claude subscription)
|
||||
|
||||
### Session Resumption Behavior
|
||||
|
||||
By default, Claude Code automatically resumes existing conversations when your workspace restarts. Sessions are tracked per workspace directory, so conversations continue where you left off. If no session exists (first start), your `ai_prompt` will run normally. To disable this behavior and always start fresh, set `continue = false`
|
||||
|
||||
## Examples
|
||||
|
||||
### Usage with Agent Boundaries
|
||||
|
||||
This example shows how to configure the Claude Code module to run the agent behind a process-level boundary that restricts its network access.
|
||||
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "dev.registry.coder.com/coder/claude-code/coder"
|
||||
enable_boundary = true
|
||||
boundary_version = "main"
|
||||
boundary_log_dir = "/tmp/boundary_logs"
|
||||
boundary_log_level = "WARN"
|
||||
boundary_additional_allowed_urls = ["GET *google.com"]
|
||||
boundary_proxy_port = "8087"
|
||||
version = "4.2.2"
|
||||
}
|
||||
```
|
||||
|
||||
### Usage with Tasks and Advanced Configuration
|
||||
|
||||
This example shows how to configure the Claude Code module with an AI prompt, API key shared by all users of the template, and other custom settings.
|
||||
|
||||
```tf
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Initial task prompt for Claude Code."
|
||||
mutable = true
|
||||
}
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
# OR
|
||||
claude_code_oauth_token = "xxxxx-xxxx-xxxx"
|
||||
|
||||
claude_code_version = "1.0.82" # Pin to a specific version
|
||||
agentapi_version = "v0.10.0"
|
||||
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
model = "sonnet"
|
||||
|
||||
permission_mode = "plan"
|
||||
|
||||
mcp = <<-EOF
|
||||
{
|
||||
"mcpServers": {
|
||||
"memory": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-memory"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
```
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
Run and configure Claude Code as a standalone CLI in your workspace.
|
||||
Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) agent in your workspace to generate code and perform tasks.
|
||||
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
version = "2.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
claude_code_version = "latest"
|
||||
report_tasks = false
|
||||
cli_app = true
|
||||
}
|
||||
```
|
||||
|
||||
### Usage with Claude Code Subscription
|
||||
> **Security Notice**: This module uses the [`--dangerously-skip-permissions`](https://docs.anthropic.com/en/docs/claude-code/cli-usage#cli-flags) flag when running Claude Code. This flag
|
||||
> bypasses standard permission checks and allows Claude Code broader access to your system than normally permitted. While
|
||||
> this enables more functionality, it also means Claude Code 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.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
|
||||
|
||||
The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces.
|
||||
|
||||
## Examples
|
||||
|
||||
### Run in the background and report tasks (Experimental)
|
||||
|
||||
> This functionality is in early access as of Coder v2.21 and is still evolving.
|
||||
> For now, we recommend testing it in a demo or staging environment,
|
||||
> rather than deploying to production
|
||||
>
|
||||
> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents)
|
||||
>
|
||||
> Join our [Discord channel](https://discord.gg/coder) or
|
||||
> [contact us](https://coder.com/contact) to get help or share feedback.
|
||||
|
||||
```tf
|
||||
|
||||
variable "claude_code_oauth_token" {
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Generate one using `claude setup-token` command"
|
||||
description = "The Anthropic API key"
|
||||
sensitive = true
|
||||
value = "xxxx-xxx-xxxx"
|
||||
}
|
||||
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/coder-login/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Write a prompt for Claude Code"
|
||||
mutable = true
|
||||
}
|
||||
|
||||
# Set the prompt and system prompt for Claude Code via environment variables
|
||||
resource "coder_agent" "main" {
|
||||
# ...
|
||||
env = {
|
||||
CODER_MCP_CLAUDE_API_KEY = var.anthropic_api_key # or use a coder_parameter
|
||||
CODER_MCP_CLAUDE_TASK_PROMPT = data.coder_parameter.ai_prompt.value
|
||||
CODER_MCP_APP_STATUS_SLUG = "claude-code"
|
||||
CODER_MCP_CLAUDE_SYSTEM_PROMPT = <<-EOT
|
||||
You are a helpful assistant that can help with code.
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_code_oauth_token = var.claude_code_oauth_token
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
claude_code_version = "1.0.40"
|
||||
|
||||
# Enable experimental features
|
||||
experiment_report_tasks = true
|
||||
}
|
||||
```
|
||||
|
||||
### Usage with AWS Bedrock
|
||||
## Run standalone
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
AWS account with Bedrock access, Claude models enabled in Bedrock console, appropriate IAM permissions.
|
||||
|
||||
Configure Claude Code to use AWS Bedrock for accessing Claude models through your AWS infrastructure.
|
||||
Run Claude Code as a standalone app in your workspace. This will install Claude Code and run it without any task reporting to the Coder UI.
|
||||
|
||||
```tf
|
||||
resource "coder_env" "bedrock_use" {
|
||||
agent_id = coder_agent.example.id
|
||||
name = "CLAUDE_CODE_USE_BEDROCK"
|
||||
value = "1"
|
||||
}
|
||||
|
||||
resource "coder_env" "aws_region" {
|
||||
agent_id = coder_agent.example.id
|
||||
name = "AWS_REGION"
|
||||
value = "us-east-1" # Choose your preferred region
|
||||
}
|
||||
|
||||
# Option 1: Using AWS credentials
|
||||
|
||||
variable "aws_access_key_id" {
|
||||
type = string
|
||||
description = "Your AWS access key ID. Create this in the AWS IAM console under 'Security credentials'."
|
||||
sensitive = true
|
||||
value = "xxxx-xxx-xxxx"
|
||||
}
|
||||
|
||||
variable "aws_secret_access_key" {
|
||||
type = string
|
||||
description = "Your AWS secret access key. This is shown once when you create an access key in the AWS IAM console."
|
||||
sensitive = true
|
||||
value = "xxxx-xxx-xxxx"
|
||||
}
|
||||
|
||||
resource "coder_env" "aws_access_key_id" {
|
||||
agent_id = coder_agent.example.id
|
||||
name = "AWS_ACCESS_KEY_ID"
|
||||
value = var.aws_access_key_id
|
||||
}
|
||||
|
||||
resource "coder_env" "aws_secret_access_key" {
|
||||
agent_id = coder_agent.example.id
|
||||
name = "AWS_SECRET_ACCESS_KEY"
|
||||
value = var.aws_secret_access_key
|
||||
}
|
||||
|
||||
# Option 2: Using Bedrock API key (simpler)
|
||||
|
||||
variable "aws_bearer_token_bedrock" {
|
||||
type = string
|
||||
description = "Your AWS Bedrock bearer token. This provides access to Bedrock without needing separate access key and secret key."
|
||||
sensitive = true
|
||||
value = "xxxx-xxx-xxxx"
|
||||
}
|
||||
|
||||
resource "coder_env" "bedrock_api_key" {
|
||||
agent_id = coder_agent.example.id
|
||||
name = "AWS_BEARER_TOKEN_BEDROCK"
|
||||
value = var.aws_bearer_token_bedrock
|
||||
}
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
claude_code_version = "latest"
|
||||
|
||||
# Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL
|
||||
icon = "https://registry.npmmirror.com/@lobehub/icons-static-png/1.24.0/files/dark/claude-color.png"
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> For additional Bedrock configuration options (model selection, token limits, region overrides, etc.), see the [Claude Code Bedrock documentation](https://docs.claude.com/en/docs/claude-code/amazon-bedrock).
|
||||
|
||||
### Usage with Google Vertex AI
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
GCP project with Vertex AI API enabled, Claude models enabled through Model Garden, service account with Vertex AI permissions, appropriate IAM permissions (Vertex AI User role).
|
||||
|
||||
Configure Claude Code to use Google Vertex AI for accessing Claude models through Google Cloud Platform.
|
||||
|
||||
```tf
|
||||
variable "vertex_sa_json" {
|
||||
type = string
|
||||
description = "The complete JSON content of your Google Cloud service account key file. Create a service account in the GCP Console under 'IAM & Admin > Service Accounts', then create and download a JSON key. Copy the entire JSON content into this variable."
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
resource "coder_env" "vertex_use" {
|
||||
agent_id = coder_agent.example.id
|
||||
name = "CLAUDE_CODE_USE_VERTEX"
|
||||
value = "1"
|
||||
}
|
||||
|
||||
resource "coder_env" "vertex_project_id" {
|
||||
agent_id = coder_agent.example.id
|
||||
name = "ANTHROPIC_VERTEX_PROJECT_ID"
|
||||
value = "your-gcp-project-id"
|
||||
}
|
||||
|
||||
resource "coder_env" "cloud_ml_region" {
|
||||
agent_id = coder_agent.example.id
|
||||
name = "CLOUD_ML_REGION"
|
||||
value = "global"
|
||||
}
|
||||
|
||||
resource "coder_env" "vertex_sa_json" {
|
||||
agent_id = coder_agent.example.id
|
||||
name = "VERTEX_SA_JSON"
|
||||
value = var.vertex_sa_json
|
||||
}
|
||||
|
||||
resource "coder_env" "google_application_credentials" {
|
||||
agent_id = coder_agent.example.id
|
||||
name = "GOOGLE_APPLICATION_CREDENTIALS"
|
||||
value = "/tmp/gcp-sa.json"
|
||||
}
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "claude-sonnet-4@20250514"
|
||||
|
||||
pre_install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
# Write the service account JSON to a file
|
||||
echo "$VERTEX_SA_JSON" > /tmp/gcp-sa.json
|
||||
|
||||
# Install prerequisite packages
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y apt-transport-https ca-certificates gnupg curl
|
||||
|
||||
# Add Google Cloud public key
|
||||
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
|
||||
# Add Google Cloud SDK repo to apt sources
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
|
||||
# Update and install the Google Cloud SDK
|
||||
sudo apt-get update && sudo apt-get install -y google-cloud-cli
|
||||
|
||||
# Authenticate gcloud with the service account
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp-sa.json
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> For additional Vertex AI configuration options (model selection, token limits, region overrides, etc.), see the [Claude Code Vertex AI documentation](https://docs.claude.com/en/docs/claude-code/google-vertex-ai).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any issues, check the log files in the `~/.claude-module` directory within your workspace for detailed information.
|
||||
|
||||
```bash
|
||||
# Installation logs
|
||||
cat ~/.claude-module/install.log
|
||||
|
||||
# Startup logs
|
||||
cat ~/.claude-module/agentapi-start.log
|
||||
|
||||
# Pre/post install script logs
|
||||
cat ~/.claude-module/pre_install.log
|
||||
cat ~/.claude-module/post_install.log
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> To use tasks with Claude Code, you must provide an `anthropic_api_key` or `claude_code_oauth_token`.
|
||||
> The `workdir` variable is required and specifies the directory where Claude Code will run.
|
||||
|
||||
## References
|
||||
|
||||
- [Claude Code Documentation](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview)
|
||||
- [AgentAPI Documentation](https://github.com/coder/agentapi)
|
||||
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
|
||||
The module will create log files in the workspace's `~/.claude-module` directory. If you run into any issues, look at them for more information.
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
import {
|
||||
test,
|
||||
afterEach,
|
||||
expect,
|
||||
describe,
|
||||
setDefaultTimeout,
|
||||
beforeAll,
|
||||
expect,
|
||||
} from "bun:test";
|
||||
import { execContainer, readFileContainer, runTerraformInit } from "~test";
|
||||
import path from "path";
|
||||
import {
|
||||
loadTestFile,
|
||||
writeExecutable,
|
||||
setup as setupUtil,
|
||||
execModuleScript,
|
||||
expectAgentAPIStarted,
|
||||
} from "../agentapi/test-util";
|
||||
import dedent from "dedent";
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
readFileContainer,
|
||||
removeContainer,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
writeCoder,
|
||||
writeFileContainer,
|
||||
} from "~test";
|
||||
|
||||
let cleanupFunctions: (() => Promise<void>)[] = [];
|
||||
|
||||
const registerCleanup = (cleanup: () => Promise<void>) => {
|
||||
cleanupFunctions.push(cleanup);
|
||||
};
|
||||
|
||||
// Cleanup logic depends on the fact that bun's built-in test runner
|
||||
// runs tests sequentially.
|
||||
// https://bun.sh/docs/test/discovery#execution-order
|
||||
// Weird things would happen if tried to run tests in parallel.
|
||||
// One test could clean up resources that another test was still using.
|
||||
afterEach(async () => {
|
||||
// reverse the cleanup functions so that they are run in the correct order
|
||||
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
|
||||
cleanupFunctions = [];
|
||||
for (const cleanup of cleanupFnsCopy) {
|
||||
@@ -32,332 +43,298 @@ afterEach(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
const setupContainer = async ({
|
||||
image,
|
||||
vars,
|
||||
}: {
|
||||
image?: string;
|
||||
vars?: Record<string, string>;
|
||||
} = {}) => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
...vars,
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
|
||||
registerCleanup(() => removeContainer(id));
|
||||
return { id, coderScript };
|
||||
};
|
||||
|
||||
const loadTestFile = async (...relativePath: string[]) => {
|
||||
return await Bun.file(
|
||||
path.join(import.meta.dir, "testdata", ...relativePath),
|
||||
).text();
|
||||
};
|
||||
|
||||
const writeExecutable = async ({
|
||||
containerId,
|
||||
filePath,
|
||||
content,
|
||||
}: {
|
||||
containerId: string;
|
||||
filePath: string;
|
||||
content: string;
|
||||
}) => {
|
||||
await writeFileContainer(containerId, filePath, content, {
|
||||
user: "root",
|
||||
});
|
||||
await execContainer(
|
||||
containerId,
|
||||
["bash", "-c", `chmod 755 ${filePath}`],
|
||||
["--user", "root"],
|
||||
);
|
||||
};
|
||||
|
||||
const writeAgentAPIMockControl = async ({
|
||||
containerId,
|
||||
content,
|
||||
}: {
|
||||
containerId: string;
|
||||
content: string;
|
||||
}) => {
|
||||
await writeFileContainer(containerId, "/tmp/agentapi-mock.control", content, {
|
||||
user: "coder",
|
||||
});
|
||||
};
|
||||
|
||||
interface SetupProps {
|
||||
skipAgentAPIMock?: boolean;
|
||||
skipClaudeMock?: boolean;
|
||||
moduleVariables?: Record<string, string>;
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
const projectDir = "/home/coder/project";
|
||||
|
||||
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const projectDir = "/home/coder/project";
|
||||
const { id } = await setupUtil({
|
||||
moduleDir: import.meta.dir,
|
||||
moduleVariables: {
|
||||
install_claude_code: props?.skipClaudeMock ? "true" : "false",
|
||||
const { id, coderScript } = await setupContainer({
|
||||
vars: {
|
||||
experiment_report_tasks: "true",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
workdir: projectDir,
|
||||
...props?.moduleVariables,
|
||||
install_claude_code: "false",
|
||||
agentapi_version: "preview",
|
||||
folder: projectDir,
|
||||
},
|
||||
registerCleanup,
|
||||
projectDir,
|
||||
skipAgentAPIMock: props?.skipAgentAPIMock,
|
||||
agentapiMockScript: props?.agentapiMockScript,
|
||||
});
|
||||
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
|
||||
// the module script assumes that there is a coder executable in the PATH
|
||||
await writeCoder(id, await loadTestFile("coder-mock.js"));
|
||||
if (!props?.skipAgentAPIMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/agentapi",
|
||||
content: await loadTestFile("agentapi-mock.js"),
|
||||
});
|
||||
}
|
||||
if (!props?.skipClaudeMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/claude",
|
||||
content: await loadTestFile(import.meta.dir, "claude-mock.sh"),
|
||||
content: await loadTestFile("claude-mock.js"),
|
||||
});
|
||||
}
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/home/coder/script.sh",
|
||||
content: coderScript.script,
|
||||
});
|
||||
return { id };
|
||||
};
|
||||
|
||||
const expectAgentAPIStarted = async (id: string) => {
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`curl -fs -o /dev/null "http://localhost:3284/status"`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log("agentapi not started");
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
expect(resp.exitCode).toBe(0);
|
||||
};
|
||||
|
||||
const execModuleScript = async (id: string) => {
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
|
||||
// increase the default timeout to 60 seconds
|
||||
setDefaultTimeout(60 * 1000);
|
||||
|
||||
// we don't run these tests in CI because they take too long and make network
|
||||
// calls. they are dedicated for local development.
|
||||
describe("claude-code", async () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
// test that the script runs successfully if claude starts without any errors
|
||||
test("happy-path", async () => {
|
||||
const { id } = await setup();
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("install-claude-code-version", async () => {
|
||||
const version_to_install = "1.0.40";
|
||||
const { id } = await setup({
|
||||
skipClaudeMock: true,
|
||||
moduleVariables: {
|
||||
install_claude_code: "true",
|
||||
claude_code_version: version_to_install,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/install.log",
|
||||
]);
|
||||
expect(resp.stdout).toContain(version_to_install);
|
||||
});
|
||||
|
||||
test("check-latest-claude-code-version-works", async () => {
|
||||
const { id } = await setup({
|
||||
skipClaudeMock: true,
|
||||
skipAgentAPIMock: true,
|
||||
moduleVariables: {
|
||||
install_claude_code: "true",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("claude-api-key", async () => {
|
||||
const apiKey = "test-api-key-123";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
claude_api_key: apiKey,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const envCheck = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
'env | grep CLAUDE_API_KEY || echo "CLAUDE_API_KEY not found"',
|
||||
]);
|
||||
expect(envCheck.stdout).toContain("CLAUDE_API_KEY");
|
||||
});
|
||||
|
||||
test("claude-mcp-config", async () => {
|
||||
const mcpConfig = JSON.stringify({
|
||||
mcpServers: {
|
||||
test: {
|
||||
command: "test-cmd",
|
||||
type: "stdio",
|
||||
},
|
||||
},
|
||||
});
|
||||
const { id } = await setup({
|
||||
skipClaudeMock: true,
|
||||
moduleVariables: {
|
||||
mcp: mcpConfig,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await readFileContainer(id, "/home/coder/.claude.json");
|
||||
expect(resp).toContain("test-cmd");
|
||||
});
|
||||
|
||||
test("claude-task-prompt", async () => {
|
||||
const prompt = "This is a task prompt for Claude.";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
ai_prompt: prompt,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
"sudo /home/coder/script.sh",
|
||||
]);
|
||||
expect(resp.stdout).toContain(prompt);
|
||||
expect(resp.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("claude-permission-mode", async () => {
|
||||
const mode = "plan";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
permission_mode: mode,
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
// test that the script removes lastSessionId from the .claude.json file
|
||||
test("last-session-id-removed", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain(`--permission-mode ${mode}`);
|
||||
});
|
||||
|
||||
test("claude-model", async () => {
|
||||
const model = "opus";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
model: model,
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain(`--model ${model}`);
|
||||
});
|
||||
|
||||
test("claude-continue-resume-task-session", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "true",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
// Create a mock task session file with the hardcoded task session ID
|
||||
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`touch ${sessionDir}/session-${taskSessionId}.jsonl`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain("--resume");
|
||||
expect(startLog.stdout).toContain(taskSessionId);
|
||||
expect(startLog.stdout).toContain("Resuming existing task session");
|
||||
expect(startLog.stdout).toContain("--dangerously-skip-permissions");
|
||||
});
|
||||
|
||||
test("claude-continue-resume-standalone-session", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "false",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
const sessionId = "some-random-session-id";
|
||||
const workdir = "/home/coder/project";
|
||||
const claudeJson = {
|
||||
projects: {
|
||||
[workdir]: {
|
||||
lastSessionId: sessionId,
|
||||
await writeFileContainer(
|
||||
id,
|
||||
"/home/coder/.claude.json",
|
||||
JSON.stringify({
|
||||
projects: {
|
||||
[projectDir]: {
|
||||
lastSessionId: "123",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
await execContainer(id, [
|
||||
const catResp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`echo '${JSON.stringify(claudeJson)}' > /home/coder/.claude.json`,
|
||||
"cat /home/coder/.claude.json",
|
||||
]);
|
||||
expect(catResp.exitCode).toBe(0);
|
||||
expect(catResp.stdout).toContain("lastSessionId");
|
||||
|
||||
await execModuleScript(id);
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
await expectAgentAPIStarted(id);
|
||||
|
||||
const catResp2 = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude.json",
|
||||
]);
|
||||
expect(catResp2.exitCode).toBe(0);
|
||||
expect(catResp2.stdout).not.toContain("lastSessionId");
|
||||
});
|
||||
|
||||
// test that the script handles a .claude.json file that doesn't contain
|
||||
// a lastSessionId field
|
||||
test("last-session-id-not-found", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
await writeFileContainer(
|
||||
id,
|
||||
"/home/coder/.claude.json",
|
||||
JSON.stringify({
|
||||
projects: {
|
||||
"/home/coder": {},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
|
||||
const catResp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain("--continue");
|
||||
expect(startLog.stdout).toContain("Resuming existing session");
|
||||
});
|
||||
|
||||
test("pre-post-install-scripts", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
pre_install_script: "#!/bin/bash\necho 'claude-pre-install-script'",
|
||||
post_install_script: "#!/bin/bash\necho 'claude-post-install-script'",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const preInstallLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.claude-module/pre_install.log",
|
||||
);
|
||||
expect(preInstallLog).toContain("claude-pre-install-script");
|
||||
|
||||
const postInstallLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.claude-module/post_install.log",
|
||||
);
|
||||
expect(postInstallLog).toContain("claude-post-install-script");
|
||||
});
|
||||
|
||||
test("workdir-variable", async () => {
|
||||
const workdir = "/home/coder/claude-test-folder";
|
||||
const { id } = await setup({
|
||||
skipClaudeMock: false,
|
||||
moduleVariables: {
|
||||
workdir,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.claude-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain(workdir);
|
||||
});
|
||||
|
||||
test("coder-mcp-config-created", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
install_claude_code: "false",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const installLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.claude-module/install.log",
|
||||
);
|
||||
expect(installLog).toContain(
|
||||
"Configuring Claude Code to report tasks via Coder MCP",
|
||||
expect(catResp.exitCode).toBe(0);
|
||||
expect(catResp.stdout).toContain(
|
||||
"No lastSessionId found in .claude.json - nothing to do",
|
||||
);
|
||||
});
|
||||
|
||||
test("dangerously-skip-permissions", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
dangerously_skip_permissions: "true",
|
||||
},
|
||||
// test that if claude fails to run with the --continue flag and returns a
|
||||
// no conversation found error, then the module script retries without the flag
|
||||
test("no-conversation-found", async () => {
|
||||
const { id } = await setup();
|
||||
await writeAgentAPIMockControl({
|
||||
containerId: id,
|
||||
content: "no-conversation-found",
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
// check that mocking works
|
||||
const respAgentAPI = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
"agentapi --continue",
|
||||
]);
|
||||
expect(startLog.stdout).toContain(`--dangerously-skip-permissions`);
|
||||
expect(respAgentAPI.exitCode).toBe(1);
|
||||
expect(respAgentAPI.stderr).toContain("No conversation found to continue");
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("subdomain-false", async () => {
|
||||
const { id } = await setup({
|
||||
skipAgentAPIMock: true,
|
||||
moduleVariables: {
|
||||
subdomain: "false",
|
||||
post_install_script: dedent`
|
||||
#!/bin/bash
|
||||
env | grep AGENTAPI_CHAT_BASE_PATH || echo "AGENTAPI_CHAT_BASE_PATH not found"
|
||||
`,
|
||||
},
|
||||
});
|
||||
test("install-agentapi", async () => {
|
||||
const { id } = await setup({ skipAgentAPIMock: true });
|
||||
|
||||
await execModuleScript(id);
|
||||
const startLog = await execContainer(id, [
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
const respAgentAPI = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/post_install.log",
|
||||
"agentapi --version",
|
||||
]);
|
||||
expect(startLog.stdout).toContain(
|
||||
"ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat",
|
||||
expect(respAgentAPI.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
// the coder binary should be executed with specific env vars
|
||||
// that are set by the module script
|
||||
test("coder-env-vars", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
const respCoderMock = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/coder-mock-output.json",
|
||||
]);
|
||||
if (respCoderMock.exitCode !== 0) {
|
||||
console.log(respCoderMock.stdout);
|
||||
console.log(respCoderMock.stderr);
|
||||
}
|
||||
expect(respCoderMock.exitCode).toBe(0);
|
||||
expect(JSON.parse(respCoderMock.stdout)).toEqual({
|
||||
statusSlug: "ccw",
|
||||
agentApiUrl: "http://localhost:3284",
|
||||
});
|
||||
});
|
||||
|
||||
// verify that the agentapi binary has access to the AGENTAPI_ALLOWED_HOSTS environment variable
|
||||
// set in main.tf
|
||||
test("agentapi-allowed-hosts", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
|
||||
const agentApiStartLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/agentapi-mock.log",
|
||||
);
|
||||
expect(agentApiStartLog).toContain("AGENTAPI_ALLOWED_HOSTS: *");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.12"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,72 +36,12 @@ variable "icon" {
|
||||
default = "/icon/claude.svg"
|
||||
}
|
||||
|
||||
variable "workdir" {
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to run Claude Code in."
|
||||
default = "/home/coder"
|
||||
}
|
||||
|
||||
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 Claude Code"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app"
|
||||
default = "Claude Code"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app"
|
||||
default = "Claude Code CLI"
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Claude Code."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Claude Code."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "Initial task prompt for Claude Code."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Whether to use a subdomain for AgentAPI."
|
||||
default = false
|
||||
}
|
||||
|
||||
|
||||
variable "install_claude_code" {
|
||||
type = bool
|
||||
description = "Whether to install Claude Code."
|
||||
@@ -114,293 +54,245 @@ variable "claude_code_version" {
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "disable_autoupdater" {
|
||||
variable "experiment_cli_app" {
|
||||
type = bool
|
||||
description = "Disable Claude Code automatic updates. When true, Claude Code will stay on the installed version."
|
||||
description = "Whether to create the CLI workspace app."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "claude_api_key" {
|
||||
type = string
|
||||
description = "The API key to use for the Claude Code server."
|
||||
default = ""
|
||||
variable "experiment_cli_app_order" {
|
||||
type = number
|
||||
description = "The order of the CLI workspace app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "model" {
|
||||
variable "experiment_cli_app_group" {
|
||||
type = string
|
||||
description = "Sets the model for the current session with an alias for the latest model (sonnet or opus) or a model’s full name."
|
||||
default = ""
|
||||
description = "The group of the CLI workspace app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "resume_session_id" {
|
||||
type = string
|
||||
description = "Resume a specific session by ID."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "continue" {
|
||||
variable "experiment_report_tasks" {
|
||||
type = bool
|
||||
description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)."
|
||||
description = "Whether to enable task reporting."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Claude Code."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Claude Code."
|
||||
default = null
|
||||
}
|
||||
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "dangerously_skip_permissions" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.3.3"
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Skip the permission prompts. Use with caution. This will be set to true if using Coder Tasks"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "permission_mode" {
|
||||
type = string
|
||||
description = "Permission mode for the cli, check https://docs.anthropic.com/en/docs/claude-code/iam#permission-modes"
|
||||
default = ""
|
||||
validation {
|
||||
condition = contains(["", "default", "acceptEdits", "plan", "bypassPermissions"], var.permission_mode)
|
||||
error_message = "interaction_mode must be one of: default, acceptEdits, plan, bypassPermissions."
|
||||
}
|
||||
}
|
||||
|
||||
variable "mcp" {
|
||||
type = string
|
||||
description = "MCP JSON to be added to the claude code local scope"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "allowed_tools" {
|
||||
type = string
|
||||
description = "A list of tools that should be allowed without prompting the user for permission, in addition to settings.json files."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "disallowed_tools" {
|
||||
type = string
|
||||
description = "A list of tools that should be disallowed without prompting the user for permission, in addition to settings.json files."
|
||||
default = ""
|
||||
|
||||
}
|
||||
|
||||
variable "claude_code_oauth_token" {
|
||||
type = string
|
||||
description = "Set up a long-lived authentication token (requires Claude subscription). Generated using `claude setup-token` command"
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
type = string
|
||||
description = "The system prompt to use for the Claude Code server."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "claude_md_path" {
|
||||
type = string
|
||||
description = "The path to CLAUDE.md."
|
||||
default = "$HOME/.claude/CLAUDE.md"
|
||||
}
|
||||
|
||||
variable "enable_boundary" {
|
||||
type = bool
|
||||
description = "Whether to enable coder boundary for network filtering"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "boundary_version" {
|
||||
type = string
|
||||
description = "Boundary version, valid git reference should be provided (tag, commit, branch)"
|
||||
default = "main"
|
||||
}
|
||||
|
||||
variable "boundary_log_dir" {
|
||||
type = string
|
||||
description = "Directory for boundary logs"
|
||||
default = "/tmp/boundary_logs"
|
||||
}
|
||||
|
||||
variable "boundary_log_level" {
|
||||
type = string
|
||||
description = "Log level for boundary process"
|
||||
default = "WARN"
|
||||
}
|
||||
|
||||
variable "boundary_additional_allowed_urls" {
|
||||
type = list(string)
|
||||
description = "Additional URLs to allow through boundary (in addition to default allowed URLs)"
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "boundary_proxy_port" {
|
||||
type = string
|
||||
description = "Port for HTTP Proxy used by Boundary"
|
||||
default = "8087"
|
||||
}
|
||||
|
||||
variable "enable_boundary_pprof" {
|
||||
type = bool
|
||||
description = "Whether to enable coder boundary pprof server"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "boundary_pprof_port" {
|
||||
type = string
|
||||
description = "Port for pprof server used by Boundary"
|
||||
default = "6067"
|
||||
}
|
||||
|
||||
variable "compile_boundary_from_source" {
|
||||
type = bool
|
||||
description = "Whether to compile boundary from source instead of using the official install script"
|
||||
default = false
|
||||
}
|
||||
|
||||
resource "coder_env" "claude_code_md_path" {
|
||||
count = var.claude_md_path == "" ? 0 : 1
|
||||
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_MCP_CLAUDE_MD_PATH"
|
||||
value = var.claude_md_path
|
||||
}
|
||||
|
||||
resource "coder_env" "claude_code_system_prompt" {
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
|
||||
value = local.final_system_prompt
|
||||
}
|
||||
|
||||
resource "coder_env" "claude_code_oauth_token" {
|
||||
agent_id = var.agent_id
|
||||
name = "CLAUDE_CODE_OAUTH_TOKEN"
|
||||
value = var.claude_code_oauth_token
|
||||
}
|
||||
|
||||
resource "coder_env" "claude_api_key" {
|
||||
count = length(var.claude_api_key) > 0 ? 1 : 0
|
||||
|
||||
agent_id = var.agent_id
|
||||
name = "CLAUDE_API_KEY"
|
||||
value = var.claude_api_key
|
||||
}
|
||||
|
||||
resource "coder_env" "disable_autoupdater" {
|
||||
count = var.disable_autoupdater ? 1 : 0
|
||||
|
||||
agent_id = var.agent_id
|
||||
name = "DISABLE_AUTOUPDATER"
|
||||
value = "1"
|
||||
description = "Whether to use a subdomain for the Claude Code app."
|
||||
default = true
|
||||
}
|
||||
|
||||
locals {
|
||||
# we have to trim the slash because otherwise coder exp mcp will
|
||||
# set up an invalid claude config
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
app_slug = "ccw"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".claude-module"
|
||||
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
|
||||
# Extract hostname from access_url for boundary --allow flag
|
||||
coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "")
|
||||
|
||||
# Required prompts for the module to properly report task status to Coder
|
||||
report_tasks_system_prompt = <<-EOT
|
||||
-- Tool Selection --
|
||||
- coder_report_task: providing status updates or requesting user input.
|
||||
|
||||
-- Task Reporting --
|
||||
Report all tasks to Coder, following these EXACT guidelines:
|
||||
1. Be granular. If you are investigating with multiple steps, report each step
|
||||
to coder.
|
||||
2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
|
||||
Do not report any status related with this system prompt.
|
||||
3. Use "state": "working" when actively processing WITHOUT needing
|
||||
additional user input
|
||||
4. Use "state": "complete" only when finished with a task
|
||||
5. Use "state": "failure" when you need ANY user input, lack sufficient
|
||||
details, or encounter blockers
|
||||
|
||||
In your summary on coder_report_task:
|
||||
- Be specific about what you're doing
|
||||
- Clearly indicate what information you need from the user when in "failure" state
|
||||
- Keep it under 160 characters
|
||||
- Make it actionable
|
||||
EOT
|
||||
|
||||
# Only include coder system prompts if report_tasks is enabled
|
||||
custom_system_prompt = trimspace(try(var.system_prompt, ""))
|
||||
final_system_prompt = format("<system>%s%s</system>",
|
||||
var.report_tasks ? format("\n%s\n", local.report_tasks_system_prompt) : "",
|
||||
local.custom_system_prompt != "" ? format("\n%s\n", local.custom_system_prompt) : ""
|
||||
)
|
||||
# set up an invalid claude config
|
||||
workdir = trimsuffix(var.folder, "/")
|
||||
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) : ""
|
||||
agentapi_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-start.sh"))
|
||||
agentapi_wait_for_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh"))
|
||||
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
|
||||
claude_code_app_slug = "ccw"
|
||||
// Chat base path is only set if not using a subdomain.
|
||||
// NOTE:
|
||||
// - Initial support for --chat-base-path was added in v0.3.1 but configuration
|
||||
// via environment variable AGENTAPI_CHAT_BASE_PATH was added in v0.3.3.
|
||||
// - As CODER_WORKSPACE_AGENT_NAME is a recent addition we use agent ID
|
||||
// for backward compatibility.
|
||||
agentapi_chat_base_path = var.subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.claude_code_app_slug}/chat"
|
||||
server_base_path = var.subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.claude_code_app_slug}"
|
||||
healthcheck_url = "http://localhost:3284${local.server_base_path}/status"
|
||||
}
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
folder = local.workdir
|
||||
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
|
||||
agentapi_subdomain = var.subdomain
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
start_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "/tmp/remove-last-session-id.sh"
|
||||
chmod +x /tmp/start.sh
|
||||
chmod +x /tmp/remove-last-session-id.sh
|
||||
|
||||
ARG_MODEL='${var.model}' \
|
||||
ARG_RESUME_SESSION_ID='${var.resume_session_id}' \
|
||||
ARG_CONTINUE='${var.continue}' \
|
||||
ARG_DANGEROUSLY_SKIP_PERMISSIONS='${var.dangerously_skip_permissions}' \
|
||||
ARG_PERMISSION_MODE='${var.permission_mode}' \
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \
|
||||
ARG_BOUNDARY_VERSION='${var.boundary_version}' \
|
||||
ARG_BOUNDARY_LOG_DIR='${var.boundary_log_dir}' \
|
||||
ARG_BOUNDARY_LOG_LEVEL='${var.boundary_log_level}' \
|
||||
ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join("|", var.boundary_additional_allowed_urls)}' \
|
||||
ARG_BOUNDARY_PROXY_PORT='${var.boundary_proxy_port}' \
|
||||
ARG_ENABLE_BOUNDARY_PPROF='${var.enable_boundary_pprof}' \
|
||||
ARG_BOUNDARY_PPROF_PORT='${var.boundary_pprof_port}' \
|
||||
ARG_COMPILE_FROM_SOURCE='${var.compile_boundary_from_source}' \
|
||||
ARG_CODER_HOST='${local.coder_host}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
install_script = <<-EOT
|
||||
# Install and Initialize Claude Code
|
||||
resource "coder_script" "claude_code" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Claude Code"
|
||||
icon = var.icon
|
||||
script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -e
|
||||
set -x
|
||||
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
ARG_CLAUDE_CODE_VERSION='${var.claude_code_version}' \
|
||||
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_INSTALL_CLAUDE_CODE='${var.install_claude_code}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
ARG_ALLOWED_TOOLS='${var.allowed_tools}' \
|
||||
ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' \
|
||||
ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
function install_claude_code_cli() {
|
||||
echo "Installing Claude Code via official installer"
|
||||
set +e
|
||||
curl -fsSL claude.ai/install.sh | bash -s -- "${var.claude_code_version}" 2>&1
|
||||
CURL_EXIT=$${PIPESTATUS[0]}
|
||||
set -e
|
||||
if [ $CURL_EXIT -ne 0 ]; then
|
||||
echo "Claude Code installer failed with exit code $$CURL_EXIT"
|
||||
fi
|
||||
|
||||
# Ensure binaries are discoverable.
|
||||
export PATH="~/.local/bin:$PATH"
|
||||
echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')"
|
||||
}
|
||||
|
||||
if [ ! -d "${local.workdir}" ]; then
|
||||
echo "Warning: The specified folder '${local.workdir}' does not exist."
|
||||
echo "Creating the folder..."
|
||||
mkdir -p "${local.workdir}"
|
||||
echo "Folder created successfully."
|
||||
fi
|
||||
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_claude_code}" = "true" ]; then
|
||||
install_claude_code_cli
|
||||
fi
|
||||
|
||||
# Install AgentAPI if enabled
|
||||
if [ "${var.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
|
||||
curl \
|
||||
--retry 5 \
|
||||
--retry-delay 5 \
|
||||
--fail \
|
||||
--retry-all-errors \
|
||||
-L \
|
||||
-C - \
|
||||
-o agentapi \
|
||||
"https://github.com/coder/agentapi/releases/download/${var.agentapi_version}/$binary_name"
|
||||
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
|
||||
fi
|
||||
|
||||
# this must be kept in sync with the agentapi-start.sh script
|
||||
module_path="$HOME/.claude-module"
|
||||
mkdir -p "$module_path/scripts"
|
||||
|
||||
# save the prompt for the agentapi start command
|
||||
echo -n "$CODER_MCP_CLAUDE_TASK_PROMPT" > "$module_path/prompt.txt"
|
||||
|
||||
echo -n "${local.agentapi_start_script_b64}" | base64 -d > "$module_path/scripts/agentapi-start.sh"
|
||||
echo -n "${local.agentapi_wait_for_start_script_b64}" | base64 -d > "$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "$module_path/scripts/remove-last-session-id.sh"
|
||||
chmod +x "$module_path/scripts/agentapi-start.sh"
|
||||
chmod +x "$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Claude Code to report tasks via Coder MCP..."
|
||||
export CODER_MCP_APP_STATUS_SLUG="${local.claude_code_app_slug}"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
coder exp mcp configure claude-code "${local.workdir}"
|
||||
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 ! command_exists claude; then
|
||||
echo "Error: Claude Code is not installed. Please enable install_claude_code or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
cd "${local.workdir}"
|
||||
|
||||
# Disable host header check since AgentAPI is proxied by Coder (which does its own validation)
|
||||
export AGENTAPI_ALLOWED_HOSTS="*"
|
||||
|
||||
# Set chat base path for non-subdomain routing (only set if not using subdomain)
|
||||
export AGENTAPI_CHAT_BASE_PATH="${local.agentapi_chat_base_path}"
|
||||
|
||||
nohup "$module_path/scripts/agentapi-start.sh" use_prompt &> "$module_path/agentapi-start.log" &
|
||||
"$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
EOT
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
resource "coder_app" "claude_code_web" {
|
||||
# use a short slug to mitigate https://github.com/coder/coder/issues/15178
|
||||
slug = local.claude_code_app_slug
|
||||
display_name = "Claude Code Web"
|
||||
agent_id = var.agent_id
|
||||
url = "http://localhost:3284/"
|
||||
icon = var.icon
|
||||
order = var.order
|
||||
group = var.group
|
||||
subdomain = var.subdomain
|
||||
healthcheck {
|
||||
url = local.healthcheck_url
|
||||
interval = 3
|
||||
threshold = 20
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_app" "claude_code" {
|
||||
count = var.experiment_cli_app ? 1 : 0
|
||||
|
||||
slug = "claude-code"
|
||||
display_name = "Claude Code CLI"
|
||||
agent_id = var.agent_id
|
||||
command = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
agentapi attach
|
||||
EOT
|
||||
icon = var.icon
|
||||
order = var.experiment_cli_app_order
|
||||
group = var.experiment_cli_app_group
|
||||
}
|
||||
|
||||
resource "coder_ai_task" "claude_code" {
|
||||
sidebar_app {
|
||||
id = coder_app.claude_code_web.id
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
run "test_claude_code_basic" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-123"
|
||||
workdir = "/home/coder/projects"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.workdir == "/home/coder/projects"
|
||||
error_message = "Workdir 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_claude_code == true
|
||||
error_message = "Install claude_code should default to true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_agentapi == true
|
||||
error_message = "Install agentapi should default to true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.report_tasks == true
|
||||
error_message = "report_tasks should default to true"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_code_with_api_key" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-456"
|
||||
workdir = "/home/coder/workspace"
|
||||
claude_api_key = "test-api-key-123"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_env.claude_api_key[0].value == "test-api-key-123"
|
||||
error_message = "Claude API key value should match the input"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_code_with_custom_options" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-789"
|
||||
workdir = "/home/coder/custom"
|
||||
order = 5
|
||||
group = "development"
|
||||
icon = "/icon/custom.svg"
|
||||
model = "opus"
|
||||
ai_prompt = "Help me write better code"
|
||||
permission_mode = "plan"
|
||||
continue = true
|
||||
install_claude_code = false
|
||||
install_agentapi = false
|
||||
claude_code_version = "1.0.0"
|
||||
agentapi_version = "v0.6.0"
|
||||
dangerously_skip_permissions = true
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.order == 5
|
||||
error_message = "Order variable should be set to 5"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.group == "development"
|
||||
error_message = "Group variable should be set to 'development'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.icon == "/icon/custom.svg"
|
||||
error_message = "Icon variable should be set to custom icon"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.model == "opus"
|
||||
error_message = "Claude model variable should be set to 'opus'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.ai_prompt == "Help me write better code"
|
||||
error_message = "AI prompt variable should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.permission_mode == "plan"
|
||||
error_message = "Permission mode should be set to 'plan'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.continue == true
|
||||
error_message = "Continue should be set to true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.claude_code_version == "1.0.0"
|
||||
error_message = "Claude Code 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'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.dangerously_skip_permissions == true
|
||||
error_message = "dangerously_skip_permissions should be set to true"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_code_with_mcp_and_tools" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-mcp"
|
||||
workdir = "/home/coder/mcp-test"
|
||||
mcp = jsonencode({
|
||||
mcpServers = {
|
||||
test = {
|
||||
command = "test-server"
|
||||
args = ["--config", "test.json"]
|
||||
}
|
||||
}
|
||||
})
|
||||
allowed_tools = "bash,python"
|
||||
disallowed_tools = "rm"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.mcp != ""
|
||||
error_message = "MCP configuration should be provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.allowed_tools == "bash,python"
|
||||
error_message = "Allowed tools should be set"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.disallowed_tools == "rm"
|
||||
error_message = "Disallowed tools should be set"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_code_with_scripts" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-scripts"
|
||||
workdir = "/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_claude_code_permission_mode_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-validation"
|
||||
workdir = "/home/coder/test"
|
||||
permission_mode = "acceptEdits"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(["", "default", "acceptEdits", "plan", "bypassPermissions"], var.permission_mode)
|
||||
error_message = "Permission mode should be one of the valid options"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_code_with_boundary" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-boundary"
|
||||
workdir = "/home/coder/boundary-test"
|
||||
enable_boundary = true
|
||||
boundary_log_dir = "/tmp/test-boundary-logs"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.enable_boundary == true
|
||||
error_message = "Boundary should be enabled"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.boundary_log_dir == "/tmp/test-boundary-logs"
|
||||
error_message = "Boundary log dir should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = local.coder_host != ""
|
||||
error_message = "Coder host should be extracted from access URL"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_code_system_prompt" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-system-prompt"
|
||||
workdir = "/home/coder/test"
|
||||
system_prompt = "Custom addition"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
|
||||
error_message = "System prompt should not be empty"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(regexall("Custom addition", coder_env.claude_code_system_prompt.value)) > 0
|
||||
error_message = "System prompt should have system_prompt variable value"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_report_tasks_default" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-report-tasks"
|
||||
workdir = "/home/coder/test"
|
||||
# report_tasks: default is true
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
|
||||
error_message = "System prompt should not be empty"
|
||||
}
|
||||
|
||||
# Ensure system prompt is wrapped by <system>
|
||||
assert {
|
||||
condition = startswith(trimspace(coder_env.claude_code_system_prompt.value), "<system>")
|
||||
error_message = "System prompt should start with <system>"
|
||||
}
|
||||
assert {
|
||||
condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "</system>")
|
||||
error_message = "System prompt should end with </system>"
|
||||
}
|
||||
|
||||
# Ensure Coder sections are injected when report_tasks=true (default)
|
||||
assert {
|
||||
condition = length(regexall("-- Tool Selection --", coder_env.claude_code_system_prompt.value)) > 0
|
||||
error_message = "System prompt should have Tool Selection section"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(regexall("-- Task Reporting --", coder_env.claude_code_system_prompt.value)) > 0
|
||||
error_message = "System prompt should have Task Reporting section"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_report_tasks_disabled" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-report-tasks"
|
||||
workdir = "/home/coder/test"
|
||||
report_tasks = false
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
|
||||
error_message = "System prompt should not be empty"
|
||||
}
|
||||
|
||||
# Ensure system prompt is wrapped by <system>
|
||||
assert {
|
||||
condition = startswith(trimspace(coder_env.claude_code_system_prompt.value), "<system>")
|
||||
error_message = "System prompt should start with <system>"
|
||||
}
|
||||
assert {
|
||||
condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "</system>")
|
||||
error_message = "System prompt should end with </system>"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
# this must be kept in sync with the main.tf file
|
||||
module_path="$HOME/.claude-module"
|
||||
scripts_dir="$module_path/scripts"
|
||||
log_file_path="$module_path/agentapi.log"
|
||||
|
||||
# if the first argument is not empty, start claude with the prompt
|
||||
if [ -n "$1" ]; then
|
||||
cp "$module_path/prompt.txt" /tmp/claude-code-prompt
|
||||
else
|
||||
rm -f /tmp/claude-code-prompt
|
||||
fi
|
||||
|
||||
# if the log file already exists, archive it
|
||||
if [ -f "$log_file_path" ]; then
|
||||
mv "$log_file_path" "$log_file_path"".$(date +%s)"
|
||||
fi
|
||||
|
||||
# see the remove-last-session-id.sh script for details
|
||||
# about why we need it
|
||||
# avoid exiting if the script fails
|
||||
bash "$scripts_dir/remove-last-session-id.sh" "$(pwd)" 2> /dev/null || true
|
||||
|
||||
# we'll be manually handling errors from this point on
|
||||
set +o errexit
|
||||
|
||||
function start_agentapi() {
|
||||
local continue_flag="$1"
|
||||
local prompt_subshell='"$(cat /tmp/claude-code-prompt)"'
|
||||
|
||||
# use low width to fit in the tasks UI sidebar. height is adjusted so that width x height ~= 80x1000 characters
|
||||
# visible in the terminal screen by default.
|
||||
agentapi server --term-width 67 --term-height 1190 -- \
|
||||
bash -c "claude $continue_flag --dangerously-skip-permissions $prompt_subshell" \
|
||||
> "$log_file_path" 2>&1
|
||||
}
|
||||
|
||||
echo "Starting AgentAPI..."
|
||||
|
||||
# attempt to start claude with the --continue flag
|
||||
start_agentapi --continue
|
||||
exit_code=$?
|
||||
|
||||
echo "First AgentAPI exit code: $exit_code"
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# if there was no conversation to continue, claude exited with an error.
|
||||
# start claude without the --continue flag.
|
||||
if grep -q "No conversation found to continue" "$log_file_path"; then
|
||||
echo "AgentAPI with --continue flag failed, starting claude without it."
|
||||
start_agentapi
|
||||
exit_code=$?
|
||||
fi
|
||||
|
||||
echo "Second AgentAPI exit code: $exit_code"
|
||||
|
||||
exit $exit_code
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
# This script waits for the agentapi server to start on port 3284.
|
||||
# It considers the server started after 3 consecutive successful responses.
|
||||
|
||||
agentapi_started=false
|
||||
|
||||
echo "Waiting for agentapi server to start on port 3284..."
|
||||
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:3284/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 3284 after 15 seconds."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "agentapi server started on port 3284."
|
||||
@@ -1,104 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
# Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles
|
||||
set -euo pipefail
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
ARG_CLAUDE_CODE_VERSION=${ARG_CLAUDE_CODE_VERSION:-}
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-}
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
|
||||
ARG_MCP=$(echo -n "${ARG_MCP:-}" | base64 -d)
|
||||
ARG_ALLOWED_TOOLS=${ARG_ALLOWED_TOOLS:-}
|
||||
ARG_DISALLOWED_TOOLS=${ARG_DISALLOWED_TOOLS:-}
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
printf "ARG_CLAUDE_CODE_VERSION: %s\n" "$ARG_CLAUDE_CODE_VERSION"
|
||||
printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
|
||||
printf "ARG_INSTALL_CLAUDE_CODE: %s\n" "$ARG_INSTALL_CLAUDE_CODE"
|
||||
printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
|
||||
printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG"
|
||||
printf "ARG_MCP: %s\n" "$ARG_MCP"
|
||||
printf "ARG_ALLOWED_TOOLS: %s\n" "$ARG_ALLOWED_TOOLS"
|
||||
printf "ARG_DISALLOWED_TOOLS: %s\n" "$ARG_DISALLOWED_TOOLS"
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
function install_claude_code_cli() {
|
||||
if [ "$ARG_INSTALL_CLAUDE_CODE" = "true" ]; then
|
||||
echo "Installing Claude Code via official installer"
|
||||
set +e
|
||||
curl -fsSL claude.ai/install.sh | bash -s -- "$ARG_CLAUDE_CODE_VERSION" 2>&1
|
||||
CURL_EXIT=${PIPESTATUS[0]}
|
||||
set -e
|
||||
if [ $CURL_EXIT -ne 0 ]; then
|
||||
echo "Claude Code installer failed with exit code $$CURL_EXIT"
|
||||
fi
|
||||
|
||||
# Ensure binaries are discoverable.
|
||||
echo "Creating a symlink for claude"
|
||||
sudo ln -s /home/coder/.local/bin/claude /usr/local/bin/claude
|
||||
|
||||
echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')"
|
||||
else
|
||||
echo "Skipping Claude Code installation as per configuration."
|
||||
fi
|
||||
}
|
||||
|
||||
function setup_claude_configurations() {
|
||||
if [ ! -d "$ARG_WORKDIR" ]; then
|
||||
echo "Warning: The specified folder '$ARG_WORKDIR' does not exist."
|
||||
echo "Creating the folder..."
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
echo "Folder created successfully."
|
||||
fi
|
||||
|
||||
module_path="$HOME/.claude-module"
|
||||
mkdir -p "$module_path"
|
||||
|
||||
if [ "$ARG_MCP" != "" ]; then
|
||||
(
|
||||
cd "$ARG_WORKDIR"
|
||||
while IFS= read -r server_name && IFS= read -r server_json; do
|
||||
echo "------------------------"
|
||||
echo "Executing: claude mcp add-json \"$server_name\" '$server_json' (in $ARG_WORKDIR)"
|
||||
claude mcp add-json "$server_name" "$server_json"
|
||||
echo "------------------------"
|
||||
echo ""
|
||||
done < <(echo "$ARG_MCP" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)')
|
||||
)
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_ALLOWED_TOOLS" ]; then
|
||||
coder --allowedTools "$ARG_ALLOWED_TOOLS"
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_DISALLOWED_TOOLS" ]; then
|
||||
coder --disallowedTools "$ARG_DISALLOWED_TOOLS"
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
function report_tasks() {
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
echo "Configuring Claude Code to report tasks via Coder MCP..."
|
||||
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
coder exp mcp configure claude-code "$ARG_WORKDIR"
|
||||
fi
|
||||
}
|
||||
|
||||
install_claude_code_cli
|
||||
setup_claude_configurations
|
||||
report_tasks
|
||||
@@ -26,19 +26,15 @@ echo ".claude.json path $claude_json_path"
|
||||
# Check if .claude.json exists
|
||||
if [ ! -f "$claude_json_path" ]; then
|
||||
echo "No .claude.json file found"
|
||||
exit 1
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Use jq to check if lastSessionId exists for the working directory and remove it
|
||||
|
||||
if jq -e ".projects[\"$working_dir\"].lastSessionId" "$claude_json_path" > /dev/null 2>&1; then
|
||||
# Remove lastSessionId and update the file
|
||||
if jq "del(.projects[\"$working_dir\"].lastSessionId)" "$claude_json_path" > "${claude_json_path}.tmp" && mv "${claude_json_path}.tmp" "$claude_json_path"; then
|
||||
echo "Removed lastSessionId from .claude.json"
|
||||
exit 0
|
||||
else
|
||||
echo "Failed to remove lastSessionId from .claude.json"
|
||||
fi
|
||||
jq "del(.projects[\"$working_dir\"].lastSessionId)" "$claude_json_path" > "${claude_json_path}.tmp" && mv "${claude_json_path}.tmp" "$claude_json_path"
|
||||
echo "Removed lastSessionId from .claude.json"
|
||||
else
|
||||
echo "No lastSessionId found in .claude.json - nothing to do"
|
||||
fi
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
# Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
ARG_MODEL=${ARG_MODEL:-}
|
||||
ARG_RESUME_SESSION_ID=${ARG_RESUME_SESSION_ID:-}
|
||||
ARG_CONTINUE=${ARG_CONTINUE:-false}
|
||||
ARG_DANGEROUSLY_SKIP_PERMISSIONS=${ARG_DANGEROUSLY_SKIP_PERMISSIONS:-}
|
||||
ARG_PERMISSION_MODE=${ARG_PERMISSION_MODE:-}
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d)
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
ARG_ENABLE_BOUNDARY=${ARG_ENABLE_BOUNDARY:-false}
|
||||
ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"main"}
|
||||
ARG_BOUNDARY_LOG_DIR=${ARG_BOUNDARY_LOG_DIR:-"/tmp/boundary_logs"}
|
||||
ARG_BOUNDARY_LOG_LEVEL=${ARG_BOUNDARY_LOG_LEVEL:-"WARN"}
|
||||
ARG_BOUNDARY_PROXY_PORT=${ARG_BOUNDARY_PROXY_PORT:-"8087"}
|
||||
ARG_ENABLE_BOUNDARY_PPROF=${ARG_ENABLE_BOUNDARY_PPROF:-false}
|
||||
ARG_BOUNDARY_PPROF_PORT=${ARG_BOUNDARY_PPROF_PORT:-"6067"}
|
||||
ARG_COMPILE_FROM_SOURCE=${ARG_COMPILE_FROM_SOURCE:-false}
|
||||
ARG_CODER_HOST=${ARG_CODER_HOST:-}
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
printf "ARG_MODEL: %s\n" "$ARG_MODEL"
|
||||
printf "ARG_RESUME: %s\n" "$ARG_RESUME_SESSION_ID"
|
||||
printf "ARG_CONTINUE: %s\n" "$ARG_CONTINUE"
|
||||
printf "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIONS"
|
||||
printf "ARG_PERMISSION_MODE: %s\n" "$ARG_PERMISSION_MODE"
|
||||
printf "ARG_AI_PROMPT: %s\n" "$ARG_AI_PROMPT"
|
||||
printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
|
||||
printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
|
||||
printf "ARG_ENABLE_BOUNDARY: %s\n" "$ARG_ENABLE_BOUNDARY"
|
||||
printf "ARG_BOUNDARY_VERSION: %s\n" "$ARG_BOUNDARY_VERSION"
|
||||
printf "ARG_BOUNDARY_LOG_DIR: %s\n" "$ARG_BOUNDARY_LOG_DIR"
|
||||
printf "ARG_BOUNDARY_LOG_LEVEL: %s\n" "$ARG_BOUNDARY_LOG_LEVEL"
|
||||
printf "ARG_BOUNDARY_PROXY_PORT: %s\n" "$ARG_BOUNDARY_PROXY_PORT"
|
||||
printf "ARG_COMPILE_FROM_SOURCE: %s\n" "$ARG_COMPILE_FROM_SOURCE"
|
||||
printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST"
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
# Clean up stale session data (see remove-last-session-id.sh for details)
|
||||
CAN_CONTINUE_CONVERSATION=false
|
||||
set +e
|
||||
bash "/tmp/remove-last-session-id.sh" "$(pwd)" 2> /dev/null
|
||||
session_cleanup_exit_code=$?
|
||||
set -e
|
||||
|
||||
case $session_cleanup_exit_code in
|
||||
0)
|
||||
CAN_CONTINUE_CONVERSATION=true
|
||||
;;
|
||||
esac
|
||||
|
||||
function install_boundary() {
|
||||
if [ "${ARG_COMPILE_FROM_SOURCE:-false}" = "true" ]; then
|
||||
# Install boundary by compiling from source
|
||||
echo "Compiling boundary from source (version: $ARG_BOUNDARY_VERSION)"
|
||||
git clone https://github.com/coder/boundary.git
|
||||
cd boundary
|
||||
git checkout "$ARG_BOUNDARY_VERSION"
|
||||
|
||||
# Build the binary
|
||||
make build
|
||||
|
||||
# Install binary and wrapper script (optional)
|
||||
sudo cp boundary /usr/local/bin/
|
||||
sudo cp scripts/boundary-wrapper.sh /usr/local/bin/boundary-run
|
||||
sudo chmod +x /usr/local/bin/boundary-run
|
||||
else
|
||||
# Install boundary using official install script
|
||||
echo "Installing boundary using official install script (version: $ARG_BOUNDARY_VERSION)"
|
||||
curl -fsSL https://raw.githubusercontent.com/coder/boundary/main/install.sh | bash -s -- --version "$ARG_BOUNDARY_VERSION"
|
||||
fi
|
||||
}
|
||||
|
||||
function validate_claude_installation() {
|
||||
if command_exists claude; then
|
||||
printf "Claude Code is installed\n"
|
||||
else
|
||||
printf "Error: Claude Code is not installed. Please enable install_claude_code or install it manually\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Hardcoded task session ID for Coder task reporting
|
||||
# This ensures all task sessions use a consistent, predictable ID
|
||||
TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2"
|
||||
|
||||
task_session_exists() {
|
||||
local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
|
||||
local project_dir="$HOME/.claude/projects/${workdir_normalized}"
|
||||
|
||||
printf "PROJECT_DIR: %s, workdir_normalized: %s\n" "$project_dir" "$workdir_normalized"
|
||||
|
||||
if [ -d "$project_dir" ] && find "$project_dir" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then
|
||||
printf "TASK_SESSION_ID: %s file found\n" "$TASK_SESSION_ID"
|
||||
return 0
|
||||
else
|
||||
printf "TASK_SESSION_ID: %s file not found\n" "$TASK_SESSION_ID"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
ARGS=()
|
||||
|
||||
function start_agentapi() {
|
||||
# For Task reporting
|
||||
export CODER_MCP_ALLOWED_TOOLS="coder_report_task"
|
||||
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
|
||||
if [ -n "$ARG_MODEL" ]; then
|
||||
ARGS+=(--model "$ARG_MODEL")
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_PERMISSION_MODE" ]; then
|
||||
ARGS+=(--permission-mode "$ARG_PERMISSION_MODE")
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_RESUME_SESSION_ID" ]; then
|
||||
echo "Resuming task session by ID: $ARG_RESUME_SESSION_ID"
|
||||
ARGS+=(--resume "$ARG_RESUME_SESSION_ID")
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
elif [ "$ARG_CONTINUE" = "true" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] && task_session_exists; then
|
||||
echo "Task session detected (ID: $TASK_SESSION_ID)"
|
||||
ARGS+=(--resume "$TASK_SESSION_ID")
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
echo "Resuming existing task session"
|
||||
elif [ "$ARG_REPORT_TASKS" = "false" ] && [ "$CAN_CONTINUE_CONVERSATION" = true ]; then
|
||||
echo "Previous session exists"
|
||||
ARGS+=(--continue)
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Resuming existing session"
|
||||
else
|
||||
echo "No existing session found"
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
if task_session_exists; then
|
||||
ARGS+=(--resume "$TASK_SESSION_ID")
|
||||
else
|
||||
ARGS+=(--session-id "$TASK_SESSION_ID")
|
||||
fi
|
||||
fi
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions -- "$ARG_AI_PROMPT")
|
||||
else
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
echo "Starting new session with prompt"
|
||||
else
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Starting new session"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Continue disabled, starting fresh session"
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
if task_session_exists; then
|
||||
ARGS+=(--resume "$TASK_SESSION_ID")
|
||||
else
|
||||
ARGS+=(--session-id "$TASK_SESSION_ID")
|
||||
fi
|
||||
fi
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions -- "$ARG_AI_PROMPT")
|
||||
else
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
echo "Starting new session with prompt"
|
||||
else
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Starting claude code session"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
|
||||
|
||||
if [ "${ARG_ENABLE_BOUNDARY:-false}" = "true" ]; then
|
||||
install_boundary
|
||||
|
||||
mkdir -p "$ARG_BOUNDARY_LOG_DIR"
|
||||
printf "Starting with coder boundary enabled\n"
|
||||
|
||||
# Build boundary args with conditional --unprivileged flag
|
||||
BOUNDARY_ARGS=(--log-dir "$ARG_BOUNDARY_LOG_DIR")
|
||||
# Add default allowed URLs
|
||||
BOUNDARY_ARGS+=(--allow "domain=anthropic.com" --allow "domain=registry.npmjs.org" --allow "domain=sentry.io" --allow "domain=claude.ai" --allow "domain=$ARG_CODER_HOST")
|
||||
|
||||
# Add any additional allowed URLs from the variable
|
||||
if [ -n "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" ]; then
|
||||
IFS='|' read -ra ADDITIONAL_URLS <<< "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS"
|
||||
for url in "${ADDITIONAL_URLS[@]}"; do
|
||||
# Quote the URL to preserve spaces within the allow rule
|
||||
BOUNDARY_ARGS+=(--allow "$url")
|
||||
done
|
||||
fi
|
||||
|
||||
# Set HTTP Proxy port used by Boundary
|
||||
BOUNDARY_ARGS+=(--proxy-port $ARG_BOUNDARY_PROXY_PORT)
|
||||
|
||||
# Set log level for boundary
|
||||
BOUNDARY_ARGS+=(--log-level $ARG_BOUNDARY_LOG_LEVEL)
|
||||
|
||||
if [ "${ARG_ENABLE_BOUNDARY_PPROF:-false}" = "true" ]; then
|
||||
# Enable boundary pprof server on specified port
|
||||
BOUNDARY_ARGS+=(--pprof)
|
||||
BOUNDARY_ARGS+=(--pprof-port ${ARG_BOUNDARY_PPROF_PORT})
|
||||
fi
|
||||
|
||||
agentapi server --type claude --term-width 67 --term-height 1190 -- \
|
||||
boundary-run "${BOUNDARY_ARGS[@]}" -- \
|
||||
claude "${ARGS[@]}"
|
||||
else
|
||||
agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
validate_claude_installation
|
||||
start_agentapi
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const args = process.argv.slice(2);
|
||||
const port = 3284;
|
||||
|
||||
const controlFile = "/tmp/agentapi-mock.control";
|
||||
let control = "";
|
||||
if (fs.existsSync(controlFile)) {
|
||||
control = fs.readFileSync(controlFile, "utf8");
|
||||
}
|
||||
|
||||
if (
|
||||
control === "no-conversation-found" &&
|
||||
args.join(" ").includes("--continue")
|
||||
) {
|
||||
// this must match the error message in the agentapi-start.sh script
|
||||
console.error("No conversation found to continue");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
"/home/coder/agentapi-mock.log",
|
||||
`AGENTAPI_ALLOWED_HOSTS: ${process.env.AGENTAPI_ALLOWED_HOSTS}`,
|
||||
);
|
||||
|
||||
console.log(`starting server on port ${port}`);
|
||||
|
||||
http
|
||||
.createServer(function (_request, response) {
|
||||
response.writeHead(200);
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
status: "stable",
|
||||
}),
|
||||
);
|
||||
})
|
||||
.listen(port);
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const main = async () => {
|
||||
console.log("mocking claude");
|
||||
// sleep for 30 minutes
|
||||
await new Promise((resolve) => setTimeout(resolve, 30 * 60 * 1000));
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "claude version v1.0.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
while true; do
|
||||
echo "$(date) - claude-mock"
|
||||
sleep 15
|
||||
done
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const statusSlugEnvVar = "CODER_MCP_APP_STATUS_SLUG";
|
||||
const agentApiUrlEnvVar = "CODER_MCP_AI_AGENTAPI_URL";
|
||||
|
||||
fs.writeFileSync(
|
||||
"/home/coder/coder-mock-output.json",
|
||||
JSON.stringify({
|
||||
statusSlug: process.env[statusSlugEnvVar] ?? "env var not set",
|
||||
agentApiUrl: process.env[agentApiUrlEnvVar] ?? "env var not set",
|
||||
}),
|
||||
);
|
||||