Compare commits

...

39 Commits

Author SHA1 Message Date
djarbz 0149b34006 feat(code-server): add machine settings option (#105)
Reimplements the changes in https://github.com/coder/registry/pull/88
with the manual version bump to 1.1.0
2025-05-16 09:37:27 -05:00
Michael Smith 22a8b2614b chore: add deploy script (#15)
Helps close out https://github.com/coder/internal/milestone/10

## Changes made
- Added `deploy-registry.yaml` workflow
2025-05-16 10:27:09 -04:00
DevCats 1f7df79f1b Revert "feat(code-server): add machine settings option" Need to add Versions in README (#104)
Reverts coder/registry#88

Need to add Readme Versions
2025-05-15 21:29:45 -05:00
djarbz 84f3cb5ac1 feat(code-server): add machine settings option (#88)
Should resolve https://github.com/coder/registry/issues/27

The goal is to provide a method for template owners to configure default
`Machine Settings` that can be overridden by developers via `User
Settings` and repositories via `Workspace Settings`.
This option allows template owners to push new settings options with a
template release that would not be ignored because the setting file
already exists.
This also formats the `settings.json` file if `jq` is installed.

Eventually, I would imagine the current `settings` option will be
depreciated in favor of this option.
2025-05-15 19:44:31 -05:00
DevCats 6718a28c87 chore: add amazon-q svg, update icon references, and tags in README.md (#101)
- Add amazon-q.svg in ./icons
- Resolve Icon path to point to new amazon-q.svg
- Set tags to agent, ai, amazon-q

---------

Co-authored-by: M Atif Ali <atif@coder.com>
2025-05-15 13:14:55 -05:00
DevCats 8d56ee8182 fix: resolve aider icon path to correct icon (#102)
- code.svg to aider.svg in README.md

---------

Co-authored-by: M Atif Ali <atif@coder.com>
2025-05-15 12:54:01 -05:00
DevCats 9788acb08e chore: update CONTRIBUTING.md to replace incomplete release script with manual instruction. (#100)
- Revised the release process description for better clarity.
- Added detailed steps for creating and pushing annotated tags.
- Updated notes on version numbering and publishing to the Coder
Registry.
2025-05-15 12:19:23 -05:00
Birdie Kingston 9a2e48b1da feat(vault-token): make supplying a vault token optional (#90)
Co-authored-by: Birdie K <5210502+moo-im-a-cow@users.noreply.github.com>
Co-authored-by: M Atif Ali <atif@coder.com>
2025-05-15 22:12:36 +05:00
Phil Hachey d77d4a8f19 feat(cursor): added slug variable to cursor module (#87)
Co-authored-by: M Atif Ali <atif@coder.com>
2025-05-15 21:39:21 +05:00
M Atif Ali c4b106b9a2 chore: remove support email (#97)
The email is only for paying customers. Removing this will help us get
random support requests.
2025-05-15 12:05:39 -04:00
Garrett Delfosse 1a5e483c21 chore: migrate new_module script (#96)
Closes https://github.com/coder/internal/issues/611

This scripts creates a new sample moduledir with required files
Run it like : ./scripts/new_module.sh my-namespace/my-module
2025-05-15 11:33:04 -04:00
M Atif Ali 1355ea4b60 chore: update module sources for community modules (#93) 2025-05-15 20:04:28 +05:00
DevCats 6c7f06e240 feat(amazon-q): introduce amazon-q module to coder/registry (#95)
Co-authored-by: M Atif Ali <atif@coder.com>
2025-05-15 20:03:46 +05:00
DevCats a87c76bea6 feat(aider): Introduce Aider Module to Coder Registry (#98)
Co-authored-by: M Atif Ali <atif@coder.com>
2025-05-15 19:37:37 +05:00
Michael Smith 84ce4ea325 chore: update all source URLs for modules to reflect new namespace format (#89)
## Changes made
- Updated all `source` properties in Terraform import snippets to use
the new namespaced Terraform protocol URLs

## Notes
- Probably need to wait until the latest version of the Registry website
is pushed to production before we merge this in, just to be on the safe
side
- I replaced all the paths via a regex, and then double-checked all the
files modified to make sure there weren't any false positives
2025-05-13 17:53:24 -04:00
Michael Smith 31b8312877 chore: add all missing README files to repo (#85)
## Changes made
- Fleshed out main top-level README file
- Added formal docs for code of conduct and security (that just lead to
the Coder Docs)
- Revamped contributing guide
- Added a few images to help support the new docs

## Notes
- Just because we're not supporting templates for the moment, I did
deliberately limit the number of mentions to it.
2025-05-09 18:21:38 -04:00
Michael Smith 496b09d93f chore: sync newest module updates to registry (#84)
## Changes made
- Copied over all changes to existing modules, making sure to preserve
all relative path updates made specifically for the Registry repo
- Copied over all modules that were created since the last sync
(Windsurf, Devcontainers-CLI)
- Copied over changes from the `test.ts` file

## Notes
- This PR does not cover https://github.com/coder/modules/pull/426,
which contains a few changes around updating the Bash scripts and the
contributing README file. @f0ssel tagging you so that you're aware, but
I'll be taking care of the `CONTRIBUTING.md` file

---------

Co-authored-by: M Atif Ali <me@matifali.dev>
2025-05-09 11:28:38 -04:00
Michael Smith 7ea1f305af chore: update README.md to have message about repo status 2025-05-08 13:54:47 -04:00
M Atif Ali cab08fbffe Merge pull request #19 from coder/dependabot/github_actions/crate-ci/typos-1.32.0 2025-05-05 13:02:09 +05:00
dependabot[bot] efed015f3a chore(deps): bump crate-ci/typos from 1.31.1 to 1.32.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.31.1 to 1.32.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.31.1...v1.32.0)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 07:43:28 +00:00
Michael Smith ba6fea8ddb chore: add logic to validate all modules (#11)
Closes https://github.com/coder/internal/issues/531

## Changes made
- Added functionality to validate the structure of module README files (frontmatter and the README body)
- Added a really basic snapshot-ish test for the module README body validation
- Updated README files that were previously violating README requirements (the old modules validation logic wasn't catching these)
- Changed `ValidationPhase` from an int to a string
2025-05-02 18:39:55 -04:00
Michael Smith 45dc925f8b fix: update repo structure validation logic to disallow false positives (#10)
* refactor: update file structure to reflect new changes

* refactor: start splitting up files

* refactor: more domain splitting

* refactor: remove directory validation from contributors file

* fix: update repo structure checks

* fix: improve check for user namespace subdirectories

* docs: add missing words to comment

* docs: update typo

* refactor: make code easier to read

* fix: update README files

* fix: remove employer field entirely

* fix: make Github field optional

* refactor: rename files
2025-05-02 11:23:52 -04:00
Michael Smith 9e18a4e3a8 chore: add prettier/typo check to CI (#14)
## Changes made
- Added back CI steps for validating the codebase for typos and formatting
- Updated README validation CI step to be dependent on typo-checking step
- Updated configuration files as needed to support the new CI step
- Updated all files that were previously getting skipped over from improperly-set-up CI logic
2025-04-29 10:09:22 -04:00
Michael Smith 0ce1e7ab01 chore: add CONTRIBUTING.md file (#13)
## Changes made
- Added `CONTRIBUTING.md` file (mostly copied over from modules repo, with some parts reworded, and some sections added)

## Notes
- This definitely isn't the final version of the file (it should definitely change we have more Bash stuff added), but it felt like it's in a good enough spot for an initial release
2025-04-29 10:07:11 -04:00
Michael Smith 9404ad9a53 chore: add Success! The configuration is valid. (#16)
More work towards closing https://github.com/coder/internal/issues/532

## Changes made
- Added Bash script to run `terraform validate` on all relevant repos
- Updated `package.json` and CI to use the script
2025-04-29 09:51:04 -04:00
Michael Smith c1e196c8b0 Merge pull request #17 from coder/mes/main-readme
chore: add main README.md file to project
2025-04-23 09:45:26 -04:00
Michael Smith 36acd61e40 fix: apply project rename to h1 title 2025-04-22 21:48:44 +00:00
Michael Smith 7d447d1672 Merge pull request #12 from coder/mes/site-check-script
fix: make site health check easier to use
2025-04-22 11:39:48 -04:00
Michael Smith 9780217535 fix: add back cron script 2025-04-22 15:07:02 +00:00
Michael Smith 494a25e688 fix: make site health check eaiser to use 2025-04-22 14:35:20 +00:00
Michael Smith a6c1e9c5ea Merge pull request #8 from coder/mes/script-disable
fix: disable cron site check
2025-04-17 19:18:48 -04:00
Michael Smith 87468aadb8 fix: disable cron site check 2025-04-17 23:07:43 +00:00
Michael Smith fd0c2698b9 Merge pull request #7 from coder/mes/extra-readmes
fix: add missing README files for registry directory
2025-04-17 15:21:00 -04:00
Michael Smith c4696836b6 fix: add missing README files for registry directory 2025-04-17 19:17:13 +00:00
Michael Smith 385966dc90 Merge pull request #6 from coder/mes/script-fix
fix: update script path
2025-04-17 14:18:50 -04:00
Michael Smith 5829345413 fix: update script path 2025-04-17 18:02:01 +00:00
Michael Smith 9e9d84db1b Merge pull request #5 from coder/mes/structure-fix
## Changes made
- Removed GCP deploy script (for now). Will be added back once we have things configured
- Moved the Apache Airflow under the NatainData user namespace (looked through the GitHub history for modules and saw that she was the creator)
2025-04-17 11:12:04 -04:00
Michael Smith 9356d8c67f fix: restructure modules repo to reflect current status 2025-04-17 15:06:01 +00:00
Michael Smith 376664ca04 chore: migrate all Coder modules to Registry repo (#4)
Addresses part of https://github.com/coder/internal/issues/532 (but doesn't fully close it out).

This is a huge PR, but chunking it up seemed pointless, since we're largely copying over existing files. I'm going to try commenting on the main areas I think are worth paying attention to

## Changes made
- Migrated over all Coder modules from coder/modules, and put them in their correct user namespaces.
- Added README.md files to all newly-created user namespaces
- Updated all image paths for every image used, to make sure they don't break (I switched the paths programmatically, and then manually verified every README to guarantee this).
- Added README.md files for each contributor who previously made a module
- Updated our `tsconfig.json` file to use modern libraries when run from the server, and made a custom `tsconfig.json` just for the `windows-rdp` module (which has more restrictive browser concerns)
- Added CI step to run all the module tests we currently have

## Notes
- There were a lot of Bash script files that weren't carried over in this PR, partly because I don't know Bash well enough to know (1) whether they're still needed, or (2) modify them to account for the new file structure. Those can be brought over later.
- We had a `lint.ts` file that provided some light validation of some of the modules. After going through it, there were so many bugs and issues with the code that I legitimately think that it barely provided a safety net at all. I got rid of it entirely, with the intention of adding the functionality that was originally intended to the current validation logic (to be handled in a separate PR).
- I changed how we set up the `.images` directory, because it felt like it would be chaos if a bunch of users try to throw all their images in one giant directory, with no guidelines on how to do it. I instead made it so that images should be scoped by namespace, which felt a lot more manageable. The `.icons` directory is still at the top level, because realistically, there are only going to be so many types of icons referenced, so it's fine for those to be shared.
- I don't think the `maintainer_github` and `contributor_github` fields make sense anymore, but those can be stripped out once we've updated the Registry site build step to use the new Registry repo
   - My gut instinct is to use the user namespace to determine the main owner of the module, and then add a `contributors` string list to indicate which other users have contributed meaningfully to it. We can then add validation to make sure that every value in that list exists as another namespace in the repo

## What still needs to be migrated (in separate PRs)
These are the main files of interest that still probably need to be copied over from the `/modules` repo:
- `new.sh`
- `terraform_validate.sh`
- `update-version.sh`

They're probably going to require enough changes that it's worth handling them in a separate PR.
2025-04-17 09:49:54 -04:00
238 changed files with 17497 additions and 493 deletions
+23
View File
@@ -0,0 +1,23 @@
# ---
# These are used for the site health script, run from GitHub Actions. They can
# be set manually if you need to run the script locally.
# Coder admins for Instatus can get this value from https://dashboard.instatus.com/developer
export INSTATUS_API_KEY=
# Can be obtained from calling the Instatus API with a valid token. This value
# might not actually need to be private, but better safe than sorry
# https://instatus.com/help/api/status-pages
export INSTATUS_PAGE_ID=
# Can be obtained from calling the Instatus API with a valid token. This value
# might not actually need to be private, but better safe than sorry
# https://instatus.com/help/api/components
export INSTATUS_COMPONENT_ID=
# Can be grabbed from https://vercel.com/codercom/registry/stores/integration/upstash/store_1YDPuBF4Jd0aNpuV/guides
# Please make sure that the token you use is KV_REST_API_TOKEN; the script needs
# to be able to queries and mutations
export VERCEL_API_KEY=
# ---
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env bash
set -o pipefail
set -u
VERBOSE="${VERBOSE:-0}"
if [[ "${VERBOSE}" -ne "0" ]]; then
set -x
fi
# List of required environment variables
required_vars=(
"INSTATUS_API_KEY"
"INSTATUS_PAGE_ID"
"INSTATUS_COMPONENT_ID"
"VERCEL_API_KEY"
)
# Check if each required variable is set
for var in "${required_vars[@]}"; do
if [[ -z "${!var:-}" ]]; then
echo "Error: Environment variable '$var' is not set."
exit 1
fi
done
REGISTRY_BASE_URL="${REGISTRY_BASE_URL:-https://registry.coder.com}"
status=0
declare -a modules=()
declare -a failures=()
# Collect all module directories containing a main.tf file
for path in $(find . -maxdepth 2 -not -path '*/.*' -type f -name main.tf | cut -d '/' -f 2 | sort -u); do
modules+=("${path}")
done
echo "Checking modules: ${modules[*]}"
# Function to update the component status on Instatus
update_component_status() {
local component_status=$1
# see https://instatus.com/help/api/components
(curl -X PUT "https://api.instatus.com/v1/$INSTATUS_PAGE_ID/components/$INSTATUS_COMPONENT_ID" \
-H "Authorization: Bearer $INSTATUS_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"status\": \"$component_status\"}")
}
# Function to create an incident
create_incident() {
local incident_name="Degraded Service"
local message="The following modules are experiencing issues:\n"
for i in "${!failures[@]}"; do
message+="$((i + 1)). ${failures[$i]}\n"
done
component_status="PARTIALOUTAGE"
if ((${#failures[@]} == ${#modules[@]})); then
component_status="MAJOROUTAGE"
fi
# see https://instatus.com/help/api/incidents
incident_id=$(curl -s -X POST "https://api.instatus.com/v1/$INSTATUS_PAGE_ID/incidents" \
-H "Authorization: Bearer $INSTATUS_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"$incident_name\",
\"message\": \"$message\",
\"components\": [\"$INSTATUS_COMPONENT_ID\"],
\"status\": \"INVESTIGATING\",
\"notify\": true,
\"statuses\": [
{
\"id\": \"$INSTATUS_COMPONENT_ID\",
\"status\": \"PARTIALOUTAGE\"
}
]
}" | jq -r '.id')
echo "Created incident with ID: $incident_id"
}
# 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" \
-H "Authorization: Bearer $INSTATUS_API_KEY" \
-H "Content-Type: application/json" | jq -r '.incidents[] | select(.status != "RESOLVED") | .id')
if [[ -n "$unresolved_incidents" ]]; then
echo "Unresolved incidents found: $unresolved_incidents"
return 0 # Indicate that there are unresolved incidents
else
echo "No unresolved incidents found."
return 1 # Indicate that no unresolved incidents exist
fi
}
force_redeploy_registry() {
# These are not secret values; safe to just expose directly in script
local VERCEL_TEAM_SLUG="codercom"
local VERCEL_TEAM_ID="team_tGkWfhEGGelkkqUUm9nXq17r"
local VERCEL_APP="registry"
local latest_res
latest_res=$(
curl "https://api.vercel.com/v6/deployments?app=$VERCEL_APP&limit=1&slug=$VERCEL_TEAM_SLUG&teamId=$VERCEL_TEAM_ID&target=production&state=BUILDING,INITIALIZING,QUEUED,READY" \
--fail \
--silent \
--header "Authorization: Bearer $VERCEL_API_KEY" \
--header "Content-Type: application/json"
)
# If we have zero deployments, something is VERY wrong. Make the whole
# script exit with a non-zero status code
local latest_id
latest_id=$(echo "${latest_res}" | jq -r '.deployments[0].uid')
if [[ "${latest_id}" = "null" ]]; then
echo "Unable to pull any previous deployments for redeployment"
echo "Please redeploy the latest deployment manually in Vercel."
echo "https://vercel.com/codercom/registry/deployments"
exit 1
fi
local latest_date_ts_seconds
latest_date_ts_seconds=$(echo "${latest_res}" | jq -r '.deployments[0].createdAt/1000|floor')
local current_date_ts_seconds
current_date_ts_seconds="$(date +%s)"
local max_redeploy_interval_seconds=7200 # 2 hours
if ((current_date_ts_seconds - latest_date_ts_seconds < max_redeploy_interval_seconds)); then
echo "The registry was deployed less than 2 hours ago."
echo "Not automatically re-deploying the regitstry."
echo "A human reading this message should decide if a redeployment is necessary."
echo "Please check the Vercel dashboard for more information."
echo "https://vercel.com/codercom/registry/deployments"
exit 1
fi
local latest_deployment_state
latest_deployment_state="$(echo "${latest_res}" | jq -r '.deployments[0].state')"
if [[ "${latest_deployment_state}" != "READY" ]]; then
echo "Last deployment was not in READY state. Skipping redeployment."
echo "A human reading this message should decide if a redeployment is necessary."
echo "Please check the Vercel dashboard for more information."
echo "https://vercel.com/codercom/registry/deployments"
exit 1
fi
echo "============================================================="
echo "!!! Redeploying registry with deployment ID: ${latest_id} !!!"
echo "============================================================="
if ! curl -X POST "https://api.vercel.com/v13/deployments?forceNew=1&skipAutoDetectionConfirmation=1&slug=$VERCEL_TEAM_SLUG&teamId=$VERCEL_TEAM_ID" \
--fail \
--header "Authorization: Bearer $VERCEL_API_KEY" \
--header "Content-Type: application/json" \
--data-raw "{ \"deploymentId\": \"${latest_id}\", \"name\": \"${VERCEL_APP}\", \"target\": \"production\" }"; then
echo "DEPLOYMENT FAILED! Please check the Vercel dashboard for more information."
echo "https://vercel.com/codercom/registry/deployments"
exit 1
fi
}
# Check each module's accessibility
for module in "${modules[@]}"; do
# Trim leading/trailing whitespace from module name
module=$(echo "${module}" | xargs)
url="${REGISTRY_BASE_URL}/modules/${module}"
printf "=== Checking module %s at %s\n" "${module}" "${url}"
status_code=$(curl --output /dev/null --head --silent --fail --location "${url}" --retry 3 --write-out "%{http_code}")
if ((status_code != 200)); then
printf "==> FAIL(%s)\n" "${status_code}"
status=1
failures+=("${module}")
else
printf "==> OK(%s)\n" "${status_code}"
fi
done
# Determine overall status and update Instatus component
if ((status == 0)); then
echo "All modules are operational."
# set to
update_component_status "OPERATIONAL"
else
echo "The following modules have issues: ${failures[*]}"
# check if all modules are down
if ((${#failures[@]} == ${#modules[@]})); then
update_component_status "MAJOROUTAGE"
else
update_component_status "PARTIALOUTAGE"
fi
# Check if there is an existing incident before creating a new one
if ! check_existing_incident; then
create_incident
fi
# If a module is down, force a reployment to try getting things back online
# ASAP
# EDIT: registry.coder.com is no longer hosted on vercel
#force_redeploy_registry
fi
exit "${status}"
+4
View File
@@ -0,0 +1,4 @@
[default.extend-words]
muc = "muc" # For Munich location code
Hashi = "Hashi"
HashiCorp = "HashiCorp"
@@ -0,0 +1,23 @@
# Check modules health on registry.coder.com
name: check-registry-site-health
on:
schedule:
- cron: "0,15,30,45 * * * *" # Runs every 15 minutes
workflow_dispatch: # Allows manual triggering of the workflow if needed
jobs:
run-script:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run check.sh
run: |
./.github/scripts/check_registry_site_health.sh
env:
INSTATUS_API_KEY: ${{ secrets.INSTATUS_API_KEY }}
INSTATUS_PAGE_ID: ${{ secrets.INSTATUS_PAGE_ID }}
INSTATUS_COMPONENT_ID: ${{ secrets.INSTATUS_COMPONENT_ID }}
VERCEL_API_KEY: ${{ secrets.VERCEL_API_KEY }}
+51 -3
View File
@@ -7,8 +7,56 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
validate-contributors:
test-terraform:
name: Validate Terraform output
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Terraform
uses: coder/coder/.github/actions/setup-tf@main
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
# We're using the latest version of Bun for now, but it might be worth
# reconsidering. They've pushed breaking changes in patch releases
# that have broken our CI.
# Our PR where issues started to pop up: https://github.com/coder/modules/pull/383
# The Bun PR that broke things: https://github.com/oven-sh/bun/pull/16067
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run TypeScript tests
run: bun test
- name: Run Terraform Validate
run: bun terraform-validate
validate-style:
name: Check for typos and unformatted code
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# Need Terraform for its formatter
- name: Install Terraform
uses: coder/coder/.github/actions/setup-tf@main
- name: Install dependencies
run: bun install
- name: Validate formatting
run: bun fmt:ci
- name: Check for typos
uses: crate-ci/typos@v1.32.0
with:
config: .github/typos.toml
validate-readme-files:
name: Validate README files
runs-on: ubuntu-latest
# We want to do some basic README checks first before we try analyzing the
# contents
needs: validate-style
steps:
- name: Check out code
uses: actions/checkout@v4
@@ -17,6 +65,6 @@ jobs:
with:
go-version: "1.23.2"
- name: Validate contributors
run: go build ./scripts/contributors && ./contributors
run: go build ./cmd/readmevalidation && ./readmevalidation
- name: Remove build file artifact
run: rm ./contributors
run: rm ./readmevalidation
+36
View File
@@ -0,0 +1,36 @@
name: deploy-registry
on:
push:
branches:
- main
tags:
# Matches release/<namespace>/<resource_name>/<semantic_version>
# (e.g., "release/whizus/exoscale-zone/v1.0.13")
- "release/*/*/v*.*.*"
jobs:
deploy:
runs-on: ubuntu-latest
# Set id-token permission for gcloud
permissions:
contents: read
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Authenticate with Google Cloud
uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935
with:
workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github
service_account: registry-v2-github@coder-registry-1.iam.gserviceaccount.com
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a
# For the time being, let's have the first couple merges to main in
# modules deploy a new version to *dev*. Once we review and make sure
# everything's working, we can deploy a new version to *main*. Maybe in
# the future we could automate this based on the result of E2E tests.
- name: Deploy to dev.registry.coder.com
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch dev
+10 -2
View File
@@ -135,5 +135,13 @@ dist
.yarn/install-state.gz
.pnp.*
# Script output
/contributors
# Things needed for CI
/readmevalidation
# Terraform files generated during testing
.terraform*
*.tfstate
*.tfstate.lock.info
# Generated credentials from google-github-actions/auth
gha-creds-*.json
+3
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

+268
View File
@@ -0,0 +1,268 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="katman_1" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 841.9 595.3">
<!-- Generator: Adobe Illustrator 29.3.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 151) -->
<defs>
<style>
.st0 {
fill: #2e3c4e;
}
.st1 {
opacity: 0;
}
.st1, .st2, .st3, .st4, .st5, .st6, .st7, .st8 {
display: none;
}
.st9 {
fill: #7300e5;
}
.st10, .st11 {
fill: #fff;
}
.st12 {
fill: url(#Adsız_degrade_4);
}
.st2 {
opacity: 0;
}
.st13 {
fill: url(#Adsız_degrade_41);
}
.st14 {
fill: #d7c8f9;
}
.st15 {
fill: none;
}
.st16 {
stroke: #d1d1d6;
stroke-width: .5px;
}
.st16, .st17, .st11 {
fill-opacity: 0;
}
.st3 {
opacity: 0;
}
.st17 {
stroke: #fff;
stroke-width: 4px;
}
.st4 {
opacity: 0;
}
.st5 {
opacity: 0;
}
.st7 {
opacity: 0;
}
.st18 {
fill: url(#Adsız_degrade_5);
}
.st8 {
opacity: 0;
}
.st19 {
fill: url(#Adsız_degrade_2);
}
</style>
<mask id="mask" x="69.2" y="33.9" width="704" height="528" maskUnits="userSpaceOnUse">
<g id="lottie-ymehjmywpqh__lottie_element_1058_2">
<g>
<rect class="st11" x="69.2" y="33.9" width="704" height="528"/>
<g class="st3">
<path class="st14" d="M324.5,185.8v33.5c0,4.4-3.6,8-8,8h-107.8c-4.4,0-8-3.6-8-8v-33.5c0-4.4,3.6-8,8-8h107.8c4.4,0,8,3.6,8,8Z"/>
</g>
</g>
</g>
</mask>
<linearGradient id="Adsız_degrade_2" data-name="Adsız degrade 2" x1="68.8" y1="563" x2="-1.8" y2="563.4" gradientTransform="translate(194.4 765.6) scale(1 -1)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff"/>
<stop offset=".5" stop-color="#fff" stop-opacity=".7"/>
<stop offset="1" stop-color="#fff" stop-opacity=".4"/>
</linearGradient>
<mask id="mask-1" x="69.2" y="33.9" width="704" height="528" maskUnits="userSpaceOnUse">
<g id="lottie-ymehjmywpqh__lottie_element_1038_2">
<g>
<rect class="st11" x="69.2" y="33.9" width="704" height="528"/>
<g class="st3">
<path d="M329.4,427.5v34c0,4.4-3.6,8-8,8h-127.2c-4.4.1-8-3.5-8-7.9v-34c0-4.4,3.6-8,8-8h127.2c4.4-.1,8,3.5,8,7.9Z"/>
</g>
</g>
</g>
</mask>
<linearGradient id="Adsız_degrade_4" data-name="Adsız degrade 4" x1="69.7" y1="567.5" x2="-11.4" y2="566.9" gradientTransform="translate(178.6 1012.1) scale(1 -1)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff"/>
<stop offset=".5" stop-color="#fff" stop-opacity=".6"/>
<stop offset="1" stop-color="#fff" stop-opacity=".2"/>
</linearGradient>
<mask id="mask-2" x="69.2" y="33.9" width="704" height="528" maskUnits="userSpaceOnUse">
<g id="lottie-ymehjmywpqh__lottie_element_1018_2">
<g>
<rect class="st11" x="69.2" y="33.9" width="704" height="528"/>
<g class="st3">
<path class="st9" d="M689.3,383.8v34.2c-.1,4.4-3.7,8-8.1,8h-150.3c-4.4,0-8-3.6-8-8v-34.2c.1-4.4,3.7-8,8.1-8h150.2c4.4,0,8,3.6,8,8Z"/>
</g>
</g>
</g>
</mask>
<linearGradient id="Adsız_degrade_41" data-name="Adsız degrade 4" x1="21.7" y1="568.3" x2="163.7" y2="568.3" gradientTransform="translate(551.4 969.2) scale(1 -1)" xlink:href="#Adsız_degrade_4"/>
<linearGradient id="Adsız_degrade_5" data-name="Adsız degrade 5" x1="56.8" y1="647.8" x2="-1.1" y2="536.3" gradientTransform="translate(349.3 2027.2) scale(3 -3)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#2fabff"/>
<stop offset=".3" stop-color="#5570ff"/>
<stop offset=".6" stop-color="#7b36ff"/>
<stop offset=".8" stop-color="#6a2cdc"/>
<stop offset="1" stop-color="#5921b8"/>
</linearGradient>
</defs>
<rect class="st15" x="69.2" y="33.9" width="704" height="528"/>
<g class="st5">
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
</g>
<g class="st2">
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
</g>
<g class="st1">
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
</g>
<g class="st5">
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
</g>
<g class="st4">
<path class="st17" d="M424.4,139.4c-2.9-1.6-7.6-1.6-10.5,0l-129.6,69.3c-2.9,1.6-5.3,5.5-5.3,8.8v153.3c0,3.3,2.4,7.3,5.2,8.9l129.6,71.9c2.9,1.6,7.6,1.6,10.5,0l130.7-71.6c2.9-1.6,5.3-5.6,5.3-8.9v-150.9c0-3.3-2.4-7.3-5.3-8.9l-130.7-72Z"/>
</g>
<g class="st8">
<path class="st17" d="M424.4,157.4c-2.9-1.6-7.7-1.6-10.6,0l-115.9,61.1c-2.9,1.5-5.3,5.5-5.3,8.8v135.9c0,3.3,2.4,7.3,5.2,8.9l116,64.5c2.9,1.6,7.6,1.6,10.5,0l117.1-63.3c2.9-1.6,5.3-5.5,5.3-8.9v-135c0-3.3-2.4-7.3-5.3-8.8l-117.1-63.2Z"/>
</g>
<g class="st3">
<g>
<path class="st19" d="M324.5,185.8v33.5c0,4.4-3.6,8-8,8h-107.8c-4.4,0-8-3.6-8-8v-33.5c0-4.4,3.6-8,8-8h107.8c4.4,0,8,3.6,8,8Z"/>
<path class="st16" d="M324.5,185.8v33.5c0,4.4-3.6,8-8,8h-107.8c-4.4,0-8-3.6-8-8v-33.5c0-4.4,3.6-8,8-8h107.8c4.4,0,8,3.6,8,8Z"/>
</g>
</g>
<g class="st7">
<g class="st6">
<path class="st0" d="M236.1,209.6c.2,0,.3-.1.4-.2,0,0,0-.3,0-.5v-1.3c0-.3-.1-.5-.3-.5s-.2,0-.4,0c-.4,0-.8.2-1.3.2-.5,0-.9,0-1.2,0-1.4,0-2.4-.4-3.1-1.1s-1-1.8-1-3.3v-.5c0-1.5.3-2.6,1-3.4s1.7-1.1,3.1-1.1,1.4,0,2.2.3c.2,0,.4,0,.4,0,.2,0,.3-.1.3-.5v-1.3c0-.2,0-.4,0-.5s-.2-.2-.3-.2c-1-.3-1.9-.4-2.9-.4-2.2,0-3.9.6-5.1,1.9-1.2,1.3-1.8,3.1-1.8,5.4s.6,4.1,1.7,5.3c1.2,1.2,2.9,1.8,5.1,1.8s2.2-.1,3.2-.5Z"/>
</g>
<g class="st6">
<path class="st0" d="M240.4,204.7c0-2.1.7-3.2,2.1-3.2s2.1,1,2.1,3.2-.7,3.2-2.1,3.2-2.1-1.1-2.1-3.2ZM246.2,208.7c.9-.9,1.3-2.3,1.3-4s-.4-3-1.3-4c-.9-.9-2.1-1.4-3.7-1.4s-2.8.5-3.7,1.4c-.9.9-1.3,2.3-1.3,4s.5,3,1.3,4c.9.9,2.1,1.4,3.7,1.4s2.8-.5,3.7-1.4Z"/>
</g>
<g class="st6">
<path class="st0" d="M252.3,209.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c.7-.5,1.4-.7,2.2-.7s.9.1,1.1.4c.2.2.4.7.4,1.2v6.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c0-1-.3-1.7-.8-2.2-.5-.5-1.2-.8-2.2-.8-1.4,0-2.6.5-3.8,1.4l-.2-.7c0-.3-.2-.4-.6-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
</g>
<g class="st6">
<path class="st0" d="M267.7,209.7c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
</g>
<g class="st6">
<path class="st0" d="M272.5,203.6c0-.8.3-1.3.7-1.7.4-.4.9-.6,1.6-.6,1.1,0,1.7.7,1.7,2v.3h-3.9ZM278.3,209.6c.2,0,.3-.1.4-.2,0,0,0-.2,0-.4v-.9c0-.3-.1-.5-.3-.5s0,0-.1,0h-.1c-1,.3-1.8.4-2.6.4s-1.8-.2-2.3-.6-.8-1-.8-1.9h5.8c.2,0,.3,0,.4,0s.1-.2.2-.4c0-.5,0-1,0-1.4,0-1.3-.4-2.4-1.1-3.1-.7-.7-1.7-1.1-3-1.1s-2.8.5-3.7,1.4c-.9,1-1.3,2.3-1.3,4s.4,3.1,1.3,4c.9.9,2.2,1.4,3.9,1.4s2.2-.2,3.2-.6Z"/>
</g>
<g class="st6">
<path class="st0" d="M283.6,209.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c.7-.5,1.4-.7,2.2-.7s.9.1,1.1.4c.2.2.4.7.4,1.2v6.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c0-1-.3-1.7-.8-2.2-.5-.5-1.2-.8-2.2-.8-1.4,0-2.6.5-3.8,1.4l-.2-.7c0-.3-.2-.4-.6-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
</g>
<g class="st6">
<path class="st0" d="M299.1,209.7c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
</g>
</g>
<g class="st3">
<g>
<path class="st12" d="M329.4,427.5v34c0,4.4-3.6,8-8,8h-127.2c-4.4.1-8-3.5-8-7.9v-34c0-4.4,3.6-8,8-8h127.2c4.4-.1,8,3.5,8,7.9Z"/>
<path class="st16" d="M329.4,427.5v34c0,4.4-3.6,8-8,8h-127.2c-4.4.1-8-3.5-8-7.9v-34c0-4.4,3.6-8,8-8h127.2c4.4-.1,8,3.5,8,7.9Z"/>
</g>
</g>
<g class="st7">
<g class="st6">
<path class="st0" d="M213.9,439.7h1.7c2.4,0,3.6,1.5,3.6,4.4v.4c0,2.9-1.2,4.4-3.6,4.4h-1.7v-9.2ZM215.9,451.2c2,0,3.6-.6,4.7-1.8,1.1-1.2,1.7-2.9,1.7-5.1s-.6-3.9-1.7-5.1c-1.1-1.2-2.7-1.8-4.8-1.8h-4.5c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v12.9c0,.2,0,.3.1.4,0,0,.2.1.4.1h4.5Z"/>
</g>
<g class="st6">
<path class="st0" d="M227.4,449.3c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM229.1,451.2c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
</g>
<g class="st6">
<path class="st0" d="M241.5,451.1c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
</g>
<g class="st6">
<path class="st0" d="M246.6,449.3c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM248.3,451.2c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
</g>
<g class="st6">
<path class="st0" d="M263,450.8c.6-.4,1.1-1.1,1.5-1.9.4-.8.6-1.8.6-2.9,0-1.6-.4-2.9-1.1-3.8-.8-.9-1.8-1.4-3.1-1.4s-1,.1-1.5.3c-.5.2-1,.5-1.4.8v-4.9c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v13.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.5c.3,0,.5-.2.6-.4v-.6c.5.4.9.7,1.5.9.5.2,1.1.3,1.7.3s1.6-.2,2.2-.7ZM257.9,448.7v-5.3c.6-.4,1.3-.5,2-.5s1.3.2,1.7.8.5,1.3.5,2.4-.2,1.9-.5,2.4-.9.8-1.6.8-1.4-.2-2-.5Z"/>
</g>
<g class="st6">
<path class="st0" d="M269.8,449.3c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM271.5,451.2c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
</g>
<g class="st6">
<path class="st0" d="M284.1,450.7c.8-.6,1.1-1.4,1.1-2.4s-.2-1.3-.5-1.7c-.3-.4-.9-.8-1.8-1.2l-1.5-.6c-.5-.2-.8-.4-1-.5-.2-.1-.2-.3-.2-.6s.1-.5.4-.6c.3-.1.6-.2,1.2-.2s1.4,0,1.9.2c.3,0,.5.1.6.1.2,0,.3-.1.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.3-.2-.8-.3-1.7-.5-2.7-.5s-2.2.3-2.9.8c-.7.5-1.1,1.3-1.1,2.2s.2,1.3.5,1.8c.4.5,1,.9,1.8,1.2l1.6.6c.5.2.8.3.9.5.1.2.2.4.2.6,0,.7-.5,1-1.4,1s-1,0-1.5-.1c-.5,0-1-.2-1.5-.3-.1,0-.2,0-.3,0-.2,0-.3.1-.3.5v.9c0,.2,0,.3,0,.4,0,0,.2.2.4.2.9.4,2,.6,3.2.6s2.2-.3,3-.9Z"/>
</g>
<g class="st6">
<path class="st0" d="M289.3,445c0-.8.3-1.3.7-1.7.4-.4.9-.6,1.6-.6,1.1,0,1.7.7,1.7,2v.3h-3.9ZM295.1,451c.2,0,.3-.1.4-.2,0,0,0-.2,0-.4v-.9c0-.3-.1-.5-.3-.5s0,0-.1,0h-.1c-1,.3-1.8.4-2.6.4s-1.8-.2-2.3-.6-.8-1-.8-1.9h5.8c.2,0,.3,0,.4,0s.1-.2.2-.4c0-.5,0-1,0-1.4,0-1.3-.4-2.4-1.1-3.1-.7-.7-1.7-1.1-3-1.1s-2.8.5-3.7,1.4c-.9,1-1.3,2.3-1.3,4s.4,3.1,1.3,4c.9.9,2.2,1.4,3.9,1.4s2.2-.2,3.2-.6Z"/>
</g>
<g class="st6">
<path class="st0" d="M303.8,450.7c.8-.6,1.1-1.4,1.1-2.4s-.2-1.3-.5-1.7c-.3-.4-.9-.8-1.8-1.2l-1.5-.6c-.5-.2-.8-.4-1-.5-.2-.1-.2-.3-.2-.6s.1-.5.4-.6c.3-.1.6-.2,1.2-.2s1.4,0,1.9.2c.3,0,.5.1.6.1.2,0,.3-.1.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.3-.2-.8-.3-1.7-.5-2.7-.5s-2.2.3-2.9.8c-.7.5-1.1,1.3-1.1,2.2s.2,1.3.5,1.8c.4.5,1,.9,1.8,1.2l1.6.6c.5.2.8.3.9.5.1.2.2.4.2.6,0,.7-.5,1-1.4,1s-1,0-1.5-.1c-.5,0-1-.2-1.5-.3-.1,0-.2,0-.3,0-.2,0-.3.1-.3.5v.9c0,.2,0,.3,0,.4,0,0,.2.2.4.2.9.4,2,.6,3.2.6s2.2-.3,3-.9Z"/>
</g>
</g>
<g class="st3">
<g>
<path class="st13" d="M689.3,383.8v34.2c-.1,4.4-3.7,8-8.1,8h-150.3c-4.4,0-8-3.6-8-8v-34.2c.1-4.4,3.7-8,8.1-8h150.2c4.4,0,8,3.6,8,8Z"/>
<path class="st16" d="M689.3,383.8v34.2c-.1,4.4-3.7,8-8.1,8h-150.3c-4.4,0-8-3.6-8-8v-34.2c.1-4.4,3.7-8,8.1-8h150.2c4.4,0,8,3.6,8,8Z"/>
</g>
</g>
<g class="st7">
<g class="st6">
<path class="st0" d="M549.5,407.8c.2,0,.4,0,.5-.1,0,0,.2-.2.2-.4l.8-2.6h4.8l.8,2.6c0,.2.2.3.2.4.1,0,.2.1.4.1h2.2c.3,0,.4-.1.4-.3s0-.1,0-.2c0,0,0-.2-.1-.3l-4.4-12.5c0-.2-.1-.3-.2-.4,0,0-.3-.1-.5-.1h-2.2c-.2,0-.3,0-.3,0,0,0-.1,0-.2.2,0,0-.1.2-.2.3l-4.4,12.5c0,.1,0,.2-.1.3,0,.1,0,.2,0,.2,0,.2.1.3.4.3h2.1ZM551.7,402.5l1.7-5.8,1.8,5.8h-3.5Z"/>
</g>
<g class="st6">
<path class="st0" d="M564.9,405.3v-5.3c.6-.4,1.3-.5,2-.5s1.3.2,1.7.8.5,1.3.5,2.4-.2,1.9-.5,2.4-.9.8-1.6.8-1.4-.2-2-.5ZM564.4,412c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-4.4c.3.3.8.5,1.3.7.5.2,1,.3,1.6.3.8,0,1.6-.2,2.2-.7.6-.4,1.1-1.1,1.5-1.9.4-.8.6-1.8.6-2.9,0-1.6-.4-2.9-1.1-3.8-.8-.9-1.8-1.4-3.1-1.4s-1.2.1-1.7.3c-.6.2-1,.5-1.4.9v-.5c-.2-.3-.4-.4-.7-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v13.3c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
</g>
<g class="st6">
<path class="st0" d="M577.2,405.3v-5.3c.6-.4,1.3-.5,2-.5s1.3.2,1.7.8.5,1.3.5,2.4-.2,1.9-.5,2.4-.9.8-1.6.8-1.4-.2-2-.5ZM576.7,412c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-4.4c.3.3.8.5,1.3.7.5.2,1,.3,1.6.3.8,0,1.6-.2,2.2-.7.6-.4,1.1-1.1,1.5-1.9.4-.8.6-1.8.6-2.9,0-1.6-.4-2.9-1.1-3.8-.8-.9-1.8-1.4-3.1-1.4s-1.2.1-1.7.3c-.6.2-1,.5-1.4.9v-.5c-.2-.3-.4-.4-.7-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v13.3c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
</g>
<g class="st6">
<path class="st0" d="M590.6,407.8c.2,0,.3-.1.4-.2,0,0,0-.2,0-.5v-1c0-.2,0-.3,0-.3,0,0-.1-.1-.3-.1s-.1,0-.2,0c0,0-.2,0-.3,0-.5,0-.7-.3-.7-1v-11.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v11.4c0,2,.9,3,2.7,3s1,0,1.4-.2Z"/>
</g>
<g class="st6">
<path class="st0" d="M595.8,395.6c.3-.3.5-.7.5-1.1s-.2-.9-.5-1.1-.7-.4-1.2-.4-.9.1-1.2.4c-.3.3-.5.7-.5,1.1s.1.9.5,1.1c.3.3.7.4,1.2.4s.9-.1,1.2-.4ZM595.5,407.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-9.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
</g>
<g class="st6">
<path class="st0" d="M604.6,408c.4,0,.8-.2,1.2-.3.2,0,.3-.1.3-.2,0,0,0-.2,0-.4v-1c0-.3-.1-.4-.3-.4s-.2,0-.3,0c-.6.2-1.1.2-1.6.2-.9,0-1.6-.2-2-.7-.4-.5-.6-1.2-.6-2.2v-.3c0-1,.2-1.8.7-2.2.4-.5,1.1-.7,2.1-.7s1,0,1.6.2c0,0,0,0,.1,0,0,0,0,0,.1,0,.2,0,.3-.2.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.4-.2-.7-.3-1.5-.4-2.3-.4-1.6,0-2.9.5-3.8,1.4-.9,1-1.4,2.3-1.4,4s.4,3,1.3,3.9c.9.9,2.1,1.4,3.7,1.4s.8,0,1.2-.1Z"/>
</g>
<g class="st6">
<path class="st0" d="M610.7,405.9c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM612.4,407.8c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
</g>
<g class="st6">
<path class="st0" d="M624.7,407.7c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
</g>
<g class="st6">
<path class="st0" d="M630.1,395.6c.3-.3.5-.7.5-1.1s-.2-.9-.5-1.1-.7-.4-1.2-.4-.9.1-1.2.4c-.3.3-.5.7-.5,1.1s.1.9.5,1.1c.3.3.7.4,1.2.4s.9-.1,1.2-.4ZM629.9,407.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-9.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
</g>
<g class="st6">
<path class="st0" d="M635.7,402.7c0-2.1.7-3.2,2.1-3.2s2.1,1,2.1,3.2-.7,3.2-2.1,3.2-2.1-1.1-2.1-3.2ZM641.5,406.7c.9-.9,1.3-2.3,1.3-4s-.4-3-1.3-4c-.9-.9-2.1-1.4-3.7-1.4s-2.8.5-3.7,1.4c-.9.9-1.3,2.3-1.3,4s.5,3,1.3,4c.9.9,2.1,1.4,3.7,1.4s2.8-.5,3.7-1.4Z"/>
</g>
<g class="st6">
<path class="st0" d="M647.6,407.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c.7-.5,1.4-.7,2.2-.7s.9.1,1.1.4c.2.2.4.7.4,1.2v6.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c0-1-.3-1.7-.8-2.2-.5-.5-1.2-.8-2.2-.8-1.4,0-2.6.5-3.8,1.4l-.2-.7c0-.3-.2-.4-.6-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
</g>
<g class="st6">
<path class="st0" d="M663.3,407.3c.8-.6,1.1-1.4,1.1-2.4s-.2-1.3-.5-1.7c-.3-.4-.9-.8-1.8-1.2l-1.5-.6c-.5-.2-.8-.4-1-.5-.2-.1-.2-.3-.2-.6s.1-.5.4-.6c.3-.1.6-.2,1.2-.2s1.4,0,1.9.2c.3,0,.5.1.6.1.2,0,.3-.1.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.3-.2-.8-.3-1.7-.5-2.7-.5s-2.2.3-2.9.8c-.7.5-1.1,1.3-1.1,2.2s.2,1.3.5,1.8c.4.5,1,.9,1.8,1.2l1.6.6c.5.2.8.3.9.5.1.2.2.4.2.6,0,.7-.5,1-1.4,1s-1,0-1.5-.1c-.5,0-1-.2-1.5-.3-.1,0-.2,0-.3,0-.2,0-.3.1-.3.5v.9c0,.2,0,.3,0,.4,0,0,.2.2.4.2.9.4,2,.6,3.2.6s2.2-.3,3-.9Z"/>
</g>
</g>
<g>
<path class="st18" d="M452.1,74c-19.6-11.3-43.8-11.3-63.4,0l-145.3,83.9c-19.6,11.3-31.7,32.3-31.7,54.9v167.8c0,22.7,12.1,43.6,31.7,54.9l145.3,83.9c19.6,11.3,43.8,11.3,63.4,0l145.3-83.9c19.6-11.3,31.7-32.3,31.7-54.9v-167.8c0-22.7-12.1-43.6-31.7-54.9l-145.3-83.9Z"/>
<path class="st10" d="M438.6,293.4l-12.7,25.4,120.5,69.8,12.7-25.4-120.5-69.8ZM422.7,269.8c-2-1.1-4.4-1.1-6.3,0l-21.1,12.2c-2,1.1-3.2,3.2-3.2,5.5v24.4c0,2.3,1.2,4.4,3.2,5.5l21.1,12.2c2,1.1,4.4,1.1,6.3,0l21.1-12.2c2-1.1,3.2-3.2,3.2-5.5v-24.4c0-2.3-1.2-4.4-3.2-5.5l-21.1-12.2ZM411.6,163.4c7.9-4.5,17.5-4.5,25.4,0l98.2,56.7c7.9,4.5,12.7,12.9,12.7,22v113.4c0,9.1-4.8,17.4-12.7,22l-98.2,56.7c-7.9,4.5-17.5,4.5-25.4,0l-98.2-56.7c-7.9-4.5-12.7-12.9-12.7-22v-113.4c0-9.1,4.8-17.4,12.7-22l98.2-56.7ZM395.7,135.2l-103.1,59.5c-15.7,9.1-25.4,25.8-25.4,44v119c0,18.1,9.7,34.9,25.4,44l103.1,59.5c15.7,9.1,35,9.1,50.8,0l103.1-59.5c15.7-9.1,25.4-25.8,25.4-44v-119c0-18.1-9.7-34.9-25.4-44l-103.1-59.5c-15.7-9.1-35-9.1-50.8,0Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 304 182" style="enable-background:new 0 0 304 182;" xml:space="preserve">
<style type="text/css">
.st0{fill:#fff;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FF9900;}
</style>
<g>
<path class="st0" d="M86.4,66.4c0,3.7,0.4,6.7,1.1,8.9c0.8,2.2,1.8,4.6,3.2,7.2c0.5,0.8,0.7,1.6,0.7,2.3c0,1-0.6,2-1.9,3l-6.3,4.2 c-0.9,0.6-1.8,0.9-2.6,0.9c-1,0-2-0.5-3-1.4C76.2,90,75,88.4,74,86.8c-1-1.7-2-3.6-3.1-5.9c-7.8,9.2-17.6,13.8-29.4,13.8 c-8.4,0-15.1-2.4-20-7.2c-4.9-4.8-7.4-11.2-7.4-19.2c0-8.5,3-15.4,9.1-20.6c6.1-5.2,14.2-7.8,24.5-7.8c3.4,0,6.9,0.3,10.6,0.8 c3.7,0.5,7.5,1.3,11.5,2.2v-7.3c0-7.6-1.6-12.9-4.7-16c-3.2-3.1-8.6-4.6-16.3-4.6c-3.5,0-7.1,0.4-10.8,1.3c-3.7,0.9-7.3,2-10.8,3.4 c-1.6,0.7-2.8,1.1-3.5,1.3c-0.7,0.2-1.2,0.3-1.6,0.3c-1.4,0-2.1-1-2.1-3.1v-4.9c0-1.6,0.2-2.8,0.7-3.5c0.5-0.7,1.4-1.4,2.8-2.1 c3.5-1.8,7.7-3.3,12.6-4.5c4.9-1.3,10.1-1.9,15.6-1.9c11.9,0,20.6,2.7,26.2,8.1c5.5,5.4,8.3,13.6,8.3,24.6V66.4z M45.8,81.6 c3.3,0,6.7-0.6,10.3-1.8c3.6-1.2,6.8-3.4,9.5-6.4c1.6-1.9,2.8-4,3.4-6.4c0.6-2.4,1-5.3,1-8.7v-4.2c-2.9-0.7-6-1.3-9.2-1.7 c-3.2-0.4-6.3-0.6-9.4-0.6c-6.7,0-11.6,1.3-14.9,4c-3.3,2.7-4.9,6.5-4.9,11.5c0,4.7,1.2,8.2,3.7,10.6 C37.7,80.4,41.2,81.6,45.8,81.6z M126.1,92.4c-1.8,0-3-0.3-3.8-1c-0.8-0.6-1.5-2-2.1-3.9L96.7,10.2c-0.6-2-0.9-3.3-0.9-4 c0-1.6,0.8-2.5,2.4-2.5h9.8c1.9,0,3.2,0.3,3.9,1c0.8,0.6,1.4,2,2,3.9l16.8,66.2l15.6-66.2c0.5-2,1.1-3.3,1.9-3.9c0.8-0.6,2.2-1,4-1 h8c1.9,0,3.2,0.3,4,1c0.8,0.6,1.5,2,1.9,3.9l15.8,67l17.3-67c0.6-2,1.3-3.3,2-3.9c0.8-0.6,2.1-1,3.9-1h9.3c1.6,0,2.5,0.8,2.5,2.5 c0,0.5-0.1,1-0.2,1.6c-0.1,0.6-0.3,1.4-0.7,2.5l-24.1,77.3c-0.6,2-1.3,3.3-2.1,3.9c-0.8,0.6-2.1,1-3.8,1h-8.6c-1.9,0-3.2-0.3-4-1 c-0.8-0.7-1.5-2-1.9-4L156,23l-15.4,64.4c-0.5,2-1.1,3.3-1.9,4c-0.8,0.7-2.2,1-4,1H126.1z M254.6,95.1c-5.2,0-10.4-0.6-15.4-1.8 c-5-1.2-8.9-2.5-11.5-4c-1.6-0.9-2.7-1.9-3.1-2.8c-0.4-0.9-0.6-1.9-0.6-2.8v-5.1c0-2.1,0.8-3.1,2.3-3.1c0.6,0,1.2,0.1,1.8,0.3 c0.6,0.2,1.5,0.6,2.5,1c3.4,1.5,7.1,2.7,11,3.5c4,0.8,7.9,1.2,11.9,1.2c6.3,0,11.2-1.1,14.6-3.3c3.4-2.2,5.2-5.4,5.2-9.5 c0-2.8-0.9-5.1-2.7-7c-1.8-1.9-5.2-3.6-10.1-5.2L246,52c-7.3-2.3-12.7-5.7-16-10.2c-3.3-4.4-5-9.3-5-14.5c0-4.2,0.9-7.9,2.7-11.1 c1.8-3.2,4.2-6,7.2-8.2c3-2.3,6.4-4,10.4-5.2c4-1.2,8.2-1.7,12.6-1.7c2.2,0,4.5,0.1,6.7,0.4c2.3,0.3,4.4,0.7,6.5,1.1 c2,0.5,3.9,1,5.7,1.6c1.8,0.6,3.2,1.2,4.2,1.8c1.4,0.8,2.4,1.6,3,2.5c0.6,0.8,0.9,1.9,0.9,3.3v4.7c0,2.1-0.8,3.2-2.3,3.2 c-0.8,0-2.1-0.4-3.8-1.2c-5.7-2.6-12.1-3.9-19.2-3.9c-5.7,0-10.2,0.9-13.3,2.8c-3.1,1.9-4.7,4.8-4.7,8.9c0,2.8,1,5.2,3,7.1 c2,1.9,5.7,3.8,11,5.5l14.2,4.5c7.2,2.3,12.4,5.5,15.5,9.6c3.1,4.1,4.6,8.8,4.6,14c0,4.3-0.9,8.2-2.6,11.6 c-1.8,3.4-4.2,6.4-7.3,8.8c-3.1,2.5-6.8,4.3-11.1,5.6C264.4,94.4,259.7,95.1,254.6,95.1z"/>
<g>
<path class="st1" d="M273.5,143.7c-32.9,24.3-80.7,37.2-121.8,37.2c-57.6,0-109.5-21.3-148.7-56.7c-3.1-2.8-0.3-6.6,3.4-4.4 c42.4,24.6,94.7,39.5,148.8,39.5c36.5,0,76.6-7.6,113.5-23.2C274.2,133.6,278.9,139.7,273.5,143.7z"/>
<path class="st1" d="M287.2,128.1c-4.2-5.4-27.8-2.6-38.5-1.3c-3.2,0.4-3.7-2.4-0.8-4.5c18.8-13.2,49.7-9.4,53.3-5 c3.6,4.5-1,35.4-18.6,50.2c-2.7,2.3-5.3,1.1-4.1-1.9C282.5,155.7,291.4,133.4,287.2,128.1z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+23
View File
@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 96 96">
<defs>
<linearGradient id="e399c19f-b68f-429d-b176-18c2117ff73c" x1="-1032.172" x2="-1059.213" y1="145.312" y2="65.426" gradientTransform="matrix(1 0 0 -1 1075 158)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#114a8b"/>
<stop offset="1" stop-color="#0669bc"/>
</linearGradient>
<linearGradient id="ac2a6fc2-ca48-4327-9a3c-d4dcc3256e15" x1="-1023.725" x2="-1029.98" y1="108.083" y2="105.968" gradientTransform="matrix(1 0 0 -1 1075 158)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-opacity=".3"/>
<stop offset=".071" stop-opacity=".2"/>
<stop offset=".321" stop-opacity=".1"/>
<stop offset=".623" stop-opacity=".05"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="a7fee970-a784-4bb1-af8d-63d18e5f7db9" x1="-1027.165" x2="-997.482" y1="147.642" y2="68.561" gradientTransform="matrix(1 0 0 -1 1075 158)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3ccbf4"/>
<stop offset="1" stop-color="#2892df"/>
</linearGradient>
</defs>
<path fill="url(#e399c19f-b68f-429d-b176-18c2117ff73c)" d="M33.338 6.544h26.038l-27.03 80.087a4.152 4.152 0 0 1-3.933 2.824H8.149a4.145 4.145 0 0 1-3.928-5.47L29.404 9.368a4.152 4.152 0 0 1 3.934-2.825z"/>
<path fill="#0078d4" d="M71.175 60.261h-41.29a1.911 1.911 0 0 0-1.305 3.309l26.532 24.764a4.171 4.171 0 0 0 2.846 1.121h23.38z"/>
<path fill="url(#ac2a6fc2-ca48-4327-9a3c-d4dcc3256e15)" d="M33.338 6.544a4.118 4.118 0 0 0-3.943 2.879L4.252 83.917a4.14 4.14 0 0 0 3.908 5.538h20.787a4.443 4.443 0 0 0 3.41-2.9l5.014-14.777 17.91 16.705a4.237 4.237 0 0 0 2.666.972H81.24L71.024 60.261l-29.781.007L59.47 6.544z"/>
<path fill="url(#a7fee970-a784-4bb1-af8d-63d18e5f7db9)" d="M66.595 9.364a4.145 4.145 0 0 0-3.928-2.82H33.648a4.146 4.146 0 0 1 3.928 2.82l25.184 74.62a4.146 4.146 0 0 1-3.928 5.472h29.02a4.146 4.146 0 0 0 3.927-5.472z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="512" height="510" viewBox="0 0 512 510" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M115.612 0H396.387C459.974 0 512 52.026 512 115.612V394.027C512 457.614 459.974 509.639 396.387 509.639H115.612C52.026 509.639 0 457.614 0 394.027V115.612C0 52.026 52.026 0 115.612 0Z" fill="#D77655"/>
<path d="M142.27 316.619L215.925 275.293L217.163 271.704L215.925 269.708L212.336 269.707L200.026 268.948L157.942 267.81L121.444 266.294L86.083 264.398L77.186 262.503L68.846 251.508L69.705 246.024L77.187 240.994L87.904 241.929L111.587 243.546L147.124 245.998L172.906 247.515L211.099 251.483H217.163L218.023 249.032L215.95 247.515L214.332 245.998L177.556 221.076L137.746 194.738L116.894 179.572L105.621 171.889L99.934 164.685L97.483 148.964L107.72 137.691L121.47 138.626L124.983 139.562L138.911 150.278L168.66 173.305L207.508 201.917L213.195 206.644L215.47 205.027L215.748 203.889L213.195 199.618L192.065 161.425L169.519 122.577L159.484 106.476L156.83 96.821C155.895 92.853 155.213 89.517 155.213 85.447L166.865 69.624L173.31 67.551L188.855 69.624L195.402 75.311L205.057 97.403L220.703 132.183L244.968 179.474L252.071 193.502L255.862 206.494L257.278 210.462L259.727 210.461V208.186L261.724 181.545L265.414 148.838L269.003 106.754L270.242 94.9L276.105 80.694L287.757 73.011L296.856 77.359L304.338 88.075L303.302 95.001L298.853 123.916L290.133 169.21L284.446 199.541H287.759L291.551 195.75L306.893 175.378L332.675 143.151L344.049 130.362L357.319 116.233L365.836 109.509L381.936 109.508L393.79 127.125L388.483 145.324L371.902 166.353L358.152 184.172L338.436 210.712L326.127 231.943L327.265 233.637L330.197 233.359L374.733 223.88L398.795 219.533L427.509 214.605L440.501 220.671L441.917 226.838L436.811 239.451L406.101 247.034L370.083 254.238L316.447 266.927L315.79 267.407L316.548 268.342L340.712 270.617L351.049 271.173H376.35L423.464 274.687L435.773 282.826L443.154 292.785L441.916 300.368L422.959 310.023L397.38 303.957L337.678 289.752L317.204 284.646L314.374 284.645V286.339L331.435 303.021L362.701 331.254L401.853 367.651L403.85 376.65L398.82 383.752L393.513 382.994L359.112 357.111L345.842 345.46L315.789 320.158L313.793 320.157V322.811L320.719 332.947L357.293 387.922L359.188 404.781L356.535 410.266L347.056 413.577L336.642 411.682L315.234 381.628L293.142 347.784L275.323 317.453L273.15 318.691L262.635 431.952L257.706 437.74L246.332 442.088L236.854 434.884L231.824 423.232L236.854 400.205L242.92 370.153L247.848 346.267L252.297 316.593L254.951 306.735L254.774 306.078L252.601 306.356L230.231 337.066L196.21 383.043L169.291 411.858L162.846 414.411L151.673 408.622L152.71 398.285L158.953 389.085L196.21 341.693L218.68 312.322L233.188 295.361L233.087 292.91H232.228L133.274 357.161L115.656 359.436L108.073 352.333L109.009 340.681L112.598 336.89L142.347 316.416L142.246 316.518L142.27 316.619Z" fill="#FCF2EE"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+41
View File
@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.9119 99.3171C72.4869 99.9307 74.2828 99.8914 75.8725 99.1264L96.4608 89.2197C98.6242 88.1787 100 85.9892 100 83.5872V16.4133C100 14.0113 98.6243 11.8218 96.4609 10.7808L75.8725 0.873756C73.7862 -0.130129 71.3446 0.11576 69.5135 1.44695C69.252 1.63711 69.0028 1.84943 68.769 2.08341L29.3551 38.0415L12.1872 25.0096C10.589 23.7965 8.35363 23.8959 6.86933 25.2461L1.36303 30.2549C-0.452552 31.9064 -0.454633 34.7627 1.35853 36.417L16.2471 50.0001L1.35853 63.5832C-0.454633 65.2374 -0.452552 68.0938 1.36303 69.7453L6.86933 74.7541C8.35363 76.1043 10.589 76.2037 12.1872 74.9905L29.3551 61.9587L68.769 97.9167C69.3925 98.5406 70.1246 99.0104 70.9119 99.3171ZM75.0152 27.2989L45.1091 50.0001L75.0152 72.7012V27.2989Z" fill="white"/>
</mask>
<g mask="url(#mask0)">
<path d="M96.4614 10.7962L75.8569 0.875542C73.4719 -0.272773 70.6217 0.211611 68.75 2.08333L1.29858 63.5832C-0.515693 65.2373 -0.513607 68.0937 1.30308 69.7452L6.81272 74.754C8.29793 76.1042 10.5347 76.2036 12.1338 74.9905L93.3609 13.3699C96.086 11.3026 100 13.2462 100 16.6667V16.4275C100 14.0265 98.6246 11.8378 96.4614 10.7962Z" fill="#0065A9"/>
<g filter="url(#filter0_d)">
<path d="M96.4614 89.2038L75.8569 99.1245C73.4719 100.273 70.6217 99.7884 68.75 97.9167L1.29858 36.4169C-0.515693 34.7627 -0.513607 31.9063 1.30308 30.2548L6.81272 25.246C8.29793 23.8958 10.5347 23.7964 12.1338 25.0095L93.3609 86.6301C96.086 88.6974 100 86.7538 100 83.3334V83.5726C100 85.9735 98.6246 88.1622 96.4614 89.2038Z" fill="#007ACC"/>
</g>
<g filter="url(#filter1_d)">
<path d="M75.8578 99.1263C73.4721 100.274 70.6219 99.7885 68.75 97.9166C71.0564 100.223 75 98.5895 75 95.3278V4.67213C75 1.41039 71.0564 -0.223106 68.75 2.08329C70.6219 0.211402 73.4721 -0.273666 75.8578 0.873633L96.4587 10.7807C98.6234 11.8217 100 14.0112 100 16.4132V83.5871C100 85.9891 98.6234 88.1786 96.4586 89.2196L75.8578 99.1263Z" fill="#1F9CF0"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.25">
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.8511 99.3171C72.4261 99.9306 74.2221 99.8913 75.8117 99.1264L96.4 89.2197C98.5634 88.1787 99.9392 85.9892 99.9392 83.5871V16.4133C99.9392 14.0112 98.5635 11.8217 96.4001 10.7807L75.8117 0.873695C73.7255 -0.13019 71.2838 0.115699 69.4527 1.44688C69.1912 1.63705 68.942 1.84937 68.7082 2.08335L29.2943 38.0414L12.1264 25.0096C10.5283 23.7964 8.29285 23.8959 6.80855 25.246L1.30225 30.2548C-0.513334 31.9064 -0.515415 34.7627 1.29775 36.4169L16.1863 50L1.29775 63.5832C-0.515415 65.2374 -0.513334 68.0937 1.30225 69.7452L6.80855 74.754C8.29285 76.1042 10.5283 76.2036 12.1264 74.9905L29.2943 61.9586L68.7082 97.9167C69.3317 98.5405 70.0638 99.0104 70.8511 99.3171ZM74.9544 27.2989L45.0483 50L74.9544 72.7012V27.2989Z" fill="url(#paint0_linear)"/>
</g>
</g>
<defs>
<filter id="filter0_d" x="-8.39411" y="15.8291" width="116.727" height="92.2456" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset/>
<feGaussianBlur stdDeviation="4.16667"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter1_d" x="60.4167" y="-8.07558" width="47.9167" height="116.151" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset/>
<feGaussianBlur stdDeviation="4.16667"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<linearGradient id="paint0_linear" x1="49.9392" y1="0.257812" x2="49.9392" y2="99.7423" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

+8
View File
@@ -0,0 +1,8 @@
<svg width="66" height="48" viewBox="0 0 66 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M64.3029 20.8302C62.9894 20.8302 62.1144 20.0449 62.1144 18.4331V9.17517C62.1144 3.26504 59.7268 0 53.5592 0H50.6941V6.24078H51.5697C53.9968 6.24078 55.1508 7.60467 55.1508 10.0431V18.2264C55.1508 21.7807 56.1853 23.2273 58.4535 23.9713C56.1853 24.6739 55.1508 26.1617 55.1508 29.716C55.1508 31.7412 55.1508 33.7663 55.1508 35.7916C55.1508 37.4861 55.1508 39.1393 54.7131 40.8337C54.2754 42.4044 53.5592 43.8922 52.5644 45.1733C52.0073 45.9174 51.3707 46.5373 50.6545 47.116V47.9425H53.5193C59.687 47.9425 62.0746 44.6774 62.0746 38.7672V29.5094C62.0746 27.8562 62.9103 27.1123 64.2634 27.1123H65.8944V20.8714H64.3029V20.8302Z" fill="#D9D9D9"/>
<path d="M44.8049 9.42443H35.9712C35.7722 9.42443 35.6131 9.25912 35.6131 9.05247V8.34987C35.6131 8.14322 35.7722 7.97791 35.9712 7.97791H44.8447C45.0436 7.97791 45.2028 8.14322 45.2028 8.34987V9.05247C45.2028 9.25912 45.0038 9.42443 44.8049 9.42443Z" fill="#D9D9D9"/>
<path d="M46.3171 18.3513H39.871C39.672 18.3513 39.5128 18.1859 39.5128 17.9792V17.2767C39.5128 17.0701 39.672 16.9047 39.871 16.9047H46.3171C46.5161 16.9047 46.6752 17.0701 46.6752 17.2767V17.9792C46.6752 18.1446 46.5161 18.3513 46.3171 18.3513Z" fill="#D9D9D9"/>
<path d="M48.8636 13.8879H35.9712C35.7722 13.8879 35.6131 13.7226 35.6131 13.5159V12.8133C35.6131 12.6067 35.7722 12.4413 35.9712 12.4413H48.8237C49.0228 12.4413 49.182 12.6067 49.182 12.8133V13.5159C49.182 13.6812 49.0626 13.8879 48.8636 13.8879Z" fill="#D9D9D9"/>
<path d="M25.7449 11.4483C26.6203 11.4483 27.4958 11.531 28.3313 11.7377V10.0431C28.3313 7.64602 29.5251 6.24078 31.9126 6.24078H32.7879V0H29.923C23.7552 0 21.3679 3.26504 21.3679 9.17517V12.2336C22.7605 11.7377 24.2329 11.4483 25.7449 11.4483Z" fill="#D9D9D9"/>
<path d="M51.5695 33.9308C50.9329 28.6819 47.0333 24.3009 42.0196 23.3089C40.6269 23.0197 39.2342 22.9783 37.8813 23.2263C37.8415 23.2263 37.8415 23.1849 37.8018 23.1849C35.6132 18.4321 30.9179 15.291 25.8246 15.291C20.7313 15.291 16.0757 18.3494 13.8474 23.1023C13.8076 23.1023 13.8076 23.1437 13.7678 23.1437C12.3353 22.9783 10.9028 23.0609 9.47035 23.433C4.5362 24.6728 0.795835 28.9711 0.119377 34.1786C0.039787 34.7159 0 35.2532 0 35.7492C0 37.3196 1.03457 38.7662 2.54664 38.9729C4.41683 39.2623 6.04827 37.7743 6.00848 35.8732C6.00848 35.5838 6.00848 35.2532 6.04827 34.9639C6.36659 32.3188 8.31638 30.087 10.863 29.467C11.6589 29.2604 12.4547 29.2191 13.2107 29.3432C15.638 29.6738 18.0255 28.3925 19.06 26.1607C19.8161 24.5075 21.0098 23.0609 22.6015 22.2757C24.3522 21.4077 26.3418 21.2838 28.1723 21.9452C30.0822 22.6477 31.5146 24.1355 32.3901 25.9953C33.3053 27.814 33.743 29.0951 35.6928 29.3432C36.4886 29.467 38.7169 29.4257 39.5526 29.3844C41.184 29.3844 42.8154 29.963 43.9694 31.1616C44.7254 31.9881 45.2825 33.0214 45.5213 34.1786C45.8793 36.0385 45.4417 37.8983 44.3673 39.3035C43.6112 40.2954 42.5767 41.0394 41.4227 41.37C40.8656 41.5354 40.3085 41.5766 39.7514 41.5766C39.4332 41.5766 38.9955 41.5766 38.4782 41.5766C36.8866 41.5766 33.5043 41.5766 30.9576 41.5766C29.2069 41.5766 27.8141 40.1302 27.8141 38.3116V26.2019C27.8141 25.7061 27.4162 25.2928 26.9387 25.2928H25.7052C23.2778 25.334 21.3281 28.1446 21.3281 31.1202C21.3281 34.096 21.3281 41.99 21.3281 41.99C21.3281 45.2137 23.8349 47.8175 26.9387 47.8175C26.9387 47.8175 40.7464 47.7761 40.9452 47.7761C44.1285 47.4454 47.0731 45.751 49.0626 43.1472C51.0522 40.6261 51.9674 37.3196 51.5695 33.9308Z" fill="#D9D9D9"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.5 MiB

+1
View File
@@ -0,0 +1 @@
<svg width="82" height="80" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" overflow="hidden"><g transform="translate(-550 -124)"><g><g><g><g><path d="M551 124 631 124 631 204 551 204Z" fill="#ED7100" fill-rule="evenodd" fill-opacity="1"/><path d="M612.069 162.386C607.327 165.345 600.717 168.353 593.46 170.855 588.339 172.62 583.33 173.978 578.865 174.838 582.727 184.68 589.944 191.037 596.977 189.853 603.514 188.75 608.387 181.093 609.1 170.801L611.096 170.939C610.304 182.347 604.893 190.545 597.309 191.825 596.648 191.937 595.984 191.991 595.323 191.991 587.945 191.991 580.718 185.209 576.871 175.194 575.733 175.38 574.625 175.542 573.584 175.653 572.173 175.803 570.901 175.879 569.769 175.879 565.95 175.879 563.726 175.025 563.141 173.328 562.414 171.218 564.496 168.566 569.328 165.445L570.414 167.125C565.704 170.167 564.814 172.046 565.032 172.677 565.263 173.348 567.279 174.313 573.372 173.665 574.267 173.57 575.216 173.433 576.187 173.28 575.537 171.297 575.014 169.205 574.647 167.028 573.406 159.673 574.056 152.438 576.48 146.654 578.969 140.715 583.031 136.99 587.917 136.166 593.803 135.171 600.075 138.691 604.679 145.579L603.017 146.69C598.862 140.476 593.349 137.28 588.249 138.138 584.063 138.844 580.539 142.143 578.325 147.427 576.046 152.866 575.44 159.709 576.62 166.695 576.988 168.876 577.515 170.966 578.173 172.937 582.618 172.1 587.651 170.742 592.807 168.965 599.927 166.51 606.392 163.572 611.01 160.689 616.207 157.447 617.201 155.444 616.969 154.772 616.769 154.189 615.095 153.299 610.097 153.653L609.957 151.657C615.171 151.289 618.171 152.116 618.86 154.12 619.619 156.32 617.334 159.101 612.069 162.386" fill="#FFFFFF" fill-rule="evenodd" fill-opacity="1"/></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31 6V22C31 23.65 29.65 25 28 25H4C2.35 25 1 23.65 1 22V6C1 4.35 2.35 3 4 3H28C29.65 3 31 4.35 31 6Z" fill="#2197F3"/>
<path d="M21 27H17V24C17 23.4478 16.5522 23 16 23C15.4478 23 15 23.4478 15 24V27H11C10.4478 27 10 27.4478 10 28C10 28.5522 10.4478 29 11 29H21C21.5522 29 22 28.5522 22 28C22 27.4478 21.5522 27 21 27Z" fill="#FFC10A"/>
<path d="M31 17V22C31 23.65 29.65 25 28 25H4C2.35 25 1 23.65 1 22V17H31Z" fill="#3F51B5"/>
</svg>

After

Width:  |  Height:  |  Size: 540 B

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title>file_type_devcontainer</title><circle cx="16" cy="16" r="14" style="fill:#193e63"/><polygon points="10.777 22.742 9.343 21.348 12.729 17.865 9.346 14.417 10.774 13.017 15.525 17.859 10.777 22.742" style="fill:#add1ea"/><polygon points="21.42 19.101 22.854 17.706 19.468 14.224 22.851 10.776 21.423 9.376 16.672 14.218 21.42 19.101" style="fill:#add1ea"/></svg>

After

Width:  |  Height:  |  Size: 575 B

+10
View File
@@ -0,0 +1,10 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 185.69 237" enable-background="new 0 0 185.69 237" xml:space="preserve">
<g>
<path fill="#333333" d="M135.96,0h-1.06H16.93C7.58,0,0,7.58,0,16.93v203.14C0,229.42,7.58,237,16.93,237h151.83
c9.35,0,16.93-7.58,16.93-16.93V50.79v-1.06L135.96,0z"/>
<circle fill="#3FA9F5" cx="92.84" cy="118.5" r="25.39"/>
<line fill="#1A1A1A" x1="185.69" y1="50.79" x2="134.9" y2="0"/>
<path fill="#3FA9F5" d="M135.96,32.8c0,9.35,7.58,16.93,16.93,16.93h32.8L135.96,0V32.8z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 600 B

+1
View File
@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68.03 68.03"><defs><style>.cls-1{fill:#da291c;}</style></defs><title>Artboard 1</title><polygon class="cls-1" points="34.02 13.31 11.27 52.72 14.52 52.72 34.02 18.94 34.02 24.57 17.77 52.72 21.02 52.72 34.02 30.2 34.02 35.83 24.27 52.72 27.52 52.72 34.02 41.46 34.02 47.09 30.77 52.72 34.02 52.72 34.02 52.72 56.77 52.72 34.02 13.31"/></svg>

After

Width:  |  Height:  |  Size: 427 B

+147
View File
@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xml:space="preserve"
width="560"
height="560"
version="1.1"
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
viewBox="0 0 560 560"
id="svg44"
sodipodi:docname="icon_raw.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
inkscape:export-filename="/home/umarcor/filebrowser/logo/icon_raw.svg.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"><metadata
id="metadata48"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="711"
id="namedview46"
showgrid="false"
inkscape:zoom="0.33714286"
inkscape:cx="-172.33051"
inkscape:cy="280"
inkscape:window-x="0"
inkscape:window-y="20"
inkscape:window-maximized="1"
inkscape:current-layer="svg44" />
<defs
id="defs4">
<style
type="text/css"
id="style2">
<![CDATA[
.fil1 {fill:#FEFEFE}
.fil6 {fill:#006498}
.fil7 {fill:#0EA5EB}
.fil8 {fill:#2979FF}
.fil3 {fill:#2BBCFF}
.fil0 {fill:#455A64}
.fil4 {fill:#53C6FC}
.fil5 {fill:#BDEAFF}
.fil2 {fill:#332C2B;fill-opacity:0.149020}
]]>
</style>
</defs>
<g
id="g85"
transform="translate(-70,-70)"><path
class="fil1"
d="M 350,71 C 504,71 629,196 629,350 629,504 504,629 350,629 196,629 71,504 71,350 71,196 196,71 350,71 Z"
id="path9"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><path
class="fil2"
d="M 475,236 593,387 C 596,503 444,639 301,585 L 225,486 339,330 c 0,0 138,-95 136,-94 z"
id="path11"
inkscape:connector-curvature="0"
style="fill:#332c2b;fill-opacity:0.14902003" /><path
class="fil3"
d="m 231,211 h 208 l 38,24 v 246 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 V 219 c 0,-5 3,-8 8,-8 z"
id="path13"
inkscape:connector-curvature="0"
style="fill:#2bbcff" /><path
class="fil4"
d="m 231,211 h 208 l 38,24 v 2 L 440,214 H 231 c -4,0 -7,3 -7,7 v 263 c -1,-1 -1,-2 -1,-3 V 219 c 0,-5 3,-8 8,-8 z"
id="path15"
inkscape:connector-curvature="0"
style="fill:#53c6fc" /><polygon
class="fil5"
points="305,212 418,212 418,310 305,310 "
id="polygon17"
style="fill:#bdeaff" /><path
class="fil5"
d="m 255,363 h 189 c 3,0 5,2 5,4 V 483 H 250 V 367 c 0,-2 2,-4 5,-4 z"
id="path19"
inkscape:connector-curvature="0"
style="fill:#bdeaff" /><polygon
class="fil6"
points="250,470 449,470 449,483 250,483 "
id="polygon21"
style="fill:#006498" /><path
class="fil6"
d="m 380,226 h 10 c 3,0 6,2 6,5 v 40 c 0,3 -3,6 -6,6 h -10 c -3,0 -6,-3 -6,-6 v -40 c 0,-3 3,-5 6,-5 z"
id="path23"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil1"
d="m 254,226 c 10,0 17,7 17,17 0,9 -7,16 -17,16 -9,0 -17,-7 -17,-16 0,-10 8,-17 17,-17 z"
id="path25"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><path
class="fil6"
d="m 267,448 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,3 -3,3 H 267 c -2,0 -3,-2 -3,-3 v 0 c 0,-2 1,-3 3,-3 z"
id="path27"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil6"
d="m 267,415 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,2 -3,2 H 267 c -2,0 -3,-1 -3,-2 v 0 c 0,-2 1,-3 3,-3 z"
id="path29"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil6"
d="m 267,381 h 165 c 2,0 3,2 3,3 v 0 c 0,2 -1,3 -3,3 H 267 c -2,0 -3,-1 -3,-3 v 0 c 0,-1 1,-3 3,-3 z"
id="path31"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil1"
d="m 236,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
id="path33"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><path
class="fil1"
d="m 463,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
id="path35"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><polygon
class="fil6"
points="305,212 284,212 284,310 305,310 "
id="polygon37"
style="fill:#006498" /><path
class="fil7"
d="m 477,479 v 2 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 v -2 c 0,4 3,8 8,8 h 238 c 5,0 8,-4 8,-8 z"
id="path39"
inkscape:connector-curvature="0"
style="fill:#0ea5eb" /><path
class="fil8"
d="M 350,70 C 505,70 630,195 630,350 630,505 505,630 350,630 195,630 70,505 70,350 70,195 195,70 350,70 Z m 0,46 C 479,116 584,221 584,350 584,479 479,584 350,584 221,584 116,479 116,350 116,221 221,116 350,116 Z"
id="path41"
inkscape:connector-curvature="0"
style="fill:#2979ff" /></g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

+1
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.5 KiB

+64
View File
@@ -0,0 +1,64 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32 62L57.975 46.98V16.964L32 2L6.02602 16.964H6.02502L32 31.984V62Z" fill="url(#paint0_linear_1725:872)"/>
<path d="M57.975 16.964L32 2L6.02602 16.964H6.02502L32 31.984L57.975 16.964Z" fill="url(#paint1_linear_1725:872)"/>
<path d="M57.975 16.964L32 2L6.02602 16.964H6.02502L32 31.984L57.975 16.964Z" fill="url(#paint2_linear_1725:872)"/>
<path d="M57.975 16.964L32 2L6.02602 16.964H6.02502L32 31.984L57.975 16.964Z" fill="url(#paint3_linear_1725:872)"/>
<path d="M6.026 46.9799V16.9639L32 31.9839L6.026 46.9799Z" fill="url(#paint4_linear_1725:872)"/>
<path d="M32 31.9844L6.026 46.9804L32 62.0004V31.9844Z" fill="url(#paint5_linear_1725:872)" style="mix-blend-mode:overlay"/>
<path d="M32 61.9999L57.975 46.9799V16.9639L32 31.9839V61.9999Z" fill="url(#paint6_linear_1725:872)"/>
<path d="M32 61.9999L57.975 46.9799V16.9639L32 31.9839V61.9999Z" fill="url(#paint7_linear_1725:872)"/>
<path d="M32 61.9999L57.975 46.9799V16.9639L32 31.9839V61.9999Z" fill="url(#paint8_linear_1725:872)"/>
<g opacity="0.8">
<path d="M25.028 27.9699L32 32V2L6 16.972L25.028 27.9699Z" fill="url(#paint9_linear_1725:872)" style="mix-blend-mode:overlay"/>
</g>
<defs>
<linearGradient id="paint0_linear_1725:872" x1="17.0683" y1="11.5629" x2="61.3962" y2="55.8908" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FDB60D"/>
<stop offset="0.54795" stop-color="#FF318C"/>
<stop offset="0.88765" stop-color="#6B57FF"/>
</linearGradient>
<linearGradient id="paint1_linear_1725:872" x1="25.5898" y1="28.0668" x2="28.4505" y2="23.112" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="white" stop-opacity="0.6"/>
<stop offset="0.08098" stop-color="#FFC524" stop-opacity="0.4"/>
<stop offset="0.70457" stop-color="#FFC524" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_1725:872" x1="25.4334" y1="5.59024" x2="29.6963" y2="12.9738" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#F9ED32" stop-opacity="0.6"/>
<stop offset="0.19796" stop-color="#FFC524" stop-opacity="0.4"/>
<stop offset="0.70457" stop-color="#FFC524" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint3_linear_1725:872" x1="38.9129" y1="4.99057" x2="37.0135" y2="8.28029" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FDB60D" stop-opacity="0.6"/>
<stop offset="0.23369" stop-color="#FDB60D" stop-opacity="0.4"/>
<stop offset="0.5185" stop-color="#FF318C" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint4_linear_1725:872" x1="6.02553" y1="31.972" x2="48.2774" y2="31.972" gradientUnits="userSpaceOnUse">
<stop offset="0.01477"/>
<stop offset="0.19258"/>
<stop offset="0.57156" stop-color="#6B57FF"/>
<stop offset="0.82576" stop-color="#FF318C"/>
</linearGradient>
<linearGradient id="paint5_linear_1725:872" x1="25.2369" y1="19.192" x2="18.7738" y2="55.8462" gradientUnits="userSpaceOnUse">
<stop offset="0.046628" stop-color="white" stop-opacity="0.86"/>
<stop offset="0.766654" stop-color="#CBCAA4" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint6_linear_1725:872" x1="32.0001" y1="39.4819" x2="38.997" y2="39.4819" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFB2FF" stop-opacity="0.6"/>
<stop offset="0.08098" stop-color="#D828FF" stop-opacity="0.4"/>
<stop offset="0.70457" stop-color="#FF318C" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint7_linear_1725:872" x1="58.0814" y1="39.4819" x2="50.9352" y2="39.4819" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#6644FF" stop-opacity="0.8"/>
<stop offset="0.09676" stop-color="#6B57FF" stop-opacity="0.4"/>
<stop offset="0.70457" stop-color="#FF318C" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint8_linear_1725:872" x1="51.8655" y1="51.3952" x2="49.8093" y2="47.8337" gradientUnits="userSpaceOnUse">
<stop offset="0.00572" stop-color="#FF318C"/>
<stop offset="0.46934" stop-color="#6B57FF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint9_linear_1725:872" x1="18.9999" y1="2.93734" x2="18.9999" y2="20.5615" gradientUnits="userSpaceOnUse">
<stop offset="0.23873" stop-color="#FF5592" stop-opacity="0.65"/>
<stop offset="0.8289" stop-color="#FF57E4" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" width="512" height="512" viewBox="0 0 512 512"><style>.st0{fill:#f8bb16}.st1{fill:#ea4334}.st2{fill:#0074bc}.st3{fill:#e2e3e4}</style><g id="Layer_1_1_"><path id="path22" class="st0" d="M322.4 140.9l-44.1-76.8H162.6c-10.3 0-19.9 5.5-24.9 14.5L44.1 241.7c-5.1 8.9-5.1 19.7 0 28.7l35.6 62.1 44-76.4 66.2-115.2h132.5z"/><path id="path26" class="st1" d="M468.2 241.7L374.6 78.6c-5.2-8.9-14.7-14.5-24.9-14.5h-71.4l44.1 76.8 66.2 115.2-66.2 115.2h88l57.8-100.9c5-8.9 5-19.8 0-28.7"/><path id="path30" class="st2" d="M410.3 371.2H189.7L123.5 256l-44 76.4 58 101c5.2 8.9 14.7 14.5 24.9 14.5h187.1c10.3 0 19.9-5.5 24.9-14.5l35.9-62.2z"/><path class="st3" d="M322.2 371.2H189.6L123.5 256l66.2-115.2h132.5L388.6 256l-66.4 115.2zM256 198.5c-31.9 0-57.6 25.7-57.6 57.6s25.7 57.6 57.6 57.6 57.6-25.7 57.6-57.6-25.8-57.6-57.6-57.6z"/></g></svg>

After

Width:  |  Height:  |  Size: 882 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="97" height="97">
<path fill="#F05133" d="M92.71 44.408 52.591 4.291c-2.31-2.311-6.057-2.311-8.369 0l-8.33 8.332L46.459 23.19c2.456-.83 5.272-.273 7.229 1.685 1.969 1.97 2.521 4.81 1.67 7.275l10.186 10.185c2.465-.85 5.307-.3 7.275 1.671 2.75 2.75 2.75 7.206 0 9.958-2.752 2.751-7.208 2.751-9.961 0-2.068-2.07-2.58-5.11-1.531-7.658l-9.5-9.499v24.997c.67.332 1.303.774 1.861 1.332 2.75 2.75 2.75 7.206 0 9.959-2.75 2.749-7.209 2.749-9.957 0-2.75-2.754-2.75-7.21 0-9.959.68-.679 1.467-1.193 2.307-1.537v-25.23c-.84-.344-1.625-.853-2.307-1.537-2.083-2.082-2.584-5.14-1.516-7.698L31.798 16.715 4.288 44.222c-2.311 2.313-2.311 6.06 0 8.371l40.121 40.118c2.31 2.311 6.056 2.311 8.369 0L92.71 52.779c2.311-2.311 2.311-6.06 0-8.371z"/>
</svg>

After

Width:  |  Height:  |  Size: 802 B

+1
View File
@@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 960 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="1648" height="1648" viewBox="0 0 1648 1648" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1248 0H400C179.086 0 0 179.086 0 400V1248C0 1468.91 179.086 1648 400 1648H1248C1468.91 1648 1648 1468.91 1648 1248V400C1648 179.086 1468.91 0 1248 0Z" fill="white"/>
<path d="M1293.43 1351.06C1349.36 1338.92 1408.68 1305.87 1408.68 1305.87L1304.86 1220.35C1253.53 1178.1 1209.85 1127.35 1175.69 1070.32C1128.5 991.535 1063.29 925.049 985.455 876.335L947.439 854.198C934.413 845.144 925.332 831.032 924.039 815.111C923.211 804.845 925.671 795.668 931.41 787.592C951.204 759.692 1053.52 638.291 1072.27 622.778C1096.41 602.819 1123.32 586.217 1148.28 567.209C1151.83 564.503 1155.39 561.812 1158.9 559.067C1159.02 558.944 1159.2 558.848 1159.31 558.74C1167.32 552.416 1174.88 545.699 1180.89 537.734C1202.59 512.606 1207.86 490.391 1209.15 480.56C1206.22 471.098 1197.46 449.942 1173.05 425.537C1188.35 426.476 1206.87 438.575 1223.66 452.81C1234.93 434.795 1246.73 415.76 1258.52 396.686C1266.39 383.945 1254.71 374.414 1254.39 374.117L1254.32 374.102C1254.32 374.102 1254.32 374.048 1254.31 374.036C1254.01 373.709 1244.48 362.03 1231.76 369.902C1204.58 386.72 1177.42 403.553 1153.26 418.847C1153.26 418.847 1124.62 418.25 1090.72 447.536C1082.74 453.56 1076.02 461.117 1069.71 469.112C1069.59 469.235 1069.48 469.397 1069.38 469.52C1066.62 473.015 1063.93 476.576 1061.24 480.14C1042.22 505.115 1025.63 532.01 1005.67 556.157C990.171 574.919 868.758 677.216 840.858 697.013C832.782 702.752 823.617 705.224 813.339 704.381C797.433 703.103 783.306 694.007 774.249 680.984L752.115 642.968C703.401 565.103 636.915 499.922 558.126 452.729C501.102 418.577 450.36 374.876 408.105 323.564L322.557 219.743C322.557 219.743 289.491 279.05 277.362 334.985C294.234 355.502 338.247 406.421 389.478 445.307C334.398 419.405 293.679 399.377 261.564 382.628C256.614 419.255 258.546 474.647 263.643 517.544C298.41 532.757 357.606 556.196 417.867 568.652C369.666 579.923 316.791 581.975 275.961 581.174C283.155 607.727 293.16 634.811 306.621 662.057C312.345 674.66 318.612 686.966 325.383 699.011C346.989 704.954 431.817 717.311 476.955 707.168C432.048 723.2 356.655 750.134 356.655 750.134C414.561 822.179 478.56 880.793 478.56 880.793C575.907 828.449 598.098 821.297 671.109 773.546C552.876 869.753 522.189 909.032 489 949.4L465.873 981.854C453.855 998.714 443.427 1016.62 434.712 1035.4C405.549 1098.14 364.269 1231.85 364.269 1231.85C356.901 1255.15 373.977 1272.23 396.6 1264.18C396.6 1264.18 530.28 1222.9 593.052 1193.74C611.817 1185.01 629.751 1174.58 646.596 1162.57L679.05 1139.45C689.94 1130.49 700.764 1121.71 712.647 1111.35C712.647 1111.35 794.346 1208.14 878.328 1271.81C878.328 1271.81 905.265 1196.42 921.294 1151.51C911.136 1196.66 923.496 1281.49 929.451 1303.08C941.469 1309.85 953.802 1316.12 966.405 1321.84C993.666 1335.31 1020.73 1345.31 1047.29 1352.5C1046.5 1311.66 1048.54 1258.8 1059.81 1210.6C1072.27 1270.86 1095.69 1330.07 1110.92 1364.82C1153.81 1369.92 1209.21 1371.85 1245.84 1366.9C1229.08 1334.79 1209.06 1294.04 1183.16 1238.99C1222.04 1290.22 1272.96 1334.23 1293.48 1351.1L1293.43 1351.06Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+3
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

+90
View File
@@ -0,0 +1,90 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:figma="http://www.figma.com/figma/ns" width="44" height="51" viewBox="0 0 44 51" version="2.0">
<title>Group.svg</title>
<desc>Created using Figma 0.90</desc>
<g id="Canvas" transform="translate(-1640 -2453)" figma:type="canvas">
<g id="Group" style="mix-blend-mode:normal;" figma:type="group">
<g id="Group" style="mix-blend-mode:normal;" figma:type="group">
<g id="Group" style="mix-blend-mode:normal;" figma:type="group">
<g id="g" style="mix-blend-mode:normal;" figma:type="group">
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path9 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path0_fill" transform="translate(1640.54 2474.36)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path10 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path1_fill" transform="translate(1645.68 2474.37)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path11 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path2_fill" transform="translate(1653.39 2474.26)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path12 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path3_fill" transform="translate(1660.43 2474.39)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path13 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path4_fill" transform="translate(1667.55 2472.54)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path14 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path5_fill" transform="translate(1672.47 2474.29)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path15 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path6_fill" transform="translate(1679.98 2474.24)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
</g>
</g>
<g id="g" style="mix-blend-mode:normal;" figma:type="group">
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path16 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path7_fill" transform="translate(1673.48 2453.69)" fill="#767677" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path17 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path8_fill" transform="translate(1643.21 2484.27)" fill="#F37726" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path18 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path9_fill" transform="translate(1643.21 2457.88)" fill="#F37726" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path19 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path10_fill" transform="translate(1643.28 2496.09)" fill="#9E9E9E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path20 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path11_fill" transform="translate(1641.87 2458.43)" fill="#616262" style="mix-blend-mode:normal;"/>
</g>
</g>
</g>
</g>
</g>
</g>
<defs>
<path id="path0_fill" d="M 1.74498 5.47533C 1.74498 7.03335 1.62034 7.54082 1.29983 7.91474C 0.943119 8.23595 0.480024 8.41358 0 8.41331L 0.124642 9.3036C 0.86884 9.31366 1.59095 9.05078 2.15452 8.56466C 2.45775 8.19487 2.6834 7.76781 2.818 7.30893C 2.95261 6.85005 2.99341 6.36876 2.93798 5.89377L 2.93798 0L 1.74498 0L 1.74498 5.43972L 1.74498 5.47533Z"/>
<path id="path1_fill" d="M 5.50204 4.76309C 5.50204 5.43081 5.50204 6.02731 5.55545 6.54368L 4.496 6.54368L 4.42478 5.48423C 4.20318 5.85909 3.88627 6.16858 3.50628 6.38125C 3.12628 6.59392 2.69675 6.70219 2.26135 6.69503C 1.22861 6.69503 0 6.13415 0 3.84608L 0 0.0445149L 1.193 0.0445149L 1.193 3.6057C 1.193 4.84322 1.57583 5.67119 2.65309 5.67119C 2.87472 5.67358 3.09459 5.63168 3.29982 5.54796C 3.50505 5.46424 3.69149 5.34039 3.84822 5.18366C 4.00494 5.02694 4.1288 4.84049 4.21252 4.63527C 4.29623 4.43004 4.33813 4.21016 4.33575 3.98853L 4.33575 0L 5.52874 0L 5.52874 4.72748L 5.50204 4.76309Z"/>
<path id="path2_fill" d="M 0.0534178 2.27264C 0.0534178 1.44466 0.0534178 0.768036 0 0.153731L 1.06836 0.153731L 1.12177 1.2666C 1.3598 0.864535 1.70247 0.534594 2.11325 0.311954C 2.52404 0.0893145 2.98754 -0.0176786 3.45435 0.00238095C 5.03908 0.00238095 6.23208 1.32892 6.23208 3.30538C 6.23208 5.63796 4.7987 6.79535 3.24958 6.79535C 2.85309 6.81304 2.45874 6.7281 2.10469 6.54874C 1.75064 6.36937 1.44888 6.10166 1.22861 5.77151L 1.22861 5.77151L 1.22861 9.33269L 0.0534178 9.33269L 0.0534178 2.29935L 0.0534178 2.27264ZM 1.22861 4.00872C 1.23184 4.17026 1.24972 4.33117 1.28203 4.48948C 1.38304 4.88479 1.61299 5.23513 1.93548 5.48506C 2.25798 5.735 2.65461 5.87026 3.06262 5.86944C 4.31794 5.86944 5.05689 4.8456 5.05689 3.3588C 5.05689 2.05897 4.36246 0.946096 3.10714 0.946096C 2.61036 0.986777 2.14548 1.20726 1.79965 1.5662C 1.45382 1.92514 1.25079 2.3979 1.22861 2.89585L 1.22861 4.00872Z"/>
<path id="path3_fill" d="M 1.31764 0.0178059L 2.75102 3.85499C 2.90237 4.28233 3.06262 4.7987 3.16946 5.18153C 3.2941 4.7898 3.42764 4.29123 3.5879 3.82828L 4.88773 0.0178059L 6.14305 0.0178059L 4.36246 4.64735C 3.47216 6.87309 2.92908 8.02158 2.11 8.71601C 1.69745 9.09283 1.19448 9.35658 0.649917 9.48166L 0.356119 8.48453C 0.736886 8.35942 1.09038 8.16304 1.39777 7.90584C 1.8321 7.55188 2.17678 7.10044 2.4038 6.5882C 2.45239 6.49949 2.48551 6.40314 2.50173 6.3033C 2.49161 6.19586 2.46457 6.0907 2.42161 5.9917L 0 0L 1.29983 0L 1.31764 0.0178059Z"/>
<path id="path4_fill" d="M 2.19013 0L 2.19013 1.86962L 3.8995 1.86962L 3.8995 2.75992L 2.19013 2.75992L 2.19013 6.26769C 2.19013 7.06896 2.42161 7.53191 3.08043 7.53191C 3.31442 7.53574 3.54789 7.5088 3.77486 7.45179L 3.82828 8.34208C 3.48794 8.45999 3.12881 8.51431 2.76882 8.50234C 2.53042 8.51726 2.29161 8.48043 2.06878 8.39437C 1.84595 8.30831 1.64438 8.17506 1.47789 8.00377C 1.11525 7.51873 0.949826 6.91431 1.01494 6.31221L 1.01494 2.75102L 0 2.75102L 0 1.86072L 1.03274 1.86072L 1.03274 0.275992L 2.19013 0Z"/>
<path id="path5_fill" d="M 1.17716 3.57899C 1.153 3.88093 1.19468 4.18451 1.29933 4.46876C 1.40398 4.75301 1.5691 5.01114 1.78329 5.22532C 1.99747 5.43951 2.2556 5.60463 2.53985 5.70928C 2.8241 5.81393 3.12768 5.85561 3.42962 5.83145C 4.04033 5.84511 4.64706 5.72983 5.21021 5.49313L 5.41498 6.38343C 4.72393 6.66809 3.98085 6.80458 3.23375 6.78406C 2.79821 6.81388 2.36138 6.74914 1.95322 6.59427C 1.54505 6.43941 1.17522 6.19809 0.869071 5.88688C 0.562928 5.57566 0.327723 5.2019 0.179591 4.79125C 0.0314584 4.38059 -0.0260962 3.94276 0.0108748 3.50777C 0.0108748 1.54912 1.17716 0 3.0824 0C 5.21911 0 5.75329 1.86962 5.75329 3.06262C 5.76471 3.24644 5.76471 3.43079 5.75329 3.61461L 1.15046 3.61461L 1.17716 3.57899ZM 4.66713 2.6887C 4.70149 2.45067 4.68443 2.20805 4.61709 1.97718C 4.54976 1.74631 4.43372 1.53255 4.2768 1.35031C 4.11987 1.16808 3.92571 1.0216 3.70739 0.920744C 3.48907 0.81989 3.25166 0.767006 3.01118 0.765656C 2.52201 0.801064 2.06371 1.01788 1.72609 1.37362C 1.38847 1.72935 1.19588 2.19835 1.18607 2.6887L 4.66713 2.6887Z"/>
<path id="path6_fill" d="M 0.0534178 2.19228C 0.0534178 1.42663 0.0534178 0.767806 0 0.162404L 1.06836 0.162404L 1.06836 1.43553L 1.12177 1.43553C 1.23391 1.04259 1.4656 0.694314 1.78468 0.439049C 2.10376 0.183783 2.4944 0.034196 2.90237 0.0110538C 3.01466 -0.00368459 3.12839 -0.00368459 3.24068 0.0110538L 3.24068 1.12393C 3.10462 1.10817 2.9672 1.10817 2.83114 1.12393C 2.427 1.13958 2.04237 1.30182 1.7491 1.58035C 1.45583 1.85887 1.27398 2.23462 1.23751 2.63743C 1.20422 2.8196 1.18635 3.00425 1.1841 3.18941L 1.1841 6.65267L 0.00890297 6.65267L 0.00890297 2.20118L 0.0534178 2.19228Z"/>
<path id="path7_fill" d="M 6.03059 2.83565C 6.06715 3.43376 5.92485 4.02921 5.6218 4.54615C 5.31875 5.0631 4.86869 5.47813 4.32893 5.73839C 3.78917 5.99864 3.18416 6.09233 2.59097 6.00753C 1.99778 5.92272 1.44326 5.66326 0.998048 5.26219C 0.552837 4.86113 0.23709 4.33661 0.0910307 3.75546C -0.0550287 3.17431 -0.0247891 2.56283 0.177897 1.99893C 0.380583 1.43503 0.746541 0.944221 1.22915 0.589037C 1.71176 0.233853 2.28918 0.0303686 2.88784 0.00450543C 3.28035 -0.0170932 3.67326 0.0391144 4.04396 0.169896C 4.41467 0.300677 4.75587 0.503453 5.04794 0.766561C 5.34 1.02967 5.57718 1.34792 5.74582 1.70301C 5.91446 2.0581 6.01124 2.44303 6.03059 2.83565L 6.03059 2.83565Z"/>
<path id="path8_fill" d="M 18.6962 7.12238C 10.6836 7.12238 3.64131 4.24672 0 0C 1.41284 3.82041 3.96215 7.1163 7.30479 9.44404C 10.6474 11.7718 14.623 13.0196 18.6962 13.0196C 22.7695 13.0196 26.745 11.7718 30.0877 9.44404C 33.4303 7.1163 35.9796 3.82041 37.3925 4.0486e-13C 33.7601 4.24672 26.7445 7.12238 18.6962 7.12238Z"/>
<path id="path9_fill" d="M 18.6962 5.89725C 26.7089 5.89725 33.7512 8.77291 37.3925 13.0196C 35.9796 9.19922 33.4303 5.90333 30.0877 3.57559C 26.745 1.24785 22.7695 4.0486e-13 18.6962 0C 14.623 4.0486e-13 10.6474 1.24785 7.30479 3.57559C 3.96215 5.90333 1.41284 9.19922 0 13.0196C 3.64131 8.76401 10.648 5.89725 18.6962 5.89725Z"/>
<path id="path10_fill" d="M 7.59576 3.56656C 7.64276 4.31992 7.46442 5.07022 7.08347 5.72186C 6.70251 6.3735 6.13619 6.89698 5.45666 7.22561C 4.77713 7.55424 4.01515 7.67314 3.26781 7.56716C 2.52046 7.46117 1.82158 7.13511 1.26021 6.63051C 0.698839 6.12591 0.300394 5.46561 0.115637 4.73375C -0.0691191 4.00188 -0.0318219 3.23159 0.222777 2.52099C 0.477376 1.8104 0.93775 1.19169 1.54524 0.743685C 2.15274 0.295678 2.87985 0.0386595 3.63394 0.00537589C 4.12793 -0.0210471 4.62229 0.0501173 5.08878 0.214803C 5.55526 0.37949 5.98473 0.63447 6.35264 0.965179C 6.72055 1.29589 7.01971 1.69584 7.233 2.1422C 7.4463 2.58855 7.56957 3.07256 7.59576 3.56656L 7.59576 3.56656Z"/>
<path id="path11_fill" d="M 2.25061 4.37943C 1.81886 4.39135 1.39322 4.27535 1.02722 4.04602C 0.661224 3.81668 0.371206 3.48424 0.193641 3.09052C 0.0160762 2.69679 -0.0411078 2.25935 0.0292804 1.83321C 0.0996686 1.40707 0.294486 1.01125 0.589233 0.695542C 0.883981 0.37983 1.2655 0.158316 1.68581 0.0588577C 2.10611 -0.0406005 2.54644 -0.0135622 2.95143 0.136572C 3.35641 0.286707 3.70796 0.553234 3.96186 0.902636C 4.21577 1.25204 4.3607 1.66872 4.37842 2.10027C 4.39529 2.6838 4.18131 3.25044 3.78293 3.67715C 3.38455 4.10387 2.83392 4.35623 2.25061 4.37943Z"/>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 328.5 120 135" enable-background="new 0 328.5 612 135" xml:space="preserve" style="&#10;">
<polygon fill="#185F85" points="29,434.2 29,439.3 29,446.3 47.4,457.1 51.9,459.7 58.3,463.2 62.7,455.5 64.3,447.3 58.3,441.9 29,425 "/>
<path fill="#0D84BE" d="M116.5,429.8V396c0-11.1,0-22.3,0-33.4l-12.1-0.3l-2.9,3.8l-3.8,7V419L58,442.2v8.9v12.4 C77.4,452,97.1,440.9,116.5,429.8z"/>
<polygon fill="#FFFFFF" points="18.5,385.5 18.5,351.7 0,362.6 0,429.8 18.5,440.3 18.5,406.8 58.3,429.8 58.3,429.8 58.3,408.4 36.9,396 58.3,383.9 58.3,362.6 "/>
<path fill="#1A9BD7" d="M58.3,328.8c19.1,11.1,38.8,22.6,58.3,33.8l-10.8,6l-8,4.5l-39.5-22.9L29,366.7c0-7,0-14.3,0-21.3 L58.3,328.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 819 B

+1
View File
@@ -0,0 +1 @@
<svg width="2270" height="2500" viewBox="0 0 256 282" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet"><g fill="#8CC84B"><path d="M116.504 3.58c6.962-3.985 16.03-4.003 22.986 0 34.995 19.774 70.001 39.517 104.99 59.303 6.581 3.707 10.983 11.031 10.916 18.614v118.968c.049 7.897-4.788 15.396-11.731 19.019-34.88 19.665-69.742 39.354-104.616 59.019-7.106 4.063-16.356 3.75-23.24-.646-10.457-6.062-20.932-12.094-31.39-18.15-2.137-1.274-4.546-2.288-6.055-4.36 1.334-1.798 3.719-2.022 5.657-2.807 4.365-1.388 8.374-3.616 12.384-5.778 1.014-.694 2.252-.428 3.224.193 8.942 5.127 17.805 10.403 26.777 15.481 1.914 1.105 3.852-.362 5.488-1.274 34.228-19.345 68.498-38.617 102.72-57.968 1.268-.61 1.969-1.956 1.866-3.345.024-39.245.006-78.497.012-117.742.145-1.576-.767-3.025-2.192-3.67-34.759-19.575-69.5-39.18-104.253-58.76a3.621 3.621 0 0 0-4.094-.006C91.2 39.257 56.465 58.88 21.712 78.454c-1.42.646-2.373 2.071-2.204 3.653.006 39.245 0 78.497 0 117.748a3.329 3.329 0 0 0 1.89 3.303c9.274 5.259 18.56 10.481 27.84 15.722 5.228 2.814 11.647 4.486 17.407 2.33 5.083-1.823 8.646-7.01 8.549-12.407.048-39.016-.024-78.038.036-117.048-.127-1.732 1.516-3.163 3.2-3 4.456-.03 8.918-.06 13.374.012 1.86-.042 3.14 1.823 2.91 3.568-.018 39.263.048 78.527-.03 117.79.012 10.464-4.287 21.85-13.966 26.97-11.924 6.177-26.662 4.867-38.442-1.056-10.198-5.09-19.93-11.097-29.947-16.55C5.368 215.886.555 208.357.604 200.466V81.497c-.073-7.74 4.504-15.197 11.29-18.85C46.768 42.966 81.636 23.27 116.504 3.58z"/><path d="M146.928 85.99c15.21-.979 31.493-.58 45.18 6.913 10.597 5.742 16.472 17.793 16.659 29.566-.296 1.588-1.956 2.464-3.472 2.355-4.413-.006-8.827.06-13.24-.03-1.872.072-2.96-1.654-3.195-3.309-1.268-5.633-4.34-11.212-9.642-13.929-8.139-4.075-17.576-3.87-26.451-3.785-6.479.344-13.446.905-18.935 4.715-4.214 2.886-5.494 8.712-3.99 13.404 1.418 3.369 5.307 4.456 8.489 5.458 18.33 4.794 37.754 4.317 55.734 10.626 7.444 2.572 14.726 7.572 17.274 15.366 3.333 10.446 1.872 22.932-5.56 31.318-6.027 6.901-14.805 10.657-23.56 12.697-11.647 2.597-23.734 2.663-35.562 1.51-11.122-1.268-22.696-4.19-31.282-11.768-7.342-6.375-10.928-16.308-10.572-25.895.085-1.619 1.697-2.748 3.248-2.615 4.444-.036 8.888-.048 13.332.006 1.775-.127 3.091 1.407 3.182 3.08.82 5.367 2.837 11 7.517 14.182 9.032 5.827 20.365 5.428 30.707 5.591 8.568-.38 18.186-.495 25.178-6.158 3.689-3.23 4.782-8.634 3.785-13.283-1.08-3.925-5.186-5.754-8.712-6.95-18.095-5.724-37.736-3.647-55.656-10.12-7.275-2.571-14.31-7.432-17.105-14.906-3.9-10.578-2.113-23.662 6.098-31.765 8.006-8.06 19.563-11.164 30.551-12.275z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 47.5 47.5" style="enable-background:new 0 0 47.5 47.5;" id="svg2" xml:space="preserve"><defs id="defs6"><clipPath id="clipPath18"><path d="M 0,38 38,38 38,0 0,0 0,38 z" id="path20"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,47.5)" id="g12"><g id="g14"><g clip-path="url(#clipPath18)" id="g16"><g transform="translate(21.8486,9.4102)" id="g22"><path d="m 0,0 c -0.395,-1.346 -2.46,-1.924 -4.613,-1.291 -2.153,0.632 -3.578,2.234 -3.183,3.581 0.395,1.346 2.461,1.924 4.613,1.29 C -1.029,2.949 0.396,1.347 0,0 m -2.849,24.447 c -9.941,0 -18,-6.908 -18,-15.428 0,-1.067 0.127,-2.108 0.367,-3.113 1.779,-3.06 3.01,-1.128 8.633,1.684 5.727,2.864 0,-4 -2,-8 -0.615,-1.231 -0.281,-2.272 0.56,-3.124 2.946,-1.804 6.544,-2.876 10.44,-2.876 9.942,0 18,6.907 18,15.429 0,8.52 -8.058,15.428 -18,15.428" id="path24" style="fill:#d99e82;fill-opacity:1;fill-rule:nonzero;stroke:none"/></g><g transform="translate(14,26)" id="g26"><path d="m 0,0 c 0,-1.657 -1.343,-3 -3,-3 -1.657,0 -3,1.343 -3,3 0,1.657 1.343,3 3,3 1.657,0 3,-1.343 3,-3" id="path28" style="fill:#5c913b;fill-opacity:1;fill-rule:nonzero;stroke:none"/></g><g transform="translate(24,28)" id="g30"><path d="m 0,0 c 0,-1.657 -1.344,-3 -3,-3 -1.657,0 -3,1.343 -3,3 0,1.657 1.343,3 3,3 1.656,0 3,-1.343 3,-3" id="path32" style="fill:#226699;fill-opacity:1;fill-rule:nonzero;stroke:none"/></g><g transform="translate(33,22)" id="g34"><path d="m 0,0 c 0,-1.657 -1.344,-3 -3,-3 -1.656,0 -3,1.343 -3,3 0,1.657 1.344,3 3,3 1.656,0 3,-1.343 3,-3" id="path36" style="fill:#dd2e44;fill-opacity:1;fill-rule:nonzero;stroke:none"/></g><g transform="translate(32,13)" id="g38"><path d="m 0,0 c 0,-1.656 -1.344,-3 -3,-3 -1.656,0 -3,1.344 -3,3 0,1.656 1.344,3 3,3 1.656,0 3,-1.344 3,-3" id="path40" style="fill:#ffcc4d;fill-opacity:1;fill-rule:nonzero;stroke:none"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg width="127" height="127" xmlns="http://www.w3.org/2000/svg">
<path d="M27.2 80c0 7.3-5.9 13.2-13.2 13.2C6.7 93.2.8 87.3.8 80c0-7.3 5.9-13.2 13.2-13.2h13.2V80zm6.6 0c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2v33c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V80z" fill="#E01E5A"/>
<path d="M47 27c-7.3 0-13.2-5.9-13.2-13.2C33.8 6.5 39.7.6 47 .6c7.3 0 13.2 5.9 13.2 13.2V27H47zm0 6.7c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H13.9C6.6 60.1.7 54.2.7 46.9c0-7.3 5.9-13.2 13.2-13.2H47z" fill="#36C5F0"/>
<path d="M99.9 46.9c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H99.9V46.9zm-6.6 0c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V13.8C66.9 6.5 72.8.6 80.1.6c7.3 0 13.2 5.9 13.2 13.2v33.1z" fill="#2EB67D"/>
<path d="M80.1 99.8c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V99.8h13.2zm0-6.6c-7.3 0-13.2-5.9-13.2-13.2 0-7.3 5.9-13.2 13.2-13.2h33.1c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H80.1z" fill="#ECB22E"/>
</svg>

After

Width:  |  Height:  |  Size: 1019 B

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#FFD814" d="M0 0l7.971 15.516L16 0H0zm6.732 6.16h-1.27V4.89h1.27v1.27zm0-1.906h-1.27V2.985h1.27v1.269zm1.904 3.81h-1.27v-1.27h1.27v1.27zm0-1.905h-1.27V4.89h1.27v1.27zm0-1.905h-1.27V2.985h1.27v1.269zm1.894 1.905H9.26V4.89h1.27v1.27zM9.26 4.254V2.985h1.27v1.269H9.26z"/></svg>

After

Width:  |  Height:  |  Size: 506 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M897.246 286.869H889.819C850.735 286.808 819.017 318.46 819.017 357.539V515.589C819.017 547.15 792.93 572.716 761.882 572.716C743.436 572.716 725.02 563.433 714.093 547.85L552.673 317.304C539.28 298.16 517.486 286.747 493.895 286.747C457.094 286.747 423.976 318.034 423.976 356.657V515.619C423.976 547.181 398.103 572.746 366.842 572.746C348.335 572.746 329.949 563.463 319.021 547.881L138.395 289.882C134.316 284.038 125.154 286.93 125.154 294.052V431.892C125.154 438.862 127.285 445.619 131.272 451.34L309.037 705.2C319.539 720.204 335.033 731.344 352.9 735.392C397.616 745.557 438.77 711.135 438.77 667.278V508.406C438.77 476.845 464.339 451.279 495.904 451.279H495.995C515.02 451.279 532.857 460.562 543.785 476.145L705.235 706.661C718.659 725.835 739.327 737.218 763.983 737.218C801.606 737.218 833.841 705.9 833.841 667.308V508.376C833.841 476.815 859.41 451.249 890.975 451.249H897.276C901.233 451.249 904.43 448.053 904.43 444.097V294.021C904.43 290.065 901.233 286.869 897.276 286.869H897.246Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+6
View File
@@ -0,0 +1,6 @@
{
"files.exclude": {
"**/terraform.tfstate": true,
"**/.terraform": true
}
}
+3
View File
@@ -0,0 +1,3 @@
# Code of Conduct
[Please see our code of conduct on the official Coder website](https://coder.com/docs/contributing/CODE_OF_CONDUCT)
+294
View File
@@ -0,0 +1,294 @@
# Contributing
## Getting started
This repo uses two main runtimes to verify the correctness of a module/template before it is published:
- [Bun](https://bun.sh/) Used to run tests for each module/template to validate overall functionality and correctness of Terraform output
- [Go](https://go.dev/) Used to validate all README files in the directory. The README content is used to populate [the Registry website](https://registry.coder.com).
### Installing Bun
To install Bun, you can run this command on Linux/MacOS:
```shell
curl -fsSL https://bun.sh/install | bash
```
Or this command on Windows:
```shell
powershell -c "irm bun.sh/install.ps1 | iex"
```
Follow the instructions to ensure that Bun is available globally. Once Bun is installed, install all necessary dependencies from the root of the repo:
Via NPM:
```shell
npm i
```
Via PNPM:
```shell
pnpm i
```
This repo does not support Yarn.
### Installing Go (optional)
This step can be skipped if you are not working on any of the README validation logic. The validation will still run as part of CI.
[Navigate to the official Go Installation page](https://go.dev/doc/install), and install the correct version for your operating system.
Once Go has been installed, verify the installation via:
```shell
go version
```
## Namespaces
All Coder resources are scoped to namespaces placed at the top level of the `/registry` directory. Any modules or templates must be placed inside a namespace to be accepted as a contribution. For example, all modules created by CoderEmployeeBob would be placed under `/registry/coderemployeebob/modules`, with a subdirectory for each individual module the user has published.
If a namespace is already taken, you will need to create a different, unique namespace, but will still be able to choose any display name. (The display name is shown in the Registry website. More info below.)
### Namespace (contributor profile) README files
More information about contributor profile README files can be found below.
### Images
Any images needed for either the main namespace directory or a module/template can be placed in a relative `/images` directory at the top of the namespace directory. (e.g., CoderEmployeeBob can have a `/registry/coderemployeebob/images` directory, that can be referenced by the main README file, as well as a README file in `/registry/coderemployeebob/modules/custom_module/README.md`.) This is to minimize the risk of file name conflicts between different users as they add images to help illustrate parts of their README files.
## Coder modules
### Adding a new module
> [!WARNING]
> These instructions cannot be followed just yet; the script referenced will be made available shortly. Contributors looking to add modules early will need to create all directories manually.
Once Bun (and possibly Go) have been installed, clone the Coder Registry repository. From there, you can run this script to make it easier to start contributing a new module or template:
```shell
./new.sh USER_NAMESPACE/NAME_OF_NEW_MODULE
```
You can also create a module file manually by creating the necessary files and directories.
### The composition of a Coder module
Each Coder Module must contain the following files:
- A `main.tf` file that defines the main Terraform-based functionality
- A `main.test.ts` file that is used to validate that the module works as expected
- A `README.md` file containing required information (listed below)
You are free to include any additional files in the module, as needed by the module. For example, the [Windows RDP module](https://github.com/coder/registry/tree/main/registry/coder/modules/windows-rdp) contains additional files for injecting specific functionality into a Coder Workspace.
> [!NOTE]
> Some legacy modules do not have test files defined just yet. This will be addressed soon.
### The `main.tf` file
This file defines all core Terraform functionality, to be mixed into your Coder workspaces. More information about [Coder's use of Terraform can be found here](https://coder.com/docs/admin/templates/extending-templates/modules), and [general information about the Terraform language can be found in the official documentation](https://developer.hashicorp.com/terraform/docs).
### The structure of a module README
Validation criteria for module README files is listed below.
### Testing a Module
> [!IMPORTANT]
> It is the responsibility of the module author to implement tests for every new module they wish to contribute. It is expected the author has tested the module locally before opening a PR. Feel free to reference existing test files to get an idea for how to set them up.
All general-purpose test helpers for validating Terraform can be found in the top-level `/testing` directory. The helpers run `terraform apply` on modules that use variables, testing the script output against containers.
When writing a test file, you can import the test utilities via the `~test` import alias:
```ts
// This works regardless of how deeply-nested your test file is in the file
// structure
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
```
> [!NOTE]
> The testing suite must be able to run docker containers with the `--network=host` flag. This typically requires running the tests on Linux as this flag does not apply to Docker Desktop for MacOS or Windows. MacOS users can work around this by using something like [colima](https://github.com/abiosoft/colima) or [Orbstack](https://orbstack.dev/) instead of Docker Desktop.
#### Running tests
You can run all tests by running this command from the root of the Registry directory:
```shell
bun test
```
Note that running _all_ tests can take some time, so you likely don't want to be running this command as part of your core development loop.
To run specific tests, you can use the `-t` flag, which accepts a filepath regex:
```shell
bun test -t '<regex_pattern>'
```
To ensure that the module runs predictably in local development, you can update the Terraform source as follows:
```tf
module "example" {
# You may need to remove the 'version' field, it is incompatible with some sources.
source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>"
}
```
## Updating README files
This repo uses Go to validate each README file. If you are working with the README files at all (i.e., creating them, modifying them), it is strongly recommended that you install Go (installation instructions mentioned above), so that the files can be validated locally.
### Validating all README files
To validate all README files throughout the entire repo, you can run the following:
```shell
go build ./cmd/readmevalidation && ./readmevalidation
```
The resulting binary is already part of the `.gitignore` file, but you can remove it with:
```shell
rm ./readmevalidation
```
### README validation criteria
The following criteria exists for two reasons:
1. Content accessibility
2. Having content be designed in a way that's easy for the Registry site build step to use
#### General README requirements
- There must be a frontmatter section.
- There must be exactly one h1 header, and it must be at the very top, directly below the frontmatter.
- The README body (if it exists) must start with an h1 header. No other content (including GitHub-Flavored Markdown alerts) is allowed to be placed above it.
- When increasing the level of a header, the header's level must be incremented by one each time.
- Any `.hcl` code snippets must be labeled as `.tf` snippets instead
```txt
\`\`\`tf
Content
\`\`\`
```
#### Namespace (contributor profile) criteria
In addition to the general criteria, all README files must have the following:
- Frontmatter metadata with support for the following fields:
- `display_name` (required string) The name to use when displaying your user profile in the Coder Registry site.
- `bio` (optional string) A short description of who you are.
- `github` (optional string) Your GitHub handle.
- `avatar_url` (optional string)  A relative/absolute URL pointing to your avatar for the Registry site. It is strongly recommended that you commit avatar images to this repo and reference them via a relative URL.
- `linkedin` (optional string)  A URL pointing to your LinkedIn page.
- `support_email` (optional string) An email for users to reach you at if they need help with a published module/template.
- `status` (string union)  If defined, this must be one of `"community"`, `"partner"`, or `"official"`. `"community"` should be used for the majority of external contributions. `"partner"` is for companies who have a formal business partnership with Coder. `"official"` should be used only by Coder employees.
- The README body (the content that goes directly below the frontmatter) is allowed to be empty, but if it isn't, it must follow all the rules above.
You are free to customize the body of a contributor profile however you like, adding any number of images or information. Its content will never be rendered in the Registry website.
Additional information can be placed in the README file below the content listed above, using any number of headers.
Additional image/video assets can be placed in the same user namespace directory where that user's main content lives.
#### Module criteria
In addition to the general criteria, all README files must have the following:
- Frontmatter that describes metadata for the module:
- `display_name` (required string) This is the name displayed on the Coder Registry website
- `description` (required string) A short description of the module, which is displayed on the Registry website
- `icon` (required string) A relative/absolute URL pointing to the icon to display for the module in the Coder Registry website.
- `verified` (optional boolean) Indicates whether the module has been officially verified by Coder. Please do not set this without approval from a Coder employee.
- `tags` (required string array) A list of metadata tags to describe the module. Used in the Registry site for search and navigation functionality.
- `maintainer_github` (deprecated string)  The name of the creator of the module. This field exists for backwards compatibility with previous versions of the Registry, but going forward, the value will be inferred from the namespace directory.
- `partner_github` (deprecated string) - The name of any additional creators for a module. This field exists for backwards compatibility with previous versions of the Registry, but should not ever be used going forward.
- The following content directly under the h1 header (without another header between them):
- A description of what the module does
- A Terraform snippet for letting other users import the functionality
```tf
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.0.19"
agent_id = coder_agent.example.id
}
```
Additional information can be placed in the README file below the content listed above, using any number of headers.
Additional image/video assets can be placed in one of two places:
1. In the same user namespace directory where that user's main content lives
2. If the image is an icon, it can be placed in the top-level `.icons` directory (this is done because a lot of modules will be based off the same products)
## Releases
The release process involves the following steps:
### 1. Create and merge a new PR
- Create a PR with your module changes
- Get your PR reviewed, approved, and merged into the `main` branch
### 2. Prepare Release (Maintainer Task)
After merging to `main`, a maintainer will:
- Check out the merge commit:
```shell
git checkout MERGE_COMMIT_ID
```
- Create annotated tags for each module that was changed:
```shell
git tag -a "release/$namespace/$module/v$version" -m "Release $namespace/$module v$version"
```
- Push the tags to origin:
```shell
git push origin release/$namespace/$module/v$version
```
For example, to release version 1.0.14 of the coder/aider module:
```shell
git tag -a "release/coder/aider/v1.0.14" -m "Release coder/aider v1.0.14"
git push origin release/coder/aider/v1.0.14
```
### Version Numbers
Version numbers should follow semantic versioning:
- **Patch version** (1.2.3 → 1.2.4): Bug fixes
- **Minor version** (1.2.3 → 1.3.0): New features, adding inputs, deprecating inputs
- **Major version** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing input types)
### 3. Publishing to Coder Registry
After tags are pushed, the changes will be published to [registry.coder.com](https://registry.coder.com).
> [!NOTE]
> Some data in registry.coder.com is fetched on demand from this repository's `main` branch. This data should update almost immediately after a release, while other changes will take some time to propagate.
+50 -2
View File
@@ -1,2 +1,50 @@
# hub
Publish Coder modules and templates for other developers to use.
# Coder Registry
[Registry Site](https://registry.coder.com) • [Coder OSS](https://github.com/coder/coder) • [Coder Docs](https://www.coder.com/docs) • [Official Discord](https://discord.gg/coder)
[![Health](https://github.com/coder/registry/actions/workflows/check_registry_site_health.yaml/badge.svg)](https://github.com/coder/registry/actions/workflows/check_registry_site_health.yaml)
Coder Registry is a community-driven platform for extending your Coder workspaces. Publish reusable Terraform as Coder Modules for users all over the world.
> [!NOTE]
> The Coder Registry repo will be updated to support Coder Templates in the coming weeks. You can currently find all official templates in the official coder/coder repo, [under the `examples/templates` directory](https://github.com/coder/coder/tree/main/examples/templates).
## Overview
Coder is built on HashiCorp's open-source Terraform language to provide developers an easy, declarative way to define the infrastructure for their remote development environments. Coder-flavored versions of Terraform allow you to mix in reusable Terraform snippets to add integrations with other popular development tools, such as JetBrains, Cursor, or Visual Studio Code.
Simply add the correct import snippet, along with any data dependencies, and your workspace can start using the new functionality immediately.
![Coder Agent Bar](./images/coder-agent-bar.png)
More information [about Coder Modules can be found here](https://coder.com/docs/admin/templates/extending-templates/modules), while more information [about Coder Templates can be found here](https://coder.com/docs/admin/templates/creating-templates).
## Getting started
The easiest way to discover new modules and templates is by visiting [the official Coder Registry website](https://registry.coder.com/). The website is a full mirror of the Coder Registry repo, and it is where .tar versions of the various resources can be downloaded from, for use within your Coder deployment.
Note that while Coder has a baseline set of requirements for allowing an external PR to be published, Coder cannot vouch for the validity or functionality of a resource until that resource has been flagged with the `verified` status. [All modules under the Coder namespace](https://github.com/coder/registry/tree/main/registry/coder) are automatically verified.
### Getting started with modules
To get started with a module, navigate to that module's page in either the registry site, or the main repo:
- [The Cursor repo directory](https://github.com/coder/registry/tree/main/registry/coder/modules/cursor)
- [The Cursor module page on the main website](https://registry.coder.com/modules/cursor)
In both cases, the main README contains a Terraform snippet for integrating the module into your workspace. The snippet for Cursor looks like this:
```tf
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.0.19"
agent_id = coder_agent.example.id
}
```
Simply include that snippet inside your Coder template, defining any data dependencies referenced, and the next time you create a new workspace, the functionality will be ready for you to use.
## Contributing
We are always accepting new contributions. [Please see our contributing guide for more information.](./CONTRIBUTING.md)
+3
View File
@@ -0,0 +1,3 @@
# Security
[Please see our security policy on the official Coder website](https://coder.com/security/policy)
Executable
BIN
View File
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
[test]
preload = ["./setup.ts"]
+354
View File
@@ -0,0 +1,354 @@
package main
import (
"bufio"
"errors"
"fmt"
"log"
"net/url"
"os"
"path"
"regexp"
"slices"
"strings"
"gopkg.in/yaml.v3"
)
var supportedResourceTypes = []string{"modules", "templates"}
type coderResourceFrontmatter struct {
Description string `yaml:"description"`
IconURL string `yaml:"icon"`
DisplayName *string `yaml:"display_name"`
Verified *bool `yaml:"verified"`
Tags []string `yaml:"tags"`
}
// coderResourceReadme represents a README describing a Terraform resource used
// to help create Coder workspaces. As of 2025-04-15, this encapsulates both
// Coder Modules and Coder Templates
type coderResourceReadme struct {
resourceType string
filePath string
body string
frontmatter coderResourceFrontmatter
}
func validateCoderResourceDisplayName(displayName *string) error {
if displayName != nil && *displayName == "" {
return errors.New("if defined, display_name must not be empty string")
}
return nil
}
func validateCoderResourceDescription(description string) error {
if description == "" {
return errors.New("frontmatter description cannot be empty")
}
return nil
}
func validateCoderResourceIconURL(iconURL string) []error {
problems := []error{}
if iconURL == "" {
problems = append(problems, errors.New("icon URL cannot be empty"))
return problems
}
isAbsoluteURL := !strings.HasPrefix(iconURL, ".") && !strings.HasPrefix(iconURL, "/")
if isAbsoluteURL {
if _, err := url.ParseRequestURI(iconURL); err != nil {
problems = append(problems, errors.New("absolute icon URL is not correctly formatted"))
}
if strings.Contains(iconURL, "?") {
problems = append(problems, errors.New("icon URLs cannot contain query parameters"))
}
return problems
}
// Would normally be skittish about having relative paths like this, but it
// should be safe because we have guarantees about the structure of the
// repo, and where this logic will run
isPermittedRelativeURL := strings.HasPrefix(iconURL, "./") ||
strings.HasPrefix(iconURL, "/") ||
strings.HasPrefix(iconURL, "../../../../.icons")
if !isPermittedRelativeURL {
problems = append(problems, fmt.Errorf("relative icon URL %q must either be scoped to that module's directory, or the top-level /.icons directory (this can usually be done by starting the path with \"../../../.icons\")", iconURL))
}
return problems
}
func validateCoderResourceTags(tags []string) error {
if tags == nil {
return errors.New("provided tags array is nil")
}
if len(tags) == 0 {
return nil
}
// All of these tags are used for the module/template filter controls in the
// Registry site. Need to make sure they can all be placed in the browser
// URL without issue
invalidTags := []string{}
for _, t := range tags {
if t != url.QueryEscape(t) {
invalidTags = append(invalidTags, t)
}
}
if len(invalidTags) != 0 {
return fmt.Errorf("found invalid tags (tags that cannot be used for filter state in the Registry website): [%s]", strings.Join(invalidTags, ", "))
}
return nil
}
// Todo: This is a holdover from the validation logic used by the Coder Modules
// repo. It gives us some assurance, but realistically, we probably want to
// parse any Terraform code snippets, and make some deeper guarantees about how
// it's structured. Just validating whether it *can* be parsed as Terraform
// would be a big improvement.
var terraformVersionRe = regexp.MustCompile("^\\s*\\bversion\\s+=")
func validateCoderResourceReadmeBody(body string) []error {
trimmed := strings.TrimSpace(body)
var errs []error
errs = append(errs, validateReadmeBody(trimmed)...)
foundParagraph := false
terraformCodeBlockCount := 0
foundTerraformVersionRef := false
lineNum := 0
isInsideCodeBlock := false
isInsideTerraform := false
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
for lineScanner.Scan() {
lineNum++
nextLine := lineScanner.Text()
// Code assumes that invalid headers would've already been handled by
// the base validation function, so we don't need to check deeper if the
// first line isn't an h1
if lineNum == 1 {
if !strings.HasPrefix(nextLine, "# ") {
break
}
continue
}
if strings.HasPrefix(nextLine, "```") {
isInsideCodeBlock = !isInsideCodeBlock
isInsideTerraform = isInsideCodeBlock && strings.HasPrefix(nextLine, "```tf")
if isInsideTerraform {
terraformCodeBlockCount++
}
if strings.HasPrefix(nextLine, "```hcl") {
errs = append(errs, errors.New("all .hcl language references must be converted to .tf"))
}
continue
}
if isInsideCodeBlock {
if isInsideTerraform {
foundTerraformVersionRef = foundTerraformVersionRef || terraformVersionRe.MatchString(nextLine)
}
continue
}
// Code assumes that we can treat this case as the end of the "h1
// section" and don't need to process any further lines
if lineNum > 1 && strings.HasPrefix(nextLine, "#") {
break
}
// Code assumes that if we've reached this point, the only other options
// are: (1) empty spaces, (2) paragraphs, (3) HTML, and (4) asset
// references made via [] syntax
trimmedLine := strings.TrimSpace(nextLine)
isParagraph := trimmedLine != "" && !strings.HasPrefix(trimmedLine, "![") && !strings.HasPrefix(trimmedLine, "<")
foundParagraph = foundParagraph || isParagraph
}
if terraformCodeBlockCount == 0 {
errs = append(errs, errors.New("did not find Terraform code block within h1 section"))
} else {
if terraformCodeBlockCount > 1 {
errs = append(errs, errors.New("cannot have more than one Terraform code block in h1 section"))
}
if !foundTerraformVersionRef {
errs = append(errs, errors.New("did not find Terraform code block that specifies 'version' field"))
}
}
if !foundParagraph {
errs = append(errs, errors.New("did not find paragraph within h1 section"))
}
if isInsideCodeBlock {
errs = append(errs, errors.New("code blocks inside h1 section do not all terminate before end of file"))
}
return errs
}
func validateCoderResourceReadme(rm coderResourceReadme) []error {
var errs []error
for _, err := range validateCoderResourceReadmeBody(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if err := validateCoderResourceDisplayName(rm.frontmatter.DisplayName); err != nil {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if err := validateCoderResourceDescription(rm.frontmatter.Description); err != nil {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if err := validateCoderResourceTags(rm.frontmatter.Tags); err != nil {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateCoderResourceIconURL(rm.frontmatter.IconURL) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
return errs
}
func parseCoderResourceReadme(resourceType string, rm readme) (coderResourceReadme, error) {
fm, body, err := separateFrontmatter(rm.rawText)
if err != nil {
return coderResourceReadme{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
}
yml := coderResourceFrontmatter{}
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
return coderResourceReadme{}, fmt.Errorf("%q: failed to parse: %v", rm.filePath, err)
}
return coderResourceReadme{
resourceType: resourceType,
filePath: rm.filePath,
body: body,
frontmatter: yml,
}, nil
}
func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[string]coderResourceReadme, error) {
resources := map[string]coderResourceReadme{}
var yamlParsingErrs []error
for _, rm := range rms {
p, err := parseCoderResourceReadme(resourceType, rm)
if err != nil {
yamlParsingErrs = append(yamlParsingErrs, err)
continue
}
resources[p.filePath] = p
}
if len(yamlParsingErrs) != 0 {
return nil, validationPhaseError{
phase: validationPhaseReadmeParsing,
errors: yamlParsingErrs,
}
}
yamlValidationErrors := []error{}
for _, readme := range resources {
errors := validateCoderResourceReadme(readme)
if len(errors) > 0 {
yamlValidationErrors = append(yamlValidationErrors, errors...)
}
}
if len(yamlValidationErrors) != 0 {
return nil, validationPhaseError{
phase: validationPhaseReadmeParsing,
errors: yamlValidationErrors,
}
}
return resources, nil
}
// Todo: Need to beef up this function by grabbing each image/video URL from
// the body's AST
func validateCoderResourceRelativeUrls(resources map[string]coderResourceReadme) error {
return nil
}
func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
registryFiles, err := os.ReadDir(rootRegistryPath)
if err != nil {
return nil, err
}
var allReadmeFiles []readme
var errs []error
for _, rf := range registryFiles {
if !rf.IsDir() {
continue
}
resourceRootPath := path.Join(rootRegistryPath, rf.Name(), resourceType)
resourceDirs, err := os.ReadDir(resourceRootPath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
errs = append(errs, err)
}
continue
}
for _, rd := range resourceDirs {
if !rd.IsDir() || rd.Name() == ".coder" {
continue
}
resourceReadmePath := path.Join(resourceRootPath, rd.Name(), "README.md")
rm, err := os.ReadFile(resourceReadmePath)
if err != nil {
errs = append(errs, err)
continue
}
allReadmeFiles = append(allReadmeFiles, readme{
filePath: resourceReadmePath,
rawText: string(rm),
})
}
}
if len(errs) != 0 {
return nil, validationPhaseError{
phase: validationPhaseFileLoad,
errors: errs,
}
}
return allReadmeFiles, nil
}
func validateAllCoderResourceFilesOfType(resourceType string) error {
if !slices.Contains(supportedResourceTypes, resourceType) {
return fmt.Errorf("resource type %q is not part of supported list [%s]", resourceType, strings.Join(supportedResourceTypes, ", "))
}
allReadmeFiles, err := aggregateCoderResourceReadmeFiles(resourceType)
if err != nil {
return err
}
log.Printf("Processing %d README files\n", len(allReadmeFiles))
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles)
if err != nil {
return err
}
log.Printf("Processed %d README files as valid Coder resources with type %q", len(resources), resourceType)
err = validateCoderResourceRelativeUrls(resources)
if err != nil {
return err
}
log.Printf("All relative URLs for %s READMEs are valid\n", resourceType)
return nil
}
@@ -0,0 +1,22 @@
package main
import (
_ "embed"
"testing"
)
//go:embed testSamples/sampleReadmeBody.md
var testBody string
func TestValidateCoderResourceReadmeBody(t *testing.T) {
t.Parallel()
t.Run("Parses a valid README body with zero issues", func(t *testing.T) {
t.Parallel()
errs := validateCoderResourceReadmeBody(testBody)
for _, e := range errs {
t.Error(e)
}
})
}
+336
View File
@@ -0,0 +1,336 @@
package main
import (
"errors"
"fmt"
"log"
"net/url"
"os"
"path"
"slices"
"strings"
"gopkg.in/yaml.v3"
)
var validContributorStatuses = []string{"official", "partner", "community"}
type contributorProfileFrontmatter struct {
DisplayName string `yaml:"display_name"`
Bio string `yaml:"bio"`
ContributorStatus string `yaml:"status"`
// Script assumes that if avatar URL is nil, the Registry site build step
// will backfill the value with the user's GitHub avatar URL
AvatarURL *string `yaml:"avatar"`
LinkedinURL *string `yaml:"linkedin"`
WebsiteURL *string `yaml:"website"`
SupportEmail *string `yaml:"support_email"`
}
type contributorProfileReadme struct {
frontmatter contributorProfileFrontmatter
namespace string
filePath string
}
func validateContributorDisplayName(displayName string) error {
if displayName == "" {
return fmt.Errorf("missing display_name")
}
return nil
}
func validateContributorLinkedinURL(linkedinURL *string) error {
if linkedinURL == nil {
return nil
}
if _, err := url.ParseRequestURI(*linkedinURL); err != nil {
return fmt.Errorf("linkedIn URL %q is not valid: %v", *linkedinURL, err)
}
return nil
}
func validateContributorSupportEmail(email *string) []error {
if email == nil {
return nil
}
errs := []error{}
// Can't 100% validate that this is correct without actually sending
// an email, and especially with some contributors being individual
// developers, we don't want to do that on every single run of the CI
// pipeline. Best we can do is verify the general structure
username, server, ok := strings.Cut(*email, "@")
if !ok {
errs = append(errs, fmt.Errorf("email address %q is missing @ symbol", *email))
return errs
}
if username == "" {
errs = append(errs, fmt.Errorf("email address %q is missing username", *email))
}
domain, tld, ok := strings.Cut(server, ".")
if !ok {
errs = append(errs, fmt.Errorf("email address %q is missing period for server segment", *email))
return errs
}
if domain == "" {
errs = append(errs, fmt.Errorf("email address %q is missing domain", *email))
}
if tld == "" {
errs = append(errs, fmt.Errorf("email address %q is missing top-level domain", *email))
}
if strings.Contains(*email, "?") {
errs = append(errs, errors.New("email is not allowed to contain query parameters"))
}
return errs
}
func validateContributorWebsite(websiteURL *string) error {
if websiteURL == nil {
return nil
}
if _, err := url.ParseRequestURI(*websiteURL); err != nil {
return fmt.Errorf("linkedIn URL %q is not valid: %v", *websiteURL, err)
}
return nil
}
func validateContributorStatus(status string) error {
if !slices.Contains(validContributorStatuses, status) {
return fmt.Errorf("contributor status %q is not valid", status)
}
return nil
}
// Can't validate the image actually leads to a valid resource in a pure
// function, but can at least catch obvious problems
func validateContributorAvatarURL(avatarURL *string) []error {
if avatarURL == nil {
return nil
}
errs := []error{}
if *avatarURL == "" {
errs = append(errs, errors.New("avatar URL must be omitted or non-empty string"))
return errs
}
// Have to use .Parse instead of .ParseRequestURI because this is the
// one field that's allowed to be a relative URL
if _, err := url.Parse(*avatarURL); err != nil {
errs = append(errs, fmt.Errorf("URL %q is not a valid relative or absolute URL", *avatarURL))
}
if strings.Contains(*avatarURL, "?") {
errs = append(errs, errors.New("avatar URL is not allowed to contain search parameters"))
}
matched := false
for _, ff := range supportedAvatarFileFormats {
matched = strings.HasSuffix(*avatarURL, ff)
if matched {
break
}
}
if !matched {
segments := strings.Split(*avatarURL, ".")
fileExtension := segments[len(segments)-1]
errs = append(errs, fmt.Errorf("avatar URL '.%s' does not end in a supported file format: [%s]", fileExtension, strings.Join(supportedAvatarFileFormats, ", ")))
}
return errs
}
func validateContributorReadme(rm contributorProfileReadme) []error {
allErrs := []error{}
if err := validateContributorDisplayName(rm.frontmatter.DisplayName); err != nil {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
}
if err := validateContributorLinkedinURL(rm.frontmatter.LinkedinURL); err != nil {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
}
if err := validateContributorWebsite(rm.frontmatter.WebsiteURL); err != nil {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
}
if err := validateContributorStatus(rm.frontmatter.ContributorStatus); err != nil {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateContributorSupportEmail(rm.frontmatter.SupportEmail) {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateContributorAvatarURL(rm.frontmatter.AvatarURL) {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
}
return allErrs
}
func parseContributorProfile(rm readme) (contributorProfileReadme, error) {
fm, _, err := separateFrontmatter(rm.rawText)
if err != nil {
return contributorProfileReadme{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
}
yml := contributorProfileFrontmatter{}
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
return contributorProfileReadme{}, fmt.Errorf("%q: failed to parse: %v", rm.filePath, err)
}
return contributorProfileReadme{
filePath: rm.filePath,
frontmatter: yml,
namespace: strings.TrimSuffix(strings.TrimPrefix(rm.filePath, "registry/"), "/README.md"),
}, nil
}
func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfileReadme, error) {
profilesByNamespace := map[string]contributorProfileReadme{}
yamlParsingErrors := []error{}
for _, rm := range readmeEntries {
p, err := parseContributorProfile(rm)
if err != nil {
yamlParsingErrors = append(yamlParsingErrors, err)
continue
}
if prev, alreadyExists := profilesByNamespace[p.namespace]; alreadyExists {
yamlParsingErrors = append(yamlParsingErrors, fmt.Errorf("%q: namespace %q conflicts with namespace from %q", p.filePath, p.namespace, prev.filePath))
continue
}
profilesByNamespace[p.namespace] = p
}
if len(yamlParsingErrors) != 0 {
return nil, validationPhaseError{
phase: validationPhaseReadmeParsing,
errors: yamlParsingErrors,
}
}
yamlValidationErrors := []error{}
for _, p := range profilesByNamespace {
errors := validateContributorReadme(p)
if len(errors) > 0 {
yamlValidationErrors = append(yamlValidationErrors, errors...)
continue
}
}
if len(yamlValidationErrors) != 0 {
return nil, validationPhaseError{
phase: validationPhaseReadmeParsing,
errors: yamlValidationErrors,
}
}
return profilesByNamespace, nil
}
func aggregateContributorReadmeFiles() ([]readme, error) {
dirEntries, err := os.ReadDir(rootRegistryPath)
if err != nil {
return nil, err
}
allReadmeFiles := []readme{}
errs := []error{}
for _, e := range dirEntries {
dirPath := path.Join(rootRegistryPath, e.Name())
if !e.IsDir() {
continue
}
readmePath := path.Join(dirPath, "README.md")
rmBytes, err := os.ReadFile(readmePath)
if err != nil {
errs = append(errs, err)
continue
}
allReadmeFiles = append(allReadmeFiles, readme{
filePath: readmePath,
rawText: string(rmBytes),
})
}
if len(errs) != 0 {
return nil, validationPhaseError{
phase: validationPhaseFileLoad,
errors: errs,
}
}
return allReadmeFiles, nil
}
func validateContributorRelativeUrls(contributors map[string]contributorProfileReadme) error {
// This function only validates relative avatar URLs for now, but it can be
// beefed up to validate more in the future
errs := []error{}
for _, con := range contributors {
// If the avatar URL is missing, we'll just assume that the Registry
// site build step will take care of filling in the data properly
if con.frontmatter.AvatarURL == nil {
continue
}
isRelativeURL := strings.HasPrefix(*con.frontmatter.AvatarURL, ".") ||
strings.HasPrefix(*con.frontmatter.AvatarURL, "/")
if !isRelativeURL {
continue
}
if strings.HasPrefix(*con.frontmatter.AvatarURL, "..") {
errs = append(errs, fmt.Errorf("%q: relative avatar URLs cannot be placed outside a user's namespaced directory", con.filePath))
continue
}
absolutePath := strings.TrimSuffix(con.filePath, "README.md") +
*con.frontmatter.AvatarURL
_, err := os.ReadFile(absolutePath)
if err != nil {
errs = append(errs, fmt.Errorf("%q: relative avatar path %q does not point to image in file system", con.filePath, *con.frontmatter.AvatarURL))
}
}
if len(errs) == 0 {
return nil
}
return validationPhaseError{
phase: validationPhaseAssetCrossReference,
errors: errs,
}
}
func validateAllContributorFiles() error {
allReadmeFiles, err := aggregateContributorReadmeFiles()
if err != nil {
return err
}
log.Printf("Processing %d README files\n", len(allReadmeFiles))
contributors, err := parseContributorFiles(allReadmeFiles)
if err != nil {
return err
}
log.Printf("Processed %d README files as valid contributor profiles", len(contributors))
err = validateContributorRelativeUrls(contributors)
if err != nil {
return err
}
log.Println("All relative URLs for READMEs are valid")
log.Printf("Processed all READMEs in the %q directory\n", rootRegistryPath)
return nil
}
+28
View File
@@ -0,0 +1,28 @@
package main
import "fmt"
// validationPhaseError represents an error that occurred during a specific
// phase of README validation. It should be used to collect ALL validation
// errors that happened during a specific phase, rather than the first one
// encountered.
type validationPhaseError struct {
phase validationPhase
errors []error
}
var _ error = validationPhaseError{}
func (vpe validationPhaseError) Error() string {
msg := fmt.Sprintf("Error during %q phase of README validation:", vpe.phase)
for _, e := range vpe.errors {
msg += fmt.Sprintf("\n- %v", e)
}
msg += "\n"
return msg
}
func addFilePathToError(filePath string, err error) error {
return fmt.Errorf("%q: %v", filePath, err)
}
+44
View File
@@ -0,0 +1,44 @@
// This package is for validating all contributors within the main Registry
// directory. It validates that it has nothing but sub-directories, and that
// each sub-directory has a README.md file. Each of those files must then
// describe a specific contributor. The contents of these files will be parsed
// by the Registry site build step, to be displayed in the Registry site's UI.
package main
import (
"fmt"
"log"
"os"
)
func main() {
log.Println("Starting README validation")
// If there are fundamental problems with how the repo is structured, we
// can't make any guarantees that any further validations will be relevant
// or accurate
repoErr := validateRepoStructure()
if repoErr != nil {
log.Println(repoErr)
os.Exit(1)
}
var errs []error
err := validateAllContributorFiles()
if err != nil {
errs = append(errs, err)
}
err = validateAllCoderResourceFilesOfType("modules")
if err != nil {
errs = append(errs, err)
}
if len(errs) == 0 {
log.Printf("Processed all READMEs in the %q directory\n", rootRegistryPath)
os.Exit(0)
}
for _, err := range errs {
fmt.Println(err)
}
os.Exit(1)
}
+178
View File
@@ -0,0 +1,178 @@
package main
import (
"bufio"
"errors"
"fmt"
"regexp"
"strings"
)
const rootRegistryPath = "./registry"
var supportedAvatarFileFormats = []string{".png", ".jpeg", ".jpg", ".gif", ".svg"}
// readme represents a single README file within the repo (usually within the
// top-level "/registry" directory).
type readme struct {
filePath string
rawText string
}
// separateFrontmatter attempts to separate a README file's frontmatter content
// from the main README body, returning both values in that order. It does not
// validate whether the structure of the frontmatter is valid (i.e., that it's
// structured as YAML).
func separateFrontmatter(readmeText string) (string, string, error) {
if readmeText == "" {
return "", "", errors.New("README is empty")
}
const fence = "---"
fm := ""
body := ""
fenceCount := 0
lineScanner := bufio.NewScanner(strings.NewReader(strings.TrimSpace(readmeText)))
for lineScanner.Scan() {
nextLine := lineScanner.Text()
if fenceCount < 2 && nextLine == fence {
fenceCount++
continue
}
// Break early if the very first line wasn't a fence, because then we
// know for certain that the README has problems
if fenceCount == 0 {
break
}
// It should be safe to trim each line of the frontmatter on a per-line
// basis, because there shouldn't be any extra meaning attached to the
// indentation. The same does NOT apply to the README; best we can do is
// gather all the lines, and then trim around it
if inReadmeBody := fenceCount >= 2; inReadmeBody {
body += nextLine + "\n"
} else {
fm += strings.TrimSpace(nextLine) + "\n"
}
}
if fenceCount < 2 {
return "", "", errors.New("README does not have two sets of frontmatter fences")
}
if fm == "" {
return "", "", errors.New("readme has frontmatter fences but no frontmatter content")
}
return fm, strings.TrimSpace(body), nil
}
var readmeHeaderRe = regexp.MustCompile("^(#{1,})(\\s*)")
// Todo: This seems to work okay for now, but the really proper way of doing
// this is by parsing this as an AST, and then checking the resulting nodes
func validateReadmeBody(body string) []error {
trimmed := strings.TrimSpace(body)
if trimmed == "" {
return []error{errors.New("README body is empty")}
}
// If the very first line of the README, there's a risk that the rest of the
// validation logic will break, since we don't have many guarantees about
// how the README is actually structured
if !strings.HasPrefix(trimmed, "# ") {
return []error{errors.New("README body must start with ATX-style h1 header (i.e., \"# \")")}
}
var errs []error
latestHeaderLevel := 0
foundFirstH1 := false
isInCodeBlock := false
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
for lineScanner.Scan() {
nextLine := lineScanner.Text()
// Have to check this because a lot of programming languages support #
// comments (including Terraform), and without any context, there's no
// way to tell the difference between a markdown header and code comment
if strings.HasPrefix(nextLine, "```") {
isInCodeBlock = !isInCodeBlock
continue
}
if isInCodeBlock {
continue
}
headerGroups := readmeHeaderRe.FindStringSubmatch(nextLine)
if headerGroups == nil {
continue
}
spaceAfterHeader := headerGroups[2]
if spaceAfterHeader == "" {
errs = append(errs, errors.New("header does not have space between header characters and main header text"))
}
nextHeaderLevel := len(headerGroups[1])
if nextHeaderLevel == 1 && !foundFirstH1 {
foundFirstH1 = true
latestHeaderLevel = 1
continue
}
// If we have obviously invalid headers, it's not really safe to keep
// proceeding with the rest of the content
if nextHeaderLevel == 1 {
errs = append(errs, errors.New("READMEs cannot contain more than h1 header"))
break
}
if nextHeaderLevel > 6 {
errs = append(errs, fmt.Errorf("README/HTML files cannot have headers exceed level 6 (found level %d)", nextHeaderLevel))
break
}
// This is something we need to enforce for accessibility, not just for
// the Registry website, but also when users are viewing the README
// files in the GitHub web view
if nextHeaderLevel > latestHeaderLevel && nextHeaderLevel != (latestHeaderLevel+1) {
errs = append(errs, fmt.Errorf("headers are not allowed to increase more than 1 level at a time"))
continue
}
// As long as the above condition passes, there's no problems with
// going up a header level or going down 1+ header levels
latestHeaderLevel = nextHeaderLevel
}
return errs
}
// validationPhase represents a specific phase during README validation. It is
// expected that each phase is discrete, and errors during one will prevent a
// future phase from starting.
type validationPhase string
const (
// validationPhaseFileStructureValidation indicates when the entire Registry
// directory is being verified for having all files be placed in the file
// system as expected.
validationPhaseFileStructureValidation validationPhase = "File structure validation"
// validationPhaseFileLoad indicates when README files are being read from
// the file system
validationPhaseFileLoad = "Filesystem reading"
// validationPhaseReadmeParsing indicates when a README's frontmatter is
// being parsed as YAML. This phase does not include YAML validation.
validationPhaseReadmeParsing = "README parsing"
// validationPhaseReadmeValidation indicates when a README's frontmatter is
// being validated as proper YAML with expected keys.
validationPhaseReadmeValidation = "README validation"
// validationPhaseAssetCrossReference indicates when a README's frontmatter
// is having all its relative URLs be validated for whether they point to
// valid resources.
validationPhaseAssetCrossReference = "Cross-referencing relative asset URLs"
)
+142
View File
@@ -0,0 +1,142 @@
package main
import (
"errors"
"fmt"
"os"
"path"
"slices"
"strings"
)
var supportedUserNameSpaceDirectories = append(supportedResourceTypes[:], ".icons", ".images")
func validateCoderResourceSubdirectory(dirPath string) []error {
errs := []error{}
subDir, err := os.Stat(dirPath)
if err != nil {
// It's valid for a specific resource directory not to exist. It's just
// that if it does exist, it must follow specific rules
if !errors.Is(err, os.ErrNotExist) {
errs = append(errs, addFilePathToError(dirPath, err))
}
return errs
}
if !subDir.IsDir() {
errs = append(errs, fmt.Errorf("%q: path is not a directory", dirPath))
return errs
}
files, err := os.ReadDir(dirPath)
if err != nil {
errs = append(errs, addFilePathToError(dirPath, err))
return errs
}
for _, f := range files {
// The .coder subdirectories are sometimes generated as part of Bun
// tests. These subdirectories will never be committed to the repo, but
// in the off chance that they don't get cleaned up properly, we want to
// skip over them
if !f.IsDir() || f.Name() == ".coder" {
continue
}
resourceReadmePath := path.Join(dirPath, f.Name(), "README.md")
_, err := os.Stat(resourceReadmePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
errs = append(errs, fmt.Errorf("%q: 'README.md' does not exist", resourceReadmePath))
} else {
errs = append(errs, addFilePathToError(resourceReadmePath, err))
}
}
mainTerraformPath := path.Join(dirPath, f.Name(), "main.tf")
_, err = os.Stat(mainTerraformPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
errs = append(errs, fmt.Errorf("%q: 'main.tf' file does not exist", mainTerraformPath))
} else {
errs = append(errs, addFilePathToError(mainTerraformPath, err))
}
}
}
return errs
}
func validateRegistryDirectory() []error {
userDirs, err := os.ReadDir(rootRegistryPath)
if err != nil {
return []error{err}
}
allErrs := []error{}
for _, d := range userDirs {
dirPath := path.Join(rootRegistryPath, d.Name())
if !d.IsDir() {
allErrs = append(allErrs, fmt.Errorf("detected non-directory file %q at base of main Registry directory", dirPath))
continue
}
contributorReadmePath := path.Join(dirPath, "README.md")
_, err := os.Stat(contributorReadmePath)
if err != nil {
allErrs = append(allErrs, err)
}
files, err := os.ReadDir(dirPath)
if err != nil {
allErrs = append(allErrs, err)
continue
}
for _, f := range files {
// Todo: Decide if there's anything more formal that we want to
// ensure about non-directories scoped to user namespaces
if !f.IsDir() {
continue
}
segment := f.Name()
filePath := path.Join(dirPath, segment)
if !slices.Contains(supportedUserNameSpaceDirectories, segment) {
allErrs = append(allErrs, fmt.Errorf("%q: only these sub-directories are allowed at top of user namespace: [%s]", filePath, strings.Join(supportedUserNameSpaceDirectories, ", ")))
continue
}
if slices.Contains(supportedResourceTypes, segment) {
errs := validateCoderResourceSubdirectory(filePath)
if len(errs) != 0 {
allErrs = append(allErrs, errs...)
}
}
}
}
return allErrs
}
func validateRepoStructure() error {
var problems []error
if errs := validateRegistryDirectory(); len(errs) != 0 {
problems = append(problems, errs...)
}
_, err := os.Stat("./.icons")
if err != nil {
problems = append(problems, errors.New("missing top-level .icons directory (used for storing reusable Coder resource icons)"))
}
if len(problems) != 0 {
return validationPhaseError{
phase: validationPhaseFileStructureValidation,
errors: problems,
}
}
return nil
}
@@ -0,0 +1,121 @@
# Goose
Run the [Goose](https://block.github.io/goose/) agent in your workspace to generate code and perform tasks.
```tf
module "goose" {
source = "registry.coder.com/coder/goose/coder"
version = "1.0.31"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
goose_version = "v1.0.16"
}
```
## Prerequisites
- `screen` must be installed in your workspace to run Goose in the background
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces.
## Examples
Your workspace must have `screen` installed to use this.
### Run in the background and report tasks (Experimental)
> This functionality is in early access as of Coder v2.21 and is still evolving.
> For now, we recommend testing it in a demo or staging environment,
> rather than deploying to production
>
> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents)
>
> Join our [Discord channel](https://discord.gg/coder) or
> [contact us](https://coder.com/contact) to get help or share feedback.
```tf
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/coder-login/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
}
variable "anthropic_api_key" {
type = string
description = "The Anthropic API key"
sensitive = true
}
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Write a prompt for Goose"
mutable = true
}
# Set the prompt and system prompt for Goose via environment variables
resource "coder_agent" "main" {
# ...
env = {
GOOSE_SYSTEM_PROMPT = <<-EOT
You are a helpful assistant that can help write code.
Run all long running tasks (e.g. npm run dev) in the background and not in the foreground.
Periodically check in on background tasks.
Notify Coder of the status of the task before and after your steps.
EOT
GOOSE_TASK_PROMPT = data.coder_parameter.ai_prompt.value
# An API key is required for experiment_auto_configure
# See https://block.github.io/goose/docs/getting-started/providers
ANTHROPIC_API_KEY = var.anthropic_api_key # or use a coder_parameter
}
}
module "goose" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/goose/coder"
version = "1.0.31"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
goose_version = "v1.0.16"
# Enable experimental features
experiment_report_tasks = true
# Run Goose in the background
experiment_use_screen = true
# Avoid configuring Goose manually
experiment_auto_configure = true
# Required for experiment_auto_configure
experiment_goose_provider = "anthropic"
experiment_goose_model = "claude-3-5-sonnet-latest"
}
```
## Run standalone
Run Goose as a standalone app in your workspace. This will install Goose and run it directly without using screen or any task reporting to the Coder UI.
```tf
module "goose" {
source = "registry.coder.com/coder/goose/coder"
version = "1.0.31"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
goose_version = "v1.0.16"
# Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL
icon = "https://raw.githubusercontent.com/block/goose/refs/heads/main/ui/desktop/src/images/icon.svg"
}
```
+72
View File
@@ -0,0 +1,72 @@
---
display_name: MODULE_NAME
description: Describe what this module does
icon: ../../../../.icons/<A_RELEVANT_ICON>.svg
maintainer_github: GITHUB_USERNAME
verified: false
tags: [helper]
---
# MODULE_NAME
<!-- Describes what this module does -->
```tf
module "MODULE_NAME" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
version = "1.0.2"
}
```
<!-- Add a screencast or screenshot here put them in .images directory -->
## Examples
### Example 1
Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
```tf
module "MODULE_NAME" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
extensions = [
"dracula-theme.theme-dracula"
]
}
```
Enter the `<author>.<name>` into the extensions array and code-server will automatically install on start.
### Example 2
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) file:
```tf
module "MODULE_NAME" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
"workbench.colorTheme" = "Dracula"
}
}
```
### Example 3
Run code-server in the background, don't fetch it from GitHub:
```tf
module "MODULE_NAME" {
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
offline = true
}
```
+108
View File
@@ -0,0 +1,108 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.17"
}
}
}
locals {
# A built-in icon like "/icon/code.svg" or a full URL of icon
icon_url = "https://raw.githubusercontent.com/coder/coder/main/site/static/icon/code.svg"
# a map of all possible values
options = {
"Option 1" = {
"name" = "Option 1",
"value" = "1"
"icon" = "/emojis/1.png"
}
"Option 2" = {
"name" = "Option 2",
"value" = "2"
"icon" = "/emojis/2.png"
}
}
}
# Add required variables for your modules and remove any unneeded variables
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "log_path" {
type = string
description = "The path to log MODULE_NAME to."
default = "/tmp/MODULE_NAME.log"
}
variable "port" {
type = number
description = "The port to run MODULE_NAME on."
default = 19999
}
variable "mutable" {
type = bool
description = "Whether the parameter is mutable."
default = true
}
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
}
# Add other variables here
resource "coder_script" "MODULE_NAME" {
agent_id = var.agent_id
display_name = "MODULE_NAME"
icon = local.icon_url
script = templatefile("${path.module}/run.sh", {
LOG_PATH : var.log_path,
})
run_on_start = true
run_on_stop = false
}
resource "coder_app" "MODULE_NAME" {
agent_id = var.agent_id
slug = "MODULE_NAME"
display_name = "MODULE_NAME"
url = "http://localhost:${var.port}"
icon = local.icon_url
subdomain = false
share = "owner"
order = var.order
# Remove if the app does not have a healthcheck endpoint
healthcheck {
url = "http://localhost:${var.port}/healthz"
interval = 5
threshold = 6
}
}
data "coder_parameter" "MODULE_NAME" {
type = "list(string)"
name = "MODULE_NAME"
display_name = "MODULE_NAME"
icon = local.icon_url
mutable = var.mutable
default = local.options["Option 1"]["value"]
dynamic "option" {
for_each = local.options
content {
icon = option.value.icon
name = option.value.name
value = option.value.value
}
}
}
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env sh
# Convert templated variables to shell variables
# shellcheck disable=SC2269
LOG_PATH=${LOG_PATH}
# shellcheck disable=SC2034
BOLD='\033[0;1m'
# shellcheck disable=SC2059
printf "$${BOLD}Installing MODULE_NAME ...\n\n"
# Add code here
# Use variables from the templatefile function in main.tf
# e.g. LOG_PATH, PORT, etc.
printf "🥳 Installation complete!\n\n"
printf "👷 Starting MODULE_NAME in background...\n\n"
# Start the app in here
# 1. Use & to run it in background
# 2. redirct stdout and stderr to log files
./app > "$${LOG_PATH}" 2>&1 &
printf "check logs at %s\n\n" "$${LOG_PATH}"
Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

+28
View File
@@ -0,0 +1,28 @@
{
"name": "registry",
"scripts": {
"fmt": "bun x prettier --write **/*.sh **/*.ts **/*.md *.md && terraform fmt -recursive -diff",
"fmt:ci": "bun x prettier --check **/*.sh **/*.ts **/*.md *.md && terraform fmt -check -recursive -diff",
"terraform-validate": "./scripts/terraform_validate.sh",
"test": "bun test",
"update-version": "./update-version.sh"
},
"devDependencies": {
"@types/bun": "^1.2.9",
"bun-types": "^1.1.23",
"gray-matter": "^4.0.3",
"marked": "^12.0.2",
"prettier": "^3.3.3",
"prettier-plugin-sh": "^0.13.1",
"prettier-plugin-terraform-formatter": "^1.2.1"
},
"peerDependencies": {
"typescript": "^5.5.4"
},
"prettier": {
"plugins": [
"prettier-plugin-sh",
"prettier-plugin-terraform-formatter"
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

-1
View File
@@ -4,7 +4,6 @@ bio: Coder provisions cloud development environments via Terraform, supporting L
github: coder
linkedin: https://www.linkedin.com/company/coderhq
website: https://www.coder.com
support_email: support@coder.com
status: official
---
+315
View File
@@ -0,0 +1,315 @@
---
display_name: Aider
description: Run Aider AI pair programming in your workspace
icon: ../../../../.icons/aider.svg
maintainer_github: coder
verified: true
tags: [agent, ai, aider]
---
# Aider
Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider and provides a persistent session using screen or tmux.
```tf
module "aider" {
source = "registry.coder.com/coder/aider/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
}
```
## Features
- **Interactive Parameter Selection**: Choose your AI provider, model, and configuration options when creating the workspace
- **Multiple AI Providers**: Supports Anthropic (Claude), OpenAI, DeepSeek, GROQ, and OpenRouter
- **Persistent Sessions**: Uses screen (default) or tmux to keep Aider running in the background
- **Optional Dependencies**: Install Playwright for web page scraping and PortAudio for voice coding
- **Project Integration**: Works with any project directory, including Git repositories
- **Browser UI**: Use Aider in your browser with a modern web interface instead of the terminal
- **Non-Interactive Mode**: Automatically processes tasks when provided via the `task_prompt` variable
## Module Parameters
| Parameter | Description | Type | Default |
| ---------------------------------- | -------------------------------------------------------------------------- | -------- | ------------------- |
| `agent_id` | The ID of a Coder agent (required) | `string` | - |
| `folder` | The folder to run Aider in | `string` | `/home/coder` |
| `install_aider` | Whether to install Aider | `bool` | `true` |
| `aider_version` | The version of Aider to install | `string` | `"latest"` |
| `use_screen` | Whether to use screen for running Aider in the background | `bool` | `true` |
| `use_tmux` | Whether to use tmux instead of screen for running Aider in the background | `bool` | `false` |
| `session_name` | Name for the persistent session (screen or tmux) | `string` | `"aider"` |
| `order` | Position of the app in the UI presentation | `number` | `null` |
| `icon` | The icon to use for the app | `string` | `"/icon/aider.svg"` |
| `experiment_report_tasks` | Whether to enable task reporting | `bool` | `true` |
| `system_prompt` | System prompt for instructing Aider on task reporting and behavior | `string` | See default in code |
| `task_prompt` | Task prompt to use with Aider | `string` | `""` |
| `ai_provider` | AI provider to use with Aider (openai, anthropic, azure, etc.) | `string` | `"anthropic"` |
| `ai_model` | AI model to use (can use Aider's built-in aliases like "sonnet", "4o") | `string` | `"sonnet"` |
| `ai_api_key` | API key for the selected AI provider | `string` | `""` |
| `custom_env_var_name` | Custom environment variable name when using custom provider | `string` | `""` |
| `experiment_pre_install_script` | Custom script to run before installing Aider | `string` | `null` |
| `experiment_post_install_script` | Custom script to run after installing Aider | `string` | `null` |
| `experiment_additional_extensions` | Additional extensions configuration in YAML format to append to the config | `string` | `null` |
> **Note**: `use_screen` and `use_tmux` cannot both be enabled at the same time. By default, `use_screen` is set to `true` and `use_tmux` is set to `false`.
## Usage Examples
### Basic setup with API key
```tf
variable "anthropic_api_key" {
type = string
description = "Anthropic API key"
sensitive = true
}
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
ai_api_key = var.anthropic_api_key
}
```
This basic setup will:
- Install Aider in the workspace
- Create a persistent screen session named "aider"
- Configure Aider to use Anthropic Claude 3.7 Sonnet model
- Enable task reporting (configures Aider to report tasks to Coder MCP)
### Using OpenAI with tmux
```tf
variable "openai_api_key" {
type = string
description = "OpenAI API key"
sensitive = true
}
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
use_tmux = true
ai_provider = "openai"
ai_model = "4o" # Uses Aider's built-in alias for gpt-4o
ai_api_key = var.openai_api_key
}
```
### Using a custom provider
```tf
variable "custom_api_key" {
type = string
description = "Custom provider API key"
sensitive = true
}
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
ai_provider = "custom"
custom_env_var_name = "MY_CUSTOM_API_KEY"
ai_model = "custom-model"
ai_api_key = var.custom_api_key
}
```
### Adding Custom Extensions (Experimental)
You can extend Aider's capabilities by adding custom extensions:
```tf
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
ai_api_key = var.anthropic_api_key
experiment_pre_install_script = <<-EOT
pip install some-custom-dependency
EOT
experiment_additional_extensions = <<-EOT
custom-extension:
args: []
cmd: custom-extension-command
description: A custom extension for Aider
enabled: true
envs: {}
name: custom-extension
timeout: 300
type: stdio
EOT
}
```
Note: The indentation in the heredoc is preserved, so you can write the YAML naturally.
## Task Reporting (Experimental)
> This functionality is in early access as of Coder v2.21 and is still evolving.
> For now, we recommend testing it in a demo or staging environment,
> rather than deploying to production
>
> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents)
>
> Join our [Discord channel](https://discord.gg/coder) or
> [contact us](https://coder.com/contact) to get help or share feedback.
Your workspace must have either `screen` or `tmux` installed to use this.
Task reporting is **enabled by default** in this module, allowing you to:
- Send an initial prompt to Aider during workspace creation
- Monitor task progress in the Coder UI
- Use the `coder_parameter` resource to collect prompts from users
### Setting up Task Reporting
To use task reporting effectively:
1. Add the Coder Login module to your template
2. Configure the necessary variables to pass the task prompt
3. Optionally add a coder_parameter to collect prompts from users
Here's a complete example:
```tf
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder-login/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
}
variable "anthropic_api_key" {
type = string
description = "Anthropic API key"
sensitive = true
}
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Write a prompt for Aider"
mutable = true
ephemeral = true
}
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
ai_api_key = var.anthropic_api_key
task_prompt = data.coder_parameter.ai_prompt.value
# Optionally customize the system prompt
system_prompt = <<-EOT
You are a helpful Coding assistant. Aim to autonomously investigate
and solve issues the user gives you and test your work, whenever possible.
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
but opt for autonomy.
YOU MUST REPORT ALL TASKS TO CODER.
When reporting tasks, you MUST follow these EXACT instructions:
- IMMEDIATELY report status after receiving ANY user message.
- Be granular. If you are investigating with multiple steps, report each step to coder.
Task state MUST be one of the following:
- Use "state": "working" when actively processing WITHOUT needing additional user input.
- Use "state": "complete" only when finished with a task.
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
Task summaries MUST:
- Include specifics about what you're doing.
- Include clear and actionable steps for the user.
- Be less than 160 characters in length.
EOT
}
```
When a task prompt is provided via the `task_prompt` variable, the module automatically:
1. Combines the system prompt with the task prompt into a single message in the format:
```
SYSTEM PROMPT:
[system_prompt content]
This is your current task: [task_prompt]
```
2. Executes the task during workspace creation using the `--message` and `--yes-always` flags
3. Logs task output to `$HOME/.aider.log` for reference
If you want to disable task reporting, set `experiment_report_tasks = false` in your module configuration.
## Using Aider in Your Workspace
After the workspace starts, Aider will be installed and configured according to your parameters. A persistent session will automatically be started during workspace creation.
### Session Options
You can run Aider in three different ways:
1. **Direct Mode**: Aider starts directly in the specified folder when you click the app button
- Simple setup without persistent context
- Suitable for quick coding sessions
2. **Screen Mode** (Default): Run Aider in a screen session that persists across connections
- Session name: "aider" (or configured via `session_name`)
3. **Tmux Mode**: Run Aider in a tmux session instead of screen
- Set `use_tmux = true` to enable
- Session name: "aider" (or configured via `session_name`)
- Configures tmux with mouse support for shared sessions
Persistent sessions (screen/tmux) allow you to:
- Disconnect and reconnect without losing context
- Run Aider in the background while doing other work
- Switch between terminal and browser interfaces
### Available AI Providers and Models
Aider supports various providers and models, and this module integrates directly with Aider's built-in model aliases:
| Provider | Example Models/Aliases | Default Model |
| ------------- | --------------------------------------------- | ---------------------- |
| **anthropic** | "sonnet" (Claude 3.7 Sonnet), "opus", "haiku" | "sonnet" |
| **openai** | "4o" (GPT-4o), "4" (GPT-4), "3.5-turbo" | "4o" |
| **azure** | Azure OpenAI models | "gpt-4" |
| **google** | "gemini" (Gemini Pro), "gemini-2.5-pro" | "gemini-2.5-pro" |
| **cohere** | "command-r-plus", etc. | "command-r-plus" |
| **mistral** | "mistral-large-latest" | "mistral-large-latest" |
| **ollama** | "llama3", etc. | "llama3" |
| **custom** | Any model name with custom ENV variable | - |
For a complete and up-to-date list of supported aliases and models, please refer to the [Aider LLM documentation](https://aider.chat/docs/llms.html) and the [Aider LLM Leaderboards](https://aider.chat/docs/leaderboards.html) which show performance comparisons across different models.
## Troubleshooting
If you encounter issues:
1. **Screen/Tmux issues**: If you can't reconnect to your session, check if the session exists with `screen -list` or `tmux list-sessions`
2. **API key issues**: Ensure you've entered the correct API key for your selected provider
3. **Browser mode issues**: If the browser interface doesn't open, check that you're accessing it from a machine that can reach your Coder workspace
For more information on using Aider, see the [Aider documentation](https://aider.chat/docs/).
```
```
+107
View File
@@ -0,0 +1,107 @@
import { describe, expect, it } from "bun:test";
import {
findResourceInstance,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
describe("aider", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
it("configures task prompt correctly", async () => {
const testPrompt = "Add a hello world function";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
task_prompt: testPrompt,
});
const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain(
`This is your current task: ${testPrompt}`,
);
expect(instance.script).toContain("aider --architect --yes-always");
});
it("handles custom system prompt", async () => {
const customPrompt = "Report all tasks with state: working";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
system_prompt: customPrompt,
});
const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain(customPrompt);
});
it("handles pre and post install scripts", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
experiment_pre_install_script: "echo 'Pre-install script executed'",
experiment_post_install_script: "echo 'Post-install script executed'",
});
const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain("Running pre-install script");
expect(instance.script).toContain("Running post-install script");
expect(instance.script).toContain("base64 -d > /tmp/pre_install.sh");
expect(instance.script).toContain("base64 -d > /tmp/post_install.sh");
});
it("validates that use_screen and use_tmux cannot both be true", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
use_screen: true,
use_tmux: true,
});
const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain(
"Error: Both use_screen and use_tmux cannot be enabled at the same time",
);
expect(instance.script).toContain("exit 1");
});
it("configures Aider with known provider and model", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
ai_provider: "anthropic",
ai_model: "sonnet",
ai_api_key: "test-anthropic-key",
});
const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain(
'export ANTHROPIC_API_KEY=\\"test-anthropic-key\\"',
);
expect(instance.script).toContain("--model sonnet");
expect(instance.script).toContain(
"Starting Aider using anthropic provider and model: sonnet",
);
});
it("handles custom provider with custom env var and API key", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
ai_provider: "custom",
custom_env_var_name: "MY_CUSTOM_API_KEY",
ai_model: "custom-model",
ai_api_key: "test-custom-key",
});
const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain(
'export MY_CUSTOM_API_KEY=\\"test-custom-key\\"',
);
expect(instance.script).toContain("--model custom-model");
expect(instance.script).toContain(
"Starting Aider using custom provider and model: custom-model",
);
});
});
+502
View File
@@ -0,0 +1,502 @@
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."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/aider.svg"
}
variable "folder" {
type = string
description = "The folder to run Aider in."
default = "/home/coder"
}
variable "install_aider" {
type = bool
description = "Whether to install Aider."
default = true
}
variable "aider_version" {
type = string
description = "The version of Aider to install."
default = "latest"
}
variable "use_screen" {
type = bool
description = "Whether to use screen for running Aider in the background"
default = true
}
variable "use_tmux" {
type = bool
description = "Whether to use tmux instead of screen for running Aider in the background"
default = false
}
variable "session_name" {
type = string
description = "Name for the persistent session (screen or tmux)"
default = "aider"
}
variable "experiment_report_tasks" {
type = bool
description = "Whether to enable task reporting."
default = true
}
variable "system_prompt" {
type = string
description = "System prompt for instructing Aider on task reporting and behavior"
default = <<-EOT
You are a helpful Coding assistant. Aim to autonomously investigate
and solve issues the user gives you and test your work, whenever possible.
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
but opt for autonomy.
YOU MUST REPORT ALL TASKS TO CODER.
When reporting tasks, you MUST follow these EXACT instructions:
- IMMEDIATELY report status after receiving ANY user message.
- Be granular. If you are investigating with multiple steps, report each step to coder.
Task state MUST be one of the following:
- Use "state": "working" when actively processing WITHOUT needing additional user input.
- Use "state": "complete" only when finished with a task.
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
Task summaries MUST:
- Include specifics about what you're doing.
- Include clear and actionable steps for the user.
- Be less than 160 characters in length.
EOT
}
variable "task_prompt" {
type = string
description = "Task prompt to use with Aider"
default = ""
}
variable "experiment_pre_install_script" {
type = string
description = "Custom script to run before installing Aider."
default = null
}
variable "experiment_post_install_script" {
type = string
description = "Custom script to run after installing Aider."
default = null
}
variable "experiment_additional_extensions" {
type = string
description = "Additional extensions configuration in YAML format to append to the config."
default = null
}
variable "ai_provider" {
type = string
description = "AI provider to use with Aider (openai, anthropic, azure, google, etc.)"
default = "anthropic"
validation {
condition = contains(["openai", "anthropic", "azure", "google", "cohere", "mistral", "ollama", "custom"], var.ai_provider)
error_message = "ai_provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom"
}
}
variable "ai_model" {
type = string
description = "AI model to use with Aider. Can use Aider's built-in aliases like '4o' (gpt-4o), 'sonnet' (claude-3-7-sonnet), 'opus' (claude-3-opus), etc."
default = "sonnet"
}
variable "ai_api_key" {
type = string
description = "API key for the selected AI provider. This will be set as the appropriate environment variable based on the provider."
default = ""
sensitive = true
}
variable "custom_env_var_name" {
type = string
description = "Custom environment variable name when using custom provider"
default = ""
}
locals {
base_extensions = <<-EOT
coder:
args:
- exp
- mcp
- server
cmd: coder
description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
enabled: true
envs:
CODER_MCP_APP_STATUS_SLUG: aider
name: Coder
timeout: 3000
type: stdio
developer:
display_name: Developer
enabled: true
name: developer
timeout: 300
type: builtin
EOT
formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}"
additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : ""
combined_extensions = <<-EOT
extensions:
${local.formatted_base}${local.additional_extensions}
EOT
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
# Combine system prompt and task prompt for aider
combined_prompt = trimspace(<<-EOT
SYSTEM PROMPT:
${var.system_prompt}
This is your current task: ${var.task_prompt}
EOT
)
# Map providers to their environment variable names
provider_env_vars = {
openai = "OPENAI_API_KEY"
anthropic = "ANTHROPIC_API_KEY"
azure = "AZURE_OPENAI_API_KEY"
google = "GOOGLE_API_KEY"
cohere = "COHERE_API_KEY"
mistral = "MISTRAL_API_KEY"
ollama = "OLLAMA_HOST"
custom = var.custom_env_var_name
}
# Get the environment variable name for selected provider
env_var_name = local.provider_env_vars[var.ai_provider]
# Model flag for aider command
model_flag = var.ai_provider == "ollama" ? "--ollama-model" : "--model"
}
# Install and Initialize Aider
resource "coder_script" "aider" {
agent_id = var.agent_id
display_name = "Aider"
icon = var.icon
script = <<-EOT
#!/bin/bash
set -e
command_exists() {
command -v "$1" >/dev/null 2>&1
}
echo "Setting up Aider AI pair programming..."
if [ "${var.use_screen}" = "true" ] && [ "${var.use_tmux}" = "true" ]; then
echo "Error: Both use_screen and use_tmux cannot be enabled at the same time."
exit 1
fi
mkdir -p "${var.folder}"
if [ "$(uname)" = "Linux" ]; then
echo "Checking dependencies for Linux..."
if [ "${var.use_tmux}" = "true" ]; then
if ! command_exists tmux; then
echo "Installing tmux for persistent sessions..."
if command -v apt-get >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y -qq tmux
else
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
apt-get install -y -qq tmux || echo "Warning: Cannot install tmux without sudo privileges"
fi
elif command -v dnf >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
sudo dnf install -y -q tmux
else
dnf install -y -q tmux || echo "Warning: Cannot install tmux without sudo privileges"
fi
else
echo "Warning: Unable to install tmux on this system. Neither apt-get nor dnf found."
fi
else
echo "tmux is already installed, skipping installation."
fi
elif [ "${var.use_screen}" = "true" ]; then
if ! command_exists screen; then
echo "Installing screen for persistent sessions..."
if command -v apt-get >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y -qq screen
else
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
apt-get install -y -qq screen || echo "Warning: Cannot install screen without sudo privileges"
fi
elif command -v dnf >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
sudo dnf install -y -q screen
else
dnf install -y -q screen || echo "Warning: Cannot install screen without sudo privileges"
fi
else
echo "Warning: Unable to install screen on this system. Neither apt-get nor dnf found."
fi
else
echo "screen is already installed, skipping installation."
fi
fi
else
echo "This module currently only supports Linux workspaces."
exit 1
fi
if [ -n "${local.encoded_pre_install_script}" ]; then
echo "Running pre-install script..."
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
chmod +x /tmp/pre_install.sh
/tmp/pre_install.sh
fi
if [ "${var.install_aider}" = "true" ]; then
echo "Installing Aider..."
if ! command_exists python3 || ! command_exists pip3; then
echo "Installing Python dependencies required for Aider..."
if command -v apt-get >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y -qq python3-pip python3-venv
else
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
apt-get install -y -qq python3-pip python3-venv || echo "Warning: Cannot install Python packages without sudo privileges"
fi
elif command -v dnf >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
sudo dnf install -y -q python3-pip python3-virtualenv
else
dnf install -y -q python3-pip python3-virtualenv || echo "Warning: Cannot install Python packages without sudo privileges"
fi
else
echo "Warning: Unable to install Python on this system. Neither apt-get nor dnf found."
fi
else
echo "Python is already installed, skipping installation."
fi
if ! command_exists aider; then
curl -LsSf https://aider.chat/install.sh | sh
fi
if [ -f "$HOME/.bashrc" ]; then
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.bashrc"; then
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.bashrc"
fi
fi
if [ -f "$HOME/.zshrc" ]; then
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.zshrc"; then
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.zshrc"
fi
fi
fi
if [ -n "${local.encoded_post_install_script}" ]; then
echo "Running post-install script..."
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
chmod +x /tmp/post_install.sh
/tmp/post_install.sh
fi
if [ "${var.experiment_report_tasks}" = "true" ]; then
echo "Configuring Aider to report tasks via Coder MCP..."
mkdir -p "$HOME/.config/aider"
cat > "$HOME/.config/aider/config.yml" << EOL
${trimspace(local.combined_extensions)}
EOL
echo "Added Coder MCP extension to Aider config.yml"
fi
echo "Starting persistent Aider session..."
touch "$HOME/.aider.log"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export PATH="$HOME/bin:$PATH"
if [ "${var.use_tmux}" = "true" ]; then
if [ -n "${var.task_prompt}" ]; then
echo "Running Aider with message in tmux session..."
# Configure tmux for shared sessions
if [ ! -f "$HOME/.tmux.conf" ]; then
echo "Creating ~/.tmux.conf with shared session settings..."
echo "set -g mouse on" > "$HOME/.tmux.conf"
fi
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
echo "set -g mouse on" >> "$HOME/.tmux.conf"
fi
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\""
echo "Aider task started in tmux session '${var.session_name}'. Check the UI for progress."
else
# Configure tmux for shared sessions
if [ ! -f "$HOME/.tmux.conf" ]; then
echo "Creating ~/.tmux.conf with shared session settings..."
echo "set -g mouse on" > "$HOME/.tmux.conf"
fi
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
echo "set -g mouse on" >> "$HOME/.tmux.conf"
fi
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${var.system_prompt}\""
echo "Tmux session '${var.session_name}' started. Access it by clicking the Aider button."
fi
else
if [ -n "${var.task_prompt}" ]; then
echo "Running Aider with message in screen session..."
if [ ! -f "$HOME/.screenrc" ]; then
echo "Creating ~/.screenrc and adding multiuser settings..."
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
fi
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
echo "Adding 'multiuser on' to ~/.screenrc..."
echo "multiuser on" >> "$HOME/.screenrc"
fi
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
echo "acladd $(whoami)" >> "$HOME/.screenrc"
fi
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
screen -U -dmS ${var.session_name} bash -c "
cd ${var.folder}
export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
export ${local.env_var_name}=\"${var.ai_api_key}\"
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
/bin/bash
"
echo "Aider task started in screen session '${var.session_name}'. Check the UI for progress."
else
if [ ! -f "$HOME/.screenrc" ]; then
echo "Creating ~/.screenrc and adding multiuser settings..."
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
fi
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
echo "Adding 'multiuser on' to ~/.screenrc..."
echo "multiuser on" >> "$HOME/.screenrc"
fi
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
echo "acladd $(whoami)" >> "$HOME/.screenrc"
fi
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
screen -U -dmS ${var.session_name} bash -c "
cd ${var.folder}
export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
export ${local.env_var_name}=\"${var.ai_api_key}\"
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
/bin/bash
"
echo "Screen session '${var.session_name}' started. Access it by clicking the Aider button."
fi
fi
echo "Aider setup complete!"
EOT
run_on_start = true
}
# Aider CLI app
resource "coder_app" "aider_cli" {
agent_id = var.agent_id
slug = "aider"
display_name = "Aider"
icon = var.icon
command = <<-EOT
#!/bin/bash
set -e
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
if [ "${var.use_tmux}" = "true" ]; then
if tmux has-session -t ${var.session_name} 2>/dev/null; then
echo "Attaching to existing Aider tmux session..."
tmux attach-session -t ${var.session_name}
else
echo "Starting new Aider tmux session..."
tmux new-session -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"; exec bash"
fi
elif [ "${var.use_screen}" = "true" ]; then
if ! screen -list | grep -q "${var.session_name}"; then
echo "Error: No existing Aider session found. Please wait for the script to start it."
exit 1
fi
screen -xRR ${var.session_name}
else
cd "${var.folder}"
echo "Starting Aider directly..."
export ${local.env_var_name}="${var.ai_api_key}"
aider ${local.model_flag} ${var.ai_model} --message "${local.combined_prompt}"
fi
EOT
order = var.order
}
@@ -0,0 +1,48 @@
---
display_name: Amazon DCV Windows
description: Amazon DCV Server and Web Client for Windows
icon: ../../../../.icons/dcv.svg
maintainer_github: coder
verified: true
tags: [windows, amazon, dcv, web, desktop]
---
# Amazon DCV Windows
Amazon DCV is high performance remote display protocol that provides a secure way to deliver remote desktop and application streaming from any cloud or data center to any device, over varying network conditions.
![Amazon DCV on a Windows workspace](../../.images/amazon-dcv-windows.png)
Enable DCV Server and Web Client on Windows workspaces.
```tf
module "dcv" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/amazon-dcv-windows/coder"
version = "1.0.24"
agent_id = resource.coder_agent.main.id
}
resource "coder_metadata" "dcv" {
count = data.coder_workspace.me.start_count
resource_id = aws_instance.dev.id # id of the instance resource
item {
key = "DCV client instructions"
value = "Run `coder port-forward ${data.coder_workspace.me.name} -p ${module.dcv[count.index].port}` and connect to **localhost:${module.dcv[count.index].port}${module.dcv[count.index].web_url_path}**"
}
item {
key = "username"
value = module.dcv[count.index].username
}
item {
key = "password"
value = module.dcv[count.index].password
sensitive = true
}
}
```
## License
Amazon DCV is free to use on AWS EC2 instances but requires a license for other cloud providers. Please see the instructions [here](https://docs.aws.amazon.com/dcv/latest/adminguide/setting-up-license.html#setting-up-license-ec2) for more information.
@@ -0,0 +1,170 @@
# Terraform variables
$adminPassword = "${admin_password}"
$port = "${port}"
$webURLPath = "${web_url_path}"
function Set-LocalAdminUser {
Write-Output "[INFO] Starting Set-LocalAdminUser function"
$securePassword = ConvertTo-SecureString $adminPassword -AsPlainText -Force
Write-Output "[DEBUG] Secure password created"
Get-LocalUser -Name Administrator | Set-LocalUser -Password $securePassword
Write-Output "[INFO] Administrator password set"
Get-LocalUser -Name Administrator | Enable-LocalUser
Write-Output "[INFO] User Administrator enabled successfully"
Read-Host "[DEBUG] Press Enter to proceed to the next step"
}
function Get-VirtualDisplayDriverRequired {
Write-Output "[INFO] Starting Get-VirtualDisplayDriverRequired function"
$token = Invoke-RestMethod -Headers @{'X-aws-ec2-metadata-token-ttl-seconds' = '21600'} -Method PUT -Uri http://169.254.169.254/latest/api/token
Write-Output "[DEBUG] Token acquired: $token"
$instanceType = Invoke-RestMethod -Headers @{'X-aws-ec2-metadata-token' = $token} -Method GET -Uri http://169.254.169.254/latest/meta-data/instance-type
Write-Output "[DEBUG] Instance type: $instanceType"
$OSVersion = ((Get-ItemProperty -Path "Microsoft.PowerShell.Core\Registry::\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ProductName).ProductName) -replace "[^0-9]", ''
Write-Output "[DEBUG] OS version: $OSVersion"
# Force boolean result
$result = (($OSVersion -ne "2019") -and ($OSVersion -ne "2022") -and ($OSVersion -ne "2025")) -and (($instanceType[0] -ne 'g') -and ($instanceType[0] -ne 'p'))
Write-Output "[INFO] VirtualDisplayDriverRequired result: $result"
Read-Host "[DEBUG] Press Enter to proceed to the next step"
return [bool]$result
}
function Download-DCV {
param (
[bool]$VirtualDisplayDriverRequired
)
Write-Output "[INFO] Starting Download-DCV function"
$downloads = @(
@{
Name = "DCV Display Driver"
Required = $VirtualDisplayDriverRequired
Path = "C:\Windows\Temp\DCVDisplayDriver.msi"
Uri = "https://d1uj6qtbmh3dt5.cloudfront.net/nice-dcv-virtual-display-x64-Release.msi"
},
@{
Name = "DCV Server"
Required = $true
Path = "C:\Windows\Temp\DCVServer.msi"
Uri = "https://d1uj6qtbmh3dt5.cloudfront.net/nice-dcv-server-x64-Release.msi"
}
)
foreach ($download in $downloads) {
if ($download.Required -and -not (Test-Path $download.Path)) {
try {
Write-Output "[INFO] Downloading $($download.Name)"
# Display progress manually (no events)
$progressActivity = "Downloading $($download.Name)"
$progressStatus = "Starting download..."
Write-Progress -Activity $progressActivity -Status $progressStatus -PercentComplete 0
# Synchronously download the file
$webClient = New-Object System.Net.WebClient
$webClient.DownloadFile($download.Uri, $download.Path)
# Update progress
Write-Progress -Activity $progressActivity -Status "Completed" -PercentComplete 100
Write-Output "[INFO] $($download.Name) downloaded successfully."
} catch {
Write-Output "[ERROR] Failed to download $($download.Name): $_"
throw
}
} else {
Write-Output "[INFO] $($download.Name) already exists. Skipping download."
}
}
Write-Output "[INFO] All downloads completed"
Read-Host "[DEBUG] Press Enter to proceed to the next step"
}
function Install-DCV {
param (
[bool]$VirtualDisplayDriverRequired
)
Write-Output "[INFO] Starting Install-DCV function"
if (-not (Get-Service -Name "dcvserver" -ErrorAction SilentlyContinue)) {
if ($VirtualDisplayDriverRequired) {
Write-Output "[INFO] Installing DCV Display Driver"
Start-Process "C:\Windows\System32\msiexec.exe" -ArgumentList "/I C:\Windows\Temp\DCVDisplayDriver.msi /quiet /norestart" -Wait
} else {
Write-Output "[INFO] DCV Display Driver installation skipped (not required)."
}
Write-Output "[INFO] Installing DCV Server"
Start-Process "C:\Windows\System32\msiexec.exe" -ArgumentList "/I C:\Windows\Temp\DCVServer.msi ADDLOCAL=ALL /quiet /norestart /l*v C:\Windows\Temp\dcv_install_msi.log" -Wait
} else {
Write-Output "[INFO] DCV Server already installed, skipping installation."
}
# Wait for the service to appear with a timeout
$timeout = 10 # seconds
$elapsed = 0
while (-not (Get-Service -Name "dcvserver" -ErrorAction SilentlyContinue) -and ($elapsed -lt $timeout)) {
Start-Sleep -Seconds 1
$elapsed++
}
if ($elapsed -ge $timeout) {
Write-Output "[WARNING] Timeout waiting for dcvserver service. A restart is required to complete installation."
Restart-SystemForDCV
} else {
Write-Output "[INFO] dcvserver service detected successfully."
}
}
function Restart-SystemForDCV {
Write-Output "[INFO] The system will restart in 10 seconds to finalize DCV installation."
Start-Sleep -Seconds 10
# Initiate restart
Restart-Computer -Force
# Exit the script after initiating restart
Write-Output "[INFO] Please wait for the system to restart..."
Exit 1
}
function Configure-DCV {
Write-Output "[INFO] Starting Configure-DCV function"
$dcvPath = "Microsoft.PowerShell.Core\Registry::\HKEY_USERS\S-1-5-18\Software\GSettings\com\nicesoftware\dcv"
# Create the required paths
@("$dcvPath\connectivity", "$dcvPath\session-management", "$dcvPath\session-management\automatic-console-session", "$dcvPath\display") | ForEach-Object {
if (-not (Test-Path $_)) {
New-Item -Path $_ -Force | Out-Null
}
}
# Set registry keys
New-ItemProperty -Path "$dcvPath\session-management" -Name create-session -PropertyType DWORD -Value 1 -Force
New-ItemProperty -Path "$dcvPath\session-management\automatic-console-session" -Name owner -Value Administrator -Force
New-ItemProperty -Path "$dcvPath\connectivity" -Name quic-port -PropertyType DWORD -Value $port -Force
New-ItemProperty -Path "$dcvPath\connectivity" -Name web-port -PropertyType DWORD -Value $port -Force
New-ItemProperty -Path "$dcvPath\connectivity" -Name web-url-path -PropertyType String -Value $webURLPath -Force
# Attempt to restart service
if (Get-Service -Name "dcvserver" -ErrorAction SilentlyContinue) {
Restart-Service -Name "dcvserver"
} else {
Write-Output "[WARNING] dcvserver service not found. Ensure the system was restarted properly."
}
Write-Output "[INFO] DCV configuration completed"
Read-Host "[DEBUG] Press Enter to proceed to the next step"
}
# Main Script Execution
Write-Output "[INFO] Starting script"
$VirtualDisplayDriverRequired = [bool](Get-VirtualDisplayDriverRequired)
Set-LocalAdminUser
Download-DCV -VirtualDisplayDriverRequired $VirtualDisplayDriverRequired
Install-DCV -VirtualDisplayDriverRequired $VirtualDisplayDriverRequired
Configure-DCV
Write-Output "[INFO] Script completed"
@@ -0,0 +1,85 @@
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 "admin_password" {
type = string
default = "coderDCV!"
sensitive = true
}
variable "port" {
type = number
description = "The port number for the DCV server."
default = 8443
}
variable "subdomain" {
type = bool
description = "Whether to use a subdomain for the DCV server."
default = true
}
variable "slug" {
type = string
description = "The slug of the web-dcv coder_app resource."
default = "web-dcv"
}
resource "coder_app" "web-dcv" {
agent_id = var.agent_id
slug = var.slug
display_name = "Web DCV"
url = "https://localhost:${var.port}${local.web_url_path}?username=${local.admin_username}&password=${var.admin_password}"
icon = "/icon/dcv.svg"
subdomain = var.subdomain
}
resource "coder_script" "install-dcv" {
agent_id = var.agent_id
display_name = "Install DCV"
icon = "/icon/dcv.svg"
run_on_start = true
script = templatefile("${path.module}/install-dcv.ps1", {
admin_password : var.admin_password,
port : var.port,
web_url_path : local.web_url_path
})
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
locals {
web_url_path = var.subdomain ? "/" : format("/@%s/%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.slug)
admin_username = "Administrator"
}
output "web_url_path" {
value = local.web_url_path
}
output "username" {
value = local.admin_username
}
output "password" {
value = var.admin_password
sensitive = true
}
output "port" {
value = var.port
}
+140
View File
@@ -0,0 +1,140 @@
---
display_name: Amazon Q
description: Run Amazon Q in your workspace to access Amazon's AI coding assistant.
icon: ../../../../.icons/amazon-q.svg
maintainer_github: coder
verified: true
tags: [agent, ai, aws, amazon-q]
---
# Amazon Q
Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's AI coding assistant. This module installs and launches Amazon Q, with support for background operation, task reporting, and custom pre/post install scripts.
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
# Required: see below for how to generate
experiment_auth_tarball = var.amazon_q_auth_tarball
}
```
![Amazon-Q in action](../../.images/amazon-q.png)
## Prerequisites
- You must generate an authenticated Amazon Q tarball on another machine:
```sh
cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0
```
Paste the result into the `experiment_auth_tarball` variable.
- To run in the background, your workspace must have `screen` or `tmux` installed.
<details>
<summary><strong>How to generate the Amazon Q auth tarball (step-by-step)</strong></summary>
**1. Install and authenticate Amazon Q on your local machine:**
- Download and install Amazon Q from the [official site](https://aws.amazon.com/q/developer/).
- Run `q login` and complete the authentication process in your terminal.
**2. Locate your Amazon Q config directory:**
- The config is typically stored at `~/.local/share/amazon-q`.
**3. Generate the tarball:**
- Run the following command in your terminal:
```sh
cd ~/.local/share/amazon-q
tar -c . | zstd | base64 -w 0
```
**4. Copy the output:**
- The command will output a long string. Copy this entire string.
**5. Paste into your Terraform variable:**
- Assign the string to the `experiment_auth_tarball` variable in your Terraform configuration, for example:
```tf
variable "amazon_q_auth_tarball" {
type = string
default = "PASTE_LONG_STRING_HERE"
}
```
**Note:**
- You must re-generate the tarball if you log out or re-authenticate Amazon Q on your local machine.
- This process is required for each user who wants to use Amazon Q in their workspace.
[Reference: Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/generate-docs.html)
</details>
## Examples
### Run Amazon Q in the background with tmux
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
experiment_auth_tarball = var.amazon_q_auth_tarball
experiment_use_tmux = true
}
```
### Enable task reporting (experimental)
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
experiment_auth_tarball = var.amazon_q_auth_tarball
experiment_report_tasks = true
}
```
### Run custom scripts before/after install
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
experiment_auth_tarball = var.amazon_q_auth_tarball
experiment_pre_install_script = "echo Pre-install!"
experiment_post_install_script = "echo Post-install!"
}
```
## Variables
| Name | Required | Default | Description |
| -------------------------------- | -------- | ------------------------ | ----------------------------------------------------------------------------------------------- |
| `agent_id` | Yes | — | The ID of a Coder agent. |
| `experiment_auth_tarball` | Yes | — | Base64-encoded, zstd-compressed tarball of a pre-authenticated Amazon Q config directory. |
| `install_amazon_q` | No | `true` | Whether to install Amazon Q. |
| `amazon_q_version` | No | `latest` | Version to install. |
| `experiment_use_screen` | No | `false` | Use GNU screen for background operation. |
| `experiment_use_tmux` | No | `false` | Use tmux for background operation. |
| `experiment_report_tasks` | No | `false` | Enable task reporting to Coder. |
| `experiment_pre_install_script` | No | `null` | Custom script to run before install. |
| `experiment_post_install_script` | No | `null` | Custom script to run after install. |
| `icon` | No | `/icon/amazon-q.svg` | The icon to use for the app. |
| `folder` | No | `/home/coder` | The folder to run Amazon Q in. |
| `order` | No | `null` | The order determines the position of app in the UI presentation. |
| `system_prompt` | No | See [main.tf](./main.tf) | The system prompt to use for Amazon Q. This should instruct the agent how to do task reporting. |
| `ai_prompt` | No | See [main.tf](./main.tf) | The initial task prompt to send to Amazon Q. |
## Notes
- Only one of `experiment_use_screen` or `experiment_use_tmux` can be true at a time.
- If neither is set, Amazon Q runs in the foreground.
- For more details, see the [main.tf](./main.tf) source.
@@ -0,0 +1,41 @@
import { describe, it, expect } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
findResourceInstance,
} from "~test";
import path from "path";
const moduleDir = path.resolve(__dirname);
const requiredVars = {
agent_id: "dummy-agent-id",
};
describe("amazon-q module", async () => {
await runTerraformInit(moduleDir);
// 1. Required variables
testRequiredVariables(moduleDir, requiredVars);
// 2. coder_script resource is created
it("creates coder_script resource", async () => {
const state = await runTerraformApply(moduleDir, requiredVars);
const scriptResource = findResourceInstance(state, "coder_script");
expect(scriptResource).toBeDefined();
expect(scriptResource.agent_id).toBe(requiredVars.agent_id);
// Optionally, check that the script contains expected lines
expect(scriptResource.script).toContain("Installing Amazon Q");
});
// 3. coder_app resource is created
it("creates coder_app resource", async () => {
const state = await runTerraformApply(moduleDir, requiredVars);
const appResource = findResourceInstance(state, "coder_app", "amazon_q");
expect(appResource).toBeDefined();
expect(appResource.agent_id).toBe(requiredVars.agent_id);
});
// Add more state-based tests as needed
});
+329
View File
@@ -0,0 +1,329 @@
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."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/amazon-q.svg"
}
variable "folder" {
type = string
description = "The folder to run Amazon Q in."
default = "/home/coder"
}
variable "install_amazon_q" {
type = bool
description = "Whether to install Amazon Q."
default = true
}
variable "amazon_q_version" {
type = string
description = "The version of Amazon Q to install."
default = "latest"
}
variable "experiment_use_screen" {
type = bool
description = "Whether to use screen for running Amazon Q in the background."
default = false
}
variable "experiment_use_tmux" {
type = bool
description = "Whether to use tmux instead of screen for running Amazon Q in the background."
default = false
}
variable "experiment_report_tasks" {
type = bool
description = "Whether to enable task reporting."
default = false
}
variable "experiment_pre_install_script" {
type = string
description = "Custom script to run before installing Amazon Q."
default = null
}
variable "experiment_post_install_script" {
type = string
description = "Custom script to run after installing Amazon Q."
default = null
}
variable "experiment_auth_tarball" {
type = string
description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/amazon-q directory. After running `q login` on another machine, you may generate it with: `cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0`"
default = "tarball"
}
variable "system_prompt" {
type = string
description = "The system prompt to use for Amazon Q. This should instruct the agent how to do task reporting."
default = <<-EOT
You are a helpful Coding assistant. Aim to autonomously investigate
and solve issues the user gives you and test your work, whenever possible.
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
but opt for autonomy.
YOU MUST REPORT ALL TASKS TO CODER.
When reporting tasks, you MUST follow these EXACT instructions:
- IMMEDIATELY report status after receiving ANY user message.
- Be granular. If you are investigating with multiple steps, report each step to coder.
Task state MUST be one of the following:
- Use "state": "working" when actively processing WITHOUT needing additional user input.
- Use "state": "complete" only when finished with a task.
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
Task summaries MUST:
- Include specifics about what you're doing.
- Include clear and actionable steps for the user.
- Be less than 160 characters in length.
EOT
}
variable "ai_prompt" {
type = string
description = "The initial task prompt to send to Amazon Q."
default = "Please help me with my coding tasks. I'll provide specific instructions as needed."
}
locals {
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
# We need to use allowed tools to limit the context Amazon Q receives.
# Amazon Q can't handle big contexts, and the `create_template_version` tool
# has a description that's too long.
mcp_json = <<EOT
{
"mcpServers": {
"coder": {
"command": "coder",
"args": ["exp", "mcp", "server", "--allowed-tools", "coder_report_task"],
"env": {
"CODER_MCP_APP_STATUS_SLUG": "amazon-q"
}
}
}
}
EOT
encoded_mcp_json = base64encode(local.mcp_json)
full_prompt = <<-EOT
${var.system_prompt}
Your first task is:
${var.ai_prompt}
EOT
}
resource "coder_script" "amazon_q" {
agent_id = var.agent_id
display_name = "Amazon Q"
icon = var.icon
script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" >/dev/null 2>&1
}
if [ -n "${local.encoded_pre_install_script}" ]; then
echo "Running pre-install script..."
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
chmod +x /tmp/pre_install.sh
/tmp/pre_install.sh
fi
if [ "${var.install_amazon_q}" = "true" ]; then
echo "Installing Amazon Q..."
PREV_DIR="$PWD"
TMP_DIR="$(mktemp -d)"
cd "$TMP_DIR"
ARCH="$(uname -m)"
case "$ARCH" in
"x86_64")
Q_URL="https://desktop-release.q.us-east-1.amazonaws.com/${var.amazon_q_version}/q-x86_64-linux.zip"
;;
"aarch64"|"arm64")
Q_URL="https://desktop-release.codewhisperer.us-east-1.amazonaws.com/${var.amazon_q_version}/q-aarch64-linux.zip"
;;
*)
echo "Error: Unsupported architecture: $ARCH. Amazon Q only supports x86_64 and arm64."
exit 1
;;
esac
echo "Downloading Amazon Q for $ARCH..."
curl --proto '=https' --tlsv1.2 -sSf "$Q_URL" -o "q.zip"
unzip q.zip
./q/install.sh --no-confirm
cd "$PREV_DIR"
export PATH="$PATH:$HOME/.local/bin"
echo "Installed Amazon Q version: $(q --version)"
fi
echo "Extracting auth tarball..."
PREV_DIR="$PWD"
echo "${var.experiment_auth_tarball}" | base64 -d > /tmp/auth.tar.zst
rm -rf ~/.local/share/amazon-q
mkdir -p ~/.local/share/amazon-q
cd ~/.local/share/amazon-q
tar -I zstd -xf /tmp/auth.tar.zst
rm /tmp/auth.tar.zst
cd "$PREV_DIR"
echo "Extracted auth tarball"
if [ -n "${local.encoded_post_install_script}" ]; then
echo "Running post-install script..."
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
chmod +x /tmp/post_install.sh
/tmp/post_install.sh
fi
if [ "${var.experiment_report_tasks}" = "true" ]; then
echo "Configuring Amazon Q to report tasks via Coder MCP..."
mkdir -p ~/.aws/amazonq
echo "${local.encoded_mcp_json}" | base64 -d > ~/.aws/amazonq/mcp.json
echo "Created the ~/.aws/amazonq/mcp.json configuration file"
fi
if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
echo "Please set only one of them to true."
exit 1
fi
if [ "${var.experiment_use_tmux}" = "true" ]; then
echo "Running Amazon Q in the background with tmux..."
if ! command_exists tmux; then
echo "Error: tmux is not installed. Please install tmux manually."
exit 1
fi
touch "$HOME/.amazon-q.log"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
tmux new-session -d -s amazon-q -c "${var.folder}" "q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log" && exec bash"
tmux send-keys -t amazon-q "${local.full_prompt}"
sleep 5
tmux send-keys -t amazon-q Enter
fi
if [ "${var.experiment_use_screen}" = "true" ]; then
echo "Running Amazon Q in the background..."
if ! command_exists screen; then
echo "Error: screen is not installed. Please install screen manually."
exit 1
fi
touch "$HOME/.amazon-q.log"
if [ ! -f "$HOME/.screenrc" ]; then
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.amazon-q.log"
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
fi
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.amazon-q.log"
echo "multiuser on" >> "$HOME/.screenrc"
fi
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.amazon-q.log"
echo "acladd $(whoami)" >> "$HOME/.screenrc"
fi
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
screen -U -dmS amazon-q bash -c '
cd ${var.folder}
q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log
exec bash
'
# Extremely hacky way to send the prompt to the screen session
# This will be fixed in the future, but `amazon-q` was not sending MCP
# tasks when an initial prompt is provided.
screen -S amazon-q -X stuff "${local.full_prompt}"
sleep 5
screen -S amazon-q -X stuff "^M"
else
if ! command_exists q; then
echo "Error: Amazon Q is not installed. Please enable install_amazon_q or install it manually."
exit 1
fi
fi
EOT
run_on_start = true
}
resource "coder_app" "amazon_q" {
slug = "amazon-q"
display_name = "Amazon Q"
agent_id = var.agent_id
command = <<-EOT
#!/bin/bash
set -e
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
if [ "${var.experiment_use_tmux}" = "true" ]; then
if tmux has-session -t amazon-q 2>/dev/null; then
echo "Attaching to existing Amazon Q tmux session." | tee -a "$HOME/.amazon-q.log"
tmux attach-session -t amazon-q
else
echo "Starting a new Amazon Q tmux session." | tee -a "$HOME/.amazon-q.log"
tmux new-session -s amazon-q -c ${var.folder} "q chat --trust-all-tools | tee -a \"$HOME/.amazon-q.log\"; exec bash"
fi
elif [ "${var.experiment_use_screen}" = "true" ]; then
if screen -list | grep -q "amazon-q"; then
echo "Attaching to existing Amazon Q screen session." | tee -a "$HOME/.amazon-q.log"
screen -xRR amazon-q
else
echo "Starting a new Amazon Q screen session." | tee -a "$HOME/.amazon-q.log"
screen -S amazon-q bash -c 'q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log"; exec bash'
fi
else
cd ${var.folder}
q chat --trust-all-tools
fi
EOT
icon = var.icon
}
@@ -0,0 +1,85 @@
---
display_name: AWS Region
description: A parameter with human region names and icons
icon: ../../../../.icons/aws.svg
maintainer_github: coder
verified: true
tags: [helper, parameter, regions, aws]
---
# AWS Region
A parameter with all AWS regions. This allows developers to select
the region closest to them.
Customize the preselected parameter value:
```tf
module "aws-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aws-region/coder"
version = "1.0.12"
default = "us-east-1"
}
provider "aws" {
region = module.aws_region.value
}
```
![AWS Regions](../../.images/aws-regions.png)
## Examples
### Customize regions
Change the display name and icon for a region using the corresponding maps:
```tf
module "aws-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aws-region/coder"
version = "1.0.12"
default = "ap-south-1"
custom_names = {
"ap-south-1" : "Awesome Mumbai!"
}
custom_icons = {
"ap-south-1" : "/emojis/1f33a.png"
}
}
provider "aws" {
region = module.aws_region.value
}
```
![AWS Custom](../../.images/aws-custom.png)
### Exclude regions
Hide the Asia Pacific regions Seoul and Osaka:
```tf
module "aws-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aws-region/coder"
version = "1.0.12"
exclude = ["ap-northeast-2", "ap-northeast-3"]
}
provider "aws" {
region = module.aws_region.value
}
```
![AWS Exclude](../../.images/aws-exclude.png)
## Related templates
For a complete AWS EC2 template, see the following examples in the [Coder Registry](https://registry.coder.com/).
- [AWS EC2 (Linux)](https://registry.coder.com/templates/aws-linux)
- [AWS EC2 (Windows)](https://registry.coder.com/templates/aws-windows)
@@ -0,0 +1,33 @@
import { describe, expect, it } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
describe("aws-region", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {});
it("default output", async () => {
const state = await runTerraformApply(import.meta.dir, {});
expect(state.outputs.value.value).toBe("");
});
it("customized default", async () => {
const state = await runTerraformApply(import.meta.dir, {
default: "us-west-2",
});
expect(state.outputs.value.value).toBe("us-west-2");
});
it("set custom order for coder_parameter", async () => {
const order = 99;
const state = await runTerraformApply(import.meta.dir, {
coder_parameter_order: order.toString(),
});
expect(state.resources).toHaveLength(1);
expect(state.resources[0].instances[0].attributes.order).toBe(order);
});
});
+199
View File
@@ -0,0 +1,199 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
}
}
}
variable "display_name" {
default = "AWS Region"
description = "The display name of the parameter."
type = string
}
variable "description" {
default = "The region to deploy workspace infrastructure."
description = "The description of the parameter."
type = string
}
variable "default" {
default = ""
description = "The default region to use if no region is specified."
type = string
}
variable "mutable" {
default = false
description = "Whether the parameter can be changed after creation."
type = bool
}
variable "custom_names" {
default = {}
description = "A map of custom display names for region IDs."
type = map(string)
}
variable "custom_icons" {
default = {}
description = "A map of custom icons for region IDs."
type = map(string)
}
variable "exclude" {
default = []
description = "A list of region IDs to exclude."
type = list(string)
}
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
locals {
# This is a static list because the regions don't change _that_
# frequently and including the `aws_regions` data source requires
# the provider, which requires a region.
regions = {
"af-south-1" = {
name = "Africa (Cape Town)"
icon = "/emojis/1f1ff-1f1e6.png"
}
"ap-east-1" = {
name = "Asia Pacific (Hong Kong)"
icon = "/emojis/1f1ed-1f1f0.png"
}
"ap-northeast-1" = {
name = "Asia Pacific (Tokyo)"
icon = "/emojis/1f1ef-1f1f5.png"
}
"ap-northeast-2" = {
name = "Asia Pacific (Seoul)"
icon = "/emojis/1f1f0-1f1f7.png"
}
"ap-northeast-3" = {
name = "Asia Pacific (Osaka)"
icon = "/emojis/1f1ef-1f1f5.png"
}
"ap-south-1" = {
name = "Asia Pacific (Mumbai)"
icon = "/emojis/1f1ee-1f1f3.png"
}
"ap-south-2" = {
name = "Asia Pacific (Hyderabad)"
icon = "/emojis/1f1ee-1f1f3.png"
}
"ap-southeast-1" = {
name = "Asia Pacific (Singapore)"
icon = "/emojis/1f1f8-1f1ec.png"
}
"ap-southeast-2" = {
name = "Asia Pacific (Sydney)"
icon = "/emojis/1f1e6-1f1fa.png"
}
"ap-southeast-3" = {
name = "Asia Pacific (Jakarta)"
icon = "/emojis/1f1ee-1f1e9.png"
}
"ap-southeast-4" = {
name = "Asia Pacific (Melbourne)"
icon = "/emojis/1f1e6-1f1fa.png"
}
"ca-central-1" = {
name = "Canada (Central)"
icon = "/emojis/1f1e8-1f1e6.png"
}
"ca-west-1" = {
name = "Canada West (Calgary)"
icon = "/emojis/1f1e8-1f1e6.png"
}
"eu-central-1" = {
name = "EU (Frankfurt)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-central-2" = {
name = "Europe (Zurich)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-north-1" = {
name = "EU (Stockholm)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-south-1" = {
name = "Europe (Milan)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-south-2" = {
name = "Europe (Spain)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-west-1" = {
name = "EU (Ireland)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-west-2" = {
name = "EU (London)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-west-3" = {
name = "EU (Paris)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"il-central-1" = {
name = "Israel (Tel Aviv)"
icon = "/emojis/1f1ee-1f1f1.png"
}
"me-south-1" = {
name = "Middle East (Bahrain)"
icon = "/emojis/1f1e7-1f1ed.png"
}
"sa-east-1" = {
name = "South America (São Paulo)"
icon = "/emojis/1f1e7-1f1f7.png"
}
"us-east-1" = {
name = "US East (N. Virginia)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"us-east-2" = {
name = "US East (Ohio)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"us-west-1" = {
name = "US West (N. California)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"us-west-2" = {
name = "US West (Oregon)"
icon = "/emojis/1f1fa-1f1f8.png"
}
}
}
data "coder_parameter" "region" {
name = "aws_region"
display_name = var.display_name
description = var.description
default = var.default == "" ? null : var.default
order = var.coder_parameter_order
mutable = var.mutable
dynamic "option" {
for_each = { for k, v in local.regions : k => v if !(contains(var.exclude, k)) }
content {
name = try(var.custom_names[option.key], option.value.name)
icon = try(var.custom_icons[option.key], option.value.icon)
value = option.key
}
}
}
output "value" {
value = data.coder_parameter.region.value
}
@@ -0,0 +1,84 @@
---
display_name: Azure Region
description: A parameter with human region names and icons
icon: ../../../../.icons/azure.svg
maintainer_github: coder
verified: true
tags: [helper, parameter, azure, regions]
---
# Azure Region
This module adds a parameter with all Azure regions, allowing developers to select the region closest to them.
```tf
module "azure_region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/azure-region/coder"
version = "1.0.12"
default = "eastus"
}
resource "azurem_resource_group" "example" {
location = module.azure_region.value
}
```
![Azure Region Default](../../.images/azure-default.png)
## Examples
### Customize existing regions
Change the display name and icon for a region using the corresponding maps:
```tf
module "azure-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/azure-region/coder"
version = "1.0.12"
custom_names = {
"australia" : "Go Australia!"
}
custom_icons = {
"australia" : "/icons/smiley.svg"
}
}
resource "azurerm_resource_group" "example" {
location = module.azure_region.value
}
```
![Azure Region Custom](../../.images/azure-custom.png)
### Exclude Regions
Hide all regions in Australia except australiacentral:
```tf
module "azure-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/azure-region/coder"
version = "1.0.12"
exclude = [
"australia",
"australiacentral2",
"australiaeast",
"australiasoutheast"
]
}
resource "azurerm_resource_group" "example" {
location = module.azure_region.value
}
```
![Azure Exclude](../../.images/azure-exclude.png)
## Related templates
For a complete Azure template, see the following examples in the [Coder Registry](https://registry.coder.com/).
- [Azure VM (Linux)](https://registry.coder.com/templates/azure-linux)
- [Azure VM (Windows)](https://registry.coder.com/templates/azure-windows)
@@ -0,0 +1,33 @@
import { describe, expect, it } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
describe("azure-region", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {});
it("default output", async () => {
const state = await runTerraformApply(import.meta.dir, {});
expect(state.outputs.value.value).toBe("");
});
it("customized default", async () => {
const state = await runTerraformApply(import.meta.dir, {
default: "westus",
});
expect(state.outputs.value.value).toBe("westus");
});
it("set custom order for coder_parameter", async () => {
const order = 99;
const state = await runTerraformApply(import.meta.dir, {
coder_parameter_order: order.toString(),
});
expect(state.resources).toHaveLength(1);
expect(state.resources[0].instances[0].attributes.order).toBe(order);
});
});
+333
View File
@@ -0,0 +1,333 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.11"
}
}
}
variable "display_name" {
default = "Azure Region"
description = "The display name of the Coder parameter."
type = string
}
variable "description" {
default = "The region where your workspace will live."
description = "Description of the Coder parameter."
}
variable "default" {
default = ""
description = "The default region to use if no region is specified."
type = string
}
variable "mutable" {
default = false
description = "Whether the parameter can be changed after creation."
type = bool
}
variable "custom_names" {
default = {}
description = "A map of custom display names for region IDs."
type = map(string)
}
variable "custom_icons" {
default = {}
description = "A map of custom icons for region IDs."
type = map(string)
}
variable "exclude" {
default = []
description = "A list of region IDs to exclude."
type = list(string)
}
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
locals {
# Note: Options are limited to 64 regions, some redundant regions have been removed.
all_regions = {
"australia" = {
name = "Australia"
icon = "/emojis/1f1e6-1f1fa.png"
}
"australiacentral" = {
name = "Australia Central"
icon = "/emojis/1f1e6-1f1fa.png"
}
"australiacentral2" = {
name = "Australia Central 2"
icon = "/emojis/1f1e6-1f1fa.png"
}
"australiaeast" = {
name = "Australia (New South Wales)"
icon = "/emojis/1f1e6-1f1fa.png"
}
"australiasoutheast" = {
name = "Australia Southeast"
icon = "/emojis/1f1e6-1f1fa.png"
}
"brazil" = {
name = "Brazil"
icon = "/emojis/1f1e7-1f1f7.png"
}
"brazilsouth" = {
name = "Brazil (Sao Paulo)"
icon = "/emojis/1f1e7-1f1f7.png"
}
"brazilsoutheast" = {
name = "Brazil Southeast"
icon = "/emojis/1f1e7-1f1f7.png"
}
"brazilus" = {
name = "Brazil US"
icon = "/emojis/1f1e7-1f1f7.png"
}
"canada" = {
name = "Canada"
icon = "/emojis/1f1e8-1f1e6.png"
}
"canadacentral" = {
name = "Canada (Toronto)"
icon = "/emojis/1f1e8-1f1e6.png"
}
"canadaeast" = {
name = "Canada East"
icon = "/emojis/1f1e8-1f1e6.png"
}
"centralindia" = {
name = "India (Pune)"
icon = "/emojis/1f1ee-1f1f3.png"
}
"centralus" = {
name = "US (Iowa)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"eastasia" = {
name = "East Asia (Hong Kong)"
icon = "/emojis/1f1f0-1f1f7.png"
}
"eastus" = {
name = "US (Virginia)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"eastus2" = {
name = "US (Virginia) 2"
icon = "/emojis/1f1fa-1f1f8.png"
}
"europe" = {
name = "Europe"
icon = "/emojis/1f30d.png"
}
"france" = {
name = "France"
icon = "/emojis/1f1eb-1f1f7.png"
}
"francecentral" = {
name = "France (Paris)"
icon = "/emojis/1f1eb-1f1f7.png"
}
"francesouth" = {
name = "France South"
icon = "/emojis/1f1eb-1f1f7.png"
}
"germany" = {
name = "Germany"
icon = "/emojis/1f1e9-1f1ea.png"
}
"germanynorth" = {
name = "Germany North"
icon = "/emojis/1f1e9-1f1ea.png"
}
"germanywestcentral" = {
name = "Germany (Frankfurt)"
icon = "/emojis/1f1e9-1f1ea.png"
}
"india" = {
name = "India"
icon = "/emojis/1f1ee-1f1f3.png"
}
"japan" = {
name = "Japan"
icon = "/emojis/1f1ef-1f1f5.png"
}
"japaneast" = {
name = "Japan (Tokyo)"
icon = "/emojis/1f1ef-1f1f5.png"
}
"japanwest" = {
name = "Japan West"
icon = "/emojis/1f1ef-1f1f5.png"
}
"jioindiacentral" = {
name = "Jio India Central"
icon = "/emojis/1f1ee-1f1f3.png"
}
"jioindiawest" = {
name = "Jio India West"
icon = "/emojis/1f1ee-1f1f3.png"
}
"koreacentral" = {
name = "Korea (Seoul)"
icon = "/emojis/1f1f0-1f1f7.png"
}
"koreasouth" = {
name = "Korea South"
icon = "/emojis/1f1f0-1f1f7.png"
}
"northcentralus" = {
name = "North Central US"
icon = "/emojis/1f1fa-1f1f8.png"
}
"northeurope" = {
name = "Europe (Ireland)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"norway" = {
name = "Norway"
icon = "/emojis/1f1f3-1f1f4.png"
}
"norwayeast" = {
name = "Norway (Oslo)"
icon = "/emojis/1f1f3-1f1f4.png"
}
"norwaywest" = {
name = "Norway West"
icon = "/emojis/1f1f3-1f1f4.png"
}
"qatarcentral" = {
name = "Qatar (Doha)"
icon = "/emojis/1f1f6-1f1e6.png"
}
"singapore" = {
name = "Singapore"
icon = "/emojis/1f1f8-1f1ec.png"
}
"southafrica" = {
name = "South Africa"
icon = "/emojis/1f1ff-1f1e6.png"
}
"southafricanorth" = {
name = "South Africa (Johannesburg)"
icon = "/emojis/1f1ff-1f1e6.png"
}
"southafricawest" = {
name = "South Africa West"
icon = "/emojis/1f1ff-1f1e6.png"
}
"southcentralus" = {
name = "US (Texas)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"southeastasia" = {
name = "Southeast Asia (Singapore)"
icon = "/emojis/1f1f0-1f1f7.png"
}
"southindia" = {
name = "South India"
icon = "/emojis/1f1ee-1f1f3.png"
}
"swedencentral" = {
name = "Sweden (Gävle)"
icon = "/emojis/1f1f8-1f1ea.png"
}
"switzerland" = {
name = "Switzerland"
icon = "/emojis/1f1e8-1f1ed.png"
}
"switzerlandnorth" = {
name = "Switzerland (Zurich)"
icon = "/emojis/1f1e8-1f1ed.png"
}
"switzerlandwest" = {
name = "Switzerland West"
icon = "/emojis/1f1e8-1f1ed.png"
}
"uae" = {
name = "United Arab Emirates"
icon = "/emojis/1f1e6-1f1ea.png"
}
"uaecentral" = {
name = "UAE Central"
icon = "/emojis/1f1e6-1f1ea.png"
}
"uaenorth" = {
name = "UAE (Dubai)"
icon = "/emojis/1f1e6-1f1ea.png"
}
"uk" = {
name = "United Kingdom"
icon = "/emojis/1f1ec-1f1e7.png"
}
"uksouth" = {
name = "UK (London)"
icon = "/emojis/1f1ec-1f1e7.png"
}
"ukwest" = {
name = "UK West"
icon = "/emojis/1f1ec-1f1e7.png"
}
"unitedstates" = {
name = "United States"
icon = "/emojis/1f1fa-1f1f8.png"
}
"westcentralus" = {
name = "West Central US"
icon = "/emojis/1f1fa-1f1f8.png"
}
"westeurope" = {
name = "Europe (Netherlands)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"westindia" = {
name = "West India"
icon = "/emojis/1f1ee-1f1f3.png"
}
"westus" = {
name = "West US"
icon = "/emojis/1f1fa-1f1f8.png"
}
"westus2" = {
name = "US (Washington)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"westus3" = {
name = "US (Arizona)"
icon = "/emojis/1f1fa-1f1f8.png"
}
}
}
data "coder_parameter" "region" {
name = "azure_region"
display_name = var.display_name
description = var.description
default = var.default == "" ? null : var.default
order = var.coder_parameter_order
mutable = var.mutable
icon = "/icon/azure.png"
dynamic "option" {
for_each = { for k, v in local.all_regions : k => v if !(contains(var.exclude, k)) }
content {
name = try(var.custom_names[option.key], option.value.name)
icon = try(var.custom_icons[option.key], option.value.icon)
value = option.key
}
}
}
output "value" {
value = data.coder_parameter.region.value
}
@@ -0,0 +1,114 @@
---
display_name: Claude Code
description: Run Claude Code in your workspace
icon: ../../../../.icons/claude.svg
maintainer_github: coder
verified: true
tags: [agent, claude-code]
---
# Claude Code
Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) agent in your workspace to generate code and perform tasks.
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "1.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
claude_code_version = "latest"
}
```
## Prerequisites
- Node.js and npm must be installed in your workspace to install Claude Code
- Either `screen` or `tmux` must be installed in your workspace to run Claude Code in the background
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces.
## Examples
### Run in the background and report tasks (Experimental)
> This functionality is in early access as of Coder v2.21 and is still evolving.
> For now, we recommend testing it in a demo or staging environment,
> rather than deploying to production
>
> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents)
>
> Join our [Discord channel](https://discord.gg/coder) or
> [contact us](https://coder.com/contact) to get help or share feedback.
Your workspace must have either `screen` or `tmux` installed to use this.
```tf
variable "anthropic_api_key" {
type = string
description = "The Anthropic API key"
sensitive = true
}
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/coder-login/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
}
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Write a prompt for Claude Code"
mutable = true
}
# Set the prompt and system prompt for Claude Code via environment variables
resource "coder_agent" "main" {
# ...
env = {
CODER_MCP_CLAUDE_API_KEY = var.anthropic_api_key # or use a coder_parameter
CODER_MCP_CLAUDE_TASK_PROMPT = data.coder_parameter.ai_prompt.value
CODER_MCP_APP_STATUS_SLUG = "claude-code"
CODER_MCP_CLAUDE_SYSTEM_PROMPT = <<-EOT
You are a helpful assistant that can help with code.
EOT
}
}
module "claude-code" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/claude-code/coder"
version = "1.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
claude_code_version = "0.2.57"
# Enable experimental features
experiment_use_screen = true # Or use experiment_use_tmux = true to use tmux instead
experiment_report_tasks = true
}
```
## Run standalone
Run Claude Code as a standalone app in your workspace. This will install Claude Code and run it directly without using screen or any task reporting to the Coder UI.
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "1.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
claude_code_version = "latest"
# Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL
icon = "https://registry.npmmirror.com/@lobehub/icons-static-png/1.24.0/files/dark/claude-color.png"
}
```
+249
View File
@@ -0,0 +1,249 @@
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."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/claude.svg"
}
variable "folder" {
type = string
description = "The folder to run Claude Code in."
default = "/home/coder"
}
variable "install_claude_code" {
type = bool
description = "Whether to install Claude Code."
default = true
}
variable "claude_code_version" {
type = string
description = "The version of Claude Code to install."
default = "latest"
}
variable "experiment_use_screen" {
type = bool
description = "Whether to use screen for running Claude Code in the background."
default = false
}
variable "experiment_use_tmux" {
type = bool
description = "Whether to use tmux instead of screen for running Claude Code in the background."
default = false
}
variable "experiment_report_tasks" {
type = bool
description = "Whether to enable task reporting."
default = false
}
variable "experiment_pre_install_script" {
type = string
description = "Custom script to run before installing Claude Code."
default = null
}
variable "experiment_post_install_script" {
type = string
description = "Custom script to run after installing Claude Code."
default = null
}
locals {
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
}
# Install and Initialize Claude Code
resource "coder_script" "claude_code" {
agent_id = var.agent_id
display_name = "Claude Code"
icon = var.icon
script = <<-EOT
#!/bin/bash
set -e
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Run pre-install script if provided
if [ -n "${local.encoded_pre_install_script}" ]; then
echo "Running pre-install script..."
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
chmod +x /tmp/pre_install.sh
/tmp/pre_install.sh
fi
# Install Claude Code if enabled
if [ "${var.install_claude_code}" = "true" ]; then
if ! command_exists npm; then
echo "Error: npm is not installed. Please install Node.js and npm first."
exit 1
fi
echo "Installing Claude Code..."
npm install -g @anthropic-ai/claude-code@${var.claude_code_version}
fi
# Run post-install script if provided
if [ -n "${local.encoded_post_install_script}" ]; then
echo "Running post-install script..."
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
chmod +x /tmp/post_install.sh
/tmp/post_install.sh
fi
if [ "${var.experiment_report_tasks}" = "true" ]; then
echo "Configuring Claude Code to report tasks via Coder MCP..."
coder exp mcp configure claude-code ${var.folder}
fi
# Handle terminal multiplexer selection (tmux or screen)
if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
echo "Please set only one of them to true."
exit 1
fi
# Run with tmux if enabled
if [ "${var.experiment_use_tmux}" = "true" ]; then
echo "Running Claude Code in the background with tmux..."
# Check if tmux is installed
if ! command_exists tmux; then
echo "Error: tmux is not installed. Please install tmux manually."
exit 1
fi
touch "$HOME/.claude-code.log"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
# Create a new tmux session in detached mode
tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions"
# Send the prompt to the tmux session if needed
if [ -n "$CODER_MCP_CLAUDE_TASK_PROMPT" ]; then
tmux send-keys -t claude-code "$CODER_MCP_CLAUDE_TASK_PROMPT"
sleep 5
tmux send-keys -t claude-code Enter
fi
fi
# Run with screen if enabled
if [ "${var.experiment_use_screen}" = "true" ]; then
echo "Running Claude Code in the background..."
# Check if screen is installed
if ! command_exists screen; then
echo "Error: screen is not installed. Please install screen manually."
exit 1
fi
touch "$HOME/.claude-code.log"
# Ensure the screenrc exists
if [ ! -f "$HOME/.screenrc" ]; then
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.claude-code.log"
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
fi
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log"
echo "multiuser on" >> "$HOME/.screenrc"
fi
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log"
echo "acladd $(whoami)" >> "$HOME/.screenrc"
fi
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
screen -U -dmS claude-code bash -c '
cd ${var.folder}
claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"
exec bash
'
# Extremely hacky way to send the prompt to the screen session
# This will be fixed in the future, but `claude` was not sending MCP
# tasks when an initial prompt is provided.
screen -S claude-code -X stuff "$CODER_MCP_CLAUDE_TASK_PROMPT"
sleep 5
screen -S claude-code -X stuff "^M"
else
# Check if claude is installed before running
if ! command_exists claude; then
echo "Error: Claude Code is not installed. Please enable install_claude_code or install it manually."
exit 1
fi
fi
EOT
run_on_start = true
}
resource "coder_app" "claude_code" {
slug = "claude-code"
display_name = "Claude Code"
agent_id = var.agent_id
command = <<-EOT
#!/bin/bash
set -e
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
if [ "${var.experiment_use_tmux}" = "true" ]; then
if tmux has-session -t claude-code 2>/dev/null; then
echo "Attaching to existing Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
tmux attach-session -t claude-code
else
echo "Starting a new Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
tmux new-session -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions | tee -a \"$HOME/.claude-code.log\"; exec bash"
fi
elif [ "${var.experiment_use_screen}" = "true" ]; then
if screen -list | grep -q "claude-code"; then
echo "Attaching to existing Claude Code screen session." | tee -a "$HOME/.claude-code.log"
screen -xRR claude-code
else
echo "Starting a new Claude Code screen session." | tee -a "$HOME/.claude-code.log"
screen -S claude-code bash -c 'claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"; exec bash'
fi
else
cd ${var.folder}
claude
fi
EOT
icon = var.icon
}
@@ -0,0 +1,115 @@
---
display_name: code-server
description: VS Code in the browser
icon: ../../../../.icons/code.svg
maintainer_github: coder
verified: true
tags: [helper, ide, web]
---
# code-server
Automatically install [code-server](https://github.com/coder/code-server) in a workspace, create an app to access it via the dashboard, install extensions, and pre-configure editor settings.
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.2.0"
agent_id = coder_agent.example.id
}
```
![Screenshot 1](https://github.com/coder/code-server/raw/main/docs/assets/screenshot-1.png?raw=true)
## Examples
### Pin Versions
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.2.0"
agent_id = coder_agent.example.id
install_version = "4.8.3"
}
```
### Pre-install Extensions
Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.2.0"
agent_id = coder_agent.example.id
extensions = [
"dracula-theme.theme-dracula"
]
}
```
Enter the `<author>.<name>` into the extensions array and code-server will automatically install on start.
### Pre-configure Settings
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-json-file) file:
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.2.0"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
"workbench.colorTheme" = "Dracula"
}
}
```
### Install multiple extensions
Just run code-server in the background, don't fetch it from GitHub:
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.2.0"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
}
```
### Offline and Use Cached Modes
By default the module looks for code-server at `/tmp/code-server` but this can be changed with `install_prefix`.
Run an existing copy of code-server if found, otherwise download from GitHub:
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.2.0"
agent_id = coder_agent.example.id
use_cached = true
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
}
```
Just run code-server in the background, don't fetch it from GitHub:
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.2.0"
agent_id = coder_agent.example.id
offline = true
}
```

Some files were not shown because too many files have changed in this diff Show More