Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f66f61d724 | |||
| 631bf027c6 | |||
| eb4c28fc61 | |||
| c551c4d84a | |||
| 4b9da4036a | |||
| 96c5f3219d | |||
| 146540c1e9 | |||
| a85436fdf4 | |||
| aa4890fe62 | |||
| ab6799ac07 | |||
| bda3eb96e8 | |||
| 6b16cd3529 | |||
| 43d05a9da4 | |||
| e3f8b6450e | |||
| c03986f9cb | |||
| 758aba4c2a | |||
| d745117782 | |||
| a99d3385c3 | |||
| c62fe569a0 | |||
| ce2087bc09 | |||
| 67f18cd4de | |||
| e0697562c1 | |||
| 499aaa676c | |||
| 3ae8c7dcff | |||
| 2cfbe5f69c | |||
| 186e0c4de6 | |||
| 69e5dc5c80 | |||
| b143b7d9ba | |||
| 0a8930d60d | |||
| d21db0d220 | |||
| 392f6b120a | |||
| 7de72fc7cc | |||
| 3e1ddbf624 | |||
| 0021a9fe7d | |||
| 70ca76e86d |
@@ -82,7 +82,8 @@ create_incident() {
|
||||
# Function to check for existing unresolved incidents
|
||||
check_existing_incident() {
|
||||
# Fetch the latest incidents with status not equal to "RESOLVED"
|
||||
local unresolved_incidents=$(curl -s -X GET "https://api.instatus.com/v1/$INSTATUS_PAGE_ID/incidents" \
|
||||
local unresolved_incidents
|
||||
unresolved_incidents=$(curl -s -X GET "https://api.instatus.com/v1/$INSTATUS_PAGE_ID/incidents" \
|
||||
-H "Authorization: Bearer $INSTATUS_API_KEY" \
|
||||
-H "Content-Type: application/json" | jq -r '.incidents[] | select(.status != "RESOLVED") | .id')
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ muc = "muc" # For Munich location code
|
||||
tyo = "tyo" # For Tokyo location code
|
||||
Hashi = "Hashi"
|
||||
HashiCorp = "HashiCorp"
|
||||
hel = "hel" # For Helsinki location code
|
||||
mavrickrishi = "mavrickrishi" # Username
|
||||
mavrick = "mavrick" # Username
|
||||
inh = "inh" # Option in setpriv command
|
||||
|
||||
@@ -29,8 +29,11 @@ jobs:
|
||||
- 'scripts/ts_test_auto.sh'
|
||||
- 'scripts/terraform_test_all.sh'
|
||||
- 'scripts/terraform_validate.sh'
|
||||
- 'scripts/shellcheck_validate.sh'
|
||||
modules:
|
||||
- 'registry/**/modules/**'
|
||||
shell:
|
||||
- '**/*.sh'
|
||||
all:
|
||||
- '**'
|
||||
- name: Set up Terraform
|
||||
@@ -64,6 +67,14 @@ jobs:
|
||||
SHARED_CHANGED: ${{ steps.filter.outputs.shared }}
|
||||
MODULE_CHANGED_FILES: ${{ steps.filter.outputs.modules_files }}
|
||||
run: bun terraform-validate
|
||||
- name: Run ShellCheck
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.filter.outputs.all_files }}
|
||||
SHARED_CHANGED: ${{ steps.filter.outputs.shared }}
|
||||
SHELL_CHANGED_FILES: ${{ steps.filter.outputs.shell_files }}
|
||||
run: bun shellcheck
|
||||
- name: Validate set -u ordering
|
||||
run: ./scripts/validate_set_u_order.sh
|
||||
validate-style:
|
||||
name: Check for typos and unformatted code
|
||||
runs-on: ubuntu-latest
|
||||
@@ -82,7 +93,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.39.2
|
||||
uses: crate-ci/typos@v1.40.0
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
|
||||
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="250" cy="250" r="250" fill="#fff"/>
|
||||
<path d="m335 150h40v200h-40zm-130 0a100 100 0 1 0 0 200 100 100 0 1 0 0-200zm0 40a60 60 0 1 1 0 120 60 60 0 1 1 0-120z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 293 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="128" cy="128" r="120" fill="black"/>
|
||||
<polygon
|
||||
points="128,70 178,170 78,170"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 216 B |
@@ -0,0 +1,22 @@
|
||||
# ShellCheck configuration for Coder Registry
|
||||
# https://www.shellcheck.net/wiki/
|
||||
|
||||
# Set default shell dialect to bash (most scripts use bash)
|
||||
shell=bash
|
||||
|
||||
# Disable checks that conflict with Terraform templating syntax
|
||||
# Many scripts use Terraform's templatefile() function with $${VAR} escape syntax
|
||||
disable=SC2154 # Variable is referenced but not assigned (injected by Terraform)
|
||||
disable=SC2034 # Variable appears unused (used via $${VAR} syntax)
|
||||
disable=SC1083 # Literal braces (Terraform's $${VAR} escape syntax)
|
||||
disable=SC2193 # Comparison arguments never equal (Terraform interpolation)
|
||||
disable=SC2125 # Brace expansion/globs in assignments (Terraform syntax)
|
||||
disable=SC2157 # Argument to -n/-z is always true/false (Terraform $${VAR} syntax)
|
||||
disable=SC2066 # Loop will only run once (Terraform $${VAR} array syntax)
|
||||
|
||||
# Disable checks that conflict with intentional patterns
|
||||
disable=SC2076 # Quoted regex in =~ (intentional literal string match, not regex, for array membership checks)
|
||||
|
||||
# Enable all optional checks for thorough analysis
|
||||
enable=all
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "registry",
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.21",
|
||||
"bun-types": "^1.2.21",
|
||||
"dedent": "^1.6.0",
|
||||
"@types/bun": "^1.3.4",
|
||||
"bun-types": "^1.3.4",
|
||||
"dedent": "^1.7.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^16.2.0",
|
||||
"prettier": "^3.6.2",
|
||||
"marked": "^16.4.2",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-sh": "^0.18.0",
|
||||
"prettier-plugin-terraform-formatter": "^1.2.1",
|
||||
"shellcheck": "^4.1.0",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.8.3",
|
||||
@@ -19,54 +21,292 @@
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
|
||||
|
||||
"@felipecrs/decompress-tarxz": ["@felipecrs/decompress-tarxz@5.0.4", "", { "dependencies": { "@xhmikosr/decompress-tar": "^8.1.0", "file-type": "^20.5.0", "is-stream": "^2.0.1", "xz-decompress": "^0.2.3" } }, "sha512-a+nAnDsiUA84Sy/a+FKYJtjOjFvNtW8Jcbi3NwE8kJKPpYAxINFLYsC9mev9/wngiNEBA3jfHn0qNFwICeZNJw=="],
|
||||
|
||||
"@reteps/dockerfmt": ["@reteps/dockerfmt@0.3.6", "", {}, "sha512-Tb5wIMvBf/nLejTQ61krK644/CEMB/cpiaIFXqGApfGqO3GwcR3qnI0DbmkFVCl2OyEp8LnLX3EkucoL0+tbFg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
|
||||
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
|
||||
|
||||
"@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=="],
|
||||
"@xhmikosr/decompress-tar": ["@xhmikosr/decompress-tar@8.1.0", "", { "dependencies": { "file-type": "^20.5.0", "is-stream": "^2.0.1", "tar-stream": "^3.1.7" } }, "sha512-m0q8x6lwxenh1CrsTby0Jrjq4vzW/QU1OLhTHMQLEdHpmjR1lgahGz++seZI0bXF3XcZw3U3xHfqZSz+JPP2Gg=="],
|
||||
|
||||
"@xhmikosr/decompress-unzip": ["@xhmikosr/decompress-unzip@7.1.0", "", { "dependencies": { "file-type": "^20.5.0", "get-stream": "^6.0.1", "yauzl": "^3.1.2" } }, "sha512-oqTYAcObqTlg8owulxFTqiaJkfv2SHsxxxz9Wg4krJAHVzGWlZsU8tAB30R6ow+aHrfv4Kub6WQ8u04NWVPUpA=="],
|
||||
|
||||
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
"b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="],
|
||||
|
||||
"dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="],
|
||||
"bare-events": ["bare-events@2.8.0", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"bl": ["bl@1.2.3", "", { "dependencies": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" } }, "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww=="],
|
||||
|
||||
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
|
||||
|
||||
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"buffer-alloc": ["buffer-alloc@1.2.0", "", { "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" } }, "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow=="],
|
||||
|
||||
"buffer-alloc-unsafe": ["buffer-alloc-unsafe@1.1.0", "", {}, "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="],
|
||||
|
||||
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
||||
|
||||
"buffer-fill": ["buffer-fill@1.0.0", "", {}, "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decompress": ["decompress@4.2.1", "", { "dependencies": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", "decompress-targz": "^4.0.0", "decompress-unzip": "^4.0.1", "graceful-fs": "^4.1.10", "make-dir": "^1.0.0", "pify": "^2.3.0", "strip-dirs": "^2.0.0" } }, "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ=="],
|
||||
|
||||
"decompress-tar": ["decompress-tar@4.1.1", "", { "dependencies": { "file-type": "^5.2.0", "is-stream": "^1.1.0", "tar-stream": "^1.5.2" } }, "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ=="],
|
||||
|
||||
"decompress-tarbz2": ["decompress-tarbz2@4.1.1", "", { "dependencies": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", "is-stream": "^1.1.0", "seek-bzip": "^1.0.5", "unbzip2-stream": "^1.0.9" } }, "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A=="],
|
||||
|
||||
"decompress-targz": ["decompress-targz@4.1.1", "", { "dependencies": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", "is-stream": "^1.1.0" } }, "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w=="],
|
||||
|
||||
"decompress-unzip": ["decompress-unzip@4.0.1", "", { "dependencies": { "file-type": "^3.8.0", "get-stream": "^2.2.0", "pify": "^2.3.0", "yauzl": "^2.4.2" } }, "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw=="],
|
||||
|
||||
"dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
||||
"envalid": ["envalid@8.1.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-OT6+qVhKVyCidaGoXflb2iK1tC8pd0OV2Q+v9n33wNhUJ+lus+rJobUj4vJaQBPxPZ0vYrPGuxdrenyCAIJcow=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
|
||||
|
||||
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
|
||||
|
||||
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
||||
|
||||
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
||||
|
||||
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
|
||||
|
||||
"file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
|
||||
|
||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||
|
||||
"is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
||||
|
||||
"is-natural-number": ["is-natural-number@4.0.1", "", {}, "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ=="],
|
||||
|
||||
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
||||
|
||||
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
||||
|
||||
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||
|
||||
"marked": ["marked@16.2.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg=="],
|
||||
"make-dir": ["make-dir@1.3.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ=="],
|
||||
|
||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||
|
||||
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
|
||||
|
||||
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
|
||||
|
||||
"pinkie": ["pinkie@2.0.4", "", {}, "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg=="],
|
||||
|
||||
"pinkie-promise": ["pinkie-promise@2.0.1", "", { "dependencies": { "pinkie": "^2.0.0" } }, "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
||||
|
||||
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||
|
||||
"seek-bzip": ["seek-bzip@1.0.6", "", { "dependencies": { "commander": "^2.8.1" }, "bin": { "seek-bunzip": "bin/seek-bunzip", "seek-table": "bin/seek-bzip-table" } }, "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
|
||||
|
||||
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||
|
||||
"sh-syntax": ["sh-syntax@0.5.8", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-JfVoxf4FxQI5qpsPbkHhZo+n6N9YMJobyl4oGEUBb/31oQYlgTjkXQD8PBiafS2UbWoxrTO0Z5PJUBXEPAG1Zw=="],
|
||||
|
||||
"shellcheck": ["shellcheck@4.1.0", "", { "dependencies": { "@felipecrs/decompress-tarxz": "5.0.4", "@xhmikosr/decompress-unzip": "7.1.0", "decompress": "4.2.1", "envalid": "8.1.0", "global-agent": "3.0.0" }, "bin": { "shellcheck": "bin/shellcheck.js" } }, "sha512-8143z6YGO4+Puwp9Ghn/g7+QxllSKlXaZSm3HXfvQXUfRXhM5P8TPORRHBBlyobl9BnniVne+d1Ff6RgNiccsQ=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
|
||||
|
||||
"strip-dirs": ["strip-dirs@2.1.0", "", { "dependencies": { "is-natural-number": "^4.0.1" } }, "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g=="],
|
||||
|
||||
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
|
||||
|
||||
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
||||
|
||||
"text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="],
|
||||
|
||||
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
|
||||
|
||||
"to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="],
|
||||
|
||||
"token-types": ["token-types@6.1.1", "", { "dependencies": { "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
||||
|
||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||
|
||||
"unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="],
|
||||
|
||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"xz-decompress": ["xz-decompress@0.2.3", "", {}, "sha512-O8v6HG8T0PrKBcpyWA13GkSYWFvncwzuzcLx5A7++l3HsE3atmoetXjIxrZ/JV/nbvSZ7WS4+3XvREZuVn+rEA=="],
|
||||
|
||||
"yauzl": ["yauzl@3.2.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" } }, "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w=="],
|
||||
|
||||
"decompress-tar/file-type": ["file-type@5.2.0", "", {}, "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ=="],
|
||||
|
||||
"decompress-tar/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="],
|
||||
|
||||
"decompress-tar/tar-stream": ["tar-stream@1.6.2", "", { "dependencies": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", "end-of-stream": "^1.0.0", "fs-constants": "^1.0.0", "readable-stream": "^2.3.0", "to-buffer": "^1.1.1", "xtend": "^4.0.0" } }, "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A=="],
|
||||
|
||||
"decompress-tarbz2/file-type": ["file-type@6.2.0", "", {}, "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg=="],
|
||||
|
||||
"decompress-tarbz2/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="],
|
||||
|
||||
"decompress-targz/file-type": ["file-type@5.2.0", "", {}, "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ=="],
|
||||
|
||||
"decompress-targz/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="],
|
||||
|
||||
"decompress-unzip/file-type": ["file-type@3.9.0", "", {}, "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA=="],
|
||||
|
||||
"decompress-unzip/get-stream": ["get-stream@2.3.1", "", { "dependencies": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" } }, "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA=="],
|
||||
|
||||
"decompress-unzip/yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
|
||||
|
||||
"make-dir/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="],
|
||||
|
||||
"roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
||||
|
||||
"to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"to-buffer/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,19 @@
|
||||
"terraform-validate": "./scripts/terraform_validate.sh",
|
||||
"tftest": "./scripts/terraform_test_all.sh",
|
||||
"tstest": "./scripts/ts_test_auto.sh",
|
||||
"shellcheck": "./scripts/shellcheck_validate.sh",
|
||||
"update-version": "./update-version.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.21",
|
||||
"bun-types": "^1.2.21",
|
||||
"dedent": "^1.6.0",
|
||||
"@types/bun": "^1.3.4",
|
||||
"bun-types": "^1.3.4",
|
||||
"dedent": "^1.7.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^16.2.0",
|
||||
"prettier": "^3.6.2",
|
||||
"marked": "^16.4.2",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-sh": "^0.18.0",
|
||||
"prettier-plugin-terraform-formatter": "^1.2.1"
|
||||
"prettier-plugin-terraform-formatter": "^1.2.1",
|
||||
"shellcheck": "^4.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
|
||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 451 KiB After Width: | Height: | Size: 451 KiB |
|
After Width: | Height: | Size: 98 KiB |
@@ -0,0 +1,7 @@
|
||||
---
|
||||
display_name: "Excellencedev"
|
||||
bio: "Love to contribute"
|
||||
avatar: "./.images/avatar.png"
|
||||
support_email: "ademiluyisuccessandexcellence@gmail.com"
|
||||
status: "community"
|
||||
---
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
display_name: Hetzner Cloud Server
|
||||
description: Provision Hetzner Cloud servers as Coder workspaces
|
||||
icon: ../../../../.icons/hetzner.svg
|
||||
tags: [vm, linux, hetzner]
|
||||
---
|
||||
|
||||
# Remote Development on Hetzner Cloud (Linux)
|
||||
|
||||
Provision Hetzner Cloud servers as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
|
||||
|
||||
> [!WARNING]
|
||||
> **Workspace Storage Persistence:** When a workspace is stopped, the Hetzner Cloud server instance is stopped but your home volume and stored data persist. This means your files and data remain intact when you resume the workspace.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Volume Management & Costs:** Hetzner Cloud volumes persist even when workspaces are stopped and will continue to incur storage costs (€0.0476/GB/month). Volumes are only automatically deleted when the workspace is completely deleted. Monitor your volumes in the [Hetzner Cloud Console](https://console.hetzner.cloud/) to manage costs effectively.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To deploy workspaces as Hetzner Cloud servers, you'll need:
|
||||
|
||||
- Hetzner Cloud [API token](https://console.hetzner.cloud/projects) (create under Security > API Tokens)
|
||||
|
||||
### Authentication
|
||||
|
||||
This template assumes that the Coder Provisioner is run in an environment that is authenticated with Hetzner Cloud.
|
||||
|
||||
Obtain a Hetzner Cloud API token from your [Hetzner Cloud Console](https://console.hetzner.cloud/projects) and provide it as the `hcloud_token` variable when creating a workspace.
|
||||
For more authentication options, see the [Terraform provider documentation](https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs#authentication).
|
||||
|
||||
> [!NOTE]
|
||||
> This template is designed to be a starting point. Edit the Terraform to extend the template to support your use case.
|
||||
@@ -0,0 +1,62 @@
|
||||
#cloud-config
|
||||
users:
|
||||
- name: ${username}
|
||||
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
|
||||
groups: sudo
|
||||
shell: /bin/bash
|
||||
packages:
|
||||
- git
|
||||
%{ if home_volume_label != "" ~}
|
||||
fs_setup:
|
||||
- device: /dev/disk/by-id/scsi-0HC_Volume_${volume_id}
|
||||
filesystem: ext4
|
||||
label: ${home_volume_label}
|
||||
overwrite: false # This prevents reformatting the disk on every boot
|
||||
|
||||
mounts:
|
||||
- [
|
||||
"/dev/disk/by-id/scsi-0HC_Volume_${volume_id}",
|
||||
"/home/${username}",
|
||||
ext4,
|
||||
"defaults,uid=1000,gid=1000",
|
||||
]
|
||||
%{ endif ~}
|
||||
write_files:
|
||||
- path: /opt/coder/init
|
||||
permissions: "0755"
|
||||
encoding: b64
|
||||
content: ${init_script}
|
||||
- path: /etc/systemd/system/coder-agent.service
|
||||
permissions: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Coder Agent
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
User=${username}
|
||||
ExecStart=/opt/coder/init
|
||||
Environment=CODER_AGENT_TOKEN=${coder_agent_token}
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
TimeoutStopSec=90
|
||||
KillMode=process
|
||||
|
||||
OOMScoreAdjust=-900
|
||||
SyslogIdentifier=coder-agent
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
runcmd:
|
||||
%{ if home_volume_label != "" ~}
|
||||
- |
|
||||
until [ -e /dev/disk/by-id/scsi-0HC_Volume_${volume_id} ]; do
|
||||
echo "Waiting for volume device..."
|
||||
sleep 2
|
||||
done
|
||||
%{ endif ~}
|
||||
- mount -a
|
||||
- chown ${username}:${username} /home/${username}
|
||||
- systemctl enable coder-agent
|
||||
- systemctl start coder-agent
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"type_meta": {
|
||||
"cx22": { "cores": 2, "memory_gb": 4, "disk_gb": 40 },
|
||||
"cx32": { "cores": 4, "memory_gb": 8, "disk_gb": 80 },
|
||||
"cx42": { "cores": 8, "memory_gb": 16, "disk_gb": 160 },
|
||||
"cx52": { "cores": 16, "memory_gb": 32, "disk_gb": 320 },
|
||||
"cpx11": { "cores": 2, "memory_gb": 2, "disk_gb": 40 },
|
||||
"cpx21": { "cores": 3, "memory_gb": 4, "disk_gb": 80 },
|
||||
"cpx31": { "cores": 4, "memory_gb": 8, "disk_gb": 160 },
|
||||
"cpx41": { "cores": 8, "memory_gb": 16, "disk_gb": 240 },
|
||||
"cpx51": { "cores": 16, "memory_gb": 32, "disk_gb": 360 },
|
||||
"ccx13": { "cores": 2, "memory_gb": 8, "disk_gb": 80 },
|
||||
"ccx23": { "cores": 4, "memory_gb": 16, "disk_gb": 160 },
|
||||
"ccx33": { "cores": 8, "memory_gb": 32, "disk_gb": 240 },
|
||||
"ccx43": { "cores": 16, "memory_gb": 64, "disk_gb": 360 },
|
||||
"ccx53": { "cores": 32, "memory_gb": 128, "disk_gb": 600 },
|
||||
"ccx63": { "cores": 48, "memory_gb": 192, "disk_gb": 960 }
|
||||
},
|
||||
"availability": {
|
||||
"fsn1": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"ash": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"hel1": ["cx22", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"hil": ["cpx11", "cpx21", "cpx31", "cpx41", "ccx13", "ccx23", "ccx33"],
|
||||
"nbg1": ["cx22", "cx32", "cx42", "cx52", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"sin": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
hcloud = {
|
||||
source = "hetznercloud/hcloud"
|
||||
}
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "hcloud_token" {
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
provider "hcloud" {
|
||||
token = var.hcloud_token
|
||||
}
|
||||
|
||||
# Available locations: https://docs.hetzner.com/cloud/general/locations/
|
||||
data "coder_parameter" "hcloud_location" {
|
||||
name = "hcloud_location"
|
||||
display_name = "Hetzner Location"
|
||||
description = "Select the Hetzner Cloud location for your workspace."
|
||||
type = "string"
|
||||
default = "fsn1"
|
||||
option {
|
||||
name = "DE Falkenstein"
|
||||
value = "fsn1"
|
||||
}
|
||||
option {
|
||||
name = "US Ashburn, VA"
|
||||
value = "ash"
|
||||
}
|
||||
option {
|
||||
name = "US Hillsboro, OR"
|
||||
value = "hil"
|
||||
}
|
||||
option {
|
||||
name = "SG Singapore"
|
||||
value = "sin"
|
||||
}
|
||||
option {
|
||||
name = "DE Nuremberg"
|
||||
value = "nbg1"
|
||||
}
|
||||
option {
|
||||
name = "FI Helsinki"
|
||||
value = "hel1"
|
||||
}
|
||||
}
|
||||
|
||||
# Available server types: https://docs.hetzner.com/cloud/servers/overview/
|
||||
data "coder_parameter" "hcloud_server_type" {
|
||||
name = "hcloud_server_type"
|
||||
display_name = "Hetzner Server Type"
|
||||
description = "Select the Hetzner Cloud server type for your workspace."
|
||||
type = "string"
|
||||
|
||||
dynamic "option" {
|
||||
for_each = local.hcloud_server_type_options_for_selected_location
|
||||
content {
|
||||
name = option.value.name
|
||||
value = option.value.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "hcloud_server" "dev" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
name = "coder-${data.coder_workspace.me.name}-dev"
|
||||
image = "ubuntu-24.04"
|
||||
server_type = data.coder_parameter.hcloud_server_type.value
|
||||
location = data.coder_parameter.hcloud_location.value
|
||||
public_net {
|
||||
ipv4_enabled = true
|
||||
ipv6_enabled = true
|
||||
}
|
||||
user_data = templatefile("cloud-config.yaml.tftpl", {
|
||||
username = lower(data.coder_workspace_owner.me.name)
|
||||
home_volume_label = "coder-${data.coder_workspace.me.id}-home"
|
||||
volume_id = hcloud_volume.home_volume.id
|
||||
init_script = base64encode(coder_agent.main.init_script)
|
||||
coder_agent_token = coder_agent.main.token
|
||||
})
|
||||
labels = {
|
||||
"coder_workspace_name" = data.coder_workspace.me.name,
|
||||
"coder_workspace_owner" = data.coder_workspace_owner.me.name,
|
||||
}
|
||||
}
|
||||
|
||||
resource "hcloud_volume" "home_volume" {
|
||||
name = "coder-${data.coder_workspace.me.id}-home"
|
||||
size = data.coder_parameter.home_volume_size.value
|
||||
location = data.coder_parameter.hcloud_location.value
|
||||
labels = {
|
||||
"coder_workspace_name" = data.coder_workspace.me.name,
|
||||
"coder_workspace_owner" = data.coder_workspace_owner.me.name,
|
||||
}
|
||||
}
|
||||
|
||||
resource "hcloud_volume_attachment" "home_volume_attachment" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
volume_id = hcloud_volume.home_volume.id
|
||||
server_id = hcloud_server.dev[count.index].id
|
||||
automount = false
|
||||
}
|
||||
|
||||
locals {
|
||||
username = lower(data.coder_workspace_owner.me.name)
|
||||
|
||||
# Data source: local JSON file under the module directory
|
||||
# Check API for latest server types & availability: https://docs.hetzner.cloud/reference/cloud#server-types
|
||||
hcloud_server_types_data = jsondecode(file("${path.module}/hetzner_server_types.json"))
|
||||
hcloud_server_type_meta = local.hcloud_server_types_data.type_meta
|
||||
hcloud_server_types_by_location = local.hcloud_server_types_data.availability
|
||||
|
||||
hcloud_server_type_options_for_selected_location = [
|
||||
for type_name in lookup(local.hcloud_server_types_by_location, data.coder_parameter.hcloud_location.value, []) : {
|
||||
name = format("%s (%d vCPU, %dGB RAM, %dGB)", upper(type_name), local.hcloud_server_type_meta[type_name].cores, local.hcloud_server_type_meta[type_name].memory_gb, local.hcloud_server_type_meta[type_name].disk_gb)
|
||||
value = type_name
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
|
||||
provider "coder" {}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
data "coder_parameter" "home_volume_size" {
|
||||
name = "home_volume_size"
|
||||
display_name = "Home volume size"
|
||||
description = "How large would you like your home volume to be (in GB)?"
|
||||
type = "number"
|
||||
default = "20"
|
||||
mutable = false
|
||||
validation {
|
||||
min = 1
|
||||
max = 100 # Adjust the max size as needed
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
|
||||
metadata {
|
||||
key = "cpu"
|
||||
display_name = "CPU Usage"
|
||||
interval = 5
|
||||
timeout = 5
|
||||
script = "coder stat cpu"
|
||||
}
|
||||
metadata {
|
||||
key = "memory"
|
||||
display_name = "Memory Usage"
|
||||
interval = 5
|
||||
timeout = 5
|
||||
script = "coder stat mem"
|
||||
}
|
||||
metadata {
|
||||
key = "home"
|
||||
display_name = "Home Usage"
|
||||
interval = 600 # every 10 minutes
|
||||
timeout = 30 # df can take a while on large filesystems
|
||||
script = "coder stat disk --path /home/${local.username}"
|
||||
}
|
||||
}
|
||||
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
version = "~> 1.0"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
order = 1
|
||||
}
|
||||
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 7.4 KiB |
@@ -15,8 +15,8 @@ up a default or custom tmux configuration with session save/restore capabilities
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -39,8 +39,8 @@ module "tmux" {
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
tmux_config = "" # Optional: custom tmux.conf content
|
||||
save_interval = 1 # Optional: save interval in minutes
|
||||
sessions = ["default", "dev", "ops"] # Optional: list of tmux sessions
|
||||
@@ -78,7 +78,7 @@ This module can provision multiple tmux sessions, each as a separate app in the
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
agent_id = var.agent_id
|
||||
sessions = ["default", "dev", "anomaly"]
|
||||
tmux_config = <<-EOT
|
||||
|
||||
@@ -144,7 +144,7 @@ main() {
|
||||
printf "$${BOLD}✅ tmux setup complete! \n\n"
|
||||
|
||||
printf "$${BOLD} Attempting to restore sessions\n"
|
||||
tmux new-session -d \; source-file ~/.tmux.conf \; run-shell '~/.tmux/plugins/tmux-resurrect/scripts/restore.sh'
|
||||
tmux new-session -d \; source-file ~/.tmux.conf \; run-shell "$HOME/.tmux/plugins/tmux-resurrect/scripts/restore.sh"
|
||||
printf "$${BOLD} Sessions restored: -> %s\n" "$(tmux ls)"
|
||||
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 590 KiB |
@@ -14,8 +14,8 @@ This module installs small, robust scripts in your workspace to create and extra
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.2"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "0.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
paths = ["./projects", "./code"]
|
||||
}
|
||||
@@ -43,8 +43,8 @@ Basic example:
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.2"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "0.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
# Paths to include in the archive (files or directories).
|
||||
directory = "~"
|
||||
@@ -61,8 +61,8 @@ Customize compression and output:
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.2"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "0.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
directory = "/"
|
||||
paths = ["/etc", "/home"]
|
||||
@@ -78,8 +78,8 @@ Enable auto-archive on stop:
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.2"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "0.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
# Creates /tmp/coder-archive.tar.gz of the users home directory (defaults).
|
||||
create_on_stop = true
|
||||
@@ -92,8 +92,8 @@ Extract on start:
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.2"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "0.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
# Where to look for the archive file to extract:
|
||||
output_dir = "/tmp"
|
||||
|
||||
@@ -6,8 +6,8 @@ EXTRACT_ON_START="${TF_EXTRACT_ON_START}"
|
||||
EXTRACT_WAIT_TIMEOUT="${TF_EXTRACT_WAIT_TIMEOUT}"
|
||||
|
||||
# Set script defaults from Terraform.
|
||||
DEFAULT_PATHS=(${TF_PATHS})
|
||||
DEFAULT_EXCLUDE_PATTERNS=(${TF_EXCLUDE_PATTERNS})
|
||||
IFS=' ' read -r -a DEFAULT_PATHS <<< "${TF_PATHS}"
|
||||
IFS=' ' read -r -a DEFAULT_EXCLUDE_PATTERNS <<< "${TF_EXCLUDE_PATTERNS}"
|
||||
DEFAULT_COMPRESSION="${TF_COMPRESSION}"
|
||||
DEFAULT_ARCHIVE_PATH="${TF_ARCHIVE_PATH}"
|
||||
DEFAULT_DIRECTORY="${TF_DIRECTORY}"
|
||||
@@ -62,6 +62,7 @@ echo "Installed extract script to: $EXTRACT_WRAPPER_PATH"
|
||||
|
||||
# 3) Optionally wait for and extract an archive on start.
|
||||
if [[ $EXTRACT_ON_START = true ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$LIB_PATH"
|
||||
|
||||
archive_wait_and_extract "$EXTRACT_WAIT_TIMEOUT" quiet || {
|
||||
|
||||
@@ -14,7 +14,7 @@ Run Auggie CLI in your workspace to access Augment's AI coding assistant with ad
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.2.2"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
@@ -41,14 +41,14 @@ data "coder_parameter" "ai_prompt" {
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/coder-login/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "0.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.2.2"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
# Authentication
|
||||
@@ -57,7 +57,7 @@ module "auggie" {
|
||||
EOF # Required for tasks
|
||||
|
||||
# Version
|
||||
auggie_version = "0.3.0"
|
||||
auggie_version = "0.2.2"
|
||||
|
||||
# Task configuration
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
@@ -74,7 +74,6 @@ EOF # Required for tasks
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/coder/project"]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
EOF
|
||||
@@ -105,7 +104,7 @@ EOF
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.2.2"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
# Multiple MCP configuration files
|
||||
@@ -128,7 +127,6 @@ module "auggie" {
|
||||
],
|
||||
"timeout": 600
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
|
||||
@@ -14,7 +14,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "3.1.1"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = var.openai_api_key
|
||||
workdir = "/home/coder/project"
|
||||
}
|
||||
@@ -34,7 +34,7 @@ module "codex" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "3.1.1"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = "..."
|
||||
workdir = "/home/coder/project"
|
||||
report_tasks = false
|
||||
@@ -55,14 +55,14 @@ data "coder_parameter" "ai_prompt" {
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/coder-login/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "3.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "3.1.1"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = "..."
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@@ -38,7 +38,8 @@ find_session_for_directory() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
local session_id=$(grep "^$target_dir|" "$SESSION_TRACKING_FILE" | cut -d'|' -f2 | head -1)
|
||||
local session_id
|
||||
session_id=$(grep "^$target_dir|" "$SESSION_TRACKING_FILE" | cut -d'|' -f2 | head -1)
|
||||
|
||||
if [ -n "$session_id" ]; then
|
||||
echo "$session_id"
|
||||
@@ -74,9 +75,12 @@ find_recent_session_file() {
|
||||
local latest_time=0
|
||||
|
||||
while IFS= read -r session_file; do
|
||||
local file_time=$(stat -c %Y "$session_file" 2> /dev/null || stat -f %m "$session_file" 2> /dev/null || echo "0")
|
||||
local first_line=$(head -n 1 "$session_file" 2> /dev/null)
|
||||
local session_cwd=$(echo "$first_line" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4)
|
||||
local file_time
|
||||
file_time=$(stat -c %Y "$session_file" 2> /dev/null || stat -f %m "$session_file" 2> /dev/null || echo "0")
|
||||
local first_line
|
||||
first_line=$(head -n 1 "$session_file" 2> /dev/null)
|
||||
local session_cwd
|
||||
session_cwd=$(echo "$first_line" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ "$session_cwd" = "$target_dir" ] && [ "$file_time" -gt "$latest_time" ]; then
|
||||
latest_file="$session_file"
|
||||
@@ -85,8 +89,10 @@ find_recent_session_file() {
|
||||
done < <(find "$sessions_dir" -type f -name "*.jsonl" 2> /dev/null)
|
||||
|
||||
if [ -n "$latest_file" ]; then
|
||||
local first_line=$(head -n 1 "$latest_file")
|
||||
local session_id=$(echo "$first_line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
|
||||
local first_line
|
||||
first_line=$(head -n 1 "$latest_file")
|
||||
local session_id
|
||||
session_id=$(echo "$first_line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
|
||||
if [ -n "$session_id" ]; then
|
||||
echo "$session_id"
|
||||
return 0
|
||||
@@ -102,7 +108,8 @@ wait_for_session_file() {
|
||||
local attempt=0
|
||||
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
local session_id=$(find_recent_session_file "$target_dir" 2> /dev/null || echo "")
|
||||
local session_id
|
||||
session_id=$(find_recent_session_file "$target_dir" 2> /dev/null || echo "")
|
||||
if [ -n "$session_id" ]; then
|
||||
echo "$session_id"
|
||||
return 0
|
||||
|
||||
@@ -13,8 +13,8 @@ Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-c
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
}
|
||||
```
|
||||
@@ -51,8 +51,8 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
@@ -71,12 +71,12 @@ Customize tool permissions, MCP servers, and Copilot settings:
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
|
||||
# Version pinning (defaults to "latest", use specific version if desired)
|
||||
copilot_version = "0.0.334"
|
||||
copilot_version = "0.2.3"
|
||||
|
||||
# Tool permissions
|
||||
allow_tools = ["shell(git)", "shell(npm)", "write"]
|
||||
@@ -101,7 +101,6 @@ module "copilot" {
|
||||
tools = ["*"]
|
||||
trust = true
|
||||
}
|
||||
|
||||
playwright = {
|
||||
command = "npx"
|
||||
args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated"]
|
||||
@@ -143,8 +142,8 @@ variable "github_token" {
|
||||
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
github_token = var.github_token
|
||||
}
|
||||
@@ -157,8 +156,8 @@ Run Copilot as a command-line tool without task reporting or web interface. This
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
report_tasks = false
|
||||
cli_app = true
|
||||
|
||||
@@ -3,7 +3,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,7 +242,7 @@ resource "coder_env" "github_token" {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.workdir
|
||||
@@ -268,7 +268,7 @@ module "agentapi" {
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
|
||||
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
|
||||
@@ -288,7 +288,7 @@ module "agentapi" {
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
|
||||
|
||||
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
@@ -299,4 +299,8 @@ module "agentapi" {
|
||||
ARG_COPILOT_MODEL='${var.copilot_model}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
command_exists() {
|
||||
|
||||
@@ -13,7 +13,7 @@ Run the Cursor Agent CLI in your workspace for interactive coding assistance and
|
||||
```tf
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -42,7 +42,7 @@ module "coder-login" {
|
||||
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ describe("cursor-cli", async () => {
|
||||
"-c",
|
||||
"cat /home/coder/.cursor-cli-module/agentapi-start.log || cat /home/coder/.cursor-cli-module/start.log || true",
|
||||
]);
|
||||
expect(startLog.stdout).toContain(`-m ${model}`);
|
||||
expect(startLog.stdout).toContain(`--model ${model}`);
|
||||
expect(startLog.stdout).toContain("-f");
|
||||
expect(startLog.stdout).toContain("test prompt");
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ resource "coder_env" "cursor_api_key" {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.folder
|
||||
@@ -179,3 +179,7 @@ module "agentapi" {
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ ARGS=()
|
||||
|
||||
# global flags
|
||||
if [ -n "$ARG_MODEL" ]; then
|
||||
ARGS+=("-m" "$ARG_MODEL")
|
||||
ARGS+=("--model" "$ARG_MODEL")
|
||||
fi
|
||||
if [ "$ARG_FORCE" = "true" ]; then
|
||||
ARGS+=("-f")
|
||||
|
||||
@@ -13,7 +13,7 @@ Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace
|
||||
```tf
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.1.2"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -46,7 +46,7 @@ variable "gemini_api_key" {
|
||||
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.1.2"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
folder = "/home/coder/project"
|
||||
@@ -94,7 +94,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
module "gemini" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.1.2"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
gemini_model = "gemini-2.5-flash"
|
||||
@@ -118,7 +118,7 @@ For enterprise users who prefer Google's Vertex AI platform:
|
||||
```tf
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.1.2"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
folder = "/home/coder/project"
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@ EOT
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.folder
|
||||
@@ -225,4 +225,8 @@ module "agentapi" {
|
||||
GEMINI_TASK_PROMPT='${var.task_prompt}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
display_name: Open WebUI
|
||||
description: A self-hosted AI chat interface supporting various LLM providers
|
||||
icon: ../../../../.icons/openwebui.svg
|
||||
verified: false
|
||||
tags: [ai, llm, chat, web, python]
|
||||
---
|
||||
|
||||
# Open WebUI
|
||||
|
||||
Open WebUI is a user-friendly web interface for interacting with Large Language Models. It provides a ChatGPT-like interface that can connect to various LLM providers including OpenAI, Ollama, and more.
|
||||
|
||||
```tf
|
||||
module "open-webui" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/open-webui/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Python 3.11 or higher** must be installed in your image (with `venv` module)
|
||||
- Port 7800 (default) or your custom port must be available
|
||||
|
||||
For Ubuntu/Debian, you can install Python 3.11 from [deadsnakes PPA](https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa):
|
||||
|
||||
```shell
|
||||
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3.11 python3.11-venv
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### With OpenAI API Key
|
||||
|
||||
```tf
|
||||
module "open-webui" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/open-webui/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
|
||||
openai_api_key = var.openai_api_key
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Port and Data Directory
|
||||
|
||||
```tf
|
||||
module "open-webui" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/open-webui/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
|
||||
http_server_port = 8080
|
||||
data_dir = "/home/coder/open-webui-data"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,94 @@
|
||||
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 "http_server_log_path" {
|
||||
type = string
|
||||
description = "The path to log Open WebUI to."
|
||||
default = "/tmp/open-webui.log"
|
||||
}
|
||||
|
||||
variable "http_server_port" {
|
||||
type = number
|
||||
description = "The port to run Open WebUI on."
|
||||
default = 7800
|
||||
}
|
||||
|
||||
variable "open_webui_version" {
|
||||
type = string
|
||||
description = "The version of Open WebUI to install"
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "data_dir" {
|
||||
type = string
|
||||
description = "The directory where Open WebUI stores its data (database, uploads, vector_db, cache)."
|
||||
default = ".open-webui"
|
||||
}
|
||||
|
||||
variable "openai_api_key" {
|
||||
type = string
|
||||
description = "OpenAI API key for accessing OpenAI models. If not provided, OpenAI integration will need to be configured manually in the UI."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "share" {
|
||||
type = string
|
||||
description = "The sharing level for the Open WebUI app. Set to 'owner' for private access, 'authenticated' for access by any authenticated user, or 'public' for public access."
|
||||
default = "owner"
|
||||
validation {
|
||||
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
|
||||
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "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
|
||||
}
|
||||
|
||||
resource "coder_script" "open-webui" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "open-webui"
|
||||
icon = "/icon/openwebui.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
HTTP_SERVER_LOG_PATH : var.http_server_log_path,
|
||||
HTTP_SERVER_PORT : var.http_server_port,
|
||||
VERSION : var.open_webui_version,
|
||||
DATA_DIR : var.data_dir,
|
||||
OPENAI_API_KEY : var.openai_api_key,
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "open-webui" {
|
||||
agent_id = var.agent_id
|
||||
slug = "open-webui"
|
||||
display_name = "Open WebUI"
|
||||
url = "http://localhost:${var.http_server_port}"
|
||||
icon = "/icon/openwebui.svg"
|
||||
subdomain = true
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
mock_provider "coder" {}
|
||||
|
||||
run "test_defaults" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-123"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.http_server_port == 7800
|
||||
error_message = "Default port should be 7800"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.http_server_log_path == "/tmp/open-webui.log"
|
||||
error_message = "Default log path should be /tmp/open-webui.log"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.share == "owner"
|
||||
error_message = "Default share should be 'owner'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.open_webui_version == "latest"
|
||||
error_message = "Default version should be 'latest'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.subdomain == true
|
||||
error_message = "App should use subdomain"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.display_name == "Open WebUI"
|
||||
error_message = "App display name should be 'Open WebUI'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_port" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-456"
|
||||
http_server_port = 9000
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.http_server_port == 9000
|
||||
error_message = "Custom port should be 9000"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.url == "http://localhost:9000"
|
||||
error_message = "App URL should use custom port"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_log_path" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-789"
|
||||
http_server_log_path = "/var/log/open-webui.log"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.http_server_log_path == "/var/log/open-webui.log"
|
||||
error_message = "Custom log path should be set"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_share_authenticated" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-auth"
|
||||
share = "authenticated"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.share == "authenticated"
|
||||
error_message = "Share should be 'authenticated'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_share_public" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-public"
|
||||
share = "public"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.share == "public"
|
||||
error_message = "Share should be 'public'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_order_and_group" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-order"
|
||||
order = 10
|
||||
group = "AI Tools"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.order == 10
|
||||
error_message = "Order should be 10"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.group == "AI Tools"
|
||||
error_message = "Group should be 'AI Tools'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_version" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-version"
|
||||
open_webui_version = "0.5.0"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.open_webui_version == "0.5.0"
|
||||
error_message = "Custom version should be '0.5.0'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_data_dir" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-data"
|
||||
data_dir = "/home/coder/open-webui-data"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.data_dir == "/home/coder/open-webui-data"
|
||||
error_message = "Custom data_dir should be set"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_default_data_dir" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-data-default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.data_dir == ".open-webui"
|
||||
error_message = "Default data_dir should be '.open-webui'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_openai_api_key" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-openai"
|
||||
openai_api_key = "sk-test-key-123"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.openai_api_key == "sk-test-key-123"
|
||||
error_message = "OpenAI API key should be set"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_default_openai_api_key" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-openai-default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.openai_api_key == ""
|
||||
error_message = "Default OpenAI API key should be empty"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -eu
|
||||
|
||||
printf '\033[0;1mInstalling Open WebUI %s...\n\n' "${VERSION}"
|
||||
|
||||
check_python_version() {
|
||||
python_cmd="$1"
|
||||
if command -v "$python_cmd" > /dev/null 2>&1; then
|
||||
version=$("$python_cmd" --version 2>&1 | awk '{print $2}')
|
||||
major=$(echo "$version" | cut -d. -f1)
|
||||
minor=$(echo "$version" | cut -d. -f2)
|
||||
if [ "$major" -eq 3 ] && [ "$minor" -ge 11 ]; then
|
||||
echo "$python_cmd"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
PYTHON_CMD=""
|
||||
for cmd in python3.13 python3.12 python3.11 python3 python; do
|
||||
if result=$(check_python_version "$cmd"); then
|
||||
PYTHON_CMD="$result"
|
||||
echo "✅ Found suitable Python: $PYTHON_CMD ($($PYTHON_CMD --version 2>&1))"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PYTHON_CMD" ]; then
|
||||
echo "❌ Python 3.11 or higher is required but not found."
|
||||
echo ""
|
||||
echo "Please install Python 3.11+ in your image. For example on Ubuntu/Debian:"
|
||||
echo " sudo add-apt-repository -y ppa:deadsnakes/ppa"
|
||||
echo " sudo apt-get update"
|
||||
echo " sudo apt-get install -y python3.11 python3.11-venv"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VENV_DIR="$HOME/.open-webui-venv"
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo "📦 Creating virtual environment..."
|
||||
"$PYTHON_CMD" -m venv "$VENV_DIR"
|
||||
fi
|
||||
. "$VENV_DIR/bin/activate"
|
||||
|
||||
if ! pip show open-webui > /dev/null 2>&1; then
|
||||
echo "📦 Installing Open WebUI version ${VERSION}..."
|
||||
if [ "${VERSION}" = "latest" ]; then
|
||||
pip install open-webui
|
||||
else
|
||||
pip install "open-webui==${VERSION}"
|
||||
fi
|
||||
echo "🥳 Open WebUI has been installed"
|
||||
else
|
||||
echo "✅ Open WebUI is already installed"
|
||||
fi
|
||||
|
||||
echo "👷 Starting Open WebUI in background..."
|
||||
echo "Check logs at ${HTTP_SERVER_LOG_PATH}"
|
||||
|
||||
DATA_DIR="${DATA_DIR}" \
|
||||
OPENAI_API_KEY="${OPENAI_API_KEY}" \
|
||||
open-webui serve --host 0.0.0.0 --port "${HTTP_SERVER_PORT}" > "${HTTP_SERVER_LOG_PATH}" 2>&1 &
|
||||
|
||||
echo "🥳 Open WebUI is ready. HTTP server is listening on port ${HTTP_SERVER_PORT}"
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
display_name: Perplexica
|
||||
description: Run Perplexica AI search engine in your workspace via Docker
|
||||
icon: ../../../../.icons/perplexica.svg
|
||||
verified: false
|
||||
tags: [ai, search, docker]
|
||||
---
|
||||
|
||||
# Perplexica
|
||||
|
||||
Run [Perplexica](https://github.com/ItzCrazyKns/Perplexica), a privacy-focused AI search engine, in your Coder workspace. Supports cloud providers (OpenAI, Anthropic Claude) and local LLMs via Ollama.
|
||||
|
||||
```tf
|
||||
module "perplexica" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/perplexica/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||
This module uses the full Perplexica image with embedded SearXNG for simpler setup with no external dependencies.
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
This module requires Docker to be available on the host.
|
||||
|
||||
## Examples
|
||||
|
||||
### With API Keys
|
||||
|
||||
```tf
|
||||
module "perplexica" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/perplexica/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
openai_api_key = var.openai_api_key
|
||||
anthropic_api_key = var.anthropic_api_key
|
||||
}
|
||||
```
|
||||
|
||||
### With Local Ollama
|
||||
|
||||
```tf
|
||||
module "perplexica" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/perplexica/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
ollama_api_url = "http://ollama-external-endpoint:11434"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
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 "docker_socket" {
|
||||
type = string
|
||||
description = "(Optional) Docker socket URI"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "port" {
|
||||
type = number
|
||||
description = "The port to run Perplexica on."
|
||||
default = 3000
|
||||
}
|
||||
|
||||
variable "data_path" {
|
||||
type = string
|
||||
description = "Host path to mount for Perplexica data persistence."
|
||||
default = "./perplexica-data"
|
||||
}
|
||||
|
||||
variable "uploads_path" {
|
||||
type = string
|
||||
description = "Host path to mount for Perplexica file uploads."
|
||||
default = "./perplexica-uploads"
|
||||
}
|
||||
|
||||
variable "openai_api_key" {
|
||||
type = string
|
||||
description = "OpenAI API key."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Anthropic API key for Claude models."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "ollama_api_url" {
|
||||
type = string
|
||||
description = "Ollama API URL for local LLM support."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "share" {
|
||||
type = string
|
||||
default = "owner"
|
||||
validation {
|
||||
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
|
||||
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "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
|
||||
}
|
||||
|
||||
resource "coder_script" "perplexica" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Perplexica"
|
||||
icon = "/icon/perplexica.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
DOCKER_HOST : var.docker_socket,
|
||||
PORT : var.port,
|
||||
DATA_PATH : var.data_path,
|
||||
UPLOADS_PATH : var.uploads_path,
|
||||
OPENAI_API_KEY : var.openai_api_key,
|
||||
ANTHROPIC_API_KEY : var.anthropic_api_key,
|
||||
OLLAMA_API_URL : var.ollama_api_url,
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "perplexica" {
|
||||
agent_id = var.agent_id
|
||||
slug = "perplexica"
|
||||
display_name = "Perplexica"
|
||||
url = "http://localhost:${var.port}"
|
||||
icon = "/icon/perplexica.svg"
|
||||
subdomain = true
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
run "plan_basic" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.perplexica.url == "http://localhost:3000"
|
||||
error_message = "Default port should be 3000"
|
||||
}
|
||||
}
|
||||
|
||||
run "plan_custom_port" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
port = 8080
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.perplexica.url == "http://localhost:8080"
|
||||
error_message = "Should use custom port"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -eu
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
printf "$${BOLD}Starting Perplexica...$${RESET}\n"
|
||||
|
||||
# Set Docker host if provided
|
||||
if [ -n "${DOCKER_HOST}" ]; then
|
||||
export DOCKER_HOST="${DOCKER_HOST}"
|
||||
fi
|
||||
|
||||
# Wait for docker to become ready
|
||||
max_attempts=10
|
||||
delay=2
|
||||
attempt=1
|
||||
|
||||
while ! docker ps; do
|
||||
if [ $attempt -ge $max_attempts ]; then
|
||||
echo "Failed to list containers after $${max_attempts} attempts."
|
||||
exit 1
|
||||
fi
|
||||
echo "Attempt $${attempt} failed, retrying in $${delay}s..."
|
||||
sleep $delay
|
||||
attempt=$(expr "$attempt" + 1)
|
||||
delay=$(expr "$delay" \* 2)
|
||||
done
|
||||
|
||||
# Pull the image
|
||||
IMAGE="itzcrazykns1337/perplexica:latest"
|
||||
docker pull "$${IMAGE}"
|
||||
|
||||
# Build docker run command
|
||||
DOCKER_ARGS="-d --rm --name perplexica -p ${PORT}:3000"
|
||||
|
||||
# Add mounts - convert relative paths to absolute
|
||||
DATA_PATH="${DATA_PATH}"
|
||||
UPLOADS_PATH="${UPLOADS_PATH}"
|
||||
|
||||
mkdir -p "$${DATA_PATH}"
|
||||
mkdir -p "$${UPLOADS_PATH}"
|
||||
|
||||
DATA_PATH_ABS=$(cd "$${DATA_PATH}" && pwd)
|
||||
UPLOADS_PATH_ABS=$(cd "$${UPLOADS_PATH}" && pwd)
|
||||
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -v $${DATA_PATH_ABS}:/home/perplexica/data"
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -v $${UPLOADS_PATH_ABS}:/home/perplexica/uploads"
|
||||
|
||||
# Add environment variables if provided
|
||||
if [ -n "${OPENAI_API_KEY}" ]; then
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -e OPENAI_API_KEY=${OPENAI_API_KEY}"
|
||||
fi
|
||||
|
||||
if [ -n "${ANTHROPIC_API_KEY}" ]; then
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -e ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}"
|
||||
fi
|
||||
|
||||
if [ -n "${OLLAMA_API_URL}" ]; then
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -e OLLAMA_API_URL=${OLLAMA_API_URL}"
|
||||
fi
|
||||
|
||||
# Run container
|
||||
docker run $${DOCKER_ARGS} "$${IMAGE}"
|
||||
|
||||
printf "\n$${BOLD}Perplexica is running on port ${PORT}$${RESET}\n"
|
||||
@@ -12,12 +12,12 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
|
||||
|
||||
```tf
|
||||
module "amp-cli" {
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "2.0.2"
|
||||
agent_id = coder_agent.main.id
|
||||
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
|
||||
install_sourcegraph_amp = true
|
||||
agentapi_version = "latest"
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
amp_api_key = var.amp_api_key
|
||||
install_amp = true
|
||||
agentapi_version = "latest"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -48,8 +48,8 @@ variable "amp_api_key" {
|
||||
module "amp-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
amp_version = "2.0.1"
|
||||
agent_id = coder_agent.main.id
|
||||
amp_version = "3.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
amp_api_key = var.amp_api_key # recommended for tasks usage
|
||||
workdir = "/home/coder/project"
|
||||
instruction_prompt = <<-EOT
|
||||
|
||||
@@ -110,6 +110,7 @@ describe("amp", async () => {
|
||||
const { id } = await setup({
|
||||
skipAmpMock: true,
|
||||
moduleVariables: {
|
||||
install_via_npm: "true",
|
||||
amp_version: "0.0.1755964909-g31e083",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
external = {
|
||||
source = "hashicorp/external"
|
||||
@@ -55,7 +55,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
default = "v0.11.1"
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
@@ -140,7 +140,7 @@ variable "base_amp_config" {
|
||||
type = string
|
||||
description = <<-EOT
|
||||
Base AMP configuration in JSON format. Can be overridden to customize AMP settings.
|
||||
|
||||
|
||||
If empty, defaults enable thinking and todos for autonomous operation. Additional options include:
|
||||
- "amp.permissions": [] (tool permissions)
|
||||
- "amp.tools.stopTimeout": 600 (extend timeout for long operations)
|
||||
@@ -148,7 +148,7 @@ variable "base_amp_config" {
|
||||
- "amp.tools.disable": ["builtin:open"] (disable tools for containers)
|
||||
- "amp.git.commit.ampThread.enabled": true (link commits to threads)
|
||||
- "amp.git.commit.coauthor.enabled": true (add Amp as co-author)
|
||||
|
||||
|
||||
Reference: https://ampcode.com/manual
|
||||
EOT
|
||||
default = ""
|
||||
@@ -160,6 +160,16 @@ variable "mcp" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "mode" {
|
||||
type = string
|
||||
description = "Set the agent mode (free, rush, smart) — controls the model, system prompt, and tool selection. Default: smart"
|
||||
default = "smart"
|
||||
validation {
|
||||
condition = contains(["", "free", "rush", "smart"], var.mode)
|
||||
error_message = "Invalid mode. Select one from (free, rush, smart)"
|
||||
}
|
||||
}
|
||||
|
||||
data "external" "env" {
|
||||
program = ["sh", "-c", "echo '{\"CODER_AGENT_TOKEN\":\"'$CODER_AGENT_TOKEN'\",\"CODER_AGENT_URL\":\"'$CODER_AGENT_URL'\"}'"]
|
||||
}
|
||||
@@ -170,6 +180,7 @@ locals {
|
||||
default_base_config = jsonencode({
|
||||
"amp.anthropic.thinking.enabled" = true
|
||||
"amp.todos.enabled" = true
|
||||
"amp.terminal.animation" = false
|
||||
})
|
||||
|
||||
user_config = jsondecode(var.base_amp_config != "" ? var.base_amp_config : local.default_base_config)
|
||||
@@ -209,7 +220,7 @@ locals {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.workdir
|
||||
@@ -237,6 +248,7 @@ module "agentapi" {
|
||||
ARG_AMP_START_DIRECTORY='${var.workdir}' \
|
||||
ARG_AMP_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_MODE='${var.mode}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
@@ -256,4 +268,6 @@ module "agentapi" {
|
||||
EOT
|
||||
}
|
||||
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
|
||||
# ANSI colors
|
||||
BOLD='\033[1m'
|
||||
GREEN='\033[0;32m'
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Load user environment
|
||||
# shellcheck source=/dev/null
|
||||
source "$HOME/.bashrc"
|
||||
# shellcheck source=/dev/null
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME/.bashrc"
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.nvm/nvm.sh" ]; then
|
||||
source "$HOME/.nvm/nvm.sh"
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="$HOME/.local/bin:$HOME/.amp/bin:$HOME/.npm-global/bin:$PATH"
|
||||
|
||||
function ensure_command() {
|
||||
@@ -27,6 +29,7 @@ echo "--------------------------------"
|
||||
printf "Workspace: %s\n" "$ARG_AMP_START_DIRECTORY"
|
||||
printf "Task Prompt: %s\n" "$ARG_AMP_TASK_PROMPT"
|
||||
printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
|
||||
printf "ARG_MODE: %s\n" "$ARG_MODE"
|
||||
echo "--------------------------------"
|
||||
|
||||
ensure_command amp
|
||||
@@ -48,6 +51,13 @@ else
|
||||
printf "amp_api_key not provided\n"
|
||||
fi
|
||||
|
||||
ARGS=()
|
||||
|
||||
if [ -n "$ARG_MODE" ]; then
|
||||
printf "Running agent in: %s mode" "$ARG_MODE"
|
||||
ARGS+=(--mode "$ARG_MODE")
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_AMP_TASK_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" == "true" ]; then
|
||||
printf "amp task prompt provided : %s" "$ARG_AMP_TASK_PROMPT\n"
|
||||
@@ -56,8 +66,8 @@ if [ -n "$ARG_AMP_TASK_PROMPT" ]; then
|
||||
PROMPT="$ARG_AMP_TASK_PROMPT"
|
||||
fi
|
||||
# Pipe the prompt into amp, which will be run inside agentapi
|
||||
agentapi server --type amp --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp"
|
||||
agentapi server --type amp --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp" "${ARGS[@]}"
|
||||
else
|
||||
printf "No task prompt given.\n"
|
||||
agentapi server --type amp --term-width=67 --term-height=1190 -- amp
|
||||
agentapi server --type amp --term-width=67 --term-height=1190 -- amp "${ARGS[@]}"
|
||||
fi
|
||||
|
||||
|
Before Width: | Height: | Size: 976 KiB After Width: | Height: | Size: 528 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 302 KiB After Width: | Height: | Size: 191 KiB |
@@ -4,11 +4,35 @@ import {
|
||||
removeContainer,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
TerraformState,
|
||||
writeFileContainer,
|
||||
} from "~test";
|
||||
import path from "path";
|
||||
import { expect } from "bun:test";
|
||||
|
||||
/**
|
||||
* Extracts all coder_env resources from Terraform state and returns them as
|
||||
* a Record of environment variable names to values.
|
||||
*/
|
||||
export const extractCoderEnvVars = (
|
||||
state: TerraformState,
|
||||
): Record<string, string> => {
|
||||
const envVars: Record<string, string> = {};
|
||||
|
||||
for (const resource of state.resources) {
|
||||
if (resource.type === "coder_env" && resource.instances.length > 0) {
|
||||
const instance = resource.instances[0].attributes;
|
||||
const name = instance.name as string;
|
||||
const value = instance.value as string;
|
||||
if (name && value) {
|
||||
envVars[name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return envVars;
|
||||
};
|
||||
|
||||
export const setupContainer = async ({
|
||||
moduleDir,
|
||||
image,
|
||||
@@ -23,10 +47,12 @@ export const setupContainer = async ({
|
||||
...vars,
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const coderEnvVars = extractCoderEnvVars(state);
|
||||
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
|
||||
return {
|
||||
id,
|
||||
coderScript,
|
||||
coderEnvVars,
|
||||
cleanup: async () => {
|
||||
if (
|
||||
process.env["DEBUG"] === "true" ||
|
||||
@@ -79,9 +105,11 @@ interface SetupProps {
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
export const setup = async (props: SetupProps): Promise<{ id: string }> => {
|
||||
export const setup = async (
|
||||
props: SetupProps,
|
||||
): Promise<{ id: string; coderEnvVars: Record<string, string> }> => {
|
||||
const projectDir = props.projectDir ?? "/home/coder/project";
|
||||
const { id, coderScript, cleanup } = await setupContainer({
|
||||
const { id, coderScript, coderEnvVars, cleanup } = await setupContainer({
|
||||
moduleDir: props.moduleDir,
|
||||
vars: props.moduleVariables,
|
||||
});
|
||||
@@ -101,7 +129,7 @@ export const setup = async (props: SetupProps): Promise<{ id: string }> => {
|
||||
filePath: "/home/coder/script.sh",
|
||||
content: coderScript.script,
|
||||
});
|
||||
return { id };
|
||||
return { id, coderEnvVars };
|
||||
};
|
||||
|
||||
export const expectAgentAPIStarted = async (
|
||||
@@ -125,18 +153,16 @@ 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,
|
||||
);
|
||||
const envArgs = env
|
||||
? Object.entries(env)
|
||||
.map(([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`)
|
||||
.join(" && ") + " && "
|
||||
: "";
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`${envArgs}set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
display_name: Antigravity
|
||||
description: Add a one-click button to launch Google Antigravity
|
||||
icon: ../../../../.icons/antigravity.svg
|
||||
verified: true
|
||||
tags: [ide, antigravity, ai, google]
|
||||
---
|
||||
|
||||
# Antigravity IDE
|
||||
|
||||
Add a button to open any workspace with a single click in [Antigravity IDE](https://antigravity.google).
|
||||
|
||||
Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder).
|
||||
|
||||
```tf
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Open in a specific directory
|
||||
|
||||
```tf
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
### Configure MCP servers for Antigravity
|
||||
|
||||
Provide a JSON-encoded string via the `mcp` input. When set, the module writes the value to `~/.gemini/antigravity/mcp_config.json` using a `coder_script` on workspace start.
|
||||
|
||||
The following example configures Antigravity to use the GitHub MCP server with authentication facilitated by the [`coder_external_auth`](https://coder.com/docs/admin/external-auth#configure-a-github-oauth-app) resource.
|
||||
|
||||
```tf
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
mcpServers = {
|
||||
"github" : {
|
||||
"url" : "https://api.githubcopilot.com/mcp/",
|
||||
"headers" : {
|
||||
"Authorization" : "Bearer ${data.coder_external_auth.github.access_token}",
|
||||
},
|
||||
"type" : "http"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
data "coder_external_auth" "github" {
|
||||
id = "github"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,130 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
runContainer,
|
||||
execContainer,
|
||||
removeContainer,
|
||||
findResourceInstance,
|
||||
readFileContainer,
|
||||
} from "~test";
|
||||
|
||||
describe("antigravity", 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.antigravity_url.value).toBe(
|
||||
"antigravity://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.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
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.antigravity_url.value).toBe(
|
||||
"antigravity://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.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder but not open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
open_recent: "false",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
open_recent: "true",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
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.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
|
||||
it("writes ~/.gemini/antigravity/mcp_config.json when mcp provided", async () => {
|
||||
const id = await runContainer("alpine");
|
||||
try {
|
||||
const mcp = JSON.stringify({
|
||||
servers: { demo: { url: "http://localhost:1234" } },
|
||||
});
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
mcp,
|
||||
});
|
||||
const script = findResourceInstance(
|
||||
state,
|
||||
"coder_script",
|
||||
"antigravity_mcp",
|
||||
).script;
|
||||
const resp = await execContainer(id, ["sh", "-c", script]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
expect(resp.exitCode).toBe(0);
|
||||
const content = await readFileContainer(
|
||||
id,
|
||||
"/root/.gemini/antigravity/mcp_config.json",
|
||||
);
|
||||
expect(content).toBe(mcp);
|
||||
} finally {
|
||||
await removeContainer(id);
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
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 Antigravity 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 = "antigravity"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app."
|
||||
default = "Antigravity IDE"
|
||||
}
|
||||
|
||||
variable "mcp" {
|
||||
type = string
|
||||
description = "JSON-encoded string to configure MCP servers for Antigravity. When set, writes ~/.gemini/antigravity/mcp_config.json."
|
||||
default = ""
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
locals {
|
||||
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
|
||||
}
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
web_app_icon = "/icon/antigravity.svg"
|
||||
web_app_slug = var.slug
|
||||
web_app_display_name = var.display_name
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
protocol = "antigravity"
|
||||
}
|
||||
|
||||
resource "coder_script" "antigravity_mcp" {
|
||||
count = var.mcp != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
display_name = "Antigravity MCP"
|
||||
icon = "/icon/antigravity.svg"
|
||||
run_on_start = true
|
||||
start_blocks_login = false
|
||||
script = <<-EOT
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
mkdir -p "$HOME/.gemini/antigravity"
|
||||
echo -n "${local.mcp_b64}" | base64 -d > "$HOME/.gemini/antigravity/mcp_config.json"
|
||||
chmod 600 "$HOME/.gemini/antigravity/mcp_config.json"
|
||||
EOT
|
||||
}
|
||||
|
||||
output "antigravity_url" {
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "Antigravity IDE URL."
|
||||
}
|
||||
|
||||
@@ -13,7 +13,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 = "4.2.3"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
@@ -45,13 +45,15 @@ This example shows how to configure the Claude Code module to run the agent behi
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "dev.registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
enable_boundary = true
|
||||
boundary_version = "main"
|
||||
boundary_log_dir = "/tmp/boundary_logs"
|
||||
boundary_log_level = "WARN"
|
||||
boundary_additional_allowed_urls = ["GET *google.com"]
|
||||
boundary_proxy_port = "8087"
|
||||
version = "4.2.3"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -70,7 +72,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.3"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@@ -78,8 +80,8 @@ module "claude-code" {
|
||||
# OR
|
||||
claude_code_oauth_token = "xxxxx-xxxx-xxxx"
|
||||
|
||||
claude_code_version = "1.0.82" # Pin to a specific version
|
||||
agentapi_version = "v0.10.0"
|
||||
claude_code_version = "2.0.62" # Pin to a specific version
|
||||
agentapi_version = "0.11.4"
|
||||
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
model = "sonnet"
|
||||
@@ -89,13 +91,10 @@ module "claude-code" {
|
||||
mcp = <<-EOF
|
||||
{
|
||||
"mcpServers": {
|
||||
"memory": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-memory"],
|
||||
"env": {}
|
||||
"my-custom-tool": {
|
||||
"command": "my-tool-server",
|
||||
"args": ["--port", "8080"]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
EOF
|
||||
@@ -109,13 +108,12 @@ Run and configure Claude Code as a standalone CLI in your workspace.
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.3"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder"
|
||||
workdir = "/home/coder/project"
|
||||
install_claude_code = true
|
||||
claude_code_version = "latest"
|
||||
claude_code_version = "2.0.62"
|
||||
report_tasks = false
|
||||
cli_app = true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -132,7 +130,7 @@ variable "claude_code_oauth_token" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.3"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_code_oauth_token = var.claude_code_oauth_token
|
||||
@@ -205,7 +203,7 @@ resource "coder_env" "bedrock_api_key" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.3"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
@@ -262,7 +260,7 @@ resource "coder_env" "google_application_credentials" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.3"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "claude-sonnet-4@20250514"
|
||||
|
||||
@@ -39,9 +39,11 @@ interface SetupProps {
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const setup = async (
|
||||
props?: SetupProps,
|
||||
): Promise<{ id: string; coderEnvVars: Record<string, string> }> => {
|
||||
const projectDir = "/home/coder/project";
|
||||
const { id } = await setupUtil({
|
||||
const { id, coderEnvVars } = await setupUtil({
|
||||
moduleDir: import.meta.dir,
|
||||
moduleVariables: {
|
||||
install_claude_code: props?.skipClaudeMock ? "true" : "false",
|
||||
@@ -61,7 +63,7 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
content: await loadTestFile(import.meta.dir, "claude-mock.sh"),
|
||||
});
|
||||
}
|
||||
return { id };
|
||||
return { id, coderEnvVars };
|
||||
};
|
||||
|
||||
setDefaultTimeout(60 * 1000);
|
||||
@@ -79,14 +81,14 @@ describe("claude-code", async () => {
|
||||
|
||||
test("install-claude-code-version", async () => {
|
||||
const version_to_install = "1.0.40";
|
||||
const { id } = await setup({
|
||||
const { id, coderEnvVars } = await setup({
|
||||
skipClaudeMock: true,
|
||||
moduleVariables: {
|
||||
install_claude_code: "true",
|
||||
claude_code_version: version_to_install,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await execModuleScript(id, coderEnvVars);
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
@@ -96,14 +98,14 @@ describe("claude-code", async () => {
|
||||
});
|
||||
|
||||
test("check-latest-claude-code-version-works", async () => {
|
||||
const { id } = await setup({
|
||||
const { id, coderEnvVars } = await setup({
|
||||
skipClaudeMock: true,
|
||||
skipAgentAPIMock: true,
|
||||
moduleVariables: {
|
||||
install_claude_code: "true",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await execModuleScript(id, coderEnvVars);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
@@ -133,13 +135,13 @@ describe("claude-code", async () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const { id } = await setup({
|
||||
const { id, coderEnvVars } = await setup({
|
||||
skipClaudeMock: true,
|
||||
moduleVariables: {
|
||||
mcp: mcpConfig,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await execModuleScript(id, coderEnvVars);
|
||||
|
||||
const resp = await readFileContainer(id, "/home/coder/.claude.json");
|
||||
expect(resp).toContain("test-cmd");
|
||||
@@ -208,13 +210,17 @@ describe("claude-code", async () => {
|
||||
});
|
||||
|
||||
// Create a mock task session file with the hardcoded task session ID
|
||||
// Note: Claude CLI creates files without "session-" prefix when using --session-id
|
||||
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`touch ${sessionDir}/session-${taskSessionId}.jsonl`,
|
||||
`cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF'
|
||||
{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"}
|
||||
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
|
||||
SESSIONEOF`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
@@ -226,46 +232,10 @@ describe("claude-code", async () => {
|
||||
]);
|
||||
expect(startLog.stdout).toContain("--resume");
|
||||
expect(startLog.stdout).toContain(taskSessionId);
|
||||
expect(startLog.stdout).toContain("Resuming existing task session");
|
||||
expect(startLog.stdout).toContain("Resuming task session");
|
||||
expect(startLog.stdout).toContain("--dangerously-skip-permissions");
|
||||
});
|
||||
|
||||
test("claude-continue-resume-standalone-session", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "false",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
const sessionId = "some-random-session-id";
|
||||
const workdir = "/home/coder/project";
|
||||
const claudeJson = {
|
||||
projects: {
|
||||
[workdir]: {
|
||||
lastSessionId: sessionId,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`echo '${JSON.stringify(claudeJson)}' > /home/coder/.claude.json`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain("--continue");
|
||||
expect(startLog.stdout).toContain("Resuming existing session");
|
||||
});
|
||||
|
||||
test("pre-post-install-scripts", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
@@ -360,4 +330,140 @@ describe("claude-code", async () => {
|
||||
"ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat",
|
||||
);
|
||||
});
|
||||
|
||||
test("partial-initialization-detection", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "true",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
|
||||
// Should start new session, not try to resume invalid one
|
||||
expect(startLog.stdout).toContain("Starting new task session");
|
||||
expect(startLog.stdout).toContain("--session-id");
|
||||
});
|
||||
|
||||
test("standalone-first-build-no-sessions", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "false",
|
||||
},
|
||||
});
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
|
||||
// Should start fresh, not try to continue
|
||||
expect(startLog.stdout).toContain("No sessions found");
|
||||
expect(startLog.stdout).toContain("starting fresh standalone session");
|
||||
expect(startLog.stdout).not.toContain("--continue");
|
||||
});
|
||||
|
||||
test("standalone-with-sessions-continues", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "false",
|
||||
},
|
||||
});
|
||||
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat > ${sessionDir}/generic-123.jsonl << 'EOF'
|
||||
{"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"}
|
||||
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
|
||||
EOF`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
|
||||
// Should continue existing session
|
||||
expect(startLog.stdout).toContain("Sessions found");
|
||||
expect(startLog.stdout).toContain(
|
||||
"Continuing most recent standalone session",
|
||||
);
|
||||
expect(startLog.stdout).toContain("--continue");
|
||||
});
|
||||
|
||||
test("task-mode-ignores-manual-sessions", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "true",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
|
||||
// Create task session (without "session-" prefix, as CLI does)
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF'
|
||||
{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"}
|
||||
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
|
||||
EOF`,
|
||||
]);
|
||||
|
||||
// Create manual session (newer)
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat > ${sessionDir}/manual-456.jsonl << 'EOF'
|
||||
{"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"}
|
||||
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"}
|
||||
EOF`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
|
||||
// Should resume task session, not manual session
|
||||
expect(startLog.stdout).toContain("Resuming task session");
|
||||
expect(startLog.stdout).toContain(taskSessionId);
|
||||
expect(startLog.stdout).not.toContain("manual-456");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,7 +86,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
default = "v0.11.4"
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
@@ -288,15 +288,20 @@ resource "coder_env" "disable_autoupdater" {
|
||||
value = "1"
|
||||
}
|
||||
|
||||
resource "coder_env" "claude_binary_path" {
|
||||
agent_id = var.agent_id
|
||||
name = "PATH"
|
||||
value = "$HOME/.local/bin:$PATH"
|
||||
}
|
||||
|
||||
locals {
|
||||
# we have to trim the slash because otherwise coder exp mcp will
|
||||
# set up an invalid claude config
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
app_slug = "ccw"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".claude-module"
|
||||
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
app_slug = "ccw"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".claude-module"
|
||||
# Extract hostname from access_url for boundary --allow flag
|
||||
coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "")
|
||||
|
||||
@@ -357,9 +362,7 @@ module "agentapi" {
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "/tmp/remove-last-session-id.sh"
|
||||
chmod +x /tmp/start.sh
|
||||
chmod +x /tmp/remove-last-session-id.sh
|
||||
|
||||
ARG_MODEL='${var.model}' \
|
||||
ARG_RESUME_SESSION_ID='${var.resume_session_id}' \
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
# Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles
|
||||
set -euo pipefail
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
@@ -45,11 +40,6 @@ function install_claude_code_cli() {
|
||||
if [ $CURL_EXIT -ne 0 ]; then
|
||||
echo "Claude Code installer failed with exit code $$CURL_EXIT"
|
||||
fi
|
||||
|
||||
# Ensure binaries are discoverable.
|
||||
echo "Creating a symlink for claude"
|
||||
sudo ln -s /home/coder/.local/bin/claude /usr/local/bin/claude
|
||||
|
||||
echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')"
|
||||
else
|
||||
echo "Skipping Claude Code installation as per configuration."
|
||||
@@ -90,12 +80,63 @@ function setup_claude_configurations() {
|
||||
|
||||
}
|
||||
|
||||
function configure_standalone_mode() {
|
||||
echo "Configuring Claude Code for standalone mode..."
|
||||
|
||||
if [ -z "${CLAUDE_API_KEY:-}" ]; then
|
||||
echo "Note: CLAUDE_API_KEY not set, skipping authentication setup"
|
||||
return
|
||||
fi
|
||||
|
||||
local claude_config="$HOME/.claude.json"
|
||||
local workdir_normalized
|
||||
workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
|
||||
|
||||
# Create or update .claude.json with minimal configuration for API key auth
|
||||
# This skips the interactive login prompt and onboarding screens
|
||||
if [ -f "$claude_config" ]; then
|
||||
echo "Updating existing Claude configuration at $claude_config"
|
||||
|
||||
jq --arg apikey "${CLAUDE_API_KEY:-}" \
|
||||
--arg workdir "$ARG_WORKDIR" \
|
||||
'.autoUpdaterStatus = "disabled" |
|
||||
.bypassPermissionsModeAccepted = true |
|
||||
.hasAcknowledgedCostThreshold = true |
|
||||
.hasCompletedOnboarding = true |
|
||||
.primaryApiKey = $apikey |
|
||||
.projects[$workdir].hasCompletedProjectOnboarding = true |
|
||||
.projects[$workdir].hasTrustDialogAccepted = true' \
|
||||
"$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config"
|
||||
else
|
||||
echo "Creating new Claude configuration at $claude_config"
|
||||
cat > "$claude_config" << EOF
|
||||
{
|
||||
"autoUpdaterStatus": "disabled",
|
||||
"bypassPermissionsModeAccepted": true,
|
||||
"hasAcknowledgedCostThreshold": true,
|
||||
"hasCompletedOnboarding": true,
|
||||
"primaryApiKey": "${CLAUDE_API_KEY:-}",
|
||||
"projects": {
|
||||
"$ARG_WORKDIR": {
|
||||
"hasCompletedProjectOnboarding": true,
|
||||
"hasTrustDialogAccepted": true
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "Standalone mode configured successfully"
|
||||
}
|
||||
|
||||
function report_tasks() {
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
echo "Configuring Claude Code to report tasks via Coder MCP..."
|
||||
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
coder exp mcp configure claude-code "$ARG_WORKDIR"
|
||||
else
|
||||
configure_standalone_mode
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# If lastSessionId is present in .claude.json, claude --continue will start a
|
||||
# conversation starting from that session. The problem is that lastSessionId
|
||||
# doesn't always point to the last session. The field is updated by claude only
|
||||
# at the point of normal CLI exit. If Claude exits with an error, or if the user
|
||||
# restarts the Coder workspace, lastSessionId will be stale, and claude --continue
|
||||
# will start from an old session.
|
||||
#
|
||||
# If lastSessionId is missing, claude seems to accurately figure out where to
|
||||
# start using the conversation history - even if the CLI previously exited with
|
||||
# an error.
|
||||
#
|
||||
# This script removes the lastSessionId field from .claude.json.
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "No working directory provided - it must be the first argument"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get absolute path of working directory
|
||||
working_dir=$(realpath "$1")
|
||||
echo "workingDir $working_dir"
|
||||
|
||||
# Path to .claude.json
|
||||
claude_json_path="$HOME/.claude.json"
|
||||
echo ".claude.json path $claude_json_path"
|
||||
|
||||
# Check if .claude.json exists
|
||||
if [ ! -f "$claude_json_path" ]; then
|
||||
echo "No .claude.json file found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use jq to check if lastSessionId exists for the working directory and remove it
|
||||
|
||||
if jq -e ".projects[\"$working_dir\"].lastSessionId" "$claude_json_path" > /dev/null 2>&1; then
|
||||
# Remove lastSessionId and update the file
|
||||
if jq "del(.projects[\"$working_dir\"].lastSessionId)" "$claude_json_path" > "${claude_json_path}.tmp" && mv "${claude_json_path}.tmp" "$claude_json_path"; then
|
||||
echo "Removed lastSessionId from .claude.json"
|
||||
exit 0
|
||||
else
|
||||
echo "Failed to remove lastSessionId from .claude.json"
|
||||
fi
|
||||
else
|
||||
echo "No lastSessionId found in .claude.json - nothing to do"
|
||||
fi
|
||||
@@ -1,14 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
# Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
@@ -51,19 +44,6 @@ printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST"
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
# Clean up stale session data (see remove-last-session-id.sh for details)
|
||||
CAN_CONTINUE_CONVERSATION=false
|
||||
set +e
|
||||
bash "/tmp/remove-last-session-id.sh" "$(pwd)" 2> /dev/null
|
||||
session_cleanup_exit_code=$?
|
||||
set -e
|
||||
|
||||
case $session_cleanup_exit_code in
|
||||
0)
|
||||
CAN_CONTINUE_CONVERSATION=true
|
||||
;;
|
||||
esac
|
||||
|
||||
function install_boundary() {
|
||||
if [ "${ARG_COMPILE_FROM_SOURCE:-false}" = "true" ]; then
|
||||
# Install boundary by compiling from source
|
||||
@@ -99,17 +79,85 @@ function validate_claude_installation() {
|
||||
# This ensures all task sessions use a consistent, predictable ID
|
||||
TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2"
|
||||
|
||||
get_project_dir() {
|
||||
local workdir_normalized
|
||||
workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
|
||||
echo "$HOME/.claude/projects/${workdir_normalized}"
|
||||
}
|
||||
|
||||
get_task_session_file() {
|
||||
echo "$(get_project_dir)/${TASK_SESSION_ID}.jsonl"
|
||||
}
|
||||
|
||||
task_session_exists() {
|
||||
local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
|
||||
local project_dir="$HOME/.claude/projects/${workdir_normalized}"
|
||||
local session_file
|
||||
session_file=$(get_task_session_file)
|
||||
|
||||
printf "PROJECT_DIR: %s, workdir_normalized: %s\n" "$project_dir" "$workdir_normalized"
|
||||
|
||||
if [ -d "$project_dir" ] && find "$project_dir" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then
|
||||
printf "TASK_SESSION_ID: %s file found\n" "$TASK_SESSION_ID"
|
||||
if [ -f "$session_file" ]; then
|
||||
printf "Task session file found: %s\n" "$session_file"
|
||||
return 0
|
||||
else
|
||||
printf "TASK_SESSION_ID: %s file not found\n" "$TASK_SESSION_ID"
|
||||
printf "Task session file not found: %s\n" "$session_file"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_valid_session() {
|
||||
local session_file="$1"
|
||||
|
||||
# Check if file exists and is not empty
|
||||
# Empty files indicate the session was created but never used so they need to be removed
|
||||
if [ ! -f "$session_file" ]; then
|
||||
printf "Session validation failed: file does not exist\n"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -s "$session_file" ]; then
|
||||
printf "Session validation failed: file is empty, removing stale file\n"
|
||||
rm -f "$session_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for minimum session content
|
||||
# Valid sessions need at least 2 lines: initial message and first response
|
||||
local line_count
|
||||
line_count=$(wc -l < "$session_file")
|
||||
if [ "$line_count" -lt 2 ]; then
|
||||
printf "Session validation failed: incomplete (only %s lines), removing incomplete file\n" "$line_count"
|
||||
rm -f "$session_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Validate JSONL format by checking first 3 lines
|
||||
# Claude session files use JSONL (JSON Lines) format where each line is valid JSON
|
||||
if ! head -3 "$session_file" | jq empty 2> /dev/null; then
|
||||
printf "Session validation failed: invalid JSONL format, removing corrupt file\n"
|
||||
rm -f "$session_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify the session has a valid sessionId field
|
||||
# This ensures the file structure matches Claude's session format
|
||||
if ! grep -q '"sessionId"' "$session_file" \
|
||||
|| ! grep -m 1 '"sessionId"' "$session_file" | jq -e '.sessionId' > /dev/null 2>&1; then
|
||||
printf "Session validation failed: no valid sessionId found, removing malformed file\n"
|
||||
rm -f "$session_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "Session validation passed: %s\n" "$session_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
has_any_sessions() {
|
||||
local project_dir
|
||||
project_dir=$(get_project_dir)
|
||||
|
||||
if [ -d "$project_dir" ] && find "$project_dir" -maxdepth 1 -name "*.jsonl" -size +0c 2> /dev/null | grep -q .; then
|
||||
printf "Sessions found in: %s\n" "$project_dir"
|
||||
return 0
|
||||
else
|
||||
printf "No sessions found in: %s\n" "$project_dir"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -132,75 +180,41 @@ function start_agentapi() {
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_RESUME_SESSION_ID" ]; then
|
||||
echo "Resuming task session by ID: $ARG_RESUME_SESSION_ID"
|
||||
echo "Resuming specified session: $ARG_RESUME_SESSION_ID"
|
||||
ARGS+=(--resume "$ARG_RESUME_SESSION_ID")
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
|
||||
|
||||
elif [ "$ARG_CONTINUE" = "true" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] && task_session_exists; then
|
||||
echo "Task session detected (ID: $TASK_SESSION_ID)"
|
||||
ARGS+=(--resume "$TASK_SESSION_ID")
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
echo "Resuming existing task session"
|
||||
elif [ "$ARG_REPORT_TASKS" = "false" ] && [ "$CAN_CONTINUE_CONVERSATION" = true ]; then
|
||||
echo "Previous session exists"
|
||||
ARGS+=(--continue)
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Resuming existing session"
|
||||
else
|
||||
echo "No existing session found"
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
if task_session_exists; then
|
||||
ARGS+=(--resume "$TASK_SESSION_ID")
|
||||
else
|
||||
ARGS+=(--session-id "$TASK_SESSION_ID")
|
||||
fi
|
||||
fi
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions -- "$ARG_AI_PROMPT")
|
||||
else
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
echo "Starting new session with prompt"
|
||||
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
local session_file
|
||||
session_file=$(get_task_session_file)
|
||||
|
||||
if task_session_exists && is_valid_session "$session_file"; then
|
||||
echo "Resuming task session: $TASK_SESSION_ID"
|
||||
ARGS+=(--resume "$TASK_SESSION_ID" --dangerously-skip-permissions)
|
||||
else
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Starting new session"
|
||||
echo "Starting new task session: $TASK_SESSION_ID"
|
||||
ARGS+=(--session-id "$TASK_SESSION_ID" --dangerously-skip-permissions)
|
||||
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
|
||||
else
|
||||
if has_any_sessions; then
|
||||
echo "Continuing most recent standalone session"
|
||||
ARGS+=(--continue)
|
||||
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
|
||||
else
|
||||
echo "No sessions found, starting fresh standalone session"
|
||||
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
|
||||
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
echo "Continue disabled, starting fresh session"
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
if task_session_exists; then
|
||||
ARGS+=(--resume "$TASK_SESSION_ID")
|
||||
else
|
||||
ARGS+=(--session-id "$TASK_SESSION_ID")
|
||||
fi
|
||||
fi
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions -- "$ARG_AI_PROMPT")
|
||||
else
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
echo "Starting new session with prompt"
|
||||
else
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Starting claude code session"
|
||||
fi
|
||||
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
|
||||
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
|
||||
printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
|
||||
@@ -226,15 +240,15 @@ function start_agentapi() {
|
||||
fi
|
||||
|
||||
# Set HTTP Proxy port used by Boundary
|
||||
BOUNDARY_ARGS+=(--proxy-port $ARG_BOUNDARY_PROXY_PORT)
|
||||
BOUNDARY_ARGS+=(--proxy-port "$ARG_BOUNDARY_PROXY_PORT")
|
||||
|
||||
# Set log level for boundary
|
||||
BOUNDARY_ARGS+=(--log-level $ARG_BOUNDARY_LOG_LEVEL)
|
||||
BOUNDARY_ARGS+=(--log-level "$ARG_BOUNDARY_LOG_LEVEL")
|
||||
|
||||
if [ "${ARG_ENABLE_BOUNDARY_PPROF:-false}" = "true" ]; then
|
||||
# Enable boundary pprof server on specified port
|
||||
BOUNDARY_ARGS+=(--pprof)
|
||||
BOUNDARY_ARGS+=(--pprof-port ${ARG_BOUNDARY_PPROF_PORT})
|
||||
BOUNDARY_ARGS+=(--pprof-port "$ARG_BOUNDARY_PPROF_PORT")
|
||||
fi
|
||||
|
||||
agentapi server --type claude --term-width 67 --term-height 1190 -- \
|
||||
|
||||
@@ -15,7 +15,7 @@ module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -30,8 +30,8 @@ module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
agent_id = coder_agent.main.id
|
||||
install_version = "4.8.3"
|
||||
agent_id = coder_agent.example.id
|
||||
install_version = "1.4.1"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -44,7 +44,7 @@ module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = [
|
||||
"dracula-theme.theme-dracula"
|
||||
]
|
||||
@@ -62,12 +62,11 @@ module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula"]
|
||||
settings = {
|
||||
"workbench.colorTheme" = "Dracula"
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
@@ -80,7 +79,7 @@ module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
|
||||
}
|
||||
```
|
||||
@@ -94,7 +93,7 @@ module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_id = coder_agent.example.id
|
||||
additional_args = "--disable-workspace-trust"
|
||||
}
|
||||
```
|
||||
@@ -110,7 +109,7 @@ module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_id = coder_agent.example.id
|
||||
use_cached = true
|
||||
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
|
||||
}
|
||||
@@ -123,7 +122,7 @@ module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_id = coder_agent.example.id
|
||||
offline = true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -88,6 +88,7 @@ function extension_installed() {
|
||||
if [ "${USE_CACHED_EXTENSIONS}" != true ]; then
|
||||
return 1
|
||||
fi
|
||||
# shellcheck disable=SC2066
|
||||
for _extension in "$${EXTENSIONS_ARRAY[@]}"; do
|
||||
if [ "$_extension" == "$1" ]; then
|
||||
echo "Extension $1 was already installed."
|
||||
@@ -99,6 +100,7 @@ function extension_installed() {
|
||||
|
||||
# Install each extension...
|
||||
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
|
||||
# shellcheck disable=SC2066
|
||||
for extension in "$${EXTENSIONLIST[@]}"; do
|
||||
if [ -z "$extension" ]; then
|
||||
continue
|
||||
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.3.3"
|
||||
version = "1.4.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "cursor" {
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.3.3"
|
||||
version = "1.4.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -45,7 +45,7 @@ The following example configures Cursor to use the GitHub MCP server with authen
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.3.3"
|
||||
version = "1.4.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
@@ -58,6 +58,7 @@ module "cursor" {
|
||||
"type" : "http"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,7 +26,10 @@ describe("cursor", async () => {
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "cursor",
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
@@ -76,21 +79,6 @@ describe("cursor", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
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 === "cursor",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
|
||||
it("writes ~/.cursor/mcp.json when mcp provided", async () => {
|
||||
const id = await runContainer("alpine");
|
||||
try {
|
||||
|
||||
@@ -64,26 +64,21 @@ locals {
|
||||
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
|
||||
}
|
||||
|
||||
resource "coder_app" "cursor" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/cursor.svg"
|
||||
slug = var.slug
|
||||
display_name = var.display_name
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = join("", [
|
||||
"cursor://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",
|
||||
])
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
coder_app_icon = "/icon/cursor.svg"
|
||||
coder_app_slug = var.slug
|
||||
coder_app_display_name = var.display_name
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
protocol = "cursor"
|
||||
}
|
||||
|
||||
resource "coder_script" "cursor_mcp" {
|
||||
@@ -103,6 +98,6 @@ resource "coder_script" "cursor_mcp" {
|
||||
}
|
||||
|
||||
output "cursor_url" {
|
||||
value = coder_app.cursor.url
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "Cursor IDE Desktop URL."
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ The devcontainers-cli module provides an easy way to install [`@devcontainers/cl
|
||||
```tf
|
||||
module "devcontainers-cli" {
|
||||
source = "registry.coder.com/coder/devcontainers-cli/coder"
|
||||
version = "1.0.33"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.0.34"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# might contain a `package.json` with `packageManager` set to something
|
||||
# other than the detected package manager. When this happens, it can
|
||||
# cause the installation to fail.
|
||||
cd "$CODER_SCRIPT_DATA_DIR"
|
||||
cd "$CODER_SCRIPT_DATA_DIR" || exit
|
||||
|
||||
# If @devcontainers/cli is already installed, we can skip
|
||||
if command -v devcontainer > /dev/null 2>&1; then
|
||||
|
||||
@@ -18,8 +18,8 @@ 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.2"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -31,8 +31,8 @@ module "dotfiles" {
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.2.2"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -42,8 +42,8 @@ module "dotfiles" {
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.2.2"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
user = "root"
|
||||
}
|
||||
```
|
||||
@@ -54,16 +54,15 @@ module "dotfiles" {
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.2.2"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
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.2"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
user = "root"
|
||||
dotfiles_uri = module.dotfiles.dotfiles_uri
|
||||
}
|
||||
@@ -77,8 +76,8 @@ 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.2"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
default_dotfiles_uri = "https://github.com/coder/dotfiles"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -5,6 +5,7 @@ set -euo pipefail
|
||||
DOTFILES_URI="${DOTFILES_URI}"
|
||||
DOTFILES_USER="${DOTFILES_USER}"
|
||||
|
||||
# shellcheck disable=SC2157
|
||||
if [ -n "$${DOTFILES_URI// }" ]; then
|
||||
if [ -z "$DOTFILES_USER" ]; then
|
||||
DOTFILES_USER="$USER"
|
||||
|
||||
@@ -14,8 +14,8 @@ 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.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
}
|
||||
```
|
||||
@@ -28,8 +28,8 @@ module "git-clone" {
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
base_dir = "~/projects/coder"
|
||||
}
|
||||
@@ -43,8 +43,8 @@ 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.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ 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.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = data.coder_parameter.git_repo.value
|
||||
}
|
||||
|
||||
@@ -80,8 +80,8 @@ module "git_clone" {
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
order = 1
|
||||
folder = "/home/${local.username}/${module.git_clone[count.index].folder_name}"
|
||||
}
|
||||
@@ -89,7 +89,7 @@ module "code-server" {
|
||||
# Create a Coder app for the website
|
||||
resource "coder_app" "website" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
agent_id = coder_agent.main.id
|
||||
agent_id = coder_agent.example.id
|
||||
order = 2
|
||||
slug = "website"
|
||||
external = true
|
||||
@@ -105,8 +105,8 @@ 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.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.example.com/coder/coder/tree/feat/example"
|
||||
git_providers = {
|
||||
"https://github.example.com/" = {
|
||||
@@ -125,8 +125,8 @@ 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.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
|
||||
}
|
||||
```
|
||||
@@ -137,8 +137,8 @@ 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.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
|
||||
git_providers = {
|
||||
"https://gitlab.example.com/" = {
|
||||
@@ -159,8 +159,8 @@ 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.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
branch_name = "feat/example"
|
||||
}
|
||||
@@ -177,8 +177,8 @@ 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.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
folder_name = "coder-dev"
|
||||
base_dir = "~/projects/coder"
|
||||
@@ -195,9 +195,9 @@ 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.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
depth = 1
|
||||
}
|
||||
@@ -212,8 +212,8 @@ This is useful for running initialization tasks like installing dependencies or
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
post_clone_script = <<-EOT
|
||||
#!/bin/bash
|
||||
|
||||
@@ -58,9 +58,10 @@ fi
|
||||
# Run post-clone script if provided
|
||||
if [ -n "$POST_CLONE_SCRIPT" ]; then
|
||||
echo "Running post-clone script..."
|
||||
echo "$POST_CLONE_SCRIPT" | base64 -d > /tmp/post_clone.sh
|
||||
chmod +x /tmp/post_clone.sh
|
||||
cd "$CLONE_PATH"
|
||||
/tmp/post_clone.sh
|
||||
rm /tmp/post_clone.sh
|
||||
POST_CLONE_TMP=$(mktemp)
|
||||
echo "$POST_CLONE_SCRIPT" | base64 -d > "$POST_CLONE_TMP"
|
||||
chmod +x "$POST_CLONE_TMP"
|
||||
cd "$CLONE_PATH" || exit
|
||||
$POST_CLONE_TMP
|
||||
rm "$POST_CLONE_TMP"
|
||||
fi
|
||||
|
||||
@@ -16,7 +16,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jfrog-oauth/coder"
|
||||
version = "1.2.3"
|
||||
version = "1.2.4"
|
||||
agent_id = coder_agent.main.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
|
||||
@@ -57,7 +57,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jfrog-oauth/coder"
|
||||
version = "1.2.3"
|
||||
version = "1.2.4"
|
||||
agent_id = coder_agent.main.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "email"
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
# Test for jfrog-oauth module
|
||||
|
||||
run "test_required_vars" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
# Mock external auth with valid access token for basic test
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run "test_empty_access_token_fails" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
# Mock external auth with empty access token
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = ""
|
||||
}
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
resource.coder_script.jfrog
|
||||
]
|
||||
}
|
||||
|
||||
run "test_valid_access_token_succeeds" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
# Mock external auth with valid access token
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify the script resource is created
|
||||
assert {
|
||||
condition = resource.coder_script.jfrog.agent_id == "test-agent-id"
|
||||
error_message = "coder_script agent_id should match the input variable"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.jfrog.display_name == "jfrog"
|
||||
error_message = "coder_script display_name should be 'jfrog'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_jfrog_url_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "invalid-url"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.jfrog_url
|
||||
]
|
||||
}
|
||||
|
||||
run "test_username_field_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "invalid"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.username_field
|
||||
]
|
||||
}
|
||||
|
||||
run "test_with_npm_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
npm = ["global", "@foo:foo", "@bar:bar"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.jfrog.run_on_start == true
|
||||
error_message = "coder_script should run on start"
|
||||
}
|
||||
|
||||
# Verify npm configuration is in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf npmc --global --repo-resolve \"global\"")
|
||||
error_message = "script should contain jf npmc command for npm"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "@foo:registry=https://example.jfrog.io/artifactory/api/npm/foo")
|
||||
error_message = "script should contain scoped npm registry for @foo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "@bar:registry=https://example.jfrog.io/artifactory/api/npm/bar")
|
||||
error_message = "script should contain scoped npm registry for @bar"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_configure_code_server" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
configure_code_server = true
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# When configure_code_server is true, env vars should be created
|
||||
assert {
|
||||
condition = length(resource.coder_env.jfrog_ide_url) == 1
|
||||
error_message = "coder_env.jfrog_ide_url should be created when configure_code_server is true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.jfrog_ide_access_token) == 1
|
||||
error_message = "coder_env.jfrog_ide_access_token should be created when configure_code_server is true"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_go_proxy_env" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
go = ["foo", "bar", "baz"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# When go package manager is configured, GOPROXY env should be set
|
||||
assert {
|
||||
condition = length(resource.coder_env.goproxy) == 1
|
||||
error_message = "coder_env.goproxy should be created when go package manager is configured"
|
||||
}
|
||||
|
||||
# Verify GOPROXY contains all repos
|
||||
assert {
|
||||
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/foo")
|
||||
error_message = "GOPROXY should contain foo repo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/bar")
|
||||
error_message = "GOPROXY should contain bar repo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/baz")
|
||||
error_message = "GOPROXY should contain baz repo"
|
||||
}
|
||||
|
||||
# Verify script contains go configuration
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf goc --global --repo-resolve \"foo\"")
|
||||
error_message = "script should contain jf goc command"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_pypi_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
pypi = ["global", "foo", "bar"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify pip configuration in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf pipc --global --repo-resolve \"global\"")
|
||||
error_message = "script should contain jf pipc command"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "index-url = https://default:valid-token-value@example.jfrog.io/artifactory/api/pypi/global/simple")
|
||||
error_message = "script should contain pip index-url configuration"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "extra-index-url")
|
||||
error_message = "script should contain extra-index-url for additional repos"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_docker_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
docker = ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify docker registration commands in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"foo.jfrog.io\"")
|
||||
error_message = "script should contain register_docker for foo.jfrog.io"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"bar.jfrog.io\"")
|
||||
error_message = "script should contain register_docker for bar.jfrog.io"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"baz.jfrog.io\"")
|
||||
error_message = "script should contain register_docker for baz.jfrog.io"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_conda_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
conda = ["conda-main", "conda-secondary", "conda-local"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify conda configuration in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "channels:")
|
||||
error_message = "script should contain conda channels configuration"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-main")
|
||||
error_message = "script should contain conda-main channel"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-secondary")
|
||||
error_message = "script should contain conda-secondary channel"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-local")
|
||||
error_message = "script should contain conda-local channel"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_maven_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
maven = ["central", "snapshots", "local"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify maven jf mvnc command
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf mvnc --global")
|
||||
error_message = "script should contain jf mvnc command"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "--repo-resolve-releases \"central\"")
|
||||
error_message = "script should contain repo-resolve-releases for central"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "--repo-resolve-snapshots \"central\"")
|
||||
error_message = "script should contain repo-resolve-snapshots for central"
|
||||
}
|
||||
|
||||
# Verify settings.xml content
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<servers>")
|
||||
error_message = "script should contain maven servers configuration"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<id>central</id>")
|
||||
error_message = "script should contain central server id"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<id>snapshots</id>")
|
||||
error_message = "script should contain snapshots server id"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<id>local</id>")
|
||||
error_message = "script should contain local server id"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<url>https://example.jfrog.io/artifactory/central</url>")
|
||||
error_message = "script should contain central repository URL"
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
findResourceInstance,
|
||||
runTerraformInit,
|
||||
runTerraformApply,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("jfrog-oauth", async () => {
|
||||
type TestVariables = {
|
||||
agent_id: string;
|
||||
jfrog_url: string;
|
||||
package_managers: string;
|
||||
|
||||
username_field?: string;
|
||||
jfrog_server_id?: string;
|
||||
external_auth_id?: string;
|
||||
configure_code_server?: boolean;
|
||||
};
|
||||
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
const fakeFrogApi = "localhost:8081/artifactory/api";
|
||||
const fakeFrogUrl = "http://localhost:8081";
|
||||
const user = "default";
|
||||
|
||||
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: "{}",
|
||||
});
|
||||
|
||||
it("generates an npmrc with scoped repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
npm: ["global", "@foo:foo", "@bar:bar"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const npmrcStanza = `cat << EOF > ~/.npmrc
|
||||
email=${user}@example.com
|
||||
registry=http://${fakeFrogApi}/npm/global
|
||||
//${fakeFrogApi}/npm/global/:_authToken=
|
||||
@foo:registry=http://${fakeFrogApi}/npm/foo
|
||||
//${fakeFrogApi}/npm/foo/:_authToken=
|
||||
@bar:registry=http://${fakeFrogApi}/npm/bar
|
||||
//${fakeFrogApi}/npm/bar/:_authToken=
|
||||
|
||||
EOF`;
|
||||
expect(coderScript.script).toContain(npmrcStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'jf npmc --global --repo-resolve "global"',
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured npm',
|
||||
);
|
||||
});
|
||||
|
||||
it("generates a pip config with extra-indexes", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
pypi: ["global", "foo", "bar"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const pipStanza = `cat << EOF > ~/.pip/pip.conf
|
||||
[global]
|
||||
index-url = https://${user}:@${fakeFrogApi}/pypi/global/simple
|
||||
extra-index-url =
|
||||
https://${user}:@${fakeFrogApi}/pypi/foo/simple
|
||||
https://${user}:@${fakeFrogApi}/pypi/bar/simple
|
||||
|
||||
EOF`;
|
||||
expect(coderScript.script).toContain(pipStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'jf pipc --global --repo-resolve "global"',
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured pypi',
|
||||
);
|
||||
});
|
||||
|
||||
it("registers multiple docker repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
docker: ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const dockerStanza = ["foo", "bar", "baz"]
|
||||
.map((r) => `register_docker "${r}.jfrog.io"`)
|
||||
.join("\n");
|
||||
expect(coderScript.script).toContain(dockerStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured docker',
|
||||
);
|
||||
});
|
||||
|
||||
it("sets goproxy with multiple repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
go: ["foo", "bar", "baz"],
|
||||
}),
|
||||
});
|
||||
const proxyEnv = findResourceInstance(state, "coder_env", "goproxy");
|
||||
const proxies = ["foo", "bar", "baz"]
|
||||
.map((r) => `https://${user}:@${fakeFrogApi}/go/${r}`)
|
||||
.join(",");
|
||||
expect(proxyEnv.value).toEqual(proxies);
|
||||
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
expect(coderScript.script).toContain(
|
||||
'jf goc --global --repo-resolve "foo"',
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured go',
|
||||
);
|
||||
});
|
||||
|
||||
it("generates a conda config with multiple repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
conda: ["conda-main", "conda-secondary", "conda-local"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const condaStanza = `cat << EOF > ~/.condarc
|
||||
channels:
|
||||
- https://${user}:@${fakeFrogApi}/conda/conda-main
|
||||
- https://${user}:@${fakeFrogApi}/conda/conda-secondary
|
||||
- https://${user}:@${fakeFrogApi}/conda/conda-local
|
||||
- defaults
|
||||
ssl_verify: true
|
||||
|
||||
EOF`;
|
||||
expect(coderScript.script).toContain(condaStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured conda',
|
||||
);
|
||||
});
|
||||
it("generates a maven settings.xml with multiple repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
maven: ["central", "snapshots", "local"],
|
||||
}),
|
||||
});
|
||||
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
|
||||
expect(coderScript.script).toContain("jf mvnc --global");
|
||||
expect(coderScript.script).toContain('--server-id-resolve="0"');
|
||||
expect(coderScript.script).toContain('--repo-resolve-releases "central"');
|
||||
expect(coderScript.script).toContain('--repo-resolve-snapshots "central"');
|
||||
expect(coderScript.script).toContain('--server-id-deploy="0"');
|
||||
expect(coderScript.script).toContain('--repo-deploy-releases "central"');
|
||||
expect(coderScript.script).toContain('--repo-deploy-snapshots "central"');
|
||||
|
||||
expect(coderScript.script).toContain("<servers>");
|
||||
expect(coderScript.script).toContain("<id>central</id>");
|
||||
expect(coderScript.script).toContain("<id>snapshots</id>");
|
||||
expect(coderScript.script).toContain("<id>local</id>");
|
||||
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/central</url>",
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/snapshots</url>",
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/local</url>",
|
||||
);
|
||||
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured maven',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -163,6 +163,13 @@ resource "coder_script" "jfrog" {
|
||||
}
|
||||
))
|
||||
run_on_start = true
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = data.coder_external_auth.jfrog.access_token != ""
|
||||
error_message = "JFrog access token is empty. Please authenticate with JFrog using external auth."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_env" "jfrog_ide_url" {
|
||||
|
||||
@@ -14,8 +14,8 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
|
||||
module "kasmvnc" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kasmvnc/coder"
|
||||
version = "1.2.6"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.7"
|
||||
agent_id = coder_agent.example.id
|
||||
desktop_environment = "xfce"
|
||||
subdomain = true
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ variable "desktop_environment" {
|
||||
description = "Specifies the desktop environment of the workspace. This should be pre-installed on the workspace."
|
||||
|
||||
validation {
|
||||
condition = contains(["xfce", "kde", "gnome", "lxde", "lxqt"], var.desktop_environment)
|
||||
condition = contains(["cinnamon", "mate", "lxde", "lxqt", "kde", "gnome", "xfce", "manual"], var.desktop_environment)
|
||||
error_message = "Invalid desktop environment. Please specify a valid desktop environment."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Exit on error, undefined variables, and pipe failures
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
error() {
|
||||
printf "💀 ERROR: %s\n" "$@"
|
||||
@@ -121,6 +120,9 @@ fi
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
|
||||
set -u
|
||||
|
||||
distro="$ID"
|
||||
distro_version="$VERSION_ID"
|
||||
codename="$VERSION_CODENAME"
|
||||
|
||||
@@ -18,7 +18,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -31,7 +31,7 @@ module "kiro" {
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -47,7 +47,7 @@ The following example configures Kiro to use the GitHub MCP server with authenti
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
@@ -60,6 +60,7 @@ module "kiro" {
|
||||
"type" : "http"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,11 +17,6 @@ run "default_output" {
|
||||
condition = output.kiro_url == "kiro://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN"
|
||||
error_message = "Default kiro_url must match expected value"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.kiro.order == null
|
||||
error_message = "coder_app order must be null by default"
|
||||
}
|
||||
}
|
||||
|
||||
run "adds_folder" {
|
||||
@@ -53,54 +48,6 @@ run "folder_and_open_recent" {
|
||||
}
|
||||
}
|
||||
|
||||
run "custom_slug_display_name" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
slug = "kiro-ai"
|
||||
display_name = "Kiro AI IDE"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.kiro.slug == "kiro-ai"
|
||||
error_message = "coder_app slug must be set to kiro-ai"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.kiro.display_name == "Kiro AI IDE"
|
||||
error_message = "coder_app display_name must be set to Kiro AI IDE"
|
||||
}
|
||||
}
|
||||
|
||||
run "sets_order" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
order = 5
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.kiro.order == 5
|
||||
error_message = "coder_app order must be set to 5"
|
||||
}
|
||||
}
|
||||
|
||||
run "sets_group" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
group = "AI IDEs"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.kiro.group == "AI IDEs"
|
||||
error_message = "coder_app group must be set to AI IDEs"
|
||||
}
|
||||
}
|
||||
|
||||
run "writes_mcp_json" {
|
||||
command = plan
|
||||
|
||||
|
||||
@@ -26,7 +26,10 @@ describe("kiro", async () => {
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "kiro",
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
@@ -55,47 +58,6 @@ describe("kiro", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
it("writes ~/.kiro/settings/mcp.json when mcp provided", async () => {
|
||||
const id = await runContainer("alpine");
|
||||
try {
|
||||
|
||||
@@ -38,18 +38,6 @@ variable "group" {
|
||||
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"
|
||||
}
|
||||
|
||||
variable "mcp" {
|
||||
type = string
|
||||
description = "JSON-encoded string to configure MCP servers for Kiro. When set, writes ~/.kiro/settings/mcp.json."
|
||||
@@ -63,26 +51,21 @@ locals {
|
||||
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
|
||||
}
|
||||
|
||||
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",
|
||||
])
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
coder_app_icon = "/icon/kiro.svg"
|
||||
coder_app_slug = "kiro-ai"
|
||||
coder_app_display_name = "Kiro AI IDE"
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
protocol = "kiro"
|
||||
}
|
||||
|
||||
resource "coder_script" "kiro_mcp" {
|
||||
@@ -102,6 +85,6 @@ resource "coder_script" "kiro_mcp" {
|
||||
}
|
||||
|
||||
output "kiro_url" {
|
||||
value = coder_app.kiro.url
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "Kiro IDE URL."
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,25 @@
|
||||
display_name: mux
|
||||
description: Coding Agent Multiplexer - Run multiple AI agents in parallel
|
||||
icon: ../../../../.icons/mux.svg
|
||||
verified: false
|
||||
verified: true
|
||||
tags: [ai, agents, development, multiplexer]
|
||||
---
|
||||
|
||||
# mux
|
||||
|
||||
Automatically install and run mux in a Coder workspace. By default, the module installs `mux@next` from npm (with a fallback to downloading the npm tarball if npm is unavailable). mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces.
|
||||
Automatically install and run [mux](https://github.com/coder/mux) in a Coder workspace. By default, the module installs `mux@next` from npm (with a fallback to downloading the npm tarball if npm is unavailable). mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces.
|
||||
|
||||
```tf
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Parallel Agent Execution**: Run multiple AI agents simultaneously on different tasks
|
||||
@@ -35,7 +37,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -46,7 +48,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
agent_id = coder_agent.main.id
|
||||
# Default is "latest"; set to a specific version to pin
|
||||
install_version = "0.4.0"
|
||||
@@ -59,7 +61,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
agent_id = coder_agent.main.id
|
||||
port = 8080
|
||||
}
|
||||
@@ -73,7 +75,7 @@ Run an existing copy of mux if found, otherwise install from npm:
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
agent_id = coder_agent.main.id
|
||||
use_cached = true
|
||||
}
|
||||
@@ -87,7 +89,7 @@ Run without installing from the network (requires mux to be pre-installed):
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
agent_id = coder_agent.main.id
|
||||
install = false
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ RESET='\033[0m'
|
||||
MUX_BINARY="${INSTALL_PREFIX}/mux"
|
||||
|
||||
function run_mux() {
|
||||
# Remove stale server lock if present
|
||||
rm -f "$HOME/.mux/server.lock"
|
||||
|
||||
local port_value
|
||||
port_value="${PORT}"
|
||||
if [ -z "$port_value" ]; then
|
||||
|
||||
@@ -14,8 +14,8 @@ Add the `slackme` command to your workspace that DMs you on Slack when your comm
|
||||
module "slackme" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/slackme/coder"
|
||||
version = "1.0.32"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.0.33"
|
||||
agent_id = coder_agent.example.id
|
||||
auth_provider_id = "slack"
|
||||
}
|
||||
```
|
||||
@@ -74,8 +74,8 @@ slackme npm run long-build
|
||||
module "slackme" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/slackme/coder"
|
||||
version = "1.0.32"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.0.33"
|
||||
agent_id = coder_agent.example.id
|
||||
auth_provider_id = "slack"
|
||||
slack_message = <<EOF
|
||||
👋 Hey there from Coder! $COMMAND took $DURATION to execute!
|
||||
|
||||
@@ -73,13 +73,13 @@ fi
|
||||
|
||||
START=$(date +%s%N)
|
||||
# Run all arguments as a command
|
||||
$@
|
||||
"$@"
|
||||
END=$(date +%s%N)
|
||||
DURATION_MS=$${DURATION_MS:-$(((END - START) / 1000000))}
|
||||
PRETTY_DURATION=$(pretty_duration $DURATION_MS)
|
||||
|
||||
set -e
|
||||
COMMAND=$(echo $@)
|
||||
COMMAND=$(echo "$@")
|
||||
SLACK_MESSAGE=$(echo "$SLACK_MESSAGE" | sed "s|\\$COMMAND|$COMMAND|g")
|
||||
SLACK_MESSAGE=$(echo "$SLACK_MESSAGE" | sed "s|\\$DURATION|$PRETTY_DURATION|g")
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
display_name: Vault CLI
|
||||
description: Installs the Hashicorp Vault CLI and optionally configures token authentication
|
||||
icon: ../../../../.icons/vault.svg
|
||||
verified: true
|
||||
tags: [helper, integration, vault, cli]
|
||||
---
|
||||
|
||||
# Vault CLI
|
||||
|
||||
Installs the [Vault](https://www.vaultproject.io/) CLI and optionally configures token authentication. This module focuses on CLI installation and can be used standalone or as a base for other authentication methods.
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The following tools are required in the workspace image:
|
||||
|
||||
- **HTTP client**: `curl`, `wget`, or `busybox` (at least one)
|
||||
- **Archive utility**: `unzip` or `busybox` (at least one)
|
||||
- **jq**: Optional but recommended for reliable JSON parsing (falls back to sed if not available)
|
||||
|
||||
## With Token Authentication
|
||||
|
||||
If you have a Vault token, you can provide it to automatically configure authentication:
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.vault_token # Optional
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Installation (CLI Only)
|
||||
|
||||
Install the Vault CLI without any authentication:
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
### With Specific Version
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_cli_version = "1.15.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Installation Directory
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
install_dir = "/home/coder/bin"
|
||||
}
|
||||
```
|
||||
|
||||
### With Vault Enterprise Namespace
|
||||
|
||||
For Vault Enterprise users who need to specify a namespace:
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.vault_token
|
||||
vault_namespace = "admin/my-namespace"
|
||||
}
|
||||
```
|
||||
|
||||
### Vault Enterprise Binary
|
||||
|
||||
Install the Vault Enterprise binary. This is required if using SAML authentication to Vault:
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
enterprise = true
|
||||
}
|
||||
```
|
||||
|
||||
## Related Modules
|
||||
|
||||
For more advanced authentication methods, see:
|
||||
|
||||
- [vault-github](https://registry.coder.com/modules/coder/vault-github) - Authenticate with Vault using GitHub tokens
|
||||
- [vault-jwt](https://registry.coder.com/modules/coder/vault-jwt) - Authenticate with Vault using OIDC/JWT
|
||||
|
||||
For simple token-based authentication, see:
|
||||
|
||||
- [vault-token](https://registry.coder.com/modules/coder/vault-token) - Authenticate with Vault using a token
|
||||
@@ -0,0 +1,97 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "vault_addr" {
|
||||
type = string
|
||||
description = "The address of the Vault server."
|
||||
}
|
||||
|
||||
variable "vault_token" {
|
||||
type = string
|
||||
description = "The Vault token to use for authentication. If not provided, only the CLI will be installed."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "install_dir" {
|
||||
type = string
|
||||
description = "The directory to install the Vault CLI to."
|
||||
default = "/usr/local/bin"
|
||||
}
|
||||
|
||||
variable "vault_cli_version" {
|
||||
type = string
|
||||
description = "The version of the Vault CLI to install."
|
||||
default = "latest"
|
||||
validation {
|
||||
condition = var.vault_cli_version == "latest" || can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+$", var.vault_cli_version))
|
||||
error_message = "vault_cli_version must be either 'latest' or a semantic version (e.g., '1.15.0')."
|
||||
}
|
||||
}
|
||||
|
||||
variable "vault_namespace" {
|
||||
type = string
|
||||
description = "The Vault Enterprise namespace to use. If not provided, no namespace will be configured."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "enterprise" {
|
||||
type = bool
|
||||
description = "Whether to install the enterprise version of the Vault CLI. Required if using SAML authentication to Vault."
|
||||
default = false
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
resource "coder_script" "vault_cli" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Vault CLI"
|
||||
icon = "/icon/vault.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
VAULT_ADDR = var.vault_addr
|
||||
VAULT_TOKEN = var.vault_token
|
||||
INSTALL_DIR = var.install_dir
|
||||
VAULT_CLI_VERSION = var.vault_cli_version
|
||||
ENTERPRISE = var.enterprise
|
||||
})
|
||||
run_on_start = true
|
||||
start_blocks_login = true
|
||||
}
|
||||
|
||||
resource "coder_env" "vault_addr" {
|
||||
agent_id = var.agent_id
|
||||
name = "VAULT_ADDR"
|
||||
value = var.vault_addr
|
||||
}
|
||||
|
||||
resource "coder_env" "vault_token" {
|
||||
count = var.vault_token != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "VAULT_TOKEN"
|
||||
value = var.vault_token
|
||||
}
|
||||
|
||||
resource "coder_env" "vault_namespace" {
|
||||
count = var.vault_namespace != null ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "VAULT_NAMESPACE"
|
||||
value = var.vault_namespace
|
||||
}
|
||||
|
||||
output "vault_cli_version" {
|
||||
description = "The version of the Vault CLI that was installed."
|
||||
value = var.vault_cli_version
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
mock_provider "coder" {}
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
|
||||
run "test_vault_cli_without_token" {
|
||||
assert {
|
||||
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
|
||||
error_message = "Display name should be 'Vault CLI'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_addr.name == "VAULT_ADDR"
|
||||
error_message = "VAULT_ADDR environment variable should be set"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_addr.value == "https://vault.example.com"
|
||||
error_message = "VAULT_ADDR should match the provided vault_addr"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_token) == 0
|
||||
error_message = "VAULT_TOKEN should not be set when vault_token is not provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_namespace) == 0
|
||||
error_message = "VAULT_NAMESPACE should not be set when vault_namespace is not provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_with_token" {
|
||||
variables {
|
||||
vault_token = "test-vault-token"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
|
||||
error_message = "Display name should be 'Vault CLI'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_addr.name == "VAULT_ADDR"
|
||||
error_message = "VAULT_ADDR environment variable should be set"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_token) == 1
|
||||
error_message = "VAULT_TOKEN should be set when vault_token is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_token[0].name == "VAULT_TOKEN"
|
||||
error_message = "VAULT_TOKEN environment variable name should be correct"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_token[0].value == "test-vault-token"
|
||||
error_message = "VAULT_TOKEN should match the provided vault_token"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_custom_version" {
|
||||
variables {
|
||||
vault_cli_version = "1.15.0"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.vault_cli_version == "1.15.0"
|
||||
error_message = "Vault CLI version output should match the provided version"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_custom_install_dir" {
|
||||
variables {
|
||||
install_dir = "/custom/install/dir"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
|
||||
error_message = "Display name should be 'Vault CLI'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_invalid_version" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
vault_cli_version = "invalid-version"
|
||||
}
|
||||
|
||||
expect_failures = [var.vault_cli_version]
|
||||
}
|
||||
|
||||
run "test_vault_cli_valid_semver" {
|
||||
variables {
|
||||
vault_cli_version = "1.18.3"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.vault_cli_version == "1.18.3"
|
||||
error_message = "Vault CLI version output should match the provided version"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_rejects_v_prefix" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
vault_cli_version = "v1.18.3"
|
||||
}
|
||||
|
||||
expect_failures = [var.vault_cli_version]
|
||||
}
|
||||
|
||||
run "test_vault_cli_with_namespace" {
|
||||
variables {
|
||||
vault_namespace = "admin/my-namespace"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_namespace) == 1
|
||||
error_message = "VAULT_NAMESPACE should be set when vault_namespace is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_namespace[0].name == "VAULT_NAMESPACE"
|
||||
error_message = "VAULT_NAMESPACE environment variable name should be correct"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_namespace[0].value == "admin/my-namespace"
|
||||
error_message = "VAULT_NAMESPACE should match the provided vault_namespace"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_with_token_and_namespace" {
|
||||
variables {
|
||||
vault_token = "test-vault-token"
|
||||
vault_namespace = "admin/my-namespace"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_token) == 1
|
||||
error_message = "VAULT_TOKEN should be set when vault_token is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_namespace) == 1
|
||||
error_message = "VAULT_NAMESPACE should be set when vault_namespace is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_token[0].value == "test-vault-token"
|
||||
error_message = "VAULT_TOKEN should match the provided vault_token"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_namespace[0].value == "admin/my-namespace"
|
||||
error_message = "VAULT_NAMESPACE should match the provided vault_namespace"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_enterprise" {
|
||||
variables {
|
||||
enterprise = true
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
|
||||
error_message = "Display name should be 'Vault CLI'"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Convert all templated variables to shell variables
|
||||
VAULT_ADDR=${VAULT_ADDR}
|
||||
VAULT_TOKEN=${VAULT_TOKEN}
|
||||
INSTALL_DIR=${INSTALL_DIR}
|
||||
VAULT_CLI_VERSION=${VAULT_CLI_VERSION}
|
||||
ENTERPRISE=${ENTERPRISE}
|
||||
|
||||
# Fetch URL content. If dest is provided, write to file; otherwise output to stdout.
|
||||
# Usage: fetch <url> [dest]
|
||||
fetch() {
|
||||
url="$1"
|
||||
dest="$${2:-}"
|
||||
|
||||
# Detect HTTP client on first run
|
||||
if [ -z "$${HTTP_CLIENT:-}" ]; then
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="curl"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="wget"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="busybox"
|
||||
else
|
||||
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$${dest}" ]; then
|
||||
# shellcheck disable=SC2195
|
||||
case "$${HTTP_CLIENT}" in
|
||||
curl) curl -sSL --fail "$${url}" -o "$${dest}" ;;
|
||||
wget) wget -O "$${dest}" "$${url}" ;;
|
||||
busybox) busybox wget -O "$${dest}" "$${url}" ;;
|
||||
esac
|
||||
else
|
||||
# shellcheck disable=SC2195
|
||||
case "$${HTTP_CLIENT}" in
|
||||
curl) curl -sSL --fail "$${url}" ;;
|
||||
wget) wget -qO- "$${url}" ;;
|
||||
busybox) busybox wget -qO- "$${url}" ;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
unzip_safe() {
|
||||
if command -v unzip > /dev/null 2>&1; then
|
||||
command unzip "$@"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
busybox unzip "$@"
|
||||
else
|
||||
printf "unzip or busybox is not installed. Please install unzip in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
install() {
|
||||
# Get the architecture of the system
|
||||
ARCH=$(uname -m)
|
||||
if [ "$${ARCH}" = "x86_64" ]; then
|
||||
ARCH="amd64"
|
||||
elif [ "$${ARCH}" = "aarch64" ]; then
|
||||
ARCH="arm64"
|
||||
else
|
||||
printf "Unsupported architecture: %s\n" "$${ARCH}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Determine OS and validate
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$${OS}" != "linux" ] && [ "$${OS}" != "darwin" ]; then
|
||||
printf "Unsupported OS: %s. Only linux and darwin are supported.\n" "$${OS}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Fetch release information from HashiCorp API
|
||||
if [ "$${VAULT_CLI_VERSION}" = "latest" ]; then
|
||||
if [ "$${ENTERPRISE}" = "true" ]; then
|
||||
API_URL="https://api.releases.hashicorp.com/v1/releases/vault/latest?license_class=enterprise"
|
||||
else
|
||||
API_URL="https://api.releases.hashicorp.com/v1/releases/vault/latest"
|
||||
fi
|
||||
else
|
||||
# For specific version, append +ent suffix for enterprise
|
||||
if [ "$${ENTERPRISE}" = "true" ]; then
|
||||
API_URL="https://api.releases.hashicorp.com/v1/releases/vault/$${VAULT_CLI_VERSION}+ent"
|
||||
else
|
||||
API_URL="https://api.releases.hashicorp.com/v1/releases/vault/$${VAULT_CLI_VERSION}"
|
||||
fi
|
||||
fi
|
||||
|
||||
API_RESPONSE=$(fetch "$${API_URL}")
|
||||
if [ -z "$${API_RESPONSE}" ]; then
|
||||
printf "Failed to fetch release information from HashiCorp API.\n"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse version and download URL from API response
|
||||
if command -v jq > /dev/null 2>&1; then
|
||||
VAULT_CLI_VERSION=$(printf '%s' "$${API_RESPONSE}" | jq -r '.version')
|
||||
DOWNLOAD_URL=$(printf '%s' "$${API_RESPONSE}" | jq -r --arg os "$${OS}" --arg arch "$${ARCH}" '.builds[] | select(.os == $os and .arch == $arch) | .url')
|
||||
else
|
||||
VAULT_CLI_VERSION=$(printf '%s' "$${API_RESPONSE}" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
|
||||
# Fallback: construct URL manually if jq not available
|
||||
DOWNLOAD_URL="https://releases.hashicorp.com/vault/$${VAULT_CLI_VERSION}/vault_$${VAULT_CLI_VERSION}_$${OS}_$${ARCH}.zip"
|
||||
fi
|
||||
|
||||
if [ -z "$${VAULT_CLI_VERSION}" ]; then
|
||||
printf "Failed to determine Vault version.\n"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$${DOWNLOAD_URL}" ]; then
|
||||
printf "Failed to determine download URL for Vault %s (%s/%s).\n" "$${VAULT_CLI_VERSION}" "$${OS}" "$${ARCH}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "Vault version: %s\n" "$${VAULT_CLI_VERSION}"
|
||||
|
||||
# Check if the vault CLI is installed and has the correct version
|
||||
installation_needed=1
|
||||
if command -v vault > /dev/null 2>&1; then
|
||||
CURRENT_VERSION=$(vault version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||
if [ "$${CURRENT_VERSION}" = "$${VAULT_CLI_VERSION}" ]; then
|
||||
printf "Vault version %s is already installed and up-to-date.\n\n" "$${CURRENT_VERSION}"
|
||||
installation_needed=0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$${installation_needed}" = "1" ]; then
|
||||
# Download and install Vault
|
||||
if [ -z "$${CURRENT_VERSION}" ]; then
|
||||
printf "Installing Vault CLI ...\n\n"
|
||||
else
|
||||
printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "$${VAULT_CLI_VERSION}"
|
||||
fi
|
||||
|
||||
# Create temporary directory for download
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
cd "$${TEMP_DIR}" || return 1
|
||||
|
||||
printf "Downloading from %s\n" "$${DOWNLOAD_URL}"
|
||||
if ! fetch "$${DOWNLOAD_URL}" vault.zip; then
|
||||
printf "Failed to download Vault.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
fi
|
||||
if ! unzip_safe vault.zip; then
|
||||
printf "Failed to unzip Vault.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Install to the specified directory
|
||||
if [ -n "$${INSTALL_DIR}" ] && [ -w "$${INSTALL_DIR}" ]; then
|
||||
mv vault "$${INSTALL_DIR}/vault"
|
||||
printf "Vault installed to %s successfully!\n\n" "$${INSTALL_DIR}"
|
||||
elif [ -n "$${INSTALL_DIR}" ] && [ ! -w "$${INSTALL_DIR}" ]; then
|
||||
# Try with sudo if install dir specified but not writable
|
||||
if sudo mv vault "$${INSTALL_DIR}/vault" 2> /dev/null; then
|
||||
printf "Vault installed to %s successfully!\n\n" "$${INSTALL_DIR}"
|
||||
else
|
||||
printf "Warning: Cannot write to %s. " "$${INSTALL_DIR}"
|
||||
mkdir -p ~/.local/bin
|
||||
if mv vault ~/.local/bin/vault; then
|
||||
printf "Installed to ~/.local/bin instead.\n"
|
||||
printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
|
||||
else
|
||||
printf "Failed to install Vault.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
elif sudo mv vault /usr/local/bin/vault 2> /dev/null; then
|
||||
printf "Vault installed successfully!\n\n"
|
||||
else
|
||||
mkdir -p ~/.local/bin
|
||||
if ! mv vault ~/.local/bin/vault; then
|
||||
printf "Failed to move Vault to local bin.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
fi
|
||||
printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
|
||||
fi
|
||||
|
||||
# Clean up temp directory
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Run installation
|
||||
if ! install; then
|
||||
printf "Failed to install Vault CLI.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Indicate token configuration status
|
||||
if [ -n "$${VAULT_TOKEN}" ]; then
|
||||
printf "Vault token has been configured via VAULT_TOKEN environment variable.\n"
|
||||
else
|
||||
printf "No Vault token provided. Use 'vault login' or set VAULT_TOKEN to authenticate.\n"
|
||||
fi
|
||||
@@ -14,8 +14,8 @@ This module lets you authenticate with [Hashicorp Vault](https://www.vaultprojec
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vault-github/coder"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
```
|
||||
@@ -46,8 +46,8 @@ To configure the Vault module, you must set up a Vault GitHub auth method. See t
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vault-github/coder"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
coder_github_auth_id = "my-github-auth-id"
|
||||
}
|
||||
@@ -59,8 +59,8 @@ module "vault" {
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vault-github/coder"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
coder_github_auth_id = "my-github-auth-id"
|
||||
vault_github_auth_path = "my-github-auth-path"
|
||||
@@ -73,9 +73,9 @@ module "vault" {
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vault-github/coder"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_cli_version = "1.15.0"
|
||||
vault_cli_version = "1.1.2"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -47,6 +47,7 @@ install() {
|
||||
if [ "$${INSTALL_VERSION}" = "latest" ]; then
|
||||
LATEST_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -v 'rc' | grep -oE 'vault/[0-9]+\.[0-9]+\.[0-9]+' | sed 's/vault\///' | sort -V | tail -n 1)
|
||||
printf "Latest version of Vault is %s.\n\n" "$${LATEST_VERSION}"
|
||||
# shellcheck disable=SC2157
|
||||
if [ -z "$${LATEST_VERSION}" ]; then
|
||||
printf "Failed to determine the latest Vault version.\n"
|
||||
return 1
|
||||
@@ -64,8 +65,10 @@ install() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2170
|
||||
if [ $${installation_needed} -eq 1 ]; then
|
||||
# Download and install Vault
|
||||
# shellcheck disable=SC2157
|
||||
if [ -z "$${CURRENT_VERSION}" ]; then
|
||||
printf "Installing Vault CLI ...\n\n"
|
||||
else
|
||||
|
||||
@@ -14,8 +14,8 @@ This module lets you authenticate with [Hashicorp Vault](https://www.vaultprojec
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vault-jwt/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||
vault_jwt_token = "eyJhbGciOiJIUzI1N..." # optional, if not present, defaults to user's oidc authentication token
|
||||
@@ -42,8 +42,8 @@ curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/d
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vault-jwt/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_auth_path = "oidc"
|
||||
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||
@@ -58,8 +58,8 @@ data "coder_workspace_owner" "me" {}
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vault-jwt/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_role = data.coder_workspace_owner.me.groups[0]
|
||||
}
|
||||
@@ -71,11 +71,11 @@ module "vault" {
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vault-jwt/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||
vault_cli_version = "1.17.5"
|
||||
vault_cli_version = "1.2.2"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -87,11 +87,11 @@ terraform {
|
||||
required_providers {
|
||||
jwt = {
|
||||
source = "geektheripper/jwt"
|
||||
version = "1.1.4"
|
||||
version = "1.2.2"
|
||||
}
|
||||
time = {
|
||||
source = "hashicorp/time"
|
||||
version = "0.11.1"
|
||||
version = "1.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,8 +132,8 @@ resource "jwt_signed_token" "vault" {
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vault-jwt/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
version = "1.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||
vault_jwt_token = jwt_signed_token.vault[0].token
|
||||
|
||||
@@ -48,6 +48,7 @@ install() {
|
||||
if [ "$${VAULT_CLI_VERSION}" = "latest" ]; then
|
||||
LATEST_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -v 'rc' | grep -oE 'vault/[0-9]+\.[0-9]+\.[0-9]+' | sed 's/vault\///' | sort -V | tail -n 1)
|
||||
printf "Latest version of Vault is %s.\n\n" "$${LATEST_VERSION}"
|
||||
# shellcheck disable=SC2157
|
||||
if [ -z "$${LATEST_VERSION}" ]; then
|
||||
printf "Failed to determine the latest Vault version.\n"
|
||||
return 1
|
||||
@@ -65,8 +66,10 @@ install() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2170
|
||||
if [ $${installation_needed} -eq 1 ]; then
|
||||
# Download and install Vault
|
||||
# shellcheck disable=SC2157
|
||||
if [ -z "$${CURRENT_VERSION}" ]; then
|
||||
printf "Installing Vault CLI ...\n\n"
|
||||
else
|
||||
|
||||