Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 656137108a |
@@ -1,9 +1,9 @@
|
||||
Closes #
|
||||
|
||||
## Description
|
||||
|
||||
<!-- Briefly describe what this PR does and why -->
|
||||
|
||||
---
|
||||
|
||||
## Type of Change
|
||||
|
||||
- [ ] New module
|
||||
@@ -12,6 +12,8 @@ Closes #
|
||||
- [ ] Documentation
|
||||
- [ ] Other
|
||||
|
||||
---
|
||||
|
||||
## Module Information
|
||||
|
||||
<!-- Delete this section if not applicable -->
|
||||
@@ -20,12 +22,18 @@ Closes #
|
||||
**New version:** `v1.0.0`
|
||||
**Breaking change:** [ ] Yes [ ] No
|
||||
|
||||
---
|
||||
|
||||
## Testing & Validation
|
||||
|
||||
- [ ] Tests pass (`bun test`)
|
||||
- [ ] Code formatted (`bun run fmt`)
|
||||
- [ ] Changes tested locally
|
||||
|
||||
---
|
||||
|
||||
## Related Issues
|
||||
|
||||
<!-- Link related issues or write "None" if not applicable -->
|
||||
|
||||
Closes #
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.34.0
|
||||
uses: crate-ci/typos@v1.33.1
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
|
||||
@@ -6,12 +6,6 @@ on:
|
||||
# Matches release/<namespace>/<resource_name>/<semantic_version>
|
||||
# (e.g., "release/whizus/exoscale-zone/v1.0.13")
|
||||
- "release/*/*/v*.*.*"
|
||||
branches: # Templates get released when merged to main
|
||||
- main
|
||||
paths:
|
||||
- ".github/workflows/deploy-registry.yaml"
|
||||
- "registry/**/templates/**"
|
||||
- ".icons/**"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -33,6 +27,7 @@ jobs:
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a
|
||||
- name: Deploy to dev.registry.coder.com
|
||||
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch main
|
||||
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch dev
|
||||
- name: Deploy to registry.coder.com
|
||||
run: gcloud builds triggers run 106610ff-41fb-4bd0-90a2-7643583fb9c0 --tag production
|
||||
run: |
|
||||
gcloud builds triggers run 106610ff-41fb-4bd0-90a2-7643583fb9c0 --branch main
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="none" viewBox="0 0 64 64">
|
||||
<defs>
|
||||
<radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="rotate(49.385 -18.029987 36.649084) scale(49.4299)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".026042" stop-color="#8DFDFD"/>
|
||||
<stop offset=".270833" stop-color="#87FBFB"/>
|
||||
<stop offset=".484416" stop-color="#74D6F4"/>
|
||||
<stop offset=".931964" stop-color="#0038FF"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="b" cx="0" cy="0" r="1" gradientTransform="rotate(132.274 3.919184 20.864728) scale(23.7857)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0500FF" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#0100FF" stop-opacity=".15"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="c" cx="0" cy="0" r="1" gradientTransform="rotate(42.678 -19.143042 44.644478) scale(41.8951)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".520394" stop-color="#FF00E5" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#FF00E5" stop-opacity=".65"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="matrix(30.00005 -22.00001 19.46596 26.54453 32.3943 42.4)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".777466" stop-color="#001AFF"/>
|
||||
<stop offset="1" stop-color="#8ACEFF"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="f" cx="0" cy="0" r="1" gradientTransform="matrix(14.91531 -8.80077 11.61873 19.69112 44.057 27.7156)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".71875" stop-color="#FA00FF" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#FF00D6" stop-opacity=".44"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="h" cx="0" cy="0" r="1" gradientTransform="rotate(63.435 -9.856848 34.706598) scale(30.4105 69.8305)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0D67A9"/>
|
||||
<stop offset="1" stop-color="#AEDDFF"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="j" cx="0" cy="0" r="1" gradientTransform="rotate(73.835 -3.838438 33.695644) scale(28.736 56.1739)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0068C9"/>
|
||||
<stop offset="1" stop-color="#fff"/>
|
||||
</radialGradient>
|
||||
<filter id="g" width="48.3057" height="34.5039" x="8.25781" y="24.2656" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur result="effect1_foregroundBlur_6490_3223" stdDeviation="1"/>
|
||||
</filter>
|
||||
<filter id="i" width="45.7057" height="31.9039" x="9.55781" y="25.5656" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur result="effect1_foregroundBlur_6490_3223" stdDeviation=".35"/>
|
||||
</filter>
|
||||
<linearGradient id="d" x1="63.9941" x2="37.1941" y1="33.6" y2="34.4" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FD3AF5"/>
|
||||
<stop offset="1" stop-color="#FD3AF5" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#a)" d="M63.9941 32c0 17.6731-14.3268 32-32 32C14.3211 64-.00586 49.6731-.00586 32c0-17.6731 14.32696-32 31.99996-32 8.306 3.75956 16.9952 17.7487 20.6338 22.1202 3.6389 4.3715 13.0573 9.8798 8.4414-1.6751 1.9681 3.1232 2.9248 8.3051 2.9248 11.5549Z"/>
|
||||
<path fill="url(#b)" d="M63.9941 32c0 17.6731-14.3268 32-32 32C14.3211 64-.00586 49.6731-.00586 32c0-17.6731 14.32696-32 31.99996-32 8.306 3.75956 16.9952 17.7487 20.6338 22.1202 3.6389 4.3715 13.0573 9.8798 8.4414-1.6751 1.9681 3.1232 2.9248 8.3051 2.9248 11.5549Z"/>
|
||||
<path fill="url(#c)" d="M63.9941 32c0 17.6731-14.3268 32-32 32C14.3211 64-.00586 49.6731-.00586 32c0-17.6731 14.32696-32 31.99996-32 8.306 3.75956 16.9952 17.7487 20.6338 22.1202 3.6389 4.3715 13.0573 9.8798 8.4414-1.6751 1.9681 3.1232 2.9248 8.3051 2.9248 11.5549Z"/>
|
||||
<path fill="url(#d)" fill-opacity=".3" d="M63.9941 32c0 17.6731-14.3268 32-32 32C14.3211 64-.00586 49.6731-.00586 32c0-17.6731 14.32696-32 31.99996-32 8.306 3.75956 16.9952 17.7487 20.6338 22.1202 3.6389 4.3715 13.0573 9.8798 8.4414-1.6751 1.9681 3.1232 2.9248 8.3051 2.9248 11.5549Z"/>
|
||||
<path fill="url(#e)" d="M61.0886 20.4758c-3.1529-5.3391-9.686-9.2821-17.5378-10.2688 2.2608 2.7375 4.3175 5.5453 6.0172 7.8664 1.2242 1.6711 2.2633 3.0899 3.0601 4.0469 3.6389 4.3711 13.0573 9.8797 8.4414-1.675.0063.0102.0127.0203.0191.0305Z"/>
|
||||
<path fill="url(#f)" d="M61.0886 20.4758c-3.1529-5.3391-9.686-9.2821-17.5378-10.2688 2.2608 2.7375 4.3175 5.5453 6.0172 7.8664 1.2242 1.6711 2.2633 3.0899 3.0601 4.0469 3.6389 4.3711 13.0573 9.8797 8.4414-1.675.0063.0102.0127.0203.0191.0305Z"/>
|
||||
<g filter="url(#g)">
|
||||
<path fill="url(#h)" d="M29.3127 27.0066c12.113-2.5862 23.3196 1.8139 25.0306 9.8279 1.711 8.014-6.7214 16.6071-18.8343 19.1933C23.396 58.614 12.1894 54.214 10.4783 46.2c-1.711-8.014 6.7215-16.6072 18.8344-19.1934Z"/>
|
||||
</g>
|
||||
<g filter="url(#i)">
|
||||
<path fill="url(#j)" fill-opacity=".2" fill-rule="evenodd" d="M48.9867 47.3643c3.1734-3.2337 4.5278-6.8744 3.8174-10.2012-.7102-3.3268-3.4332-6.0967-7.6507-7.7527-4.2039-1.6506-9.7148-2.1025-15.5122-.8645-5.7973 1.2377-10.6433 3.9007-13.8065 7.1243-3.1734 3.2339-4.5276 6.8744-3.8173 10.2012.7103 3.3267 3.4332 6.0968 7.6506 7.7527 4.2039 1.6506 9.7148 2.1024 15.5122.8647 5.7974-1.2377 10.6433-3.9009 13.8065-7.1245Zm-13.4778 8.6636c12.1128-2.5862 20.5452-11.1793 18.8343-19.1933-1.7112-8.0141-12.9177-12.4142-25.0305-9.828-12.1131 2.5862-20.54545 11.1795-18.8344 19.1935 1.711 8.0138 12.9176 12.414 25.0306 9.8278Z" clip-rule="evenodd"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg fill="none" height="64" viewBox="0 0 64 64" width="64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1=".850001" x2="62.62" y1="62.72" y2="1.81"><stop offset="0" stop-color="#ff9419"/><stop offset=".43" stop-color="#ff021d"/><stop offset=".99" stop-color="#e600ff"/></linearGradient><clipPath id="b"><path d="m0 0h64v64h-64z"/></clipPath><g clip-path="url(#b)"><path d="m20.34 3.66-16.68 16.68c-2.34 2.34-3.66 5.52-3.66 8.84v29.82c0 2.76 2.24 5 5 5h29.82c3.32 0 6.49-1.32 8.84-3.66l16.68-16.68c2.34-2.34 3.66-5.52 3.66-8.84v-29.82c0-2.76-2.24-5-5-5h-29.82c-3.32 0-6.49 1.32-8.84 3.66z" fill="url(#a)"/><path d="m48 16h-40v40h40z" fill="#000"/><path d="m30 47h-17v4h17z" fill="#fff"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 785 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="24" viewBox="0 0 20 24" fill="none"><path d="M3.80081 18.5661C1.32306 24.0572 6.59904 25.434 10.4904 22.2205C11.6339 25.8242 15.926 23.1361 17.4652 20.3445C20.8578 14.1915 19.4877 7.91459 19.1361 6.61988C16.7244 -2.20972 4.67055 -2.21852 2.59581 6.6649C2.11136 8.21946 2.10284 9.98752 1.82846 11.8233C1.69011 12.749 1.59258 13.3398 1.23436 14.3135C1.02841 14.8733 0.745043 15.3704 0.299833 16.2082C-0.391594 17.5095 -0.0998802 20.021 3.46397 18.7186V18.7195L3.80081 18.5661Z" fill="white"></path><path d="M10.9614 10.4413C9.97202 10.4413 9.82422 9.25893 9.82422 8.55407C9.82422 7.91791 9.93824 7.4124 10.1542 7.09197C10.3441 6.81003 10.6158 6.66699 10.9614 6.66699C11.3071 6.66699 11.6036 6.81228 11.8128 7.09892C12.0511 7.42554 12.177 7.92861 12.177 8.55407C12.177 9.73591 11.7226 10.4413 10.9616 10.4413H10.9614Z" fill="black"></path><path d="M15.0318 10.4413C14.0423 10.4413 13.8945 9.25893 13.8945 8.55407C13.8945 7.91791 14.0086 7.4124 14.2245 7.09197C14.4144 6.81003 14.6861 6.66699 15.0318 6.66699C15.3774 6.66699 15.6739 6.81228 15.8831 7.09892C16.1214 7.42554 16.2474 7.92861 16.2474 8.55407C16.2474 9.73591 15.793 10.4413 15.0319 10.4413H15.0318Z" fill="black"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,35 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none" viewBox="0 0 512 512">
|
||||
<g transform="translate(256 256)scale(8.96)">
|
||||
<linearGradient id="a" x1="6.221" x2="37.408" y1="6.221" y2="37.408" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" style="stop-color:#f0f0f0;stop-opacity:1"/>
|
||||
<stop offset="100%" style="stop-color:#bbc1c4;stop-opacity:1"/>
|
||||
</linearGradient>
|
||||
<path d="M24 5C13.507 5 5 13.507 5 24s8.507 19 19 19 19-8.507 19-19S34.493 5 24 5" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#a);fill-rule:nonzero;opacity:1" transform="translate(-24 -24)"/>
|
||||
</g>
|
||||
<g transform="translate(256 256)scale(8.96)">
|
||||
<linearGradient id="b" x1="12.859" x2="35.224" y1="12.859" y2="35.224" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" style="stop-color:#e04f12;stop-opacity:1"/>
|
||||
<stop offset="61.5%" style="stop-color:#ce400d;stop-opacity:1"/>
|
||||
<stop offset="100%" style="stop-color:#c03409;stop-opacity:1"/>
|
||||
</linearGradient>
|
||||
<path d="M24 40c8.837 0 16-7.163 16-16S32.837 8 24 8 8 15.163 8 24s7.163 16 16 16" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#b);fill-rule:nonzero;opacity:1" transform="translate(-24 -24)"/>
|
||||
</g>
|
||||
<path d="m30.414 20 3.89-3.89c.708-.708.449-1.772 0-2.221l-2.195-2.195a1.573 1.573 0 0 0-2.218.001l-7.194 7.194c-.549.549-.752 1.469-.001 2.22l7.196 7.196c.76.76 1.592.625 2.218-.001l2.194-2.194c.707-.707.716-1.505.001-2.22z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#000;fill-rule:nonzero;opacity:.05" transform="translate(40.96 40.96)scale(8.96)"/>
|
||||
<path d="m33.951 14.244-2.195-2.195a1.07 1.07 0 0 0-1.511 0l-7.195 7.195c-.386.386-.487 1.025 0 1.512l7.196 7.196c.491.491 1.087.424 1.511 0l2.195-2.195c.464-.464.469-1.044 0-1.512L29.707 20l4.244-4.244c.465-.465.335-1.177 0-1.512" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#000;fill-rule:nonzero;opacity:.07" transform="translate(40.96 40.96)scale(8.96)"/>
|
||||
<path d="m17.586 28-3.89 3.89c-.708.708-.449 1.772 0 2.221l2.195 2.195c.611.609 1.606.61 2.218-.001l7.194-7.194c.549-.549.752-1.469.001-2.22l-7.196-7.196c-.76-.76-1.592-.625-2.218.001l-2.194 2.194c-.707.707-.716 1.505-.001 2.22z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#000;fill-rule:nonzero;opacity:.05" transform="translate(40.96 40.96)scale(8.96)"/>
|
||||
<path d="m14.049 33.756 2.195 2.195a1.07 1.07 0 0 0 1.511 0l7.195-7.195c.386-.386.487-1.025 0-1.512l-7.196-7.196c-.491-.491-1.087-.424-1.511 0l-2.195 2.195c-.464.464-.469 1.044 0 1.512L18.293 28l-4.244 4.244c-.465.465-.335 1.177 0 1.512" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#000;fill-rule:nonzero;opacity:.07" transform="translate(40.96 40.96)scale(8.96)"/>
|
||||
<g transform="translate(296.83 220.16)scale(8.96)">
|
||||
<linearGradient id="c" x1="23.755" x2="38.564" y1="9.93" y2="33.557" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" style="stop-color:#fcfcfc;stop-opacity:1"/>
|
||||
<stop offset="100%" style="stop-color:#c3c9cd;stop-opacity:1"/>
|
||||
</linearGradient>
|
||||
<path d="m33.598 14.598-2.196-2.196a.57.57 0 0 0-.804 0l-7.196 7.196a.57.57 0 0 0 0 .804l7.196 7.196a.57.57 0 0 0 .804 0l2.196-2.196a.57.57 0 0 0 0-.804L29 20l4.598-4.598a.57.57 0 0 0 0-.804" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#c);fill-rule:nonzero;opacity:1" transform="translate(-28.5 -20)"/>
|
||||
</g>
|
||||
<g transform="translate(215.17 291.84)scale(8.96)">
|
||||
<linearGradient id="d" x1="11.438" x2="26.247" y1="17.637" y2="41.265" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" style="stop-color:#fcfcfc;stop-opacity:1"/>
|
||||
<stop offset="100%" style="stop-color:#c3c9cd;stop-opacity:1"/>
|
||||
</linearGradient>
|
||||
<path d="M14.402 23.402 19 28l-4.598 4.598a.57.57 0 0 0 0 .804l2.196 2.196a.57.57 0 0 0 .804 0l7.196-7.196a.57.57 0 0 0 0-.804l-7.196-7.196a.57.57 0 0 0-.804 0l-2.196 2.196a.57.57 0 0 0 0 .804" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#d);fill-rule:nonzero;opacity:1" transform="translate(-19.5 -28)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M66.1403 24.9315H106.4V33.5714H14.6005V99.3601H106.4V33.5714L115 33.5682V107.997L14.591 108V99.3695H6V24.9409H14.0531V24.9346L57.5399 24.9315V12H66.1403V24.9315Z" fill="white"/>
|
||||
<path d="M77.94 57.3044V70.2485H69.3521V57.3044H77.94Z" fill="white"/>
|
||||
<path d="M52.1701 57.3044V70.2485H43.5822V57.3044H52.1701Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 452 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="90" viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 5.625C6.8842 5.625 5.625 6.8842 5.625 8.4375V70.3125H0V8.4375C0 3.7776 3.7776 0 8.4375 0H83.7925C87.551 0 89.4333 4.5442 86.7756 7.20186L40.3642 53.6133H53.4375V47.8125H59.0625V55.0195C59.0625 57.3495 57.1737 59.2383 54.8438 59.2383H34.7392L25.0712 68.9062H68.9062V33.75H74.5312V68.9062C74.5312 72.0128 72.0128 74.5312 68.9062 74.5312H19.4462L9.60248 84.375H81.5625C83.1158 84.375 84.375 83.1158 84.375 81.5625V19.6875H90V81.5625C90 86.2224 86.2224 90 81.5625 90H6.20749C2.44898 90 0.566723 85.4558 3.22438 82.7981L49.46 36.5625H36.5625V42.1875H30.9375V35.1562C30.9375 32.8263 32.8263 30.9375 35.1562 30.9375H55.085L64.9288 21.0938H21.0938V56.25H15.4688V21.0938C15.4688 17.9871 17.9871 15.4688 21.0938 15.4688H70.5538L80.3975 5.625H8.4375Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 904 B |
@@ -4,14 +4,12 @@ Welcome! This guide covers how to contribute to the Coder Registry, whether you'
|
||||
|
||||
## What is the Coder Registry?
|
||||
|
||||
The Coder Registry is a collection of Terraform modules and templates for Coder workspaces. Modules provide IDEs, authentication integrations, development tools, and other workspace functionality. Templates provide complete workspace configurations for different platforms and use cases that appear as community templates on the registry website.
|
||||
The Coder Registry is a collection of Terraform modules that extend Coder workspaces with development tools like VS Code, Cursor, JetBrains IDEs, and more.
|
||||
|
||||
## Types of Contributions
|
||||
|
||||
- **[New Modules](#creating-a-new-module)** - Add support for a new tool or functionality
|
||||
- **[New Templates](#creating-a-new-template)** - Create complete workspace configurations
|
||||
- **[Existing Modules](#contributing-to-existing-modules)** - Fix bugs, add features, or improve documentation
|
||||
- **[Existing Templates](#contributing-to-existing-templates)** - Improve workspace templates
|
||||
- **[Bug Reports](#reporting-issues)** - Report problems or request features
|
||||
|
||||
## Setup
|
||||
@@ -38,15 +36,7 @@ bun install
|
||||
|
||||
### Understanding Namespaces
|
||||
|
||||
All modules and templates are organized under `/registry/[namespace]/`. Each contributor gets their own namespace with both modules and templates directories:
|
||||
|
||||
```
|
||||
registry/[namespace]/
|
||||
├── modules/ # Individual components and tools
|
||||
└── templates/ # Complete workspace configurations
|
||||
```
|
||||
|
||||
For example: `/registry/your-username/modules/` and `/registry/your-username/templates/`. If a namespace is taken, choose a different unique namespace, but you can still use any display name on the Registry website.
|
||||
All modules are organized under `/registry/[namespace]/modules/`. Each contributor gets their own namespace (e.g., `/registry/your-username/modules/`). If a namespace is taken, choose a different unique namespace, but you can still use any display name on the Registry website.
|
||||
|
||||
### Images and Icons
|
||||
|
||||
@@ -146,171 +136,15 @@ git push origin your-branch
|
||||
|
||||
---
|
||||
|
||||
## Creating a New Template
|
||||
|
||||
Templates are complete Coder workspace configurations that users can deploy directly. Unlike modules (which are components), templates provide full infrastructure definitions for specific platforms or use cases.
|
||||
|
||||
### Template Structure
|
||||
|
||||
Templates follow the same namespace structure as modules but are located in the `templates` directory:
|
||||
|
||||
```
|
||||
registry/[your-username]/templates/[template-name]/
|
||||
├── main.tf # Complete Terraform configuration
|
||||
├── README.md # Documentation with frontmatter
|
||||
├── [additional files] # Scripts, configs, etc.
|
||||
```
|
||||
|
||||
### 1. Create Your Template Directory
|
||||
|
||||
```bash
|
||||
mkdir -p registry/[your-username]/templates/[template-name]
|
||||
cd registry/[your-username]/templates/[template-name]
|
||||
```
|
||||
|
||||
### 2. Create Template Files
|
||||
|
||||
#### main.tf
|
||||
|
||||
Your `main.tf` should be a complete Coder template configuration including:
|
||||
|
||||
- Required providers (coder, and your infrastructure provider)
|
||||
- Coder agent configuration
|
||||
- Infrastructure resources (containers, VMs, etc.)
|
||||
- Registry modules for IDEs, tools, and integrations
|
||||
|
||||
Example structure:
|
||||
|
||||
```terraform
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
# Add your infrastructure provider (docker, aws, etc.)
|
||||
}
|
||||
}
|
||||
|
||||
# Coder data sources
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
# Coder agent
|
||||
resource "coder_agent" "main" {
|
||||
arch = "amd64"
|
||||
os = "linux"
|
||||
startup_script = <<-EOT
|
||||
# Startup commands here
|
||||
EOT
|
||||
}
|
||||
|
||||
# Registry modules for IDEs, tools, and integrations
|
||||
module "code-server" {
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "~> 1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
# Your infrastructure resources
|
||||
# (Docker containers, AWS instances, etc.)
|
||||
```
|
||||
|
||||
#### README.md
|
||||
|
||||
Create documentation with proper frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
display_name: "Template Name"
|
||||
description: "Brief description of what this template provides"
|
||||
icon: "../../../../.icons/platform.svg"
|
||||
verified: false
|
||||
tags: ["platform", "use-case", "tools"]
|
||||
---
|
||||
|
||||
# Template Name
|
||||
|
||||
Describe what the template provides and how to use it.
|
||||
|
||||
Include any setup requirements, resource information, or usage notes that users need to know.
|
||||
```
|
||||
|
||||
### 3. Test Your Template
|
||||
|
||||
Templates should be tested to ensure they work correctly. Test with Coder:
|
||||
|
||||
```bash
|
||||
cd registry/[your-username]/templates/[template-name]
|
||||
coder templates push [template-name] -d .
|
||||
```
|
||||
|
||||
### 4. Template Best Practices
|
||||
|
||||
- **Use registry modules**: Leverage existing modules for IDEs, tools, and integrations
|
||||
- **Provide sensible defaults**: Make the template work out-of-the-box
|
||||
- **Include metadata**: Add useful workspace metadata (CPU, memory, disk usage)
|
||||
- **Document prerequisites**: Clearly explain infrastructure requirements
|
||||
- **Use variables**: Allow customization of common settings
|
||||
- **Follow naming conventions**: Use descriptive, consistent naming
|
||||
|
||||
### 5. Template Guidelines
|
||||
|
||||
- Templates appear as "Community Templates" on the registry website
|
||||
- Include proper error handling and validation
|
||||
- Test with Coder before submitting
|
||||
- Document any required permissions or setup steps
|
||||
- Use semantic versioning in your README frontmatter
|
||||
|
||||
---
|
||||
|
||||
## Contributing to Existing Templates
|
||||
|
||||
### 1. Types of Template Improvements
|
||||
|
||||
**Bug fixes:**
|
||||
|
||||
- Fix infrastructure provisioning issues
|
||||
- Resolve agent connectivity problems
|
||||
- Correct resource naming or tagging
|
||||
|
||||
**Feature additions:**
|
||||
|
||||
- Add new registry modules for additional functionality
|
||||
- Include additional infrastructure options
|
||||
- Improve startup scripts or automation
|
||||
|
||||
**Platform updates:**
|
||||
|
||||
- Update base images or AMIs
|
||||
- Adapt to new platform features
|
||||
- Improve security configurations
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Clarify prerequisites and setup steps
|
||||
- Add troubleshooting guides
|
||||
- Improve usage examples
|
||||
|
||||
### 2. Testing Template Changes
|
||||
|
||||
Testing template modifications thoroughly is necessary. Test with Coder:
|
||||
|
||||
```bash
|
||||
coder templates push test-[template-name] -d .
|
||||
```
|
||||
|
||||
### 3. Maintain Compatibility
|
||||
|
||||
- Don't remove existing variables without clear migration path
|
||||
- Preserve backward compatibility when possible
|
||||
- Test that existing workspaces still function
|
||||
- Document any breaking changes clearly
|
||||
|
||||
---
|
||||
|
||||
## Contributing to Existing Modules
|
||||
|
||||
### 1. Make Your Changes
|
||||
### 1. Find the Module
|
||||
|
||||
```bash
|
||||
find registry -name "*[module-name]*" -type d
|
||||
```
|
||||
|
||||
### 2. Make Your Changes
|
||||
|
||||
**For bug fixes:**
|
||||
|
||||
@@ -332,7 +166,7 @@ coder templates push test-[template-name] -d .
|
||||
- Add missing variable documentation
|
||||
- Improve usage examples
|
||||
|
||||
### 2. Test Your Changes
|
||||
### 3. Test Your Changes
|
||||
|
||||
```bash
|
||||
# Test a specific module
|
||||
@@ -342,7 +176,7 @@ bun test -t 'module-name'
|
||||
bun test
|
||||
```
|
||||
|
||||
### 3. Maintain Backward Compatibility
|
||||
### 4. Maintain Backward Compatibility
|
||||
|
||||
- New variables should have default values
|
||||
- Don't break existing functionality
|
||||
@@ -374,7 +208,6 @@ bun test
|
||||
We have different PR templates for different types of contributions. GitHub will show you options to choose from, or you can manually select:
|
||||
|
||||
- **New Module**: Use `?template=new_module.md`
|
||||
- **New Template**: Use `?template=new_template.md`
|
||||
- **Bug Fix**: Use `?template=bug_fix.md`
|
||||
- **Feature**: Use `?template=feature.md`
|
||||
- **Documentation**: Use `?template=documentation.md`
|
||||
@@ -391,13 +224,6 @@ Example: `https://github.com/coder/registry/compare/main...your-branch?template=
|
||||
- `main.test.ts` - Working tests
|
||||
- `README.md` - Documentation with frontmatter
|
||||
|
||||
### Every Template Must Have
|
||||
|
||||
- `main.tf` - Complete Terraform configuration
|
||||
- `README.md` - Documentation with frontmatter
|
||||
|
||||
Templates don't require test files like modules do, but should be manually tested before submission.
|
||||
|
||||
### README Frontmatter
|
||||
|
||||
Module README frontmatter must include:
|
||||
@@ -478,7 +304,7 @@ When reporting bugs, include:
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Examples**: Check `/registry/coder/modules/` for well-structured modules and `/registry/coder/templates/` for complete templates
|
||||
- **Examples**: Check `/registry/coder/modules/` for well-structured modules
|
||||
- **Issues**: Open an issue for technical problems
|
||||
- **Community**: Reach out to the Coder community for questions
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ brew install go
|
||||
sudo apt install golang-go
|
||||
```
|
||||
|
||||
## Reviewing a PR
|
||||
## Daily Tasks
|
||||
|
||||
### Review PRs
|
||||
|
||||
Check that PRs have:
|
||||
|
||||
@@ -24,7 +26,7 @@ Check that PRs have:
|
||||
- [ ] Formatted code (`bun run fmt`)
|
||||
- [ ] Avatar image for new namespaces (`avatar.png` or `avatar.svg` in `.images/`)
|
||||
|
||||
### Version Guidelines
|
||||
#### Version Guidelines
|
||||
|
||||
When reviewing PRs, ensure the version change follows semantic versioning:
|
||||
|
||||
@@ -40,7 +42,7 @@ PRs should clearly indicate the version change (e.g., `v1.2.3 → v1.2.4`).
|
||||
go build ./cmd/readmevalidation && ./readmevalidation
|
||||
```
|
||||
|
||||
## Making a Release
|
||||
## Releases
|
||||
|
||||
### Create Release Tags
|
||||
|
||||
@@ -97,3 +99,5 @@ status: "community" # or "partner", "official"
|
||||
- **Tests fail**: Ensure Docker with `--network=host`, check Terraform syntax
|
||||
- **Wrong file structure**: Use `./scripts/new_module.sh` for new modules
|
||||
- **Missing namespace avatar**: Must be `avatar.png` or `avatar.svg` in `.images/` directory
|
||||
|
||||
That's it. Keep it simple.
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "registry",
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.18",
|
||||
"bun-types": "^1.2.18",
|
||||
"dedent": "^1.6.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^16.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-sh": "^0.18.0",
|
||||
"prettier-plugin-terraform-formatter": "^1.2.1",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.8.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@reteps/dockerfmt": ["@reteps/dockerfmt@0.3.6", "", {}, "sha512-Tb5wIMvBf/nLejTQ61krK644/CEMB/cpiaIFXqGApfGqO3GwcR3qnI0DbmkFVCl2OyEp8LnLX3EkucoL0+tbFg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.14", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||
|
||||
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
|
||||
|
||||
"gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
|
||||
|
||||
"is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
||||
|
||||
"js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
|
||||
|
||||
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||
|
||||
"marked": ["marked@16.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-MUKMXDjsD/eptB7GPzxo4xcnLS6oo7/RHimUMHEDRhUooPwmN9BEpMl7AEOJv3bmso169wHI2wUF9VQgL7zfmA=="],
|
||||
|
||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||
|
||||
"prettier-plugin-sh": ["prettier-plugin-sh@0.18.0", "", { "dependencies": { "@reteps/dockerfmt": "^0.3.6", "sh-syntax": "^0.5.8" }, "peerDependencies": { "prettier": "^3.6.0" } }, "sha512-cW1XL27FOJQ/qGHOW6IHwdCiNWQsAgK+feA8V6+xUTaH0cD3Mh+tFAtBvEEWvuY6hTDzRV943Fzeii+qMOh7nQ=="],
|
||||
|
||||
"prettier-plugin-terraform-formatter": ["prettier-plugin-terraform-formatter@1.2.1", "", { "peerDependencies": { "prettier": ">= 1.16.0" }, "optionalPeers": ["prettier"] }, "sha512-rdzV61Bs/Ecnn7uAS/vL5usTX8xUWM+nQejNLZxt3I1kJH5WSeLEmq7LYu1wCoEQF+y7Uv1xGvPRfl3lIe6+tA=="],
|
||||
|
||||
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||
|
||||
"sh-syntax": ["sh-syntax@0.5.8", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-JfVoxf4FxQI5qpsPbkHhZo+n6N9YMJobyl4oGEUBb/31oQYlgTjkXQD8PBiafS2UbWoxrTO0Z5PJUBXEPAG1Zw=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
}
|
||||
}
|
||||
@@ -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.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/crypto v0.11.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/term v0.10.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.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/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
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=
|
||||
|
||||
@@ -8,17 +8,16 @@
|
||||
"update-version": "./update-version.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.18",
|
||||
"bun-types": "^1.2.18",
|
||||
"dedent": "^1.6.0",
|
||||
"@types/bun": "^1.2.9",
|
||||
"bun-types": "^1.1.23",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^16.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-sh": "^0.18.0",
|
||||
"marked": "^12.0.2",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-sh": "^0.13.1",
|
||||
"prettier-plugin-terraform-formatter": "^1.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"prettier": {
|
||||
"plugins": [
|
||||
@@ -26,4 +25,4 @@
|
||||
"prettier-plugin-terraform-formatter"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="160" height="160" fill="#090B0B"/>
|
||||
<path d="M35 79.8451C35 67.7235 45.4217 60 59.7516 60C74.0815 60 82.1149 66.7044 82.3863 76.5733L70.0105 76.9488C69.6848 71.478 64.7725 67.8844 59.7516 67.9917C52.8581 68.1257 47.7558 72.6579 47.7558 79.8451C47.7558 87.0322 52.8581 91.4839 59.7516 91.4839C64.7725 91.4839 69.5762 88.0513 70.119 82.5805L82.4948 82.8486C82.1692 92.8784 73.6472 99.6901 59.7516 99.6901C45.856 99.6901 35 91.913 35 79.8451Z" fill="white"/>
|
||||
<path d="M88.0795 62.3769C89.3463 61.1103 91.1109 60.7075 92.934 60.8066C94.7578 60.9058 96.8171 61.5088 98.9613 62.4736C101.042 63.4099 103.274 64.7207 105.547 66.3466C107.821 64.721 110.052 63.4108 112.132 62.4745C114.277 61.5095 116.337 60.9057 118.162 60.8066C119.985 60.7075 121.749 61.1102 123.016 62.3769C124.282 63.6436 124.685 65.4084 124.586 67.2314C124.487 69.0554 123.882 71.1151 122.917 73.2597C121.981 75.3406 120.67 77.571 119.044 79.8447C120.67 82.1183 121.981 84.3488 122.917 86.4296C123.882 88.5741 124.487 90.634 124.586 92.4579C124.685 94.2808 124.282 96.0457 123.016 97.3124C121.749 98.5791 119.985 98.9818 118.162 98.8828C116.337 98.7836 114.277 98.1808 112.132 97.2158C110.051 96.2794 107.82 94.9687 105.546 93.3427C103.273 94.9685 101.042 96.2795 98.9613 97.2158C96.817 98.1805 94.7578 98.7845 92.934 98.8837C91.111 98.9828 89.3462 98.5799 88.0795 97.3134C86.8128 96.0467 86.4101 94.2821 86.5092 92.4589C86.6083 90.6349 87.2113 88.5752 88.1762 86.4306C89.1125 84.3496 90.4232 82.1185 92.0492 79.8447C90.4234 77.571 89.1125 75.3405 88.1762 73.2597C87.2113 71.1152 86.6084 69.0553 86.5092 67.2314C86.4101 65.4083 86.8128 63.6437 88.0795 62.3769ZM93.9565 82.3495C92.6833 84.208 91.6584 86.0034 90.9125 87.6611C90.0348 89.6118 89.5764 91.2963 89.5043 92.622C89.4323 93.9484 89.7487 94.7404 90.2006 95.1923C90.6526 95.6441 91.4447 95.9597 92.7709 95.8876C94.0964 95.8155 95.7805 95.3579 97.7309 94.4804C99.3887 93.7344 101.184 92.7078 103.042 91.4345C101.438 90.138 99.8307 88.6984 98.2621 87.1298C96.6935 85.5612 95.253 83.954 93.9565 82.3495ZM117.137 82.3486C115.84 83.9533 114.4 85.5609 112.831 87.1298C111.263 88.6984 109.656 90.138 108.051 91.4345C109.91 92.708 111.706 93.7344 113.364 94.4804C115.314 95.358 116.999 95.8156 118.325 95.8876C119.651 95.9596 120.442 95.6432 120.894 95.1913C121.346 94.7393 121.662 93.9474 121.59 92.621C121.518 91.2955 121.06 89.6114 120.182 87.6611C119.436 86.0031 118.411 84.2074 117.137 82.3486ZM105.547 70.0937C103.831 71.4383 102.091 72.9741 100.383 74.6816C98.6759 76.389 97.1398 78.1289 95.7953 79.8447C97.1397 81.5604 98.6759 83.3004 100.383 85.0078C102.09 86.715 103.831 88.2512 105.546 89.5956C107.262 88.2512 109.003 86.7161 110.71 85.0087C112.418 83.3014 113.953 81.5604 115.297 79.8447C113.953 78.1291 112.418 76.3888 110.71 74.6816C109.003 72.9743 107.263 71.4381 105.547 70.0937ZM107.481 81.582H104.481V77.582H107.481V81.582ZM92.7709 63.8017C91.4446 63.7296 90.6526 64.0462 90.2006 64.498C89.7487 64.95 89.4322 65.7419 89.5043 67.0683C89.5764 68.3939 90.0348 70.0787 90.9125 72.0292C91.6583 73.6867 92.6835 75.4816 93.9565 77.3398C95.2529 75.7354 96.6936 74.129 98.2621 72.5605C99.831 70.9916 101.438 69.5505 103.042 68.2538C101.184 66.9808 99.3885 65.9558 97.7309 65.2099C95.7805 64.3323 94.0965 63.8738 92.7709 63.8017ZM118.325 63.8027C116.999 63.8747 115.314 64.3322 113.364 65.2099C111.706 65.9558 109.91 66.9805 108.051 68.2538C109.656 69.5504 111.263 70.9908 112.831 72.5595C114.4 74.128 115.84 75.7355 117.136 77.3398C118.41 75.481 119.436 73.6861 120.182 72.0283C121.06 70.0779 121.518 68.3938 121.59 67.0683C121.662 65.7421 121.346 64.95 120.894 64.498C120.442 64.0463 119.651 63.7307 118.325 63.8027Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 975 KiB |
@@ -1,15 +0,0 @@
|
||||
---
|
||||
display_name: Coder Labs
|
||||
bio: Collection of example templates and modules for Coder. Designed for reference, not production use.
|
||||
github: coder
|
||||
avatar: ./.images/avatar.svg
|
||||
linkedin: https://www.linkedin.com/company/coderhq
|
||||
website: https://discord.gg/coder
|
||||
status: community
|
||||
---
|
||||
|
||||
å
|
||||
|
||||
# Coder Labs
|
||||
|
||||
Collection of example templates and modules for Coder. Designed for reference, not production use.
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
display_name: Docker Build
|
||||
description: Build Docker containers from Dockerfile as Coder workspaces
|
||||
icon: ../../../../.icons/docker.svg
|
||||
verified: true
|
||||
tags: [docker, container, dockerfile]
|
||||
---
|
||||
|
||||
# Remote Development on Docker Containers (Build from Dockerfile)
|
||||
|
||||
Build and provision Docker containers from a Dockerfile as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
|
||||
|
||||
This template builds a custom Docker image from the included Dockerfile, allowing you to customize the development environment by modifying the Dockerfile rather than using a pre-built image.
|
||||
|
||||
<!-- TODO: Add screenshot -->
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Infrastructure
|
||||
|
||||
The VM you run Coder on must have a running Docker socket and the `coder` user must be added to the Docker group:
|
||||
|
||||
```sh
|
||||
# Add coder user to Docker group
|
||||
sudo adduser coder docker
|
||||
|
||||
# Restart Coder server
|
||||
sudo systemctl restart coder
|
||||
|
||||
# Test Docker
|
||||
sudo -u coder docker ps
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
This template provisions the following resources:
|
||||
|
||||
- Docker image (built from Dockerfile and kept locally)
|
||||
- Docker container pod (ephemeral)
|
||||
- Docker volume (persistent on `/home/coder`)
|
||||
|
||||
This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the `build/Dockerfile`. Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles.
|
||||
|
||||
> [!NOTE]
|
||||
> This template is designed to be a starting point! Edit the Terraform and Dockerfile to extend the template to support your use case.
|
||||
|
||||
### Editing the image
|
||||
|
||||
Edit the `build/Dockerfile` and run `coder templates push` to update workspaces. The image will be rebuilt automatically when the Dockerfile changes.
|
||||
|
||||
## Difference from the standard Docker template
|
||||
|
||||
The main difference between this template and the standard Docker template is:
|
||||
|
||||
- **Standard Docker template**: Uses a pre-built image (e.g., `codercom/enterprise-base:ubuntu`)
|
||||
- **Docker Build template**: Builds a custom image from the included `build/Dockerfile`
|
||||
|
||||
This allows for more customization of the development environment while maintaining the same workspace functionality.
|
||||
@@ -1,18 +0,0 @@
|
||||
FROM ubuntu
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
curl \
|
||||
git \
|
||||
golang \
|
||||
sudo \
|
||||
vim \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG USER=coder
|
||||
RUN useradd --groups sudo --no-create-home --shell /bin/bash ${USER} \
|
||||
&& echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \
|
||||
&& chmod 0440 /etc/sudoers.d/${USER}
|
||||
USER ${USER}
|
||||
WORKDIR /home/${USER}
|
||||
@@ -1,222 +0,0 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
username = data.coder_workspace_owner.me.name
|
||||
}
|
||||
|
||||
variable "docker_socket" {
|
||||
default = ""
|
||||
description = "(Optional) Docker socket URI"
|
||||
type = string
|
||||
}
|
||||
|
||||
provider "docker" {
|
||||
# Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default
|
||||
host = var.docker_socket != "" ? var.docker_socket : null
|
||||
}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
arch = data.coder_provisioner.me.arch
|
||||
os = "linux"
|
||||
startup_script = <<-EOT
|
||||
set -e
|
||||
|
||||
# Prepare user home with default files on first start.
|
||||
if [ ! -f ~/.init_done ]; then
|
||||
cp -rT /etc/skel ~
|
||||
touch ~/.init_done
|
||||
fi
|
||||
|
||||
# Install the latest code-server.
|
||||
# Append "--version x.x.x" to install a specific version of code-server.
|
||||
curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server
|
||||
|
||||
# Start code-server in the background.
|
||||
/tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &
|
||||
EOT
|
||||
|
||||
# These environment variables allow you to make Git commits right away after creating a
|
||||
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
|
||||
# You can remove this block if you'd prefer to configure Git manually or using
|
||||
# dotfiles. (see docs/dotfiles.md)
|
||||
env = {
|
||||
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
|
||||
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
|
||||
}
|
||||
|
||||
# The following metadata blocks are optional. They are used to display
|
||||
# information about your workspace in the dashboard. You can remove them
|
||||
# if you don't want to display any information.
|
||||
# For basic resources, you can use the `coder stat` command.
|
||||
# If you need more control, you can write your own script.
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Home Disk"
|
||||
key = "3_home_disk"
|
||||
script = "coder stat disk --path $${HOME}"
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage (Host)"
|
||||
key = "4_cpu_usage_host"
|
||||
script = "coder stat cpu --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory Usage (Host)"
|
||||
key = "5_mem_usage_host"
|
||||
script = "coder stat mem --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Load Average (Host)"
|
||||
key = "6_load_host"
|
||||
# get load avg scaled by number of cores
|
||||
script = <<EOT
|
||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||
EOT
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Swap Usage (Host)"
|
||||
key = "7_swap_host"
|
||||
script = <<EOT
|
||||
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||
EOT
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_app" "code-server" {
|
||||
agent_id = coder_agent.main.id
|
||||
slug = "code-server"
|
||||
display_name = "code-server"
|
||||
url = "http://localhost:13337/?folder=/home/${local.username}"
|
||||
icon = "/icon/code.svg"
|
||||
subdomain = false
|
||||
share = "owner"
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:13337/healthz"
|
||||
interval = 5
|
||||
threshold = 6
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_volume" "home_volume" {
|
||||
name = "coder-${data.coder_workspace.me.id}-home"
|
||||
# Protect the volume from being deleted due to changes in attributes.
|
||||
lifecycle {
|
||||
ignore_changes = all
|
||||
}
|
||||
# Add labels in Docker to keep track of orphan resources.
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
labels {
|
||||
label = "coder.owner_id"
|
||||
value = data.coder_workspace_owner.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_id"
|
||||
value = data.coder_workspace.me.id
|
||||
}
|
||||
# This field becomes outdated if the workspace is renamed but can
|
||||
# be useful for debugging or cleaning out dangling volumes.
|
||||
labels {
|
||||
label = "coder.workspace_name_at_creation"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_image" "main" {
|
||||
name = "coder-${data.coder_workspace.me.id}"
|
||||
build {
|
||||
context = "./build"
|
||||
build_args = {
|
||||
USER = local.username
|
||||
}
|
||||
}
|
||||
triggers = {
|
||||
dir_sha1 = sha1(join("", [for f in fileset(path.module, "build/*") : filesha1(f)]))
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_container" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
image = docker_image.main.name
|
||||
# Uses lower() to avoid Docker restriction on container names.
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||
# Hostname makes the shell more user friendly: coder@my-workspace:~$
|
||||
hostname = data.coder_workspace.me.name
|
||||
# Use the docker gateway if the access URL is 127.0.0.1
|
||||
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
|
||||
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
|
||||
host {
|
||||
host = "host.docker.internal"
|
||||
ip = "host-gateway"
|
||||
}
|
||||
volumes {
|
||||
container_path = "/home/${local.username}"
|
||||
volume_name = docker_volume.home_volume.name
|
||||
read_only = false
|
||||
}
|
||||
|
||||
# Add labels in Docker to keep track of orphan resources.
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
labels {
|
||||
label = "coder.owner_id"
|
||||
value = data.coder_workspace_owner.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_id"
|
||||
value = data.coder_workspace.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_name"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
---
|
||||
display_name: Tasks on Docker
|
||||
description: Run Coder Tasks on Docker with an example application
|
||||
icon: ../../../../.icons/tasks.svg
|
||||
maintainer_github: coder-labs
|
||||
verified: false
|
||||
tags: [docker, container, ai, tasks]
|
||||
---
|
||||
|
||||
# Run Coder Tasks on Docker
|
||||
|
||||
This is an example template for running [Coder Tasks](https://coder.com/docs/ai-coder/tasks), Claude Code, along with a [real world application](https://realworld-docs.netlify.app/).
|
||||
|
||||

|
||||
|
||||
This is a fantastic starting point for working with AI agents with Coder Tasks. Try prompts such as:
|
||||
|
||||
- "Make the background color blue"
|
||||
- "Add a dark mode"
|
||||
- "Rewrite the entire backend in Go"
|
||||
|
||||
## Included in this template
|
||||
|
||||
This template is designed to be an example and a reference for building other templates with Coder Tasks. You can always run Coder Tasks on different infrastructure (e.g. as on Kubernetes, VMs) and with your own GitHub repositories, MCP servers, images, etc.
|
||||
|
||||
Additionally, this template uses our [Claude Code](https://registry.coder.com/modules/coder/claude-code) module, but [other agents](https://registry.coder.com/modules?search=tag%3Aagent) or even [custom agents](https://coder.com/docs/ai-coder/custom-agents) can be used in its place.
|
||||
|
||||
This template uses a [Workspace Preset](https://coder.com/docs/admin/templates/extending-templates/parameters#workspace-presets) that pre-defines:
|
||||
|
||||
- Universal Container Image (e.g. contains Node.js, Java, Python, Ruby, etc)
|
||||
- MCP servers (desktop-commander for long-running logs, playwright for previewing changes)
|
||||
- System prompt and [repository](https://github.com/coder-contrib/realworld-django-rest-framework-angular) for the AI agent
|
||||
- Startup script to initialize the repository and start the development server
|
||||
|
||||
## Add this template to your Coder deployment
|
||||
|
||||
You can also add this template to your Coder deployment and begin tinkering right away!
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Coder installed (see [our docs](https://coder.com/docs/install)), ideally a Linux VM with Docker
|
||||
- Anthropic API Key (or access to Anthropic models via Bedrock or Vertex, see [Claude Code docs](https://docs.anthropic.com/en/docs/claude-code/third-party-integrations))
|
||||
- Access to a Docker socket
|
||||
- If on the local VM, ensure the `coder` user is added to the Docker group (docs)
|
||||
|
||||
```sh
|
||||
# Add coder user to Docker group
|
||||
sudo adduser coder docker
|
||||
|
||||
# Restart Coder server
|
||||
sudo systemctl restart coder
|
||||
|
||||
# Test Docker
|
||||
sudo -u coder docker ps
|
||||
```
|
||||
|
||||
- If on a remote VM, see the [Docker Terraform provider documentation](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs#remote-hosts) to configure a remote host
|
||||
|
||||
To import this template into Coder, first create a template from "Scratch" in the template editor.
|
||||
|
||||
Visit this URL for your Coder deployment:
|
||||
|
||||
```sh
|
||||
https://coder.example.com/templates/new?exampleId=scratch
|
||||
```
|
||||
|
||||
After creating the template, paste the contents from [main.tf](./main.tf) into the template editor and save.
|
||||
|
||||
Alternatively, you can use the Coder CLI to [push the template](https://coder.com/docs/reference/cli/templates_push)
|
||||
|
||||
```sh
|
||||
# Download the CLI
|
||||
curl -L https://coder.com/install.sh | sh
|
||||
|
||||
# Log in to your deployment
|
||||
coder login https://coder.example.com
|
||||
|
||||
# Clone the registry
|
||||
git clone https://github.com/coder/registry
|
||||
cd registry
|
||||
|
||||
# Navigate to this template
|
||||
cd registry/coder-labs/templates/tasks-docker
|
||||
|
||||
# Push the template
|
||||
coder templates push
|
||||
```
|
||||
@@ -1,426 +0,0 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
provider "docker" {}
|
||||
|
||||
# The Claude Code module does the automatic task reporting
|
||||
# Other agent modules: https://registry.coder.com/modules?search=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 = "2.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/projects"
|
||||
install_claude_code = true
|
||||
claude_code_version = "latest"
|
||||
order = 999
|
||||
|
||||
experiment_post_install_script = data.coder_parameter.setup_script.value
|
||||
|
||||
# This enables Coder Tasks
|
||||
experiment_report_tasks = true
|
||||
}
|
||||
|
||||
# You can also use a model provider, like AWS Bedrock or Vertex by replacing
|
||||
# this with the special env vars from the Claude Code docs.
|
||||
# see: https://docs.anthropic.com/en/docs/claude-code/third-party-integrations
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Generate one at: https://console.anthropic.com/settings/keys"
|
||||
sensitive = true
|
||||
}
|
||||
resource "coder_env" "anthropic_api_key" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_CLAUDE_API_KEY"
|
||||
value = var.anthropic_api_key
|
||||
}
|
||||
|
||||
# We are using presets to set the prompts, image, and set up instructions
|
||||
# See https://coder.com/docs/admin/templates/extending-templates/parameters#workspace-presets
|
||||
data "coder_workspace_preset" "default" {
|
||||
name = "Real World App: Angular + Django"
|
||||
default = true
|
||||
parameters = {
|
||||
"system_prompt" = <<-EOT
|
||||
-- Framing --
|
||||
You are a helpful assistant that can help with code. You are running inside a Coder Workspace and provide status updates to the user via Coder MCP. Stay on track, feel free to debug, but when the original plan fails, do not choose a different route/architecture without checking the user first.
|
||||
|
||||
-- Tool Selection --
|
||||
- playwright: previewing your changes after you made them
|
||||
to confirm it worked as expected
|
||||
- desktop-commander - use only for commands that keep running
|
||||
(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.
|
||||
|
||||
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.
|
||||
|
||||
EOT
|
||||
|
||||
"setup_script" = <<-EOT
|
||||
# Set up projects dir
|
||||
mkdir -p /home/coder/projects
|
||||
cd $HOME/projects
|
||||
|
||||
# Packages: Install additional packages
|
||||
sudo apt-get update && sudo apt-get install -y tmux
|
||||
if ! command -v google-chrome >/dev/null 2>&1; then
|
||||
yes | npx playwright install chrome
|
||||
fi
|
||||
|
||||
# MCP: Install and configure MCP Servers
|
||||
npm install -g @wonderwhy-er/desktop-commander
|
||||
claude mcp add playwright npx -- @playwright/mcp@latest --headless --isolated --no-sandbox
|
||||
claude mcp add desktop-commander desktop-commander
|
||||
|
||||
# Repo: Clone and pull changes from the git repository
|
||||
if [ ! -d "realworld-django-rest-framework-angular" ]; then
|
||||
git clone https://github.com/coder-contrib/realworld-django-rest-framework-angular.git
|
||||
else
|
||||
cd realworld-django-rest-framework-angular
|
||||
git fetch
|
||||
# Check for uncommitted changes
|
||||
if git diff-index --quiet HEAD -- && \
|
||||
[ -z "$(git status --porcelain --untracked-files=no)" ] && \
|
||||
[ -z "$(git log --branches --not --remotes)" ]; then
|
||||
echo "Repo is clean. Pulling latest changes..."
|
||||
git pull
|
||||
else
|
||||
echo "Repo has uncommitted or unpushed changes. Skipping pull."
|
||||
fi
|
||||
|
||||
cd ..
|
||||
fi
|
||||
|
||||
# Initialize: Start the development server
|
||||
cd realworld-django-rest-framework-angular && ./start-dev.sh
|
||||
EOT
|
||||
"preview_port" = "4200"
|
||||
"container_image" = "codercom/example-universal:ubuntu"
|
||||
"jetbrains_ide" = "PY"
|
||||
}
|
||||
|
||||
# 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
|
||||
# expiration_policy {
|
||||
# ttl = 86400 # Time (in seconds) after which unclaimed prebuilds are expired (1 day)
|
||||
# }
|
||||
# }
|
||||
}
|
||||
|
||||
# Advanced parameters (these are all set via preset)
|
||||
data "coder_parameter" "system_prompt" {
|
||||
name = "system_prompt"
|
||||
display_name = "System Prompt"
|
||||
type = "string"
|
||||
form_type = "textarea"
|
||||
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"
|
||||
type = "string"
|
||||
form_type = "textarea"
|
||||
description = "Script to run before running the agent"
|
||||
mutable = false
|
||||
}
|
||||
data "coder_parameter" "container_image" {
|
||||
name = "container_image"
|
||||
display_name = "Container Image"
|
||||
type = "string"
|
||||
default = "codercom/example-universal:ubuntu"
|
||||
mutable = false
|
||||
}
|
||||
data "coder_parameter" "preview_port" {
|
||||
name = "preview_port"
|
||||
display_name = "Preview Port"
|
||||
description = "The port the web app is running to preview in Tasks"
|
||||
type = "number"
|
||||
default = "3000"
|
||||
mutable = false
|
||||
}
|
||||
|
||||
# Other variables for Claude Code
|
||||
resource "coder_env" "claude_task_prompt" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_CLAUDE_TASK_PROMPT"
|
||||
value = data.coder_parameter.ai_prompt.value
|
||||
}
|
||||
resource "coder_env" "app_status_slug" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_APP_STATUS_SLUG"
|
||||
value = "claude-code"
|
||||
}
|
||||
resource "coder_env" "claude_system_prompt" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
|
||||
value = data.coder_parameter.system_prompt.value
|
||||
}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
arch = data.coder_provisioner.me.arch
|
||||
os = "linux"
|
||||
startup_script = <<-EOT
|
||||
set -e
|
||||
# Prepare user home with default files on first start.
|
||||
if [ ! -f ~/.init_done ]; then
|
||||
cp -rT /etc/skel ~
|
||||
touch ~/.init_done
|
||||
fi
|
||||
EOT
|
||||
|
||||
# These environment variables allow you to make Git commits right away after creating a
|
||||
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
|
||||
# You can remove this block if you'd prefer to configure Git manually or using
|
||||
# dotfiles. (see docs/dotfiles.md)
|
||||
env = {
|
||||
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
|
||||
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
|
||||
}
|
||||
|
||||
# The following metadata blocks are optional. They are used to display
|
||||
# information about your workspace in the dashboard. You can remove them
|
||||
# if you don't want to display any information.
|
||||
# For basic resources, you can use the `coder stat` command.
|
||||
# If you need more control, you can write your own script.
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Home Disk"
|
||||
key = "3_home_disk"
|
||||
script = "coder stat disk --path $${HOME}"
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage (Host)"
|
||||
key = "4_cpu_usage_host"
|
||||
script = "coder stat cpu --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory Usage (Host)"
|
||||
key = "5_mem_usage_host"
|
||||
script = "coder stat mem --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Load Average (Host)"
|
||||
key = "6_load_host"
|
||||
# get load avg scaled by number of cores
|
||||
script = <<EOT
|
||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||
EOT
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Swap Usage (Host)"
|
||||
key = "7_swap_host"
|
||||
script = <<EOT
|
||||
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||
EOT
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
# See https://registry.coder.com/modules/coder/code-server
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
folder = "/home/coder/projects"
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
|
||||
settings = {
|
||||
"workbench.colorTheme" : "Default Dark Modern"
|
||||
}
|
||||
|
||||
# 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
|
||||
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"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
|
||||
# JetBrains IDEs to make available for the user to select
|
||||
jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
|
||||
default = "IU"
|
||||
|
||||
# Default folder to open when starting a JetBrains IDE
|
||||
folder = "/home/coder/projects"
|
||||
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
version = "~> 1.0"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
order = 2
|
||||
}
|
||||
|
||||
resource "docker_volume" "home_volume" {
|
||||
name = "coder-${data.coder_workspace.me.id}-home"
|
||||
# Protect the volume from being deleted due to changes in attributes.
|
||||
lifecycle {
|
||||
ignore_changes = all
|
||||
}
|
||||
# Add labels in Docker to keep track of orphan resources.
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
labels {
|
||||
label = "coder.owner_id"
|
||||
value = data.coder_workspace_owner.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_id"
|
||||
value = data.coder_workspace.me.id
|
||||
}
|
||||
# This field becomes outdated if the workspace is renamed but can
|
||||
# be useful for debugging or cleaning out dangling volumes.
|
||||
labels {
|
||||
label = "coder.workspace_name_at_creation"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_app" "preview" {
|
||||
agent_id = coder_agent.main.id
|
||||
slug = "preview"
|
||||
display_name = "Preview your app"
|
||||
icon = "${data.coder_workspace.me.access_url}/emojis/1f50e.png"
|
||||
url = "http://localhost:${data.coder_parameter.preview_port.value}"
|
||||
share = "authenticated"
|
||||
subdomain = true
|
||||
open_in = "tab"
|
||||
order = 0
|
||||
healthcheck {
|
||||
url = "http://localhost:${data.coder_parameter.preview_port.value}/"
|
||||
interval = 5
|
||||
threshold = 15
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_container" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
image = data.coder_parameter.container_image.value
|
||||
# Uses lower() to avoid Docker restriction on container names.
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||
# Hostname makes the shell more user friendly: coder@my-workspace:~$
|
||||
hostname = data.coder_workspace.me.name
|
||||
user = "coder"
|
||||
# Use the docker gateway if the access URL is 127.0.0.1
|
||||
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
|
||||
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
|
||||
host {
|
||||
host = "host.docker.internal"
|
||||
ip = "host-gateway"
|
||||
}
|
||||
volumes {
|
||||
container_path = "/home/coder"
|
||||
volume_name = docker_volume.home_volume.name
|
||||
read_only = false
|
||||
}
|
||||
|
||||
# Add labels in Docker to keep track of orphan resources.
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
labels {
|
||||
label = "coder.owner_id"
|
||||
value = data.coder_workspace_owner.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_id"
|
||||
value = data.coder_workspace.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_name"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 40 KiB |
@@ -1,54 +0,0 @@
|
||||
---
|
||||
display_name: AgentAPI
|
||||
description: Building block for modules that need to run an agentapi server
|
||||
icon: ../../../../.icons/coder.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [internal]
|
||||
---
|
||||
|
||||
# AgentAPI
|
||||
|
||||
The AgentAPI module is a building block for modules that need to run an agentapi server. It is intended primarily for internal use by Coder to create modules compatible with Tasks.
|
||||
|
||||
We do not recommend using this module directly. Instead, please consider using one of our [Tasks-compatible AI agent modules](https://registry.coder.com/modules?search=tag%3Atasks).
|
||||
|
||||
```tf
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.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 = "Goose"
|
||||
cli_app_slug = "goose-cli"
|
||||
cli_app_display_name = "Goose CLI"
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
start_script = local.start_script
|
||||
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_PROVIDER='${var.goose_provider}' \
|
||||
ARG_MODEL='${var.goose_model}' \
|
||||
ARG_GOOSE_CONFIG="$(echo -n '${base64encode(local.combined_extensions)}' | base64 -d)" \
|
||||
ARG_INSTALL='${var.install_goose}' \
|
||||
ARG_GOOSE_VERSION='${var.goose_version}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
## For module developers
|
||||
|
||||
For a complete example of how to use this module, see the [goose module](https://github.com/coder/registry/blob/main/registry/coder/modules/goose/main.tf).
|
||||
@@ -1,151 +0,0 @@
|
||||
import {
|
||||
test,
|
||||
afterEach,
|
||||
expect,
|
||||
describe,
|
||||
setDefaultTimeout,
|
||||
beforeAll,
|
||||
} from "bun:test";
|
||||
import { execContainer, readFileContainer, runTerraformInit } from "~test";
|
||||
import {
|
||||
loadTestFile,
|
||||
writeExecutable,
|
||||
setup as setupUtil,
|
||||
execModuleScript,
|
||||
expectAgentAPIStarted,
|
||||
} from "./test-util";
|
||||
|
||||
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) {
|
||||
try {
|
||||
await cleanup();
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
interface SetupProps {
|
||||
skipAgentAPIMock?: boolean;
|
||||
moduleVariables?: Record<string, string>;
|
||||
}
|
||||
|
||||
const moduleDirName = ".agentapi-module";
|
||||
|
||||
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const projectDir = "/home/coder/project";
|
||||
const { id } = await setupUtil({
|
||||
moduleVariables: {
|
||||
experiment_report_tasks: "true",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
web_app_display_name: "AgentAPI Web",
|
||||
web_app_slug: "agentapi-web",
|
||||
web_app_icon: "/icon/coder.svg",
|
||||
cli_app_display_name: "AgentAPI CLI",
|
||||
cli_app_slug: "agentapi-cli",
|
||||
agentapi_version: "latest",
|
||||
module_dir_name: moduleDirName,
|
||||
start_script: await loadTestFile(import.meta.dir, "agentapi-start.sh"),
|
||||
folder: projectDir,
|
||||
...props?.moduleVariables,
|
||||
},
|
||||
registerCleanup,
|
||||
projectDir,
|
||||
skipAgentAPIMock: props?.skipAgentAPIMock,
|
||||
moduleDir: import.meta.dir,
|
||||
});
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/aiagent",
|
||||
content: await loadTestFile(import.meta.dir, "ai-agent-mock.js"),
|
||||
});
|
||||
return { id };
|
||||
};
|
||||
|
||||
// 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("agentapi", async () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
test("happy-path", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("custom-port", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
agentapi_port: "3827",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id, 3827);
|
||||
});
|
||||
|
||||
test("pre-post-install-scripts", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
pre_install_script: `#!/bin/bash\necho "pre-install"`,
|
||||
install_script: `#!/bin/bash\necho "install"`,
|
||||
post_install_script: `#!/bin/bash\necho "post-install"`,
|
||||
},
|
||||
});
|
||||
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
|
||||
const preInstallLog = await readFileContainer(
|
||||
id,
|
||||
`/home/coder/${moduleDirName}/pre_install.log`,
|
||||
);
|
||||
const installLog = await readFileContainer(
|
||||
id,
|
||||
`/home/coder/${moduleDirName}/install.log`,
|
||||
);
|
||||
const postInstallLog = await readFileContainer(
|
||||
id,
|
||||
`/home/coder/${moduleDirName}/post_install.log`,
|
||||
);
|
||||
|
||||
expect(preInstallLog).toContain("pre-install");
|
||||
expect(installLog).toContain("install");
|
||||
expect(postInstallLog).toContain("post-install");
|
||||
});
|
||||
|
||||
test("install-agentapi", async () => {
|
||||
const { id } = await setup({ skipAgentAPIMock: true });
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
const respAgentAPI = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"agentapi --version",
|
||||
]);
|
||||
expect(respAgentAPI.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,213 +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."
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
variable "web_app_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 "web_app_group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "web_app_icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "The display name of the web app."
|
||||
}
|
||||
|
||||
variable "web_app_slug" {
|
||||
type = string
|
||||
description = "The slug of the web app."
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to run AgentAPI in."
|
||||
default = "/home/coder"
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create the CLI workspace app."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "cli_app_order" {
|
||||
type = number
|
||||
description = "The order of the CLI workspace app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "cli_app_group" {
|
||||
type = string
|
||||
description = "The group of the CLI workspace app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "cli_app_icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
default = "/icon/claude.svg"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "The display name of the CLI workspace app."
|
||||
}
|
||||
|
||||
variable "cli_app_slug" {
|
||||
type = string
|
||||
description = "The slug of the CLI workspace app."
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing the agent used by AgentAPI."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "install_script" {
|
||||
type = string
|
||||
description = "Script to install the agent used by AgentAPI."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing the agent used by AgentAPI."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "start_script" {
|
||||
type = string
|
||||
description = "Script that starts AgentAPI."
|
||||
}
|
||||
|
||||
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.2.3"
|
||||
}
|
||||
|
||||
variable "agentapi_port" {
|
||||
type = number
|
||||
description = "The port used by AgentAPI."
|
||||
default = 3284
|
||||
}
|
||||
|
||||
variable "module_dir_name" {
|
||||
type = string
|
||||
description = "Name of the subdirectory in the home directory for module files."
|
||||
}
|
||||
|
||||
|
||||
locals {
|
||||
# we always trim the slash for consistency
|
||||
workdir = trimsuffix(var.folder, "/")
|
||||
encoded_pre_install_script = var.pre_install_script != null ? base64encode(var.pre_install_script) : ""
|
||||
encoded_install_script = var.install_script != null ? base64encode(var.install_script) : ""
|
||||
encoded_post_install_script = var.post_install_script != null ? base64encode(var.post_install_script) : ""
|
||||
agentapi_start_script_b64 = base64encode(var.start_script)
|
||||
agentapi_wait_for_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh"))
|
||||
main_script = file("${path.module}/scripts/main.sh")
|
||||
}
|
||||
|
||||
resource "coder_script" "agentapi" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Install and start AgentAPI"
|
||||
icon = var.web_app_icon
|
||||
script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.main_script)}' | base64 -d > /tmp/main.sh
|
||||
chmod +x /tmp/main.sh
|
||||
|
||||
ARG_MODULE_DIR_NAME='${var.module_dir_name}' \
|
||||
ARG_WORKDIR="$(echo -n '${base64encode(local.workdir)}' | base64 -d)" \
|
||||
ARG_PRE_INSTALL_SCRIPT="$(echo -n '${local.encoded_pre_install_script}' | base64 -d)" \
|
||||
ARG_INSTALL_SCRIPT="$(echo -n '${local.encoded_install_script}' | base64 -d)" \
|
||||
ARG_INSTALL_AGENTAPI='${var.install_agentapi}' \
|
||||
ARG_AGENTAPI_VERSION='${var.agentapi_version}' \
|
||||
ARG_START_SCRIPT="$(echo -n '${local.agentapi_start_script_b64}' | base64 -d)" \
|
||||
ARG_WAIT_FOR_START_SCRIPT="$(echo -n '${local.agentapi_wait_for_start_script_b64}' | base64 -d)" \
|
||||
ARG_POST_INSTALL_SCRIPT="$(echo -n '${local.encoded_post_install_script}' | base64 -d)" \
|
||||
ARG_AGENTAPI_PORT='${var.agentapi_port}' \
|
||||
/tmp/main.sh
|
||||
EOT
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "agentapi_web" {
|
||||
slug = var.web_app_slug
|
||||
display_name = var.web_app_display_name
|
||||
agent_id = var.agent_id
|
||||
url = "http://localhost:${var.agentapi_port}/"
|
||||
icon = var.web_app_icon
|
||||
order = var.web_app_order
|
||||
group = var.web_app_group
|
||||
subdomain = true
|
||||
healthcheck {
|
||||
url = "http://localhost:${var.agentapi_port}/status"
|
||||
interval = 3
|
||||
threshold = 20
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_app" "agentapi_cli" {
|
||||
count = var.cli_app ? 1 : 0
|
||||
|
||||
slug = var.cli_app_slug
|
||||
display_name = var.cli_app_display_name
|
||||
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.cli_app_icon
|
||||
order = var.cli_app_order
|
||||
group = var.cli_app_group
|
||||
}
|
||||
|
||||
resource "coder_ai_task" "agentapi" {
|
||||
sidebar_app {
|
||||
id = coder_app.agentapi_web.id
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
port=${1:-3284}
|
||||
|
||||
# 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 $port..."
|
||||
for i in $(seq 1 150); do
|
||||
for j in $(seq 1 3); do
|
||||
sleep 0.1
|
||||
if curl -fs -o /dev/null "http://localhost:$port/status"; then
|
||||
echo "agentapi response received ($j/3)"
|
||||
else
|
||||
echo "agentapi server not responding ($i/15)"
|
||||
continue 2
|
||||
fi
|
||||
done
|
||||
agentapi_started=true
|
||||
break
|
||||
done
|
||||
|
||||
if [ "$agentapi_started" != "true" ]; then
|
||||
echo "Error: agentapi server did not start on port $port after 15 seconds."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "agentapi server started on port $port."
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
set -o nounset
|
||||
MODULE_DIR_NAME="$ARG_MODULE_DIR_NAME"
|
||||
WORKDIR="$ARG_WORKDIR"
|
||||
PRE_INSTALL_SCRIPT="$ARG_PRE_INSTALL_SCRIPT"
|
||||
INSTALL_SCRIPT="$ARG_INSTALL_SCRIPT"
|
||||
INSTALL_AGENTAPI="$ARG_INSTALL_AGENTAPI"
|
||||
AGENTAPI_VERSION="$ARG_AGENTAPI_VERSION"
|
||||
START_SCRIPT="$ARG_START_SCRIPT"
|
||||
WAIT_FOR_START_SCRIPT="$ARG_WAIT_FOR_START_SCRIPT"
|
||||
POST_INSTALL_SCRIPT="$ARG_POST_INSTALL_SCRIPT"
|
||||
AGENTAPI_PORT="$ARG_AGENTAPI_PORT"
|
||||
set +o nounset
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
module_path="$HOME/${MODULE_DIR_NAME}"
|
||||
mkdir -p "$module_path/scripts"
|
||||
|
||||
if [ ! -d "${WORKDIR}" ]; then
|
||||
echo "Warning: The specified folder '${WORKDIR}' does not exist."
|
||||
echo "Creating the folder..."
|
||||
mkdir -p "${WORKDIR}"
|
||||
echo "Folder created successfully."
|
||||
fi
|
||||
if [ -n "${PRE_INSTALL_SCRIPT}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo -n "${PRE_INSTALL_SCRIPT}" >"$module_path/pre_install.sh"
|
||||
chmod +x "$module_path/pre_install.sh"
|
||||
"$module_path/pre_install.sh" 2>&1 | tee "$module_path/pre_install.log"
|
||||
fi
|
||||
|
||||
echo "Running install script..."
|
||||
echo -n "${INSTALL_SCRIPT}" >"$module_path/install.sh"
|
||||
chmod +x "$module_path/install.sh"
|
||||
"$module_path/install.sh" 2>&1 | tee "$module_path/install.log"
|
||||
|
||||
# Install AgentAPI if enabled
|
||||
if [ "${INSTALL_AGENTAPI}" = "true" ]; then
|
||||
echo "Installing AgentAPI..."
|
||||
arch=$(uname -m)
|
||||
if [ "$arch" = "x86_64" ]; then
|
||||
binary_name="agentapi-linux-amd64"
|
||||
elif [ "$arch" = "aarch64" ]; then
|
||||
binary_name="agentapi-linux-arm64"
|
||||
else
|
||||
echo "Error: Unsupported architecture: $arch"
|
||||
exit 1
|
||||
fi
|
||||
if [ "${AGENTAPI_VERSION}" = "latest" ]; then
|
||||
# for the latest release the download URL pattern is different than for tagged releases
|
||||
# https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases
|
||||
download_url="https://github.com/coder/agentapi/releases/latest/download/$binary_name"
|
||||
else
|
||||
download_url="https://github.com/coder/agentapi/releases/download/${AGENTAPI_VERSION}/$binary_name"
|
||||
fi
|
||||
curl \
|
||||
--retry 5 \
|
||||
--retry-delay 5 \
|
||||
--fail \
|
||||
--retry-all-errors \
|
||||
-L \
|
||||
-C - \
|
||||
-o agentapi \
|
||||
"$download_url"
|
||||
chmod +x agentapi
|
||||
sudo mv agentapi /usr/local/bin/agentapi
|
||||
fi
|
||||
if ! command_exists agentapi; then
|
||||
echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "${START_SCRIPT}" >"$module_path/scripts/agentapi-start.sh"
|
||||
echo -n "${WAIT_FOR_START_SCRIPT}" >"$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
chmod +x "$module_path/scripts/agentapi-start.sh"
|
||||
chmod +x "$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
|
||||
if [ -n "${POST_INSTALL_SCRIPT}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo -n "${POST_INSTALL_SCRIPT}" >"$module_path/post_install.sh"
|
||||
chmod +x "$module_path/post_install.sh"
|
||||
"$module_path/post_install.sh" 2>&1 | tee "$module_path/post_install.log"
|
||||
fi
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
cd "${WORKDIR}"
|
||||
nohup "$module_path/scripts/agentapi-start.sh" true "${AGENTAPI_PORT}" &>"$module_path/agentapi-start.log" &
|
||||
"$module_path/scripts/agentapi-wait-for-start.sh" "${AGENTAPI_PORT}"
|
||||
@@ -1,130 +0,0 @@
|
||||
import {
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
removeContainer,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
writeFileContainer,
|
||||
} from "~test";
|
||||
import path from "path";
|
||||
import { expect } from "bun:test";
|
||||
|
||||
export const setupContainer = async ({
|
||||
moduleDir,
|
||||
image,
|
||||
vars,
|
||||
}: {
|
||||
moduleDir: string;
|
||||
image?: string;
|
||||
vars?: Record<string, string>;
|
||||
}) => {
|
||||
const state = await runTerraformApply(moduleDir, {
|
||||
agent_id: "foo",
|
||||
...vars,
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
|
||||
return { id, coderScript, cleanup: () => removeContainer(id) };
|
||||
};
|
||||
|
||||
export const loadTestFile = async (
|
||||
moduleDir: string,
|
||||
...relativePath: [string, ...string[]]
|
||||
) => {
|
||||
return await Bun.file(
|
||||
path.join(moduleDir, "testdata", ...relativePath),
|
||||
).text();
|
||||
};
|
||||
|
||||
export 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"],
|
||||
);
|
||||
};
|
||||
|
||||
interface SetupProps {
|
||||
skipAgentAPIMock?: boolean;
|
||||
moduleDir: string;
|
||||
moduleVariables: Record<string, string>;
|
||||
projectDir?: string;
|
||||
registerCleanup: (cleanup: () => Promise<void>) => void;
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
export const setup = async (props: SetupProps): Promise<{ id: string }> => {
|
||||
const projectDir = props.projectDir ?? "/home/coder/project";
|
||||
const { id, coderScript, cleanup } = await setupContainer({
|
||||
moduleDir: props.moduleDir,
|
||||
vars: props.moduleVariables,
|
||||
});
|
||||
props.registerCleanup(cleanup);
|
||||
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
|
||||
if (!props?.skipAgentAPIMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/agentapi",
|
||||
content:
|
||||
props.agentapiMockScript ??
|
||||
(await loadTestFile(import.meta.dir, "agentapi-mock.js")),
|
||||
});
|
||||
}
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/home/coder/script.sh",
|
||||
content: coderScript.script,
|
||||
});
|
||||
return { id };
|
||||
};
|
||||
|
||||
export const expectAgentAPIStarted = async (
|
||||
id: string,
|
||||
port: number = 3284,
|
||||
) => {
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`curl -fs -o /dev/null "http://localhost:${port}/status"`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log("agentapi not started");
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
expect(resp.exitCode).toBe(0);
|
||||
};
|
||||
|
||||
export const execModuleScript = async (
|
||||
id: string,
|
||||
env?: Record<string, string>,
|
||||
) => {
|
||||
const envArgs = Object.entries(env ?? {})
|
||||
.map(([key, value]) => ["--env", `${key}=${value}`])
|
||||
.flat();
|
||||
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`,
|
||||
],
|
||||
envArgs,
|
||||
);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const http = require("http");
|
||||
const args = process.argv.slice(2);
|
||||
const portIdx = args.findIndex((arg) => arg === "--port") + 1;
|
||||
const port = portIdx ? args[portIdx] : 3284;
|
||||
|
||||
console.log(`starting server on port ${port}`);
|
||||
|
||||
http
|
||||
.createServer(function (_request, response) {
|
||||
response.writeHead(200);
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
status: "stable",
|
||||
}),
|
||||
);
|
||||
})
|
||||
.listen(port);
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
use_prompt=${1:-false}
|
||||
port=${2:-3284}
|
||||
|
||||
module_path="$HOME/.agentapi-module"
|
||||
log_file_path="$module_path/agentapi.log"
|
||||
|
||||
echo "using prompt: $use_prompt" >>/home/coder/test-agentapi-start.log
|
||||
echo "using port: $port" >>/home/coder/test-agentapi-start.log
|
||||
|
||||
agentapi server --port "$port" --term-width 67 --term-height 1190 -- \
|
||||
bash -c aiagent \
|
||||
>"$log_file_path" 2>&1
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const main = async () => {
|
||||
console.log("mocking an ai agent");
|
||||
// sleep for 30 minutes
|
||||
await new Promise((resolve) => setTimeout(resolve, 30 * 60 * 1000));
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -4,7 +4,7 @@ description: Run Claude Code in your workspace
|
||||
icon: ../../../../.icons/claude.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [agent, claude-code, ai, tasks]
|
||||
tags: [agent, claude-code, ai]
|
||||
---
|
||||
|
||||
# Claude Code
|
||||
@@ -14,7 +14,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.0.3"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -85,11 +85,11 @@ resource "coder_agent" "main" {
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.0.3"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
claude_code_version = "1.0.40"
|
||||
claude_code_version = "0.2.57"
|
||||
|
||||
# Enable experimental features
|
||||
experiment_report_tasks = true
|
||||
@@ -103,7 +103,7 @@ Run Claude Code as a standalone app in your workspace. This will install Claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.0.3"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -113,7 +113,3 @@ module "claude-code" {
|
||||
icon = "https://registry.npmmirror.com/@lobehub/icons-static-png/1.24.0/files/dark/claude-color.png"
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
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.
|
||||
|
||||
@@ -100,7 +100,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.2.3"
|
||||
default = "v0.2.2"
|
||||
}
|
||||
|
||||
locals {
|
||||
|
||||
@@ -19,7 +19,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -32,7 +32,7 @@ module "dotfiles" {
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -43,7 +43,7 @@ module "dotfiles" {
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
user = "root"
|
||||
}
|
||||
@@ -55,14 +55,14 @@ module "dotfiles" {
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
module "dotfiles-root" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
user = "root"
|
||||
dotfiles_uri = module.dotfiles.dotfiles_uri
|
||||
@@ -77,7 +77,7 @@ You can set a default dotfiles repository for all users by setting the `default_
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
default_dotfiles_uri = "https://github.com/coder/dotfiles"
|
||||
}
|
||||
|
||||
@@ -26,12 +26,6 @@ variable "agent_id" {
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "description" {
|
||||
type = string
|
||||
description = "A custom description for the dotfiles parameter. This is shown in the UI - and allows you to customize the instructions you give to your users."
|
||||
default = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace"
|
||||
}
|
||||
|
||||
variable "default_dotfiles_uri" {
|
||||
type = string
|
||||
description = "The default dotfiles URI if the workspace user does not provide one"
|
||||
@@ -70,7 +64,7 @@ data "coder_parameter" "dotfiles_uri" {
|
||||
display_name = "Dotfiles URL"
|
||||
order = var.coder_parameter_order
|
||||
default = var.default_dotfiles_uri
|
||||
description = var.description
|
||||
description = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace"
|
||||
mutable = true
|
||||
icon = "/icon/dotfiles.svg"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ A file browser for your workspace.
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -30,7 +30,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -42,7 +42,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
database_path = ".config/filebrowser.db"
|
||||
}
|
||||
@@ -54,7 +54,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = "main"
|
||||
subdomain = false
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("filebrowser", async () => {
|
||||
);
|
||||
|
||||
testBaseLine(output);
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
it("runs with database_path var", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
@@ -63,7 +63,7 @@ describe("filebrowser", async () => {
|
||||
database_path: ".config/filebrowser.db",
|
||||
});
|
||||
|
||||
const output = await executeScriptInContainer(
|
||||
const output = await await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/curl",
|
||||
"sh",
|
||||
@@ -71,20 +71,20 @@ describe("filebrowser", async () => {
|
||||
);
|
||||
|
||||
testBaseLine(output);
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
it("runs with folder var", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder/project",
|
||||
});
|
||||
const output = await executeScriptInContainer(
|
||||
const output = await await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/curl",
|
||||
"sh",
|
||||
"apk add bash",
|
||||
);
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
it("runs with subdomain=false", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
@@ -93,7 +93,7 @@ describe("filebrowser", async () => {
|
||||
subdomain: false,
|
||||
});
|
||||
|
||||
const output = await executeScriptInContainer(
|
||||
const output = await await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/curl",
|
||||
"sh",
|
||||
@@ -101,5 +101,5 @@ describe("filebrowser", async () => {
|
||||
);
|
||||
|
||||
testBaseLine(output);
|
||||
}, 15000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -97,6 +97,7 @@ resource "coder_script" "filebrowser" {
|
||||
LOG_PATH : var.log_path,
|
||||
PORT : var.port,
|
||||
FOLDER : var.folder,
|
||||
LOG_PATH : var.log_path,
|
||||
DB_PATH : var.database_path,
|
||||
SUBDOMAIN : var.subdomain,
|
||||
SERVER_BASE_PATH : local.server_base_path
|
||||
@@ -127,4 +128,3 @@ locals {
|
||||
url = "http://localhost:${var.port}${local.server_base_path}"
|
||||
healthcheck_url = "http://localhost:${var.port}${local.server_base_path}/health"
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export FB_DATABASE="${DB_PATH}"
|
||||
# Check if filebrowser db exists
|
||||
if [[ ! -f "${DB_PATH}" ]]; then
|
||||
filebrowser config init 2>&1 | tee -a ${LOG_PATH}
|
||||
filebrowser users add admin "coderPASSWORD" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH}
|
||||
filebrowser users add admin "" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH}
|
||||
fi
|
||||
|
||||
filebrowser config set --baseurl=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH}
|
||||
|
||||
@@ -15,7 +15,7 @@ This module allows you to automatically clone a repository by URL and skip if it
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
}
|
||||
@@ -29,7 +29,7 @@ module "git-clone" {
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
base_dir = "~/projects/coder"
|
||||
@@ -44,7 +44,7 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
}
|
||||
@@ -70,7 +70,7 @@ data "coder_parameter" "git_repo" {
|
||||
module "git_clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = data.coder_parameter.git_repo.value
|
||||
}
|
||||
@@ -104,7 +104,7 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.example.com/coder/coder/tree/feat/example"
|
||||
git_providers = {
|
||||
@@ -123,7 +123,7 @@ To GitLab clone with a specific branch like `feat/example`
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
|
||||
}
|
||||
@@ -135,7 +135,7 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
|
||||
git_providers = {
|
||||
@@ -156,7 +156,7 @@ For example, to clone the `feat/example` branch:
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
branch_name = "feat/example"
|
||||
@@ -165,8 +165,7 @@ module "git-clone" {
|
||||
|
||||
## Git clone with different destination folder
|
||||
|
||||
By default, the repository will be cloned into a folder matching the repository name.
|
||||
You can use the `folder_name` attribute to change the name of the destination folder to something else.
|
||||
By default, the repository will be cloned into a folder matching the repository name. You can use the `folder_name` attribute to change the name of the destination folder to something else.
|
||||
|
||||
For example, this will clone into the `~/projects/coder/coder-dev` folder:
|
||||
|
||||
@@ -174,28 +173,10 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder:
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
folder_name = "coder-dev"
|
||||
base_dir = "~/projects/coder"
|
||||
}
|
||||
```
|
||||
|
||||
## Git shallow clone
|
||||
|
||||
Limit the clone history to speed-up workspace startup by setting `depth`.
|
||||
|
||||
When `depth` is greater than `0` the module runs `git clone --depth <depth>`.
|
||||
If not defined, the default, `0`, performs a full clone.
|
||||
|
||||
```tf
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
depth = 1
|
||||
}
|
||||
```
|
||||
|
||||
@@ -56,12 +56,6 @@ variable "folder_name" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "depth" {
|
||||
description = "If > 0, perform a shallow clone using this depth."
|
||||
type = number
|
||||
default = 0
|
||||
}
|
||||
|
||||
locals {
|
||||
# Remove query parameters and fragments from the URL
|
||||
url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "")
|
||||
@@ -119,7 +113,6 @@ resource "coder_script" "git_clone" {
|
||||
CLONE_PATH = local.clone_path,
|
||||
REPO_URL : local.clone_url,
|
||||
BRANCH_NAME : local.branch_name,
|
||||
DEPTH = var.depth,
|
||||
})
|
||||
display_name = "Git Clone"
|
||||
icon = "/icon/git.svg"
|
||||
|
||||
@@ -5,7 +5,6 @@ CLONE_PATH="${CLONE_PATH}"
|
||||
BRANCH_NAME="${BRANCH_NAME}"
|
||||
# Expand home if it's specified!
|
||||
CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}"
|
||||
DEPTH="${DEPTH}"
|
||||
|
||||
# Check if the variable is empty...
|
||||
if [ -z "$REPO_URL" ]; then
|
||||
@@ -37,18 +36,10 @@ fi
|
||||
if [ -z "$(ls -A "$CLONE_PATH")" ]; then
|
||||
if [ -z "$BRANCH_NAME" ]; then
|
||||
echo "Cloning $REPO_URL to $CLONE_PATH..."
|
||||
if [ "$DEPTH" -gt 0 ]; then
|
||||
git clone --depth "$DEPTH" "$REPO_URL" "$CLONE_PATH"
|
||||
else
|
||||
git clone "$REPO_URL" "$CLONE_PATH"
|
||||
fi
|
||||
git clone "$REPO_URL" "$CLONE_PATH"
|
||||
else
|
||||
echo "Cloning $REPO_URL to $CLONE_PATH on branch $BRANCH_NAME..."
|
||||
if [ "$DEPTH" -gt 0 ]; then
|
||||
git clone --depth "$DEPTH" -b "$BRANCH_NAME" "$REPO_URL" "$CLONE_PATH"
|
||||
else
|
||||
git clone "$REPO_URL" -b "$BRANCH_NAME" "$CLONE_PATH"
|
||||
fi
|
||||
git clone "$REPO_URL" -b "$BRANCH_NAME" "$CLONE_PATH"
|
||||
fi
|
||||
else
|
||||
echo "$CLONE_PATH already exists and isn't empty, skipping clone!"
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Run Goose in your workspace
|
||||
icon: ../../../../.icons/goose.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [agent, goose, ai, tasks]
|
||||
tags: [agent, goose, ai]
|
||||
---
|
||||
|
||||
# Goose
|
||||
@@ -13,27 +13,36 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener
|
||||
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
goose_version = "v1.0.31"
|
||||
goose_provider = "anthropic"
|
||||
goose_model = "claude-3-5-sonnet-latest"
|
||||
agentapi_version = "latest"
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
goose_version = "v1.0.16"
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `screen` or `tmux` must be installed in your workspace to run Goose in the background
|
||||
- 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
|
||||
Your workspace must have `screen` or `tmux` installed to use the background session functionality.
|
||||
|
||||
### 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
|
||||
module "coder-login" {
|
||||
@@ -72,23 +81,37 @@ resource "coder_agent" "main" {
|
||||
EOT
|
||||
GOOSE_TASK_PROMPT = data.coder_parameter.ai_prompt.value
|
||||
|
||||
# An API key is required for experiment_auto_configure
|
||||
# See https://block.github.io/goose/docs/getting-started/providers
|
||||
ANTHROPIC_API_KEY = var.anthropic_api_key # or use a coder_parameter
|
||||
}
|
||||
}
|
||||
|
||||
module "goose" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
goose_version = "v1.0.31"
|
||||
agentapi_version = "latest"
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
goose_version = "v1.0.16"
|
||||
|
||||
goose_provider = "anthropic"
|
||||
goose_model = "claude-3-5-sonnet-latest"
|
||||
# Enable experimental features
|
||||
experiment_report_tasks = true
|
||||
|
||||
# Run Goose in the background with screen (pick one: screen or tmux)
|
||||
experiment_use_screen = true
|
||||
# experiment_use_tmux = true # Alternative: use tmux instead of screen
|
||||
|
||||
# Optional: customize the session name (defaults to "goose")
|
||||
# session_name = "goose-session"
|
||||
|
||||
# Avoid configuring Goose manually
|
||||
experiment_auto_configure = true
|
||||
|
||||
# Required for experiment_auto_configure
|
||||
experiment_goose_provider = "anthropic"
|
||||
experiment_goose_model = "claude-3-5-sonnet-latest"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -100,11 +123,11 @@ You can extend Goose's capabilities by adding custom extensions. For example, to
|
||||
module "goose" {
|
||||
# ... other configuration ...
|
||||
|
||||
pre_install_script = <<-EOT
|
||||
experiment_pre_install_script = <<-EOT
|
||||
npm i -g @wonderwhy-er/desktop-commander@latest
|
||||
EOT
|
||||
|
||||
additional_extensions = <<-EOT
|
||||
experiment_additional_extensions = <<-EOT
|
||||
desktop-commander:
|
||||
args: []
|
||||
cmd: desktop-commander
|
||||
@@ -122,6 +145,20 @@ This will add the desktop-commander extension to Goose, allowing it to run comma
|
||||
|
||||
Note: The indentation in the heredoc is preserved, so you can write the YAML naturally.
|
||||
|
||||
## Troubleshooting
|
||||
## Run standalone
|
||||
|
||||
The module will create log files in the workspace's `~/.goose-module` directory. If you run into any issues, look at them for more information.
|
||||
Run Goose as a standalone app in your workspace. This will install Goose and run it directly without using screen or tmux, and without any task reporting to the Coder UI.
|
||||
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
goose_version = "v1.0.16"
|
||||
|
||||
# Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL
|
||||
icon = "https://raw.githubusercontent.com/block/goose/refs/heads/main/ui/desktop/src/images/icon.svg"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
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 "../agentapi/test-util";
|
||||
import dedent from "dedent";
|
||||
|
||||
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) {
|
||||
try {
|
||||
await cleanup();
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
interface SetupProps {
|
||||
skipAgentAPIMock?: boolean;
|
||||
skipGooseMock?: 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_goose: props?.skipGooseMock ? "true" : "false",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
goose_provider: "test-provider",
|
||||
goose_model: "test-model",
|
||||
...props?.moduleVariables,
|
||||
},
|
||||
registerCleanup,
|
||||
projectDir,
|
||||
skipAgentAPIMock: props?.skipAgentAPIMock,
|
||||
agentapiMockScript: props?.agentapiMockScript,
|
||||
});
|
||||
if (!props?.skipGooseMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/goose",
|
||||
content: await loadTestFile(import.meta.dir, "goose-mock.sh"),
|
||||
});
|
||||
}
|
||||
return { id };
|
||||
};
|
||||
|
||||
// increase the default timeout to 60 seconds
|
||||
setDefaultTimeout(60 * 1000);
|
||||
|
||||
describe("goose", async () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
test("happy-path", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("install-version", async () => {
|
||||
const { id } = await setup({
|
||||
skipGooseMock: true,
|
||||
moduleVariables: {
|
||||
install_goose: "true",
|
||||
goose_version: "v1.0.24",
|
||||
},
|
||||
});
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`"$HOME/.local/bin/goose" --version`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
expect(resp.exitCode).toBe(0);
|
||||
expect(resp.stdout).toContain("1.0.24");
|
||||
});
|
||||
|
||||
test("install-stable", async () => {
|
||||
const { id } = await setup({
|
||||
skipGooseMock: true,
|
||||
moduleVariables: {
|
||||
install_goose: "true",
|
||||
goose_version: "stable",
|
||||
},
|
||||
});
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`"$HOME/.local/bin/goose" --version`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
expect(resp.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("config", async () => {
|
||||
const expected =
|
||||
dedent`
|
||||
GOOSE_PROVIDER: anthropic
|
||||
GOOSE_MODEL: claude-3-5-sonnet-latest
|
||||
extensions:
|
||||
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: goose
|
||||
CODER_MCP_AI_AGENTAPI_URL: http://localhost:3284
|
||||
name: Coder
|
||||
timeout: 3000
|
||||
type: stdio
|
||||
developer:
|
||||
display_name: Developer
|
||||
enabled: true
|
||||
name: developer
|
||||
timeout: 300
|
||||
type: builtin
|
||||
custom-stuff:
|
||||
enabled: true
|
||||
name: custom-stuff
|
||||
timeout: 300
|
||||
type: builtin
|
||||
`.trim() + "\n";
|
||||
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
goose_provider: "anthropic",
|
||||
goose_model: "claude-3-5-sonnet-latest",
|
||||
additional_extensions: dedent`
|
||||
custom-stuff:
|
||||
enabled: true
|
||||
name: custom-stuff
|
||||
timeout: 300
|
||||
type: builtin
|
||||
`.trim(),
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.config/goose/config.yaml",
|
||||
);
|
||||
expect(resp).toEqual(expected);
|
||||
});
|
||||
|
||||
test("pre-post-install-scripts", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
|
||||
post_install_script: "#!/bin/bash\necho 'post-install-script'",
|
||||
},
|
||||
});
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const preInstallLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.goose-module/pre_install.log",
|
||||
);
|
||||
expect(preInstallLog).toContain("pre-install-script");
|
||||
|
||||
const postInstallLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.goose-module/post_install.log",
|
||||
);
|
||||
expect(postInstallLog).toContain("post-install-script");
|
||||
});
|
||||
|
||||
const promptFile = "/home/coder/.goose-module/prompt.txt";
|
||||
const agentapiStartLog = "/home/coder/.goose-module/agentapi-start.log";
|
||||
|
||||
test("start-with-prompt", async () => {
|
||||
const { id } = await setup({
|
||||
agentapiMockScript: await loadTestFile(
|
||||
import.meta.dir,
|
||||
"agentapi-mock-print-args.js",
|
||||
),
|
||||
});
|
||||
await execModuleScript(id, {
|
||||
GOOSE_TASK_PROMPT: "custom-test-prompt",
|
||||
});
|
||||
const prompt = await readFileContainer(id, promptFile);
|
||||
expect(prompt).toContain("custom-test-prompt");
|
||||
|
||||
const agentapiMockOutput = await readFileContainer(id, agentapiStartLog);
|
||||
expect(agentapiMockOutput).toContain(
|
||||
"'goose run --interactive --instructions /home/coder/.goose-module/prompt.txt '",
|
||||
);
|
||||
});
|
||||
|
||||
test("start-without-prompt", async () => {
|
||||
const { id } = await setup({
|
||||
agentapiMockScript: await loadTestFile(
|
||||
import.meta.dir,
|
||||
"agentapi-mock-print-args.js",
|
||||
),
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const agentapiMockOutput = await readFileContainer(id, agentapiStartLog);
|
||||
expect(agentapiMockOutput).toContain("'goose '");
|
||||
|
||||
const prompt = await execContainer(id, ["ls", "-l", promptFile]);
|
||||
expect(prompt.exitCode).not.toBe(0);
|
||||
expect(prompt.stderr).toContain("No such file or directory");
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,48 +54,67 @@ variable "goose_version" {
|
||||
default = "stable"
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
variable "experiment_use_screen" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
description = "Whether to use screen for running Goose in the background."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
variable "experiment_use_tmux" {
|
||||
type = bool
|
||||
description = "Whether to use tmux instead of screen for running Goose in the background."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "session_name" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.2.3"
|
||||
description = "Name for the persistent session (screen or tmux)"
|
||||
default = "goose"
|
||||
}
|
||||
|
||||
variable "goose_provider" {
|
||||
variable "experiment_report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_auto_configure" {
|
||||
type = bool
|
||||
description = "Whether to automatically configure Goose."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_goose_provider" {
|
||||
type = string
|
||||
description = "The provider to use for Goose (e.g., anthropic)."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "goose_model" {
|
||||
variable "experiment_goose_model" {
|
||||
type = string
|
||||
description = "The model to use for Goose (e.g., claude-3-5-sonnet-latest)."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
variable "experiment_pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Goose."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
variable "experiment_post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Goose."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "additional_extensions" {
|
||||
variable "experiment_additional_extensions" {
|
||||
type = string
|
||||
description = "Additional extensions configuration in YAML format to append to the config."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
app_slug = "goose"
|
||||
base_extensions = <<-EOT
|
||||
coder:
|
||||
args:
|
||||
@@ -106,8 +125,7 @@ coder:
|
||||
description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
|
||||
enabled: true
|
||||
envs:
|
||||
CODER_MCP_APP_STATUS_SLUG: ${local.app_slug}
|
||||
CODER_MCP_AI_AGENTAPI_URL: http://localhost:3284
|
||||
CODER_MCP_APP_STATUS_SLUG: goose
|
||||
name: Coder
|
||||
timeout: 3000
|
||||
type: stdio
|
||||
@@ -121,47 +139,204 @@ EOT
|
||||
|
||||
# Add two spaces to each line of extensions to match YAML structure
|
||||
formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}"
|
||||
additional_extensions = var.additional_extensions != null ? "\n ${replace(trimspace(var.additional_extensions), "\n", "\n ")}" : ""
|
||||
combined_extensions = <<-EOT
|
||||
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
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".goose-module"
|
||||
|
||||
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) : ""
|
||||
}
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.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 = "Goose"
|
||||
cli_app_slug = "${local.app_slug}-cli"
|
||||
cli_app_display_name = "Goose CLI"
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
start_script = local.start_script
|
||||
install_script = <<-EOT
|
||||
# Install and Initialize Goose
|
||||
resource "coder_script" "goose" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Goose"
|
||||
icon = var.icon
|
||||
script = <<-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
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
ARG_PROVIDER='${var.goose_provider}' \
|
||||
ARG_MODEL='${var.goose_model}' \
|
||||
ARG_GOOSE_CONFIG="$(echo -n '${base64encode(local.combined_extensions)}' | base64 -d)" \
|
||||
ARG_INSTALL='${var.install_goose}' \
|
||||
ARG_GOOSE_VERSION='${var.goose_version}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
# Run pre-install script if provided
|
||||
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
|
||||
|
||||
# Install Goose if enabled
|
||||
if [ "${var.install_goose}" = "true" ]; then
|
||||
if ! command_exists npm; then
|
||||
echo "Error: npm is not installed. Please install Node.js and npm first."
|
||||
exit 1
|
||||
fi
|
||||
echo "Installing Goose..."
|
||||
RELEASE_TAG=v${var.goose_version} curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash
|
||||
fi
|
||||
|
||||
# Run post-install script if provided
|
||||
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
|
||||
|
||||
# Configure Goose if auto-configure is enabled
|
||||
if [ "${var.experiment_auto_configure}" = "true" ]; then
|
||||
echo "Configuring Goose..."
|
||||
mkdir -p "$HOME/.config/goose"
|
||||
cat > "$HOME/.config/goose/config.yaml" << EOL
|
||||
GOOSE_PROVIDER: ${var.experiment_goose_provider}
|
||||
GOOSE_MODEL: ${var.experiment_goose_model}
|
||||
${trimspace(local.combined_extensions)}
|
||||
EOL
|
||||
fi
|
||||
|
||||
# Write system prompt to config
|
||||
mkdir -p "$HOME/.config/goose"
|
||||
echo "$GOOSE_SYSTEM_PROMPT" > "$HOME/.config/goose/.goosehints"
|
||||
|
||||
# Handle terminal multiplexer selection (tmux or screen)
|
||||
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
|
||||
|
||||
# Determine goose command
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run with tmux if enabled
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
echo "Running Goose in the background with tmux..."
|
||||
|
||||
# Check if tmux is installed
|
||||
if ! command_exists tmux; then
|
||||
echo "Error: tmux is not installed. Please install tmux manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.goose.log"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
# 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
|
||||
|
||||
# Create a new tmux session in detached mode
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"; exec bash"
|
||||
elif [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Running Goose in the background..."
|
||||
|
||||
# Check if screen is installed
|
||||
if ! command_exists screen; then
|
||||
echo "Error: screen is not installed. Please install screen manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.goose.log"
|
||||
|
||||
# Ensure the screenrc exists
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.goose.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/.goose.log"
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.goose.log"
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
screen -U -dmS ${var.session_name} bash -c "
|
||||
cd ${var.folder}
|
||||
\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"
|
||||
/bin/bash
|
||||
"
|
||||
fi
|
||||
EOT
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "goose" {
|
||||
slug = "goose"
|
||||
display_name = "Goose"
|
||||
agent_id = var.agent_id
|
||||
command = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Determine goose command
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t ${var.session_name} 2>/dev/null; then
|
||||
echo "Attaching to existing Goose tmux session." | tee -a "$HOME/.goose.log"
|
||||
tmux attach-session -t ${var.session_name}
|
||||
else
|
||||
echo "Starting a new Goose tmux session." | tee -a "$HOME/.goose.log"
|
||||
tmux new-session -s ${var.session_name} -c ${var.folder} "\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"; exec bash"
|
||||
fi
|
||||
elif [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
# Check if session exists first
|
||||
if ! screen -list | grep -q "${var.session_name}"; then
|
||||
echo "Error: No existing Goose session found. Please wait for the script to start it."
|
||||
exit 1
|
||||
fi
|
||||
# Only attach to existing session
|
||||
screen -xRR ${var.session_name}
|
||||
else
|
||||
cd ${var.folder}
|
||||
"$GOOSE_CMD" run --text "Review goosehints. Your task: $GOOSE_TASK_PROMPT" --interactive
|
||||
fi
|
||||
EOT
|
||||
icon = var.icon
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
set -o nounset
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "provider: $ARG_PROVIDER"
|
||||
echo "model: $ARG_MODEL"
|
||||
echo "goose_config: $ARG_GOOSE_CONFIG"
|
||||
echo "install: $ARG_INSTALL"
|
||||
echo "goose_version: $ARG_GOOSE_VERSION"
|
||||
echo "--------------------------------"
|
||||
|
||||
set +o nounset
|
||||
|
||||
if [ "${ARG_INSTALL}" = "true" ]; then
|
||||
echo "Installing Goose..."
|
||||
parsed_version="${ARG_GOOSE_VERSION}"
|
||||
if [ "${ARG_GOOSE_VERSION}" = "stable" ]; then
|
||||
parsed_version=""
|
||||
fi
|
||||
curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | GOOSE_VERSION="${parsed_version}" CONFIGURE=false bash
|
||||
echo "Goose installed"
|
||||
else
|
||||
echo "Skipping Goose installation"
|
||||
fi
|
||||
|
||||
if [ "${ARG_GOOSE_CONFIG}" != "" ]; then
|
||||
echo "Configuring Goose..."
|
||||
mkdir -p "$HOME/.config/goose"
|
||||
echo "GOOSE_PROVIDER: $ARG_PROVIDER" >"$HOME/.config/goose/config.yaml"
|
||||
echo "GOOSE_MODEL: $ARG_MODEL" >>"$HOME/.config/goose/config.yaml"
|
||||
echo "$ARG_GOOSE_CONFIG" >>"$HOME/.config/goose/config.yaml"
|
||||
else
|
||||
echo "Skipping Goose configuration"
|
||||
fi
|
||||
|
||||
if [ "${GOOSE_SYSTEM_PROMPT}" != "" ]; then
|
||||
echo "Setting Goose system prompt..."
|
||||
mkdir -p "$HOME/.config/goose"
|
||||
echo "$GOOSE_SYSTEM_PROMPT" >"$HOME/.config/goose/.goosehints"
|
||||
else
|
||||
echo "Goose system prompt not set. use the GOOSE_SYSTEM_PROMPT environment variable to set it."
|
||||
fi
|
||||
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# this must be kept up to date with main.tf
|
||||
MODULE_DIR="$HOME/.goose-module"
|
||||
mkdir -p "$MODULE_DIR"
|
||||
|
||||
if [ ! -z "$GOOSE_TASK_PROMPT" ]; then
|
||||
echo "Starting with a prompt"
|
||||
PROMPT="Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT"
|
||||
PROMPT_FILE="$MODULE_DIR/prompt.txt"
|
||||
echo -n "$PROMPT" >"$PROMPT_FILE"
|
||||
GOOSE_ARGS=(run --interactive --instructions "$PROMPT_FILE")
|
||||
else
|
||||
echo "Starting without a prompt"
|
||||
GOOSE_ARGS=()
|
||||
fi
|
||||
|
||||
agentapi server --term-width 67 --term-height 1190 -- \
|
||||
bash -c "$(printf '%q ' "$GOOSE_CMD" "${GOOSE_ARGS[@]}")"
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const http = require("http");
|
||||
const args = process.argv.slice(2);
|
||||
console.log(args);
|
||||
const port = 3284;
|
||||
|
||||
console.log(`starting server on port ${port}`);
|
||||
|
||||
http
|
||||
.createServer(function (_request, response) {
|
||||
response.writeHead(200);
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
status: "stable",
|
||||
}),
|
||||
);
|
||||
})
|
||||
.listen(port);
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
while true; do
|
||||
echo "$(date) - goose-mock"
|
||||
sleep 15
|
||||
done
|
||||
@@ -10,25 +10,12 @@ tags: [integration, vault, hashicorp, hvs]
|
||||
|
||||
# HCP Vault Secrets
|
||||
|
||||
> [!WARNING]
|
||||
> **⚠️ DEPRECATED: HCP Vault Secrets is being sunset**
|
||||
>
|
||||
> HashiCorp has announced that HCP Vault Secrets will no longer be available for purchase by new customers after **June 30th, 2025**. This module will stop working when HCP Vault Secrets is fully discontinued.
|
||||
>
|
||||
> **Use these Coder registry modules instead:**
|
||||
>
|
||||
> - **[vault-token](https://registry.coder.com/modules/vault-token)** - Connect to Vault using access tokens
|
||||
> - **[vault-jwt](https://registry.coder.com/modules/vault-jwt)** - Connect to Vault using JWT/OIDC authentication
|
||||
> - **[vault-github](https://registry.coder.com/modules/vault-github)** - Connect to Vault using GitHub authentication
|
||||
>
|
||||
> These modules work with both self-hosted Vault and HCP Vault Dedicated. For migration help, see the [official HashiCorp announcement](https://developer.hashicorp.com/hcp/docs/vault-secrets/end-of-sale-announcement).
|
||||
|
||||
This module lets you fetch all or selective secrets from a [HCP Vault Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets) app into your [Coder](https://coder.com) workspaces. It makes use of the [`hcp_vault_secrets_app`](https://registry.terraform.io/providers/hashicorp/hcp/latest/docs/data-sources/vault_secrets_app) data source from the [HCP provider](https://registry.terraform.io/providers/hashicorp/hcp/latest).
|
||||
|
||||
```tf
|
||||
module "vault" {
|
||||
source = "registry.coder.com/coder/hcp-vault-secrets/coder"
|
||||
version = "1.0.32"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
app_name = "demo-app"
|
||||
project_id = "aaa-bbb-ccc"
|
||||
@@ -54,7 +41,7 @@ To fetch all secrets from the HCP Vault Secrets app, skip the `secrets` input.
|
||||
```tf
|
||||
module "vault" {
|
||||
source = "registry.coder.com/coder/hcp-vault-secrets/coder"
|
||||
version = "1.0.32"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
app_name = "demo-app"
|
||||
project_id = "aaa-bbb-ccc"
|
||||
@@ -68,7 +55,7 @@ To fetch selective secrets from the HCP Vault Secrets app, set the `secrets` inp
|
||||
```tf
|
||||
module "vault" {
|
||||
source = "registry.coder.com/coder/hcp-vault-secrets/coder"
|
||||
version = "1.0.32"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
app_name = "demo-app"
|
||||
project_id = "aaa-bbb-ccc"
|
||||
@@ -83,7 +70,7 @@ Set `client_id` and `client_secret` as module inputs.
|
||||
```tf
|
||||
module "vault" {
|
||||
source = "registry.coder.com/coder/hcp-vault-secrets/coder"
|
||||
version = "1.0.32"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
app_name = "demo-app"
|
||||
project_id = "aaa-bbb-ccc"
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
---
|
||||
display_name: JetBrains Fleet
|
||||
description: Add a one-click button to launch JetBrains Fleet to connect to your workspace.
|
||||
icon: ../../../../.icons/fleet.svg
|
||||
verified: true
|
||||
tags: [ide, jetbrains, fleet]
|
||||
---
|
||||
|
||||
# Jetbrains Fleet
|
||||
|
||||
This module adds a Jetbrains Fleet button to your Coder workspace that opens the workspace in JetBrains Fleet using SSH remote development.
|
||||
|
||||
JetBrains Fleet is a next-generation IDE that supports collaborative development and distributed architectures. It connects to your Coder workspace via SSH, providing a seamless remote development experience.
|
||||
|
||||
```tf
|
||||
module "jetbrains_fleet" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-fleet/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- JetBrains Fleet must be installed locally on your development machine
|
||||
- Download Fleet from: https://www.jetbrains.com/fleet/
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Fleet needs you to either have Coder CLI installed with `coder config-ssh` run or [Coder Desktop](https://coder.com/docs/user-guides/desktop).
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic usage
|
||||
|
||||
```tf
|
||||
module "jetbrains_fleet" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-fleet/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
### Open a specific folder
|
||||
|
||||
```tf
|
||||
module "jetbrains_fleet" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-fleet/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
### Customize app name and grouping
|
||||
|
||||
```tf
|
||||
module "jetbrains_fleet" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-fleet/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
display_name = "Fleet"
|
||||
group = "JetBrains IDEs"
|
||||
order = 1
|
||||
}
|
||||
```
|
||||
|
||||
### With custom agent name
|
||||
|
||||
```tf
|
||||
module "jetbrains_fleet" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-fleet/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = coder_agent.example.name
|
||||
}
|
||||
```
|
||||
@@ -1,100 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("jetbrains-fleet", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
expect(state.outputs.fleet_url.value).toBe(
|
||||
"fleet://fleet.ssh/default.coder",
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "fleet",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
});
|
||||
expect(state.outputs.fleet_url.value).toBe(
|
||||
"fleet://fleet.ssh/default.coder?pwd=/foo/bar",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds agent_name to hostname", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
agent_name: "myagent",
|
||||
});
|
||||
expect(state.outputs.fleet_url.value).toBe(
|
||||
"fleet://fleet.ssh/myagent.default.default.coder",
|
||||
);
|
||||
});
|
||||
|
||||
it("custom display name and slug", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
display_name: "My Fleet",
|
||||
slug: "my-fleet",
|
||||
});
|
||||
expect(state.outputs.fleet_url.value).toBe(
|
||||
"fleet://fleet.ssh/default.coder",
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "fleet",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances[0].attributes.display_name).toBe("My Fleet");
|
||||
expect(coder_app?.instances[0].attributes.slug).toBe("my-fleet");
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "fleet",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
|
||||
it("expect group to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
group: "JetBrains IDEs",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "fleet",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.group).toBe("JetBrains IDEs");
|
||||
});
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "agent_name" {
|
||||
type = string
|
||||
description = "The name of the agent"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in Fleet IDE."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the app."
|
||||
default = "fleet"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app."
|
||||
default = "JetBrains Fleet"
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
locals {
|
||||
workspace_name = lower(data.coder_workspace.me.name)
|
||||
owner_name = lower(data.coder_workspace_owner.me.name)
|
||||
agent_name = lower(var.agent_name)
|
||||
hostname = var.agent_name != "" ? "${local.agent_name}.${local.workspace_name}.${local.owner_name}.coder" : "${local.workspace_name}.coder"
|
||||
}
|
||||
|
||||
resource "coder_app" "fleet" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/fleet.svg"
|
||||
slug = var.slug
|
||||
display_name = var.display_name
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = join("", [
|
||||
"fleet://fleet.ssh/",
|
||||
local.hostname,
|
||||
var.folder != "" ? join("", ["?pwd=", var.folder]) : ""
|
||||
])
|
||||
}
|
||||
|
||||
output "fleet_url" {
|
||||
value = coder_app.fleet.url
|
||||
description = "Fleet IDE connection URL."
|
||||
}
|
||||
@@ -18,7 +18,7 @@ Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prereq
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
|
||||
@@ -36,7 +36,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
@@ -50,7 +50,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["IU", "PY"]
|
||||
@@ -65,7 +65,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["IU", "PY"]
|
||||
@@ -90,7 +90,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
@@ -108,7 +108,7 @@ Due to the highest priority of the `ide_download_link` parameter in the `(jetbra
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("jetbrains-gateway", async () => {
|
||||
folder: "/home/coder",
|
||||
});
|
||||
expect(state.outputs.url.value).toBe(
|
||||
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent_id=foo",
|
||||
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz",
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
|
||||
@@ -348,8 +348,6 @@ resource "coder_app" "gateway" {
|
||||
local.build_number,
|
||||
"&ide_download_link=",
|
||||
local.download_link,
|
||||
"&agent_id=",
|
||||
var.agent_id,
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
---
|
||||
display_name: JetBrains Toolbox
|
||||
description: Add JetBrains IDE integrations to your Coder workspaces with configurable options.
|
||||
icon: ../../../../.icons/jetbrains.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [ide, jetbrains, parameter]
|
||||
---
|
||||
|
||||
# JetBrains IDEs
|
||||
|
||||
This module adds JetBrains IDE buttons to launch IDEs directly from the dashboard by integrating with the JetBrains Toolbox.
|
||||
|
||||
```tf
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
> [!IMPORTANT]
|
||||
> This module requires Coder version 2.24+ and [JetBrains Toolbox](https://www.jetbrains.com/toolbox-app/) version 2.7 or higher.
|
||||
|
||||
> [!WARNING]
|
||||
> JetBrains recommends a minimum of 4 CPU cores and 8GB of RAM.
|
||||
> Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prerequisites.html#min_requirements) to confirm other system requirements.
|
||||
|
||||
## Examples
|
||||
|
||||
### Pre-configured Mode (Direct App Creation)
|
||||
|
||||
When `default` contains IDE codes, those IDEs are created directly without user selection:
|
||||
|
||||
```tf
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA
|
||||
}
|
||||
```
|
||||
|
||||
### User Choice with Limited Options
|
||||
|
||||
```tf
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
# Show parameter with limited options
|
||||
options = ["IU", "PY"] # Only these IDEs are available for selection
|
||||
}
|
||||
```
|
||||
|
||||
### Early Access Preview (EAP) Versions
|
||||
|
||||
```tf
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["IU", "PY"]
|
||||
channel = "eap" # Use Early Access Preview versions
|
||||
major_version = "2025.2" # Specific major version
|
||||
}
|
||||
```
|
||||
|
||||
### Custom IDE Configuration
|
||||
|
||||
```tf
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/workspace/project"
|
||||
|
||||
# Custom IDE metadata (display names and icons)
|
||||
ide_config = {
|
||||
"IU" = {
|
||||
name = "IntelliJ IDEA"
|
||||
icon = "/custom/icons/intellij.svg"
|
||||
build = "251.26927.53"
|
||||
}
|
||||
"PY" = {
|
||||
name = "PyCharm"
|
||||
icon = "/custom/icons/pycharm.svg"
|
||||
build = "251.23774.211"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Single IDE for Specific Use Case
|
||||
|
||||
```tf
|
||||
module "jetbrains_pycharm" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/workspace/project"
|
||||
|
||||
default = ["PY"] # Only PyCharm
|
||||
|
||||
# Specific version for consistency
|
||||
major_version = "2025.1"
|
||||
channel = "release"
|
||||
}
|
||||
```
|
||||
|
||||
## Behavior
|
||||
|
||||
### Parameter vs Direct Apps
|
||||
|
||||
- **`default = []` (empty)**: Creates a `coder_parameter` allowing users to select IDEs from `options`
|
||||
- **`default` with values**: Skips parameter and directly creates `coder_app` resources for the specified IDEs
|
||||
|
||||
### Version Resolution
|
||||
|
||||
- Build numbers are fetched from the JetBrains API for the latest compatible versions when internet access is available
|
||||
- If the API is unreachable (air-gapped environments), the module automatically falls back to build numbers from `ide_config`
|
||||
- `major_version` and `channel` control which API endpoint is queried (when API access is available)
|
||||
|
||||
## Supported IDEs
|
||||
|
||||
All JetBrains IDEs with remote development capabilities:
|
||||
|
||||
- [CLion (`CL`)](https://www.jetbrains.com/clion/)
|
||||
- [GoLand (`GO`)](https://www.jetbrains.com/go/)
|
||||
- [IntelliJ IDEA Ultimate (`IU`)](https://www.jetbrains.com/idea/)
|
||||
- [PhpStorm (`PS`)](https://www.jetbrains.com/phpstorm/)
|
||||
- [PyCharm Professional (`PY`)](https://www.jetbrains.com/pycharm/)
|
||||
- [Rider (`RD`)](https://www.jetbrains.com/rider/)
|
||||
- [RubyMine (`RM`)](https://www.jetbrains.com/ruby/)
|
||||
- [RustRover (`RR`)](https://www.jetbrains.com/rust/)
|
||||
- [WebStorm (`WS`)](https://www.jetbrains.com/webstorm/)
|
||||
@@ -1,250 +0,0 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
http = {
|
||||
source = "hashicorp/http"
|
||||
version = ">= 3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The resource ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "agent_name" {
|
||||
type = string
|
||||
description = "The name of a Coder agent. Needed for workspaces with multiple agents."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The directory to open in the IDE. e.g. /home/coder/project"
|
||||
validation {
|
||||
condition = can(regex("^(?:/[^/]+)+/?$", var.folder))
|
||||
error_message = "The folder must be a full path and must not start with a ~."
|
||||
}
|
||||
}
|
||||
|
||||
variable "default" {
|
||||
default = []
|
||||
type = set(string)
|
||||
description = <<-EOT
|
||||
The default IDE selection. Removes the selection from the UI. e.g. ["CL", "GO", "IU"]
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "coder_app_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 "coder_parameter_order" {
|
||||
type = number
|
||||
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "major_version" {
|
||||
type = string
|
||||
description = "The major version of the IDE. i.e. 2025.1"
|
||||
default = "latest"
|
||||
validation {
|
||||
condition = can(regex("^[0-9]{4}\\.[0-2]{1}$", var.major_version)) || var.major_version == "latest"
|
||||
error_message = "The major_version must be a valid version number. i.e. 2025.1 or latest"
|
||||
}
|
||||
}
|
||||
|
||||
variable "channel" {
|
||||
type = string
|
||||
description = "JetBrains IDE release channel. Valid values are release and eap."
|
||||
default = "release"
|
||||
validation {
|
||||
condition = can(regex("^(release|eap)$", var.channel))
|
||||
error_message = "The channel must be either release or eap."
|
||||
}
|
||||
}
|
||||
|
||||
variable "options" {
|
||||
type = set(string)
|
||||
description = "The list of IDE product codes."
|
||||
default = ["CL", "GO", "IU", "PS", "PY", "RD", "RM", "RR", "WS"]
|
||||
validation {
|
||||
condition = (
|
||||
alltrue([
|
||||
for code in var.options : contains(["CL", "GO", "IU", "PS", "PY", "RD", "RM", "RR", "WS"], code)
|
||||
])
|
||||
)
|
||||
error_message = "The options must be a set of valid product codes. Valid product codes are ${join(",", ["CL", "GO", "IU", "PS", "PY", "RD", "RM", "RR", "WS"])}."
|
||||
}
|
||||
# check if the set is empty
|
||||
validation {
|
||||
condition = length(var.options) > 0
|
||||
error_message = "The options must not be empty."
|
||||
}
|
||||
}
|
||||
|
||||
variable "releases_base_link" {
|
||||
type = string
|
||||
description = "URL of the JetBrains releases base link."
|
||||
default = "https://data.services.jetbrains.com"
|
||||
validation {
|
||||
condition = can(regex("^https?://.+$", var.releases_base_link))
|
||||
error_message = "The releases_base_link must be a valid HTTP/S address."
|
||||
}
|
||||
}
|
||||
|
||||
variable "download_base_link" {
|
||||
type = string
|
||||
description = "URL of the JetBrains download base link."
|
||||
default = "https://download.jetbrains.com"
|
||||
validation {
|
||||
condition = can(regex("^https?://.+$", var.download_base_link))
|
||||
error_message = "The download_base_link must be a valid HTTP/S address."
|
||||
}
|
||||
}
|
||||
|
||||
data "http" "jetbrains_ide_versions" {
|
||||
for_each = length(var.default) == 0 ? var.options : var.default
|
||||
url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}&latest=true${var.major_version == "latest" ? "" : "&major_version=${var.major_version}"}"
|
||||
}
|
||||
|
||||
variable "ide_config" {
|
||||
description = <<-EOT
|
||||
A map of JetBrains IDE configurations.
|
||||
The key is the product code and the value is an object with the following properties:
|
||||
- name: The name of the IDE.
|
||||
- icon: The icon of the IDE.
|
||||
- build: The build number of the IDE.
|
||||
Example:
|
||||
{
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
|
||||
}
|
||||
EOT
|
||||
type = map(object({
|
||||
name = string
|
||||
icon = string
|
||||
build = string
|
||||
}))
|
||||
default = {
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
|
||||
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "251.26927.60" },
|
||||
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "251.26927.74" },
|
||||
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "251.26927.67" },
|
||||
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "251.26927.47" },
|
||||
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "251.26927.79" },
|
||||
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "251.26927.40" }
|
||||
}
|
||||
validation {
|
||||
condition = length(var.ide_config) > 0
|
||||
error_message = "The ide_config must not be empty."
|
||||
}
|
||||
# ide_config must be a superset of var.. options
|
||||
validation {
|
||||
condition = alltrue([
|
||||
for code in var.options : contains(keys(var.ide_config), code)
|
||||
])
|
||||
error_message = "The ide_config must be a superset of var.options."
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
# Parse HTTP responses once with error handling for air-gapped environments
|
||||
parsed_responses = {
|
||||
for code in length(var.default) == 0 ? var.options : var.default : code => try(
|
||||
jsondecode(data.http.jetbrains_ide_versions[code].response_body),
|
||||
{} # Return empty object if API call fails
|
||||
)
|
||||
}
|
||||
|
||||
# Dynamically generate IDE configurations based on options with fallback to ide_config
|
||||
options_metadata = {
|
||||
for code in length(var.default) == 0 ? var.options : var.default : code => {
|
||||
icon = var.ide_config[code].icon
|
||||
name = var.ide_config[code].name
|
||||
identifier = code
|
||||
key = code
|
||||
|
||||
# Use API build number if available, otherwise fall back to ide_config build number
|
||||
build = length(keys(local.parsed_responses[code])) > 0 ? (
|
||||
local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0].build
|
||||
) : var.ide_config[code].build
|
||||
|
||||
# Store API data for potential future use (only if API is available)
|
||||
json_data = length(keys(local.parsed_responses[code])) > 0 ? local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0] : null
|
||||
response_key = length(keys(local.parsed_responses[code])) > 0 ? keys(local.parsed_responses[code])[0] : null
|
||||
}
|
||||
}
|
||||
|
||||
# Convert the parameter value to a set for for_each
|
||||
selected_ides = length(var.default) == 0 ? toset(jsondecode(coalesce(data.coder_parameter.jetbrains_ides[0].value, "[]"))) : toset(var.default)
|
||||
}
|
||||
|
||||
data "coder_parameter" "jetbrains_ides" {
|
||||
count = length(var.default) == 0 ? 1 : 0
|
||||
type = "list(string)"
|
||||
name = "jetbrains_ides"
|
||||
display_name = "JetBrains IDEs"
|
||||
icon = "/icon/jetbrains-toolbox.svg"
|
||||
mutable = true
|
||||
default = jsonencode([])
|
||||
order = var.coder_parameter_order
|
||||
form_type = "multi-select" # requires Coder version 2.24+
|
||||
|
||||
dynamic "option" {
|
||||
for_each = var.options
|
||||
content {
|
||||
icon = var.ide_config[option.value].icon
|
||||
name = var.ide_config[option.value].name
|
||||
value = option.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_app" "jetbrains" {
|
||||
for_each = local.selected_ides
|
||||
agent_id = var.agent_id
|
||||
slug = "jetbrains-${lower(each.key)}"
|
||||
display_name = local.options_metadata[each.key].name
|
||||
icon = local.options_metadata[each.key].icon
|
||||
external = true
|
||||
order = var.coder_app_order
|
||||
url = join("", [
|
||||
"jetbrains://gateway/coder?&workspace=", # requires 2.6.3+ version of Toolbox
|
||||
data.coder_workspace.me.name,
|
||||
"&owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&folder=",
|
||||
var.folder,
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=",
|
||||
"$SESSION_TOKEN",
|
||||
"&ide_product_code=",
|
||||
each.key,
|
||||
"&ide_build_number=",
|
||||
local.options_metadata[each.key].build,
|
||||
var.agent_name != null ? "&agent_name=${var.agent_name}" : "",
|
||||
])
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
display_name: Kiro IDE
|
||||
description: Add a one-click button to launch Kiro IDE
|
||||
icon: ../../../../.icons/kiro.svg
|
||||
verified: true
|
||||
tags: [ide, kiro, ai, aws]
|
||||
---
|
||||
|
||||
# Kiro IDE
|
||||
|
||||
Add a button to open any workspace with a single click in [Kiro IDE](https://kiro.dev).
|
||||
|
||||
Kiro is an AI-powered IDE from AWS that helps developers build from concept to production with spec-driven development, featuring AI agents, hooks, and steering files.
|
||||
|
||||
Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder) and [open-remote-ssh extension](https://open-vsx.org/extension/jeanp413/open-remote-ssh) for establishing connections to Coder workspaces.
|
||||
|
||||
```tf
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Open in a specific directory
|
||||
|
||||
```tf
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
### Open with custom display name and order
|
||||
|
||||
```tf
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
display_name = "Kiro AI IDE"
|
||||
order = 1
|
||||
}
|
||||
```
|
||||
@@ -1,93 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("kiro", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
expect(state.outputs.kiro_url.value).toBe(
|
||||
"kiro://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "kiro",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
});
|
||||
expect(state.outputs.kiro_url.value).toBe(
|
||||
"kiro://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder and open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
open_recent: "true",
|
||||
});
|
||||
expect(state.outputs.kiro_url.value).toBe(
|
||||
"kiro://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("custom slug and display_name", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
slug: "kiro-ai",
|
||||
display_name: "Kiro AI IDE",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "kiro",
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.slug).toBe("kiro-ai");
|
||||
expect(coder_app?.instances[0].attributes.display_name).toBe("Kiro AI IDE");
|
||||
});
|
||||
|
||||
it("sets order", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "5",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "kiro",
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(5);
|
||||
});
|
||||
|
||||
it("sets group", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
group: "AI IDEs",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "kiro",
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.group).toBe("AI IDEs");
|
||||
});
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in Kiro IDE."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "open_recent" {
|
||||
type = bool
|
||||
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the app."
|
||||
default = "kiro"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app."
|
||||
default = "Kiro IDE"
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_app" "kiro" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/kiro.svg"
|
||||
slug = var.slug
|
||||
display_name = var.display_name
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = join("", [
|
||||
"kiro://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
}
|
||||
|
||||
output "kiro_url" {
|
||||
value = coder_app.kiro.url
|
||||
description = "Kiro IDE URL."
|
||||
}
|
||||
@@ -1,31 +1,24 @@
|
||||
---
|
||||
display_name: RDP Desktop
|
||||
display_name: Windows RDP Desktop
|
||||
description: Enable RDP on Windows and add a one-click Coder Desktop button for seamless access
|
||||
icon: ../../../../.icons/rdp.svg
|
||||
icon: ../../../../.icons/desktop.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
supported_os: [windows]
|
||||
tags: [rdp, windows, desktop, local]
|
||||
tags: [rdp, windows, desktop, remote]
|
||||
---
|
||||
|
||||
# Windows RDP Desktop
|
||||
|
||||
This module enables Remote Desktop Protocol (RDP) on Windows workspaces and adds a one-click button to launch RDP sessions directly through [Coder Desktop](https://coder.com/docs/user-guides/desktop). It provides a complete, standalone solution for RDP access, eliminating the need for manual configuration or port forwarding through the Coder CLI.
|
||||
|
||||
<!--
|
||||
2025-07-07 - Prettier isn't formatting GFM comments properly if they don't
|
||||
start with a letter.
|
||||
See https://github.com/prettier/prettier/issues/15479
|
||||
-->
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> [Coder Desktop](https://coder.com/docs/user-guides/desktop) is required on client devices to use the Local Windows RDP access feature.
|
||||
> **Note**: [Coder Desktop](https://coder.com/docs/user-guides/desktop) is required on client devices to use the Local Windows RDP access feature.
|
||||
|
||||
```tf
|
||||
module "rdp_desktop" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/local-windows-rdp/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = coder_agent.main.name
|
||||
}
|
||||
@@ -58,7 +51,7 @@ Uses default credentials (Username: `Administrator`, Password: `coderRDP!`):
|
||||
module "rdp_desktop" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/local-windows-rdp/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = coder_agent.main.name
|
||||
}
|
||||
@@ -72,7 +65,7 @@ Specify a custom display name for the `coder_app` button:
|
||||
module "rdp_desktop" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/local-windows-rdp/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.windows.id
|
||||
agent_name = "windows"
|
||||
display_name = "Windows Desktop"
|
||||
|
||||
@@ -73,7 +73,7 @@ describe("local-windows-rdp", async () => {
|
||||
expect(app).not.toBeNull();
|
||||
expect(app?.slug).toBe("rdp-desktop");
|
||||
expect(app?.display_name).toBe("RDP Desktop");
|
||||
expect(app?.icon).toBe("/icon/rdp.svg");
|
||||
expect(app?.icon).toBe("/icon/desktop.svg");
|
||||
expect(app?.external).toBe(true);
|
||||
|
||||
// Verify the URI format
|
||||
@@ -95,7 +95,7 @@ describe("local-windows-rdp", async () => {
|
||||
// Verify the script was created
|
||||
expect(script).not.toBeNull();
|
||||
expect(script?.display_name).toBe("Configure RDP");
|
||||
expect(script?.icon).toBe("/icon/rdp.svg");
|
||||
expect(script?.icon).toBe("/icon/desktop.svg");
|
||||
expect(script?.run_on_start).toBe(true);
|
||||
expect(script?.run_on_stop).toBe(false);
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ data "coder_workspace" "me" {}
|
||||
resource "coder_script" "rdp_setup" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Configure RDP"
|
||||
icon = "/icon/rdp.svg"
|
||||
icon = "/icon/desktop.svg"
|
||||
script = templatefile("${path.module}/configure-rdp.ps1", {
|
||||
username = var.username
|
||||
password = var.password
|
||||
@@ -73,8 +73,9 @@ resource "coder_app" "rdp_desktop" {
|
||||
slug = "rdp-desktop"
|
||||
display_name = var.display_name
|
||||
url = "coder://${local.server_name}/v0/open/ws/${data.coder_workspace.me.name}/agent/${var.agent_name}/rdp?username=${var.username}&password=${var.password}"
|
||||
icon = "/icon/rdp.svg"
|
||||
icon = "/icon/desktop.svg"
|
||||
external = true
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,11 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de
|
||||
```tf
|
||||
# AWS example. See below for examples of using this module with other providers
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.2"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -31,10 +32,11 @@ module "windows_rdp" {
|
||||
|
||||
```tf
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.2"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -42,10 +44,11 @@ module "windows_rdp" {
|
||||
|
||||
```tf
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.2"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.google_compute_instance.dev[0].id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -55,8 +58,13 @@ module "windows_rdp" {
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.1"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
devolutions_gateway_version = "2025.2.2" # Specify a specific version
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
devolutions_gateway_version = "2025.1.6" # Specify a specific version
|
||||
}
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Test on Microsoft Azure.
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
|
||||
type TestVariables = Readonly<{
|
||||
agent_id: string;
|
||||
resource_id: string;
|
||||
share?: string;
|
||||
admin_username?: string;
|
||||
admin_password?: string;
|
||||
@@ -44,11 +45,13 @@ describe("Web RDP", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
resource_id: "bar",
|
||||
});
|
||||
|
||||
it("Has the PowerShell script install Devolutions Gateway", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
resource_id: "bar",
|
||||
});
|
||||
|
||||
const lines = findWindowsRdpScript(state)
|
||||
@@ -93,6 +96,7 @@ describe("Web RDP", async () => {
|
||||
import.meta.dir,
|
||||
{
|
||||
agent_id: "foo",
|
||||
resource_id: "bar",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -112,6 +116,7 @@ describe("Web RDP", async () => {
|
||||
import.meta.dir,
|
||||
{
|
||||
agent_id: "foo",
|
||||
resource_id: "bar",
|
||||
admin_username: customAdminUsername,
|
||||
admin_password: customAdminPassword,
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,11 @@ variable "agent_id" {
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "resource_id" {
|
||||
type = string
|
||||
description = "The ID of the primary Coder resource (e.g. VM)."
|
||||
}
|
||||
|
||||
variable "admin_username" {
|
||||
type = string
|
||||
default = "Administrator"
|
||||
@@ -48,14 +53,14 @@ variable "admin_password" {
|
||||
|
||||
variable "devolutions_gateway_version" {
|
||||
type = string
|
||||
default = "2025.2.2"
|
||||
default = "2025.2.1"
|
||||
description = "Version of Devolutions Gateway to install. Defaults to the latest available version."
|
||||
}
|
||||
|
||||
resource "coder_script" "windows-rdp" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "windows-rdp"
|
||||
icon = "/icon/rdp.svg"
|
||||
icon = "/icon/desktop.svg"
|
||||
|
||||
script = templatefile("${path.module}/powershell-installation-script.tftpl", {
|
||||
admin_username = var.admin_username
|
||||
@@ -96,7 +101,7 @@ resource "coder_app" "rdp-docs" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Local RDP Docs"
|
||||
slug = "rdp-docs"
|
||||
icon = "/icon/windows.svg"
|
||||
url = "https://coder.com/docs/user-guides/workspace-access/remote-desktops#rdp"
|
||||
icon = "https://raw.githubusercontent.com/matifali/logos/main/windows.svg"
|
||||
url = "https://coder.com/docs/ides/remote-desktops#rdp-desktop"
|
||||
external = true
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
---
|
||||
display_name: Zed
|
||||
description: Add a one-click button to launch Zed
|
||||
icon: ../../../../.icons/zed.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [ide, zed, editor]
|
||||
---
|
||||
|
||||
# Zed
|
||||
|
||||
Add a button to open any workspace with a single click in Zed.
|
||||
|
||||
Zed is a high-performance, multiplayer code editor from the creators of Atom and Tree-sitter.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Zed needs you to either have Coder CLI installed with `coder config-ssh` run or [Coder Desktop](https://coder.com/docs/user-guides/desktop)
|
||||
|
||||
```tf
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Open in a specific directory
|
||||
|
||||
```tf
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
### Custom display name and order
|
||||
|
||||
```tf
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
display_name = "Zed Editor"
|
||||
order = 1
|
||||
}
|
||||
```
|
||||
|
||||
### With custom agent name
|
||||
|
||||
```tf
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = coder_agent.example.name
|
||||
}
|
||||
```
|
||||
@@ -1,77 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("zed", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
expect(state.outputs.zed_url.value).toBe("zed://ssh/default.coder");
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "zed",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
});
|
||||
expect(state.outputs.zed_url.value).toBe("zed://ssh/default.coder/foo/bar");
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "zed",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
|
||||
it("expect display_name to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
display_name: "Custom Zed",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "zed",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.display_name).toBe("Custom Zed");
|
||||
});
|
||||
|
||||
it("adds agent_name to hostname", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
agent_name: "myagent",
|
||||
});
|
||||
expect(state.outputs.zed_url.value).toBe(
|
||||
"zed://ssh/myagent.default.default.coder",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent"
|
||||
}
|
||||
|
||||
variable "agent_name" {
|
||||
type = string
|
||||
description = "The name of the agent"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in Zed"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)"
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to"
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the app"
|
||||
default = "zed"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app"
|
||||
default = "Zed"
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
locals {
|
||||
workspace_name = lower(data.coder_workspace.me.name)
|
||||
owner_name = lower(data.coder_workspace_owner.me.name)
|
||||
agent_name = lower(var.agent_name)
|
||||
hostname = var.agent_name != "" ? "${local.agent_name}.${local.workspace_name}.${local.owner_name}.coder" : "${local.workspace_name}.coder"
|
||||
}
|
||||
|
||||
resource "coder_app" "zed" {
|
||||
agent_id = var.agent_id
|
||||
display_name = var.display_name
|
||||
slug = var.slug
|
||||
icon = "/icon/zed.svg"
|
||||
external = true
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = "zed://ssh/${local.hostname}${var.folder}"
|
||||
}
|
||||
|
||||
output "zed_url" {
|
||||
value = coder_app.zed.url
|
||||
description = "Zed URL"
|
||||
}
|
||||
@@ -10,7 +10,7 @@ tags: [vm, linux, aws, persistent, devcontainer]
|
||||
# Remote Development on AWS EC2 VMs using a Devcontainer
|
||||
|
||||
Provision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs) with this example template.
|
||||

|
||||

|
||||
|
||||
<!-- TODO: Add screenshot -->
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
@@ -20,13 +20,16 @@ To deploy workspaces as DigitalOcean Droplets, you'll need:
|
||||
- DigitalOcean [personal access token (PAT)](https://docs.digitalocean.com/reference/api/create-personal-access-token)
|
||||
|
||||
- DigitalOcean project ID (you can get your project information via the `doctl` CLI by running `doctl projects list`)
|
||||
|
||||
- Remove the following sections from the `main.tf` file if you don't want to
|
||||
associate your workspaces with a project:
|
||||
|
||||
- `variable "project_uuid"`
|
||||
- `resource "digitalocean_project_resources" "project"`
|
||||
|
||||
- **Optional:** DigitalOcean SSH key ID (obtain via the `doctl` CLI by running
|
||||
`doctl compute ssh-key list`)
|
||||
|
||||
- Note that this is only required for Fedora images to work.
|
||||
|
||||
### Authentication
|
||||
|
||||
@@ -9,7 +9,7 @@ tags: [vm, linux, gcp, devcontainer]
|
||||
|
||||
# Remote Development in a Devcontainer on Google Compute Engine
|
||||
|
||||

|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -34,6 +34,7 @@ a service account:
|
||||
|
||||
1. Click **Create and continue**, and choose the following IAM roles to grant to
|
||||
the service account:
|
||||
|
||||
- Compute Admin
|
||||
- Service Account User
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
@@ -32,6 +32,7 @@ a service account:
|
||||
|
||||
1. Click **Create and continue**, and choose the following IAM roles to grant to
|
||||
the service account:
|
||||
|
||||
- Compute Admin
|
||||
- Service Account User
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ a service account:
|
||||
|
||||
1. Click **Create and continue**, and choose the following IAM roles to grant to
|
||||
the service account:
|
||||
|
||||
- Compute Admin
|
||||
- Service Account User
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ a service account:
|
||||
|
||||
1. Click **Create and continue**, and choose the following IAM roles to grant to
|
||||
the service account:
|
||||
|
||||
- Compute Admin
|
||||
- Service Account User
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ Develop in an Incus System Container and run nested Docker containers using Incu
|
||||
|
||||
1. Install [Incus](https://linuxcontainers.org/incus/) on the same machine as Coder.
|
||||
2. Allow Coder to access the Incus socket.
|
||||
|
||||
- If you're running Coder as system service, run `sudo usermod -aG incus-admin coder` and restart the Coder service.
|
||||
- If you're running Coder as a Docker Compose service, get the group ID of the `incus-admin` group by running `getent group incus-admin` and add the following to your `compose.yaml` file:
|
||||
|
||||
|
||||
@@ -324,13 +324,3 @@ export const writeFileContainer = async (
|
||||
}
|
||||
expect(proc.exitCode).toBe(0);
|
||||
};
|
||||
|
||||
export const readFileContainer = async (id: string, path: string) => {
|
||||
const proc = await execContainer(id, ["cat", path], ["--user", "root"]);
|
||||
if (proc.exitCode !== 0) {
|
||||
console.log(proc.stderr);
|
||||
console.log(proc.stdout);
|
||||
}
|
||||
expect(proc.exitCode).toBe(0);
|
||||
return proc.stdout;
|
||||
};
|
||||
|
||||