Compare commits

..

15 Commits

Author SHA1 Message Date
Michael Smith 2e5f3bfea1 refactor: rename files 2025-05-02 15:20:57 +00:00
Michael Smith cc51af1b6e Merge branch 'main' into mes/module-validation 2025-05-02 15:19:43 +00:00
Michael Smith d4d307451f Merge pull request #18 from coder/mes/tighten-schema
chore: update README files and validation to reflect new requirements
2025-04-28 15:41:27 -04:00
Michael Smith 7e8d4b7f27 fix: make Github field optional 2025-04-28 19:37:06 +00:00
Michael Smith c731a5c64e fix: remove employer field entirely 2025-04-28 19:24:34 +00:00
Michael Smith 45640689e4 fix: update README files 2025-04-28 19:22:06 +00:00
Michael Smith 31c69e28a8 refactor: make code easier to read 2025-04-18 21:43:52 +00:00
Michael Smith fdc0f98c0f docs: update typo 2025-04-18 21:29:42 +00:00
Michael Smith eea1e059f0 docs: add missing words to comment 2025-04-18 21:29:21 +00:00
Michael Smith 1dcc645b14 fix: improve check for user namespace subdirectories 2025-04-18 21:26:26 +00:00
Michael Smith 812dd8faaf fix: update repo structure checks 2025-04-18 21:14:37 +00:00
Michael Smith 52c1f45da6 refactor: remove directory validation from contributors file 2025-04-18 20:51:18 +00:00
Michael Smith 1a5c102c2f refactor: more domain splitting 2025-04-18 20:49:04 +00:00
Michael Smith 36d33895b7 refactor: start splitting up files 2025-04-18 20:05:05 +00:00
Michael Smith 404b1e0a46 refactor: update file structure to reflect new changes 2025-04-18 19:16:35 +00:00
409 changed files with 1356 additions and 32736 deletions
-31
View File
@@ -1,31 +0,0 @@
Closes #
## Description
<!-- Briefly describe what this PR does and why -->
## Type of Change
- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other
## Module Information
<!-- Delete this section if not applicable -->
**Path:** `registry/[namespace]/modules/[module-name]`
**New version:** `v1.0.0`
**Breaking change:** [ ] Yes [ ] No
## Testing & Validation
- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] Changes tested locally
## Related Issues
<!-- Link related issues or write "None" if not applicable -->
-1
View File
@@ -1 +0,0 @@
../AGENTS.md
-4
View File
@@ -4,7 +4,3 @@ updates:
directory: "/"
schedule:
interval: "weekly"
groups:
github-actions:
patterns:
- "*"
-238
View File
@@ -1,238 +0,0 @@
#!/bin/bash
# Version Bump Script
# Usage: ./version-bump.sh <bump_type> [base_ref]
# bump_type: patch, minor, or major
# base_ref: base reference for diff (default: origin/main)
set -euo pipefail
usage() {
echo "Usage: $0 <bump_type> [base_ref]"
echo " bump_type: patch, minor, or major"
echo " base_ref: base reference for diff (default: origin/main)"
echo ""
echo "Examples:"
echo " $0 patch # Update versions with patch bump"
echo " $0 minor # Update versions with minor bump"
echo " $0 major # Update versions with major bump"
exit 1
}
validate_version() {
local version="$1"
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "❌ Invalid version format: '$version'. Expected X.Y.Z format." >&2
return 1
fi
return 0
}
bump_version() {
local current_version="$1"
local bump_type="$2"
IFS='.' read -r major minor patch <<< "$current_version"
if ! [[ "$major" =~ ^[0-9]+$ ]] || ! [[ "$minor" =~ ^[0-9]+$ ]] || ! [[ "$patch" =~ ^[0-9]+$ ]]; then
echo "❌ Version components must be numeric: major='$major' minor='$minor' patch='$patch'" >&2
return 1
fi
case "$bump_type" in
"patch")
echo "$major.$minor.$((patch + 1))"
;;
"minor")
echo "$major.$((minor + 1)).0"
;;
"major")
echo "$((major + 1)).0.0"
;;
*)
echo "❌ Invalid bump type: '$bump_type'. Expected patch, minor, or major." >&2
return 1
;;
esac
}
update_readme_version() {
local readme_path="$1"
local namespace="$2"
local module_name="$3"
local new_version="$4"
if [ ! -f "$readme_path" ]; then
return 1
fi
local module_source="registry.coder.com/${namespace}/${module_name}/coder"
if grep -q "source.*${module_source}" "$readme_path"; then
echo "Updating version references for $namespace/$module_name in $readme_path"
awk -v module_source="$module_source" -v new_version="$new_version" '
/source.*=.*/ {
if ($0 ~ module_source) {
in_target_module = 1
} else {
in_target_module = 0
}
}
/version.*=.*"/ {
if (in_target_module) {
gsub(/version[[:space:]]*=[[:space:]]*"[^"]*"/, "version = \"" new_version "\"")
in_target_module = 0
}
}
{ print }
' "$readme_path" > "${readme_path}.tmp" && mv "${readme_path}.tmp" "$readme_path"
return 0
elif grep -q 'version\s*=\s*"' "$readme_path"; then
echo "⚠️ Found version references but no module source match for $namespace/$module_name"
return 1
fi
return 1
}
main() {
if [ $# -lt 1 ] || [ $# -gt 2 ]; then
usage
fi
local bump_type="$1"
local base_ref="${2:-origin/main}"
case "$bump_type" in
"patch" | "minor" | "major") ;;
*)
echo "❌ Invalid bump type: '$bump_type'. Expected patch, minor, or major." >&2
exit 1
;;
esac
echo "🔍 Detecting modified modules..."
local changed_files
changed_files=$(git diff --name-only "${base_ref}"...HEAD)
local modules
modules=$(echo "$changed_files" | grep -E '^registry/[^/]+/modules/[^/]+/' | cut -d'/' -f1-4 | sort -u)
if [ -z "$modules" ]; then
echo "❌ No modules detected in changes"
exit 1
fi
echo "Found modules:"
echo "$modules"
echo ""
local bumped_modules=""
local updated_readmes=""
local untagged_modules=""
local has_changes=false
while IFS= read -r module_path; do
if [ -z "$module_path" ]; then continue; fi
local namespace
namespace=$(echo "$module_path" | cut -d'/' -f2)
local module_name
module_name=$(echo "$module_path" | cut -d'/' -f4)
echo "📦 Processing: $namespace/$module_name"
local latest_tag
latest_tag=$(git tag -l "release/${namespace}/${module_name}/v*" | sort -V | tail -1)
local readme_path="$module_path/README.md"
local current_version
if [ -z "$latest_tag" ]; then
if [ -f "$readme_path" ] && grep -q 'version\s*=\s*"' "$readme_path"; then
local readme_version
readme_version=$(grep 'version\s*=\s*"' "$readme_path" | head -1 | sed 's/.*version\s*=\s*"\([^"]*\)".*/\1/')
echo "No git tag found, but README shows version: $readme_version"
if ! validate_version "$readme_version"; then
echo "Starting from v1.0.0 instead"
current_version="1.0.0"
else
current_version="$readme_version"
untagged_modules="$untagged_modules\n- $namespace/$module_name (README: v$readme_version)"
fi
else
echo "No existing tags or version references found for $namespace/$module_name, starting from v1.0.0"
current_version="1.0.0"
fi
else
current_version=$(echo "$latest_tag" | sed 's/.*\/v//')
echo "Found git tag: $latest_tag (v$current_version)"
fi
echo "Current version: $current_version"
if ! validate_version "$current_version"; then
exit 1
fi
local new_version
new_version=$(bump_version "$current_version" "$bump_type")
echo "New version: $new_version"
if update_readme_version "$readme_path" "$namespace" "$module_name" "$new_version"; then
updated_readmes="$updated_readmes\n- $namespace/$module_name"
has_changes=true
fi
bumped_modules="$bumped_modules\n- $namespace/$module_name: v$current_version → v$new_version"
echo ""
done <<< "$modules"
# Always run formatter to ensure consistent formatting
echo "🔧 Running formatter to ensure consistent formatting..."
if command -v bun > /dev/null 2>&1; then
bun fmt > /dev/null 2>&1 || echo "⚠️ Warning: bun fmt failed, but continuing..."
else
echo "⚠️ Warning: bun not found, skipping formatting"
fi
echo ""
echo "📋 Summary:"
echo "Bump Type: $bump_type"
echo ""
echo "Modules Updated:"
echo -e "$bumped_modules"
echo ""
if [ -n "$updated_readmes" ]; then
echo "READMEs Updated:"
echo -e "$updated_readmes"
echo ""
fi
if [ -n "$untagged_modules" ]; then
echo "⚠️ Modules Without Git Tags:"
echo -e "$untagged_modules"
echo "These modules were versioned based on README content. Consider creating proper release tags after merging."
echo ""
fi
if [ "$has_changes" = true ]; then
echo "✅ Version bump completed successfully!"
echo "📝 README files have been updated with new versions."
echo ""
echo "Next steps:"
echo "1. Review the changes: git diff"
echo "2. Commit the changes: git add . && git commit -m 'chore: bump module versions ($bump_type)'"
echo "3. Push the changes: git push"
exit 0
else
echo "️ No README files were updated (no version references found matching module sources)."
echo "Version calculations completed, but no files were modified."
exit 0
fi
}
main "$@"
+1 -7
View File
@@ -1,10 +1,4 @@
[default.extend-words]
muc = "muc" # For Munich location code
tyo = "tyo" # For Tokyo location code
Hashi = "Hashi"
HashiCorp = "HashiCorp"
mavrickrishi = "mavrickrishi" # Username
mavrick = "mavrick" # Username
[files]
extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive
HashiCorp = "HashiCorp"
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Run check.sh
run: |
+5 -5
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Set up Terraform
uses: coder/coder/.github/actions/setup-tf@main
- name: Set up Bun
@@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
@@ -48,7 +48,7 @@ jobs:
- name: Validate formatting
run: bun fmt:ci
- name: Check for typos
uses: crate-ci/typos@v1.37.2
uses: crate-ci/typos@v1.31.1
with:
config: .github/typos.toml
validate-readme-files:
@@ -59,9 +59,9 @@ jobs:
needs: validate-style
steps:
- name: Check out code
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version: "1.23.2"
- name: Validate contributors
-42
View File
@@ -1,42 +0,0 @@
name: deploy-registry
on:
schedule:
# Runs at 02:30 UTC Monday through Friday
- cron: "30 2 * * 1-5"
push:
tags:
# Matches release/<namespace>/<resource_name>/<semantic_version>
# (e.g., "release/whizus/exoscale-zone/v1.0.13")
- "release/*/*/v*.*.*"
branches: # Templates get released when merged to main
- main
paths:
- ".github/workflows/deploy-registry.yaml"
- "registry/**/templates/**"
- "registry/**/README.md"
- ".icons/**"
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@v5
- name: Authenticate with Google Cloud
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
with:
workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github
service_account: registry-v2-github@coder-registry-1.iam.gserviceaccount.com
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
- name: Deploy to dev.registry.coder.com
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch main
- name: Deploy to registry.coder.com
run: gcloud builds triggers run 106610ff-41fb-4bd0-90a2-7643583fb9c0 --tag production
-24
View File
@@ -1,24 +0,0 @@
name: golangci-lint
on:
push:
branches:
- main
- master
pull_request:
permissions:
contents: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: stable
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.1
-117
View File
@@ -1,117 +0,0 @@
name: Create Release
on:
push:
tags:
- "release/*/*/v*.*.*"
jobs:
create-release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
- name: Extract tag information
id: tag_info
run: |
TAG=${GITHUB_REF#refs/tags/}
echo "tag=$TAG" >> $GITHUB_OUTPUT
IFS='/' read -ra PARTS <<< "$TAG"
NAMESPACE="${PARTS[1]}"
MODULE="${PARTS[2]}"
VERSION="${PARTS[3]}"
echo "namespace=$NAMESPACE" >> $GITHUB_OUTPUT
echo "module=$MODULE" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "module_path=registry/$NAMESPACE/modules/$MODULE" >> $GITHUB_OUTPUT
RELEASE_TITLE="$NAMESPACE/$MODULE $VERSION"
echo "release_title=$RELEASE_TITLE" >> $GITHUB_OUTPUT
- name: Find previous tag
id: prev_tag
env:
NAMESPACE: ${{ steps.tag_info.outputs.namespace }}
MODULE: ${{ steps.tag_info.outputs.module }}
CURRENT_TAG: ${{ steps.tag_info.outputs.tag }}
run: |
PREV_TAG=$(git tag -l "release/$NAMESPACE/$MODULE/v*" | sort -V | grep -B1 "$CURRENT_TAG" | head -1)
if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "$CURRENT_TAG" ]; then
echo "No previous tag found, using initial commit"
PREV_TAG=$(git rev-list --max-parents=0 HEAD)
fi
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
echo "Previous tag: $PREV_TAG"
- name: Generate changelog
id: changelog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MODULE_PATH: ${{ steps.tag_info.outputs.module_path }}
PREV_TAG: ${{ steps.prev_tag.outputs.prev_tag }}
CURRENT_TAG: ${{ steps.tag_info.outputs.tag }}
run: |
echo "Generating changelog for $MODULE_PATH between $PREV_TAG and $CURRENT_TAG"
COMMITS=$(git log --oneline --no-merges "$PREV_TAG..$CURRENT_TAG" -- "$MODULE_PATH")
if [ -z "$COMMITS" ]; then
echo "No commits found for this module"
echo "changelog=No changes found for this module." >> $GITHUB_OUTPUT
exit 0
fi
if [[ "$PREV_TAG" == release/* ]]; then
FULL_CHANGELOG=$(gh api repos/:owner/:repo/releases/generate-notes \
--field tag_name="$CURRENT_TAG" \
--field previous_tag_name="$PREV_TAG" \
--jq '.body')
else
echo "New module detected, skipping GitHub API"
FULL_CHANGELOG=""
fi
MODULE_COMMIT_SHAS=$(git log --format="%H" --no-merges "$PREV_TAG..$CURRENT_TAG" -- "$MODULE_PATH")
FILTERED_CHANGELOG="## What's Changed\n\n"
for sha in $MODULE_COMMIT_SHAS; do
SHORT_SHA=${sha:0:7}
COMMIT_LINES=$(echo "$FULL_CHANGELOG" | grep -E "$SHORT_SHA|$(git log --format='%s' -n 1 $sha)" || true)
if [ -n "$COMMIT_LINES" ]; then
FILTERED_CHANGELOG="${FILTERED_CHANGELOG}${COMMIT_LINES}\n"
else
COMMIT_MSG=$(git log --format="%s" -n 1 $sha)
AUTHOR=$(gh api repos/:owner/:repo/commits/$sha --jq '.author.login // .commit.author.name')
FILTERED_CHANGELOG="${FILTERED_CHANGELOG}* $COMMIT_MSG by @$AUTHOR\n"
fi
done
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo -e "$FILTERED_CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ steps.tag_info.outputs.tag }}
RELEASE_TITLE: ${{ steps.tag_info.outputs.release_title }}
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
run: |
gh release create "$TAG_NAME" \
--title "$RELEASE_TITLE" \
--notes "$CHANGELOG"
-120
View File
@@ -1,120 +0,0 @@
name: Version Bump
on:
pull_request:
types: [labeled]
paths:
- "registry/**/modules/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
version-bump:
if: github.event.label.name == 'version:patch' || github.event.label.name == 'version:minor' || github.event.label.name == 'version:major'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Set up Terraform
uses: coder/coder/.github/actions/setup-tf@main
- name: Install dependencies
run: bun install
- name: Extract bump type from label
id: bump-type
run: |
case "${{ github.event.label.name }}" in
"version:patch")
echo "type=patch" >> $GITHUB_OUTPUT
;;
"version:minor")
echo "type=minor" >> $GITHUB_OUTPUT
;;
"version:major")
echo "type=major" >> $GITHUB_OUTPUT
;;
*)
echo "Invalid version label: ${{ github.event.label.name }}"
exit 1
;;
esac
- name: Check version bump requirements
id: version-check
run: |
output_file=$(mktemp)
if ./.github/scripts/version-bump.sh "${{ steps.bump-type.outputs.type }}" origin/main > "$output_file" 2>&1; then
echo "Script completed successfully"
else
echo "Script failed"
cat "$output_file"
exit 1
fi
{
echo "output<<EOF"
cat "$output_file"
echo "EOF"
} >> $GITHUB_OUTPUT
cat "$output_file"
if git diff --quiet; then
echo "versions_up_to_date=true" >> $GITHUB_OUTPUT
echo "✅ All module versions are already up to date"
else
echo "versions_up_to_date=false" >> $GITHUB_OUTPUT
echo "❌ Module versions need to be updated"
echo "Files that would be changed:"
git diff --name-only
echo ""
echo "Diff preview:"
git diff
git checkout .
git clean -fd
exit 1
fi
- name: Comment on PR - Failure
if: failure() && steps.version-check.outputs.versions_up_to_date == 'false'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `${{ steps.version-check.outputs.output }}`;
const bumpType = `${{ steps.bump-type.outputs.type }}`;
let comment = `## ❌ Version Bump Validation Failed\n\n`;
comment += `**Bump Type:** \`${bumpType}\`\n\n`;
comment += `Module versions need to be updated but haven't been bumped yet.\n\n`;
comment += `**Required Actions:**\n`;
comment += `1. Run the version bump script locally: \`./.github/scripts/version-bump.sh ${bumpType}\`\n`;
comment += `2. Commit the changes: \`git add . && git commit -m "chore: bump module versions (${bumpType})"\`\n`;
comment += `3. Push the changes: \`git push\`\n\n`;
comment += `### Script Output:\n\`\`\`\n${output}\n\`\`\`\n\n`;
comment += `> Please update the module versions and push the changes to continue.`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
-3
View File
@@ -145,6 +145,3 @@ dist
# Generated credentials from google-github-actions/auth
gha-creds-*.json
# IDEs
.idea
-213
View File
@@ -1,213 +0,0 @@
version: "2"
linters:
default: none
enable:
- asciicheck
- bidichk
- bodyclose
- dogsled
- dupl
- errcheck
- errname
- errorlint
- exhaustruct
- forcetypeassert
- gocognit
- gocritic
- godot
- gomodguard
- gosec
- govet
- importas
- ineffassign
- makezero
- misspell
- nestif
- nilnil
# - noctx
# - paralleltest
- revive
- staticcheck
# - tparallel
- unconvert
- unused
settings:
dupl:
threshold: 412
godot:
scope: all
capital: true
exhaustruct:
include:
- httpmw\.\w+
- github.com/coder/coder/v2/coderd/database\.[^G][^e][^t]\w+Params
gocognit:
min-complexity: 300
goconst:
min-len: 4
min-occurrences: 3
gocritic:
enabled-checks:
- badLock
- badRegexp
- boolExprSimplify
- builtinShadow
- builtinShadowDecl
- commentedOutImport
- deferUnlambda
- dupImport
- dynamicFmtString
- emptyDecl
- emptyFallthrough
- emptyStringTest
- evalOrder
- externalErrorReassign
- filepathJoin
- hexLiteral
- httpNoBody
- importShadow
- indexAlloc
- initClause
- methodExprCall
- nestingReduce
- nilValReturn
- preferFilepathJoin
- rangeAppendAll
- regexpPattern
- redundantSprint
- regexpSimplify
- ruleguard
- sliceClear
- sortSlice
- sprintfQuotedString
- sqlQuery
- stringConcatSimplify
- stringXbytes
- todoCommentWithoutDetail
- tooManyResultsChecker
- truncateCmp
- typeAssertChain
- typeDefFirst
- unlabelStmt
- weakCond
- whyNoLint
settings:
ruleguard:
failOn: all
rules: ${base-path}/scripts/rules.go
gosec:
excludes:
- G601
govet:
disable:
- loopclosure
importas:
no-unaliased: true
misspell:
locale: US
ignore-rules:
- trialer
nestif:
min-complexity: 20
revive:
severity: warning
rules:
- name: atomic
- name: bare-return
- name: blank-imports
- name: bool-literal-in-expr
- name: call-to-gc
- name: confusing-results
- name: constant-logical-expr
- name: context-as-argument
- name: context-keys-type
# - name: deep-exit
- name: defer
- name: dot-imports
- name: duplicated-imports
- name: early-return
- name: empty-block
- name: empty-lines
- name: error-naming
- name: error-return
- name: error-strings
- name: errorf
- name: exported
- name: flag-parameter
- name: get-return
- name: identical-branches
- name: if-return
- name: import-shadowing
- name: increment-decrement
- name: indent-error-flow
- name: modifies-value-receiver
- name: package-comments
- name: range
- name: receiver-naming
- name: redefines-builtin-id
- name: string-of-int
- name: struct-tag
- name: superfluous-else
- name: time-naming
- name: unconditional-recursion
- name: unexported-naming
- name: unexported-return
- name: unhandled-error
- name: unnecessary-stmt
- name: unreachable-code
- name: unused-parameter
- name: unused-receiver
- name: var-declaration
- name: var-naming
- name: waitgroup-by-value
staticcheck:
checks:
- all
- SA4006 # Detects redundant assignments
- SA4009 # Detects redundant variable declarations
- SA1019
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- errcheck
- exhaustruct
- forcetypeassert
path: _test\.go
- linters:
- exhaustruct
path: scripts/*
- linters:
- ALL
path: scripts/rules.go
paths:
- scripts/rules.go
- coderd/database/dbmem
- node_modules
- .git
- third_party$
- builtin$
- examples$
issues:
max-issues-per-linter: 0
max-same-issues: 0
fix: true
formatters:
enable:
- goimports
- gofmt
exclusions:
generated: lax
paths:
- scripts/rules.go
- coderd/database/dbmem
- node_modules
- .git
- third_party$
- builtin$
- examples$
-3
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

+14 -13
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

-4
View File
@@ -1,4 +0,0 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Akamai</title>
<path d="M13.0548 0C6.384 0 .961 5.3802.961 12.0078.961 18.6354 6.3698 24 13.0548 24c.6168 0 .6454-.3572.0859-.5293-4.9349-1.5063-8.5352-6.069-8.5352-11.4629 0-5.4656 3.6725-10.0706 8.6934-11.5195C13.8153.3448 13.6716 0 13.0548 0Zm2.3242 1.8223c-5.2648 0-9.5254 4.2606-9.5254 9.5254 0 1.2193.2285 2.3818.6445 3.4433.1722.459.4454.4584.4024.0137-.0287-.3156-.0567-.6447-.0567-.9746 0-5.2648 4.2606-9.5254 9.5254-9.5254 4.9779 0 6.4698 2.2235 6.6563 2.08.2008-.1577-1.808-4.5624-7.6465-4.5624zm.4687 4.0703c-1.8622.0592-3.651.7168-5.1035 1.8554-.2582.2009-.1567.3284.1445.1993 2.4675-1.076 5.5812-1.1046 8.6368-.043 2.0514.7173 3.2413 1.7364 3.3418 1.6934.1578-.0718-1.1915-2.2226-3.6446-3.1407-1.1135-.4196-2.2576-.6-3.375-.5644z" fill="#0096D6"/>
</svg>

Before

Width:  |  Height:  |  Size: 852 B

-13
View File
@@ -1,13 +0,0 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M68.2075 2.20837C63.1244 -0.736125 56.8639 -0.736125 51.7925 2.20837L14.1826 23.9766C9.09954 26.9211 5.9751 32.3544 5.9751 38.2317V81.7683C5.9751 87.6456 9.09954 93.0789 14.1826 96.0234L51.7925 117.792C56.8755 120.736 63.1361 120.736 68.2075 117.792L105.817 96.0234C110.9 93.0789 114.025 87.6456 114.025 81.7683V38.2317C114.025 32.3544 110.9 26.9211 105.817 23.9766L68.2075 2.20837Z" fill="url(#paint0_linear_292_106)"/>
<path d="M53.6113 18.0993L26.937 33.5346C22.8682 35.8832 20.3733 40.2298 20.3733 44.9387V75.8208C20.3733 80.5297 22.8798 84.8647 26.937 87.2249L53.6113 102.66C57.6801 105.009 62.6815 105.009 66.7503 102.66L93.4247 87.2249C97.4934 84.8763 99.9883 80.5297 99.9883 75.8208V44.9387C99.9883 40.2298 97.4818 35.8949 93.4247 33.5346L66.7503 18.0993C62.6815 15.7507 57.6801 15.7507 53.6113 18.0993ZM57.7151 25.4138C59.7436 24.2337 62.2502 24.2337 64.2787 25.4138L89.7056 40.1246C91.7342 41.3048 92.9933 43.4664 92.9933 45.8267V75.26C92.9933 77.6086 91.7458 79.7819 89.7056 80.962L64.2787 95.6728C62.2502 96.853 59.7436 96.853 57.7151 95.6728L32.2881 80.962C30.2596 79.7819 29.0005 77.6203 29.0005 75.26V45.8267C29.0005 43.4781 30.2479 41.3048 32.2881 40.1246L57.7151 25.4138ZM60.5947 53.0127C60.0817 52.7206 59.4638 52.7206 58.9508 53.0127L53.4831 56.1792C52.9701 56.4713 52.667 57.0205 52.667 57.6047V63.9377C52.667 64.5219 52.9818 65.0711 53.4831 65.3632L58.9508 68.5297C59.4638 68.8218 60.0817 68.8218 60.5947 68.5297L66.0624 65.3632C66.5754 65.0711 66.8785 64.5219 66.8785 63.9377V57.6047C66.8785 57.0205 66.5638 56.4713 66.0624 56.1792L60.5947 53.0127ZM64.6984 59.1237L61.4108 65.7021L92.6086 83.8014L95.8962 77.223L64.6984 59.1237Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_292_106" x1="86.3055" y1="14.2705" x2="41.9042" y2="99.5712" gradientUnits="userSpaceOnUse">
<stop stop-color="#2FABFF"/>
<stop offset="0.31" stop-color="#5570FF"/>
<stop offset="0.62" stop-color="#7B36FF"/>
<stop offset="0.81" stop-color="#6A2CDC"/>
<stop offset="1" stop-color="#5921B8"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

-8
View File
@@ -1,8 +0,0 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="25" height="25"/>
<path d="M5.50694 21.1637C5.17323 21.1637 4.89218 21.1064 4.66378 20.9926C4.436 20.8787 4.26333 20.7052 4.1476 20.4763C4.03187 20.2473 3.97247 19.9672 3.97247 19.6382V16.2463C3.97247 15.8281 3.88859 15.5239 3.72265 15.3341C3.55549 15.1449 3.25912 15.0449 2.83418 15.0353C2.70191 15.0353 2.59598 14.9859 2.51577 14.8853C2.43433 14.7859 2.39453 14.6708 2.39453 14.5425C2.39453 14.4033 2.43433 14.2882 2.51577 14.1984C2.5966 14.1087 2.70375 14.0593 2.83418 14.0496C3.25912 14.0394 3.55549 13.94 3.72265 13.7508C3.88981 13.5616 3.97247 13.2622 3.97247 12.8537V9.46177C3.97247 8.96352 4.10474 8.58456 4.36742 8.3261C4.6301 8.06763 5.01035 7.9375 5.50694 7.9375H9.55926C9.71173 7.9375 9.83725 7.98269 9.9389 8.07185C10.0399 8.16162 10.0914 8.27669 10.0914 8.41466C10.0914 8.5448 10.0485 8.65626 9.96278 8.75145C9.87706 8.84664 9.76316 8.89423 9.62049 8.89423H5.8578C5.6441 8.89423 5.48245 8.94906 5.37162 9.05871C5.26018 9.16836 5.20446 9.33766 5.20446 9.5678V12.9754C5.20446 13.2742 5.14323 13.5454 5.02199 13.7894C4.90075 14.034 4.73848 14.2256 4.53581 14.3659C4.33313 14.5051 4.09616 14.575 3.82246 14.575V14.5147C4.09616 14.5147 4.33313 14.5846 4.53581 14.7238C4.73848 14.863 4.90075 15.0552 5.02199 15.3004C5.14323 15.5444 5.20446 15.8155 5.20446 16.1143V19.537C5.20446 19.7671 5.26018 19.9358 5.37162 20.0461C5.48306 20.1569 5.64533 20.2106 5.8578 20.2106H9.62049C9.76194 20.2106 9.87583 20.2582 9.96278 20.3527C10.0497 20.4479 10.0914 20.56 10.0914 20.6895C10.0914 20.8191 10.0412 20.9299 9.9389 21.0251C9.83725 21.1203 9.71112 21.1673 9.55926 21.1673H5.50694V21.1643V21.1637Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
<path d="M15.4423 21.1634C15.2898 21.1634 15.1643 21.1158 15.0626 21.0212C14.961 20.926 14.9102 20.8139 14.9102 20.6856C14.9102 20.5573 14.953 20.444 15.0387 20.3488C15.1245 20.2536 15.2384 20.2067 15.381 20.2067H19.1437C19.3574 20.2067 19.5191 20.153 19.6299 20.0422C19.7413 19.9325 19.7971 19.7632 19.7971 19.5331V16.1104C19.7971 15.8116 19.8583 15.5405 19.9795 15.2965C20.1008 15.0519 20.263 14.8603 20.4657 14.7199C20.6684 14.5807 20.9054 14.5108 21.1791 14.5108V14.5711C20.9054 14.5711 20.6684 14.5012 20.4657 14.362C20.263 14.2229 20.1008 14.0307 19.9795 13.7855C19.8583 13.5415 19.7971 13.2703 19.7971 12.9715V9.5639C19.7971 9.33496 19.7413 9.16566 19.6299 9.0548C19.5185 8.94515 19.3562 8.89033 19.1437 8.89033H15.381C15.2396 8.89033 15.1257 8.84273 15.0387 8.74754C14.953 8.65355 14.9102 8.54089 14.9102 8.41076C14.9102 8.27158 14.9604 8.15771 15.0626 8.06795C15.1637 7.97818 15.2898 7.93359 15.4423 7.93359H19.4946C19.9912 7.93359 20.3702 8.06373 20.6341 8.32219C20.898 8.58065 21.029 8.95961 21.029 9.45786V12.8498C21.029 13.2583 21.1129 13.5583 21.2789 13.7469C21.446 13.9361 21.7424 14.0361 22.1673 14.0457C22.2996 14.0554 22.4055 14.1048 22.4858 14.1945C22.5672 14.2843 22.607 14.3994 22.607 14.5385C22.607 14.6687 22.5672 14.7826 22.4858 14.8814C22.4055 14.9808 22.2978 15.0314 22.1673 15.0314C21.7424 15.041 21.4466 15.141 21.2789 15.3302C21.1117 15.5194 21.029 15.823 21.029 16.2424V19.6343C21.029 19.9639 20.9709 20.2422 20.8539 20.4723C20.737 20.7025 20.5655 20.8736 20.3377 20.9887C20.1093 21.1025 19.8283 21.1598 19.4946 21.1598H15.4423V21.1628V21.1634Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
<path d="M16.4845 15.8401C17.2224 15.8401 17.8206 15.2515 17.8206 14.5255C17.8206 13.7996 17.2224 13.2109 16.4845 13.2109C15.7467 13.2109 15.1484 13.7996 15.1484 14.5255C15.1484 15.2515 15.7467 15.8401 16.4845 15.8401Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
<path d="M9.00014 15.8401C9.73798 15.8401 10.3362 15.2515 10.3362 14.5255C10.3362 13.7996 9.73798 13.2109 9.00014 13.2109C8.2623 13.2109 7.66406 13.7996 7.66406 14.5255C7.66406 15.2515 8.2623 15.8401 9.00014 15.8401Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
<path d="M12.0442 4.13327L11.942 6.81971C11.942 6.97033 11.7974 7.04564 11.5084 7.04564C11.2194 7.04564 11.0749 6.97033 11.0749 6.81971C11.0492 6.15036 11.0284 5.63103 11.0112 5.26291C11.0027 4.88637 10.9941 4.61826 10.9855 4.45921C10.9769 4.30016 10.9727 4.20376 10.9727 4.17062V4.12062C10.9727 3.92843 11.1515 3.83203 11.5084 3.83203C11.8654 3.83203 12.0442 3.93264 12.0442 4.13327ZM14.213 4.13327L14.1108 6.81971C14.1108 6.97033 13.9663 7.04564 13.6773 7.04564C13.3883 7.04564 13.2437 6.97033 13.2437 6.81971C13.218 6.15036 13.1972 5.63103 13.1801 5.26291C13.1715 4.88637 13.1629 4.61826 13.1543 4.45921C13.1458 4.30016 13.1415 4.20376 13.1415 4.17062V4.12062C13.1415 3.92843 13.3203 3.83203 13.6773 3.83203C14.0342 3.83203 14.213 3.93264 14.213 4.13327Z" fill="#F8F7F7" stroke="#F8F7F7" stroke-width="0.259057" stroke-miterlimit="10"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

-27
View File
@@ -1,27 +0,0 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_293_450)">
<path d="M58.5288 0.34381L5.96707 27.4409C4.81829 28.1161 4.48575 29.1238 4.49583 30.5849L4.5966 32.4089L60.0907 60.8664L115.454 32.8623C115.454 32.8623 115.514 32.5802 115.514 31.5926C115.514 31.5926 115.464 30.2927 115.172 29.6881C114.849 29.0028 114.164 28.3881 113.721 28.0153C112.542 27.0378 111.363 26.1712 109.357 25.0426C80.6075 8.78835 61.7031 0.414348 61.7031 0.414348C60.4837 -0.0391172 59.476 -0.200349 58.5288 0.34381Z" fill="#DEA66C"/>
<path d="M5.32223 92.3671C8.38565 94.3926 13.9784 97.0529 19.6316 100.116C32.772 107.231 49.3789 115.877 57.6421 119.273C58.7606 119.736 59.4257 119.958 60.2017 119.998C60.7761 120.029 61.7233 119.646 61.7233 119.646C61.7233 119.646 60.1815 64.0306 60.0908 60.8563C60.0001 57.7526 58.7002 56.7247 57.5514 55.979C55.7577 54.8302 51.868 53.0365 51.868 53.0365C51.868 53.0365 38.2438 46.2648 24.6399 39.1302C18.1704 35.7343 12.7087 32.2577 6.83378 29.3958C5.75554 28.8819 4.48584 29.5269 4.48584 30.7361V90.9967C4.48584 91.571 4.83854 92.0547 5.32223 92.3671Z" fill="#B38251"/>
<path d="M113.993 92.7198L61.834 119.615C60.9976 120.049 60 119.444 60 118.497L60.0907 60.8765C60.0907 60.3222 59.9194 59.1029 59.7481 58.6192C59.2745 57.2084 60.3023 56.6441 60.7659 56.4123L112.501 29.7687C113.872 29.0633 115.514 30.0609 115.514 31.6027V90.2409C115.514 91.2788 114.93 92.2361 113.993 92.7198Z" fill="#966239"/>
<path opacity="0.5" d="M90.6947 43.4231C90.6947 43.4231 62.1062 58.0448 61.6427 59.2843C61.1791 60.5137 84.0741 49.5096 86.7546 48.129C89.7878 46.5671 113.61 33.8297 113.61 32.429C113.62 31.2802 90.6947 43.4231 90.6947 43.4231Z" fill="#212121"/>
<g opacity="0.5">
<path d="M8.41577 89.0316C12.1342 90.906 15.8224 92.8408 20.4175 95.1887C20.4175 94.4732 20.4175 94.1105 20.4175 93.395C15.8425 91.0571 12.1443 89.1324 8.41577 87.2379C8.41577 87.9534 8.41577 88.3162 8.41577 89.0316Z" fill="#212121"/>
<path d="M16.2456 89.8479C17.2936 90.382 17.8176 90.654 18.8052 91.1579C18.8152 88.034 18.8152 86.4721 18.8253 83.3482C17.8378 82.8443 17.3238 82.5823 16.2859 82.0483C16.2658 85.1721 16.2557 86.724 16.2456 89.8479Z" fill="#212121"/>
<path d="M9.73584 86.5023C10.6025 86.9457 11.0862 87.1976 12.0737 87.7015C12.0939 84.5877 12.1039 83.0358 12.1241 79.922C11.1265 79.4081 10.6428 79.1662 9.76607 78.7027C9.75599 81.8266 9.74592 83.3885 9.73584 86.5023Z" fill="#212121"/>
<path d="M14.603 82.1188C16.9913 83.3381 18.261 83.9931 20.3671 85.0613C19.3191 83.1567 18.7447 82.1893 17.5959 80.2344C16.4169 81.0003 15.8022 81.3731 14.603 82.1188Z" fill="#212121"/>
<path d="M8.41577 78.9445C10.1994 79.8716 11.3986 80.4863 13.7566 81.6956C12.5877 79.7306 12.0032 78.7531 10.9249 76.8284C9.82655 77.6245 9.31263 78.0376 8.41577 78.9445Z" fill="#212121"/>
</g>
<g opacity="0.5">
<path d="M27.5622 97.6475C28.3986 98.0808 28.8319 98.3126 29.6683 98.7459C29.6079 95.0375 29.5776 93.1834 29.5071 89.475C28.7009 89.0518 28.2979 88.8402 27.4917 88.427C27.5219 92.1152 27.532 93.9593 27.5622 97.6475Z" fill="#212121"/>
<path d="M24.4888 96.9925C27.5522 98.6048 29.9304 99.814 32.8728 101.386C32.8527 100.328 32.0365 99.0482 30.928 98.4536C29.1947 97.5366 28.2072 97.0328 26.4538 96.1158C25.3252 95.5414 24.4888 95.9545 24.4888 96.9925Z" fill="#212121"/>
<path d="M30.3233 86.5225C30.8373 87.6108 31.0993 88.165 31.6132 89.2533C31.2101 90.1099 30.9985 90.5432 30.5753 91.3897C30.938 91.3594 31.1194 91.3493 31.4822 91.3191C31.9659 90.6742 32.2077 90.3517 32.6713 89.7068C32.2481 88.7092 32.0264 88.2053 31.583 87.2077C32.8124 87.8829 33.417 88.2255 34.5355 88.8805C34.5355 92.488 32.1775 94.0802 28.6304 92.226C25.0833 90.3819 22.7354 86.2605 22.7354 82.6428C25.3755 83.9629 27.1491 84.8295 30.3233 86.5225Z" fill="#212121"/>
</g>
<path d="M97.0231 37.2761C96.5192 36.9032 42.4158 8.63719 42.4158 8.63719L25.6274 17.3135L81.1014 45.9222L81.1316 45.9524L97.3657 37.6388C97.3657 37.6388 97.3355 37.5582 97.2448 37.4575C97.1743 37.397 97.0533 37.3063 97.0231 37.2761Z" fill="#FFE0B2"/>
<path d="M97.285 37.5179C88.0948 42.0425 81.1014 45.9222 81.1014 45.9222C81.3331 46.1539 81.3432 46.4562 81.3432 46.6074C81.313 47.7763 81.2928 48.7034 81.2626 49.8623C81.1215 55.8581 81.0409 61.864 80.8394 67.8598C81.5145 67.4567 82.3308 68.3435 82.9454 67.8598C83.8927 67.1141 85.9081 63.829 86.2104 63.7282C86.6135 63.5972 87.0468 63.698 87.46 63.6779C89.2738 63.5771 93.3349 59.8788 94.2015 59.7075C95.0379 59.5463 97.799 60.0804 97.799 60.0804C97.799 60.0804 97.6075 41.7906 97.537 38.7171C97.5168 37.6288 97.285 37.5179 97.285 37.5179Z" fill="#BF9F85"/>
</g>
<defs>
<clipPath id="clip0_293_450">
<rect width="120" height="120" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.6 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

-4
View File
@@ -1,4 +0,0 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="160" height="160" fill="white"/>
<path d="M57.933 54C75.2624 54.0001 84.9775 62.7841 85.3057 75.7138L70.3392 76.2054C69.9453 69.0379 64.0048 64.3297 57.933 64.4701C49.5965 64.6458 43.4257 70.5838 43.4256 79.9999C43.4256 89.4162 49.5964 95.2491 57.933 95.2491C64.0048 95.2485 69.8139 90.7514 70.4704 83.5838L85.4368 83.9354C85.043 97.0757 74.7372 106 57.933 106C41.1286 106 28 95.8108 28 79.9999C28.0001 64.1189 40.6035 54 57.933 54ZM132 55.5364V104.726H92.6151V55.5364H132Z" fill="#090B0B"/>
</svg>

Before

Width:  |  Height:  |  Size: 612 B

-2
View File
@@ -1,2 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 575 B

-10
View File
@@ -1,10 +0,0 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_293_107)">
<path d="M60.285 96.2158V119.197C99.2373 119.197 129.566 81.5665 116.824 40.7488C111.256 22.9113 97.0819 8.73763 79.2444 3.16935C38.4267 -9.5728 0.795288 20.7566 0.795288 59.7089H23.8294C23.8328 59.7089 23.8355 59.7029 23.8355 59.7029C23.8388 35.2575 48.0312 16.3581 73.7321 25.684C83.2536 29.1394 90.8485 36.7329 94.3059 46.2537C103.633 71.9395 84.7622 96.1224 60.3376 96.1571V73.2498L37.3703 73.2485L37.3662 96.2158H60.285ZM37.3622 113.866H19.7183L19.7142 96.2158H37.3662L37.3622 113.866ZM19.7264 96.2158H4.93541C4.92934 96.2158 4.92461 96.2117 4.92461 96.2117V81.4275C4.92461 81.4275 4.92934 81.4167 4.93541 81.4167H19.7156C19.7216 81.4167 19.7264 81.4215 19.7264 81.4215V96.2158Z" fill="#0080FF"/>
</g>
<defs>
<clipPath id="clip0_293_107">
<rect width="120" height="120" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 953 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M118.049 51.0329C115.107 49.0467 107.377 48.1984 101.756 49.7167C101.453 44.1017 98.5669 39.37 93.2862 35.2415L91.3313 33.9253L90.0286 35.8988C87.4676 39.7981 86.3886 44.993 86.771 49.7151C87.0725 52.6245 88.0816 55.8951 90.0286 58.2681C82.7137 62.524 75.9715 61.5579 46.112 61.5579H0.0103499C-0.124524 68.3204 0.95923 81.3298 9.20719 91.92C10.118 93.0898 11.1176 94.2214 12.2014 95.3116C18.907 102.047 29.0385 106.986 44.1888 107C67.3015 107.021 87.1042 94.4904 99.1493 64.1919C103.113 64.2572 113.576 64.9049 118.697 54.98C118.822 54.8129 119.999 52.3475 119.999 52.3475L118.048 51.0313L118.049 51.0329ZM30.0968 44.8481H17.133V57.8511H30.0968V44.8481ZM46.845 44.8481H33.8812V57.8511H46.845V44.8481ZM63.5932 44.8481H50.6294V57.8511H63.5932V44.8481ZM80.3414 44.8481H67.3777V57.8511H80.3414V44.8481ZM13.3486 44.8481H0.384824V57.8511H13.3486V44.8481ZM30.0968 28.4249H17.133V41.4279H30.0968V28.4249ZM46.845 28.4249H33.8812V41.4279H46.845V28.4249ZM63.5932 28.4249H50.6294V41.4279H63.5932V28.4249ZM63.5932 12H50.6294V25.003H63.5932V12Z" fill="#1D63ED"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-1
View File
@@ -1 +0,0 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><text x="50%" y="50%" font-size="96px" text-anchor="middle" dominant-baseline="middle" font-family="Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, sans-serif">🔌</text></svg>

Before

Width:  |  Height:  |  Size: 247 B

-60
View File
@@ -1,60 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="none" viewBox="0 0 64 64">
<defs>
<radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="rotate(49.385 -18.029987 36.649084) scale(49.4299)" gradientUnits="userSpaceOnUse">
<stop offset=".026042" stop-color="#8DFDFD"/>
<stop offset=".270833" stop-color="#87FBFB"/>
<stop offset=".484416" stop-color="#74D6F4"/>
<stop offset=".931964" stop-color="#0038FF"/>
</radialGradient>
<radialGradient id="b" cx="0" cy="0" r="1" gradientTransform="rotate(132.274 3.919184 20.864728) scale(23.7857)" gradientUnits="userSpaceOnUse">
<stop stop-color="#0500FF" stop-opacity="0"/>
<stop offset="1" stop-color="#0100FF" stop-opacity=".15"/>
</radialGradient>
<radialGradient id="c" cx="0" cy="0" r="1" gradientTransform="rotate(42.678 -19.143042 44.644478) scale(41.8951)" gradientUnits="userSpaceOnUse">
<stop offset=".520394" stop-color="#FF00E5" stop-opacity="0"/>
<stop offset="1" stop-color="#FF00E5" stop-opacity=".65"/>
</radialGradient>
<radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="matrix(30.00005 -22.00001 19.46596 26.54453 32.3943 42.4)" gradientUnits="userSpaceOnUse">
<stop offset=".777466" stop-color="#001AFF"/>
<stop offset="1" stop-color="#8ACEFF"/>
</radialGradient>
<radialGradient id="f" cx="0" cy="0" r="1" gradientTransform="matrix(14.91531 -8.80077 11.61873 19.69112 44.057 27.7156)" gradientUnits="userSpaceOnUse">
<stop offset=".71875" stop-color="#FA00FF" stop-opacity="0"/>
<stop offset="1" stop-color="#FF00D6" stop-opacity=".44"/>
</radialGradient>
<radialGradient id="h" cx="0" cy="0" r="1" gradientTransform="rotate(63.435 -9.856848 34.706598) scale(30.4105 69.8305)" gradientUnits="userSpaceOnUse">
<stop stop-color="#0D67A9"/>
<stop offset="1" stop-color="#AEDDFF"/>
</radialGradient>
<radialGradient id="j" cx="0" cy="0" r="1" gradientTransform="rotate(73.835 -3.838438 33.695644) scale(28.736 56.1739)" gradientUnits="userSpaceOnUse">
<stop stop-color="#0068C9"/>
<stop offset="1" stop-color="#fff"/>
</radialGradient>
<filter id="g" width="48.3057" height="34.5039" x="8.25781" y="24.2656" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur result="effect1_foregroundBlur_6490_3223" stdDeviation="1"/>
</filter>
<filter id="i" width="45.7057" height="31.9039" x="9.55781" y="25.5656" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur result="effect1_foregroundBlur_6490_3223" stdDeviation=".35"/>
</filter>
<linearGradient id="d" x1="63.9941" x2="37.1941" y1="33.6" y2="34.4" gradientUnits="userSpaceOnUse">
<stop stop-color="#FD3AF5"/>
<stop offset="1" stop-color="#FD3AF5" stop-opacity="0"/>
</linearGradient>
</defs>
<path fill="url(#a)" d="M63.9941 32c0 17.6731-14.3268 32-32 32C14.3211 64-.00586 49.6731-.00586 32c0-17.6731 14.32696-32 31.99996-32 8.306 3.75956 16.9952 17.7487 20.6338 22.1202 3.6389 4.3715 13.0573 9.8798 8.4414-1.6751 1.9681 3.1232 2.9248 8.3051 2.9248 11.5549Z"/>
<path fill="url(#b)" d="M63.9941 32c0 17.6731-14.3268 32-32 32C14.3211 64-.00586 49.6731-.00586 32c0-17.6731 14.32696-32 31.99996-32 8.306 3.75956 16.9952 17.7487 20.6338 22.1202 3.6389 4.3715 13.0573 9.8798 8.4414-1.6751 1.9681 3.1232 2.9248 8.3051 2.9248 11.5549Z"/>
<path fill="url(#c)" d="M63.9941 32c0 17.6731-14.3268 32-32 32C14.3211 64-.00586 49.6731-.00586 32c0-17.6731 14.32696-32 31.99996-32 8.306 3.75956 16.9952 17.7487 20.6338 22.1202 3.6389 4.3715 13.0573 9.8798 8.4414-1.6751 1.9681 3.1232 2.9248 8.3051 2.9248 11.5549Z"/>
<path fill="url(#d)" fill-opacity=".3" d="M63.9941 32c0 17.6731-14.3268 32-32 32C14.3211 64-.00586 49.6731-.00586 32c0-17.6731 14.32696-32 31.99996-32 8.306 3.75956 16.9952 17.7487 20.6338 22.1202 3.6389 4.3715 13.0573 9.8798 8.4414-1.6751 1.9681 3.1232 2.9248 8.3051 2.9248 11.5549Z"/>
<path fill="url(#e)" d="M61.0886 20.4758c-3.1529-5.3391-9.686-9.2821-17.5378-10.2688 2.2608 2.7375 4.3175 5.5453 6.0172 7.8664 1.2242 1.6711 2.2633 3.0899 3.0601 4.0469 3.6389 4.3711 13.0573 9.8797 8.4414-1.675.0063.0102.0127.0203.0191.0305Z"/>
<path fill="url(#f)" d="M61.0886 20.4758c-3.1529-5.3391-9.686-9.2821-17.5378-10.2688 2.2608 2.7375 4.3175 5.5453 6.0172 7.8664 1.2242 1.6711 2.2633 3.0899 3.0601 4.0469 3.6389 4.3711 13.0573 9.8797 8.4414-1.675.0063.0102.0127.0203.0191.0305Z"/>
<g filter="url(#g)">
<path fill="url(#h)" d="M29.3127 27.0066c12.113-2.5862 23.3196 1.8139 25.0306 9.8279 1.711 8.014-6.7214 16.6071-18.8343 19.1933C23.396 58.614 12.1894 54.214 10.4783 46.2c-1.711-8.014 6.7215-16.6072 18.8344-19.1934Z"/>
</g>
<g filter="url(#i)">
<path fill="url(#j)" fill-opacity=".2" fill-rule="evenodd" d="M48.9867 47.3643c3.1734-3.2337 4.5278-6.8744 3.8174-10.2012-.7102-3.3268-3.4332-6.0967-7.6507-7.7527-4.2039-1.6506-9.7148-2.1025-15.5122-.8645-5.7973 1.2377-10.6433 3.9007-13.8065 7.1243-3.1734 3.2339-4.5276 6.8744-3.8173 10.2012.7103 3.3267 3.4332 6.0968 7.6506 7.7527 4.2039 1.6506 9.7148 2.1024 15.5122.8647 5.7974-1.2377 10.6433-3.9009 13.8065-7.1245Zm-13.4778 8.6636c12.1128-2.5862 20.5452-11.1793 18.8343-19.1933-1.7112-8.0141-12.9177-12.4142-25.0305-9.828-12.1131 2.5862-20.54545 11.1795-18.8344 19.1935 1.711 8.0138 12.9176 12.414 25.0306 9.8278Z" clip-rule="evenodd"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.6 KiB

-1
View File
@@ -1 +0,0 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="#3186FF"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-0)"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-1)"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-2)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-0" x1="7" x2="11" y1="15.5" y2="12"><stop stop-color="#08B962"></stop><stop offset="1" stop-color="#08B962" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-1" x1="8" x2="11.5" y1="5.5" y2="11"><stop stop-color="#F94543"></stop><stop offset="1" stop-color="#F94543" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-2" x1="3.5" x2="17.5" y1="13.5" y2="12"><stop stop-color="#FABC12"></stop><stop offset=".46" stop-color="#FABC12" stop-opacity="0"></stop></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

-1
View File
@@ -1 +0,0 @@
<svg fill="none" height="64" viewBox="0 0 64 64" width="64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1=".850001" x2="62.62" y1="62.72" y2="1.81"><stop offset="0" stop-color="#ff9419"/><stop offset=".43" stop-color="#ff021d"/><stop offset=".99" stop-color="#e600ff"/></linearGradient><clipPath id="b"><path d="m0 0h64v64h-64z"/></clipPath><g clip-path="url(#b)"><path d="m20.34 3.66-16.68 16.68c-2.34 2.34-3.66 5.52-3.66 8.84v29.82c0 2.76 2.24 5 5 5h29.82c3.32 0 6.49-1.32 8.84-3.66l16.68-16.68c2.34-2.34 3.66-5.52 3.66-8.84v-29.82c0-2.76-2.24-5-5-5h-29.82c-3.32 0-6.49 1.32-8.84 3.66z" fill="url(#a)"/><path d="m48 16h-40v40h40z" fill="#000"/><path d="m30 47h-17v4h17z" fill="#fff"/></g></svg>

Before

Width:  |  Height:  |  Size: 785 B

+90 -14
View File
@@ -1,14 +1,90 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.401 63.1422C13.401 66.8082 13.1064 68.0022 12.3488 68.882C11.5056 69.6378 10.4111 70.0558 9.27646 70.0551L9.57107 72.1499C11.3301 72.1736 13.0369 71.555 14.369 70.4112C15.0857 69.5411 15.619 68.5363 15.9372 67.4566C16.2554 66.3769 16.3518 65.2444 16.2208 64.1268V50.2591H13.401V63.0584V63.1422Z" fill="white"/>
<path d="M34.4304 61.4899C34.4304 63.061 34.4304 64.4646 34.5567 65.6795H32.0525L31.8842 63.1867C31.3604 64.0687 30.6113 64.797 29.7132 65.2974C28.815 65.7978 27.7997 66.0525 26.7706 66.0357C24.3296 66.0357 21.4256 64.7159 21.4256 59.3323V50.3874H24.2454V58.7667C24.2454 61.6785 25.1503 63.6266 27.6965 63.6266C28.2204 63.6323 28.7401 63.5337 29.2252 63.3367C29.7103 63.1397 30.1509 62.8483 30.5214 62.4795C30.8918 62.1107 31.1846 61.672 31.3825 61.1892C31.5803 60.7063 31.6794 60.1889 31.6737 59.6674V50.2827H34.4935V61.4061L34.4304 61.4899Z" fill="white"/>
<path d="M39.7754 55.3709C39.7754 53.4228 39.7754 51.8307 39.6491 50.3853H42.1743L42.3006 53.0038C42.8632 52.0578 43.6731 51.2814 44.6441 50.7576C45.615 50.2337 46.7106 49.982 47.8139 50.0292C51.5597 50.0292 54.3795 53.1504 54.3795 57.8009C54.3795 63.2893 50.9915 66.0126 47.3299 66.0126C46.3928 66.0542 45.4607 65.8544 44.6238 65.4324C43.787 65.0103 43.0737 64.3804 42.5531 63.6036V71.9828H39.7754V55.4338V55.3709ZM42.5531 59.4558C42.5607 59.8359 42.603 60.2145 42.6794 60.587C42.9181 61.5172 43.4616 62.3415 44.2239 62.9296C44.9862 63.5177 45.9236 63.8359 46.888 63.834C49.8551 63.834 51.6018 61.425 51.6018 57.9266C51.6018 54.8682 49.9604 52.2497 46.9933 52.2497C45.8191 52.3454 44.7202 52.8642 43.9028 53.7087C43.0854 54.5533 42.6055 55.6657 42.5531 56.8373V59.4558Z" fill="white"/>
<path d="M59.4037 50.3711L62.7917 59.3997C63.1494 60.4052 63.5282 61.6202 63.7807 62.521C64.0753 61.5993 64.3909 60.4262 64.7697 59.3369L67.8421 50.3711H70.8092L66.6005 61.2641C64.4962 66.5011 63.2125 69.2035 61.2765 70.8374C60.3014 71.7241 59.1126 72.3446 57.8254 72.6389L57.131 70.2928C58.031 69.9984 58.8665 69.5363 59.5931 68.9311C60.6197 68.0983 61.4344 67.0361 61.971 65.8308C62.0858 65.6221 62.1641 65.3954 62.2024 65.1605C62.1785 64.9077 62.1146 64.6602 62.0131 64.4273L56.2892 50.3292H59.3616L59.4037 50.3711Z" fill="white"/>
<path d="M78.295 45.9766V50.3757H82.3353V52.4705H78.295V60.7241C78.295 62.6094 78.8421 63.6987 80.3993 63.6987C80.9524 63.7077 81.5042 63.6443 82.0407 63.5102L82.1669 65.605C81.3625 65.8824 80.5137 66.0102 79.6628 65.9821C79.0993 66.0172 78.5348 65.9305 78.0081 65.728C77.4814 65.5255 77.005 65.212 76.6115 64.809C75.7543 63.6677 75.3633 62.2455 75.5172 60.8288V52.4496H73.1183V50.3547H75.5593V46.626L78.295 45.9766Z" fill="white"/>
<path d="M87.5296 58.5154C87.4725 59.2258 87.571 59.9401 87.8183 60.609C88.0657 61.2778 88.456 61.8852 88.9622 62.3891C89.4685 62.8931 90.0786 63.2816 90.7505 63.5278C91.4223 63.7741 92.1399 63.8721 92.8536 63.8153C94.2971 63.8474 95.7312 63.5762 97.0622 63.0193L97.5462 65.1141C95.9128 65.7839 94.1565 66.105 92.3906 66.0567C91.3611 66.1269 90.3286 65.9746 89.3639 65.6102C88.3991 65.2458 87.525 64.678 86.8014 63.9457C86.0777 63.2134 85.5218 62.334 85.1717 61.3678C84.8215 60.4015 84.6855 59.3713 84.7729 58.3478C84.7729 53.7392 87.5296 50.0942 92.0329 50.0942C97.0833 50.0942 98.3459 54.4933 98.3459 57.3004C98.3729 57.7329 98.3729 58.1667 98.3459 58.5992H87.4665L87.5296 58.5154ZM95.7786 56.4206C95.8598 55.8605 95.8195 55.2897 95.6603 54.7464C95.5012 54.2032 95.2269 53.7002 94.856 53.2714C94.4851 52.8427 94.0261 52.498 93.5101 52.2607C92.9941 52.0234 92.4329 51.899 91.8645 51.8958C90.7083 51.9791 89.6251 52.4893 88.827 53.3263C88.029 54.1633 87.5738 55.2668 87.5506 56.4206H95.7786Z" fill="white"/>
<path d="M102.624 55.1347C102.624 53.3332 102.624 51.783 102.498 50.3586H105.023V53.3542H105.15C105.415 52.4296 105.962 51.6101 106.717 51.0095C107.471 50.4089 108.394 50.0569 109.358 50.0024C109.624 49.9678 109.893 49.9678 110.158 50.0024V52.621C109.836 52.5839 109.512 52.5839 109.19 52.621C108.235 52.6578 107.326 53.0395 106.632 53.6949C105.939 54.3503 105.509 55.2344 105.423 56.1822C105.345 56.6108 105.302 57.0453 105.297 57.4809V65.6298H102.519V55.1557L102.624 55.1347Z" fill="white"/>
<path d="M101.389 8.2955C101.475 9.70282 101.139 11.1039 100.422 12.3202C99.7061 13.5366 98.6423 14.5131 97.3665 15.1255C96.0907 15.7378 94.6607 15.9583 93.2586 15.7587C91.8565 15.5592 90.5459 14.9487 89.4935 14.005C88.4412 13.0613 87.6949 11.8272 87.3497 10.4598C87.0045 9.09235 87.0759 7.65357 87.555 6.32675C88.0341 4.99992 88.8991 3.84508 90.0398 3.00935C91.1805 2.17362 92.5453 1.69484 93.9603 1.63398C94.8881 1.58316 95.8168 1.71542 96.693 2.02314C97.5692 2.33086 98.3757 2.80798 99.066 3.42706C99.7563 4.04614 100.317 4.79496 100.716 5.63047C101.114 6.46597 101.343 7.37169 101.389 8.2955Z" fill="#767677"/>
<path d="M59.7782 90.3351C40.8393 90.3351 24.1939 83.5688 15.5872 73.5765C18.9266 82.5657 24.9523 90.3208 32.8531 95.7978C40.7538 101.275 50.1506 104.211 59.7782 104.211C69.406 104.211 78.8026 101.275 86.7036 95.7978C94.6043 90.3208 100.63 82.5657 103.969 73.5765C95.3838 83.5688 78.8015 90.3351 59.7782 90.3351Z" fill="#F37726"/>
<path d="M59.7782 25.3579C78.7173 25.3579 95.3628 32.1242 103.969 42.1164C100.63 33.1273 94.6043 25.3722 86.7036 19.8952C78.8026 14.4182 69.406 11.4821 59.7782 11.4821C50.1506 11.4821 40.7538 14.4182 32.8531 19.8952C24.9523 25.3722 18.9266 33.1273 15.5872 42.1164C24.1939 32.1033 40.7552 25.3579 59.7782 25.3579Z" fill="#F37726"/>
<path d="M33.7064 109.78C33.8175 111.553 33.396 113.318 32.4955 114.852C31.5951 116.385 30.2565 117.617 28.6503 118.39C27.0442 119.163 25.2431 119.443 23.4767 119.194C21.7102 118.944 20.0583 118.177 18.7315 116.99C17.4046 115.802 16.4628 114.249 16.0261 112.527C15.5894 110.805 15.6776 108.992 16.2793 107.32C16.8811 105.648 17.9693 104.192 19.4052 103.138C20.8411 102.084 22.5597 101.479 24.3421 101.401C25.5097 101.339 26.6782 101.506 27.7808 101.894C28.8834 102.281 29.8985 102.881 30.7681 103.659C31.6377 104.438 32.3448 105.379 32.849 106.429C33.3531 107.479 33.6445 108.618 33.7064 109.78Z" fill="#9E9E9E"/>
<path d="M17.7396 23.0808C16.7191 23.1089 15.713 22.836 14.848 22.2964C13.9829 21.7567 13.2974 20.9745 12.8777 20.0481C12.458 19.1217 12.3228 18.0924 12.4892 17.0897C12.6556 16.0871 13.116 15.1557 13.8127 14.4129C14.5094 13.67 15.4112 13.1488 16.4046 12.9148C17.3981 12.6808 18.4388 12.7444 19.3961 13.0977C20.3533 13.4509 21.1843 14.078 21.7844 14.9002C22.3845 15.7223 22.7271 16.7027 22.769 17.7181C22.8089 19.0911 22.3031 20.4244 21.3615 21.4284C20.4198 22.4325 19.1183 23.0263 17.7396 23.0808Z" fill="#616262"/>
</svg>
<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>

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="24" viewBox="0 0 20 24" fill="none"><path d="M3.80081 18.5661C1.32306 24.0572 6.59904 25.434 10.4904 22.2205C11.6339 25.8242 15.926 23.1361 17.4652 20.3445C20.8578 14.1915 19.4877 7.91459 19.1361 6.61988C16.7244 -2.20972 4.67055 -2.21852 2.59581 6.6649C2.11136 8.21946 2.10284 9.98752 1.82846 11.8233C1.69011 12.749 1.59258 13.3398 1.23436 14.3135C1.02841 14.8733 0.745043 15.3704 0.299833 16.2082C-0.391594 17.5095 -0.0998802 20.021 3.46397 18.7186V18.7195L3.80081 18.5661Z" fill="white"></path><path d="M10.9614 10.4413C9.97202 10.4413 9.82422 9.25893 9.82422 8.55407C9.82422 7.91791 9.93824 7.4124 10.1542 7.09197C10.3441 6.81003 10.6158 6.66699 10.9614 6.66699C11.3071 6.66699 11.6036 6.81228 11.8128 7.09892C12.0511 7.42554 12.177 7.92861 12.177 8.55407C12.177 9.73591 11.7226 10.4413 10.9616 10.4413H10.9614Z" fill="black"></path><path d="M15.0318 10.4413C14.0423 10.4413 13.8945 9.25893 13.8945 8.55407C13.8945 7.91791 14.0086 7.4124 14.2245 7.09197C14.4144 6.81003 14.6861 6.66699 15.0318 6.66699C15.3774 6.66699 15.6739 6.81228 15.8831 7.09892C16.1214 7.42554 16.2474 7.92861 16.2474 8.55407C16.2474 9.73591 15.793 10.4413 15.0319 10.4413H15.0318Z" fill="black"></path></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.5 KiB

-21
View File
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" baseProfile="full" width="340" height="310" xmlns="http://www.w3.org/2000/svg">
<g stroke="white" stroke-width="1.5">
<g transform="rotate(30) skewX(30)">
<rect x="110" y="-72" width="175" height="75" fill="#333333" transform="skewX(-50)"/>
<rect x="110" y="3" width="87.5" height="75" fill="#CDCDCD" transform="skewX(-50)"/>
<rect x="16.5" y="78.9" width="87.5" height="25" fill="#CDCDCD" />
<rect x="16.5" y="104.5" width="175" height="25" fill="#888888" />
<rect x="16.5" y="130" width="175" height="50" fill="#DD4814" />
<rect x="104" y="166" width="89.5" height="25" fill="#CDCDCD" transform="skewY(-40)"/>
<rect x="228.3" y="29.5" width="87.5" height="75" fill="#888888" transform="skewX(-50)"/>
<rect x="191.8" y="266" width="89.5" height="25" fill="#888888" transform="skewY(-40)"/>
<rect x="192" y="291" width="179.5" height="50" fill="#DD4814" transform="skewY(-40)"/>
<rect x="282.3" y="240" width="89.1" height="50" fill="#333333" transform="skewY(-40)"/>
<rect x="194" y="3.7" width="87.5" height="25" fill="#333333" />
</g>
<line x1="93" y1="57" x2="93" y2="88" />
<line x1="169" y1="131" x2="92" y2="88" />
<line x1="92" y1="88" x2="14" y2="128" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-6
View File
@@ -1,6 +0,0 @@
<svg width="251" height="251" viewBox="0 0 251 251" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 47.0195C39.45 49.6394 71.06 81.3272 73.54 120.815H119.61C117.05 55.8589 64.93 3.64245 0 0.942627V47.0195Z" fill="#0DC09D"/>
<path d="M73.8 131.324C71.18 170.771 39.49 202.379 0 204.859V250.926C64.96 248.366 117.18 196.249 119.88 131.324H73.8Z" fill="#0DC09D"/>
<path d="M176.201 120.545C178.821 81.0972 210.511 49.4894 250.001 47.0095V0.942627C185.041 3.50245 132.821 55.619 130.121 120.545H176.201Z" fill="#0DC09D"/>
<path d="M250.001 204.849C210.551 202.229 178.941 170.542 176.461 131.054H130.391C132.951 196.01 185.071 248.226 250.001 250.926V204.849Z" fill="#0DC09D"/>
</svg>

Before

Width:  |  Height:  |  Size: 693 B

-2
View File
@@ -1,2 +0,0 @@
<?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="#06D092" d="M8 0L1 4v8l7 4 7-4V4L8 0zm3.119 8.797L9.254 9.863 7.001 8.65v2.549l-2.118 1.33v-5.33l1.68-1.018 2.332 1.216V4.794l2.23-1.322-.006 5.325z"/></svg>

Before

Width:  |  Height:  |  Size: 390 B

-15
View File
@@ -1,15 +0,0 @@
<svg width="721" height="721" viewBox="0 0 721 721" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1637_2935)">
<g clip-path="url(#clip1_1637_2935)">
<path d="M304.246 295.411V249.828C304.246 245.989 305.687 243.109 309.044 241.191L400.692 188.412C413.167 181.215 428.042 177.858 443.394 177.858C500.971 177.858 537.44 222.482 537.44 269.982C537.44 273.34 537.44 277.179 536.959 281.018L441.954 225.358C436.197 222 430.437 222 424.68 225.358L304.246 295.411ZM518.245 472.945V364.024C518.245 357.304 515.364 352.507 509.608 349.149L389.174 279.096L428.519 256.543C431.877 254.626 434.757 254.626 438.115 256.543L529.762 309.323C556.154 324.679 573.905 357.304 573.905 388.971C573.905 425.436 552.315 459.024 518.245 472.941V472.945ZM275.937 376.982L236.592 353.952C233.235 352.034 231.794 349.154 231.794 345.315V239.756C231.794 188.416 271.139 149.548 324.4 149.548C344.555 149.548 363.264 156.268 379.102 168.262L284.578 222.964C278.822 226.321 275.942 231.119 275.942 237.838V376.986L275.937 376.982ZM360.626 425.922L304.246 394.255V327.083L360.626 295.416L417.002 327.083V394.255L360.626 425.922ZM396.852 571.789C376.698 571.789 357.989 565.07 342.151 553.075L436.674 498.374C442.431 495.017 445.311 490.219 445.311 483.499V344.352L485.138 367.382C488.495 369.299 489.936 372.179 489.936 376.018V481.577C489.936 532.917 450.109 571.785 396.852 571.785V571.789ZM283.134 464.79L191.486 412.01C165.094 396.654 147.343 364.029 147.343 332.362C147.343 295.416 169.415 262.309 203.48 248.393V357.791C203.48 364.51 206.361 369.308 212.117 372.665L332.074 442.237L292.729 464.79C289.372 466.707 286.491 466.707 283.134 464.79ZM277.859 543.48C223.639 543.48 183.813 502.695 183.813 452.314C183.813 448.475 184.294 444.636 184.771 440.797L279.295 495.498C285.051 498.856 290.812 498.856 296.568 495.498L417.002 425.927V471.509C417.002 475.349 415.562 478.229 412.204 480.146L320.557 532.926C308.081 540.122 293.206 543.48 277.854 543.48H277.859ZM396.852 600.576C454.911 600.576 503.37 559.313 514.41 504.612C568.149 490.696 602.696 440.315 602.696 388.976C602.696 355.387 588.303 322.762 562.392 299.25C564.791 289.173 566.231 279.096 566.231 269.024C566.231 200.411 510.571 149.067 446.274 149.067C433.322 149.067 420.846 150.984 408.37 155.305C386.775 134.192 357.026 120.758 324.4 120.758C266.342 120.758 217.883 162.02 206.843 216.721C153.104 230.637 118.557 281.018 118.557 332.357C118.557 365.946 132.95 398.571 158.861 422.083C156.462 432.16 155.022 442.237 155.022 452.309C155.022 520.922 210.682 572.266 274.978 572.266C287.931 572.266 300.407 570.349 312.883 566.028C334.473 587.141 364.222 600.576 396.852 600.576Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_1637_2935">
<rect width="720" height="720" fill="white" transform="translate(0.606934 0.899902)"/>
</clipPath>
<clipPath id="clip1_1637_2935">
<rect width="484.139" height="479.818" fill="white" transform="translate(118.557 120.758)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="64" viewBox="0 0 25.6 25.6" width="64"><style><![CDATA[.B{stroke-linecap:round}.C{stroke-linejoin:round}.D{stroke-linejoin:miter}.E{stroke-width:.716}]]></style><g fill="none" stroke="#fff"><path d="M18.983 18.636c.163-1.357.114-1.555 1.124-1.336l.257.023c.777.035 1.793-.125 2.4-.402 1.285-.596 2.047-1.592.78-1.33-2.89.596-3.1-.383-3.1-.383 3.053-4.53 4.33-10.28 3.227-11.687-3.004-3.84-8.205-2.024-8.292-1.976l-.028.005c-.57-.12-1.2-.19-1.93-.2-1.308-.02-2.3.343-3.054.914 0 0-9.277-3.822-8.846 4.807.092 1.836 2.63 13.9 5.66 10.25C8.29 15.987 9.36 14.86 9.36 14.86c.53.353 1.167.533 1.834.468l.052-.044a2.01 2.01 0 0 0 .021.518c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.162l-.046.183c.306.245.285 1.76.33 2.842s.116 2.093.337 2.688.48 2.13 2.53 1.7c1.713-.367 3.023-.896 3.143-5.81" fill="#000" stroke="#000" stroke-linecap="butt" stroke-width="2.149" class="D"/><path d="M23.535 15.6c-2.89.596-3.1-.383-3.1-.383 3.053-4.53 4.33-10.28 3.228-11.687-3.004-3.84-8.205-2.023-8.292-1.976l-.028.005a10.31 10.31 0 0 0-1.929-.201c-1.308-.02-2.3.343-3.054.914 0 0-9.278-3.822-8.846 4.807.092 1.836 2.63 13.9 5.66 10.25C8.29 15.987 9.36 14.86 9.36 14.86c.53.353 1.167.533 1.834.468l.052-.044a2.02 2.02 0 0 0 .021.518c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.162l-.046.183c.306.245.52 1.593.484 2.815s-.06 2.06.18 2.716.48 2.13 2.53 1.7c1.713-.367 2.6-1.32 2.725-2.906.088-1.128.286-.962.3-1.97l.16-.478c.183-1.53.03-2.023 1.085-1.793l.257.023c.777.035 1.794-.125 2.39-.402 1.285-.596 2.047-1.592.78-1.33z" fill="#336791" stroke="none"/><g class="E"><g class="B"><path d="M12.814 16.467c-.08 2.846.02 5.712.298 6.4s.875 2.05 2.926 1.612c1.713-.367 2.337-1.078 2.607-2.647l.633-5.017M10.356 2.2S1.072-1.596 1.504 7.033c.092 1.836 2.63 13.9 5.66 10.25C8.27 15.95 9.27 14.907 9.27 14.907m6.1-13.4c-.32.1 5.164-2.005 8.282 1.978 1.1 1.407-.175 7.157-3.228 11.687" class="C"/><path d="M20.425 15.17s.2.98 3.1.382c1.267-.262.504.734-.78 1.33-1.054.49-3.418.615-3.457-.06-.1-1.745 1.244-1.215 1.147-1.652-.088-.394-.69-.78-1.086-1.744-.347-.84-4.76-7.29 1.224-6.333.22-.045-1.56-5.7-7.16-5.782S7.99 8.196 7.99 8.196" stroke-linejoin="bevel"/></g><g class="C"><path d="M11.247 15.768c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.163.35-.49-.002-1.27-.482-1.468-.232-.096-.542-.216-.94.23z"/><path d="M11.196 15.753c-.08-.513.168-1.122.433-1.836.398-1.07 1.316-2.14.582-5.537-.547-2.53-4.22-.527-4.22-.184s.166 1.74-.06 3.365c-.297 2.122 1.35 3.916 3.246 3.733" class="B"/></g></g><g fill="#fff" class="D"><path d="M10.322 8.145c-.017.117.215.43.516.472s.558-.202.575-.32-.215-.246-.516-.288-.56.02-.575.136z" stroke-width=".239"/><path d="M19.486 7.906c.016.117-.215.43-.516.472s-.56-.202-.575-.32.215-.246.516-.288.56.02.575.136z" stroke-width=".119"/></g><path d="M20.562 7.095c.05.92-.198 1.545-.23 2.524-.046 1.422.678 3.05-.413 4.68" class="B C E"/></g></svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

-137
View File
@@ -1,137 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="800"
height="800"
viewBox="0 0 211.66666 211.66666"
version="1.1"
id="svg924"
inkscape:export-filename="/home/daniela/Documents/proxmox/Proxmox/Marketing/Logo/proxmox-logo/Screen/Full Lockup/stacked/proxmox-logo-color-stacked-bgblack.png"
inkscape:export-xdpi="360"
inkscape:export-ydpi="360"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
sodipodi:docname="proxmox-logo-stacked-inverted-color-bgtrans.svg">
<defs
id="defs918" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="0.38489655"
inkscape:cx="541.41545"
inkscape:cy="189.70435"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
inkscape:pagecheckerboard="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1720"
inkscape:window-height="1343"
inkscape:window-x="1720"
inkscape:window-y="27"
inkscape:window-maximized="0"
units="px" />
<metadata
id="metadata921">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-11.346916,-31.368461)">
<g
id="g209">
<g
transform="matrix(0.84666672,0,0,0.84666672,544.05161,-814.30036)"
id="g1288"
style="fill:#ffffff">
<g
id="g1286"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:25.8336px;line-height:125%;font-family:Helion;-inkscape-font-specification:Helion;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none"
transform="matrix(1.2435137,0,0,1.2435137,-791.06481,553.75862)">
<path
inkscape:connector-curvature="0"
id="path1272"
d="m 168.85142,500.9595 h -11.85747 c -0.46554,0.0129 -0.85842,0.18085 -1.17864,0.50374 -0.32023,0.32294 -0.48707,0.72335 -0.50052,1.20125 v 16.3783 c 1.27229,-0.0318 2.33145,-0.472 3.17749,-1.32073 0.84604,-0.84873 1.2852,-1.91543 1.3175,-3.2001 h 9.04164 c 1.28573,-0.0317 2.35673,-0.47199 3.21302,-1.32072 0.85624,-0.84873 1.30079,-1.91543 1.33364,-3.2001 v -4.49499 c -0.0328,-1.28573 -0.4774,-2.35673 -1.33364,-3.21301 -0.85629,-0.85625 -1.92729,-1.3008 -3.21302,-1.33364 z m -9.04164,9.60997 v -5.06332 h 7.93081 c 0.0463,-0.0231 0.23141,0.0232 0.55542,0.13885 0.32398,0.11573 0.50912,0.43972 0.55541,0.97198 v 2.81583 c 0.0231,0.0474 -0.0231,0.23681 -0.13885,0.56833 -0.11573,0.33154 -0.43972,0.52098 -0.97198,0.56833 z"
style="fill:#ffffff;fill-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path1274"
d="m 194.05931,508.89031 v -3.38416 c -0.0318,-1.28573 -0.47201,-2.35673 -1.32072,-3.21301 -0.84875,-0.85625 -1.91545,-1.3008 -3.2001,-1.33364 h -11.85747 c -0.47684,0.0129 -0.87295,0.18085 -1.18833,0.50374 -0.31538,0.32294 -0.47899,0.72335 -0.49083,1.20125 v 16.3783 c 1.27336,-0.0318 2.33683,-0.472 3.1904,-1.32073 0.85357,-0.84873 1.29705,-1.91543 1.33042,-3.2001 v -1.13666 h 5.14082 l 2.60916,3.71999 c 0.4187,0.60063 0.94398,1.07208 1.57583,1.41437 0.63182,0.34229 1.33793,0.51667 2.11833,0.52313 0.37618,-5.4e-4 0.74107,-0.0447 1.09468,-0.1324 0.35358,-0.0877 0.68618,-0.21582 0.99781,-0.38427 l -3.64249,-5.19249 c 1.05592,-0.22872 1.92133,-0.74647 2.59625,-1.55322 0.67487,-0.80675 1.02362,-1.77011 1.04624,-2.8901 z m -13.53663,0.5425 v -3.92666 h 7.87915 c 0.0474,-0.0231 0.23679,0.0232 0.56833,0.13885 0.33151,0.11573 0.52096,0.43972 0.56833,0.97198 v 1.705 c 0.0237,0.0463 -0.0237,0.23143 -0.14208,0.55541 -0.11842,0.324 -0.44995,0.50914 -0.99458,0.55542 z"
style="fill:#ffffff;fill-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path1276"
d="m 210.1751,500.9595 h -9.01581 c -1.28467,0.0328 -2.35137,0.47739 -3.2001,1.33364 -0.84873,0.85628 -1.28897,1.92728 -1.32072,3.21301 v 9.01581 c 0.0317,1.28467 0.47199,2.35137 1.32072,3.2001 0.84873,0.84873 1.91543,1.28897 3.2001,1.32073 h 9.01581 c 1.28465,-0.0318 2.35135,-0.472 3.2001,-1.32073 0.84871,-0.84873 1.28895,-1.91543 1.32072,-3.2001 v -9.01581 c -0.0318,-1.28573 -0.47201,-2.35673 -1.32072,-3.21301 -0.84875,-0.85625 -1.91545,-1.3008 -3.2001,-1.33364 z m 0,12.4258 c 0.0237,0.0474 -0.0237,0.23681 -0.14208,0.56833 -0.11842,0.33153 -0.44995,0.52098 -0.99458,0.56833 h -6.74249 c -0.0474,0.0237 -0.23681,-0.0237 -0.56833,-0.14208 -0.33153,-0.1184 -0.52098,-0.44993 -0.56833,-0.99458 v -6.76832 c -0.0237,-0.0463 0.0237,-0.23141 0.14208,-0.55541 0.1184,-0.32398 0.44993,-0.50912 0.99458,-0.55542 h 6.74249 c 0.0473,-0.0231 0.23679,0.0232 0.56833,0.13885 0.33151,0.11573 0.52096,0.43972 0.56833,0.97198 z"
style="fill:#ffffff;fill-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path1278"
d="m 237.4767,502.25116 c -0.39183,-0.39179 -0.84822,-0.69964 -1.36917,-0.92354 -0.52099,-0.22387 -1.08071,-0.33797 -1.67916,-0.34229 -0.6367,0.005 -1.22333,0.1308 -1.75989,0.37781 -0.53659,0.24705 -1.00052,0.58611 -1.39177,1.01719 l -3.90082,4.28832 -3.92666,-4.28832 c -0.4015,-0.44238 -0.86434,-0.78467 -1.38854,-1.02688 -0.5242,-0.24217 -1.1033,-0.36487 -1.73729,-0.36812 -0.59847,0.004 -1.15819,0.11842 -1.67916,0.34229 -0.52097,0.2239 -0.97736,0.53175 -1.36916,0.92354 l 7.05248,7.74998 -7.05248,7.74998 c 0.3918,0.40419 0.84819,0.71957 1.36916,0.94615 0.52097,0.22657 1.08069,0.34175 1.67916,0.34552 0.62538,-0.005 1.20878,-0.13079 1.75021,-0.37782 0.54142,-0.24703 1.00857,-0.58609 1.40145,-1.01718 l 3.90083,-4.28832 3.90082,4.28832 c 0.39125,0.43109 0.85518,0.77015 1.39177,1.01718 0.53656,0.24703 1.12319,0.37297 1.75989,0.37782 0.59845,-0.004 1.15817,-0.11895 1.67916,-0.34552 0.52096,-0.22658 0.97734,-0.54196 1.36917,-0.94615 l -7.05249,-7.74998 z"
style="fill:#ffffff;fill-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path1280"
d="m 260.98042,500.9595 h -2.84166 c -0.92947,0.0129 -1.75721,0.2648 -2.48322,0.75562 -0.72604,0.49085 -1.27607,1.14314 -1.6501,1.95687 l 0.0258,-0.0517 -2.66082,5.83832 -2.635,-5.83832 v 0.0517 c -0.36275,-0.81373 -0.90955,-1.46602 -1.64041,-1.95687 -0.73087,-0.49082 -1.56184,-0.74269 -2.49291,-0.75562 h -2.81583 c -0.48922,0.0129 -0.89286,0.18085 -1.21093,0.50374 -0.31808,0.32294 -0.48276,0.72335 -0.49406,1.20125 v 16.3783 c 1.27336,-0.0318 2.33683,-0.472 3.19041,-1.32073 0.85357,-0.84873 1.29704,-1.91543 1.33041,-3.2001 v -8.65414 c 0.002,-0.11785 0.0371,-0.2115 0.10656,-0.28094 0.0694,-0.0694 0.16307,-0.10493 0.28094,-0.10656 0.0673,0.002 0.13293,0.0237 0.19698,0.0646 0.064,0.0409 0.11032,0.0883 0.13885,0.14208 l 5.01166,11.05664 c 0.0947,0.19752 0.23464,0.3579 0.41979,0.48115 0.18512,0.12325 0.38964,0.18675 0.61354,0.19052 0.22226,-0.003 0.42354,-0.0619 0.60385,-0.1776 0.18028,-0.11571 0.32344,-0.27179 0.42948,-0.46823 L 257.4154,505.687 c 0.0393,-0.0538 0.0899,-0.10116 0.15177,-0.14208 0.0619,-0.0409 0.13184,-0.0624 0.2099,-0.0646 0.10547,0.002 0.19158,0.0371 0.25833,0.10656 0.0667,0.0694 0.10116,0.16309 0.10333,0.28094 v 8.65414 c 0.0333,1.28467 0.47682,2.35137 1.33042,3.2001 0.85355,0.84873 1.91702,1.28897 3.19041,1.32073 v -16.3783 c -0.0119,-0.4779 -0.17548,-0.87831 -0.49084,-1.20125 -0.3154,-0.32289 -0.71151,-0.49081 -1.18833,-0.50374 z"
style="fill:#ffffff;fill-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path1282"
d="m 278.79561,500.9595 h -9.01581 c -1.28467,0.0328 -2.35137,0.47739 -3.20009,1.33364 -0.84874,0.85628 -1.28898,1.92728 -1.32073,3.21301 v 9.01581 c 0.0317,1.28467 0.47199,2.35137 1.32073,3.2001 0.84872,0.84873 1.91542,1.28897 3.20009,1.32073 h 9.01581 c 1.28466,-0.0318 2.35135,-0.472 3.2001,-1.32073 0.84871,-0.84873 1.28896,-1.91543 1.32073,-3.2001 v -9.01581 c -0.0318,-1.28573 -0.47202,-2.35673 -1.32073,-3.21301 -0.84875,-0.85625 -1.91544,-1.3008 -3.2001,-1.33364 z m 0,12.4258 c 0.0237,0.0474 -0.0237,0.23681 -0.14208,0.56833 -0.11842,0.33153 -0.44994,0.52098 -0.99458,0.56833 h -6.74248 c -0.0474,0.0237 -0.23681,-0.0237 -0.56834,-0.14208 -0.33153,-0.1184 -0.52097,-0.44993 -0.56833,-0.99458 v -6.76832 c -0.0237,-0.0463 0.0237,-0.23141 0.14209,-0.55541 0.11839,-0.32398 0.44992,-0.50912 0.99458,-0.55542 h 6.74248 c 0.0473,-0.0231 0.23679,0.0232 0.56833,0.13885 0.33152,0.11573 0.52096,0.43972 0.56833,0.97198 z"
style="fill:#ffffff;fill-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path1284"
d="m 306.0972,502.25116 c -0.39182,-0.39179 -0.84821,-0.69964 -1.36916,-0.92354 -0.52099,-0.22387 -1.08071,-0.33797 -1.67916,-0.34229 -0.6367,0.005 -1.22333,0.1308 -1.75989,0.37781 -0.5366,0.24705 -1.00052,0.58611 -1.39177,1.01719 l -3.90083,4.28832 -3.92665,-4.28832 c -0.4015,-0.44238 -0.86435,-0.78467 -1.38854,-1.02688 -0.52421,-0.24217 -1.1033,-0.36487 -1.73729,-0.36812 -0.59847,0.004 -1.15819,0.11842 -1.67916,0.34229 -0.52097,0.2239 -0.97736,0.53175 -1.36917,0.92354 l 7.05249,7.74998 -7.05249,7.74998 c 0.39181,0.40419 0.8482,0.71957 1.36917,0.94615 0.52097,0.22657 1.08069,0.34175 1.67916,0.34552 0.62538,-0.005 1.20878,-0.13079 1.7502,-0.37782 0.54142,-0.24703 1.00857,-0.58609 1.40146,-1.01718 l 3.90082,-4.28832 3.90083,4.28832 c 0.39125,0.43109 0.85517,0.77015 1.39177,1.01718 0.53656,0.24703 1.12319,0.37297 1.75989,0.37782 0.59845,-0.004 1.15817,-0.11895 1.67916,-0.34552 0.52095,-0.22658 0.97734,-0.54196 1.36916,-0.94615 l -7.05248,-7.74998 z"
style="fill:#ffffff;fill-opacity:1" />
</g>
</g>
<path
inkscape:connector-curvature="0"
id="path1290"
style="fill:#e57000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.730891"
d="m 141.89758,116.88641 25.41237,27.92599 c -2.7927,2.88547 -6.70224,4.65495 -10.98527,4.65495 -4.56076,0 -8.56315,-1.95514 -11.35592,-5.02707 l -14.05575,-15.45172 -10.9846,-12.10215 10.9846,-12.00854 14.05575,-15.452532 c 2.79277,-3.071175 6.79516,-5.027074 11.35592,-5.027074 4.28303,0 8.19257,1.768759 10.98527,4.561525 z"
sodipodi:nodetypes="ccscccccscc" />
<path
inkscape:connector-curvature="0"
id="path1292"
style="fill:#e57000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.730891"
d="m 92.334245,116.8861 -25.41238,27.92603 c 2.7927,2.88547 6.702237,4.65495 10.985287,4.65495 4.560744,0 8.563138,-1.95514 11.355905,-5.02707 L 103.3188,128.98825 114.30338,116.8861 103.3188,104.87756 89.263057,89.425028 c -2.792767,-3.071215 -6.795161,-5.027074 -11.355905,-5.027074 -4.28305,0 -8.192587,1.768759 -10.985287,4.561526 z"
sodipodi:nodetypes="ccscccccscc" />
<path
inkscape:connector-curvature="0"
id="path1294"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.66712"
d="m 127.12922,130.73289 -10.02585,-11.04587 -10.0263,11.04587 -23.195348,25.48982 c 2.548811,2.54902 6.118169,4.16327 10.025148,4.16327 4.16359,0 7.64778,-1.69975 10.28111,-4.58817 l 12.91539,-14.10405 12.82882,14.10405 c 2.5493,2.80293 6.20218,4.58817 10.36513,4.58817 3.9091,0 7.47768,-1.61425 10.02652,-4.16327 z"
sodipodi:nodetypes="ccccscccscc" />
<path
inkscape:connector-curvature="0"
id="path1296"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.66712"
d="M 127.1286,103.03813 117.10275,114.084 107.07645,103.03813 83.881116,77.548321 c 2.548838,-2.548932 6.118156,-4.163226 10.025162,-4.163226 4.163603,0 7.647762,1.699732 10.281082,4.588143 l 12.91539,14.104094 12.82882,-14.104094 c 2.54934,-2.802999 6.20221,-4.588143 10.36513,-4.588143 3.90911,0 7.47772,1.614294 10.02653,4.163226 z"
sodipodi:nodetypes="ccccscccscc" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

-35
View File
@@ -1,35 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none" viewBox="0 0 512 512">
<g transform="translate(256 256)scale(8.96)">
<linearGradient id="a" x1="6.221" x2="37.408" y1="6.221" y2="37.408" gradientUnits="userSpaceOnUse">
<stop offset="0%" style="stop-color:#f0f0f0;stop-opacity:1"/>
<stop offset="100%" style="stop-color:#bbc1c4;stop-opacity:1"/>
</linearGradient>
<path d="M24 5C13.507 5 5 13.507 5 24s8.507 19 19 19 19-8.507 19-19S34.493 5 24 5" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#a);fill-rule:nonzero;opacity:1" transform="translate(-24 -24)"/>
</g>
<g transform="translate(256 256)scale(8.96)">
<linearGradient id="b" x1="12.859" x2="35.224" y1="12.859" y2="35.224" gradientUnits="userSpaceOnUse">
<stop offset="0%" style="stop-color:#e04f12;stop-opacity:1"/>
<stop offset="61.5%" style="stop-color:#ce400d;stop-opacity:1"/>
<stop offset="100%" style="stop-color:#c03409;stop-opacity:1"/>
</linearGradient>
<path d="M24 40c8.837 0 16-7.163 16-16S32.837 8 24 8 8 15.163 8 24s7.163 16 16 16" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#b);fill-rule:nonzero;opacity:1" transform="translate(-24 -24)"/>
</g>
<path d="m30.414 20 3.89-3.89c.708-.708.449-1.772 0-2.221l-2.195-2.195a1.573 1.573 0 0 0-2.218.001l-7.194 7.194c-.549.549-.752 1.469-.001 2.22l7.196 7.196c.76.76 1.592.625 2.218-.001l2.194-2.194c.707-.707.716-1.505.001-2.22z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#000;fill-rule:nonzero;opacity:.05" transform="translate(40.96 40.96)scale(8.96)"/>
<path d="m33.951 14.244-2.195-2.195a1.07 1.07 0 0 0-1.511 0l-7.195 7.195c-.386.386-.487 1.025 0 1.512l7.196 7.196c.491.491 1.087.424 1.511 0l2.195-2.195c.464-.464.469-1.044 0-1.512L29.707 20l4.244-4.244c.465-.465.335-1.177 0-1.512" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#000;fill-rule:nonzero;opacity:.07" transform="translate(40.96 40.96)scale(8.96)"/>
<path d="m17.586 28-3.89 3.89c-.708.708-.449 1.772 0 2.221l2.195 2.195c.611.609 1.606.61 2.218-.001l7.194-7.194c.549-.549.752-1.469.001-2.22l-7.196-7.196c-.76-.76-1.592-.625-2.218.001l-2.194 2.194c-.707.707-.716 1.505-.001 2.22z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#000;fill-rule:nonzero;opacity:.05" transform="translate(40.96 40.96)scale(8.96)"/>
<path d="m14.049 33.756 2.195 2.195a1.07 1.07 0 0 0 1.511 0l7.195-7.195c.386-.386.487-1.025 0-1.512l-7.196-7.196c-.491-.491-1.087-.424-1.511 0l-2.195 2.195c-.464.464-.469 1.044 0 1.512L18.293 28l-4.244 4.244c-.465.465-.335 1.177 0 1.512" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#000;fill-rule:nonzero;opacity:.07" transform="translate(40.96 40.96)scale(8.96)"/>
<g transform="translate(296.83 220.16)scale(8.96)">
<linearGradient id="c" x1="23.755" x2="38.564" y1="9.93" y2="33.557" gradientUnits="userSpaceOnUse">
<stop offset="0%" style="stop-color:#fcfcfc;stop-opacity:1"/>
<stop offset="100%" style="stop-color:#c3c9cd;stop-opacity:1"/>
</linearGradient>
<path d="m33.598 14.598-2.196-2.196a.57.57 0 0 0-.804 0l-7.196 7.196a.57.57 0 0 0 0 .804l7.196 7.196a.57.57 0 0 0 .804 0l2.196-2.196a.57.57 0 0 0 0-.804L29 20l4.598-4.598a.57.57 0 0 0 0-.804" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#c);fill-rule:nonzero;opacity:1" transform="translate(-28.5 -20)"/>
</g>
<g transform="translate(215.17 291.84)scale(8.96)">
<linearGradient id="d" x1="11.438" x2="26.247" y1="17.637" y2="41.265" gradientUnits="userSpaceOnUse">
<stop offset="0%" style="stop-color:#fcfcfc;stop-opacity:1"/>
<stop offset="100%" style="stop-color:#c3c9cd;stop-opacity:1"/>
</linearGradient>
<path d="M14.402 23.402 19 28l-4.598 4.598a.57.57 0 0 0 0 .804l2.196 2.196a.57.57 0 0 0 .804 0l7.196-7.196a.57.57 0 0 0 0-.804l-7.196-7.196a.57.57 0 0 0-.804 0l-2.196 2.196a.57.57 0 0 0 0 .804" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#d);fill-rule:nonzero;opacity:1" transform="translate(-19.5 -28)"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.6 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#75aadb" d="M71.4 38.8c-1.5-.6-3.9-1-6.9-1.1-4.2-.1-9 .4-9.2.5v20c13.3.6 15.5-1.7 15.5-1.7 11.6-5.9 4.3-16.2.6-17.7z"/><path fill="#75aadb" d="M64 0C28.6 0 0 28.6 0 64s28.6 64 64 64 64-28.6 64-64S99.3 0 64 0zm28.6 89.8H82L64.4 63.5h-9V84h9v5.8H41.5v-5.7l7.6-.1-.1-45.9c-.8-.2-7.5-.8-7.5-.8V32c1 1 7.9 1.2 7.9 1.2 1.6.1 3.9.2 5.2-.1 9.3-1.7 16.4-.4 16.4-.4 14 3.2 14.2 15.8 10.3 22.6-3.5 5.8-10.3 7.2-10.3 7.2l14.4 21.8 7.2-.1v5.6z"/><path d="M41.595 87.073v-2.726l1.82-.141a59.125 59.125 0 013.752-.144h1.931V37.996l-.938-.127c-.516-.07-2.204-.248-3.752-.397l-2.813-.27v-2.51c0-2.332.027-2.495.39-2.3 1.583.847 10.7 1.07 15.83.388 4.202-.558 11.495-.425 14.035.257 5.483 1.472 9.11 4.646 10.824 9.473.717 2.018.817 5.847.216 8.224-.903 3.572-2.39 6.048-4.865 8.101-1.482 1.23-4.847 3.03-6.145 3.29-.397.079-.772.224-.832.321-.06.098 3.123 5.072 7.075 11.054l7.184 10.876 3.633-.068 3.634-.068V89.8l-5.242-.008-5.24-.007-8.82-13.234-8.817-13.234h-9.178V84.061h9.049V89.8H41.595zm25.158-29.162c3.476-.55 7.265-2.774 8.973-5.263 2.511-3.663 1.537-8.99-2.294-12.547-1.357-1.26-2.205-1.63-4.794-2.1-2.124-.386-8.66-.454-11.706-.122l-1.544.168-.058 10.083-.057 10.082.72.106c1.366.2 8.67-.075 10.76-.407z" fill="#fff" stroke="#fff" stroke-width=".788"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-5
View File
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="32" viewBox="0 0 375 375" width="32" xmlns="http://www.w3.org/2000/svg">
<rect fill="#0071ff" height="375.001088" rx="58.59392" stroke-width=".91553" width="375.001088" x=".0009759" y="-.0066962"/>
<path d="m150.428 322.264c-29.063-6.202-53.897-22.439-73.115-47.804-19.507-25.746-27.838-55.355-25.723-91.414 6.655-62.013 47.667-106.753 99.687-120.411 4.509-.989 8.353-3.462 12.55-1.322 3.22 1.64 6.028 4.467 7.206 7.251 1.25 2.955 1.877 21.54.99 29.331-1.076 9.46-3.877 12.418-14.566 15.388-29.723 10.195-48.105 34.07-53.697 61.017-4.8 29.668 2.951 59.729 21.528 78.727 8.966 8.993 17.92 14.24 30.869 18.086 8.646 2.57 13.393 5.758 15.036 10.102 1.085 2.867 1.63 22.984.779 28.772-1.33 9.046-1.702 9.796-5.792 11.667-5.029 2.3-7.404 2.392-15.752.61zm50.708.29c-3.092-1.402-5.673-4.83-6.73-8.94-.134-9.408-2.366-25.754 1.02-33.373 1.88-4.128 4.65-5.999 12.433-8.396 21.267-6.551 37.593-19.88 46.806-38.213 11.11-22.108 11.877-55.183 1.808-77.975-9.154-20.723-25.7-35.217-48.555-42.534-8.872-2.84-12.004-5.065-12.968-9.21-1.002-4.31-1.435-19.87-.785-28.218.682-8.766 1.249-9.99 6.162-13.318 3.701-2.505 5.482-2.446 17.223.575 36.718 10.077 65.97 33.597 83.026 66.68 18.495 37.034 19.191 86.11 1.742 122.655-17.233 36.09-50.591 62.511-88.622 70.194-8.172 1.65-9.07 1.656-12.56.073z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-5
View File
@@ -1,5 +0,0 @@
<svg width="400" height="400" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.41508 17.2983L7.88484 12.7653L9.51146 18.9412L11.8745 18.2949L9.52018 9.32758L0.69527 6.93747L0.066864 9.35199L6.13926 11.0015L1.68806 15.5279L3.41508 17.2983Z" fill="#F34E3F"/>
<path d="M16.3044 12.0436L18.6675 11.3973L16.3132 2.43003L7.48824 0.0399246L6.85984 2.45444L14.312 4.47881L16.3044 12.0436Z" fill="#F34E3F"/>
<path d="M12.9126 15.4902L15.2756 14.8439L12.9213 5.87659L4.09639 3.48648L3.46799 5.901L10.9201 7.92537L12.9126 15.4902Z" fill="#F34E3F"/>
</svg>

Before

Width:  |  Height:  |  Size: 576 B

-5
View File
@@ -1,5 +0,0 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M66.1403 24.9315H106.4V33.5714H14.6005V99.3601H106.4V33.5714L115 33.5682V107.997L14.591 108V99.3695H6V24.9409H14.0531V24.9346L57.5399 24.9315V12H66.1403V24.9315Z" fill="white"/>
<path d="M77.94 57.3044V70.2485H69.3521V57.3044H77.94Z" fill="white"/>
<path d="M52.1701 57.3044V70.2485H43.5822V57.3044H52.1701Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 452 B

-1
View File
@@ -1 +0,0 @@
<svg fill="none" height="2500" width="2500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 160"><g clip-rule="evenodd" fill-rule="evenodd"><path d="M0 116h160v28.996c0 8.287-6.722 15.004-14.998 15.004H14.998C6.716 160 0 153.293 0 144.996zm0 0h160v30H0z" fill="#1bb91f"/><path d="M83 70V0h-6v146h6V76h77v-6zM0 15.007C0 6.719 6.722 0 14.998 0h130.004C153.285 0 160 6.725 160 15.007V146H0z" fill="#3c3c3c"/></g></svg>

Before

Width:  |  Height:  |  Size: 419 B

-3
View File
@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="90" viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 5.625C6.8842 5.625 5.625 6.8842 5.625 8.4375V70.3125H0V8.4375C0 3.7776 3.7776 0 8.4375 0H83.7925C87.551 0 89.4333 4.5442 86.7756 7.20186L40.3642 53.6133H53.4375V47.8125H59.0625V55.0195C59.0625 57.3495 57.1737 59.2383 54.8438 59.2383H34.7392L25.0712 68.9062H68.9062V33.75H74.5312V68.9062C74.5312 72.0128 72.0128 74.5312 68.9062 74.5312H19.4462L9.60248 84.375H81.5625C83.1158 84.375 84.375 83.1158 84.375 81.5625V19.6875H90V81.5625C90 86.2224 86.2224 90 81.5625 90H6.20749C2.44898 90 0.566723 85.4558 3.22438 82.7981L49.46 36.5625H36.5625V42.1875H30.9375V35.1562C30.9375 32.8263 32.8263 30.9375 35.1562 30.9375H55.085L64.9288 21.0938H21.0938V56.25H15.4688V21.0938C15.4688 17.9871 17.9871 15.4688 21.0938 15.4688H70.5538L80.3975 5.625H8.4375Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 904 B

-22
View File
@@ -1,22 +0,0 @@
# Ignore symlinks to avoid Prettier errors
CLAUDE.md
.github/copilot-instructions.md
# Ignore node_modules and dependencies
node_modules/
# Ignore Terraform files (formatted by terraform fmt)
*.tf
*.hcl
*.tfvars
# Ignore generated and temporary files
.terraform/
*.tfstate
*.tfstate.backup
*.tfstate.lock.info
# Ignore other files that shouldn't be formatted
bun.lock
go.sum
go.mod
-168
View File
@@ -1,168 +0,0 @@
# AGENTS.md
This file provides guidance to AI coding assistants when working with code in this repository.
## Project Overview
The Coder Registry is a community-driven repository for Terraform modules and templates that extend Coder workspaces. It's organized with:
- **Modules**: Individual components and tools (IDEs, auth integrations, dev tools)
- **Templates**: Complete workspace configurations for different platforms
- **Namespaces**: Each contributor has their own namespace under `/registry/[namespace]/`
## Common Development Commands
### Formatting
```bash
bun run fmt # Format all code (Prettier + Terraform)
bun run fmt:ci # Check formatting (CI mode)
```
### Testing
```bash
# Test all modules with .tftest.hcl files
bun run test
# Test specific module (from module directory)
terraform init -upgrade
terraform test -verbose
# Validate Terraform syntax
./scripts/terraform_validate.sh
```
### Module Creation
```bash
# Generate new module scaffold
./scripts/new_module.sh namespace/module-name
```
### TypeScript Testing & Setup
The repository uses Bun for TypeScript testing with utilities:
- `test/test.ts` - Testing utilities for container management and Terraform operations
- `setup.ts` - Test cleanup (removes .tfstate files and test containers)
- Container-based testing with Docker for module validation
## Architecture & Organization
### Directory Structure
```
registry/[namespace]/
├── README.md # Contributor info with frontmatter
├── .images/ # Namespace avatar (avatar.png/svg)
├── modules/ # Individual components
│ └── [module]/ # Each module has main.tf, README.md, tests
└── templates/ # Complete workspace configs
└── [template]/ # Each template has main.tf, README.md
```
### Key Components
**Module Structure**: Each module contains:
- `main.tf` - Terraform implementation
- `README.md` - Documentation with YAML frontmatter
- `.tftest.hcl` - Terraform test files (required)
- `run.sh` - Optional startup script
**Template Structure**: Each template contains:
- `main.tf` - Complete Coder template configuration
- `README.md` - Documentation with YAML frontmatter
- Additional configs, scripts as needed
### README Frontmatter Requirements
All modules/templates require YAML frontmatter:
```yaml
---
display_name: "Module Name"
description: "Brief description"
icon: "../../../../.icons/tool.svg"
verified: false
tags: ["tag1", "tag2"]
---
```
## Testing Requirements
### Module Testing
- Every module MUST have `.tftest.hcl` test files
- Optional `main.test.ts` files for container-based testing or complex business logic validation
- Tests use Docker containers with `--network=host` flag
- Linux required for testing (Docker Desktop on macOS/Windows won't work)
- Use Colima or OrbStack on macOS instead of Docker Desktop
### Test Utilities
The `test/test.ts` file provides:
- `runTerraformApply()` - Execute Terraform with variables
- `executeScriptInContainer()` - Run coder_script resources in containers
- `testRequiredVariables()` - Validate required variables
- Container management functions
## Validation & Quality
### Automated Validation
The Go validation tool (`cmd/readmevalidation/`) checks:
- Repository structure integrity
- Contributor README files
- Module and template documentation
- Frontmatter format compliance
### Versioning
Use semantic versioning for modules:
- **Patch** (1.2.3 → 1.2.4): Bug fixes
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
- **Major** (1.2.3 → 2.0.0): Breaking changes
## Dependencies & Tools
### Required Tools
- **Terraform** - Module development and testing
- **Docker** - Container-based testing
- **Bun** - JavaScript runtime for formatting/scripts
- **Go 1.23+** - Validation tooling
### Development Dependencies
- Prettier with Terraform and shell plugins
- TypeScript for test utilities
- Various npm packages for documentation processing
## Workflow Notes
### Contributing Process
1. Create namespace (first-time contributors)
2. Generate module/template files using scripts
3. Implement functionality and tests
4. Run formatting and validation
5. Submit PR with appropriate template
### Testing Workflow
- All modules must pass `terraform test`
- Use `bun run test` for comprehensive testing
- Format code with `bun run fmt` before submission
- Manual testing recommended for templates
### Namespace Management
- Each contributor gets unique namespace
- Namespace avatar required (avatar.png/svg in .images/)
- Namespace README with contributor info and frontmatter
-1
View File
@@ -1 +0,0 @@
AGENTS.md
-3
View File
@@ -1,3 +0,0 @@
# Code of Conduct
[Please see our code of conduct on the official Coder website](https://coder.com/docs/contributing/CODE_OF_CONDUCT)
+120 -422
View File
@@ -1,502 +1,200 @@
# Contributing to the Coder Registry
# Contributing
Welcome! This guide covers how to contribute to the Coder Registry, whether you're creating a new module or improving an existing one.
## Getting started
## What is the Coder Registry?
This repo uses two main runtimes to verify the correctness of a module/template before it is published:
The Coder Registry is a collection of Terraform modules and templates for Coder workspaces. Modules provide IDEs, authentication integrations, development tools, and other workspace functionality. Templates provide complete workspace configurations for different platforms and use cases that appear as community templates on the registry website.
- [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
## Types of Contributions
### Installing Bun
- **[New Modules](#creating-a-new-module)** - Add support for a new tool or functionality
- **[New Templates](#creating-a-new-template)** - Create complete workspace configurations
- **[Existing Modules](#contributing-to-existing-modules)** - Fix bugs, add features, or improve documentation
- **[Existing Templates](#contributing-to-existing-templates)** - Improve workspace templates
- **[Bug Reports](#reporting-issues)** - Report problems or request features
To install Bun, you can run this command on Linux/MacOS:
## Setup
### Prerequisites
- Basic Terraform knowledge (for module development)
- Terraform installed ([installation guide](https://developer.hashicorp.com/terraform/install))
- Docker (for running tests)
### Install Dependencies
Install Bun (for formatting and scripts):
```bash
```shell
curl -fsSL https://bun.sh/install | bash
```
Install project dependencies:
Or this command on Windows:
```bash
bun install
```shell
powershell -c "irm bun.sh/install.ps1 | iex"
```
### Understanding Namespaces
Follow the instructions to ensure that Bun is available globally. Once Bun is installed, install all necessary dependencies from the root of the repo:
All modules and templates are organized under `/registry/[namespace]/`. Each contributor gets their own namespace with both modules and templates directories:
Via NPM:
```
registry/[namespace]/
├── modules/ # Individual components and tools
└── templates/ # Complete workspace configurations
```shell
npm i
```
For example: `/registry/your-username/modules/` and `/registry/your-username/templates/`. If a namespace is taken, choose a different unique namespace, but you can still use any display name on the Registry website.
Via PNPM:
### Images and Icons
- **Namespace avatars**: Must be named `avatar.png` or `avatar.svg` in `/registry/[namespace]/.images/`
- **Module screenshots/demos**: Use `/registry/[namespace]/.images/` for module-specific images
- **Module icons**: Use the shared `/.icons/` directory at the root for module icons
---
## Creating a New Module
### 1. Create Your Namespace (First Time Only)
If you're a new contributor, create your namespace:
```bash
mkdir -p registry/[your-username]
mkdir -p registry/[your-username]/.images
```shell
pnpm i
```
#### Add Your Avatar
This repo does not support Yarn.
Every namespace must have an avatar. We recommend using your GitHub avatar:
### Installing Go (optional)
1. Download your GitHub avatar from `https://github.com/[your-username].png`
2. Save it as `avatar.png` in `registry/[your-username]/.images/`
3. This gives you a properly sized, square image that's already familiar to the community
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.
The avatar must be:
[Navigate to the official Go Installation page](https://go.dev/doc/install), and install the correct version for your operating system.
- Named exactly `avatar.png` or `avatar.svg`
- Square image (recommended: 400x400px minimum)
- Supported formats: `.png` or `.svg` only
Once Go has been installed, verify the installation via:
#### Create Your Namespace README
Create `registry/[your-username]/README.md`:
```markdown
---
display_name: "Your Name"
bio: "Brief description of who you are and what you do"
avatar: "./.images/avatar.png"
github: "your-username"
linkedin: "https://www.linkedin.com/in/your-username" # Optional
website: "https://yourwebsite.com" # Optional
support_email: "you@example.com" # Optional
status: "community"
---
# Your Name
Brief description of who you are and what you do.
```shell
go version
```
> **Note**: The `avatar` must point to `./.images/avatar.png` or `./.images/avatar.svg`.
### Adding a new module/template (coming soon)
### 2. Generate Module Files
Once Bun (and possibly Go) have been installed, clone this repository. From there, you can run this script to make it easier to start contributing a new module or template:
```bash
./scripts/new_module.sh [your-username]/[module-name]
cd registry/[your-username]/modules/[module-name]
```shell
./new.sh NAME_OF_NEW_MODULE
```
This script generates:
You can also create the correct module/template files manually.
- `main.tf` - Terraform configuration template
- `README.md` - Documentation template with frontmatter
- `run.sh` - Script for module execution (can be deleted if not required)
## Testing a Module
### 3. Build Your Module
> [!IMPORTANT]
> It is the responsibility of the module author to implement tests for every new module they wish to contribute. It falls to the author to test the module locally before submitting a PR.
1. **Edit `main.tf`** to implement your module's functionality
2. **Update `README.md`** with:
- Accurate description and usage examples
- Correct icon path (usually `../../../../.icons/your-icon.svg`)
- Proper tags that describe your module
3. **Create at least one `.tftest.hcl`** to test your module with `terraform test`
4. **Add any scripts** or additional files your module needs
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.
### 4. Test and Submit
> [!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 and 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.
```bash
# Test your module (from the module directory)
terraform init -upgrade
terraform test -verbose
You can reference the existing `*.test.ts` files to get an idea for how to set up tests.
# Or run all tests in the repo
./scripts/terraform_test_all.sh
You can run all tests by running this command:
# Format code
bun run fmt
# Commit and create PR (do not push to main directly)
git add .
git commit -m "Add [module-name] module"
git push origin your-branch
```shell
bun test
```
> **Important**: It is your responsibility to implement tests for every new module. Test your module locally before opening a PR. The testing suite requires Docker containers with the `--network=host` flag, which typically requires running tests on Linux (this flag doesn't work with Docker Desktop on macOS/Windows). macOS users can use [Colima](https://github.com/abiosoft/colima) or [OrbStack](https://orbstack.dev/) instead of Docker Desktop.
Note that tests can take some time to run, so you probably don't want to be running this as part of your development loop.
---
To run specific tests, you can use the `-t` flag, which accepts a filepath regex:
## Creating a New Template
Templates are complete Coder workspace configurations that users can deploy directly. Unlike modules (which are components), templates provide full infrastructure definitions for specific platforms or use cases.
### Template Structure
Templates follow the same namespace structure as modules but are located in the `templates` directory:
```
registry/[your-username]/templates/[template-name]/
├── main.tf # Complete Terraform configuration
├── README.md # Documentation with frontmatter
├── [additional files] # Scripts, configs, etc.
```shell
bun test -t '<regex_pattern>'
```
### 1. Create Your Template Directory
To ensure that the module runs predictably in local development, you can update the Terraform source as follows:
```bash
mkdir -p registry/[your-username]/templates/[template-name]
cd registry/[your-username]/templates/[template-name]
```
### 2. Create Template Files
#### main.tf
Your `main.tf` should be a complete Coder template configuration including:
- Required providers (coder, and your infrastructure provider)
- Coder agent configuration
- Infrastructure resources (containers, VMs, etc.)
- Registry modules for IDEs, tools, and integrations
Example structure:
```terraform
terraform {
required_providers {
coder = {
source = "coder/coder"
}
# Add your infrastructure provider (docker, aws, etc.)
}
```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>"
}
# Coder data sources
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
# Coder agent
resource "coder_agent" "main" {
arch = "amd64"
os = "linux"
startup_script = <<-EOT
# Startup commands here
EOT
}
# Registry modules for IDEs, tools, and integrations
module "code-server" {
source = "registry.coder.com/coder/code-server/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
}
# Your infrastructure resources
# (Docker containers, AWS instances, etc.)
```
#### README.md
## Adding/modifying README files
Create documentation with proper frontmatter:
This repo uses Go to do a quick validation of each README. If you are working with the README files at all, it is strongly recommended that you install Go, so that the files can be validated locally.
```markdown
---
display_name: "Template Name"
description: "Brief description of what this template provides"
icon: "../../../../.icons/platform.svg"
verified: false
tags: ["platform", "use-case", "tools"]
---
### Validating all README files
# Template Name
To validate all README files throughout the entire repo, you can run the following:
Describe what the template provides and how to use it.
Include any setup requirements, resource information, or usage notes that users need to know.
```shell
go build ./cmd/readmevalidation && ./readmevalidation
```
### 3. Test Your Template
The resulting binary is already part of the `.gitignore` file, but you can quickly remove it with:
Templates should be tested to ensure they work correctly. Test with Coder:
```bash
cd registry/[your-username]/templates/[template-name]
coder templates push [template-name] -d .
```shell
rm ./readmevalidation
```
### 4. Template Best Practices
### README validation criteria
- **Use registry modules**: Leverage existing modules for IDEs, tools, and integrations
- **Provide sensible defaults**: Make the template work out-of-the-box
- **Include metadata**: Add useful workspace metadata (CPU, memory, disk usage)
- **Document prerequisites**: Clearly explain infrastructure requirements
- **Use variables**: Allow customization of common settings
- **Follow naming conventions**: Use descriptive, consistent naming
The following criteria exists for one of two reasons: (1) content accessibility, or (2) having content be designed in a way that's easy for the Registry site build step to use:
### 5. Template Guidelines
#### General README requirements
- Templates appear as "Community Templates" on the registry website
- Include proper error handling and validation
- Test with Coder before submitting
- Document any required permissions or setup steps
- Use semantic versioning in your README frontmatter
- There must be a frontmatter section.
- There must be exactly one h1 header, and it must be at the very top
- 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.
- Additional image/video assets can be placed in one of two places:
- In the same user namespace directory where that user's main content lives
- In the top-level `.icons` directory
- Any `.hcl` code snippets must be labeled as `.tf` snippets instead
---
```txt
\`\`\`tf
Content
\`\`\`
```
## Contributing to Existing Templates
#### Contributor profiles
### 1. Types of Template Improvements
- The README body is allowed to be empty, but if it isn't, it must follow all the rules above.
- The frontmatter supports 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` (required string) Your GitHub handle
- `avatar_url` (optional string)  A relative/absolute URL pointing to your avatar
- `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
- `employer_github` (optional string) The name of another user namespace whom you'd like to have associated with your account. The namespace must also exist in the repo, or else the README validation will fail.
- `status` (optional string union)  If defined, must be one of "community", "partner", or "official". "Community" is treated as the default value if not specified, and should be used for the majority of external contributions. "Official" should be used for Coder and Coder satellite companies. "Partner" is for companies who have a formal business agreement with Coder.
**Bug fixes:**
#### Modules and templates
- Fix infrastructure provisioning issues
- Resolve agent connectivity problems
- Correct resource naming or tagging
- The frontmatter supports the following fields:
- `description` (required string) A short description of what the module/template does.
- `icon` (required string)  A URL pointing to the icon to use for the module/template when viewing it on the Registry website.
- `display_name` (optional string) A name to display instead of the name intuited from the module's/template's directory name
- `verified` (optional boolean)  A boolean indicated that the Coder team has officially tested and vouched for the functionality/reliability of a given module or template. This field should only be changed by Coder employees.
- `tags` (optional string array) A list of tags to associate with the module/template. Users will be able to search for these tags from the Registry website.
**Feature additions:**
## Releases
- Add new registry modules for additional functionality
- Include additional infrastructure options
- Improve startup scripts or automation
The release process is automated with these steps:
**Platform updates:**
### 1. Create and merge a new PR
- Update base images or AMIs
- Adapt to new platform features
- Improve security configurations
- Create a PR with your module changes
- Get your PR reviewed, approved, and merged into the `main` branch
**Documentation:**
### 2. Prepare Release (Maintainer Task)
- Clarify prerequisites and setup steps
- Add troubleshooting guides
- Improve usage examples
After merging to `main`, a maintainer will:
### 2. Testing Template Changes
- View all modules and their current versions:
Testing template modifications thoroughly is necessary. Test with Coder:
```shell
./release.sh --list
```
```bash
coder templates push test-[template-name] -d .
```
- Determine the next version number based on changes:
### 3. Maintain Compatibility
- **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)
- Don't remove existing variables without clear migration path
- Preserve backward compatibility when possible
- Test that existing workspaces still function
- Document any breaking changes clearly
- Create and push an annotated tag:
---
```shell
# Fetch latest changes
git fetch origin
# Create and push tag
./release.sh module-name 1.2.3 --push
```
## Contributing to Existing Modules
The tag format will be: `release/module-name/v1.2.3`
### 1. Make Your Changes
### 3. Publishing to Coder Registry
**For bug fixes:**
Our automated processes will handle publishing new data to [registry.coder.com](https://registry.coder.com).
- Reproduce the issue
- Fix the code in `main.tf`
- Add/update tests
- Update documentation if needed
**For new features:**
- Add new variables with sensible defaults
- Implement the feature
- Add tests for new functionality
- Update README with new variables
**For documentation:**
- Fix typos and unclear explanations
- Add missing variable documentation
- Improve usage examples
### 2. Test Your Changes
```bash
# Test a specific module (from the module directory)
terraform init -upgrade
terraform test -verbose
# Test all modules
./scripts/terraform_test_all.sh
```
### 3. Maintain Backward Compatibility
- New variables should have default values
- Don't break existing functionality
- Test that minimal configurations still work
---
## Submitting Changes
1. **Fork and branch:**
```bash
git checkout -b fix/module-name-issue
```
2. **Commit with clear messages:**
```bash
git commit -m "Fix version parsing in module-name"
```
3. **Open PR with:**
- Clear title describing the change
- What you changed and why
- Any breaking changes
### Using PR Templates
We have different PR templates for different types of contributions. GitHub will show you options to choose from, or you can manually select:
- **New Module**: Use `?template=new_module.md`
- **New Template**: Use `?template=new_template.md`
- **Bug Fix**: Use `?template=bug_fix.md`
- **Feature**: Use `?template=feature.md`
- **Documentation**: Use `?template=documentation.md`
Example: `https://github.com/coder/registry/compare/main...your-branch?template=new_module.md`
---
## Requirements
### Every Module Must Have
- `main.tf` - Terraform code
- One or more `.tftest.hcl` files - Working tests with `terraform test`
- `README.md` - Documentation with frontmatter
### Every Template Must Have
- `main.tf` - Complete Terraform configuration
- `README.md` - Documentation with frontmatter
Templates don't require test files like modules do, but should be manually tested before submission.
### README Frontmatter
Module README frontmatter must include:
```yaml
---
display_name: "Module Name" # Required - Name shown on Registry website
description: "What it does" # Required - Short description
icon: "../../../../.icons/tool.svg" # Required - Path to icon file
verified: false # Optional - Set by maintainers only
tags: ["tag1", "tag2"] # Required - Array of descriptive tags
---
```
### README Requirements
All README files must follow these rules:
- Must have frontmatter section with proper YAML
- Exactly one h1 header directly below frontmatter
- When increasing header levels, increment by one each time
- Use `tf` instead of `hcl` for code blocks
### Best Practices
- Use descriptive variable names and descriptions
- Include helpful comments
- Test all functionality
- Follow existing code patterns in the module
---
## Versioning Guidelines
When you modify a module, you need to update its version number in the README. Understanding version numbers helps you describe the impact of your changes:
- **Patch** (1.2.3 → 1.2.4): Bug fixes
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
- **Major** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing types)
### Updating Module Versions
If your changes require a version bump, use the version bump script:
```bash
# For bug fixes
./.github/scripts/version-bump.sh patch
# For new features
./.github/scripts/version-bump.sh minor
# For breaking changes
./.github/scripts/version-bump.sh major
```
The script will:
1. Detect which modules you've modified
2. Calculate the new version number
3. Update all version references in the module's README
4. Show you a summary of changes
**Important**: Only run the version bump script if your changes require a new release. Documentation-only changes don't need version updates.
---
## Reporting Issues
When reporting bugs, include:
- Module name and version
- Expected vs actual behavior
- Minimal reproduction case
- Error messages
- Environment details (OS, Terraform version)
---
## Getting Help
- **Examples**: Check `/registry/coder/modules/` for well-structured modules and `/registry/coder/templates/` for complete templates
- **Issues**: Open an issue for technical problems
- **Community**: Reach out to the Coder community for questions
## Common Pitfalls
1. **Missing frontmatter** in README
2. **No tests** or broken tests
3. **Hardcoded values** instead of variables
4. **Breaking changes** without defaults
5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`) before submitting
## For Maintainers
Guidelines for reviewing PRs, managing releases, and maintaining the registry. [See the maintainer guide for detailed information.](./MAINTAINER.md)
Happy contributing! 🚀
> [!NOTE]
> Some data in registry.coder.com is fetched on demand from the [coder/modules](https://github.com/coder/modules) repo's `main` branch. This data should update almost immediately after a release, while other changes will take some time to propagate.
-145
View File
@@ -1,145 +0,0 @@
# Maintainer Guide
Quick reference for maintaining the Coder Registry repository.
## Setup
Install Go for README validation:
```bash
# macOS
brew install go
# Linux
sudo apt install golang-go
```
## Reviewing a PR
Check that PRs have:
- [ ] All required files (`main.tf`, `README.md`, at least one `.tftest.hcl`)
- [ ] Proper frontmatter in README
- [ ] Working tests (`terraform test`)
- [ ] Formatted code (`bun run fmt`)
- [ ] Avatar image for new namespaces (`avatar.png` or `avatar.svg` in `.images/`)
- [ ] Version label: `version:patch`, `version:minor`, or `version:major`
### Version Guidelines
When reviewing PRs, ensure the version change follows semantic versioning:
- **Patch** (1.2.3 → 1.2.4): Bug fixes
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
- **Major** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing types)
PRs should clearly indicate the intended version change (e.g., `v1.2.3 → v1.2.4`) and include the appropriate label: `version:patch`, `version:minor`, or `version:major`.
The “Version Bump” CI uses this label to validate required updates (README version refs, etc.).
### Validate READMEs
```bash
go build ./cmd/readmevalidation && ./readmevalidation
```
## Making a Release
### Automated Tag and Release Process
After merging a PR, use the automated script to create and push release tags:
**Prerequisites:**
- Ensure all module versions are updated in their respective README files (the script uses this as the source of truth)
- Make sure you have the necessary permissions to push tags to the repository
**Steps:**
1. **Checkout the merge commit:**
```bash
git checkout MERGE_COMMIT_ID
```
2. **Run the tag release script:**
```bash
./scripts/tag_release.sh
```
3. **Review and confirm:**
- The script will automatically scan all modules in the registry
- It will detect which modules need version bumps by comparing README versions to existing tags
- A summary will be displayed showing which modules need tagging
- Confirm the list is correct when prompted
4. **Automatic tagging:**
- After confirmation, the script will automatically create all necessary release tags
- Tags will be pushed to the remote repository
- The script operates on the current checked-out commit
**Example output:**
```text
🔍 Scanning all modules for missing release tags...
📦 coder/code-server: v4.1.2 (needs tag)
✅ coder/dotfiles: v1.0.5 (already tagged)
## Tags to be created:
- `release/coder/code-server/v4.1.2`
❓ Do you want to proceed with creating and pushing these release tags?
Continue? [y/N]: y
```
### Manual Process (Fallback)
If the automated script fails, you can manually tag and release modules:
```bash
# Checkout the merge commit
git checkout MERGE_COMMIT_ID
# Create and push the release tag using the version from the PR
git tag -a "release/$namespace/$module/v$version" -m "Release $namespace/$module v$version"
git push origin release/$namespace/$module/v$version
```
Example: If PR shows `v1.2.3 → v1.2.4`, use `v1.2.4` in the tag.
### Publishing
Changes are automatically published to [registry.coder.com](https://registry.coder.com) after tags are pushed.
## README Requirements
### Module Frontmatter (Required)
```yaml
display_name: "Module Name"
description: "What it does"
icon: "../../../../.icons/tool.svg"
verified: false # Optional - Set by maintainers only
tags: ["tag1", "tag2"]
```
### Namespace Frontmatter (Required)
```yaml
display_name: "Your Name"
bio: "Brief description of who you are and what you do"
avatar: "./.images/avatar.png"
github: "username"
linkedin: "https://www.linkedin.com/in/username" # Optional
website: "https://yourwebsite.com" # Optional
support_email: "you@example.com" # Optional
status: "community" # or "partner", "official"
```
## Common Issues
- **README validation fails**: Check YAML syntax, ensure h1 header after frontmatter
- **Tests fail**: Ensure Docker with `--network=host`, check Terraform syntax
- **Wrong file structure**: Use `./scripts/new_module.sh` for new modules
- **Missing namespace avatar**: Must be `avatar.png` or `avatar.svg` in `.images/` directory
+1 -52
View File
@@ -1,54 +1,3 @@
# 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)
## For Maintainers
Guidelines for maintainers reviewing PRs and managing releases. [See the maintainer guide for more information.](./MAINTAINER.md)
Publish Coder modules and templates for other developers to use.
-3
View File
@@ -1,3 +0,0 @@
# Security
[Please see our security policy on the official Coder website](https://coder.com/security/policy)
-72
View File
@@ -1,72 +0,0 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "registry",
"devDependencies": {
"@types/bun": "^1.2.21",
"bun-types": "^1.2.21",
"dedent": "^1.6.0",
"gray-matter": "^4.0.3",
"marked": "^16.2.0",
"prettier": "^3.6.2",
"prettier-plugin-sh": "^0.18.0",
"prettier-plugin-terraform-formatter": "^1.2.1",
},
"peerDependencies": {
"typescript": "^5.8.3",
},
},
},
"packages": {
"@reteps/dockerfmt": ["@reteps/dockerfmt@0.3.6", "", {}, "sha512-Tb5wIMvBf/nLejTQ61krK644/CEMB/cpiaIFXqGApfGqO3GwcR3qnI0DbmkFVCl2OyEp8LnLX3EkucoL0+tbFg=="],
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
"@types/node": ["@types/node@24.0.14", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw=="],
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
"gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
"is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
"js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"marked": ["marked@16.2.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"prettier-plugin-sh": ["prettier-plugin-sh@0.18.0", "", { "dependencies": { "@reteps/dockerfmt": "^0.3.6", "sh-syntax": "^0.5.8" }, "peerDependencies": { "prettier": "^3.6.0" } }, "sha512-cW1XL27FOJQ/qGHOW6IHwdCiNWQsAgK+feA8V6+xUTaH0cD3Mh+tFAtBvEEWvuY6hTDzRV943Fzeii+qMOh7nQ=="],
"prettier-plugin-terraform-formatter": ["prettier-plugin-terraform-formatter@1.2.1", "", { "peerDependencies": { "prettier": ">= 1.16.0" }, "optionalPeers": ["prettier"] }, "sha512-rdzV61Bs/Ecnn7uAS/vL5usTX8xUWM+nQejNLZxt3I1kJH5WSeLEmq7LYu1wCoEQF+y7Uv1xGvPRfl3lIe6+tA=="],
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
"sh-syntax": ["sh-syntax@0.5.8", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-JfVoxf4FxQI5qpsPbkHhZo+n6N9YMJobyl4oGEUBb/31oQYlgTjkXQD8PBiafS2UbWoxrTO0Z5PJUBXEPAG1Zw=="],
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
}
}
Executable
BIN
View File
Binary file not shown.
-146
View File
@@ -1,146 +0,0 @@
package main
import (
"bufio"
"context"
"strings"
"golang.org/x/xerrors"
)
func validateCoderModuleReadmeBody(body string) []error {
var errs []error
trimmed := strings.TrimSpace(body)
if baseErrs := validateReadmeBody(trimmed); len(baseErrs) != 0 {
errs = append(errs, baseErrs...)
}
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, xerrors.New("all hcl code blocks 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, xerrors.New("did not find Terraform code block within h1 section"))
} else {
if terraformCodeBlockCount > 1 {
errs = append(errs, xerrors.New("cannot have more than one Terraform code block in h1 section"))
}
if !foundTerraformVersionRef {
errs = append(errs, xerrors.New("did not find Terraform code block that specifies 'version' field"))
}
}
if !foundParagraph {
errs = append(errs, xerrors.New("did not find paragraph within h1 section"))
}
if isInsideCodeBlock {
errs = append(errs, xerrors.New("code blocks inside h1 section do not all terminate before end of file"))
}
return errs
}
func validateCoderModuleReadme(rm coderResourceReadme) []error {
var errs []error
for _, err := range validateCoderModuleReadmeBody(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateResourceGfmAlerts(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if fmErrs := validateCoderResourceFrontmatter("modules", rm.filePath, rm.frontmatter); len(fmErrs) != 0 {
errs = append(errs, fmErrs...)
}
return errs
}
func validateAllCoderModuleReadmes(resources []coderResourceReadme) error {
var yamlValidationErrors []error
for _, readme := range resources {
errs := validateCoderModuleReadme(readme)
if len(errs) > 0 {
yamlValidationErrors = append(yamlValidationErrors, errs...)
}
}
if len(yamlValidationErrors) != 0 {
return validationPhaseError{
phase: validationPhaseReadme,
errors: yamlValidationErrors,
}
}
return nil
}
func validateAllCoderModules() error {
const resourceType = "modules"
allReadmeFiles, err := aggregateCoderResourceReadmeFiles(resourceType)
if err != nil {
return err
}
logger.Info(context.Background(), "processing template README files", "resource_type", resourceType, "num_files", len(allReadmeFiles))
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles)
if err != nil {
return err
}
err = validateAllCoderModuleReadmes(resources)
if err != nil {
return err
}
logger.Info(context.Background(), "processed README files as valid Coder resources", "resource_type", resourceType, "num_files", len(resources))
if err := validateCoderResourceRelativeURLs(resources); err != nil {
return err
}
logger.Info(context.Background(), "all relative URLs for READMEs are valid", "resource_type", resourceType)
return nil
}
-22
View File
@@ -1,22 +0,0 @@
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 := validateCoderModuleReadmeBody(testBody)
for _, e := range errs {
t.Error(e)
}
})
}
-355
View File
@@ -1,355 +0,0 @@
package main
import (
"bufio"
"errors"
"net/url"
"os"
"path"
"regexp"
"slices"
"strings"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
)
var (
supportedResourceTypes = []string{"modules", "templates"}
operatingSystems = []string{"windows", "macos", "linux"}
gfmAlertTypes = []string{"NOTE", "IMPORTANT", "CAUTION", "WARNING", "TIP"}
// TODO: This is a holdover from the validation logic used by the Coder Modules repo. It gives us some assurance, but
// realistically, we probably want to parse any Terraform code snippets, and make some deeper guarantees about how it's
// structured. Just validating whether it *can* be parsed as Terraform would be a big improvement.
terraformVersionRe = regexp.MustCompile(`^\s*\bversion\s+=`)
// Matches the format "> [!INFO]". Deliberately using a broad pattern to catch formatting issues that can mess up
// the renderer for the Registry website
gfmAlertRegex = regexp.MustCompile(`^>(\s*)\[!(\w+)\](\s*)(.*)`)
)
type coderResourceFrontmatter struct {
Description string `yaml:"description"`
IconURL string `yaml:"icon"`
DisplayName *string `yaml:"display_name"`
Verified *bool `yaml:"verified"`
Tags []string `yaml:"tags"`
OperatingSystems []string `yaml:"supported_os"`
}
// A slice version of the struct tags from coderResourceFrontmatter. Might be worth using reflection to generate this
// list at runtime in the future, but this should be okay for now
var supportedCoderResourceStructKeys = []string{
"description", "icon", "display_name", "verified", "tags", "supported_os",
// TODO: This is an old, officially deprecated key from the archived coder/modules repo. We can remove this once we
// make sure that the Registry Server is no longer checking this field.
"maintainer_github",
}
// 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 validateSupportedOperatingSystems(systems []string) []error {
var errs []error
for _, s := range systems {
if slices.Contains(operatingSystems, s) {
continue
}
errs = append(errs, xerrors.Errorf("detected unknown operating system %q", s))
}
return errs
}
func validateCoderResourceDisplayName(displayName *string) error {
if displayName != nil && *displayName == "" {
return xerrors.New("if defined, display_name must not be empty string")
}
return nil
}
func validateCoderResourceDescription(description string) error {
if description == "" {
return xerrors.New("frontmatter description cannot be empty")
}
return nil
}
func isPermittedRelativeURL(checkURL string) bool {
// 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.
return strings.HasPrefix(checkURL, "./") || strings.HasPrefix(checkURL, "/") || strings.HasPrefix(checkURL, "../../../../.icons")
}
func validateCoderResourceIconURL(iconURL string) []error {
if iconURL == "" {
return []error{xerrors.New("icon URL cannot be empty")}
}
var errs []error
// If the URL does not have a relative path.
if !strings.HasPrefix(iconURL, ".") && !strings.HasPrefix(iconURL, "/") {
if _, err := url.ParseRequestURI(iconURL); err != nil {
errs = append(errs, xerrors.New("absolute icon URL is not correctly formatted"))
}
if strings.Contains(iconURL, "?") {
errs = append(errs, xerrors.New("icon URLs cannot contain query parameters"))
}
return errs
}
// If the URL has a relative path.
if !isPermittedRelativeURL(iconURL) {
errs = append(errs, xerrors.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 errs
}
func validateCoderResourceTags(tags []string) error {
if tags == nil {
return xerrors.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.
var invalidTags []string
for _, t := range tags {
if t != url.QueryEscape(t) {
invalidTags = append(invalidTags, t)
}
}
if len(invalidTags) != 0 {
return xerrors.Errorf("found invalid tags (tags that cannot be used for filter state in the Registry website): [%s]", strings.Join(invalidTags, ", "))
}
return nil
}
func validateCoderResourceFrontmatter(resourceType string, filePath string, fm coderResourceFrontmatter) []error {
if !slices.Contains(supportedResourceTypes, resourceType) {
return []error{xerrors.Errorf("cannot process unknown resource type %q", resourceType)}
}
var errs []error
if err := validateCoderResourceDisplayName(fm.DisplayName); err != nil {
errs = append(errs, addFilePathToError(filePath, err))
}
if err := validateCoderResourceDescription(fm.Description); err != nil {
errs = append(errs, addFilePathToError(filePath, err))
}
if err := validateCoderResourceTags(fm.Tags); err != nil {
errs = append(errs, addFilePathToError(filePath, err))
}
for _, err := range validateCoderResourceIconURL(fm.IconURL) {
errs = append(errs, addFilePathToError(filePath, err))
}
for _, err := range validateSupportedOperatingSystems(fm.OperatingSystems) {
errs = append(errs, addFilePathToError(filePath, err))
}
return errs
}
func parseCoderResourceReadme(resourceType string, rm readme) (coderResourceReadme, []error) {
fm, body, err := separateFrontmatter(rm.rawText)
if err != nil {
return coderResourceReadme{}, []error{xerrors.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)}
}
keyErrs := validateFrontmatterYamlKeys(fm, supportedCoderResourceStructKeys)
if len(keyErrs) != 0 {
var remapped []error
for _, e := range keyErrs {
remapped = append(remapped, addFilePathToError(rm.filePath, e))
}
return coderResourceReadme{}, remapped
}
yml := coderResourceFrontmatter{}
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
return coderResourceReadme{}, []error{xerrors.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) ([]coderResourceReadme, error) {
if !slices.Contains(supportedResourceTypes, resourceType) {
return nil, xerrors.Errorf("cannot process unknown resource type %q", resourceType)
}
resources := map[string]coderResourceReadme{}
var yamlParsingErrs []error
for _, rm := range rms {
p, errs := parseCoderResourceReadme(resourceType, rm)
if len(errs) != 0 {
yamlParsingErrs = append(yamlParsingErrs, errs...)
continue
}
resources[p.filePath] = p
}
if len(yamlParsingErrs) != 0 {
return nil, validationPhaseError{
phase: validationPhaseReadme,
errors: yamlParsingErrs,
}
}
var serialized []coderResourceReadme
for _, r := range resources {
serialized = append(serialized, r)
}
slices.SortFunc(serialized, func(r1 coderResourceReadme, r2 coderResourceReadme) int {
return strings.Compare(r1.filePath, r2.filePath)
})
return serialized, nil
}
// Todo: Need to beef up this function by grabbing each image/video URL from
// the body's AST.
func validateCoderResourceRelativeURLs(_ []coderResourceReadme) error {
return nil
}
func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
if !slices.Contains(supportedResourceTypes, resourceType) {
return nil, xerrors.Errorf("cannot process unknown resource type %q", resourceType)
}
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: validationPhaseFile,
errors: errs,
}
}
return allReadmeFiles, nil
}
func validateResourceGfmAlerts(readmeBody string) []error {
trimmed := strings.TrimSpace(readmeBody)
if trimmed == "" {
return nil
}
var errs []error
var sourceLine string
isInsideGfmQuotes := false
isInsideCodeBlock := false
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
for lineScanner.Scan() {
sourceLine = lineScanner.Text()
if strings.HasPrefix(sourceLine, "```") {
isInsideCodeBlock = !isInsideCodeBlock
continue
}
if isInsideCodeBlock {
continue
}
isInsideGfmQuotes = isInsideGfmQuotes && strings.HasPrefix(sourceLine, "> ")
currentMatch := gfmAlertRegex.FindStringSubmatch(sourceLine)
if currentMatch == nil {
continue
}
// Nested GFM alerts is such a weird mistake that it's probably not really safe to keep trying to process the
// rest of the content, so this will prevent any other validations from happening for the given line
if isInsideGfmQuotes {
errs = append(errs, errors.New("registry does not support nested GFM alerts"))
continue
}
leadingWhitespace := currentMatch[1]
if len(leadingWhitespace) != 1 {
errs = append(errs, errors.New("GFM alerts must have one space between the '>' and the start of the GFM brackets"))
}
isInsideGfmQuotes = true
alertHeader := currentMatch[2]
upperHeader := strings.ToUpper(alertHeader)
if !slices.Contains(gfmAlertTypes, upperHeader) {
errs = append(errs, xerrors.Errorf("GFM alert type %q is not supported", alertHeader))
}
if alertHeader != upperHeader {
errs = append(errs, xerrors.Errorf("GFM alerts must be in all caps"))
}
trailingWhitespace := currentMatch[3]
if trailingWhitespace != "" {
errs = append(errs, xerrors.Errorf("GFM alerts must not have any trailing whitespace after the closing bracket"))
}
extraContent := currentMatch[4]
if extraContent != "" {
errs = append(errs, xerrors.Errorf("GFM alerts must not have any extra content on the same line"))
}
}
if gfmAlertRegex.Match([]byte(sourceLine)) {
errs = append(errs, xerrors.Errorf("README has an incomplete GFM alert at the end of the file"))
}
return errs
}
-122
View File
@@ -1,122 +0,0 @@
package main
import (
"bufio"
"context"
"strings"
"golang.org/x/xerrors"
)
func validateCoderTemplateReadmeBody(body string) []error {
var errs []error
trimmed := strings.TrimSpace(body)
if baseErrs := validateReadmeBody(trimmed); len(baseErrs) != 0 {
errs = append(errs, baseErrs...)
}
var nextLine string
foundParagraph := false
isInsideCodeBlock := false
lineNum := 0
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
if strings.HasPrefix(nextLine, "```hcl") {
errs = append(errs, xerrors.New("all .hcl language references must be converted to .tf"))
}
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 !foundParagraph {
errs = append(errs, xerrors.New("did not find paragraph within h1 section"))
}
if isInsideCodeBlock {
errs = append(errs, xerrors.New("code blocks inside h1 section do not all terminate before end of file"))
}
return errs
}
func validateCoderTemplateReadme(rm coderResourceReadme) []error {
var errs []error
for _, err := range validateCoderTemplateReadmeBody(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateResourceGfmAlerts(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if fmErrs := validateCoderResourceFrontmatter("templates", rm.filePath, rm.frontmatter); len(fmErrs) != 0 {
errs = append(errs, fmErrs...)
}
return errs
}
func validateAllCoderTemplateReadmes(resources []coderResourceReadme) error {
var yamlValidationErrors []error
for _, readme := range resources {
errs := validateCoderTemplateReadme(readme)
if len(errs) > 0 {
yamlValidationErrors = append(yamlValidationErrors, errs...)
}
}
if len(yamlValidationErrors) != 0 {
return validationPhaseError{
phase: validationPhaseReadme,
errors: yamlValidationErrors,
}
}
return nil
}
func validateAllCoderTemplates() error {
const resourceType = "templates"
allReadmeFiles, err := aggregateCoderResourceReadmeFiles(resourceType)
if err != nil {
return err
}
logger.Info(context.Background(), "processing template README files", "resource_type", resourceType, "num_files", len(allReadmeFiles))
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles)
if err != nil {
return err
}
err = validateAllCoderTemplateReadmes(resources)
if err != nil {
return err
}
logger.Info(context.Background(), "processed README files as valid Coder resources", "resource_type", resourceType, "num_files", len(resources))
if err := validateCoderResourceRelativeURLs(resources); err != nil {
return err
}
logger.Info(context.Background(), "all relative URLs for READMEs are valid", "resource_type", resourceType)
return nil
}
+98 -116
View File
@@ -1,35 +1,33 @@
package main
import (
"context"
"errors"
"fmt"
"log"
"net/url"
"os"
"path"
"slices"
"strings"
"golang.org/x/xerrors"
"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"`
DisplayName string `yaml:"display_name"`
Bio string `yaml:"bio"`
// Script assumes that if value is nil, the Registry site build step will
// backfill the value with the user's GitHub avatar URL
AvatarURL *string `yaml:"avatar"`
GithubUsername *string `yaml:"github"`
LinkedinURL *string `yaml:"linkedin"`
WebsiteURL *string `yaml:"website"`
SupportEmail *string `yaml:"support_email"`
ContributorStatus *string `yaml:"status"`
}
// A slice version of the struct tags from contributorProfileFrontmatter. Might be worth using reflection to generate
// this list at runtime in the future, but this should be okay for now
var supportedContributorProfileStructKeys = []string{"display_name", "bio", "status", "avatar", "linkedin", "github", "website", "support_email"}
type contributorProfileReadme struct {
type contributorProfile struct {
frontmatter contributorProfileFrontmatter
namespace string
filePath string
@@ -37,7 +35,7 @@ type contributorProfileReadme struct {
func validateContributorDisplayName(displayName string) error {
if displayName == "" {
return xerrors.New("missing display_name")
return fmt.Errorf("missing display_name")
}
return nil
@@ -49,62 +47,47 @@ func validateContributorLinkedinURL(linkedinURL *string) error {
}
if _, err := url.ParseRequestURI(*linkedinURL); err != nil {
return xerrors.Errorf("linkedIn URL %q is not valid: %v", *linkedinURL, err)
return fmt.Errorf("linkedIn URL %q is not valid: %v", *linkedinURL, err)
}
return nil
}
func validateGithubUsername(username *string) error {
if username == nil {
return nil
}
name := *username
trimmed := strings.TrimSpace(name)
if trimmed == "" {
return xerrors.New("username must have non-whitespace characters")
}
if name != trimmed {
return xerrors.Errorf("username %q has extra whitespace", trimmed)
}
return nil
}
// validateContributorSupportEmail does best effort validation of a contributors email address. We can't 100% validate
// that this is correct without actually sending an email, especially because some contributors are individual developers
// and we don't want to do that on every single run of the CI pipeline. The best we can do is verify the general structure.
func validateContributorSupportEmail(email *string) []error {
if email == nil {
return nil
}
var errs []error
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, xerrors.Errorf("email address %q is missing @ symbol", *email))
errs = append(errs, fmt.Errorf("email address %q is missing @ symbol", *email))
return errs
}
if username == "" {
errs = append(errs, xerrors.Errorf("email address %q is missing username", *email))
errs = append(errs, fmt.Errorf("email address %q is missing username", *email))
}
domain, tld, ok := strings.Cut(server, ".")
if !ok {
errs = append(errs, xerrors.Errorf("email address %q is missing period for server segment", *email))
errs = append(errs, fmt.Errorf("email address %q is missing period for server segment", *email))
return errs
}
if domain == "" {
errs = append(errs, xerrors.Errorf("email address %q is missing domain", *email))
errs = append(errs, fmt.Errorf("email address %q is missing domain", *email))
}
if tld == "" {
errs = append(errs, xerrors.Errorf("email address %q is missing top-level domain", *email))
errs = append(errs, fmt.Errorf("email address %q is missing top-level domain", *email))
}
if strings.Contains(*email, "?") {
errs = append(errs, xerrors.New("email is not allowed to contain query parameters"))
errs = append(errs, errors.New("email is not allowed to contain query parameters"))
}
return errs
@@ -116,40 +99,47 @@ func validateContributorWebsite(websiteURL *string) error {
}
if _, err := url.ParseRequestURI(*websiteURL); err != nil {
return xerrors.Errorf("linkedIn URL %q is not valid: %v", *websiteURL, err)
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 xerrors.Errorf("contributor status %q is not valid", status)
func validateContributorStatus(status *string) error {
if status == nil {
return nil
}
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.
// 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 == "" {
return []error{xerrors.New("avatar URL must be omitted or non-empty string")}
errs = append(errs, errors.New("avatar URL must be omitted or non-empty string"))
return errs
}
var errs []error
// Have to use .Parse instead of .ParseRequestURI because this is the one field that's allowed to be a relative URL.
// 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, xerrors.Errorf("URL %q is not a valid relative or absolute URL", *avatarURL))
errs = append(errs, fmt.Errorf("URL %q is not a valid relative or absolute URL", *avatarURL))
}
if strings.Contains(*avatarURL, "?") {
errs = append(errs, xerrors.New("avatar URL is not allowed to contain search parameters"))
errs = append(errs, errors.New("avatar URL is not allowed to contain search parameters"))
}
var matched bool
matched := false
for _, ff := range supportedAvatarFileFormats {
matched = strings.HasSuffix(*avatarURL, ff)
if matched {
@@ -159,101 +149,90 @@ func validateContributorAvatarURL(avatarURL *string) []error {
if !matched {
segments := strings.Split(*avatarURL, ".")
fileExtension := segments[len(segments)-1]
errs = append(errs, xerrors.Errorf("avatar URL '.%s' does not end in a supported file format: [%s]", fileExtension, strings.Join(supportedAvatarFileFormats, ", ")))
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 {
var allErrs []error
func validateContributorYaml(yml contributorProfile) []error {
allErrs := []error{}
if err := validateContributorDisplayName(rm.frontmatter.DisplayName); err != nil {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
if err := validateContributorDisplayName(yml.frontmatter.DisplayName); err != nil {
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
}
if err := validateContributorLinkedinURL(rm.frontmatter.LinkedinURL); err != nil {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
if err := validateContributorLinkedinURL(yml.frontmatter.LinkedinURL); err != nil {
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
}
if err := validateGithubUsername(rm.frontmatter.GithubUsername); err != nil {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
if err := validateContributorWebsite(yml.frontmatter.WebsiteURL); err != nil {
allErrs = append(allErrs, addFilePathToError(yml.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))
if err := validateContributorStatus(yml.frontmatter.ContributorStatus); err != nil {
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
}
for _, err := range validateContributorSupportEmail(rm.frontmatter.SupportEmail) {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
for _, err := range validateContributorSupportEmail(yml.frontmatter.SupportEmail) {
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
}
for _, err := range validateContributorAvatarURL(rm.frontmatter.AvatarURL) {
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
for _, err := range validateContributorAvatarURL(yml.frontmatter.AvatarURL) {
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
}
return allErrs
}
func parseContributorProfile(rm readme) (contributorProfileReadme, []error) {
func parseContributorProfile(rm readme) (contributorProfile, error) {
fm, _, err := separateFrontmatter(rm.rawText)
if err != nil {
return contributorProfileReadme{}, []error{xerrors.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)}
}
keyErrs := validateFrontmatterYamlKeys(fm, supportedContributorProfileStructKeys)
if len(keyErrs) != 0 {
var remapped []error
for _, e := range keyErrs {
remapped = append(remapped, addFilePathToError(rm.filePath, e))
}
return contributorProfileReadme{}, remapped
return contributorProfile{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
}
yml := contributorProfileFrontmatter{}
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
return contributorProfileReadme{}, []error{xerrors.Errorf("%q: failed to parse: %v", rm.filePath, err)}
return contributorProfile{}, fmt.Errorf("%q: failed to parse: %v", rm.filePath, err)
}
return contributorProfileReadme{
return contributorProfile{
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{}
var yamlParsingErrors []error
func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfile, error) {
profilesByNamespace := map[string]contributorProfile{}
yamlParsingErrors := []error{}
for _, rm := range readmeEntries {
p, errs := parseContributorProfile(rm)
if len(errs) != 0 {
yamlParsingErrors = append(yamlParsingErrors, errs...)
p, err := parseContributorProfile(rm)
if err != nil {
yamlParsingErrors = append(yamlParsingErrors, err)
continue
}
if prev, alreadyExists := profilesByNamespace[p.namespace]; alreadyExists {
yamlParsingErrors = append(yamlParsingErrors, xerrors.Errorf("%q: namespace %q conflicts with namespace from %q", p.filePath, p.namespace, prev.filePath))
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: validationPhaseReadme,
phase: validationPhaseReadmeParsing,
errors: yamlParsingErrors,
}
}
var yamlValidationErrors []error
yamlValidationErrors := []error{}
for _, p := range profilesByNamespace {
if errors := validateContributorReadme(p); len(errors) > 0 {
errors := validateContributorYaml(p)
if len(errors) > 0 {
yamlValidationErrors = append(yamlValidationErrors, errors...)
continue
}
}
if len(yamlValidationErrors) != 0 {
return nil, validationPhaseError{
phase: validationPhaseReadme,
phase: validationPhaseReadmeParsing,
errors: yamlValidationErrors,
}
}
@@ -267,15 +246,14 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
return nil, err
}
var allReadmeFiles []readme
var errs []error
dirPath := ""
allReadmeFiles := []readme{}
errs := []error{}
for _, e := range dirEntries {
dirPath := path.Join(rootRegistryPath, e.Name())
if !e.IsDir() {
continue
}
dirPath = path.Join(rootRegistryPath, e.Name())
readmePath := path.Join(dirPath, "README.md")
rmBytes, err := os.ReadFile(readmePath)
if err != nil {
@@ -290,7 +268,7 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
if len(errs) != 0 {
return nil, validationPhaseError{
phase: validationPhaseFile,
phase: validationPhaseFileLoad,
errors: errs,
}
}
@@ -298,31 +276,34 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
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.
var errs []error
func validateContributorRelativeUrls(contributors map[string]contributorProfile) 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 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
}
if !strings.HasPrefix(*con.frontmatter.AvatarURL, ".") || !strings.HasPrefix(*con.frontmatter.AvatarURL, "/") {
isRelativeURL := strings.HasPrefix(*con.frontmatter.AvatarURL, ".") ||
strings.HasPrefix(*con.frontmatter.AvatarURL, "/")
if !isRelativeURL {
continue
}
isAvatarInApprovedSpot := strings.HasPrefix(*con.frontmatter.AvatarURL, "./.images/") ||
strings.HasPrefix(*con.frontmatter.AvatarURL, ".images/")
if !isAvatarInApprovedSpot {
errs = append(errs, xerrors.Errorf("%q: relative avatar URLs cannot be placed outside a user's namespaced directory", con.filePath))
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
if _, err := os.ReadFile(absolutePath); err != nil {
errs = append(errs, xerrors.Errorf("%q: relative avatar path %q does not point to image in file system", con.filePath, absolutePath))
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))
}
}
@@ -330,7 +311,7 @@ func validateContributorRelativeURLs(contributors map[string]contributorProfileR
return nil
}
return validationPhaseError{
phase: validationPhaseCrossReference,
phase: validationPhaseAssetCrossReference,
errors: errs,
}
}
@@ -341,18 +322,19 @@ func validateAllContributorFiles() error {
return err
}
logger.Info(context.Background(), "processing README files", "num_files", len(allReadmeFiles))
log.Printf("Processing %d README files\n", len(allReadmeFiles))
contributors, err := parseContributorFiles(allReadmeFiles)
if err != nil {
return err
}
logger.Info(context.Background(), "processed README files as valid contributor profiles", "num_contributors", len(contributors))
log.Printf("Processed %d README files as valid contributor profiles", len(contributors))
if err := validateContributorRelativeURLs(contributors); err != nil {
err = validateContributorRelativeUrls(contributors)
if err != nil {
return err
}
logger.Info(context.Background(), "all relative URLs for READMEs are valid")
log.Println("All relative URLs for READMEs are valid")
logger.Info(context.Background(), "processed all READMEs in directory", "dir", rootRegistryPath)
log.Printf("Processed all READMEs in the %q directory\n", rootRegistryPath)
return nil
}
+7 -9
View File
@@ -1,13 +1,11 @@
package main
import (
"fmt"
import "fmt"
"golang.org/x/xerrors"
)
// 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.
// 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
@@ -16,7 +14,7 @@ type validationPhaseError struct {
var _ error = validationPhaseError{}
func (vpe validationPhaseError) Error() string {
msg := fmt.Sprintf("Error during %q phase of README validation:", vpe.phase)
msg := fmt.Sprintf("Error during %q phase of README validation:", vpe.phase.String())
for _, e := range vpe.errors {
msg += fmt.Sprintf("\n- %v", e)
}
@@ -26,5 +24,5 @@ func (vpe validationPhaseError) Error() string {
}
func addFilePathToError(filePath string, err error) error {
return xerrors.Errorf("%q: %v", filePath, err)
return fmt.Errorf("%q: %v", filePath, err)
}
+12 -24
View File
@@ -6,46 +6,34 @@
package main
import (
"context"
"fmt"
"log"
"os"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
)
var logger = slog.Make(sloghuman.Sink(os.Stdout))
func main() {
logger.Info(context.Background(), "starting README validation")
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.
err := validateRepoStructure()
if err != nil {
logger.Error(context.Background(), "error when validating the repo structure", "error", err.Error())
// 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 = validateAllCoderModules()
if err != nil {
errs = append(errs, err)
}
err = validateAllCoderTemplates()
errs := []error{}
err := validateAllContributorFiles()
if err != nil {
errs = append(errs, err)
}
if len(errs) == 0 {
logger.Info(context.Background(), "processed all READMEs in directory", "dir", rootRegistryPath)
os.Exit(0)
}
for _, err := range errs {
logger.Error(context.Background(), err.Error())
fmt.Println(err)
}
os.Exit(1)
}
+69 -151
View File
@@ -2,194 +2,112 @@ package main
import (
"bufio"
"errors"
"fmt"
"regexp"
"slices"
"strings"
"golang.org/x/xerrors"
)
// 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 rootRegistryPath = "./registry"
const (
rootRegistryPath = "./registry"
var supportedAvatarFileFormats = []string{".png", ".jpeg", ".jpg", ".gif", ".svg"}
// --- validationPhases ---
// validationPhaseStructure indicates when the entire Registry
// directory is being verified for having all files be placed in the file
// system as expected.
validationPhaseStructure validationPhase = "File structure validation"
// ValidationPhaseFile indicates when README files are being read from
// the file system.
validationPhaseFile validationPhase = "Filesystem reading"
// ValidationPhaseReadme indicates when a README's frontmatter is
// being parsed as YAML. This phase does not include YAML validation.
validationPhaseReadme validationPhase = "README parsing"
// ValidationPhaseCrossReference indicates when a README's frontmatter
// is having all its relative URLs be validated for whether they point to
// valid resources.
validationPhaseCrossReference validationPhase = "Cross-referencing relative asset URLs"
// --- end of validationPhases ---.
)
var (
supportedAvatarFileFormats = []string{".png", ".jpeg", ".jpg", ".gif", ".svg"}
// Matches markdown headers placed at the beginning of a line (e.g., "# " or "### "). To make the logic for
// validateReadmeBody easier, this pattern deliberately matches on invalid headers (header levels must be in the
// range 16 to be valid). The function has checks to see if the level is correct.
readmeHeaderRe = regexp.MustCompile(`^(#+)(\s*)`)
)
// readme represents a single README file within the repo (usually within the top-level "/registry" directory).
// 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
// 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) (readmeFrontmatter string, readmeBody string, err error) {
func separateFrontmatter(readmeText string) (string, string, error) {
if readmeText == "" {
return "", "", xerrors.New("README is empty")
return "", "", errors.New("README is empty")
}
const fence = "---"
var fm strings.Builder
var body strings.Builder
fm := ""
body := ""
fenceCount := 0
lineScanner := bufio.NewScanner(strings.NewReader(strings.TrimSpace(readmeText)))
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.
// 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.
// 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 {
fmt.Fprintf(&body, "%s\n", nextLine)
body += nextLine + "\n"
} else {
fmt.Fprintf(&fm, "%s\n", strings.TrimSpace(nextLine))
fm += strings.TrimSpace(nextLine) + "\n"
}
}
if fenceCount < 2 {
return "", "", xerrors.New("README does not have two sets of frontmatter fences")
return "", "", errors.New("README does not have two sets of frontmatter fences")
}
if fm.Len() == 0 {
return "", "", xerrors.New("readme has frontmatter fences but no frontmatter content")
if fm == "" {
return "", "", errors.New("readme has frontmatter fences but no frontmatter content")
}
return fm.String(), strings.TrimSpace(body.String()), nil
return fm, strings.TrimSpace(body), nil
}
// 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)
// 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 int
if trimmed == "" {
return []error{xerrors.New("README body is empty")}
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 = iota
// validationPhaseFileLoad indicates when README files are being read from
// the file system
validationPhaseFileLoad
// validationPhaseReadmeParsing indicates when a README's frontmatter is
// being parsed as YAML. This phase does not include YAML validation.
validationPhaseReadmeParsing
// validationPhaseReadmeValidation indicates when a README's frontmatter is
// being validated as proper YAML with expected keys.
validationPhaseReadmeValidation
// validationPhaseAssetCrossReference indicates when a README's frontmatter
// is having all its relative URLs be validated for whether they point to
// valid resources.
validationPhaseAssetCrossReference
)
func (p validationPhase) String() string {
switch p {
case validationPhaseFileStructureValidation:
return "File structure validation"
case validationPhaseFileLoad:
return "Filesystem reading"
case validationPhaseReadmeParsing:
return "README parsing"
case validationPhaseReadmeValidation:
return "README validation"
case validationPhaseAssetCrossReference:
return "Cross-referencing relative asset URLs"
default:
return fmt.Sprintf("Unknown validation phase: %d", p)
}
// If the very first line of the README doesn't start with an ATX-style H1 header, 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{xerrors.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
}
// In the Markdown spec it is mandatory to have a space following the header # symbol(s).
if headerGroups[2] == "" {
errs = append(errs, xerrors.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, xerrors.New("READMEs cannot contain more than h1 header"))
break
}
if nextHeaderLevel > 6 {
errs = append(errs, xerrors.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, xerrors.New("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
}
func validateFrontmatterYamlKeys(frontmatter string, allowedKeys []string) []error {
if len(allowedKeys) == 0 {
return []error{xerrors.New("Set of allowed keys is empty")}
}
var key string
var cutOk bool
var line string
var errs []error
lineScanner := bufio.NewScanner(strings.NewReader(frontmatter))
for lineScanner.Scan() {
line = lineScanner.Text()
key, _, cutOk = strings.Cut(line, ":")
if !cutOk || slices.Contains(allowedKeys, key) {
continue
}
errs = append(errs, xerrors.Errorf("detected unknown key %q", key))
}
return errs
}
+57 -69
View File
@@ -2,131 +2,121 @@ package main
import (
"errors"
"fmt"
"os"
"path"
"regexp"
"slices"
"strings"
"golang.org/x/xerrors"
)
var supportedUserNameSpaceDirectories = append(supportedResourceTypes, ".images")
var (
supportedResourceTypes = []string{"modules", "templates"}
supportedUserNameSpaceDirectories = append(supportedResourceTypes[:], ".icons", ".images")
)
// validNameRe validates that names contain only alphanumeric characters and hyphens
var validNameRe = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$`)
// validateCoderResourceSubdirectory validates that the structure of a module or template within a namespace follows all
// expected file conventions
func validateCoderResourceSubdirectory(dirPath string) []error {
resourceDir, err := os.Stat(dirPath)
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.
// 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) {
return []error{addFilePathToError(dirPath, err)}
errs = append(errs, addFilePathToError(dirPath, err))
}
return errs
}
if !resourceDir.IsDir() {
return []error{xerrors.Errorf("%q: path is not a directory", dirPath)}
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 {
return []error{addFilePathToError(dirPath, err)}
errs = append(errs, addFilePathToError(dirPath, err))
return errs
}
var errs []error
for _, f := range files {
// The .coder subdirectories are sometimes generated as part of our 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.
// 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
}
// Validate module/template name
if !validNameRe.MatchString(f.Name()) {
errs = append(errs, xerrors.Errorf("%q: name contains invalid characters (only alphanumeric characters and hyphens are allowed)", path.Join(dirPath, f.Name())))
continue
}
resourceReadmePath := path.Join(dirPath, f.Name(), "README.md")
if _, err := os.Stat(resourceReadmePath); err != nil {
_, err := os.Stat(resourceReadmePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
errs = append(errs, xerrors.Errorf("%q: 'README.md' does not exist", resourceReadmePath))
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")
if _, err := os.Stat(mainTerraformPath); err != nil {
_, err = os.Stat(mainTerraformPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
errs = append(errs, xerrors.Errorf("%q: 'main.tf' file does not exist", mainTerraformPath))
errs = append(errs, fmt.Errorf("%q: 'main.tf' file does not exist", mainTerraformPath))
} else {
errs = append(errs, addFilePathToError(mainTerraformPath, err))
}
}
}
return errs
}
// validateRegistryDirectory validates that the contents of `/registry` follow all expected file conventions. This
// includes the top-level structure of the individual namespace directories.
func validateRegistryDirectory() []error {
namespaceDirs, err := os.ReadDir(rootRegistryPath)
userDirs, err := os.ReadDir(rootRegistryPath)
if err != nil {
return []error{err}
}
var allErrs []error
for _, nDir := range namespaceDirs {
namespacePath := path.Join(rootRegistryPath, nDir.Name())
if !nDir.IsDir() {
allErrs = append(allErrs, xerrors.Errorf("detected non-directory file %q at base of main Registry directory", namespacePath))
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
}
// Validate namespace name
if !validNameRe.MatchString(nDir.Name()) {
allErrs = append(allErrs, xerrors.Errorf("%q: namespace name contains invalid characters (only alphanumeric characters and hyphens are allowed)", namespacePath))
continue
}
contributorReadmePath := path.Join(namespacePath, "README.md")
if _, err := os.Stat(contributorReadmePath); err != nil {
contributorReadmePath := path.Join(dirPath, "README.md")
_, err := os.Stat(contributorReadmePath)
if err != nil {
allErrs = append(allErrs, err)
}
files, err := os.ReadDir(namespacePath)
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 at the top
// level of each user namespace.
// 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(namespacePath, segment)
filePath := path.Join(dirPath, segment)
if !slices.Contains(supportedUserNameSpaceDirectories, segment) {
allErrs = append(allErrs, xerrors.Errorf("%q: only these sub-directories are allowed at top of user namespace: [%s]", filePath, strings.Join(supportedUserNameSpaceDirectories, ", ")))
continue
}
if !slices.Contains(supportedResourceTypes, 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 errs := validateCoderResourceSubdirectory(filePath); len(errs) != 0 {
allErrs = append(allErrs, errs...)
if slices.Contains(supportedResourceTypes, segment) {
errs := validateCoderResourceSubdirectory(filePath)
if len(errs) != 0 {
allErrs = append(allErrs, errs...)
}
}
}
}
@@ -134,23 +124,21 @@ func validateRegistryDirectory() []error {
return allErrs
}
// validateRepoStructure validates that the structure of the repo is "correct enough" to do all necessary validation
// checks. It is NOT an exhaustive validation of the entire repo structure it only checks the parts of the repo that
// are relevant for the main validation steps
func validateRepoStructure() error {
var errs []error
if vrdErrs := validateRegistryDirectory(); len(vrdErrs) != 0 {
errs = append(errs, vrdErrs...)
var problems []error
if errs := validateRegistryDirectory(); len(errs) != 0 {
problems = append(problems, errs...)
}
if _, err := os.Stat("./.icons"); err != nil {
errs = append(errs, xerrors.New("missing top-level .icons directory (used for storing reusable Coder resource icons)"))
_, 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(errs) != 0 {
if len(problems) != 0 {
return validationPhaseError{
phase: validationPhaseStructure,
errors: errs,
phase: validationPhaseFileStructureValidation,
errors: problems,
}
}
return nil
@@ -1,121 +0,0 @@
# 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"
}
```
-21
View File
@@ -1,21 +0,0 @@
run "plan_with_required_vars" {
command = plan
variables {
agent_id = "example-agent-id"
}
}
run "app_url_uses_port" {
command = plan
variables {
agent_id = "example-agent-id"
port = 19999
}
assert {
condition = resource.coder_app.MODULE_NAME.url == "http://localhost:19999"
error_message = "Expected MODULE_NAME app URL to include configured port"
}
}
+9 -8
View File
@@ -2,6 +2,7 @@
display_name: MODULE_NAME
description: Describe what this module does
icon: ../../../../.icons/<A_RELEVANT_ICON>.svg
maintainer_github: GITHUB_USERNAME
verified: false
tags: [helper]
---
@@ -13,8 +14,8 @@ tags: [helper]
```tf
module "MODULE_NAME" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
version = "1.0.0"
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.2"
}
```
@@ -29,8 +30,8 @@ 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.0"
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
extensions = [
"dracula-theme.theme-dracula"
@@ -47,8 +48,8 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
```tf
module "MODULE_NAME" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
version = "1.0.0"
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
@@ -63,8 +64,8 @@ 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.0"
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
offline = true
}
+2 -1
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.17"
}
}
}
@@ -105,3 +105,4 @@ data "coder_parameter" "MODULE_NAME" {
}
}
}
-33
View File
@@ -1,33 +0,0 @@
---
display_name: NAMESPACE_NAME
bio: Brief description of what this namespace provides
github: your-github-username
avatar: ./.images/avatar.svg
linkedin: https://www.linkedin.com/in/your-profile
website: https://your-website.com
status: community
---
# NAMESPACE_NAME
Brief description of what this namespace provides. Include information about:
- What types of templates/modules you offer
- Your focus areas (e.g., specific cloud providers, technologies)
- Any special features or configurations
## Templates
List your available templates here:
- **template-name**: Brief description
## Modules
List your available modules here:
- **module-name**: Brief description
## Contributing
If you'd like to contribute to this namespace, please [open an issue](https://github.com/coder/registry/issues) or submit a pull request.
-58
View File
@@ -1,58 +0,0 @@
---
name: TEMPLATE_NAME
description: A brief description of what this template does
tags: [tag1, tag2, tag3]
icon: /icon/TEMPLATE_NAME.svg
---
# TEMPLATE_NAME
A brief description of what this template provides and its use case.
## Features
- Feature 1
- Feature 2
- Feature 3
## Requirements
- List any prerequisites or requirements
- Provider-specific requirements (e.g., Docker, AWS credentials)
- Minimum Coder version if applicable
## Usage
1. Step-by-step instructions on how to use this template
2. Any configuration that needs to be done
3. How to customize the template
## Variables
| Name | Description | Type | Default | Required |
| ----------- | --------------------------- | -------- | ----------------- | -------- |
| example_var | Description of the variable | `string` | `"default_value"` | no |
## Resources Created
- List of resources that will be created
- Brief description of each resource
## Customization
Explain how users can customize this template for their needs:
- How to modify the startup script
- How to add additional software
- How to configure different providers
## Troubleshooting
### Common Issues
- Issue 1 and its solution
- Issue 2 and its solution
## Contributing
Contributions are welcome! Please see the [contributing guidelines](../../CONTRIBUTING.md) for more information.
-172
View File
@@ -1,172 +0,0 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
# Add your provider here (e.g., docker, aws, gcp, azure)
# docker = {
# source = "kreuzwerker/docker"
# }
}
}
locals {
username = data.coder_workspace_owner.me.name
}
# Add your variables here
# variable "example_var" {
# default = "default_value"
# description = "Description of the variable"
# type = string
# }
# Configure your provider here
# provider "docker" {
# host = var.docker_socket != "" ? var.docker_socket : null
# }
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
startup_script = <<-EOT
set -e
# Prepare user home with default files on first start.
if [ ! -f ~/.init_done ]; then
cp -rT /etc/skel ~
touch ~/.init_done
fi
# Add any commands that should be executed at workspace startup here
EOT
# These environment variables allow you to make Git commits right away after creating a
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
# You can remove this block if you'd prefer to configure Git manually or using
# dotfiles. (see docs/dotfiles.md)
env = {
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
}
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
# For basic templates, you can remove the "display_apps" block.
metadata {
display_name = "CPU Usage"
key = "0_cpu_usage"
script = "coder stat cpu"
interval = 10
timeout = 1
}
metadata {
display_name = "RAM Usage"
key = "1_ram_usage"
script = "coder stat mem"
interval = 10
timeout = 1
}
metadata {
display_name = "Home Disk"
key = "3_home_disk"
script = "coder stat disk --path $${HOME}"
interval = 60
timeout = 1
}
display_apps {
vscode = true
vscode_insiders = false
ssh_helper = false
port_forwarding_helper = true
web_terminal = true
}
}
# Add your resources here (e.g., docker container, VM, etc.)
# resource "docker_image" "main" {
# name = "codercom/enterprise-base:ubuntu"
# }
# resource "docker_container" "workspace" {
# count = data.coder_workspace.me.start_count
# image = docker_image.main.image_id
# # Uses lower() to avoid Docker restriction on container names.
# name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# # Hostname makes the shell more user friendly: coder@my-workspace:~$
# hostname = data.coder_workspace.me.name
# # Use the docker gateway if the access URL is 127.0.0.1
# entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\.0\.0\.1/", "host.docker.internal")]
# env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
# host {
# host = "host.docker.internal"
# ip = "host-gateway"
# }
# volumes {
# container_path = "/home/${local.username}"
# volume_name = docker_volume.home_volume[0].name
# read_only = false
# }
# # Add labels in Docker to keep track of orphan resources.
# labels {
# label = "coder.owner"
# value = data.coder_workspace_owner.me.name
# }
# labels {
# label = "coder.owner_id"
# value = data.coder_workspace_owner.me.id
# }
# labels {
# label = "coder.workspace_id"
# value = data.coder_workspace.me.id
# }
# labels {
# label = "coder.workspace_name"
# value = data.coder_workspace.me.name
# }
# }
# resource "docker_volume" "home_volume" {
# count = data.coder_workspace.me.start_count
# name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}-home"
# # Protect the volume from being deleted due to changes in attributes.
# lifecycle {
# ignore_changes = all
# }
# # Add labels in Docker to keep track of orphan resources.
# labels {
# label = "coder.owner"
# value = data.coder_workspace_owner.me.name
# }
# labels {
# label = "coder.owner_id"
# value = data.coder_workspace_owner.me.id
# }
# labels {
# label = "coder.workspace_id"
# value = data.coder_workspace.me.id
# }
# labels {
# label = "coder.workspace_name"
# value = data.coder_workspace.me.name
# }
# }
resource "coder_metadata" "workspace_info" {
resource_id = coder_agent.main.id
item {
key = "TEMPLATE_NAME"
value = "TEMPLATE_NAME"
}
}
+1 -22
View File
@@ -2,25 +2,4 @@ module coder.com/coder-registry
go 1.23.2
require (
cdr.dev/slog v1.6.1
github.com/quasilyte/go-ruleguard/dsl v0.3.22
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v0.7.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.29.0 // indirect
)
require gopkg.in/yaml.v3 v3.0.1
-76
View File
@@ -1,79 +1,3 @@
cdr.dev/slog v1.6.1 h1:IQjWZD0x6//sfv5n+qEhbu3wBkmtBQY5DILXNvMaIv4=
cdr.dev/slog v1.6.1/go.mod h1:eHEYQLaZvxnIAXC+XdTSNLb/kgA/X2RVSF72v5wsxEI=
cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/logging v1.7.0 h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5I=
cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M=
cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI=
cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE=
github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg=
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108=
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU=
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 h1:2FZP5XuJY9zQyGM5N0rtovnoXjiMUEIUMvw0m9wlpLc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o=
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

+11 -12
View File
@@ -1,24 +1,23 @@
{
"name": "registry",
"name": "modules",
"scripts": {
"fmt": "bun x prettier --write . && terraform fmt -recursive -diff",
"fmt:ci": "bun x prettier --check . && terraform fmt -check -recursive -diff",
"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": "./scripts/terraform_test_all.sh",
"test": "bun test",
"update-version": "./update-version.sh"
},
"devDependencies": {
"@types/bun": "^1.2.21",
"bun-types": "^1.2.21",
"dedent": "^1.6.0",
"@types/bun": "^1.2.9",
"bun-types": "^1.1.23",
"gray-matter": "^4.0.3",
"marked": "^16.2.0",
"prettier": "^3.6.2",
"prettier-plugin-sh": "^0.18.0",
"marked": "^12.0.2",
"prettier": "^3.3.3",
"prettier-plugin-sh": "^0.13.1",
"prettier-plugin-terraform-formatter": "^1.2.1"
},
"peerDependencies": {
"typescript": "^5.8.3"
"typescript": "^5.5.4"
},
"prettier": {
"plugins": [
@@ -26,4 +25,4 @@
"prettier-plugin-terraform-formatter"
]
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 KiB

-7
View File
@@ -1,7 +0,0 @@
---
display_name: Jash
bio: Coder user and contributor.
github: AJ0070
avatar: ./.images/avatar.png
status: community
---
-23
View File
@@ -1,23 +0,0 @@
---
display_name: "pgAdmin"
description: "A web-based interface for managing PostgreSQL databases in your Coder workspace."
icon: "../../../../.icons/pgadmin.svg"
maintainer_github: "AJ0070"
verified: false
tags: ["database", "postgres", "pgadmin", "web-ide"]
---
# pgAdmin
This module adds a pgAdmin app to your Coder workspace, providing a powerful web-based interface for managing PostgreSQL databases.
It can be served on a Coder subdomain for easy access, or on `localhost` if you prefer to use port-forwarding.
```tf
module "pgadmin" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/AJ0070/pgadmin/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
@@ -1,10 +0,0 @@
import { describe } from "bun:test";
import { runTerraformInit, testRequiredVariables } from "~test";
describe("pgadmin", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
});
-108
View File
@@ -1,108 +0,0 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
variable "agent_id" {
type = string
description = "The agent to install pgAdmin on."
}
variable "port" {
type = number
description = "The port to run pgAdmin on."
default = 5050
}
variable "subdomain" {
type = bool
description = "If true, the app will be served on a subdomain."
default = true
}
variable "config" {
type = any
description = "A map of pgAdmin configuration settings."
default = {
DEFAULT_EMAIL = "admin@coder.com"
DEFAULT_PASSWORD = "coderPASSWORD"
SERVER_MODE = false
MASTER_PASSWORD_REQUIRED = false
LISTEN_ADDRESS = "127.0.0.1"
}
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_app" "pgadmin" {
count = data.coder_workspace.me.start_count
agent_id = var.agent_id
display_name = "pgAdmin"
slug = "pgadmin"
icon = "/icon/pgadmin.svg"
url = local.url
subdomain = var.subdomain
share = "owner"
healthcheck {
url = local.healthcheck_url
interval = 5
threshold = 6
}
}
resource "coder_script" "pgadmin" {
agent_id = var.agent_id
display_name = "Install and run pgAdmin"
icon = "/icon/pgadmin.svg"
run_on_start = true
script = templatefile("${path.module}/run.sh", {
PORT = var.port,
LOG_PATH = "/tmp/pgadmin.log",
SERVER_BASE_PATH = local.server_base_path,
CONFIG = local.config_content,
PGADMIN_DATA_DIR = local.pgadmin_data_dir,
PGADMIN_LOG_DIR = local.pgadmin_log_dir,
PGADMIN_VENV_DIR = local.pgadmin_venv_dir
})
}
locals {
server_base_path = var.subdomain ? "" : format("/@%s/%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, "pgadmin")
url = "http://localhost:${var.port}${local.server_base_path}"
healthcheck_url = "http://localhost:${var.port}${local.server_base_path}/"
# pgAdmin data directories (user-local paths)
pgadmin_data_dir = "$HOME/.pgadmin"
pgadmin_log_dir = "$HOME/.pgadmin/logs"
pgadmin_venv_dir = "$HOME/.pgadmin/venv"
base_config = merge(var.config, {
LISTEN_PORT = var.port
# Override paths for user installation
DATA_DIR = local.pgadmin_data_dir
LOG_FILE = "${local.pgadmin_log_dir}/pgadmin4.log"
SQLITE_PATH = "${local.pgadmin_data_dir}/pgadmin4.db"
SESSION_DB_PATH = "${local.pgadmin_data_dir}/sessions"
STORAGE_DIR = "${local.pgadmin_data_dir}/storage"
# Disable initial setup prompts for automated deployment
SETUP_AUTH = false
})
config_with_path = var.subdomain ? local.base_config : merge(local.base_config, {
APPLICATION_ROOT = local.server_base_path
})
config_content = join("\n", [
for key, value in local.config_with_path :
format("%s = %s", key,
can(regex("^(true|false)$", tostring(value))) ? (value ? "True" : "False") :
can(tonumber(value)) ? tostring(value) :
format("'%s'", tostring(value))
)
])
}
-76
View File
@@ -1,76 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
PORT=${PORT}
LOG_PATH=${LOG_PATH}
SERVER_BASE_PATH=${SERVER_BASE_PATH}
BOLD='\033[0;1m'
printf "$${BOLD}Installing pgAdmin!\n"
# Check if Python 3 is available
if ! command -v python3 > /dev/null 2>&1; then
echo "⚠️ Warning: Python 3 is not installed. Please install Python 3 before using this module."
exit 0
fi
# Setup pgAdmin directories (from Terraform configuration)
PGADMIN_DATA_DIR="${PGADMIN_DATA_DIR}"
PGADMIN_LOG_DIR="${PGADMIN_LOG_DIR}"
PGADMIN_VENV_DIR="${PGADMIN_VENV_DIR}"
printf "Setting up pgAdmin directories...\n"
mkdir -p "$PGADMIN_DATA_DIR"
mkdir -p "$PGADMIN_LOG_DIR"
# Check if pgAdmin virtual environment already exists and is working
if [ -f "$PGADMIN_VENV_DIR/bin/pgadmin4" ] && [ -f "$PGADMIN_VENV_DIR/bin/activate" ]; then
printf "🥳 pgAdmin virtual environment already exists\n\n"
else
printf "Creating Python virtual environment for pgAdmin...\n"
if ! python3 -m venv "$PGADMIN_VENV_DIR"; then
echo "⚠️ Warning: Failed to create virtual environment"
exit 0
fi
printf "Installing pgAdmin 4 in virtual environment...\n"
if ! "$PGADMIN_VENV_DIR/bin/pip" install pgadmin4; then
echo "⚠️ Warning: Failed to install pgAdmin4"
exit 0
fi
printf "🥳 pgAdmin has been installed successfully\n\n"
fi
printf "$${BOLD}Configuring pgAdmin...\n"
if [ -f "$PGADMIN_VENV_DIR/bin/pgadmin4" ]; then
# pgAdmin installs to a predictable location in the virtual environment
PYTHON_VERSION=$("$PGADMIN_VENV_DIR/bin/python" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
PGADMIN_INSTALL_DIR="$PGADMIN_VENV_DIR/lib/python$PYTHON_VERSION/site-packages/pgadmin4"
# Create pgAdmin config file in the correct location (next to config.py)
cat > "$PGADMIN_INSTALL_DIR/config_local.py" << EOF
# pgAdmin configuration for Coder workspace
${CONFIG}
EOF
printf "📄 Config written to $PGADMIN_INSTALL_DIR/config_local.py\n"
printf "$${BOLD}Starting pgAdmin in background...\n"
printf "📝 Check logs at $${LOG_PATH}\n"
printf "🌐 Serving at http://localhost:${PORT}${SERVER_BASE_PATH}\n"
# Create required directories
mkdir -p "$PGADMIN_DATA_DIR/sessions"
mkdir -p "$PGADMIN_DATA_DIR/storage"
# Start pgadmin4 from the virtual environment with proper environment
cd "$PGADMIN_DATA_DIR"
PYTHONPATH="$PGADMIN_INSTALL_DIR:$${PYTHONPATH:-}" "$PGADMIN_VENV_DIR/bin/pgadmin4" > "$${LOG_PATH}" 2>&1 &
else
printf "⚠️ Warning: pgAdmin4 virtual environment not found\n"
printf "📝 Installation may have failed - check logs above\n"
fi
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

-14
View File
@@ -1,14 +0,0 @@
---
display_name: "Benraouane Soufiane"
bio: "Full stack developer creating awesome things."
avatar: "./.images/avatar.png"
github: "benraouanesoufiane"
linkedin: "https://www.linkedin.com/in/benraouane-soufiane" # Optional
website: "https://benraouanesoufiane.com" # Optional
support_email: "hello@benraouanesoufiane.com" # Optional
status: "community"
---
# Benraouane Soufiane
Full stack developer creating awesome things.
@@ -1,82 +0,0 @@
---
display_name: RustDesk
description: Run RustDesk in your workspace with virtual display
icon: ../../../../.icons/rustdesk.svg
verified: false
tags: [rustdesk, rdp, vm]
---
# RustDesk
Launches RustDesk within your workspace with a virtual display to provide remote desktop access. The module outputs the RustDesk ID and password needed to connect from external RustDesk clients.
```tf
module "rustdesk" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/BenraouaneSoufiane/rustdesk/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
## Features
- Automatically sets up virtual display (Xvfb)
- Downloads and configures RustDesk
- Outputs RustDesk ID and password for easy connection
- Provides external app link to RustDesk web client for browser-based access
- Starts virtual display (Xvfb) with customizable resolution
- Customizable screen resolution and RustDesk version
## Requirements
- Coder v2.5 or higher
- Linux workspace with `apt`, `dnf`, or `yum` package manager
## Examples
### Custom configuration with specific version
```tf
module "rustdesk" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/BenraouaneSoufiane/rustdesk/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
rustdesk_password = "mycustompass"
xvfb_resolution = "1920x1080x24"
rustdesk_version = "1.4.1"
}
```
### Docker container configuration
It requires coder' server to be run as root, when using with Docker, add the following to your `docker_container` resource:
```tf
resource "docker_container" "workspace" {
# ... other configuration ...
user = "root"
privileged = true
network_mode = "host"
ports {
internal = 21115
external = 21115
}
ports {
internal = 21116
external = 21116
}
ports {
internal = 21118
external = 21118
}
ports {
internal = 21119
external = 21119
}
}
```
@@ -1,75 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
variable "log_path" {
type = string
description = "The path to log rustdesk to."
default = "/tmp/rustdesk.log"
}
variable "agent_id" {
description = "Attach RustDesk setup to this agent"
type = string
}
variable "order" {
description = "Run order among scripts/apps"
type = number
default = 1
}
# Optional knobs passed as env (you can expose these as variables too)
variable "rustdesk_password" {
description = "If empty, the script will generate one"
type = string
default = ""
sensitive = true
}
variable "xvfb_resolution" {
description = "Xvfb screen size/depth"
type = string
default = "1024x768x16"
}
variable "rustdesk_version" {
description = "RustDesk version to install (use 'latest' for most recent release)"
type = string
default = "latest"
}
resource "coder_script" "rustdesk" {
agent_id = var.agent_id
display_name = "RustDesk"
run_on_start = true
# Prepend env as bash exports, then append the script file literally.
script = <<-EOT
# --- module-provided env knobs ---
export RUSTDESK_PASSWORD="${var.rustdesk_password}"
export XVFB_RESOLUTION="${var.xvfb_resolution}"
export RUSTDESK_VERSION="${var.rustdesk_version}"
# ---------------------------------
${file("${path.module}/run.sh")}
EOT
}
resource "coder_app" "rustdesk" {
agent_id = var.agent_id
slug = "rustdesk"
display_name = "Rustdesk"
url = "https://rustdesk.com/web"
icon = "/icon/rustdesk.svg"
order = var.order
external = true
}
@@ -1,117 +0,0 @@
#!/usr/bin/env bash
BOLD='\033[0;1m'
RESET='\033[0m'
printf "${BOLD}🖥️ Installing RustDesk Remote Desktop\n${RESET}"
# ---- configurable knobs (env overrides) ----
RUSTDESK_VERSION="${RUSTDESK_VERSION:-latest}"
LOG_PATH="${LOG_PATH:-/tmp/rustdesk.log}"
# ---- fetch latest version if needed ----
if [ "$RUSTDESK_VERSION" = "latest" ]; then
printf "🔍 Fetching latest RustDesk version...\n"
RUSTDESK_VERSION=$(curl -s https://api.github.com/repos/rustdesk/rustdesk/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' || echo "1.4.1")
printf "📌 Fetched RustDesk version: ${RUSTDESK_VERSION}\n"
else
printf "📌 Using specified RustDesk version: ${RUSTDESK_VERSION}\n"
fi
XVFB_RESOLUTION="${XVFB_RESOLUTION:-1024x768x16}"
RUSTDESK_PASSWORD="${RUSTDESK_PASSWORD:-}"
# ---- detect package manager & arch ----
ARCH="$(uname -m)"
case "$ARCH" in
x86_64 | amd64) PKG_ARCH="x86_64" ;;
aarch64 | arm64) PKG_ARCH="aarch64" ;;
*)
echo "❌ Unsupported arch: $ARCH"
exit 1
;;
esac
if command -v apt-get > /dev/null 2>&1; then
PKG_SYS="deb"
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.deb"
INSTALL_DEPS='apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y wget libva2 libva-drm2 libva-x11-2 libgstreamer-plugins-base1.0-0 gstreamer1.0-pipewire xfce4 xfce4-goodies xvfb x11-xserver-utils dbus-x11 libegl1 libgl1 libglx0 libglu1-mesa mesa-utils libxrandr2 libxss1 libgtk-3-0t64 libgbm1 libdrm2 libxcomposite1 libxdamage1 libxfixes3'
INSTALL_CMD="apt-get install -y ./${PKG_NAME}"
CLEAN_CMD="rm -f \"${PKG_NAME}\""
elif command -v dnf > /dev/null 2>&1; then
PKG_SYS="rpm"
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.rpm"
INSTALL_DEPS='dnf install -y wget libva libva-intel-driver gstreamer1-plugins-base pipewire xfce4-session xfce4-panel xorg-x11-server-Xvfb xorg-x11-xauth dbus-x11 mesa-libEGL mesa-libGL mesa-libGLU mesa-dri-drivers libXrandr libXScrnSaver gtk3 mesa-libgbm libdrm libXcomposite libXdamage libXfixes'
INSTALL_CMD="dnf install -y ./${PKG_NAME}"
CLEAN_CMD="rm -f \"${PKG_NAME}\""
elif command -v yum > /dev/null 2>&1; then
PKG_SYS="rpm"
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.rpm"
INSTALL_DEPS='yum install -y wget libva libva-intel-driver gstreamer1-plugins-base pipewire xfce4-session xfce4-panel xorg-x11-server-Xvfb xorg-x11-xauth dbus-x11 mesa-libEGL mesa-libGL mesa-libGLU mesa-dri-drivers libXrandr libXScrnSaver gtk3 mesa-libgbm libdrm libXcomposite libXdamage libXfixes'
INSTALL_CMD="yum install -y ./${PKG_NAME}"
CLEAN_CMD="rm -f \"${PKG_NAME}\""
else
echo "❌ Unsupported distro: need apt, dnf, or yum."
exit 1
fi
# ---- install rustdesk if missing ----
if ! command -v rustdesk > /dev/null 2>&1; then
printf "📦 Installing dependencies...\n"
sudo bash -c "$INSTALL_DEPS" 2>&1 | tee -a "${LOG_PATH}"
printf "⬇️ Downloading RustDesk ${RUSTDESK_VERSION} (${PKG_SYS}, ${PKG_ARCH})...\n"
URL="https://github.com/rustdesk/rustdesk/releases/download/${RUSTDESK_VERSION}/${PKG_NAME}"
wget -q "$URL" 2>&1 | tee -a "${LOG_PATH}"
printf "🔧 Installing RustDesk...\n"
sudo bash -c "$INSTALL_CMD" 2>&1 | tee -a "${LOG_PATH}"
printf "🧹 Cleaning up...\n"
bash -c "$CLEAN_CMD" 2>&1 | tee -a "${LOG_PATH}"
else
printf "✅ RustDesk already installed\n"
fi
# ---- start virtual display ----
echo "Starting Xvfb with resolution ${XVFB_RESOLUTION}"
Xvfb :99 -screen 0 "${XVFB_RESOLUTION}" >> "${LOG_PATH}" 2>&1 &
export DISPLAY=:99
# Wait for X to be ready
for i in {1..10}; do
if xdpyinfo -display :99 > /dev/null 2>&1; then
echo "X display is ready"
break
fi
sleep 1
done
# ---- create (or accept) password and start rustdesk ----
if [[ -z "${RUSTDESK_PASSWORD}" ]]; then
RUSTDESK_PASSWORD="$(tr -dc 'a-zA-Z0-9@' < /dev/urandom | head -c 10)@97"
fi
echo "Starting XFCE desktop environment..."
xfce4-session >> "${LOG_PATH}" 2>&1 &
echo "Waiting for xfce4-session to initialize..."
sleep 5
printf "🔐 Setting RustDesk password and starting service...\n"
rustdesk >> "${LOG_PATH}" 2>&1 &
sleep 2
rustdesk --password "${RUSTDESK_PASSWORD}" >> "${LOG_PATH}" 2>&1 &
sleep 3
RID="$(rustdesk --get-id 2> /dev/null || echo 'ID_PENDING')"
printf "🥳 RustDesk setup complete!\n\n"
printf "${BOLD}📋 Connection Details:${RESET}\n"
printf " RustDesk ID: ${RID}\n"
printf " RustDesk Password: ${RUSTDESK_PASSWORD}\n"
printf " Display: ${DISPLAY} (${XVFB_RESOLUTION})\n"
printf "\n📝 Logs available at: ${LOG_PATH}\n\n"
echo "Setup script completed successfully. All services running in background."
exit 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

-13
View File
@@ -1,13 +0,0 @@
---
display_name: "Jay Kumar"
bio: "I'm a Software Engineer :)"
avatar: "./.images/avatar.jpeg"
github: "35C4n0r"
linkedin: "https://www.linkedin.com/in/jaykum4r"
support_email: "work.jaykumar@gmail.com"
status: "community"
---
# Your Name
I'm a Software Engineer :)
-99
View File
@@ -1,99 +0,0 @@
---
display_name: "tmux"
description: "tmux with session persistence and plugins"
icon: "../../../../.icons/tmux.svg"
verified: false
tags: ["tmux", "terminal", "persistent"]
---
# tmux
This module provisions and configures [tmux](https://github.com/tmux/tmux) with session persistence and plugin support
for a Coder agent. It automatically installs tmux, the Tmux Plugin Manager (TPM), and a set of useful plugins, and sets
up a default or custom tmux configuration with session save/restore capabilities.
```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
}
```
## Features
- Installs tmux if not already present
- Installs TPM (Tmux Plugin Manager)
- Configures tmux with plugins for sensible defaults, session persistence, and automation:
- `tmux-plugins/tpm`
- `tmux-plugins/tmux-sensible`
- `tmux-plugins/tmux-resurrect`
- `tmux-plugins/tmux-continuum`
- Supports custom tmux configuration
- Enables automatic session save
- Configurable save interval
- **Supports multiple named tmux sessions, each as a separate app in the Coder UI**
## Usage
```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.1"
agent_id = coder_agent.example.id
tmux_config = "" # Optional: custom tmux.conf content
save_interval = 1 # Optional: save interval in minutes
sessions = ["default", "dev", "ops"] # Optional: list of tmux sessions
order = 1 # Optional: UI order
group = "Terminal" # Optional: UI group
icon = "/icon/tmux.svg" # Optional: app icon
}
```
## Multi-Session Support
This module can provision multiple tmux sessions, each as a separate app in the Coder UI. Use the `sessions` variable to specify a list of session names. For each session, a `coder_app` is created, allowing you to launch or attach to that session directly from the UI.
- **sessions**: List of tmux session names (default: `["default"]`).
## How It Works
- **tmux Installation:**
- Checks if tmux is installed; if not, installs it using the system's package manager (supports apt, yum, dnf,
zypper, apk, brew).
- **TPM Installation:**
- Installs the Tmux Plugin Manager (TPM) to `~/.tmux/plugins/tpm` if not already present.
- **tmux Configuration:**
- If `tmux_config` is provided, writes it to `~/.tmux.conf`.
- Otherwise, generates a default configuration with plugin support and session persistence (using tmux-resurrect and
tmux-continuum).
- Sets up key bindings for quick session save (`Ctrl+s`) and restore (`Ctrl+r`).
- **Plugin Installation:**
- Installs plugins via TPM.
- **Session Persistence:**
- Enables automatic session save/restore at the configured interval.
## Example
```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.1"
agent_id = var.agent_id
sessions = ["default", "dev", "anomaly"]
tmux_config = <<-EOT
set -g mouse on
set -g history-limit 10000
EOT
group = "Terminal"
order = 2
}
```
> [!IMPORTANT]
> If you provide a custom `tmux_config`, it will completely replace the default configuration. Ensure you include plugin and TPM initialization lines if you want plugin support and session persistence.
> The script will attempt to install dependencies using `sudo` where required.
> If `git` is not installed, TPM installation will fail.
> If you are using custom config, you'll be responsible for setting up persistence and plugins.
> The `order`, `group`, and `icon` variables allow you to customize how tmux apps appear in the Coder UI.
> In case of session restart or shh reconnection, the tmux session will be automatically restored :)
@@ -1,37 +0,0 @@
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("tmux 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);
// check that the script contains expected lines
expect(scriptResource.script).toContain("Installing tmux");
expect(scriptResource.script).toContain(
"Installing Tmux Plugin Manager (TPM)",
);
expect(scriptResource.script).toContain("tmux configuration created at");
expect(scriptResource.script).toContain("✅ tmux setup complete!");
});
});
-78
View File
@@ -1,78 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "tmux_config" {
type = string
description = "Custom tmux configuration to apply."
default = ""
}
variable "save_interval" {
type = number
description = "Save interval (in minutes)."
default = 1
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/tmux.svg"
}
variable "sessions" {
type = list(string)
description = "List of tmux sessions to create or start."
default = ["default"]
}
resource "coder_script" "tmux" {
agent_id = var.agent_id
display_name = "tmux"
icon = "/icon/terminal.svg"
script = templatefile("${path.module}/scripts/run.sh", {
TMUX_CONFIG = var.tmux_config
SAVE_INTERVAL = var.save_interval
})
run_on_start = true
run_on_stop = false
}
resource "coder_app" "tmux_sessions" {
for_each = toset(var.sessions)
agent_id = var.agent_id
slug = "tmux-${each.value}"
display_name = "tmux - ${each.value}"
icon = var.icon
order = var.order
group = var.group
command = templatefile("${path.module}/scripts/start.sh", {
SESSION_NAME = each.value
})
}
@@ -1,153 +0,0 @@
#!/usr/bin/env bash
BOLD='\033[0;1m'
# Convert templated variables to shell variables
SAVE_INTERVAL="${SAVE_INTERVAL}"
TMUX_CONFIG="${TMUX_CONFIG}"
# Function to install tmux
install_tmux() {
printf "Checking for tmux installation\n"
if command -v tmux &> /dev/null; then
printf "tmux is already installed \n\n"
return 0
fi
printf "Installing tmux \n\n"
# Detect package manager and install tmux
if command -v apt-get &> /dev/null; then
sudo apt-get update
sudo apt-get install -y tmux
elif command -v yum &> /dev/null; then
sudo yum install -y tmux
elif command -v dnf &> /dev/null; then
sudo dnf install -y tmux
elif command -v zypper &> /dev/null; then
sudo zypper install -y tmux
elif command -v apk &> /dev/null; then
sudo apk add tmux
elif command -v brew &> /dev/null; then
brew install tmux
else
printf "No supported package manager found. Please install tmux manually. \n"
exit 1
fi
printf "tmux installed successfully \n"
}
# Function to install Tmux Plugin Manager (TPM)
install_tpm() {
local tpm_dir="$HOME/.tmux/plugins/tpm"
if [ -d "$tpm_dir" ]; then
printf "TPM is already installed"
return 0
fi
printf "Installing Tmux Plugin Manager (TPM) \n"
# Create plugins directory
mkdir -p "$HOME/.tmux/plugins"
# Clone TPM repository
if command -v git &> /dev/null; then
git clone https://github.com/tmux-plugins/tpm "$tpm_dir"
printf "TPM installed successfully"
else
printf "Git is not installed. Please install git to use tmux plugins. \n"
exit 1
fi
}
# Function to create tmux configuration
setup_tmux_config() {
printf "Setting up tmux configuration \n"
local config_dir="$HOME/.tmux"
local config_file="$HOME/.tmux.conf"
mkdir -p "$config_dir"
if [ -n "$TMUX_CONFIG" ]; then
printf "$TMUX_CONFIG" > "$config_file"
printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n"
else
cat > "$config_file" << EOF
# Tmux Configuration File
# =============================================================================
# PLUGIN CONFIGURATION
# =============================================================================
# List of plugins
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'
# tmux-continuum configuration
set -g @continuum-restore 'on'
set -g @continuum-save-interval '$${SAVE_INTERVAL}'
set -g @continuum-boot 'on'
set -g status-right 'Continuum status: #{continuum_status}'
# =============================================================================
# KEY BINDINGS FOR SESSION MANAGEMENT
# =============================================================================
# Quick session save and restore
bind C-s run-shell "~/.tmux/plugins/tmux-resurrect/scripts/save.sh"
bind C-r run-shell "~/.tmux/plugins/tmux-resurrect/scripts/restore.sh"
# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf)
run '~/.tmux/plugins/tpm/tpm'
EOF
printf "tmux configuration created at {$config_file} \n\n"
fi
}
# Function to install tmux plugins
install_plugins() {
printf "Installing tmux plugins"
# Check if TPM is installed
if [ ! -d "$HOME/.tmux/plugins/tpm" ]; then
printf "TPM is not installed. Cannot install plugins. \n"
return 1
fi
# Install plugins using TPM
"$HOME/.tmux/plugins/tpm/bin/install_plugins"
printf "tmux plugins installed successfully \n"
}
# Main execution
main() {
printf "$${BOLD} 🛠️Setting up tmux with session persistence! \n\n"
printf ""
# Install dependencies
install_tmux
install_tpm
# Setup tmux configuration
setup_tmux_config
# Install plugins
install_plugins
printf "$${BOLD}✅ tmux setup complete! \n\n"
printf "$${BOLD} Attempting to restore sessions\n"
tmux new-session -d \; source-file ~/.tmux.conf \; run-shell '~/.tmux/plugins/tmux-resurrect/scripts/restore.sh'
printf "$${BOLD} Sessions restored: -> %s\n" "$(tmux ls)"
}
# Run main function
main
@@ -1,37 +0,0 @@
#!/usr/bin/env bash
# Convert templated variables to shell variables
SESSION_NAME='${SESSION_NAME}'
# Function to check if tmux is installed
check_tmux() {
if ! command -v tmux &> /dev/null; then
echo "tmux is not installed. Please run the tmux setup script first."
exit 1
fi
}
# Function to handle a single session
handle_session() {
local session_name="$1"
# Check if the session exists
if tmux has-session -t "$session_name" 2> /dev/null; then
echo "Session '$session_name' exists, attaching to it..."
tmux attach-session -t "$session_name"
else
echo "Session '$session_name' does not exist, creating it..."
tmux new-session -d -s "$session_name"
tmux attach-session -t "$session_name"
fi
}
# Main function
main() {
# Check if tmux is installed
check_tmux
handle_session "${SESSION_NAME}"
}
# Run the main function
main
-5
View File
@@ -1,5 +0,0 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="160" height="160" fill="#090B0B"/>
<path d="M35 79.8451C35 67.7235 45.4217 60 59.7516 60C74.0815 60 82.1149 66.7044 82.3863 76.5733L70.0105 76.9488C69.6848 71.478 64.7725 67.8844 59.7516 67.9917C52.8581 68.1257 47.7558 72.6579 47.7558 79.8451C47.7558 87.0322 52.8581 91.4839 59.7516 91.4839C64.7725 91.4839 69.5762 88.0513 70.119 82.5805L82.4948 82.8486C82.1692 92.8784 73.6472 99.6901 59.7516 99.6901C45.856 99.6901 35 91.913 35 79.8451Z" fill="white"/>
<path d="M88.0795 62.3769C89.3463 61.1103 91.1109 60.7075 92.934 60.8066C94.7578 60.9058 96.8171 61.5088 98.9613 62.4736C101.042 63.4099 103.274 64.7207 105.547 66.3466C107.821 64.721 110.052 63.4108 112.132 62.4745C114.277 61.5095 116.337 60.9057 118.162 60.8066C119.985 60.7075 121.749 61.1102 123.016 62.3769C124.282 63.6436 124.685 65.4084 124.586 67.2314C124.487 69.0554 123.882 71.1151 122.917 73.2597C121.981 75.3406 120.67 77.571 119.044 79.8447C120.67 82.1183 121.981 84.3488 122.917 86.4296C123.882 88.5741 124.487 90.634 124.586 92.4579C124.685 94.2808 124.282 96.0457 123.016 97.3124C121.749 98.5791 119.985 98.9818 118.162 98.8828C116.337 98.7836 114.277 98.1808 112.132 97.2158C110.051 96.2794 107.82 94.9687 105.546 93.3427C103.273 94.9685 101.042 96.2795 98.9613 97.2158C96.817 98.1805 94.7578 98.7845 92.934 98.8837C91.111 98.9828 89.3462 98.5799 88.0795 97.3134C86.8128 96.0467 86.4101 94.2821 86.5092 92.4589C86.6083 90.6349 87.2113 88.5752 88.1762 86.4306C89.1125 84.3496 90.4232 82.1185 92.0492 79.8447C90.4234 77.571 89.1125 75.3405 88.1762 73.2597C87.2113 71.1152 86.6084 69.0553 86.5092 67.2314C86.4101 65.4083 86.8128 63.6437 88.0795 62.3769ZM93.9565 82.3495C92.6833 84.208 91.6584 86.0034 90.9125 87.6611C90.0348 89.6118 89.5764 91.2963 89.5043 92.622C89.4323 93.9484 89.7487 94.7404 90.2006 95.1923C90.6526 95.6441 91.4447 95.9597 92.7709 95.8876C94.0964 95.8155 95.7805 95.3579 97.7309 94.4804C99.3887 93.7344 101.184 92.7078 103.042 91.4345C101.438 90.138 99.8307 88.6984 98.2621 87.1298C96.6935 85.5612 95.253 83.954 93.9565 82.3495ZM117.137 82.3486C115.84 83.9533 114.4 85.5609 112.831 87.1298C111.263 88.6984 109.656 90.138 108.051 91.4345C109.91 92.708 111.706 93.7344 113.364 94.4804C115.314 95.358 116.999 95.8156 118.325 95.8876C119.651 95.9596 120.442 95.6432 120.894 95.1913C121.346 94.7393 121.662 93.9474 121.59 92.621C121.518 91.2955 121.06 89.6114 120.182 87.6611C119.436 86.0031 118.411 84.2074 117.137 82.3486ZM105.547 70.0937C103.831 71.4383 102.091 72.9741 100.383 74.6816C98.6759 76.389 97.1398 78.1289 95.7953 79.8447C97.1397 81.5604 98.6759 83.3004 100.383 85.0078C102.09 86.715 103.831 88.2512 105.546 89.5956C107.262 88.2512 109.003 86.7161 110.71 85.0087C112.418 83.3014 113.953 81.5604 115.297 79.8447C113.953 78.1291 112.418 76.3888 110.71 74.6816C109.003 72.9743 107.263 71.4381 105.547 70.0937ZM107.481 81.582H104.481V77.582H107.481V81.582ZM92.7709 63.8017C91.4446 63.7296 90.6526 64.0462 90.2006 64.498C89.7487 64.95 89.4322 65.7419 89.5043 67.0683C89.5764 68.3939 90.0348 70.0787 90.9125 72.0292C91.6583 73.6867 92.6835 75.4816 93.9565 77.3398C95.2529 75.7354 96.6936 74.129 98.2621 72.5605C99.831 70.9916 101.438 69.5505 103.042 68.2538C101.184 66.9808 99.3885 65.9558 97.7309 65.2099C95.7805 64.3323 94.0965 63.8738 92.7709 63.8017ZM118.325 63.8027C116.999 63.8747 115.314 64.3322 113.364 65.2099C111.706 65.9558 109.91 66.9805 108.051 68.2538C109.656 69.5504 111.263 70.9908 112.831 72.5595C114.4 74.128 115.84 75.7355 117.136 77.3398C118.41 75.481 119.436 73.6861 120.182 72.0283C121.06 70.0779 121.518 68.3938 121.59 67.0683C121.662 65.7421 121.346 64.95 120.894 64.498C120.442 64.0463 119.651 63.7307 118.325 63.8027Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 KiB

-15
View File
@@ -1,15 +0,0 @@
---
display_name: Coder Labs
bio: Collection of example templates and modules for Coder. Designed for reference, not production use.
github: coder
avatar: ./.images/avatar.svg
linkedin: https://www.linkedin.com/company/coderhq
website: https://discord.gg/coder
status: official
---
å
# Coder Labs
Collection of example templates and modules for Coder. Designed for reference, not production use.
@@ -1,161 +0,0 @@
---
display_name: Auggie CLI
icon: ../../../../.icons/auggie.svg
description: Run Auggie CLI in your workspace for AI-powered coding assistance with AgentAPI integration
verified: true
tags: [agent, auggie, ai, tasks, augment]
---
# Auggie CLI
Run Auggie CLI in your workspace to access Augment's AI coding assistant with advanced context understanding and codebase integration. This module integrates with [AgentAPI](https://github.com/coder/agentapi).
```tf
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
```
## Prerequisites
- **Node.js and npm must be sourced/available before the auggie module installs** - ensure they are installed in your workspace image or via earlier provisioning steps
- You must add the [Coder Login](https://registry.coder.com/modules/coder/coder-login) module to your template
- **Augment session token for authentication (required for tasks). [Instructions](https://docs.augmentcode.com/cli/setup-auggie/authentication) to get the session token**
## Examples
### Usage with Tasks and Configuration
```tf
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Initial task prompt for Auggie CLI"
mutable = true
}
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/coder-login/coder"
version = "1.0.31"
agent_id = coder_agent.example.id
}
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
# Authentication
augment_session_token = <<-EOF
{"accessToken":"xxxx-yyyy-zzzz-jjjj","tenantURL":"https://d1.api.augmentcode.com/","scopes":["read","write"]}
EOF # Required for tasks
# Version
auggie_version = "0.3.0"
# Task configuration
ai_prompt = data.coder_parameter.ai_prompt.value
continue_previous_conversation = true
interaction_mode = "quiet"
auggie_model = "gpt5"
report_tasks = true
# MCP configuration for additional integrations
mcp = <<-EOF
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/coder/project"]
}
}
}
EOF
# Workspace guidelines
rules = <<-EOT
# Project Guidelines
## Code Style
- Use TypeScript for all new JavaScript files
- Follow consistent naming conventions
- Add comprehensive comments for complex logic
## Testing
- Write unit tests for all new functions
- Ensure test coverage above 80%
## Documentation
- Update README.md for any new features
- Document API changes in CHANGELOG.md
EOT
}
```
### Using Multiple MCP Configuration Files
```tf
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
# Multiple MCP configuration files
mcp_files = [
"/path/to/filesystem-mcp.json",
"/path/to/database-mcp.json",
"/path/to/api-mcp.json"
]
mcp = <<-EOF
{
"mcpServers": {
"Test MCP": {
"command": "uv",
"args": [
"--directory",
"/home/coder/test-mcp",
"run",
"server.py"
],
"timeout": 600
}
}
}
EOF
}
```
### Troubleshooting
If you have any issues, please take a look at the log files below.
```bash
# Installation logs
cat ~/.auggie-module/install.log
# Startup logs
cat ~/.auggie-module/agentapi-start.log
# Pre/post install script logs
cat ~/.auggie-module/pre_install.log
cat ~/.auggie-module/post_install.log
```
> [!NOTE]
> To use tasks with Auggie CLI, create a `coder_parameter` named `"AI Prompt"` and pass its value to the auggie module's `ai_prompt` variable. The `folder` variable is required for the module to function correctly.
## References
- [Auggie CLI Reference](https://docs.augmentcode.com/cli/reference)
- [Auggie CLI MCP Integration](https://docs.augmentcode.com/cli/integrations#mcp-integrations)
- [Augment Code Documentation](https://docs.augmentcode.com/)
- [AgentAPI Documentation](https://github.com/coder/agentapi)
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
@@ -1,186 +0,0 @@
run "test_auggie_basic" {
command = plan
variables {
agent_id = "test-agent-123"
folder = "/home/coder/projects"
}
assert {
condition = coder_env.auggie_session_auth.name == "AUGMENT_SESSION_AUTH"
error_message = "Auggie session auth environment variable should be set correctly"
}
assert {
condition = var.folder == "/home/coder/projects"
error_message = "Folder variable should be set correctly"
}
assert {
condition = var.agent_id == "test-agent-123"
error_message = "Agent ID variable should be set correctly"
}
assert {
condition = var.install_auggie == true
error_message = "Install auggie should default to true"
}
assert {
condition = var.install_agentapi == true
error_message = "Install agentapi should default to true"
}
}
run "test_auggie_with_session_token" {
command = plan
variables {
agent_id = "test-agent-456"
folder = "/home/coder/workspace"
augment_session_token = "test-session-token-123"
}
assert {
condition = coder_env.auggie_session_auth.value == "test-session-token-123"
error_message = "Auggie session token value should match the input"
}
}
run "test_auggie_with_custom_options" {
command = plan
variables {
agent_id = "test-agent-789"
folder = "/home/coder/custom"
order = 5
group = "development"
icon = "/icon/custom.svg"
auggie_model = "gpt-4"
ai_prompt = "Help me write better code"
interaction_mode = "compact"
continue_previous_conversation = true
install_auggie = false
install_agentapi = false
auggie_version = "1.0.0"
agentapi_version = "v0.6.0"
}
assert {
condition = var.order == 5
error_message = "Order variable should be set to 5"
}
assert {
condition = var.group == "development"
error_message = "Group variable should be set to 'development'"
}
assert {
condition = var.icon == "/icon/custom.svg"
error_message = "Icon variable should be set to custom icon"
}
assert {
condition = var.auggie_model == "gpt-4"
error_message = "Auggie model variable should be set to 'gpt-4'"
}
assert {
condition = var.ai_prompt == "Help me write better code"
error_message = "AI prompt variable should be set correctly"
}
assert {
condition = var.interaction_mode == "compact"
error_message = "Interaction mode should be set to 'compact'"
}
assert {
condition = var.continue_previous_conversation == true
error_message = "Continue previous conversation should be set to true"
}
assert {
condition = var.auggie_version == "1.0.0"
error_message = "Auggie version should be set to '1.0.0'"
}
assert {
condition = var.agentapi_version == "v0.6.0"
error_message = "AgentAPI version should be set to 'v0.6.0'"
}
}
run "test_auggie_with_mcp_and_rules" {
command = plan
variables {
agent_id = "test-agent-mcp"
folder = "/home/coder/mcp-test"
mcp = jsonencode({
mcpServers = {
test = {
command = "test-server"
args = ["--config", "test.json"]
}
}
})
mcp_files = [
"/path/to/mcp1.json",
"/path/to/mcp2.json"
]
rules = "# General coding rules\n- Write clean code\n- Add comments"
}
assert {
condition = var.mcp != ""
error_message = "MCP configuration should be provided"
}
assert {
condition = length(var.mcp_files) == 2
error_message = "Should have 2 MCP files"
}
assert {
condition = var.rules != ""
error_message = "Rules should be provided"
}
}
run "test_auggie_with_scripts" {
command = plan
variables {
agent_id = "test-agent-scripts"
folder = "/home/coder/scripts"
pre_install_script = "echo 'Pre-install script'"
post_install_script = "echo 'Post-install script'"
}
assert {
condition = var.pre_install_script == "echo 'Pre-install script'"
error_message = "Pre-install script should be set correctly"
}
assert {
condition = var.post_install_script == "echo 'Post-install script'"
error_message = "Post-install script should be set correctly"
}
}
run "test_auggie_interaction_mode_validation" {
command = plan
variables {
agent_id = "test-agent-validation"
folder = "/home/coder/test"
interaction_mode = "print"
}
assert {
condition = contains(["interactive", "print", "quiet", "compact"], var.interaction_mode)
error_message = "Interaction mode should be one of the valid options"
}
}
@@ -1,348 +0,0 @@
import {
test,
afterEach,
describe,
setDefaultTimeout,
beforeAll,
expect,
} from "bun:test";
import { execContainer, readFileContainer, runTerraformInit } from "~test";
import {
loadTestFile,
writeExecutable,
setup as setupUtil,
execModuleScript,
expectAgentAPIStarted,
} from "../../../coder/modules/agentapi/test-util";
import dedent from "dedent";
let cleanupFunctions: (() => Promise<void>)[] = [];
const registerCleanup = (cleanup: () => Promise<void>) => {
cleanupFunctions.push(cleanup);
};
afterEach(async () => {
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
cleanupFunctions = [];
for (const cleanup of cleanupFnsCopy) {
try {
await cleanup();
} catch (error) {
console.error("Error during cleanup:", error);
}
}
});
interface SetupProps {
skipAgentAPIMock?: boolean;
skipAuggieMock?: boolean;
moduleVariables?: Record<string, string>;
agentapiMockScript?: string;
}
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const projectDir = "/home/coder/project";
const { id } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
install_auggie: props?.skipAuggieMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
folder: projectDir,
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
if (!props?.skipAuggieMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/auggie",
content: await loadTestFile(import.meta.dir, "auggie-mock.sh"),
});
}
return { id };
};
setDefaultTimeout(60 * 1000);
describe("auggie", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
test("happy-path", async () => {
const { id } = await setup();
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("install-auggie-version", async () => {
const version_to_install = "0.3.0";
const { id } = await setup({
skipAuggieMock: true,
moduleVariables: {
install_auggie: "true",
auggie_version: version_to_install,
pre_install_script: dedent`
#!/usr/bin/env bash
set -euo pipefail
# Install Node.js and npm via system package manager
if ! command -v node >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y nodejs npm
fi
# Configure npm to use user directory (avoids permission issues)
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
# Persist npm user directory configuration
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
echo "prefix=$HOME/.npm-global" > ~/.npmrc
`,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`cat /home/coder/.auggie-module/install.log`,
]);
expect(resp.stdout).toContain(version_to_install);
});
test("check-latest-auggie-version-works", async () => {
const { id } = await setup({
skipAuggieMock: true,
skipAgentAPIMock: true,
moduleVariables: {
install_auggie: "true",
pre_install_script: dedent`
#!/usr/bin/env bash
set -euo pipefail
# Install Node.js and npm via system package manager
if ! command -v node >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y nodejs npm
fi
# Configure npm to use user directory (avoids permission issues)
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
# Persist npm user directory configuration
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
echo "prefix=$HOME/.npm-global" > ~/.npmrc
`,
},
});
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("auggie-session-token", async () => {
const sessionToken = "test-session-token-123";
const { id } = await setup({
moduleVariables: {
augment_session_token: sessionToken,
},
});
await execModuleScript(id);
const envCheck = await execContainer(id, [
"bash",
"-c",
`env | grep AUGMENT_SESSION_AUTH || echo "AUGMENT_SESSION_AUTH not found"`,
]);
expect(envCheck.stdout).toContain("AUGMENT_SESSION_AUTH");
});
test("auggie-mcp-config", async () => {
const mcpConfig = JSON.stringify({
mcpServers: {
test: {
command: "test-cmd",
type: "stdio",
},
},
});
const { id } = await setup({
moduleVariables: {
mcp: mcpConfig,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.auggie-module/agentapi-start.log",
);
expect(resp).toContain("--mcp-config");
});
test("auggie-rules", async () => {
const rules = "Always use TypeScript for new files";
const { id } = await setup({
moduleVariables: {
install_auggie: "false", // Don't need to install auggie to test rules file creation
rules: rules,
},
});
await execModuleScript(id);
const rulesFile = await readFileContainer(
id,
"/home/coder/.augment/rules.md",
);
expect(rulesFile).toContain(rules);
});
test("auggie-ai-task-prompt", async () => {
const prompt = "This is a task prompt for Auggie.";
const { id } = await setup({
moduleVariables: {
ai_prompt: prompt,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`cat /home/coder/.auggie-module/agentapi-start.log`,
]);
expect(resp.stdout).toContain(prompt);
});
test("auggie-interaction-mode", async () => {
const mode = "compact";
const { id } = await setup({
moduleVariables: {
interaction_mode: mode,
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.auggie-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain(`--${mode}`);
});
test("auggie-model", async () => {
const model = "gpt-4";
const { id } = await setup({
moduleVariables: {
auggie_model: model,
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.auggie-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain(`--model ${model}`);
});
test("auggie-continue-previous-conversation", async () => {
const { id } = await setup({
moduleVariables: {
continue_previous_conversation: "true",
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.auggie-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain("--continue");
});
test("pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'auggie-pre-install-script'",
post_install_script: "#!/bin/bash\necho 'auggie-post-install-script'",
},
});
await execModuleScript(id);
const preInstallLog = await readFileContainer(
id,
"/home/coder/.auggie-module/pre_install.log",
);
expect(preInstallLog).toContain("auggie-pre-install-script");
const postInstallLog = await readFileContainer(
id,
"/home/coder/.auggie-module/post_install.log",
);
expect(postInstallLog).toContain("auggie-post-install-script");
});
test("folder-variable", async () => {
const folder = "/home/coder/auggie-test-folder";
const { id } = await setup({
skipAuggieMock: false,
moduleVariables: {
folder,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.auggie-module/agentapi-start.log",
);
expect(resp).toContain(folder);
});
test("coder-mcp-config-created", async () => {
const { id } = await setup({
moduleVariables: {
install_auggie: "false", // Don't need to install auggie to test MCP config creation
},
});
await execModuleScript(id);
const mcpConfig = await readFileContainer(
id,
"/home/coder/.augment/coder_mcp.json",
);
expect(mcpConfig).toContain("mcpServers");
expect(mcpConfig).toContain("coder");
expect(mcpConfig).toContain("CODER_MCP_APP_STATUS_SLUG");
expect(mcpConfig).toContain("CODER_MCP_AI_AGENTAPI_URL");
});
test("mcp-files-array", async () => {
const mcpFiles = ["/path/to/mcp1.json", "/path/to/mcp2.json"];
const { id } = await setup({
moduleVariables: {
mcp_files: JSON.stringify(mcpFiles),
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.auggie-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain("mcp1.json");
expect(startLog.stdout).toContain("mcp2.json");
});
});

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