Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48cb3e58b0 | |||
| 0950466310 | |||
| e6fdca44f3 | |||
| 98f63d375b | |||
| 9da899ee66 | |||
| 578e2131f7 | |||
| 6b9d0d4803 | |||
| eb27843e4a | |||
| e95d90d9e8 | |||
| bd5ad3b3e4 | |||
| 6537aebb1f | |||
| 358f47b6ed | |||
| 121328f671 | |||
| dbd4928706 | |||
| a1cea027dc | |||
| 9aacddef1a | |||
| 58faf32b81 | |||
| 225aff06a7 | |||
| dd7b31d2ac | |||
| da67cd3b36 | |||
| 77392cc146 | |||
| 7a2b1ac76d | |||
| e5ccf74ccc | |||
| a47ff911e1 | |||
| a8e23647c5 | |||
| 960ec18d35 | |||
| eae64160bd | |||
| b58bfebcf3 | |||
| 05124309ee | |||
| 6d1e99d6ae | |||
| 01b70dcbaa | |||
| e54ceb3b92 | |||
| f5bf6687e7 | |||
| 7cf60c4e59 | |||
| e9870049bb | |||
| cc40d6c355 | |||
| 87310838d4 | |||
| 9e7ce393c5 | |||
| 13a25ff4af |
@@ -0,0 +1,39 @@
|
||||
## 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 -->
|
||||
|
||||
Closes #
|
||||
@@ -0,0 +1,238 @@
|
||||
#!/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,4 +1,7 @@
|
||||
[default.extend-words]
|
||||
muc = "muc" # For Munich location code
|
||||
Hashi = "Hashi"
|
||||
HashiCorp = "HashiCorp"
|
||||
HashiCorp = "HashiCorp"
|
||||
|
||||
[files]
|
||||
extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.32.0
|
||||
uses: crate-ci/typos@v1.34.0
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
|
||||
@@ -2,12 +2,16 @@ name: deploy-registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
# Matches release/<namespace>/<resource_name>/<semantic_version>
|
||||
# (e.g., "release/whizus/exoscale-zone/v1.0.13")
|
||||
- "release/*/*/v*.*.*"
|
||||
branches: # Templates get released when merged to main
|
||||
- main
|
||||
paths:
|
||||
- ".github/workflows/deploy-registry.yaml"
|
||||
- "registry/**/templates/**"
|
||||
- ".icons/**"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -29,7 +33,6 @@ jobs:
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a
|
||||
- name: Deploy to dev.registry.coder.com
|
||||
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch dev
|
||||
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 --branch main
|
||||
run: gcloud builds triggers run 106610ff-41fb-4bd0-90a2-7643583fb9c0 --tag production
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
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@v4
|
||||
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@v7
|
||||
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
|
||||
});
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -1,268 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="katman_1" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 841.9 595.3">
|
||||
<!-- Generator: Adobe Illustrator 29.3.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 151) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #2e3c4e;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st1, .st2, .st3, .st4, .st5, .st6, .st7, .st8 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.st9 {
|
||||
fill: #7300e5;
|
||||
}
|
||||
|
||||
.st10, .st11 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st12 {
|
||||
fill: url(#Adsız_degrade_4);
|
||||
}
|
||||
|
||||
.st2 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st13 {
|
||||
fill: url(#Adsız_degrade_41);
|
||||
}
|
||||
|
||||
.st14 {
|
||||
fill: #d7c8f9;
|
||||
}
|
||||
|
||||
.st15 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.st16 {
|
||||
stroke: #d1d1d6;
|
||||
stroke-width: .5px;
|
||||
}
|
||||
|
||||
.st16, .st17, .st11 {
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
.st3 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st17 {
|
||||
stroke: #fff;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.st4 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st5 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st7 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st18 {
|
||||
fill: url(#Adsız_degrade_5);
|
||||
}
|
||||
|
||||
.st8 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st19 {
|
||||
fill: url(#Adsız_degrade_2);
|
||||
}
|
||||
</style>
|
||||
<mask id="mask" x="69.2" y="33.9" width="704" height="528" maskUnits="userSpaceOnUse">
|
||||
<g id="lottie-ymehjmywpqh__lottie_element_1058_2">
|
||||
<g>
|
||||
<rect class="st11" x="69.2" y="33.9" width="704" height="528"/>
|
||||
<g class="st3">
|
||||
<path class="st14" d="M324.5,185.8v33.5c0,4.4-3.6,8-8,8h-107.8c-4.4,0-8-3.6-8-8v-33.5c0-4.4,3.6-8,8-8h107.8c4.4,0,8,3.6,8,8Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient id="Adsız_degrade_2" data-name="Adsız degrade 2" x1="68.8" y1="563" x2="-1.8" y2="563.4" gradientTransform="translate(194.4 765.6) scale(1 -1)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".5" stop-color="#fff" stop-opacity=".7"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity=".4"/>
|
||||
</linearGradient>
|
||||
<mask id="mask-1" x="69.2" y="33.9" width="704" height="528" maskUnits="userSpaceOnUse">
|
||||
<g id="lottie-ymehjmywpqh__lottie_element_1038_2">
|
||||
<g>
|
||||
<rect class="st11" x="69.2" y="33.9" width="704" height="528"/>
|
||||
<g class="st3">
|
||||
<path d="M329.4,427.5v34c0,4.4-3.6,8-8,8h-127.2c-4.4.1-8-3.5-8-7.9v-34c0-4.4,3.6-8,8-8h127.2c4.4-.1,8,3.5,8,7.9Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient id="Adsız_degrade_4" data-name="Adsız degrade 4" x1="69.7" y1="567.5" x2="-11.4" y2="566.9" gradientTransform="translate(178.6 1012.1) scale(1 -1)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".5" stop-color="#fff" stop-opacity=".6"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity=".2"/>
|
||||
</linearGradient>
|
||||
<mask id="mask-2" x="69.2" y="33.9" width="704" height="528" maskUnits="userSpaceOnUse">
|
||||
<g id="lottie-ymehjmywpqh__lottie_element_1018_2">
|
||||
<g>
|
||||
<rect class="st11" x="69.2" y="33.9" width="704" height="528"/>
|
||||
<g class="st3">
|
||||
<path class="st9" d="M689.3,383.8v34.2c-.1,4.4-3.7,8-8.1,8h-150.3c-4.4,0-8-3.6-8-8v-34.2c.1-4.4,3.7-8,8.1-8h150.2c4.4,0,8,3.6,8,8Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient id="Adsız_degrade_41" data-name="Adsız degrade 4" x1="21.7" y1="568.3" x2="163.7" y2="568.3" gradientTransform="translate(551.4 969.2) scale(1 -1)" xlink:href="#Adsız_degrade_4"/>
|
||||
<linearGradient id="Adsız_degrade_5" data-name="Adsız degrade 5" x1="56.8" y1="647.8" x2="-1.1" y2="536.3" gradientTransform="translate(349.3 2027.2) scale(3 -3)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#2fabff"/>
|
||||
<stop offset=".3" stop-color="#5570ff"/>
|
||||
<stop offset=".6" stop-color="#7b36ff"/>
|
||||
<stop offset=".8" stop-color="#6a2cdc"/>
|
||||
<stop offset="1" stop-color="#5921b8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect class="st15" x="69.2" y="33.9" width="704" height="528"/>
|
||||
<g class="st5">
|
||||
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
|
||||
</g>
|
||||
<g class="st2">
|
||||
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
|
||||
</g>
|
||||
<g class="st1">
|
||||
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
|
||||
</g>
|
||||
<g class="st5">
|
||||
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
|
||||
</g>
|
||||
<g class="st4">
|
||||
<path class="st17" d="M424.4,139.4c-2.9-1.6-7.6-1.6-10.5,0l-129.6,69.3c-2.9,1.6-5.3,5.5-5.3,8.8v153.3c0,3.3,2.4,7.3,5.2,8.9l129.6,71.9c2.9,1.6,7.6,1.6,10.5,0l130.7-71.6c2.9-1.6,5.3-5.6,5.3-8.9v-150.9c0-3.3-2.4-7.3-5.3-8.9l-130.7-72Z"/>
|
||||
</g>
|
||||
<g class="st8">
|
||||
<path class="st17" d="M424.4,157.4c-2.9-1.6-7.7-1.6-10.6,0l-115.9,61.1c-2.9,1.5-5.3,5.5-5.3,8.8v135.9c0,3.3,2.4,7.3,5.2,8.9l116,64.5c2.9,1.6,7.6,1.6,10.5,0l117.1-63.3c2.9-1.6,5.3-5.5,5.3-8.9v-135c0-3.3-2.4-7.3-5.3-8.8l-117.1-63.2Z"/>
|
||||
</g>
|
||||
<g class="st3">
|
||||
<g>
|
||||
<path class="st19" d="M324.5,185.8v33.5c0,4.4-3.6,8-8,8h-107.8c-4.4,0-8-3.6-8-8v-33.5c0-4.4,3.6-8,8-8h107.8c4.4,0,8,3.6,8,8Z"/>
|
||||
<path class="st16" d="M324.5,185.8v33.5c0,4.4-3.6,8-8,8h-107.8c-4.4,0-8-3.6-8-8v-33.5c0-4.4,3.6-8,8-8h107.8c4.4,0,8,3.6,8,8Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st7">
|
||||
<g class="st6">
|
||||
<path class="st0" d="M236.1,209.6c.2,0,.3-.1.4-.2,0,0,0-.3,0-.5v-1.3c0-.3-.1-.5-.3-.5s-.2,0-.4,0c-.4,0-.8.2-1.3.2-.5,0-.9,0-1.2,0-1.4,0-2.4-.4-3.1-1.1s-1-1.8-1-3.3v-.5c0-1.5.3-2.6,1-3.4s1.7-1.1,3.1-1.1,1.4,0,2.2.3c.2,0,.4,0,.4,0,.2,0,.3-.1.3-.5v-1.3c0-.2,0-.4,0-.5s-.2-.2-.3-.2c-1-.3-1.9-.4-2.9-.4-2.2,0-3.9.6-5.1,1.9-1.2,1.3-1.8,3.1-1.8,5.4s.6,4.1,1.7,5.3c1.2,1.2,2.9,1.8,5.1,1.8s2.2-.1,3.2-.5Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M240.4,204.7c0-2.1.7-3.2,2.1-3.2s2.1,1,2.1,3.2-.7,3.2-2.1,3.2-2.1-1.1-2.1-3.2ZM246.2,208.7c.9-.9,1.3-2.3,1.3-4s-.4-3-1.3-4c-.9-.9-2.1-1.4-3.7-1.4s-2.8.5-3.7,1.4c-.9.9-1.3,2.3-1.3,4s.5,3,1.3,4c.9.9,2.1,1.4,3.7,1.4s2.8-.5,3.7-1.4Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M252.3,209.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c.7-.5,1.4-.7,2.2-.7s.9.1,1.1.4c.2.2.4.7.4,1.2v6.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c0-1-.3-1.7-.8-2.2-.5-.5-1.2-.8-2.2-.8-1.4,0-2.6.5-3.8,1.4l-.2-.7c0-.3-.2-.4-.6-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M267.7,209.7c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M272.5,203.6c0-.8.3-1.3.7-1.7.4-.4.9-.6,1.6-.6,1.1,0,1.7.7,1.7,2v.3h-3.9ZM278.3,209.6c.2,0,.3-.1.4-.2,0,0,0-.2,0-.4v-.9c0-.3-.1-.5-.3-.5s0,0-.1,0h-.1c-1,.3-1.8.4-2.6.4s-1.8-.2-2.3-.6-.8-1-.8-1.9h5.8c.2,0,.3,0,.4,0s.1-.2.2-.4c0-.5,0-1,0-1.4,0-1.3-.4-2.4-1.1-3.1-.7-.7-1.7-1.1-3-1.1s-2.8.5-3.7,1.4c-.9,1-1.3,2.3-1.3,4s.4,3.1,1.3,4c.9.9,2.2,1.4,3.9,1.4s2.2-.2,3.2-.6Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M283.6,209.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c.7-.5,1.4-.7,2.2-.7s.9.1,1.1.4c.2.2.4.7.4,1.2v6.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c0-1-.3-1.7-.8-2.2-.5-.5-1.2-.8-2.2-.8-1.4,0-2.6.5-3.8,1.4l-.2-.7c0-.3-.2-.4-.6-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M299.1,209.7c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st3">
|
||||
<g>
|
||||
<path class="st12" d="M329.4,427.5v34c0,4.4-3.6,8-8,8h-127.2c-4.4.1-8-3.5-8-7.9v-34c0-4.4,3.6-8,8-8h127.2c4.4-.1,8,3.5,8,7.9Z"/>
|
||||
<path class="st16" d="M329.4,427.5v34c0,4.4-3.6,8-8,8h-127.2c-4.4.1-8-3.5-8-7.9v-34c0-4.4,3.6-8,8-8h127.2c4.4-.1,8,3.5,8,7.9Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st7">
|
||||
<g class="st6">
|
||||
<path class="st0" d="M213.9,439.7h1.7c2.4,0,3.6,1.5,3.6,4.4v.4c0,2.9-1.2,4.4-3.6,4.4h-1.7v-9.2ZM215.9,451.2c2,0,3.6-.6,4.7-1.8,1.1-1.2,1.7-2.9,1.7-5.1s-.6-3.9-1.7-5.1c-1.1-1.2-2.7-1.8-4.8-1.8h-4.5c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v12.9c0,.2,0,.3.1.4,0,0,.2.1.4.1h4.5Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M227.4,449.3c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM229.1,451.2c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M241.5,451.1c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M246.6,449.3c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM248.3,451.2c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M263,450.8c.6-.4,1.1-1.1,1.5-1.9.4-.8.6-1.8.6-2.9,0-1.6-.4-2.9-1.1-3.8-.8-.9-1.8-1.4-3.1-1.4s-1,.1-1.5.3c-.5.2-1,.5-1.4.8v-4.9c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v13.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.5c.3,0,.5-.2.6-.4v-.6c.5.4.9.7,1.5.9.5.2,1.1.3,1.7.3s1.6-.2,2.2-.7ZM257.9,448.7v-5.3c.6-.4,1.3-.5,2-.5s1.3.2,1.7.8.5,1.3.5,2.4-.2,1.9-.5,2.4-.9.8-1.6.8-1.4-.2-2-.5Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M269.8,449.3c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM271.5,451.2c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M284.1,450.7c.8-.6,1.1-1.4,1.1-2.4s-.2-1.3-.5-1.7c-.3-.4-.9-.8-1.8-1.2l-1.5-.6c-.5-.2-.8-.4-1-.5-.2-.1-.2-.3-.2-.6s.1-.5.4-.6c.3-.1.6-.2,1.2-.2s1.4,0,1.9.2c.3,0,.5.1.6.1.2,0,.3-.1.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.3-.2-.8-.3-1.7-.5-2.7-.5s-2.2.3-2.9.8c-.7.5-1.1,1.3-1.1,2.2s.2,1.3.5,1.8c.4.5,1,.9,1.8,1.2l1.6.6c.5.2.8.3.9.5.1.2.2.4.2.6,0,.7-.5,1-1.4,1s-1,0-1.5-.1c-.5,0-1-.2-1.5-.3-.1,0-.2,0-.3,0-.2,0-.3.1-.3.5v.9c0,.2,0,.3,0,.4,0,0,.2.2.4.2.9.4,2,.6,3.2.6s2.2-.3,3-.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M289.3,445c0-.8.3-1.3.7-1.7.4-.4.9-.6,1.6-.6,1.1,0,1.7.7,1.7,2v.3h-3.9ZM295.1,451c.2,0,.3-.1.4-.2,0,0,0-.2,0-.4v-.9c0-.3-.1-.5-.3-.5s0,0-.1,0h-.1c-1,.3-1.8.4-2.6.4s-1.8-.2-2.3-.6-.8-1-.8-1.9h5.8c.2,0,.3,0,.4,0s.1-.2.2-.4c0-.5,0-1,0-1.4,0-1.3-.4-2.4-1.1-3.1-.7-.7-1.7-1.1-3-1.1s-2.8.5-3.7,1.4c-.9,1-1.3,2.3-1.3,4s.4,3.1,1.3,4c.9.9,2.2,1.4,3.9,1.4s2.2-.2,3.2-.6Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M303.8,450.7c.8-.6,1.1-1.4,1.1-2.4s-.2-1.3-.5-1.7c-.3-.4-.9-.8-1.8-1.2l-1.5-.6c-.5-.2-.8-.4-1-.5-.2-.1-.2-.3-.2-.6s.1-.5.4-.6c.3-.1.6-.2,1.2-.2s1.4,0,1.9.2c.3,0,.5.1.6.1.2,0,.3-.1.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.3-.2-.8-.3-1.7-.5-2.7-.5s-2.2.3-2.9.8c-.7.5-1.1,1.3-1.1,2.2s.2,1.3.5,1.8c.4.5,1,.9,1.8,1.2l1.6.6c.5.2.8.3.9.5.1.2.2.4.2.6,0,.7-.5,1-1.4,1s-1,0-1.5-.1c-.5,0-1-.2-1.5-.3-.1,0-.2,0-.3,0-.2,0-.3.1-.3.5v.9c0,.2,0,.3,0,.4,0,0,.2.2.4.2.9.4,2,.6,3.2.6s2.2-.3,3-.9Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st3">
|
||||
<g>
|
||||
<path class="st13" d="M689.3,383.8v34.2c-.1,4.4-3.7,8-8.1,8h-150.3c-4.4,0-8-3.6-8-8v-34.2c.1-4.4,3.7-8,8.1-8h150.2c4.4,0,8,3.6,8,8Z"/>
|
||||
<path class="st16" d="M689.3,383.8v34.2c-.1,4.4-3.7,8-8.1,8h-150.3c-4.4,0-8-3.6-8-8v-34.2c.1-4.4,3.7-8,8.1-8h150.2c4.4,0,8,3.6,8,8Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st7">
|
||||
<g class="st6">
|
||||
<path class="st0" d="M549.5,407.8c.2,0,.4,0,.5-.1,0,0,.2-.2.2-.4l.8-2.6h4.8l.8,2.6c0,.2.2.3.2.4.1,0,.2.1.4.1h2.2c.3,0,.4-.1.4-.3s0-.1,0-.2c0,0,0-.2-.1-.3l-4.4-12.5c0-.2-.1-.3-.2-.4,0,0-.3-.1-.5-.1h-2.2c-.2,0-.3,0-.3,0,0,0-.1,0-.2.2,0,0-.1.2-.2.3l-4.4,12.5c0,.1,0,.2-.1.3,0,.1,0,.2,0,.2,0,.2.1.3.4.3h2.1ZM551.7,402.5l1.7-5.8,1.8,5.8h-3.5Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M564.9,405.3v-5.3c.6-.4,1.3-.5,2-.5s1.3.2,1.7.8.5,1.3.5,2.4-.2,1.9-.5,2.4-.9.8-1.6.8-1.4-.2-2-.5ZM564.4,412c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-4.4c.3.3.8.5,1.3.7.5.2,1,.3,1.6.3.8,0,1.6-.2,2.2-.7.6-.4,1.1-1.1,1.5-1.9.4-.8.6-1.8.6-2.9,0-1.6-.4-2.9-1.1-3.8-.8-.9-1.8-1.4-3.1-1.4s-1.2.1-1.7.3c-.6.2-1,.5-1.4.9v-.5c-.2-.3-.4-.4-.7-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v13.3c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M577.2,405.3v-5.3c.6-.4,1.3-.5,2-.5s1.3.2,1.7.8.5,1.3.5,2.4-.2,1.9-.5,2.4-.9.8-1.6.8-1.4-.2-2-.5ZM576.7,412c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-4.4c.3.3.8.5,1.3.7.5.2,1,.3,1.6.3.8,0,1.6-.2,2.2-.7.6-.4,1.1-1.1,1.5-1.9.4-.8.6-1.8.6-2.9,0-1.6-.4-2.9-1.1-3.8-.8-.9-1.8-1.4-3.1-1.4s-1.2.1-1.7.3c-.6.2-1,.5-1.4.9v-.5c-.2-.3-.4-.4-.7-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v13.3c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M590.6,407.8c.2,0,.3-.1.4-.2,0,0,0-.2,0-.5v-1c0-.2,0-.3,0-.3,0,0-.1-.1-.3-.1s-.1,0-.2,0c0,0-.2,0-.3,0-.5,0-.7-.3-.7-1v-11.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v11.4c0,2,.9,3,2.7,3s1,0,1.4-.2Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M595.8,395.6c.3-.3.5-.7.5-1.1s-.2-.9-.5-1.1-.7-.4-1.2-.4-.9.1-1.2.4c-.3.3-.5.7-.5,1.1s.1.9.5,1.1c.3.3.7.4,1.2.4s.9-.1,1.2-.4ZM595.5,407.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-9.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M604.6,408c.4,0,.8-.2,1.2-.3.2,0,.3-.1.3-.2,0,0,0-.2,0-.4v-1c0-.3-.1-.4-.3-.4s-.2,0-.3,0c-.6.2-1.1.2-1.6.2-.9,0-1.6-.2-2-.7-.4-.5-.6-1.2-.6-2.2v-.3c0-1,.2-1.8.7-2.2.4-.5,1.1-.7,2.1-.7s1,0,1.6.2c0,0,0,0,.1,0,0,0,0,0,.1,0,.2,0,.3-.2.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.4-.2-.7-.3-1.5-.4-2.3-.4-1.6,0-2.9.5-3.8,1.4-.9,1-1.4,2.3-1.4,4s.4,3,1.3,3.9c.9.9,2.1,1.4,3.7,1.4s.8,0,1.2-.1Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M610.7,405.9c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM612.4,407.8c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M624.7,407.7c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M630.1,395.6c.3-.3.5-.7.5-1.1s-.2-.9-.5-1.1-.7-.4-1.2-.4-.9.1-1.2.4c-.3.3-.5.7-.5,1.1s.1.9.5,1.1c.3.3.7.4,1.2.4s.9-.1,1.2-.4ZM629.9,407.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-9.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M635.7,402.7c0-2.1.7-3.2,2.1-3.2s2.1,1,2.1,3.2-.7,3.2-2.1,3.2-2.1-1.1-2.1-3.2ZM641.5,406.7c.9-.9,1.3-2.3,1.3-4s-.4-3-1.3-4c-.9-.9-2.1-1.4-3.7-1.4s-2.8.5-3.7,1.4c-.9.9-1.3,2.3-1.3,4s.5,3,1.3,4c.9.9,2.1,1.4,3.7,1.4s2.8-.5,3.7-1.4Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M647.6,407.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c.7-.5,1.4-.7,2.2-.7s.9.1,1.1.4c.2.2.4.7.4,1.2v6.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c0-1-.3-1.7-.8-2.2-.5-.5-1.2-.8-2.2-.8-1.4,0-2.6.5-3.8,1.4l-.2-.7c0-.3-.2-.4-.6-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M663.3,407.3c.8-.6,1.1-1.4,1.1-2.4s-.2-1.3-.5-1.7c-.3-.4-.9-.8-1.8-1.2l-1.5-.6c-.5-.2-.8-.4-1-.5-.2-.1-.2-.3-.2-.6s.1-.5.4-.6c.3-.1.6-.2,1.2-.2s1.4,0,1.9.2c.3,0,.5.1.6.1.2,0,.3-.1.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.3-.2-.8-.3-1.7-.5-2.7-.5s-2.2.3-2.9.8c-.7.5-1.1,1.3-1.1,2.2s.2,1.3.5,1.8c.4.5,1,.9,1.8,1.2l1.6.6c.5.2.8.3.9.5.1.2.2.4.2.6,0,.7-.5,1-1.4,1s-1,0-1.5-.1c-.5,0-1-.2-1.5-.3-.1,0-.2,0-.3,0-.2,0-.3.1-.3.5v.9c0,.2,0,.3,0,.4,0,0,.2.2.4.2.9.4,2,.6,3.2.6s2.2-.3,3-.9Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st18" d="M452.1,74c-19.6-11.3-43.8-11.3-63.4,0l-145.3,83.9c-19.6,11.3-31.7,32.3-31.7,54.9v167.8c0,22.7,12.1,43.6,31.7,54.9l145.3,83.9c19.6,11.3,43.8,11.3,63.4,0l145.3-83.9c19.6-11.3,31.7-32.3,31.7-54.9v-167.8c0-22.7-12.1-43.6-31.7-54.9l-145.3-83.9Z"/>
|
||||
<path class="st10" d="M438.6,293.4l-12.7,25.4,120.5,69.8,12.7-25.4-120.5-69.8ZM422.7,269.8c-2-1.1-4.4-1.1-6.3,0l-21.1,12.2c-2,1.1-3.2,3.2-3.2,5.5v24.4c0,2.3,1.2,4.4,3.2,5.5l21.1,12.2c2,1.1,4.4,1.1,6.3,0l21.1-12.2c2-1.1,3.2-3.2,3.2-5.5v-24.4c0-2.3-1.2-4.4-3.2-5.5l-21.1-12.2ZM411.6,163.4c7.9-4.5,17.5-4.5,25.4,0l98.2,56.7c7.9,4.5,12.7,12.9,12.7,22v113.4c0,9.1-4.8,17.4-12.7,22l-98.2,56.7c-7.9,4.5-17.5,4.5-25.4,0l-98.2-56.7c-7.9-4.5-12.7-12.9-12.7-22v-113.4c0-9.1,4.8-17.4,12.7-22l98.2-56.7ZM395.7,135.2l-103.1,59.5c-15.7,9.1-25.4,25.8-25.4,44v119c0,18.1,9.7,34.9,25.4,44l103.1,59.5c15.7,9.1,35,9.1,50.8,0l103.1-59.5c15.7-9.1,25.4-25.8,25.4-44v-119c0-18.1-9.7-34.9-25.4-44l-103.1-59.5c-15.7-9.1-35-9.1-50.8,0Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<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: 20 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,27 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,4 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 612 B |
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 953 B |
@@ -0,0 +1,3 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,90 +1,14 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,21 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#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>
|
||||
|
After Width: | Height: | Size: 390 B |
@@ -0,0 +1,35 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,5 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 452 B |
@@ -0,0 +1,3 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 904 B |
@@ -1,294 +1,319 @@
|
||||
# Contributing
|
||||
# Contributing to the Coder Registry
|
||||
|
||||
## Getting started
|
||||
Welcome! This guide covers how to contribute to the Coder Registry, whether you're creating a new module or improving an existing one.
|
||||
|
||||
This repo uses two main runtimes to verify the correctness of a module/template before it is published:
|
||||
## What is the Coder Registry?
|
||||
|
||||
- [Bun](https://bun.sh/) – Used to run tests for each module/template to validate overall functionality and correctness of Terraform output
|
||||
- [Go](https://go.dev/) – Used to validate all README files in the directory. The README content is used to populate [the Registry website](https://registry.coder.com).
|
||||
The Coder Registry is a collection of Terraform modules that extend Coder workspaces with development tools like VS Code, Cursor, JetBrains IDEs, and more.
|
||||
|
||||
### Installing Bun
|
||||
## Types of Contributions
|
||||
|
||||
To install Bun, you can run this command on Linux/MacOS:
|
||||
- **[New Modules](#creating-a-new-module)** - Add support for a new tool or functionality
|
||||
- **[Existing Modules](#contributing-to-existing-modules)** - Fix bugs, add features, or improve documentation
|
||||
- **[Bug Reports](#reporting-issues)** - Report problems or request features
|
||||
|
||||
```shell
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
```
|
||||
|
||||
Or this command on Windows:
|
||||
Install project dependencies:
|
||||
|
||||
```shell
|
||||
powershell -c "irm bun.sh/install.ps1 | iex"
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
Follow the instructions to ensure that Bun is available globally. Once Bun is installed, install all necessary dependencies from the root of the repo:
|
||||
### Understanding Namespaces
|
||||
|
||||
Via NPM:
|
||||
All modules are organized under `/registry/[namespace]/modules/`. Each contributor gets their own namespace (e.g., `/registry/your-username/modules/`). If a namespace is taken, choose a different unique namespace, but you can still use any display name on the Registry website.
|
||||
|
||||
```shell
|
||||
npm i
|
||||
### 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
|
||||
```
|
||||
|
||||
Via PNPM:
|
||||
#### Add Your Avatar
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
Every namespace must have an avatar. We recommend using your GitHub avatar:
|
||||
|
||||
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
|
||||
|
||||
The avatar must be:
|
||||
|
||||
- Named exactly `avatar.png` or `avatar.svg`
|
||||
- Square image (recommended: 400x400px minimum)
|
||||
- Supported formats: `.png` or `.svg` only
|
||||
|
||||
#### 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_url: "./.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.
|
||||
```
|
||||
|
||||
This repo does not support Yarn.
|
||||
> **Note**: The `avatar_url` must point to `./.images/avatar.png` or `./.images/avatar.svg`.
|
||||
|
||||
### Installing Go (optional)
|
||||
### 2. Generate Module Files
|
||||
|
||||
This step can be skipped if you are not working on any of the README validation logic. The validation will still run as part of CI.
|
||||
|
||||
[Navigate to the official Go Installation page](https://go.dev/doc/install), and install the correct version for your operating system.
|
||||
|
||||
Once Go has been installed, verify the installation via:
|
||||
|
||||
```shell
|
||||
go version
|
||||
```bash
|
||||
./scripts/new_module.sh [your-username]/[module-name]
|
||||
cd registry/[your-username]/modules/[module-name]
|
||||
```
|
||||
|
||||
## Namespaces
|
||||
This script generates:
|
||||
|
||||
All Coder resources are scoped to namespaces placed at the top level of the `/registry` directory. Any modules or templates must be placed inside a namespace to be accepted as a contribution. For example, all modules created by CoderEmployeeBob would be placed under `/registry/coderemployeebob/modules`, with a subdirectory for each individual module the user has published.
|
||||
- `main.tf` - Terraform configuration template
|
||||
- `README.md` - Documentation template with frontmatter
|
||||
- `run.sh` - Script for module execution (can be deleted if not required)
|
||||
|
||||
If a namespace is already taken, you will need to create a different, unique namespace, but will still be able to choose any display name. (The display name is shown in the Registry website. More info below.)
|
||||
### 3. Build Your Module
|
||||
|
||||
### Namespace (contributor profile) README files
|
||||
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 `main.test.ts`** to test your module
|
||||
4. **Add any scripts** or additional files your module needs
|
||||
|
||||
More information about contributor profile README files can be found below.
|
||||
### 4. Test and Submit
|
||||
|
||||
### Images
|
||||
```bash
|
||||
# Test your module
|
||||
bun test -t 'module-name'
|
||||
|
||||
Any images needed for either the main namespace directory or a module/template can be placed in a relative `/images` directory at the top of the namespace directory. (e.g., CoderEmployeeBob can have a `/registry/coderemployeebob/images` directory, that can be referenced by the main README file, as well as a README file in `/registry/coderemployeebob/modules/custom_module/README.md`.) This is to minimize the risk of file name conflicts between different users as they add images to help illustrate parts of their README files.
|
||||
# Format code
|
||||
bun fmt
|
||||
|
||||
## Coder modules
|
||||
|
||||
### Adding a new module
|
||||
|
||||
> [!WARNING]
|
||||
> These instructions cannot be followed just yet; the script referenced will be made available shortly. Contributors looking to add modules early will need to create all directories manually.
|
||||
|
||||
Once Bun (and possibly Go) have been installed, clone the Coder Registry repository. From there, you can run this script to make it easier to start contributing a new module or template:
|
||||
|
||||
```shell
|
||||
./new.sh USER_NAMESPACE/NAME_OF_NEW_MODULE
|
||||
# Commit and create PR
|
||||
git add .
|
||||
git commit -m "Add [module-name] module"
|
||||
git push origin your-branch
|
||||
```
|
||||
|
||||
You can also create a module file manually by creating the necessary files and directories.
|
||||
> **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.
|
||||
|
||||
### The composition of a Coder module
|
||||
---
|
||||
|
||||
Each Coder Module must contain the following files:
|
||||
## Contributing to Existing Modules
|
||||
|
||||
- A `main.tf` file that defines the main Terraform-based functionality
|
||||
- A `main.test.ts` file that is used to validate that the module works as expected
|
||||
- A `README.md` file containing required information (listed below)
|
||||
### 1. Find the Module
|
||||
|
||||
You are free to include any additional files in the module, as needed by the module. For example, the [Windows RDP module](https://github.com/coder/registry/tree/main/registry/coder/modules/windows-rdp) contains additional files for injecting specific functionality into a Coder Workspace.
|
||||
|
||||
> [!NOTE]
|
||||
> Some legacy modules do not have test files defined just yet. This will be addressed soon.
|
||||
|
||||
### The `main.tf` file
|
||||
|
||||
This file defines all core Terraform functionality, to be mixed into your Coder workspaces. More information about [Coder's use of Terraform can be found here](https://coder.com/docs/admin/templates/extending-templates/modules), and [general information about the Terraform language can be found in the official documentation](https://developer.hashicorp.com/terraform/docs).
|
||||
|
||||
### The structure of a module README
|
||||
|
||||
Validation criteria for module README files is listed below.
|
||||
|
||||
### Testing a Module
|
||||
|
||||
> [!IMPORTANT]
|
||||
> It is the responsibility of the module author to implement tests for every new module they wish to contribute. It is expected the author has tested the module locally before opening a PR. Feel free to reference existing test files to get an idea for how to set them up.
|
||||
|
||||
All general-purpose test helpers for validating Terraform can be found in the top-level `/testing` directory. The helpers run `terraform apply` on modules that use variables, testing the script output against containers.
|
||||
|
||||
When writing a test file, you can import the test utilities via the `~test` import alias:
|
||||
|
||||
```ts
|
||||
// This works regardless of how deeply-nested your test file is in the file
|
||||
// structure
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
```bash
|
||||
find registry -name "*[module-name]*" -type d
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The testing suite must be able to run docker containers with the `--network=host` flag. This typically requires running the tests on Linux as this flag does not apply to Docker Desktop for MacOS or Windows. MacOS users can work around this by using something like [colima](https://github.com/abiosoft/colima) or [Orbstack](https://orbstack.dev/) instead of Docker Desktop.
|
||||
### 2. Make Your Changes
|
||||
|
||||
#### Running tests
|
||||
**For bug fixes:**
|
||||
|
||||
You can run all tests by running this command from the root of the Registry directory:
|
||||
- Reproduce the issue
|
||||
- Fix the code in `main.tf`
|
||||
- Add/update tests
|
||||
- Update documentation if needed
|
||||
|
||||
```shell
|
||||
**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
|
||||
|
||||
### 3. Test Your Changes
|
||||
|
||||
```bash
|
||||
# Test a specific module
|
||||
bun test -t 'module-name'
|
||||
|
||||
# Test all modules
|
||||
bun test
|
||||
```
|
||||
|
||||
Note that running _all_ tests can take some time, so you likely don't want to be running this command as part of your core development loop.
|
||||
### 4. Maintain Backward Compatibility
|
||||
|
||||
To run specific tests, you can use the `-t` flag, which accepts a filepath regex:
|
||||
- New variables should have default values
|
||||
- Don't break existing functionality
|
||||
- Test that minimal configurations still work
|
||||
|
||||
```shell
|
||||
bun test -t '<regex_pattern>'
|
||||
---
|
||||
|
||||
## 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`
|
||||
- **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
|
||||
- `main.test.ts` - Working tests
|
||||
- `README.md` - Documentation with frontmatter
|
||||
|
||||
### 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
|
||||
---
|
||||
```
|
||||
|
||||
To ensure that the module runs predictably in local development, you can update the Terraform source as follows:
|
||||
### README Requirements
|
||||
|
||||
```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>"
|
||||
}
|
||||
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
|
||||
```
|
||||
|
||||
## Updating README files
|
||||
The script will:
|
||||
|
||||
This repo uses Go to validate each README file. If you are working with the README files at all (i.e., creating them, modifying them), it is strongly recommended that you install Go (installation instructions mentioned above), so that the files can be validated locally.
|
||||
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
|
||||
|
||||
### Validating all README files
|
||||
**Important**: Only run the version bump script if your changes require a new release. Documentation-only changes don't need version updates.
|
||||
|
||||
To validate all README files throughout the entire repo, you can run the following:
|
||||
---
|
||||
|
||||
```shell
|
||||
go build ./cmd/readmevalidation && ./readmevalidation
|
||||
```
|
||||
## Reporting Issues
|
||||
|
||||
The resulting binary is already part of the `.gitignore` file, but you can remove it with:
|
||||
When reporting bugs, include:
|
||||
|
||||
```shell
|
||||
rm ./readmevalidation
|
||||
```
|
||||
- Module name and version
|
||||
- Expected vs actual behavior
|
||||
- Minimal reproduction case
|
||||
- Error messages
|
||||
- Environment details (OS, Terraform version)
|
||||
|
||||
### README validation criteria
|
||||
---
|
||||
|
||||
The following criteria exists for two reasons:
|
||||
## Getting Help
|
||||
|
||||
1. Content accessibility
|
||||
2. Having content be designed in a way that's easy for the Registry site build step to use
|
||||
- **Examples**: Check `/registry/coder/modules/` for well-structured modules
|
||||
- **Issues**: Open an issue for technical problems
|
||||
- **Community**: Reach out to the Coder community for questions
|
||||
|
||||
#### General README requirements
|
||||
## Common Pitfalls
|
||||
|
||||
- There must be a frontmatter section.
|
||||
- There must be exactly one h1 header, and it must be at the very top, directly below the frontmatter.
|
||||
- The README body (if it exists) must start with an h1 header. No other content (including GitHub-Flavored Markdown alerts) is allowed to be placed above it.
|
||||
- When increasing the level of a header, the header's level must be incremented by one each time.
|
||||
- Any `.hcl` code snippets must be labeled as `.tf` snippets instead
|
||||
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** `bun fmt` before submitting
|
||||
|
||||
````txt
|
||||
```tf
|
||||
Content
|
||||
```
|
||||
````
|
||||
|
||||
#### Namespace (contributor profile) criteria
|
||||
|
||||
In addition to the general criteria, all README files must have the following:
|
||||
|
||||
- Frontmatter metadata with support for the following fields:
|
||||
|
||||
- `display_name` (required string) – The name to use when displaying your user profile in the Coder Registry site.
|
||||
- `bio` (optional string) – A short description of who you are.
|
||||
- `github` (optional string) – Your GitHub handle.
|
||||
- `avatar_url` (optional string) – A relative/absolute URL pointing to your avatar for the Registry site. It is strongly recommended that you commit avatar images to this repo and reference them via a relative URL.
|
||||
- `linkedin` (optional string) – A URL pointing to your LinkedIn page.
|
||||
- `support_email` (optional string) – An email for users to reach you at if they need help with a published module/template.
|
||||
- `status` (string union) – If defined, this must be one of `"community"`, `"partner"`, or `"official"`. `"community"` should be used for the majority of external contributions. `"partner"` is for companies who have a formal business partnership with Coder. `"official"` should be used only by Coder employees.
|
||||
|
||||
- The README body (the content that goes directly below the frontmatter) is allowed to be empty, but if it isn't, it must follow all the rules above.
|
||||
|
||||
You are free to customize the body of a contributor profile however you like, adding any number of images or information. Its content will never be rendered in the Registry website.
|
||||
|
||||
Additional information can be placed in the README file below the content listed above, using any number of headers.
|
||||
|
||||
Additional image/video assets can be placed in the same user namespace directory where that user's main content lives.
|
||||
|
||||
#### Module criteria
|
||||
|
||||
In addition to the general criteria, all README files must have the following:
|
||||
|
||||
- Frontmatter that describes metadata for the module:
|
||||
- `display_name` (required string) – This is the name displayed on the Coder Registry website
|
||||
- `description` (required string) – A short description of the module, which is displayed on the Registry website
|
||||
- `icon` (required string) – A relative/absolute URL pointing to the icon to display for the module in the Coder Registry website.
|
||||
- `verified` (optional boolean) – Indicates whether the module has been officially verified by Coder. Please do not set this without approval from a Coder employee.
|
||||
- `tags` (required string array) – A list of metadata tags to describe the module. Used in the Registry site for search and navigation functionality.
|
||||
- `maintainer_github` (deprecated string) – The name of the creator of the module. This field exists for backwards compatibility with previous versions of the Registry, but going forward, the value will be inferred from the namespace directory.
|
||||
- `partner_github` (deprecated string) - The name of any additional creators for a module. This field exists for backwards compatibility with previous versions of the Registry, but should not ever be used going forward.
|
||||
- The following content directly under the h1 header (without another header between them):
|
||||
|
||||
- A description of what the module does
|
||||
- A Terraform snippet for letting other users import the functionality
|
||||
|
||||
```tf
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.0.19"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
Additional information can be placed in the README file below the content listed above, using any number of headers.
|
||||
|
||||
Additional image/video assets can be placed in one of two places:
|
||||
|
||||
1. In the same user namespace directory where that user's main content lives
|
||||
2. If the image is an icon, it can be placed in the top-level `.icons` directory (this is done because a lot of modules will be based off the same products)
|
||||
|
||||
## Releases
|
||||
|
||||
The release process involves the following steps:
|
||||
|
||||
### 1. Create and merge a new PR
|
||||
|
||||
- Create a PR with your module changes
|
||||
- Get your PR reviewed, approved, and merged into the `main` branch
|
||||
|
||||
### 2. Prepare Release (Maintainer Task)
|
||||
|
||||
After merging to `main`, a maintainer will:
|
||||
|
||||
- Check out the merge commit:
|
||||
|
||||
```shell
|
||||
git checkout MERGE_COMMIT_ID
|
||||
```
|
||||
|
||||
- Create annotated tags for each module that was changed:
|
||||
|
||||
```shell
|
||||
git tag -a "release/$namespace/$module/v$version" -m "Release $namespace/$module v$version"
|
||||
```
|
||||
|
||||
- Push the tags to origin:
|
||||
|
||||
```shell
|
||||
git push origin release/$namespace/$module/v$version
|
||||
```
|
||||
|
||||
For example, to release version 1.0.14 of the coder/aider module:
|
||||
|
||||
```shell
|
||||
git tag -a "release/coder/aider/v1.0.14" -m "Release coder/aider v1.0.14"
|
||||
git push origin release/coder/aider/v1.0.14
|
||||
```
|
||||
|
||||
### Version Numbers
|
||||
|
||||
Version numbers should follow semantic versioning:
|
||||
|
||||
- **Patch version** (1.2.3 → 1.2.4): Bug fixes
|
||||
- **Minor version** (1.2.3 → 1.3.0): New features, adding inputs, deprecating inputs
|
||||
- **Major version** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing input types)
|
||||
|
||||
### 3. Publishing to Coder Registry
|
||||
|
||||
After tags are pushed, the changes will be published to [registry.coder.com](https://registry.coder.com).
|
||||
|
||||
> [!NOTE]
|
||||
> Some data in registry.coder.com is fetched on demand from this repository's `main` branch. This data should update almost immediately after a release, while other changes will take some time to propagate.
|
||||
Happy contributing! 🚀
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# 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`, `main.test.ts`, `README.md`)
|
||||
- [ ] Proper frontmatter in README
|
||||
- [ ] Working tests (`bun test`)
|
||||
- [ ] Formatted code (`bun run fmt`)
|
||||
- [ ] Avatar image for new namespaces (`avatar.png` or `avatar.svg` in `.images/`)
|
||||
|
||||
### 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 version change (e.g., `v1.2.3 → v1.2.4`).
|
||||
|
||||
### Validate READMEs
|
||||
|
||||
```bash
|
||||
go build ./cmd/readmevalidation && ./readmevalidation
|
||||
```
|
||||
|
||||
## Making a Release
|
||||
|
||||
### Create Release Tags
|
||||
|
||||
After merging a PR:
|
||||
|
||||
1. Get the new version from the PR (shown as `old → new`)
|
||||
2. Checkout the merge commit and create the tag:
|
||||
|
||||
```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"
|
||||
maintainer_github: "username"
|
||||
partner_github: "partner-name" # Optional - For official partner modules
|
||||
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_url: "./.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
|
||||
@@ -2,8 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
@@ -15,7 +15,14 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var supportedResourceTypes = []string{"modules", "templates"}
|
||||
var (
|
||||
supportedResourceTypes = []string{"modules", "templates"}
|
||||
|
||||
// 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+=`)
|
||||
)
|
||||
|
||||
type coderResourceFrontmatter struct {
|
||||
Description string `yaml:"description"`
|
||||
@@ -49,36 +56,36 @@ func validateCoderResourceDescription(description string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCoderResourceIconURL(iconURL string) []error {
|
||||
problems := []error{}
|
||||
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 == "" {
|
||||
problems = append(problems, xerrors.New("icon URL cannot be empty"))
|
||||
return problems
|
||||
return []error{xerrors.New("icon URL cannot be empty")}
|
||||
}
|
||||
|
||||
isAbsoluteURL := !strings.HasPrefix(iconURL, ".") && !strings.HasPrefix(iconURL, "/")
|
||||
if isAbsoluteURL {
|
||||
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 {
|
||||
problems = append(problems, xerrors.New("absolute icon URL is not correctly formatted"))
|
||||
errs = append(errs, xerrors.New("absolute icon URL is not correctly formatted"))
|
||||
}
|
||||
if strings.Contains(iconURL, "?") {
|
||||
problems = append(problems, xerrors.New("icon URLs cannot contain query parameters"))
|
||||
errs = append(errs, xerrors.New("icon URLs cannot contain query parameters"))
|
||||
}
|
||||
return problems
|
||||
return errs
|
||||
}
|
||||
|
||||
// Would normally be skittish about having relative paths like this, but it
|
||||
// should be safe because we have guarantees about the structure of the
|
||||
// repo, and where this logic will run.
|
||||
isPermittedRelativeURL := strings.HasPrefix(iconURL, "./") ||
|
||||
strings.HasPrefix(iconURL, "/") ||
|
||||
strings.HasPrefix(iconURL, "../../../../.icons")
|
||||
if !isPermittedRelativeURL {
|
||||
problems = append(problems, 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))
|
||||
// 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 problems
|
||||
return errs
|
||||
}
|
||||
|
||||
func validateCoderResourceTags(tags []string) error {
|
||||
@@ -89,9 +96,8 @@ func validateCoderResourceTags(tags []string) error {
|
||||
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.
|
||||
// All of these tags are used for the module/template filter controls in the Registry site. Need to make sure they
|
||||
// can all be placed in the browser URL without issue.
|
||||
invalidTags := []string{}
|
||||
for _, t := range tags {
|
||||
if t != url.QueryEscape(t) {
|
||||
@@ -105,16 +111,11 @@ func validateCoderResourceTags(tags []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Todo: This is a holdover from the validation logic used by the Coder Modules
|
||||
// repo. It gives us some assurance, but realistically, we probably want to
|
||||
// parse any Terraform code snippets, and make some deeper guarantees about how
|
||||
// it's structured. Just validating whether it *can* be parsed as Terraform
|
||||
// would be a big improvement.
|
||||
var terraformVersionRe = regexp.MustCompile(`^\s*\bversion\s+=`)
|
||||
|
||||
func validateCoderResourceReadmeBody(body string) []error {
|
||||
trimmed := strings.TrimSpace(body)
|
||||
var errs []error
|
||||
|
||||
trimmed := strings.TrimSpace(body)
|
||||
// TODO: this may cause unexpected behavior since the errors slice may have a 0 length. Add a test.
|
||||
errs = append(errs, validateReadmeBody(trimmed)...)
|
||||
|
||||
foundParagraph := false
|
||||
@@ -130,9 +131,8 @@ func validateCoderResourceReadmeBody(body string) []error {
|
||||
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.
|
||||
// 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
|
||||
@@ -159,15 +159,13 @@ func validateCoderResourceReadmeBody(body string) []error {
|
||||
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.
|
||||
// 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.
|
||||
// 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
|
||||
@@ -250,7 +248,7 @@ func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[strin
|
||||
}
|
||||
if len(yamlParsingErrs) != 0 {
|
||||
return nil, validationPhaseError{
|
||||
phase: validationPhaseReadmeParsing,
|
||||
phase: validationPhaseReadme,
|
||||
errors: yamlParsingErrs,
|
||||
}
|
||||
}
|
||||
@@ -264,7 +262,7 @@ func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[strin
|
||||
}
|
||||
if len(yamlValidationErrors) != 0 {
|
||||
return nil, validationPhaseError{
|
||||
phase: validationPhaseReadmeParsing,
|
||||
phase: validationPhaseReadme,
|
||||
errors: yamlValidationErrors,
|
||||
}
|
||||
}
|
||||
@@ -274,7 +272,7 @@ func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[strin
|
||||
|
||||
// Todo: Need to beef up this function by grabbing each image/video URL from
|
||||
// the body's AST.
|
||||
func validateCoderResourceRelativeUrls(_ map[string]coderResourceReadme) error {
|
||||
func validateCoderResourceRelativeURLs(_ map[string]coderResourceReadme) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -321,7 +319,7 @@ func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
|
||||
|
||||
if len(errs) != 0 {
|
||||
return nil, validationPhaseError{
|
||||
phase: validationPhaseFileLoad,
|
||||
phase: validationPhaseFile,
|
||||
errors: errs,
|
||||
}
|
||||
}
|
||||
@@ -338,17 +336,16 @@ func validateAllCoderResourceFilesOfType(resourceType string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Processing %d README files\n", len(allReadmeFiles))
|
||||
logger.Info(context.Background(), "rocessing README files", "num_files", len(allReadmeFiles))
|
||||
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Processed %d README files as valid Coder resources with type %q", len(resources), resourceType)
|
||||
logger.Info(context.Background(), "rocessed README files as valid Coder resources", "num_files", len(resources), "type", resourceType)
|
||||
|
||||
err = validateCoderResourceRelativeUrls(resources)
|
||||
if err != nil {
|
||||
if err := validateCoderResourceRelativeURLs(resources); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("All relative URLs for %s READMEs are valid\n", resourceType)
|
||||
logger.Info(context.Background(), "all relative URLs for READMEs are valid", "type", resourceType)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"context"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
@@ -50,6 +50,9 @@ func validateContributorLinkedinURL(linkedinURL *string) error {
|
||||
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
|
||||
@@ -57,10 +60,6 @@ func validateContributorSupportEmail(email *string) []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))
|
||||
@@ -110,21 +109,18 @@ func validateContributorStatus(status string) error {
|
||||
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 == "" {
|
||||
errs = append(errs, xerrors.New("avatar URL must be omitted or non-empty string"))
|
||||
return errs
|
||||
return []error{xerrors.New("avatar URL must be omitted or non-empty string")}
|
||||
}
|
||||
|
||||
// Have to use .Parse instead of .ParseRequestURI because this is the
|
||||
// one field that's allowed to be a relative URL.
|
||||
errs := []error{}
|
||||
// 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))
|
||||
}
|
||||
@@ -132,7 +128,7 @@ func validateContributorAvatarURL(avatarURL *string) []error {
|
||||
errs = append(errs, xerrors.New("avatar URL is not allowed to contain search parameters"))
|
||||
}
|
||||
|
||||
matched := false
|
||||
var matched bool
|
||||
for _, ff := range supportedAvatarFileFormats {
|
||||
matched = strings.HasSuffix(*avatarURL, ff)
|
||||
if matched {
|
||||
@@ -210,22 +206,21 @@ func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfil
|
||||
}
|
||||
if len(yamlParsingErrors) != 0 {
|
||||
return nil, validationPhaseError{
|
||||
phase: validationPhaseReadmeParsing,
|
||||
phase: validationPhaseReadme,
|
||||
errors: yamlParsingErrors,
|
||||
}
|
||||
}
|
||||
|
||||
yamlValidationErrors := []error{}
|
||||
for _, p := range profilesByNamespace {
|
||||
errors := validateContributorReadme(p)
|
||||
if len(errors) > 0 {
|
||||
if errors := validateContributorReadme(p); len(errors) > 0 {
|
||||
yamlValidationErrors = append(yamlValidationErrors, errors...)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(yamlValidationErrors) != 0 {
|
||||
return nil, validationPhaseError{
|
||||
phase: validationPhaseReadmeParsing,
|
||||
phase: validationPhaseReadme,
|
||||
errors: yamlValidationErrors,
|
||||
}
|
||||
}
|
||||
@@ -241,12 +236,13 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
|
||||
|
||||
allReadmeFiles := []readme{}
|
||||
errs := []error{}
|
||||
dirPath := ""
|
||||
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 {
|
||||
@@ -261,7 +257,7 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
|
||||
|
||||
if len(errs) != 0 {
|
||||
return nil, validationPhaseError{
|
||||
phase: validationPhaseFileLoad,
|
||||
phase: validationPhaseFile,
|
||||
errors: errs,
|
||||
}
|
||||
}
|
||||
@@ -269,18 +265,18 @@ 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.
|
||||
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
|
||||
|
||||
for _, con := range contributors {
|
||||
// If the avatar URL is missing, we'll just assume that the Registry site build step will take care of filling
|
||||
// in the data properly.
|
||||
if con.frontmatter.AvatarURL == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
isRelativeURL := strings.HasPrefix(*con.frontmatter.AvatarURL, ".") ||
|
||||
strings.HasPrefix(*con.frontmatter.AvatarURL, "/")
|
||||
if !isRelativeURL {
|
||||
if !strings.HasPrefix(*con.frontmatter.AvatarURL, ".") || !strings.HasPrefix(*con.frontmatter.AvatarURL, "/") {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -291,10 +287,8 @@ func validateContributorRelativeUrls(contributors map[string]contributorProfileR
|
||||
continue
|
||||
}
|
||||
|
||||
absolutePath := strings.TrimSuffix(con.filePath, "README.md") +
|
||||
*con.frontmatter.AvatarURL
|
||||
_, err := os.Stat(absolutePath)
|
||||
if err != nil {
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -303,7 +297,7 @@ func validateContributorRelativeUrls(contributors map[string]contributorProfileR
|
||||
return nil
|
||||
}
|
||||
return validationPhaseError{
|
||||
phase: validationPhaseAssetCrossReference,
|
||||
phase: validationPhaseCrossReference,
|
||||
errors: errs,
|
||||
}
|
||||
}
|
||||
@@ -314,19 +308,18 @@ func validateAllContributorFiles() error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Processing %d README files\n", len(allReadmeFiles))
|
||||
logger.Info(context.Background(), "processing README files", "num_files", len(allReadmeFiles))
|
||||
contributors, err := parseContributorFiles(allReadmeFiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Processed %d README files as valid contributor profiles", len(contributors))
|
||||
logger.Info(context.Background(), "processed README files as valid contributor profiles", "num_contributors", len(contributors))
|
||||
|
||||
err = validateContributorRelativeUrls(contributors)
|
||||
if err != nil {
|
||||
if err := validateContributorRelativeURLs(contributors); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("All relative URLs for READMEs are valid")
|
||||
logger.Info(context.Background(), "all relative URLs for READMEs are valid")
|
||||
|
||||
log.Printf("Processed all READMEs in the %q directory\n", rootRegistryPath)
|
||||
logger.Info(context.Background(), "processed all READMEs in directory", "dir", rootRegistryPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
"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
|
||||
|
||||
@@ -7,7 +7,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@@ -19,12 +18,11 @@ var logger = slog.Make(sloghuman.Sink(os.Stdout))
|
||||
func main() {
|
||||
logger.Info(context.Background(), "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.
|
||||
// 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 {
|
||||
log.Println(err)
|
||||
logger.Error(context.Background(), "error when validating the repo structure", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,26 +2,55 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
const rootRegistryPath = "./registry"
|
||||
// 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
|
||||
|
||||
var supportedAvatarFileFormats = []string{".png", ".jpeg", ".jpg", ".gif", ".svg"}
|
||||
const (
|
||||
rootRegistryPath = "./registry"
|
||||
|
||||
// readme represents a single README file within the repo (usually within the
|
||||
// top-level "/registry" directory).
|
||||
// --- 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, must be at the beginning of a line, such as "# " or "### ".
|
||||
readmeHeaderRe = regexp.MustCompile(`^(#+)(\s*)`)
|
||||
)
|
||||
|
||||
// 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) {
|
||||
if readmeText == "" {
|
||||
@@ -29,8 +58,9 @@ func separateFrontmatter(readmeText string) (readmeFrontmatter string, readmeBod
|
||||
}
|
||||
|
||||
const fence = "---"
|
||||
fm := ""
|
||||
body := ""
|
||||
|
||||
var fm strings.Builder
|
||||
var body strings.Builder
|
||||
fenceCount := 0
|
||||
|
||||
lineScanner := bufio.NewScanner(strings.NewReader(strings.TrimSpace(readmeText)))
|
||||
@@ -40,36 +70,32 @@ func separateFrontmatter(readmeText string) (readmeFrontmatter string, readmeBod
|
||||
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 {
|
||||
body += nextLine + "\n"
|
||||
fmt.Fprintf(&body, "%s\n", nextLine)
|
||||
} else {
|
||||
fm += strings.TrimSpace(nextLine) + "\n"
|
||||
fmt.Fprintf(&fm, "%s\n", strings.TrimSpace(nextLine))
|
||||
}
|
||||
}
|
||||
if fenceCount < 2 {
|
||||
return "", "", xerrors.New("README does not have two sets of frontmatter fences")
|
||||
}
|
||||
if fm == "" {
|
||||
if fm.Len() == 0 {
|
||||
return "", "", xerrors.New("readme has frontmatter fences but no frontmatter content")
|
||||
}
|
||||
|
||||
return fm, strings.TrimSpace(body), nil
|
||||
return fm.String(), strings.TrimSpace(body.String()), nil
|
||||
}
|
||||
|
||||
var readmeHeaderRe = regexp.MustCompile(`^(#+)(\s*)`)
|
||||
|
||||
// Todo: This seems to work okay for now, but the really proper way of doing
|
||||
// this is by parsing this as an AST, and then checking the resulting nodes.
|
||||
// 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)
|
||||
|
||||
@@ -77,9 +103,8 @@ func validateReadmeBody(body string) []error {
|
||||
return []error{xerrors.New("README body is empty")}
|
||||
}
|
||||
|
||||
// If the very first line of the README, there's a risk that the rest of the
|
||||
// validation logic will break, since we don't have many guarantees about
|
||||
// how the README is actually structured.
|
||||
// If 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., \"# \")")}
|
||||
}
|
||||
@@ -93,9 +118,8 @@ func validateReadmeBody(body string) []error {
|
||||
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.
|
||||
// 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
|
||||
@@ -109,8 +133,8 @@ func validateReadmeBody(body string) []error {
|
||||
continue
|
||||
}
|
||||
|
||||
spaceAfterHeader := headerGroups[2]
|
||||
if spaceAfterHeader == "" {
|
||||
// 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"))
|
||||
}
|
||||
|
||||
@@ -121,8 +145,7 @@ func validateReadmeBody(body string) []error {
|
||||
continue
|
||||
}
|
||||
|
||||
// If we have obviously invalid headers, it's not really safe to keep
|
||||
// proceeding with the rest of the content.
|
||||
// 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
|
||||
@@ -132,43 +155,16 @@ func validateReadmeBody(body string) []error {
|
||||
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.
|
||||
// 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.
|
||||
// As long as the above condition passes, there's no problems with going up a header level or going down 1+ header levels.
|
||||
latestHeaderLevel = nextHeaderLevel
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// validationPhase represents a specific phase during README validation. It is
|
||||
// expected that each phase is discrete, and errors during one will prevent a
|
||||
// future phase from starting.
|
||||
type validationPhase string
|
||||
|
||||
const (
|
||||
// ValidationPhaseFileStructureValidation indicates when the entire Registry
|
||||
// directory is being verified for having all files be placed in the file
|
||||
// system as expected.
|
||||
validationPhaseFileStructureValidation validationPhase = "File structure validation"
|
||||
|
||||
// ValidationPhaseFileLoad indicates when README files are being read from
|
||||
// the file system.
|
||||
validationPhaseFileLoad = "Filesystem reading"
|
||||
|
||||
// ValidationPhaseReadmeParsing indicates when a README's frontmatter is
|
||||
// being parsed as YAML. This phase does not include YAML validation.
|
||||
validationPhaseReadmeParsing = "README parsing"
|
||||
|
||||
// ValidationPhaseAssetCrossReference indicates when a README's frontmatter
|
||||
// is having all its relative URLs be validated for whether they point to
|
||||
// valid resources.
|
||||
validationPhaseAssetCrossReference = "Cross-referencing relative asset URLs"
|
||||
)
|
||||
|
||||
@@ -13,40 +13,33 @@ import (
|
||||
var supportedUserNameSpaceDirectories = append(supportedResourceTypes, ".icons", ".images")
|
||||
|
||||
func validateCoderResourceSubdirectory(dirPath string) []error {
|
||||
errs := []error{}
|
||||
|
||||
subDir, err := os.Stat(dirPath)
|
||||
if err != nil {
|
||||
// It's valid for a specific resource directory not to exist. It's just
|
||||
// that if it does exist, it must follow specific rules.
|
||||
// It's valid for a specific resource directory not to exist. It's just that if it does exist, it must follow specific rules.
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
errs = append(errs, addFilePathToError(dirPath, err))
|
||||
return []error{addFilePathToError(dirPath, err)}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
if !subDir.IsDir() {
|
||||
errs = append(errs, xerrors.Errorf("%q: path is not a directory", dirPath))
|
||||
return errs
|
||||
return []error{xerrors.Errorf("%q: path is not a directory", dirPath)}
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
errs = append(errs, addFilePathToError(dirPath, err))
|
||||
return errs
|
||||
return []error{addFilePathToError(dirPath, err)}
|
||||
}
|
||||
|
||||
errs := []error{}
|
||||
for _, f := range files {
|
||||
// The .coder subdirectories are sometimes generated as part of Bun
|
||||
// tests. These subdirectories will never be committed to the repo, but
|
||||
// in the off chance that they don't get cleaned up properly, we want to
|
||||
// skip over them.
|
||||
// The .coder subdirectories are sometimes generated as part of Bun tests. These subdirectories will never be
|
||||
// committed to the repo, but in the off chance that they don't get cleaned up properly, we want to skip over them.
|
||||
if !f.IsDir() || f.Name() == ".coder" {
|
||||
continue
|
||||
}
|
||||
|
||||
resourceReadmePath := path.Join(dirPath, f.Name(), "README.md")
|
||||
_, err := os.Stat(resourceReadmePath)
|
||||
if err != nil {
|
||||
if _, err := os.Stat(resourceReadmePath); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
errs = append(errs, xerrors.Errorf("%q: 'README.md' does not exist", resourceReadmePath))
|
||||
} else {
|
||||
@@ -55,8 +48,7 @@ func validateCoderResourceSubdirectory(dirPath string) []error {
|
||||
}
|
||||
|
||||
mainTerraformPath := path.Join(dirPath, f.Name(), "main.tf")
|
||||
_, err = os.Stat(mainTerraformPath)
|
||||
if err != nil {
|
||||
if _, err := os.Stat(mainTerraformPath); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
errs = append(errs, xerrors.Errorf("%q: 'main.tf' file does not exist", mainTerraformPath))
|
||||
} else {
|
||||
@@ -64,7 +56,6 @@ func validateCoderResourceSubdirectory(dirPath string) []error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -83,8 +74,7 @@ func validateRegistryDirectory() []error {
|
||||
}
|
||||
|
||||
contributorReadmePath := path.Join(dirPath, "README.md")
|
||||
_, err := os.Stat(contributorReadmePath)
|
||||
if err != nil {
|
||||
if _, err := os.Stat(contributorReadmePath); err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
}
|
||||
|
||||
@@ -95,8 +85,7 @@ func validateRegistryDirectory() []error {
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
// Todo: Decide if there's anything more formal that we want to
|
||||
// ensure about non-directories scoped to user namespaces.
|
||||
// TODO: Decide if there's anything more formal that we want to ensure about non-directories scoped to user namespaces.
|
||||
if !f.IsDir() {
|
||||
continue
|
||||
}
|
||||
@@ -110,8 +99,7 @@ func validateRegistryDirectory() []error {
|
||||
}
|
||||
|
||||
if slices.Contains(supportedResourceTypes, segment) {
|
||||
errs := validateCoderResourceSubdirectory(filePath)
|
||||
if len(errs) != 0 {
|
||||
if errs := validateCoderResourceSubdirectory(filePath); len(errs) != 0 {
|
||||
allErrs = append(allErrs, errs...)
|
||||
}
|
||||
}
|
||||
@@ -122,20 +110,19 @@ func validateRegistryDirectory() []error {
|
||||
}
|
||||
|
||||
func validateRepoStructure() error {
|
||||
var problems []error
|
||||
if errs := validateRegistryDirectory(); len(errs) != 0 {
|
||||
problems = append(problems, errs...)
|
||||
var errs []error
|
||||
if vrdErrs := validateRegistryDirectory(); len(vrdErrs) != 0 {
|
||||
errs = append(errs, vrdErrs...)
|
||||
}
|
||||
|
||||
_, err := os.Stat("./.icons")
|
||||
if err != nil {
|
||||
problems = append(problems, xerrors.New("missing top-level .icons directory (used for storing reusable Coder resource icons)"))
|
||||
if _, err := os.Stat("./.icons"); err != nil {
|
||||
errs = append(errs, xerrors.New("missing top-level .icons directory (used for storing reusable Coder resource icons)"))
|
||||
}
|
||||
|
||||
if len(problems) != 0 {
|
||||
if len(errs) != 0 {
|
||||
return validationPhaseError{
|
||||
phase: validationPhaseFileStructureValidation,
|
||||
errors: problems,
|
||||
phase: validationPhaseStructure,
|
||||
errors: errs,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -20,7 +20,7 @@ require (
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
go.opentelemetry.io/otel v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.16.0 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/crypto v0.11.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/term v0.10.0 // indirect
|
||||
)
|
||||
|
||||
@@ -51,17 +51,17 @@ go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiM
|
||||
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
|
||||
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
|
||||
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg=
|
||||
|
||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,308 @@
|
||||
{
|
||||
"name": "registry",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "registry",
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.9",
|
||||
"bun-types": "^1.1.23",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^12.0.2",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-sh": "^0.13.1",
|
||||
"prettier-plugin-terraform-formatter": "^1.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bun": {
|
||||
"version": "1.2.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.18.tgz",
|
||||
"integrity": "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bun-types": "1.2.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
|
||||
"integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bun-types": {
|
||||
"version": "1.2.18",
|
||||
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.2.18.tgz",
|
||||
"integrity": "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"bin": {
|
||||
"esparse": "bin/esparse.js",
|
||||
"esvalidate": "bin/esvalidate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/extend-shallow": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extendable": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gray-matter": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
||||
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-yaml": "^3.13.1",
|
||||
"kind-of": "^6.0.2",
|
||||
"section-matter": "^1.0.0",
|
||||
"strip-bom-string": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extendable": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
||||
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "12.0.2",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
|
||||
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/mvdan-sh": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/mvdan-sh/-/mvdan-sh-0.10.1.tgz",
|
||||
"integrity": "sha512-kMbrH0EObaKmK3nVRKUIIya1dpASHIEusM13S4V1ViHFuxuNxCo+arxoa6j/dbV22YBGjl7UKJm9QQKJ2Crzhg==",
|
||||
"deprecated": "See https://github.com/mvdan/sh/issues/1145",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-sh": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.13.1.tgz",
|
||||
"integrity": "sha512-ytMcl1qK4s4BOFGvsc9b0+k9dYECal7U29bL/ke08FEUsF/JLN0j6Peo0wUkFDG4y2UHLMhvpyd6Sd3zDXe/eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mvdan-sh": "^0.10.1",
|
||||
"sh-syntax": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prettier": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-terraform-formatter": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-terraform-formatter/-/prettier-plugin-terraform-formatter-1.2.1.tgz",
|
||||
"integrity": "sha512-rdzV61Bs/Ecnn7uAS/vL5usTX8xUWM+nQejNLZxt3I1kJH5WSeLEmq7LYu1wCoEQF+y7Uv1xGvPRfl3lIe6+tA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"prettier": ">= 1.16.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"prettier": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/section-matter": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
||||
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"extend-shallow": "^2.0.1",
|
||||
"kind-of": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/sh-syntax": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.4.2.tgz",
|
||||
"integrity": "sha512-/l2UZ5fhGZLVZa16XQM9/Vq/hezGGbdHeVEA01uWjOL1+7Ek/gt6FquW0iKKws4a9AYPYvlz6RyVvjh3JxOteg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/strip-bom-string": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
||||
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,16 @@
|
||||
"update-version": "./update-version.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.9",
|
||||
"bun-types": "^1.1.23",
|
||||
"@types/bun": "^1.2.18",
|
||||
"bun-types": "^1.2.18",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^12.0.2",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-sh": "^0.13.1",
|
||||
"marked": "^16.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-sh": "^0.18.0",
|
||||
"prettier-plugin-terraform-formatter": "^1.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.5.4"
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"prettier": {
|
||||
"plugins": [
|
||||
@@ -25,4 +25,4 @@
|
||||
"prettier-plugin-terraform-formatter"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" fill="#090B0B"/>
|
||||
<path d="M427.787 172H296.889V188.277H427.787V172Z" fill="white"/>
|
||||
<path d="M427.787 190.878H296.889V207.154H427.787V190.878Z" fill="white"/>
|
||||
<path d="M427.787 209.74H296.889V226.017H427.787V209.74Z" fill="white"/>
|
||||
<path d="M427.787 228.618H296.889V244.895H427.787V228.618Z" fill="white"/>
|
||||
<path d="M427.787 247.496H296.889V263.773H427.787V247.496Z" fill="white"/>
|
||||
<path d="M428 266.359H297.102V282.636H428V266.359Z" fill="white"/>
|
||||
<path d="M427.68 285.237H296.783V301.513H427.68V285.237Z" fill="white"/>
|
||||
<path d="M427.68 304.115H296.783V320.391H427.68V304.115Z" fill="white"/>
|
||||
<path d="M427.893 322.993H296.996V339.269H427.893V322.993Z" fill="white"/>
|
||||
<path d="M245.778 172H116.325V188.277H245.778V172Z" fill="white"/>
|
||||
<path d="M262.024 190.878H100.17V207.154H262.024V190.878Z" fill="white"/>
|
||||
<path d="M262.024 304.115H100.17V320.391H262.024V304.115Z" fill="white"/>
|
||||
<path d="M245.747 322.993H116.325V339.269H245.747V322.993Z" fill="white"/>
|
||||
<path d="M138.839 247.496H84V263.773H138.839V247.496Z" fill="white"/>
|
||||
<path d="M148.665 266.374H84V282.651H148.665V266.374Z" fill="white"/>
|
||||
<path d="M278.088 266.374H213.422V282.651H278.088V266.374Z" fill="white"/>
|
||||
<path d="M278.087 285.237H207.17V301.513H278.087V285.237Z" fill="white"/>
|
||||
<path d="M156.652 285.237H84V301.513H156.652V285.237Z" fill="white"/>
|
||||
<path d="M156.652 209.74H84V226.017H156.652V209.74Z" fill="white"/>
|
||||
<path d="M278.118 209.74H207.17V226.017H278.118V209.74Z" fill="white"/>
|
||||
<path d="M148.665 228.618H84V244.895H148.665V228.618Z" fill="white"/>
|
||||
<path d="M278.118 228.618H213.392V244.895H278.118V228.618Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,15 @@
|
||||
---
|
||||
display_name: Coder Labs
|
||||
bio: Collection of example templates and modules for Coder. Designed for reference, not production use.
|
||||
github: coder
|
||||
avatar: ./.images/avatar.svg
|
||||
linkedin: https://www.linkedin.com/company/coderhq
|
||||
website: https://discord.gg/coder
|
||||
status: community
|
||||
---
|
||||
|
||||
å
|
||||
|
||||
# Coder Labs
|
||||
|
||||
Collection of example templates and modules for Coder. Designed for reference, not production use.
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
display_name: Tasks on Docker
|
||||
description: Run Coder Tasks on Docker with an example application
|
||||
icon: ../../../../.icons/tasks.svg
|
||||
maintainer_github: coder
|
||||
verified: false
|
||||
tags: [docker, container, ai, tasks]
|
||||
---
|
||||
|
||||
# Run Coder Tasks on Docker
|
||||
|
||||
This is an example template for running [Coder Tasks](https://coder.com/docs/ai-coder/tasks), Claude Code, along with a [real world application](https://realworld-docs.netlify.app/).
|
||||
|
||||

|
||||
|
||||
This is a fantastic starting point for working with AI agents with Coder Tasks. Try prompts such as:
|
||||
|
||||
- "Make the background color blue"
|
||||
- "Add a dark mode"
|
||||
- "Rewrite the entire backend in Go"
|
||||
|
||||
## Included in this template
|
||||
|
||||
This template is designed to be an example and a reference for building other templates with Coder Tasks. You can always run Coder Tasks on different infrastructure (e.g. as on Kubernetes, VMs) and with your own GitHub repositories, MCP servers, images, etc.
|
||||
|
||||
Additionally, this template uses our [Claude Code](https://registry.coder.com/modules/coder/claude-code) module, but [other agents](https://registry.coder.com/modules?search=tag%3Aagent) or even [custom agents](https://coder.com/docs/ai-coder/custom-agents) can be used in its place.
|
||||
|
||||
This template uses a [Workspace Preset](https://coder.com/docs/admin/templates/extending-templates/parameters#workspace-presets) that pre-defines:
|
||||
|
||||
- Universal Container Image (e.g. contains Node.js, Java, Python, Ruby, etc)
|
||||
- MCP servers (desktop-commander for long-running logs, playwright for previewing changes)
|
||||
- System prompt and [repository](https://github.com/coder-contrib/realworld-django-rest-framework-angular) for the AI agent
|
||||
- Startup script to initialize the repository and start the development server
|
||||
|
||||
## Add this template to your Coder deployment
|
||||
|
||||
You can also add this template to your Coder deployment and begin tinkering right away!
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Coder installed (see [our docs](https://coder.com/docs/install)), ideally a Linux VM with Docker
|
||||
- Anthropic API Key (or access to Anthropic models via Bedrock or Vertex, see [Claude Code docs](https://docs.anthropic.com/en/docs/claude-code/third-party-integrations))
|
||||
- Access to a Docker socket
|
||||
- If on the local VM, ensure the `coder` user is added to the Docker group (docs)
|
||||
|
||||
```sh
|
||||
# Add coder user to Docker group
|
||||
sudo adduser coder docker
|
||||
|
||||
# Restart Coder server
|
||||
sudo systemctl restart coder
|
||||
|
||||
# Test Docker
|
||||
sudo -u coder docker ps
|
||||
```
|
||||
|
||||
- If on a remote VM, see the [Docker Terraform provider documentation](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs#remote-hosts) to configure a remote host
|
||||
|
||||
To import this template into Coder, first create a template from "Scratch" in the template editor.
|
||||
|
||||
Visit this URL for your Coder deployment:
|
||||
|
||||
```sh
|
||||
https://coder.example.com/templates/new?exampleId=scratch
|
||||
```
|
||||
|
||||
After creating the template, paste the contents from [main.tf](./main.tf) into the template editor and save.
|
||||
|
||||
Alternatively, you can use the Coder CLI to [push the template](https://coder.com/docs/reference/cli/templates_push)
|
||||
|
||||
```sh
|
||||
# Download the CLI
|
||||
curl -L https://coder.com/install.sh | sh
|
||||
|
||||
# Log in to your deployment
|
||||
coder login https://coder.example.com
|
||||
|
||||
# Clone the registry
|
||||
git clone https://github.com/coder/registry
|
||||
cd registry
|
||||
|
||||
# Navigate to this template
|
||||
cd registry/coder-labs/templates/tasks-docker
|
||||
|
||||
# Push the template
|
||||
coder templates push
|
||||
```
|
||||
@@ -0,0 +1,426 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# This template requires a valid Docker socket
|
||||
# However, you can reference our Kubernetes/VM
|
||||
# example templates and adapt the Claude Code module
|
||||
#
|
||||
# see: https://registry.coder.com/templates
|
||||
provider "docker" {}
|
||||
|
||||
# The Claude Code module does the automatic task reporting
|
||||
# Other agent modules: https://registry.coder.com/modules?search=agent
|
||||
# Or use a custom agent:
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/projects"
|
||||
install_claude_code = true
|
||||
claude_code_version = "latest"
|
||||
order = 999
|
||||
|
||||
experiment_post_install_script = data.coder_parameter.setup_script.value
|
||||
|
||||
# This enables Coder Tasks
|
||||
experiment_report_tasks = true
|
||||
}
|
||||
|
||||
# You can also use a model provider, like AWS Bedrock or Vertex by replacing
|
||||
# this with the special env vars from the Claude Code docs.
|
||||
# see: https://docs.anthropic.com/en/docs/claude-code/third-party-integrations
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Generate one at: https://console.anthropic.com/settings/keys"
|
||||
sensitive = true
|
||||
}
|
||||
resource "coder_env" "anthropic_api_key" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_CLAUDE_API_KEY"
|
||||
value = var.anthropic_api_key
|
||||
}
|
||||
|
||||
# We are using presets to set the prompts, image, and set up instructions
|
||||
# See https://coder.com/docs/admin/templates/extending-templates/parameters#workspace-presets
|
||||
data "coder_workspace_preset" "default" {
|
||||
name = "Real World App: Angular + Django"
|
||||
default = true
|
||||
parameters = {
|
||||
"system_prompt" = <<-EOT
|
||||
-- Framing --
|
||||
You are a helpful assistant that can help with code. You are running inside a Coder Workspace and provide status updates to the user via Coder MCP. Stay on track, feel free to debug, but when the original plan fails, do not choose a different route/architecture without checking the user first.
|
||||
|
||||
-- Tool Selection --
|
||||
- playwright: previewing your changes after you made them
|
||||
to confirm it worked as expected
|
||||
- desktop-commander - use only for commands that keep running
|
||||
(servers, dev watchers, GUI apps).
|
||||
- Built-in tools - use for everything else:
|
||||
(file operations, git commands, builds & installs, one-off shell commands)
|
||||
|
||||
Remember this decision rule:
|
||||
- Stays running? → desktop-commander
|
||||
- Finishes immediately? → built-in tools
|
||||
|
||||
-- Context --
|
||||
There is an existing app and tmux dev server running on port 8000. Be sure to read it's CLAUDE.md (./realworld-django-rest-framework-angular/CLAUDE.md) to learn more about it.
|
||||
|
||||
Since this app is for demo purposes and the user is previewing the homepage and subsequent pages, aim to make the first visual change/prototype very quickly so the user can preview it, then focus on backend or logic which can be a more involved, long-running architecture plan.
|
||||
|
||||
EOT
|
||||
|
||||
"setup_script" = <<-EOT
|
||||
# Set up projects dir
|
||||
mkdir -p /home/coder/projects
|
||||
cd $HOME/projects
|
||||
|
||||
# Packages: Install additional packages
|
||||
sudo apt-get update && sudo apt-get install -y tmux
|
||||
if ! command -v google-chrome >/dev/null 2>&1; then
|
||||
yes | npx playwright install chrome
|
||||
fi
|
||||
|
||||
# MCP: Install and configure MCP Servers
|
||||
npm install -g @wonderwhy-er/desktop-commander
|
||||
claude mcp add playwright npx -- @playwright/mcp@latest --headless --isolated --no-sandbox
|
||||
claude mcp add desktop-commander desktop-commander
|
||||
|
||||
# Repo: Clone and pull changes from the git repository
|
||||
if [ ! -d "realworld-django-rest-framework-angular" ]; then
|
||||
git clone https://github.com/coder-contrib/realworld-django-rest-framework-angular.git
|
||||
else
|
||||
cd realworld-django-rest-framework-angular
|
||||
git fetch
|
||||
# Check for uncommitted changes
|
||||
if git diff-index --quiet HEAD -- && \
|
||||
[ -z "$(git status --porcelain --untracked-files=no)" ] && \
|
||||
[ -z "$(git log --branches --not --remotes)" ]; then
|
||||
echo "Repo is clean. Pulling latest changes..."
|
||||
git pull
|
||||
else
|
||||
echo "Repo has uncommitted or unpushed changes. Skipping pull."
|
||||
fi
|
||||
|
||||
cd ..
|
||||
fi
|
||||
|
||||
# Initialize: Start the development server
|
||||
cd realworld-django-rest-framework-angular && ./start-dev.sh
|
||||
EOT
|
||||
"preview_port" = "4200"
|
||||
"container_image" = "codercom/example-universal:ubuntu"
|
||||
"jetbrains_ide" = "PY"
|
||||
}
|
||||
|
||||
# Pre-builds is a Coder Premium
|
||||
# feature to speed up workspace creation
|
||||
#
|
||||
# see https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces
|
||||
# prebuilds {
|
||||
# instances = 1
|
||||
# expiration_policy {
|
||||
# ttl = 86400 # Time (in seconds) after which unclaimed prebuilds are expired (1 day)
|
||||
# }
|
||||
# }
|
||||
}
|
||||
|
||||
# Advanced parameters (these are all set via preset)
|
||||
data "coder_parameter" "system_prompt" {
|
||||
name = "system_prompt"
|
||||
display_name = "System Prompt"
|
||||
type = "string"
|
||||
form_type = "textarea"
|
||||
description = "System prompt for the agent with generalized instructions"
|
||||
mutable = false
|
||||
}
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Write a prompt for Claude Code"
|
||||
mutable = true
|
||||
}
|
||||
data "coder_parameter" "setup_script" {
|
||||
name = "setup_script"
|
||||
display_name = "Setup Script"
|
||||
type = "string"
|
||||
form_type = "textarea"
|
||||
description = "Script to run before running the agent"
|
||||
mutable = false
|
||||
}
|
||||
data "coder_parameter" "container_image" {
|
||||
name = "container_image"
|
||||
display_name = "Container Image"
|
||||
type = "string"
|
||||
default = "codercom/example-universal:ubuntu"
|
||||
mutable = false
|
||||
}
|
||||
data "coder_parameter" "preview_port" {
|
||||
name = "preview_port"
|
||||
display_name = "Preview Port"
|
||||
description = "The port the web app is running to preview in Tasks"
|
||||
type = "number"
|
||||
default = "3000"
|
||||
mutable = false
|
||||
}
|
||||
|
||||
# Other variables for Claude Code
|
||||
resource "coder_env" "claude_task_prompt" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_CLAUDE_TASK_PROMPT"
|
||||
value = data.coder_parameter.ai_prompt.value
|
||||
}
|
||||
resource "coder_env" "app_status_slug" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_APP_STATUS_SLUG"
|
||||
value = "claude-code"
|
||||
}
|
||||
resource "coder_env" "claude_system_prompt" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
|
||||
value = data.coder_parameter.system_prompt.value
|
||||
}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
arch = data.coder_provisioner.me.arch
|
||||
os = "linux"
|
||||
startup_script = <<-EOT
|
||||
set -e
|
||||
# Prepare user home with default files on first start.
|
||||
if [ ! -f ~/.init_done ]; then
|
||||
cp -rT /etc/skel ~
|
||||
touch ~/.init_done
|
||||
fi
|
||||
EOT
|
||||
|
||||
# These environment variables allow you to make Git commits right away after creating a
|
||||
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
|
||||
# You can remove this block if you'd prefer to configure Git manually or using
|
||||
# dotfiles. (see docs/dotfiles.md)
|
||||
env = {
|
||||
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
|
||||
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
|
||||
}
|
||||
|
||||
# The following metadata blocks are optional. They are used to display
|
||||
# information about your workspace in the dashboard. You can remove them
|
||||
# if you don't want to display any information.
|
||||
# For basic resources, you can use the `coder stat` command.
|
||||
# If you need more control, you can write your own script.
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Home Disk"
|
||||
key = "3_home_disk"
|
||||
script = "coder stat disk --path $${HOME}"
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage (Host)"
|
||||
key = "4_cpu_usage_host"
|
||||
script = "coder stat cpu --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory Usage (Host)"
|
||||
key = "5_mem_usage_host"
|
||||
script = "coder stat mem --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Load Average (Host)"
|
||||
key = "6_load_host"
|
||||
# get load avg scaled by number of cores
|
||||
script = <<EOT
|
||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||
EOT
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Swap Usage (Host)"
|
||||
key = "7_swap_host"
|
||||
script = <<EOT
|
||||
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||
EOT
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
# See https://registry.coder.com/modules/coder/code-server
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
folder = "/home/coder/projects"
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
|
||||
settings = {
|
||||
"workbench.colorTheme" : "Default Dark Modern"
|
||||
}
|
||||
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
version = "~> 1.0"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
order = 1
|
||||
}
|
||||
|
||||
module "vscode" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-desktop/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
|
||||
# JetBrains IDEs to make available for the user to select
|
||||
jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
|
||||
default = "IU"
|
||||
|
||||
# Default folder to open when starting a JetBrains IDE
|
||||
folder = "/home/coder/projects"
|
||||
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
version = "~> 1.0"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
order = 2
|
||||
}
|
||||
|
||||
resource "docker_volume" "home_volume" {
|
||||
name = "coder-${data.coder_workspace.me.id}-home"
|
||||
# Protect the volume from being deleted due to changes in attributes.
|
||||
lifecycle {
|
||||
ignore_changes = all
|
||||
}
|
||||
# Add labels in Docker to keep track of orphan resources.
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
labels {
|
||||
label = "coder.owner_id"
|
||||
value = data.coder_workspace_owner.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_id"
|
||||
value = data.coder_workspace.me.id
|
||||
}
|
||||
# This field becomes outdated if the workspace is renamed but can
|
||||
# be useful for debugging or cleaning out dangling volumes.
|
||||
labels {
|
||||
label = "coder.workspace_name_at_creation"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_app" "preview" {
|
||||
agent_id = coder_agent.main.id
|
||||
slug = "preview"
|
||||
display_name = "Preview your app"
|
||||
icon = "${data.coder_workspace.me.access_url}/emojis/1f50e.png"
|
||||
url = "http://localhost:${data.coder_parameter.preview_port.value}"
|
||||
share = "authenticated"
|
||||
subdomain = true
|
||||
open_in = "tab"
|
||||
order = 0
|
||||
healthcheck {
|
||||
url = "http://localhost:${data.coder_parameter.preview_port.value}/"
|
||||
interval = 5
|
||||
threshold = 15
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_container" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
image = data.coder_parameter.container_image.value
|
||||
# Uses lower() to avoid Docker restriction on container names.
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||
# Hostname makes the shell more user friendly: coder@my-workspace:~$
|
||||
hostname = data.coder_workspace.me.name
|
||||
user = "coder"
|
||||
# Use the docker gateway if the access URL is 127.0.0.1
|
||||
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
|
||||
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
|
||||
host {
|
||||
host = "host.docker.internal"
|
||||
ip = "host-gateway"
|
||||
}
|
||||
volumes {
|
||||
container_path = "/home/coder"
|
||||
volume_name = docker_volume.home_volume.name
|
||||
read_only = false
|
||||
}
|
||||
|
||||
# Add labels in Docker to keep track of orphan resources.
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
labels {
|
||||
label = "coder.owner_id"
|
||||
value = data.coder_workspace_owner.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_id"
|
||||
value = data.coder_workspace.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_name"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,4 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 612 B |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 308 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 654 KiB After Width: | Height: | Size: 414 KiB |
|
Before Width: | Height: | Size: 526 KiB After Width: | Height: | Size: 428 KiB |
|
Before Width: | Height: | Size: 205 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 85 KiB |
@@ -2,7 +2,7 @@
|
||||
display_name: Coder
|
||||
bio: Coder provisions cloud development environments via Terraform, supporting Linux, macOS, Windows, X86, ARM, Kubernetes and more.
|
||||
github: coder
|
||||
avatar: ./.images/avatar.png
|
||||
avatar: ./.images/avatar.svg
|
||||
linkedin: https://www.linkedin.com/company/coderhq
|
||||
website: https://www.coder.com
|
||||
status: official
|
||||
|
||||
@@ -114,25 +114,6 @@ module "amazon-q" {
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Name | Required | Default | Description |
|
||||
| -------------------------------- | -------- | ------------------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| `agent_id` | Yes | — | The ID of a Coder agent. |
|
||||
| `experiment_auth_tarball` | Yes | — | Base64-encoded, zstd-compressed tarball of a pre-authenticated Amazon Q config directory. |
|
||||
| `install_amazon_q` | No | `true` | Whether to install Amazon Q. |
|
||||
| `amazon_q_version` | No | `latest` | Version to install. |
|
||||
| `experiment_use_screen` | No | `false` | Use GNU screen for background operation. |
|
||||
| `experiment_use_tmux` | No | `false` | Use tmux for background operation. |
|
||||
| `experiment_report_tasks` | No | `false` | Enable task reporting to Coder. |
|
||||
| `experiment_pre_install_script` | No | `null` | Custom script to run before install. |
|
||||
| `experiment_post_install_script` | No | `null` | Custom script to run after install. |
|
||||
| `icon` | No | `/icon/amazon-q.svg` | The icon to use for the app. |
|
||||
| `folder` | No | `/home/coder` | The folder to run Amazon Q in. |
|
||||
| `order` | No | `null` | The order determines the position of app in the UI presentation. |
|
||||
| `system_prompt` | No | See [main.tf](./main.tf) | The system prompt to use for Amazon Q. This should instruct the agent how to do task reporting. |
|
||||
| `ai_prompt` | No | See [main.tf](./main.tf) | The initial task prompt to send to Amazon Q. |
|
||||
|
||||
## Notes
|
||||
|
||||
- Only one of `experiment_use_screen` or `experiment_use_tmux` can be true at a time.
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Run Claude Code in your workspace
|
||||
icon: ../../../../.icons/claude.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [agent, claude-code, ai]
|
||||
tags: [agent, claude-code, ai, tasks]
|
||||
---
|
||||
|
||||
# Claude Code
|
||||
@@ -14,7 +14,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "1.3.0"
|
||||
version = "2.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -30,7 +30,6 @@ module "claude-code" {
|
||||
## Prerequisites
|
||||
|
||||
- Node.js and npm must be installed in your workspace to install Claude Code
|
||||
- Either `screen` or `tmux` must be installed in your workspace to run Claude Code in the background
|
||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
|
||||
|
||||
The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces.
|
||||
@@ -48,8 +47,6 @@ The `codercom/oss-dogfood:latest` container image can be used for testing on con
|
||||
> Join our [Discord channel](https://discord.gg/coder) or
|
||||
> [contact us](https://coder.com/contact) to get help or share feedback.
|
||||
|
||||
Your workspace must have either `screen` or `tmux` installed to use this.
|
||||
|
||||
```tf
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
@@ -88,26 +85,25 @@ resource "coder_agent" "main" {
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "1.1.0"
|
||||
version = "2.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
claude_code_version = "0.2.57"
|
||||
claude_code_version = "1.0.40"
|
||||
|
||||
# Enable experimental features
|
||||
experiment_use_screen = true # Or use experiment_use_tmux = true to use tmux instead
|
||||
experiment_report_tasks = true
|
||||
}
|
||||
```
|
||||
|
||||
## Run standalone
|
||||
|
||||
Run Claude Code as a standalone app in your workspace. This will install Claude Code and run it directly without using screen or any task reporting to the Coder UI.
|
||||
Run Claude Code as a standalone app in your workspace. This will install Claude Code and run it without any task reporting to the Coder UI.
|
||||
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "1.3.0"
|
||||
version = "2.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -117,3 +113,7 @@ module "claude-code" {
|
||||
icon = "https://registry.npmmirror.com/@lobehub/icons-static-png/1.24.0/files/dark/claude-color.png"
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
The module will create log files in the workspace's `~/.claude-module` directory. If you run into any issues, look at them for more information.
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
import {
|
||||
test,
|
||||
afterEach,
|
||||
expect,
|
||||
describe,
|
||||
setDefaultTimeout,
|
||||
beforeAll,
|
||||
} from "bun:test";
|
||||
import path from "path";
|
||||
import {
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
removeContainer,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
writeCoder,
|
||||
writeFileContainer,
|
||||
} from "~test";
|
||||
|
||||
let cleanupFunctions: (() => Promise<void>)[] = [];
|
||||
|
||||
const registerCleanup = (cleanup: () => Promise<void>) => {
|
||||
cleanupFunctions.push(cleanup);
|
||||
};
|
||||
|
||||
// Cleanup logic depends on the fact that bun's built-in test runner
|
||||
// runs tests sequentially.
|
||||
// https://bun.sh/docs/test/discovery#execution-order
|
||||
// Weird things would happen if tried to run tests in parallel.
|
||||
// One test could clean up resources that another test was still using.
|
||||
afterEach(async () => {
|
||||
// reverse the cleanup functions so that they are run in the correct order
|
||||
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
|
||||
cleanupFunctions = [];
|
||||
for (const cleanup of cleanupFnsCopy) {
|
||||
try {
|
||||
await cleanup();
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const setupContainer = async ({
|
||||
image,
|
||||
vars,
|
||||
}: {
|
||||
image?: string;
|
||||
vars?: Record<string, string>;
|
||||
} = {}) => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
...vars,
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
|
||||
registerCleanup(() => removeContainer(id));
|
||||
return { id, coderScript };
|
||||
};
|
||||
|
||||
const loadTestFile = async (...relativePath: string[]) => {
|
||||
return await Bun.file(
|
||||
path.join(import.meta.dir, "testdata", ...relativePath),
|
||||
).text();
|
||||
};
|
||||
|
||||
const writeExecutable = async ({
|
||||
containerId,
|
||||
filePath,
|
||||
content,
|
||||
}: {
|
||||
containerId: string;
|
||||
filePath: string;
|
||||
content: string;
|
||||
}) => {
|
||||
await writeFileContainer(containerId, filePath, content, {
|
||||
user: "root",
|
||||
});
|
||||
await execContainer(
|
||||
containerId,
|
||||
["bash", "-c", `chmod 755 ${filePath}`],
|
||||
["--user", "root"],
|
||||
);
|
||||
};
|
||||
|
||||
const writeAgentAPIMockControl = async ({
|
||||
containerId,
|
||||
content,
|
||||
}: {
|
||||
containerId: string;
|
||||
content: string;
|
||||
}) => {
|
||||
await writeFileContainer(containerId, "/tmp/agentapi-mock.control", content, {
|
||||
user: "coder",
|
||||
});
|
||||
};
|
||||
|
||||
interface SetupProps {
|
||||
skipAgentAPIMock?: boolean;
|
||||
skipClaudeMock?: boolean;
|
||||
}
|
||||
|
||||
const projectDir = "/home/coder/project";
|
||||
|
||||
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const { id, coderScript } = await setupContainer({
|
||||
vars: {
|
||||
experiment_report_tasks: "true",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
install_claude_code: "false",
|
||||
agentapi_version: "preview",
|
||||
folder: projectDir,
|
||||
},
|
||||
});
|
||||
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
|
||||
// the module script assumes that there is a coder executable in the PATH
|
||||
await writeCoder(id, await loadTestFile("coder-mock.js"));
|
||||
if (!props?.skipAgentAPIMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/agentapi",
|
||||
content: await loadTestFile("agentapi-mock.js"),
|
||||
});
|
||||
}
|
||||
if (!props?.skipClaudeMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/claude",
|
||||
content: await loadTestFile("claude-mock.js"),
|
||||
});
|
||||
}
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/home/coder/script.sh",
|
||||
content: coderScript.script,
|
||||
});
|
||||
return { id };
|
||||
};
|
||||
|
||||
const expectAgentAPIStarted = async (id: string) => {
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`curl -fs -o /dev/null "http://localhost:3284/status"`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log("agentapi not started");
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
expect(resp.exitCode).toBe(0);
|
||||
};
|
||||
|
||||
const execModuleScript = async (id: string) => {
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
|
||||
// increase the default timeout to 60 seconds
|
||||
setDefaultTimeout(60 * 1000);
|
||||
|
||||
// we don't run these tests in CI because they take too long and make network
|
||||
// calls. they are dedicated for local development.
|
||||
describe("claude-code", async () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
// test that the script runs successfully if claude starts without any errors
|
||||
test("happy-path", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"sudo /home/coder/script.sh",
|
||||
]);
|
||||
expect(resp.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
// test that the script removes lastSessionId from the .claude.json file
|
||||
test("last-session-id-removed", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
await writeFileContainer(
|
||||
id,
|
||||
"/home/coder/.claude.json",
|
||||
JSON.stringify({
|
||||
projects: {
|
||||
[projectDir]: {
|
||||
lastSessionId: "123",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const catResp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude.json",
|
||||
]);
|
||||
expect(catResp.exitCode).toBe(0);
|
||||
expect(catResp.stdout).toContain("lastSessionId");
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
|
||||
const catResp2 = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude.json",
|
||||
]);
|
||||
expect(catResp2.exitCode).toBe(0);
|
||||
expect(catResp2.stdout).not.toContain("lastSessionId");
|
||||
});
|
||||
|
||||
// test that the script handles a .claude.json file that doesn't contain
|
||||
// a lastSessionId field
|
||||
test("last-session-id-not-found", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
await writeFileContainer(
|
||||
id,
|
||||
"/home/coder/.claude.json",
|
||||
JSON.stringify({
|
||||
projects: {
|
||||
"/home/coder": {},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
|
||||
const catResp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(catResp.exitCode).toBe(0);
|
||||
expect(catResp.stdout).toContain(
|
||||
"No lastSessionId found in .claude.json - nothing to do",
|
||||
);
|
||||
});
|
||||
|
||||
// test that if claude fails to run with the --continue flag and returns a
|
||||
// no conversation found error, then the module script retries without the flag
|
||||
test("no-conversation-found", async () => {
|
||||
const { id } = await setup();
|
||||
await writeAgentAPIMockControl({
|
||||
containerId: id,
|
||||
content: "no-conversation-found",
|
||||
});
|
||||
// check that mocking works
|
||||
const respAgentAPI = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"agentapi --continue",
|
||||
]);
|
||||
expect(respAgentAPI.exitCode).toBe(1);
|
||||
expect(respAgentAPI.stderr).toContain("No conversation found to continue");
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("install-agentapi", async () => {
|
||||
const { id } = await setup({ skipAgentAPIMock: true });
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
const respAgentAPI = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"agentapi --version",
|
||||
]);
|
||||
expect(respAgentAPI.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
// the coder binary should be executed with specific env vars
|
||||
// that are set by the module script
|
||||
test("coder-env-vars", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
const respCoderMock = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/coder-mock-output.json",
|
||||
]);
|
||||
if (respCoderMock.exitCode !== 0) {
|
||||
console.log(respCoderMock.stdout);
|
||||
console.log(respCoderMock.stderr);
|
||||
}
|
||||
expect(respCoderMock.exitCode).toBe(0);
|
||||
expect(JSON.parse(respCoderMock.stdout)).toEqual({
|
||||
statusSlug: "ccw",
|
||||
agentApiUrl: "http://localhost:3284",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,16 +54,22 @@ variable "claude_code_version" {
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "experiment_use_screen" {
|
||||
variable "experiment_cli_app" {
|
||||
type = bool
|
||||
description = "Whether to use screen for running Claude Code in the background."
|
||||
description = "Whether to create the CLI workspace app."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_use_tmux" {
|
||||
type = bool
|
||||
description = "Whether to use tmux instead of screen for running Claude Code in the background."
|
||||
default = false
|
||||
variable "experiment_cli_app_order" {
|
||||
type = number
|
||||
description = "The order of the CLI workspace app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_cli_app_group" {
|
||||
type = string
|
||||
description = "The group of the CLI workspace app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_report_tasks" {
|
||||
@@ -84,9 +90,29 @@ variable "experiment_post_install_script" {
|
||||
default = null
|
||||
}
|
||||
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.2.2"
|
||||
}
|
||||
|
||||
locals {
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
# we have to trim the slash because otherwise coder exp mcp will
|
||||
# set up an invalid claude config
|
||||
workdir = trimsuffix(var.folder, "/")
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
agentapi_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-start.sh"))
|
||||
agentapi_wait_for_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh"))
|
||||
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.js"))
|
||||
claude_code_app_slug = "ccw"
|
||||
}
|
||||
|
||||
# Install and Initialize Claude Code
|
||||
@@ -97,23 +123,18 @@ resource "coder_script" "claude_code" {
|
||||
script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Check if the specified folder exists
|
||||
if [ ! -d "${var.folder}" ]; then
|
||||
echo "Warning: The specified folder '${var.folder}' does not exist."
|
||||
if [ ! -d "${local.workdir}" ]; then
|
||||
echo "Warning: The specified folder '${local.workdir}' does not exist."
|
||||
echo "Creating the folder..."
|
||||
# The folder must exist before tmux is started or else claude will start
|
||||
# in the home directory.
|
||||
mkdir -p "${var.folder}"
|
||||
mkdir -p "${local.workdir}"
|
||||
echo "Folder created successfully."
|
||||
fi
|
||||
|
||||
# Run pre-install script if provided
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
|
||||
@@ -121,17 +142,89 @@ resource "coder_script" "claude_code" {
|
||||
/tmp/pre_install.sh
|
||||
fi
|
||||
|
||||
# Install Claude Code if enabled
|
||||
if [ "${var.install_claude_code}" = "true" ]; then
|
||||
if ! command_exists npm; then
|
||||
echo "Error: npm is not installed. Please install Node.js and npm first."
|
||||
exit 1
|
||||
echo "npm not found, checking for Node.js installation..."
|
||||
if ! command_exists node; then
|
||||
echo "Node.js not found, installing Node.js via NVM..."
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
if [ ! -d "$NVM_DIR" ]; then
|
||||
mkdir -p "$NVM_DIR"
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
else
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
fi
|
||||
|
||||
nvm install --lts
|
||||
nvm use --lts
|
||||
nvm alias default node
|
||||
|
||||
echo "Node.js installed: $(node --version)"
|
||||
echo "npm installed: $(npm --version)"
|
||||
else
|
||||
echo "Node.js is installed but npm is not available. Please install npm manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "Installing Claude Code..."
|
||||
npm install -g @anthropic-ai/claude-code@${var.claude_code_version}
|
||||
fi
|
||||
|
||||
# Run post-install script if provided
|
||||
if ! command_exists node; then
|
||||
echo "Error: Node.js is not installed. Please install Node.js manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install AgentAPI if enabled
|
||||
if [ "${var.install_agentapi}" = "true" ]; then
|
||||
echo "Installing AgentAPI..."
|
||||
arch=$(uname -m)
|
||||
if [ "$arch" = "x86_64" ]; then
|
||||
binary_name="agentapi-linux-amd64"
|
||||
elif [ "$arch" = "aarch64" ]; then
|
||||
binary_name="agentapi-linux-arm64"
|
||||
else
|
||||
echo "Error: Unsupported architecture: $arch"
|
||||
exit 1
|
||||
fi
|
||||
curl \
|
||||
--retry 5 \
|
||||
--retry-delay 5 \
|
||||
--fail \
|
||||
--retry-all-errors \
|
||||
-L \
|
||||
-C - \
|
||||
-o agentapi \
|
||||
"https://github.com/coder/agentapi/releases/download/${var.agentapi_version}/$binary_name"
|
||||
chmod +x agentapi
|
||||
sudo mv agentapi /usr/local/bin/agentapi
|
||||
fi
|
||||
if ! command_exists agentapi; then
|
||||
echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# this must be kept in sync with the agentapi-start.sh script
|
||||
module_path="$HOME/.claude-module"
|
||||
mkdir -p "$module_path/scripts"
|
||||
|
||||
# save the prompt for the agentapi start command
|
||||
echo -n "$CODER_MCP_CLAUDE_TASK_PROMPT" > "$module_path/prompt.txt"
|
||||
|
||||
echo -n "${local.agentapi_start_script_b64}" | base64 -d > "$module_path/scripts/agentapi-start.sh"
|
||||
echo -n "${local.agentapi_wait_for_start_script_b64}" | base64 -d > "$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "$module_path/scripts/remove-last-session-id.js"
|
||||
chmod +x "$module_path/scripts/agentapi-start.sh"
|
||||
chmod +x "$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Claude Code to report tasks via Coder MCP..."
|
||||
export CODER_MCP_APP_STATUS_SLUG="${local.claude_code_app_slug}"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
coder exp mcp configure claude-code "${local.workdir}"
|
||||
fi
|
||||
|
||||
if [ -n "${local.encoded_post_install_script}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
|
||||
@@ -139,99 +232,43 @@ resource "coder_script" "claude_code" {
|
||||
/tmp/post_install.sh
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Claude Code to report tasks via Coder MCP..."
|
||||
coder exp mcp configure claude-code ${var.folder}
|
||||
fi
|
||||
|
||||
# Handle terminal multiplexer selection (tmux or screen)
|
||||
if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
|
||||
echo "Please set only one of them to true."
|
||||
if ! command_exists claude; then
|
||||
echo "Error: Claude Code is not installed. Please enable install_claude_code or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run with tmux if enabled
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
echo "Running Claude Code in the background with tmux..."
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
# Check if tmux is installed
|
||||
if ! command_exists tmux; then
|
||||
echo "Error: tmux is not installed. Please install tmux manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.claude-code.log"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
# Create a new tmux session in detached mode
|
||||
tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions"
|
||||
|
||||
# Send the prompt to the tmux session if needed
|
||||
if [ -n "$CODER_MCP_CLAUDE_TASK_PROMPT" ]; then
|
||||
tmux send-keys -t claude-code "$CODER_MCP_CLAUDE_TASK_PROMPT"
|
||||
sleep 5
|
||||
tmux send-keys -t claude-code Enter
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run with screen if enabled
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Running Claude Code in the background..."
|
||||
|
||||
# Check if screen is installed
|
||||
if ! command_exists screen; then
|
||||
echo "Error: screen is not installed. Please install screen manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.claude-code.log"
|
||||
|
||||
# Ensure the screenrc exists
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.claude-code.log"
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log"
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log"
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
screen -U -dmS claude-code bash -c '
|
||||
cd ${var.folder}
|
||||
claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"
|
||||
exec bash
|
||||
'
|
||||
# Extremely hacky way to send the prompt to the screen session
|
||||
# This will be fixed in the future, but `claude` was not sending MCP
|
||||
# tasks when an initial prompt is provided.
|
||||
screen -S claude-code -X stuff "$CODER_MCP_CLAUDE_TASK_PROMPT"
|
||||
sleep 5
|
||||
screen -S claude-code -X stuff "^M"
|
||||
else
|
||||
# Check if claude is installed before running
|
||||
if ! command_exists claude; then
|
||||
echo "Error: Claude Code is not installed. Please enable install_claude_code or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
cd "${local.workdir}"
|
||||
nohup "$module_path/scripts/agentapi-start.sh" use_prompt &> "$module_path/agentapi-start.log" &
|
||||
"$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
EOT
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "claude_code_web" {
|
||||
# use a short slug to mitigate https://github.com/coder/coder/issues/15178
|
||||
slug = local.claude_code_app_slug
|
||||
display_name = "Claude Code Web"
|
||||
agent_id = var.agent_id
|
||||
url = "http://localhost:3284/"
|
||||
icon = var.icon
|
||||
order = var.order
|
||||
group = var.group
|
||||
subdomain = true
|
||||
healthcheck {
|
||||
url = "http://localhost:3284/status"
|
||||
interval = 3
|
||||
threshold = 20
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_app" "claude_code" {
|
||||
count = var.experiment_cli_app ? 1 : 0
|
||||
|
||||
slug = "claude-code"
|
||||
display_name = "Claude Code"
|
||||
display_name = "Claude Code CLI"
|
||||
agent_id = var.agent_id
|
||||
command = <<-EOT
|
||||
#!/bin/bash
|
||||
@@ -240,28 +277,15 @@ resource "coder_app" "claude_code" {
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t claude-code 2>/dev/null; then
|
||||
echo "Attaching to existing Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
|
||||
tmux attach-session -t claude-code
|
||||
else
|
||||
echo "Starting a new Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
|
||||
tmux new-session -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions | tee -a \"$HOME/.claude-code.log\"; exec bash"
|
||||
fi
|
||||
elif [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
if screen -list | grep -q "claude-code"; then
|
||||
echo "Attaching to existing Claude Code screen session." | tee -a "$HOME/.claude-code.log"
|
||||
screen -xRR claude-code
|
||||
else
|
||||
echo "Starting a new Claude Code screen session." | tee -a "$HOME/.claude-code.log"
|
||||
screen -S claude-code bash -c 'claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"; exec bash'
|
||||
fi
|
||||
else
|
||||
cd ${var.folder}
|
||||
claude
|
||||
fi
|
||||
agentapi attach
|
||||
EOT
|
||||
icon = var.icon
|
||||
order = var.order
|
||||
group = var.group
|
||||
order = var.experiment_cli_app_order
|
||||
group = var.experiment_cli_app_group
|
||||
}
|
||||
|
||||
resource "coder_ai_task" "claude_code" {
|
||||
sidebar_app {
|
||||
id = coder_app.claude_code_web.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
# this must be kept in sync with the main.tf file
|
||||
module_path="$HOME/.claude-module"
|
||||
scripts_dir="$module_path/scripts"
|
||||
log_file_path="$module_path/agentapi.log"
|
||||
|
||||
# if the first argument is not empty, start claude with the prompt
|
||||
if [ -n "$1" ]; then
|
||||
cp "$module_path/prompt.txt" /tmp/claude-code-prompt
|
||||
else
|
||||
rm -f /tmp/claude-code-prompt
|
||||
fi
|
||||
|
||||
# if the log file already exists, archive it
|
||||
if [ -f "$log_file_path" ]; then
|
||||
mv "$log_file_path" "$log_file_path"".$(date +%s)"
|
||||
fi
|
||||
|
||||
# see the remove-last-session-id.js script for details
|
||||
# about why we need it
|
||||
# avoid exiting if the script fails
|
||||
node "$scripts_dir/remove-last-session-id.js" "$(pwd)" || true
|
||||
|
||||
# we'll be manually handling errors from this point on
|
||||
set +o errexit
|
||||
|
||||
function start_agentapi() {
|
||||
local continue_flag="$1"
|
||||
local prompt_subshell='"$(cat /tmp/claude-code-prompt)"'
|
||||
|
||||
# use low width to fit in the tasks UI sidebar. height is adjusted so that width x height ~= 80x1000 characters
|
||||
# visible in the terminal screen by default.
|
||||
agentapi server --term-width 67 --term-height 1190 -- \
|
||||
bash -c "claude $continue_flag --dangerously-skip-permissions $prompt_subshell" \
|
||||
> "$log_file_path" 2>&1
|
||||
}
|
||||
|
||||
echo "Starting AgentAPI..."
|
||||
|
||||
# attempt to start claude with the --continue flag
|
||||
start_agentapi --continue
|
||||
exit_code=$?
|
||||
|
||||
echo "First AgentAPI exit code: $exit_code"
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# if there was no conversation to continue, claude exited with an error.
|
||||
# start claude without the --continue flag.
|
||||
if grep -q "No conversation found to continue" "$log_file_path"; then
|
||||
echo "AgentAPI with --continue flag failed, starting claude without it."
|
||||
start_agentapi
|
||||
exit_code=$?
|
||||
fi
|
||||
|
||||
echo "Second AgentAPI exit code: $exit_code"
|
||||
|
||||
exit $exit_code
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
# This script waits for the agentapi server to start on port 3284.
|
||||
# It considers the server started after 3 consecutive successful responses.
|
||||
|
||||
agentapi_started=false
|
||||
|
||||
echo "Waiting for agentapi server to start on port 3284..."
|
||||
for i in $(seq 1 150); do
|
||||
for j in $(seq 1 3); do
|
||||
sleep 0.1
|
||||
if curl -fs -o /dev/null "http://localhost:3284/status"; then
|
||||
echo "agentapi response received ($j/3)"
|
||||
else
|
||||
echo "agentapi server not responding ($i/15)"
|
||||
continue 2
|
||||
fi
|
||||
done
|
||||
agentapi_started=true
|
||||
break
|
||||
done
|
||||
|
||||
if [ "$agentapi_started" != "true" ]; then
|
||||
echo "Error: agentapi server did not start on port 3284 after 15 seconds."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "agentapi server started on port 3284."
|
||||
@@ -0,0 +1,40 @@
|
||||
// If lastSessionId is present in .claude.json, claude --continue will start a
|
||||
// conversation starting from that session. The problem is that lastSessionId
|
||||
// doesn't always point to the last session. The field is updated by claude only
|
||||
// at the point of normal CLI exit. If Claude exits with an error, or if the user
|
||||
// restarts the Coder workspace, lastSessionId will be stale, and claude --continue
|
||||
// will start from an old session.
|
||||
//
|
||||
// If lastSessionId is missing, claude seems to accurately figure out where to
|
||||
// start using the conversation history - even if the CLI previously exited with
|
||||
// an error.
|
||||
//
|
||||
// This script removes the lastSessionId field from .claude.json.
|
||||
const path = require("path")
|
||||
const fs = require("fs")
|
||||
|
||||
const workingDirArg = process.argv[2]
|
||||
if (!workingDirArg) {
|
||||
console.log("No working directory provided - it must be the first argument")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const workingDir = path.resolve(workingDirArg)
|
||||
console.log("workingDir", workingDir)
|
||||
|
||||
|
||||
const claudeJsonPath = path.join(process.env.HOME, ".claude.json")
|
||||
console.log(".claude.json path", claudeJsonPath)
|
||||
if (!fs.existsSync(claudeJsonPath)) {
|
||||
console.log("No .claude.json file found")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, "utf8"))
|
||||
if ("projects" in claudeJson && workingDir in claudeJson.projects && "lastSessionId" in claudeJson.projects[workingDir]) {
|
||||
delete claudeJson.projects[workingDir].lastSessionId
|
||||
fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2))
|
||||
console.log("Removed lastSessionId from .claude.json")
|
||||
} else {
|
||||
console.log("No lastSessionId found in .claude.json - nothing to do")
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const args = process.argv.slice(2);
|
||||
const port = 3284;
|
||||
|
||||
const controlFile = "/tmp/agentapi-mock.control";
|
||||
let control = "";
|
||||
if (fs.existsSync(controlFile)) {
|
||||
control = fs.readFileSync(controlFile, "utf8");
|
||||
}
|
||||
|
||||
if (
|
||||
control === "no-conversation-found" &&
|
||||
args.join(" ").includes("--continue")
|
||||
) {
|
||||
// this must match the error message in the agentapi-start.sh script
|
||||
console.error("No conversation found to continue");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`starting server on port ${port}`);
|
||||
|
||||
http
|
||||
.createServer(function (_request, response) {
|
||||
response.writeHead(200);
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
status: "stable",
|
||||
}),
|
||||
);
|
||||
})
|
||||
.listen(port);
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const main = async () => {
|
||||
console.log("mocking claude");
|
||||
// sleep for 30 minutes
|
||||
await new Promise((resolve) => setTimeout(resolve, 30 * 60 * 1000));
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const statusSlugEnvVar = "CODER_MCP_APP_STATUS_SLUG";
|
||||
const agentApiUrlEnvVar = "CODER_MCP_AI_AGENTAPI_URL";
|
||||
|
||||
fs.writeFileSync(
|
||||
"/home/coder/coder-mock-output.json",
|
||||
JSON.stringify({
|
||||
statusSlug: process.env[statusSlugEnvVar] ?? "env var not set",
|
||||
agentApiUrl: process.env[agentApiUrlEnvVar] ?? "env var not set",
|
||||
}),
|
||||
);
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
display_name: Coder Login
|
||||
description: Automatically logs the user into Coder on their workspace
|
||||
icon: ../../../../.icons/coder-white.svg
|
||||
icon: ../../../../.icons/coder.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper]
|
||||
|
||||
@@ -15,7 +15,7 @@ A file browser for your workspace.
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -30,7 +30,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -42,7 +42,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
database_path = ".config/filebrowser.db"
|
||||
}
|
||||
@@ -54,7 +54,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = "main"
|
||||
subdomain = false
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("filebrowser", async () => {
|
||||
);
|
||||
|
||||
testBaseLine(output);
|
||||
});
|
||||
}, 15000);
|
||||
|
||||
it("runs with database_path var", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
@@ -63,7 +63,7 @@ describe("filebrowser", async () => {
|
||||
database_path: ".config/filebrowser.db",
|
||||
});
|
||||
|
||||
const output = await await executeScriptInContainer(
|
||||
const output = await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/curl",
|
||||
"sh",
|
||||
@@ -71,20 +71,21 @@ describe("filebrowser", async () => {
|
||||
);
|
||||
|
||||
testBaseLine(output);
|
||||
});
|
||||
}, 15000);
|
||||
|
||||
it("runs with folder var", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder/project",
|
||||
});
|
||||
const output = await await executeScriptInContainer(
|
||||
const output = await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/curl",
|
||||
"sh",
|
||||
"apk add bash",
|
||||
);
|
||||
});
|
||||
|
||||
}, 15000);
|
||||
|
||||
it("runs with subdomain=false", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
@@ -93,7 +94,7 @@ describe("filebrowser", async () => {
|
||||
subdomain: false,
|
||||
});
|
||||
|
||||
const output = await await executeScriptInContainer(
|
||||
const output = await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/curl",
|
||||
"sh",
|
||||
@@ -101,5 +102,5 @@ describe("filebrowser", async () => {
|
||||
);
|
||||
|
||||
testBaseLine(output);
|
||||
});
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
@@ -97,7 +97,6 @@ resource "coder_script" "filebrowser" {
|
||||
LOG_PATH : var.log_path,
|
||||
PORT : var.port,
|
||||
FOLDER : var.folder,
|
||||
LOG_PATH : var.log_path,
|
||||
DB_PATH : var.database_path,
|
||||
SUBDOMAIN : var.subdomain,
|
||||
SERVER_BASE_PATH : local.server_base_path
|
||||
@@ -128,3 +127,4 @@ locals {
|
||||
url = "http://localhost:${var.port}${local.server_base_path}"
|
||||
healthcheck_url = "http://localhost:${var.port}${local.server_base_path}/health"
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export FB_DATABASE="${DB_PATH}"
|
||||
# Check if filebrowser db exists
|
||||
if [[ ! -f "${DB_PATH}" ]]; then
|
||||
filebrowser config init 2>&1 | tee -a ${LOG_PATH}
|
||||
filebrowser users add admin "" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH}
|
||||
filebrowser users add admin "coderPASSWORD" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH}
|
||||
fi
|
||||
|
||||
filebrowser config set --baseurl=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH}
|
||||
|
||||
@@ -18,7 +18,7 @@ Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prereq
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
|
||||
@@ -36,7 +36,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
@@ -50,7 +50,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["IU", "PY"]
|
||||
@@ -65,7 +65,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["IU", "PY"]
|
||||
@@ -90,7 +90,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
@@ -108,7 +108,7 @@ Due to the highest priority of the `ide_download_link` parameter in the `(jetbra
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("jetbrains-gateway", async () => {
|
||||
folder: "/home/coder",
|
||||
});
|
||||
expect(state.outputs.url.value).toBe(
|
||||
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz",
|
||||
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent_id=foo",
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
|
||||
@@ -348,6 +348,8 @@ resource "coder_app" "gateway" {
|
||||
local.build_number,
|
||||
"&ide_download_link=",
|
||||
local.download_link,
|
||||
"&agent_id=",
|
||||
var.agent_id,
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
display_name: RDP Desktop
|
||||
description: Enable RDP on Windows and add a one-click Coder Desktop button for seamless access
|
||||
icon: ../../../../.icons/rdp.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
supported_os: [windows]
|
||||
tags: [rdp, windows, desktop, local]
|
||||
---
|
||||
|
||||
# Windows RDP Desktop
|
||||
|
||||
This module enables Remote Desktop Protocol (RDP) on Windows workspaces and adds a one-click button to launch RDP sessions directly through [Coder Desktop](https://coder.com/docs/user-guides/desktop). It provides a complete, standalone solution for RDP access, eliminating the need for manual configuration or port forwarding through the Coder CLI.
|
||||
|
||||
> [!NOTE]
|
||||
> [Coder Desktop](https://coder.com/docs/user-guides/desktop) is required on client devices to use the Local Windows RDP access feature.
|
||||
|
||||
```tf
|
||||
module "rdp_desktop" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/local-windows-rdp/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = coder_agent.main.name
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Standalone Solution**: Automatically configures RDP on Windows workspaces
|
||||
- ✅ **One-click Access**: Launch RDP sessions directly through Coder Desktop
|
||||
- ✅ **No Port Forwarding**: Uses Coder Desktop URI handling
|
||||
- ✅ **Auto-configuration**: Sets up Windows firewall, services, and authentication
|
||||
- ✅ **Secure**: Configurable credentials with sensitive variable handling
|
||||
- ✅ **Customizable**: Display name, credentials, and UI ordering options
|
||||
|
||||
## What This Module Does
|
||||
|
||||
1. **Enables RDP** on the Windows workspace
|
||||
2. **Sets the administrator password** for RDP authentication
|
||||
3. **Configures Windows Firewall** to allow RDP connections
|
||||
4. **Starts RDP services** automatically
|
||||
5. **Creates a Coder Desktop button** for one-click access
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Uses default credentials (Username: `Administrator`, Password: `coderRDP!`):
|
||||
|
||||
```tf
|
||||
module "rdp_desktop" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/local-windows-rdp/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = coder_agent.main.name
|
||||
}
|
||||
```
|
||||
|
||||
### Custom display name
|
||||
|
||||
Specify a custom display name for the `coder_app` button:
|
||||
|
||||
```tf
|
||||
module "rdp_desktop" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/local-windows-rdp/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.windows.id
|
||||
agent_name = "windows"
|
||||
display_name = "Windows Desktop"
|
||||
order = 1
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,120 @@
|
||||
# PowerShell script to configure RDP for Coder Desktop access
|
||||
# This script enables RDP, sets the admin password, and configures necessary settings
|
||||
|
||||
Write-Output "[Coder RDP Setup] Starting RDP configuration..."
|
||||
|
||||
# Function to set the administrator password
|
||||
function Set-AdminPassword {
|
||||
param (
|
||||
[string]$adminUsername,
|
||||
[string]$adminPassword
|
||||
)
|
||||
|
||||
Write-Output "[Coder RDP Setup] Setting password for user: $adminUsername"
|
||||
|
||||
try {
|
||||
# Convert password to secure string
|
||||
$securePassword = ConvertTo-SecureString -AsPlainText $adminPassword -Force
|
||||
|
||||
# Set the password for the user
|
||||
Get-LocalUser -Name $adminUsername | Set-LocalUser -Password $securePassword
|
||||
|
||||
# Enable the user account (in case it's disabled)
|
||||
Get-LocalUser -Name $adminUsername | Enable-LocalUser
|
||||
|
||||
Write-Output "[Coder RDP Setup] Successfully set password for $adminUsername"
|
||||
} catch {
|
||||
Write-Error "[Coder RDP Setup] Failed to set password: $_"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Function to enable and configure RDP
|
||||
function Enable-RDP {
|
||||
Write-Output "[Coder RDP Setup] Enabling Remote Desktop..."
|
||||
|
||||
try {
|
||||
# Enable RDP
|
||||
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0 -Force
|
||||
|
||||
# Disable Network Level Authentication (NLA) for easier access
|
||||
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "UserAuthentication" -Value 0 -Force
|
||||
|
||||
# Set security layer to RDP Security Layer
|
||||
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "SecurityLayer" -Value 1 -Force
|
||||
|
||||
Write-Output "[Coder RDP Setup] RDP enabled successfully"
|
||||
} catch {
|
||||
Write-Error "[Coder RDP Setup] Failed to enable RDP: $_"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Function to configure Windows Firewall for RDP
|
||||
function Configure-Firewall {
|
||||
Write-Output "[Coder RDP Setup] Configuring Windows Firewall for RDP..."
|
||||
|
||||
try {
|
||||
# Enable RDP firewall rules
|
||||
Enable-NetFirewallRule -DisplayGroup "Remote Desktop" -ErrorAction SilentlyContinue
|
||||
|
||||
# If the above fails, try alternative method
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
netsh advfirewall firewall set rule group="remote desktop" new enable=Yes
|
||||
}
|
||||
|
||||
Write-Output "[Coder RDP Setup] Firewall configured successfully"
|
||||
} catch {
|
||||
Write-Warning "[Coder RDP Setup] Failed to configure firewall rules: $_"
|
||||
# Continue anyway as RDP might still work
|
||||
}
|
||||
}
|
||||
|
||||
# Function to ensure RDP service is running
|
||||
function Start-RDPService {
|
||||
Write-Output "[Coder RDP Setup] Starting Remote Desktop Services..."
|
||||
|
||||
try {
|
||||
# Start the Terminal Services
|
||||
Set-Service -Name "TermService" -StartupType Automatic -ErrorAction SilentlyContinue
|
||||
Start-Service -Name "TermService" -ErrorAction SilentlyContinue
|
||||
|
||||
# Start Remote Desktop Services UserMode Port Redirector
|
||||
Set-Service -Name "UmRdpService" -StartupType Automatic -ErrorAction SilentlyContinue
|
||||
Start-Service -Name "UmRdpService" -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Output "[Coder RDP Setup] RDP services started successfully"
|
||||
} catch {
|
||||
Write-Warning "[Coder RDP Setup] Some RDP services may not have started: $_"
|
||||
# Continue anyway
|
||||
}
|
||||
}
|
||||
|
||||
# Main execution
|
||||
try {
|
||||
# Template variables from Terraform
|
||||
$username = "${username}"
|
||||
$password = "${password}"
|
||||
|
||||
# Validate inputs
|
||||
if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) {
|
||||
Write-Error "[Coder RDP Setup] Username or password is empty"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Execute configuration steps
|
||||
Set-AdminPassword -adminUsername $username -adminPassword $password
|
||||
Enable-RDP
|
||||
Configure-Firewall
|
||||
Start-RDPService
|
||||
|
||||
Write-Output "[Coder RDP Setup] RDP configuration completed successfully!"
|
||||
Write-Output "[Coder RDP Setup] You can now connect using:"
|
||||
Write-Output " Username: $username"
|
||||
Write-Output " Password: [hidden]"
|
||||
Write-Output " Port: 3389 (default)"
|
||||
|
||||
} catch {
|
||||
Write-Error "[Coder RDP Setup] An unexpected error occurred: $_"
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
type TerraformState,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
type TestVariables = Readonly<{
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
display_name?: string;
|
||||
order?: number;
|
||||
}>;
|
||||
|
||||
function findRdpApp(state: TerraformState) {
|
||||
for (const resource of state.resources) {
|
||||
const isRdpAppResource =
|
||||
resource.type === "coder_app" && resource.name === "rdp_desktop";
|
||||
|
||||
if (!isRdpAppResource) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const instance of resource.instances) {
|
||||
if (instance.attributes.slug === "rdp-desktop") {
|
||||
return instance.attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findRdpScript(state: TerraformState) {
|
||||
for (const resource of state.resources) {
|
||||
const isRdpScriptResource =
|
||||
resource.type === "coder_script" && resource.name === "rdp_setup";
|
||||
|
||||
if (!isRdpScriptResource) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const instance of resource.instances) {
|
||||
if (instance.attributes.display_name === "Configure RDP") {
|
||||
return instance.attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
describe("local-windows-rdp", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||
agent_id: "test-agent-id",
|
||||
agent_name: "test-agent",
|
||||
});
|
||||
|
||||
it("should create RDP app with default values", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "test-agent-id",
|
||||
agent_name: "main",
|
||||
});
|
||||
|
||||
const app = findRdpApp(state);
|
||||
|
||||
// Verify the app was created
|
||||
expect(app).not.toBeNull();
|
||||
expect(app?.slug).toBe("rdp-desktop");
|
||||
expect(app?.display_name).toBe("RDP Desktop");
|
||||
expect(app?.icon).toBe("/icon/rdp.svg");
|
||||
expect(app?.external).toBe(true);
|
||||
|
||||
// Verify the URI format
|
||||
expect(app?.url).toStartWith("coder://");
|
||||
expect(app?.url).toContain("/v0/open/ws/");
|
||||
expect(app?.url).toContain("/agent/main/rdp");
|
||||
expect(app?.url).toContain("username=Administrator");
|
||||
expect(app?.url).toContain("password=coderRDP!");
|
||||
});
|
||||
|
||||
it("should create RDP configuration script", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "test-agent-id",
|
||||
agent_name: "main",
|
||||
});
|
||||
|
||||
const script = findRdpScript(state);
|
||||
|
||||
// Verify the script was created
|
||||
expect(script).not.toBeNull();
|
||||
expect(script?.display_name).toBe("Configure RDP");
|
||||
expect(script?.icon).toBe("/icon/rdp.svg");
|
||||
expect(script?.run_on_start).toBe(true);
|
||||
expect(script?.run_on_stop).toBe(false);
|
||||
|
||||
// Verify the script contains PowerShell configuration
|
||||
expect(script?.script).toContain("Set-AdminPassword");
|
||||
expect(script?.script).toContain("Enable-RDP");
|
||||
expect(script?.script).toContain("Configure-Firewall");
|
||||
expect(script?.script).toContain("Start-RDPService");
|
||||
});
|
||||
|
||||
it("should create RDP app with custom values", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "custom-agent-id",
|
||||
agent_name: "windows-agent",
|
||||
username: "CustomUser",
|
||||
password: "CustomPass123!",
|
||||
display_name: "Custom RDP",
|
||||
order: 5,
|
||||
});
|
||||
|
||||
const app = findRdpApp(state);
|
||||
|
||||
// Verify custom values
|
||||
expect(app?.display_name).toBe("Custom RDP");
|
||||
expect(app?.order).toBe(5);
|
||||
|
||||
// Verify custom credentials in URI
|
||||
expect(app?.url).toContain("/agent/windows-agent/rdp");
|
||||
expect(app?.url).toContain("username=CustomUser");
|
||||
expect(app?.url).toContain("password=CustomPass123!");
|
||||
});
|
||||
|
||||
it("should pass custom credentials to PowerShell script", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "test-agent-id",
|
||||
agent_name: "main",
|
||||
username: "TestAdmin",
|
||||
password: "TestPassword123!",
|
||||
});
|
||||
|
||||
const script = findRdpScript(state);
|
||||
|
||||
// Verify custom credentials are in the script
|
||||
expect(script?.script).toContain('$username = "TestAdmin"');
|
||||
expect(script?.script).toContain('$password = "TestPassword123!"');
|
||||
});
|
||||
|
||||
it("should handle sensitive password variable", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "test-agent-id",
|
||||
agent_name: "main",
|
||||
password: "SensitivePass123!",
|
||||
});
|
||||
|
||||
const app = findRdpApp(state);
|
||||
|
||||
// Verify password is included in URI even when sensitive
|
||||
expect(app?.url).toContain("password=SensitivePass123!");
|
||||
});
|
||||
|
||||
it("should use correct default agent name", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "test-agent-id",
|
||||
agent_name: "main",
|
||||
});
|
||||
|
||||
const app = findRdpApp(state);
|
||||
expect(app?.url).toContain("/agent/main/rdp");
|
||||
});
|
||||
|
||||
it("should construct proper Coder URI format", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "test-agent-id",
|
||||
agent_name: "test-agent",
|
||||
username: "TestUser",
|
||||
password: "TestPass",
|
||||
});
|
||||
|
||||
const app = findRdpApp(state);
|
||||
|
||||
// Verify complete URI structure
|
||||
expect(app?.url).toMatch(
|
||||
/^coder:\/\/[^\/]+\/v0\/open\/ws\/[^\/]+\/agent\/test-agent\/rdp\?username=TestUser&password=TestPass$/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "agent_name" {
|
||||
type = string
|
||||
description = "The name of the Coder agent."
|
||||
}
|
||||
|
||||
variable "username" {
|
||||
type = string
|
||||
description = "The username for RDP authentication."
|
||||
default = "Administrator"
|
||||
}
|
||||
|
||||
variable "password" {
|
||||
type = string
|
||||
description = "The password for RDP authentication."
|
||||
default = "coderRDP!"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name for the RDP app button."
|
||||
default = "RDP Desktop"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
locals {
|
||||
# Extract server name from workspace access URL
|
||||
server_name = regex("https?:\\/\\/([^\\/]+)", data.coder_workspace.me.access_url)[0]
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
resource "coder_script" "rdp_setup" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Configure RDP"
|
||||
icon = "/icon/rdp.svg"
|
||||
script = templatefile("${path.module}/configure-rdp.ps1", {
|
||||
username = var.username
|
||||
password = var.password
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "rdp_desktop" {
|
||||
agent_id = var.agent_id
|
||||
slug = "rdp-desktop"
|
||||
display_name = var.display_name
|
||||
url = "coder://${local.server_name}/v0/open/ws/${data.coder_workspace.me.name}/agent/${var.agent_name}/rdp?username=${var.username}&password=${var.password}"
|
||||
icon = "/icon/rdp.svg"
|
||||
external = true
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
@@ -15,7 +15,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
accept_license = true
|
||||
}
|
||||
@@ -31,7 +31,7 @@ module "vscode-web" {
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
install_prefix = "/home/coder/.vscode-web"
|
||||
folder = "/home/coder"
|
||||
@@ -45,7 +45,7 @@ module "vscode-web" {
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
|
||||
accept_license = true
|
||||
@@ -60,7 +60,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula"]
|
||||
settings = {
|
||||
@@ -78,7 +78,7 @@ By default, this module installs the latest. To pin a specific version, retrieve
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
|
||||
accept_license = true
|
||||
|
||||
@@ -121,6 +121,12 @@ variable "use_cached" {
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "disable_trust" {
|
||||
type = bool
|
||||
description = "Disables workspace trust protection for VS Code Web."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "extensions_dir" {
|
||||
type = string
|
||||
description = "Override the directory to store extensions in."
|
||||
@@ -169,6 +175,7 @@ resource "coder_script" "vscode-web" {
|
||||
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
|
||||
OFFLINE : var.offline,
|
||||
USE_CACHED : var.use_cached,
|
||||
DISABLE_TRUST : var.disable_trust,
|
||||
EXTENSIONS_DIR : var.extensions_dir,
|
||||
FOLDER : var.folder,
|
||||
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
|
||||
|
||||
@@ -16,10 +16,16 @@ if [ -n "${SERVER_BASE_PATH}" ]; then
|
||||
SERVER_BASE_PATH_ARG="--server-base-path=${SERVER_BASE_PATH}"
|
||||
fi
|
||||
|
||||
# Set disable workspace trust
|
||||
DISABLE_TRUST_ARG=""
|
||||
if [ "${DISABLE_TRUST}" = true ]; then
|
||||
DISABLE_TRUST_ARG="--disable-workspace-trust"
|
||||
fi
|
||||
|
||||
run_vscode_web() {
|
||||
echo "👷 Running $VSCODE_WEB serve-local $EXTENSION_ARG $SERVER_BASE_PATH_ARG --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
|
||||
echo "👷 Running $VSCODE_WEB serve-local $EXTENSION_ARG $SERVER_BASE_PATH_ARG $DISABLE_TRUST_ARG --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
|
||||
echo "Check logs at ${LOG_PATH}!"
|
||||
"$VSCODE_WEB" serve-local "$EXTENSION_ARG" "$SERVER_BASE_PATH_ARG" --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &
|
||||
"$VSCODE_WEB" serve-local "$EXTENSION_ARG" "$SERVER_BASE_PATH_ARG" "$DISABLE_TRUST_ARG" --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &
|
||||
}
|
||||
|
||||
# Check if the settings file exists...
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
display_name: Windows RDP
|
||||
display_name: RDP Web
|
||||
description: RDP Server and Web Client, powered by Devolutions Gateway
|
||||
icon: ../../../../.icons/desktop.svg
|
||||
maintainer_github: coder
|
||||
@@ -14,11 +14,10 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de
|
||||
```tf
|
||||
# AWS example. See below for examples of using this module with other providers
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.2"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -32,11 +31,10 @@ module "windows_rdp" {
|
||||
|
||||
```tf
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.2"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -44,14 +42,21 @@ module "windows_rdp" {
|
||||
|
||||
```tf
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.google_compute_instance.dev[0].id
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.2"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
### With Custom Devolutions Gateway Version
|
||||
|
||||
- [ ] Test on Microsoft Azure.
|
||||
```tf
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.2"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
devolutions_gateway_version = "2025.2.2" # Specify a specific version
|
||||
}
|
||||
```
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
|
||||
type TestVariables = Readonly<{
|
||||
agent_id: string;
|
||||
resource_id: string;
|
||||
share?: string;
|
||||
admin_username?: string;
|
||||
admin_password?: string;
|
||||
@@ -45,13 +44,11 @@ describe("Web RDP", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
resource_id: "bar",
|
||||
});
|
||||
|
||||
it("Has the PowerShell script install Devolutions Gateway", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
resource_id: "bar",
|
||||
});
|
||||
|
||||
const lines = findWindowsRdpScript(state)
|
||||
@@ -96,7 +93,6 @@ describe("Web RDP", async () => {
|
||||
import.meta.dir,
|
||||
{
|
||||
agent_id: "foo",
|
||||
resource_id: "bar",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -116,7 +112,6 @@ describe("Web RDP", async () => {
|
||||
import.meta.dir,
|
||||
{
|
||||
agent_id: "foo",
|
||||
resource_id: "bar",
|
||||
admin_username: customAdminUsername,
|
||||
admin_password: customAdminPassword,
|
||||
},
|
||||
|
||||
@@ -4,11 +4,23 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 "share" {
|
||||
type = string
|
||||
default = "owner"
|
||||
@@ -23,11 +35,6 @@ variable "agent_id" {
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "resource_id" {
|
||||
type = string
|
||||
description = "The ID of the primary Coder resource (e.g. VM)."
|
||||
}
|
||||
|
||||
variable "admin_username" {
|
||||
type = string
|
||||
default = "Administrator"
|
||||
@@ -39,14 +46,21 @@ variable "admin_password" {
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "devolutions_gateway_version" {
|
||||
type = string
|
||||
default = "2025.2.2"
|
||||
description = "Version of Devolutions Gateway to install. Defaults to the latest available version."
|
||||
}
|
||||
|
||||
resource "coder_script" "windows-rdp" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "windows-rdp"
|
||||
icon = "/icon/desktop.svg"
|
||||
icon = "/icon/rdp.svg"
|
||||
|
||||
script = templatefile("${path.module}/powershell-installation-script.tftpl", {
|
||||
admin_username = var.admin_username
|
||||
admin_password = var.admin_password
|
||||
admin_username = var.admin_username
|
||||
admin_password = var.admin_password
|
||||
devolutions_gateway_version = var.devolutions_gateway_version
|
||||
|
||||
# Wanted to have this be in the powershell template file, but Terraform
|
||||
# doesn't allow recursive calls to the templatefile function. Have to feed
|
||||
@@ -68,6 +82,8 @@ resource "coder_app" "windows-rdp" {
|
||||
url = "http://localhost:7171"
|
||||
icon = "/icon/desktop.svg"
|
||||
subdomain = true
|
||||
order = var.order
|
||||
group = var.group
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:7171"
|
||||
@@ -78,9 +94,9 @@ resource "coder_app" "windows-rdp" {
|
||||
|
||||
resource "coder_app" "rdp-docs" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Local RDP"
|
||||
display_name = "Local RDP Docs"
|
||||
slug = "rdp-docs"
|
||||
icon = "https://raw.githubusercontent.com/matifali/logos/main/windows.svg"
|
||||
url = "https://coder.com/docs/ides/remote-desktops#rdp-desktop"
|
||||
icon = "/icon/windows.svg"
|
||||
url = "https://coder.com/docs/user-guides/workspace-access/remote-desktops#rdp"
|
||||
external = true
|
||||
}
|
||||
|
||||
@@ -16,12 +16,17 @@ function Configure-RDP {
|
||||
New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "SecurityLayer" -Value 1 -PropertyType DWORD -Force
|
||||
# Enable RDP through Windows Firewall
|
||||
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
|
||||
|
||||
# Disable UDP. It doesn't work via `coder port-forward` and is broken due to MTU issues in Coder Connect.
|
||||
# Requires a restart to take effect. c.f. https://github.com/coder/internal/issues/608#issuecomment-2965923672
|
||||
New-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' -Name "SelectTransport" -Value 1 -PropertyType DWORD -Force
|
||||
Restart-Service -Name "TermService" -Force
|
||||
}
|
||||
|
||||
function Install-DevolutionsGateway {
|
||||
# Define the module name and version
|
||||
$moduleName = "DevolutionsGateway"
|
||||
$moduleVersion = "2024.1.5"
|
||||
$moduleVersion = "${devolutions_gateway_version}"
|
||||
|
||||
# Install the module with the specified version for all users
|
||||
# This requires administrator privileges
|
||||
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 94 KiB |
@@ -0,0 +1,65 @@
|
||||
---
|
||||
display_name: Zed
|
||||
description: Add a one-click button to launch Zed
|
||||
icon: ../../../../.icons/zed.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [ide, zed, editor]
|
||||
---
|
||||
|
||||
# Zed
|
||||
|
||||
Add a button to open any workspace with a single click in Zed.
|
||||
|
||||
Zed is a high-performance, multiplayer code editor from the creators of Atom and Tree-sitter.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Zed needs you to either have Coder CLI installed with `coder config-ssh` run or [Coder Desktop](https://coder.com/docs/user-guides/desktop)
|
||||
|
||||
```tf
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Open in a specific directory
|
||||
|
||||
```tf
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
### Custom display name and order
|
||||
|
||||
```tf
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
display_name = "Zed Editor"
|
||||
order = 1
|
||||
}
|
||||
```
|
||||
|
||||
### With custom agent name
|
||||
|
||||
```tf
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = coder_agent.example.name
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("zed", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
expect(state.outputs.zed_url.value).toBe(
|
||||
"zed://ssh/default.coder",
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "zed",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
});
|
||||
expect(state.outputs.zed_url.value).toBe(
|
||||
"zed://ssh/default.coder/foo/bar",
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "zed",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
|
||||
it("expect display_name to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
display_name: "Custom Zed",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "zed",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.display_name).toBe("Custom Zed");
|
||||
});
|
||||
|
||||
it("adds agent_name to hostname", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
agent_name: "myagent",
|
||||
});
|
||||
expect(state.outputs.zed_url.value).toBe(
|
||||
"zed://ssh/myagent.default.default.coder",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent"
|
||||
}
|
||||
|
||||
variable "agent_name" {
|
||||
type = string
|
||||
description = "The name of the agent"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in Zed"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)"
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to"
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the app"
|
||||
default = "zed"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app"
|
||||
default = "Zed"
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
locals {
|
||||
workspace_name = lower(data.coder_workspace.me.name)
|
||||
owner_name = lower(data.coder_workspace_owner.me.name)
|
||||
agent_name = lower(var.agent_name)
|
||||
hostname = var.agent_name != "" ? "${local.agent_name}.${local.workspace_name}.${local.owner_name}.coder" : "${local.workspace_name}.coder"
|
||||
}
|
||||
|
||||
resource "coder_app" "zed" {
|
||||
agent_id = var.agent_id
|
||||
display_name = var.display_name
|
||||
slug = var.slug
|
||||
icon = "/icon/zed.svg"
|
||||
external = true
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = "zed://ssh/${local.hostname}${var.folder}"
|
||||
}
|
||||
|
||||
output "zed_url" {
|
||||
value = coder_app.zed.url
|
||||
description = "Zed URL"
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
display_name: AWS EC2 (Devcontainer)
|
||||
description: Provision AWS EC2 VMs with a devcontainer as Coder workspaces
|
||||
icon: ../../../../.icons/aws.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [vm, linux, aws, persistent, devcontainer]
|
||||
---
|
||||
|
||||
# Remote Development on AWS EC2 VMs using a Devcontainer
|
||||
|
||||
Provision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs) with this example template.
|
||||

|
||||
|
||||
<!-- TODO: Add screenshot -->
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Authentication
|
||||
|
||||
By default, this template authenticates to AWS using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration).
|
||||
|
||||
The simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`.
|
||||
|
||||
To use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template.
|
||||
|
||||
## Required permissions / policy
|
||||
|
||||
The following sample policy allows Coder to create EC2 instances and modify
|
||||
instances provisioned by Coder:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "VisualEditor0",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ec2:GetDefaultCreditSpecification",
|
||||
"ec2:DescribeIamInstanceProfileAssociations",
|
||||
"ec2:DescribeTags",
|
||||
"ec2:DescribeInstances",
|
||||
"ec2:DescribeInstanceTypes",
|
||||
"ec2:DescribeInstanceStatus",
|
||||
"ec2:CreateTags",
|
||||
"ec2:RunInstances",
|
||||
"ec2:DescribeInstanceCreditSpecifications",
|
||||
"ec2:DescribeImages",
|
||||
"ec2:ModifyDefaultCreditSpecification",
|
||||
"ec2:DescribeVolumes"
|
||||
],
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Sid": "CoderResources",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ec2:DescribeInstanceAttribute",
|
||||
"ec2:UnmonitorInstances",
|
||||
"ec2:TerminateInstances",
|
||||
"ec2:StartInstances",
|
||||
"ec2:StopInstances",
|
||||
"ec2:DeleteTags",
|
||||
"ec2:MonitorInstances",
|
||||
"ec2:CreateTags",
|
||||
"ec2:RunInstances",
|
||||
"ec2:ModifyInstanceAttribute",
|
||||
"ec2:ModifyInstanceCreditSpecification"
|
||||
],
|
||||
"Resource": "arn:aws:ec2:*:*:instance/*",
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"aws:ResourceTag/Coder_Provisioned": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
This template provisions the following resources:
|
||||
|
||||
- AWS Instance
|
||||
|
||||
Coder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.
|
||||
|
||||
> **Note**
|
||||
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
|
||||
|
||||
## Caching
|
||||
|
||||
To speed up your builds, you can use a container registry as a cache.
|
||||
When creating the template, set the parameter `cache_repo` to a valid Docker repository in the form `host.tld/path/to/repo`.
|
||||
|
||||
See the [Envbuilder Terraform Provider Examples](https://github.com/coder/terraform-provider-envbuilder/blob/main/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf/) for a more complete example of how the provider works.
|
||||
|
||||
> [!NOTE]
|
||||
> We recommend using a registry cache with authentication enabled.
|
||||
> To allow Envbuilder to authenticate with a registry cache hosted on ECR, specify an IAM instance
|
||||
> profile that has read and write access to the given registry. For more information, see the
|
||||
> [AWS documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html).
|
||||
>
|
||||
> Alternatively, you can specify the variable `cache_repo_docker_config_path`
|
||||
> with the path to a Docker config `.json` on disk containing valid credentials for the registry.
|
||||
|
||||
## code-server
|
||||
|
||||
`code-server` is installed via the [`code-server`](https://registry.coder.com/modules/code-server) registry module. For a list of all modules and templates pplease check [Coder Registry](https://registry.coder.com).
|
||||
|
After Width: | Height: | Size: 124 KiB |