5 Commits

Author SHA1 Message Date
octoturge 9424746305 Document API/direct-URL fallback for missing Gitea Secrets tab
Provision Coder Templates / provision (push) Failing after 1m16s
Some Gitea/Forgejo versions drop the Secrets nav link from Settings ->
Actions while Runners and Variables still show, even though the page
and API underneath still work. Document the direct-URL and curl-based
workarounds so the token-rotation/auto-provisioning bootstrap isn't
blocked by it.
2026-08-15 12:32:42 +02:00
octoturge a5a760b24a Add daily Coder token auto-rotation workflow
This Coder deployment caps API token lifetime at 168h (7 days), so
rather than raising that cap deployment-wide, add
.gitea/workflows/rotate-coder-token.yml: runs daily, mints a new 168h
coder token, PUTs it into this repo's CODER_SESSION_TOKEN secret via
Gitea's actions/secrets API (confirmed against the live instance's
swagger.v1.json - PUT /repos/{owner}/{repo}/actions/secrets/{name} with
{"data": "..."}), then deletes the token(s) it replaced. Old token isn't
touched until the new one is confirmed live, so a failed run fails safe.

Needs a one-time GITEA_ROTATION_TOKEN secret (a Gitea PAT with
write:repository scope, no expiration) so the workflow can write to its
own repo's secrets going forward - documented in README. After that,
CODER_SESSION_TOKEN (used by coder-templates.yml) never needs manual
attention again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 12:02:22 +02:00
octoturge a9583535f0 Add Gitea Actions workflow to auto-provision Coder templates
.gitea/workflows/coder-templates.yml pushes every templates/<env>/ dir
to Coder as profiles-<env> on every push to main that touches
templates/**, scripts/**, or profile-templates/** - coder templates push
creates the template on first run and updates it thereafter, so adding a
new templates/<env>/ directory is enough to provision it, no workflow
edits needed.

It also diffs templates/ against the previous commit and runs
`coder templates delete profiles-<env>` for any directory that was
removed. Deletion fails (loudly, as a job warning, not a hard failure)
rather than succeeding if the template still has active workspaces,
since coder templates delete already refuses that server-side.

Runs on the ubuntu-latest self-hosted Gitea runner already registered on
this instance (confirmed via gitea-runner-1's /data/.runner labels) and
installs the coder CLI itself. Needs CODER_URL and CODER_SESSION_TOKEN
as repo/org Actions secrets - documented in README, left for the user to
set up since token creation needs their own Coder login.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 11:49:56 +02:00
octoturge f6c432d21d Install Agent Skills into all 3 AI CLIs, and use Bun instead of npm
Adds scripts/install-skills.sh: pulls this repo's extensions/ skill
bundles (SKILL.md-format) at workspace startup and installs them into
Claude Code (~/.claude/skills), GitHub Copilot CLI (~/.copilot/skills),
and Antigravity CLI (~/.gemini/config/skills). Every env gets the common
awesome-skills-plugin bundle; COBOL/3D/TTRPG additionally get their
matching skill(s) from custom-specialty-plugin via a per-template
SPECIALTY_SKILLS value.

Each template also installs Bun via a coder_script and hooks ~/.bun/bin
onto PATH in .bashrc (the bun.sh installer doesn't reliably do this in a
scripted shell). The CLI setup wizard now uses `bun install -g` instead
of `npm install -g` for GitHub Copilot CLI and Claude Code CLI.

All six templates re-validated with terraform init/validate against the
real coder-server container on octo-winsrv.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 11:41:09 +02:00
octoturge 1beaac6e4f Split single-container Coder template into per-env templates
The old root main.tf tried to switch dev profiles inside one shared
container via a coder_parameter dropdown; the settings-application path
looked for a *.json cache file that never existed (the cache was written
as *.code-profile), so profile settings never actually applied, and VS
Code extensions were copied from extensions/ (which turns out to be
Claude Code plugin bundles, not real VS Code extension packages).

Replace it with one independent Coder template per environment
(templates/default, 3d-printing, cobol, python, ttrpg, web). Each reads
its matching profile-templates/*.code-profile file at template-push time
via file()/jsondecode(), feeds the extension ID list straight into the
code-server module's `extensions` input, and writes the raw settings.json
text via a coder_script - no runtime Gitea zip download needed anymore.

Also add scripts/cli-setup-wizard.sh, hooked into every new interactive
shell until completed, offering to install/log into GitHub Copilot CLI,
Google Antigravity CLI, and Claude Code CLI. VS Code extensions are
deliberately not asked about there since Terraform already handles them.

All six templates verified with `terraform init`/`validate` against the
real coder-server container on octo-winsrv.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 11:28:26 +02:00
12 changed files with 2441 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
* text=auto eol=lf
*.sh text eol=lf
*.tf text eol=lf
*.code-profile -text
+80
View File
@@ -0,0 +1,80 @@
name: Provision Coder Templates
# Keeps Coder templates in sync with templates/*/ in this repo:
# - every push to main pushes a new version of each templates/<env>/ dir
# (coder templates push creates it if it doesn't exist yet, so adding a
# new templates/<env>/ directory is enough to provision a new one)
# - if a templates/<env>/ directory is removed on main, its template is
# deleted from Coder. `coder templates delete` refuses to delete a
# template that still has active workspaces, so this can't silently
# orphan running workspaces - it just fails loudly and needs a human.
#
# Requires two repo/org secrets (Settings > Actions > Secrets):
# CODER_URL e.g. https://code.octoturge.com
# CODER_SESSION_TOKEN a token from `coder tokens create`, ideally under a
# dedicated service account rather than a personal one
on:
push:
branches: [main]
paths:
- "templates/**"
- "scripts/**"
- "profile-templates/**"
- ".gitea/workflows/coder-templates.yml"
workflow_dispatch: {}
jobs:
provision:
runs-on: ubuntu-latest
env:
CODER_URL: ${{ secrets.CODER_URL }}
CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }}
steps:
- name: Checkout (full history, needed to detect removed templates)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install coder CLI
run: |
set -e
curl -fsSL https://coder.com/install.sh | sh
coder version
- name: Push (create or update) every template
run: |
set -e
for dir in templates/*/; do
name="profiles-$(basename "$dir")"
echo "::group::Pushing $name from $dir"
coder templates push "$name" -d "$dir" --yes \
-m "auto-provisioned from ${GITHUB_SHA:0:12}"
echo "::endgroup::"
done
- name: Delete templates whose directory was removed
if: github.event_name == 'push'
run: |
set -e
PREV_SHA="$(git rev-parse HEAD~1 2>/dev/null || true)"
if [ -z "$PREV_SHA" ]; then
echo "No previous commit on this branch (first push), nothing to diff. Skipping."
exit 0
fi
OLD_DIRS="$(git ls-tree -d --name-only "$PREV_SHA" -- templates 2>/dev/null | xargs -n1 basename 2>/dev/null || true)"
if [ -z "$OLD_DIRS" ]; then
echo "No templates/ directory at $PREV_SHA, nothing to diff. Skipping."
exit 0
fi
for old in $OLD_DIRS; do
if [ ! -d "templates/$old" ]; then
name="profiles-$old"
echo "::group::Deleting $name (templates/$old was removed)"
coder templates delete "$name" --yes \
|| echo "::warning::Failed to delete $name - check for active workspaces still using it."
echo "::endgroup::"
fi
done
+83
View File
@@ -0,0 +1,83 @@
name: Rotate Coder API Token
# Keeps the CODER_SESSION_TOKEN secret (used by coder-templates.yml) alive
# forever without anyone needing to remember to refresh it. This Coder
# deployment caps token lifetime at 168h (7 days), so this runs daily,
# mints a fresh 168h token, writes it back into this repo's
# CODER_SESSION_TOKEN secret via the Gitea API, then deletes the token(s)
# it replaced.
#
# One-time bootstrap (see README "Auto-provisioning" section): a
# GITEA_ROTATION_TOKEN secret holding a Gitea personal access token
# (write:repository scope, no expiration) with permission to write this
# repo's Actions secrets. Nothing else needs to touch this ever again.
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch: {}
jobs:
rotate:
runs-on: ubuntu-latest
env:
CODER_URL: ${{ secrets.CODER_URL }}
CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }}
GITEA_ROTATION_TOKEN: ${{ secrets.GITEA_ROTATION_TOKEN }}
GITEA_API_URL: ${{ github.server_url }}/api/v1
GITEA_REPO_PATH: ${{ github.repository }}
steps:
- name: Install coder CLI and jq
run: |
set -e
curl -fsSL https://coder.com/install.sh | sh
coder version
command -v jq >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y -qq jq)
- name: Create a new token
id: new_token
run: |
set -e
NAME="gitea-ci-$(date -u +%Y%m%dT%H%M%SZ)"
TOKEN="$(coder tokens create --name "$NAME" --lifetime 168h)"
if [ -z "$TOKEN" ]; then
echo "::error::coder tokens create returned no token"
exit 1
fi
echo "::add-mask::$TOKEN"
echo "name=$NAME" >> "$GITHUB_OUTPUT"
echo "token=$TOKEN" >> "$GITHUB_OUTPUT"
- name: Publish new token to CODER_SESSION_TOKEN secret
env:
NEW_TOKEN: ${{ steps.new_token.outputs.token }}
run: |
set -e
BODY="$(jq -n --arg data "$NEW_TOKEN" '{data:$data}')"
HTTP_STATUS="$(curl -s -o /tmp/put-secret.out -w '%{http_code}' \
-X PUT \
-H "Authorization: token ${GITEA_ROTATION_TOKEN}" \
-H "Content-Type: application/json" \
-d "$BODY" \
"${GITEA_API_URL}/repos/${GITEA_REPO_PATH}/actions/secrets/CODER_SESSION_TOKEN")"
if [ "$HTTP_STATUS" != "201" ] && [ "$HTTP_STATUS" != "204" ]; then
echo "::error::Failed to update CODER_SESSION_TOKEN secret (HTTP $HTTP_STATUS)"
cat /tmp/put-secret.out
exit 1
fi
echo "CODER_SESSION_TOKEN secret updated (HTTP $HTTP_STATUS)."
- name: Delete the token(s) this replaced
env:
KEEP_NAME: ${{ steps.new_token.outputs.name }}
run: |
set -e
coder tokens list -o json \
| jq -r --arg keep "$KEEP_NAME" \
'.[] | select(.token_name | startswith("gitea-ci-")) | select(.token_name != $keep) | .id' \
| while read -r id; do
[ -z "$id" ] && continue
echo "Removing superseded token $id"
coder tokens delete "$id" --delete \
|| echo "::warning::Failed to delete superseded token $id"
done
+211
View File
@@ -1,2 +1,213 @@
# Profiles-for-Coder
Coder templates for per-discipline dev environments (Default, 3D Printing,
COBOL, Python, TTRPG, Web). Each environment is its own Coder **template** -
not a dropdown inside one shared container, which is what this repo used to
do and which didn't actually work.
## Layout
```
templates/
default/main.tf # Standard Default Dev
3d-printing/main.tf # 3D Printing & Engineering
cobol/main.tf # COBOL Modern Mainframe
python/main.tf # Python Engineering
ttrpg/main.tf # TTRPG & Lore Building
web/main.tf # Web Applications
scripts/
cli-setup-wizard.sh # shared first-run wizard, see below
install-skills.sh # shared Agent Skills installer, see below
profile-templates/ # VS Code .code-profile exports, one per env
extensions/ # Agent Skills bundles, installed per env (see below)
```
Each `templates/<env>/main.tf` is a complete, independent Coder template
(agent, docker container, code-server, JetBrains). They're deliberately not
built from a shared Terraform module - only their `locals` block differs
(which profile file to read). The naming convention is `profiles-<dir>`
(matches what `.gitea/workflows/coder-templates.yml` does automatically -
see below). To push by hand:
```sh
coder templates push profiles-default -d templates/default
coder templates push profiles-3d-printing -d templates/3d-printing
coder templates push profiles-cobol -d templates/cobol
coder templates push profiles-python -d templates/python
coder templates push profiles-ttrpg -d templates/ttrpg
coder templates push profiles-web -d templates/web
```
### How the VS Code profile gets applied
`profile-templates/*.code-profile` is a real VS Code Profile export: a JSON
file whose `settings` and `extensions` fields are themselves JSON-encoded
strings (double/triple-nested). Each template's `main.tf` reads and decodes
its matching file **at `terraform apply`/push time** (via `file()` +
`jsondecode()`), then:
- passes the extension ID list straight into the `code-server` module's
`extensions` input, so code-server installs them on first boot - no
interactive prompt needed, Terraform handles it declaratively;
- writes the raw `settings.json` text (comments and all - VS Code tolerates
JSONC) to `~/.local/share/code-server/User/settings.json` via a
`coder_script`.
This replaces the old approach, which downloaded a zip of this repo from
Gitea *inside* the running container and tried to apply settings from
`~/.local/share/profiles-cache/<profile>.json` - a path that never matched
the actual `.code-profile` file extension, so settings never applied. That
bug (plus the single shared container) is why "one container, many envs"
never really worked.
### Bun
Every template installs [Bun](https://bun.sh) via a `coder_script`
(`curl -fsSL https://bun.sh/install | bash`) and hooks `~/.bun/bin` onto
`PATH` in `~/.bashrc` (the installer doesn't reliably do this itself in a
non-interactive/scripted shell). The CLI setup wizard below uses
`bun install -g <pkg>` instead of `npm install -g <pkg>` for everything it
installs.
### CLI setup wizard
`scripts/cli-setup-wizard.sh` is dropped onto every workspace and hooked
into `~/.bashrc`. It runs in every new interactive terminal - until the user
finishes it - and offers to install + log into:
- **GitHub Copilot CLI** (`bun install -g @github/copilot`, then `copilot login`)
- **Google Antigravity CLI** (`curl -fsSL https://antigravity.google/cli/install.sh | bash`, binary `agy`)
- **Claude Code CLI** (`bun install -g @anthropic-ai/claude-code`, then `claude`)
It does **not** ask about VS Code extensions, since those are handled by
Terraform (see above). Once the user confirms completion it writes a
sentinel file (`~/.cache/coder-cli-wizard/done`) and stops prompting. It can
always be re-run manually: `bash /opt/coder/cli-setup-wizard.sh --force`.
### `extensions/` directory -> Agent Skills
`extensions/awesome-skills-plugin` and `extensions/custom-specialty-plugin`
are **Agent Skills bundles** (`plugin.json` + `SKILL.md` files) - the old
root `main.tf` copied this folder straight into code-server's VS Code
extensions directory, which never worked since these aren't VS Code
extension packages.
Each template now runs `scripts/install-skills.sh` (via a `coder_script`,
pulling a fresh zip of this repo from Gitea rather than embedding ~2.5MB
into Terraform state) to install skills into all three AI CLIs' personal
skills directories:
| CLI | Skills directory |
| --- | --- |
| Claude Code | `~/.claude/skills/<name>/` |
| GitHub Copilot CLI | `~/.copilot/skills/<name>/` |
| Antigravity CLI | `~/.gemini/config/skills/<name>/` (per antigravity.google/docs/skills - some third-party docs disagree on this path, worth a spot-check on a live workspace) |
Every environment gets the full `extensions/awesome-skills-plugin/skills/*`
bundle. On top of that, whichever env has a matching entry in
`extensions/custom-specialty-plugin/skills/` gets it installed too, set via
the `SPECIALTY_SKILLS` env var passed to the script from each template's
`install_skills` `coder_script`:
- COBOL -> `cobol-teacher`
- 3D Printing -> `openscad-parametric`
- TTRPG -> `foundryvtt-modding` and `ttrpg-lore-weaver`
- Default / Python / Web -> none (no matching specialty skill exists yet)
### Auto-provisioning (Gitea Actions)
`.gitea/workflows/coder-templates.yml` keeps Coder in sync with this repo on
every push to `main` that touches `templates/**`, `scripts/**`, or
`profile-templates/**`:
- **Add** a new `templates/<env>/` directory -> next push creates a new
Coder template `profiles-<env>` automatically. No workflow edits needed.
- **Edit** an existing `templates/<env>/main.tf` (or a shared script/profile
it references) -> next push updates that template with a new version.
- **Remove** a `templates/<env>/` directory -> next push deletes
`profiles-<env>` from Coder. `coder templates delete` refuses if the
template still has active workspaces, so this fails loudly instead of
silently orphaning anyone's running workspace - that failure only shows up
as a `::warning::` in the job log, it doesn't fail the whole run.
It runs on the `ubuntu-latest` self-hosted runner already registered on this
Gitea instance and installs the `coder` CLI itself via `coder.com/install.sh`.
**One-time setup required** (not something this workflow can do for itself -
needs a human with Coder access). This deployment caps API token lifetime at
168h (7 days), so rather than raising that cap deployment-wide,
`.gitea/workflows/rotate-coder-token.yml` (see below) keeps a fresh token
flowing into the secret automatically:
1. Create a Coder API token - ideally under a dedicated service account
rather than a personal login, since this token can create/delete
templates:
```sh
coder login https://code.octoturge.com
coder tokens create --name gitea-ci --lifetime 168h
```
2. In Gitea, go to this repo's **Settings -> Actions -> Secrets** (or the
org-level equivalent to share across repos) and add:
- `CODER_URL` = `https://code.octoturge.com`
- `CODER_SESSION_TOKEN` = the token printed by step 1
Until those secrets exist, `coder-templates.yml` will run and fail cleanly
at the `coder templates push` step rather than doing anything destructive.
**If the Secrets tab doesn't show up** (some Gitea/Forgejo versions drop the
*Secrets* nav link from Settings -> Actions while *Runners* and *Variables*
still show, even though the page and API underneath both still work - see
[forgejo#938](https://codeberg.org/forgejo/forgejo/issues/938)), try either:
1. Go straight to the URL the nav link would normally point at:
`https://<gitea-host>/<owner>/<repo>/settings/actions/secrets`. If a
working "Add Secret" form loads there, it's just a missing nav link - add
the secrets on that page as normal.
2. If that also won't load, set the secrets via the Actions Secrets API
instead, using a Gitea personal access token (`write:repository` scope,
Settings -> Applications -> Generate New Token):
```sh
GITEA_PAT="<your Gitea PAT>"
OWNER=octoturge
REPO=Profiles-for-Coder
curl -s -X PUT "https://<gitea-host>/api/v1/repos/$OWNER/$REPO/actions/secrets/CODER_URL" \
-H "Authorization: token $GITEA_PAT" -H "Content-Type: application/json" \
-d '{"data":"https://code.octoturge.com"}'
curl -s -X PUT "https://<gitea-host>/api/v1/repos/$OWNER/$REPO/actions/secrets/CODER_SESSION_TOKEN" \
-H "Authorization: token $GITEA_PAT" -H "Content-Type: application/json" \
-d "{\"data\":\"$(coder tokens create --name gitea-ci --lifetime 168h)\"}"
```
A `201`/`204` response means the secret was saved. This is the exact same
endpoint `rotate-coder-token.yml` uses at runtime, so if it works here it
confirms the workflow itself will be able to update the secret later too.
Never paste a PAT or Coder token into a chat/ticket - run these commands
from a trusted shell only.
### Token rotation (Gitea Actions)
`.gitea/workflows/rotate-coder-token.yml` runs daily and keeps
`CODER_SESSION_TOKEN` alive forever without anyone needing to remember to
refresh it: it mints a new 168h Coder token, writes it into the
`CODER_SESSION_TOKEN` secret via the Gitea API, then deletes the token(s) it
just replaced. If a run ever fails, the previous token is still untouched
and still valid (nothing gets deleted until the new one is confirmed live),
so it fails safe rather than locking you out.
**One-time bootstrap** (also needs a human - this is what lets the rotation
workflow write to its own repo's secrets):
1. Create a Gitea personal access token with **write:repository** scope and
**no expiration** (Settings -> Applications -> Generate New Token). This
one doesn't rotate itself, so give it a long life up front.
2. Add it as a repo/org Actions secret named `GITEA_ROTATION_TOKEN` (same
Settings -> Actions -> Secrets page as above - if that tab is missing,
see the nav-link workaround / API fallback in the "Auto-provisioning"
section above, same `curl -X PUT .../actions/secrets/<name>` pattern,
just with `GITEA_ROTATION_TOKEN` as the secret name and the PAT itself as
the value).
After that, `CODER_SESSION_TOKEN` never needs manual attention again - you
can also trigger a rotation on demand from Gitea's Actions tab
(`workflow_dispatch`) instead of waiting for the daily schedule.
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# Coder workspace first-run CLI setup wizard.
#
# Meant to be `source`d from a new interactive shell (e.g. via .bashrc). It asks,
# once per user per workspace, whether to install and log into a few optional
# AI coding CLIs. It re-runs on every new terminal until the user lets it finish
# (or explicitly skips it for good), then gets out of the way.
#
# VS Code / code-server extensions are intentionally NOT asked about here -
# they're installed declaratively by the Coder template itself (the
# `code-server` module's `extensions` input, populated from the matching
# profile-templates/*.code-profile file at template-push time).
#
# Manual re-run: bash /opt/coder/cli-setup-wizard.sh --force
set -u
WIZARD_DONE_FILE="${HOME}/.cache/coder-cli-wizard/done"
FORCE=0
[ "${1:-}" = "--force" ] && FORCE=1
export BUN_INSTALL="${HOME}/.bun"
export PATH="${BUN_INSTALL}/bin:${HOME}/.local/bin:${PATH}"
# Only bother interactive shells with a real terminal attached, and only until
# the user marks the wizard as done.
if [ "$FORCE" -ne 1 ]; then
case "$-" in
*i*) : ;;
*) return 0 2>/dev/null || exit 0 ;;
esac
[ -t 0 ] || { return 0 2>/dev/null || exit 0; }
[ -f "$WIZARD_DONE_FILE" ] && { return 0 2>/dev/null || exit 0; }
fi
mkdir -p "$(dirname "$WIZARD_DONE_FILE")"
ask_yes_no() {
local prompt="$1" reply
read -r -p "$prompt [y/N] " reply
case "$reply" in
[Yy]*) return 0 ;;
*) return 1 ;;
esac
}
echo ""
echo "==================================================================="
echo " Coder workspace setup wizard"
echo " Runs once per new terminal until you finish it. Ctrl+C any time"
echo " to skip for now - it'll ask again next terminal."
echo "==================================================================="
# --- GitHub Copilot CLI ---
if command -v copilot >/dev/null 2>&1; then
echo "GitHub Copilot CLI already installed, skipping."
elif command -v bun >/dev/null 2>&1; then
if ask_yes_no "Install GitHub Copilot CLI and log in?"; then
if bun install -g @github/copilot; then
copilot login || echo "Install succeeded but login didn't complete. Retry any time with: copilot login"
else
echo "Copilot CLI install failed. Retry later with: bun install -g @github/copilot && copilot login"
fi
fi
else
echo "Skipping GitHub Copilot CLI: bun not found on this workspace image."
fi
# --- Google Antigravity CLI (agy) ---
if command -v agy >/dev/null 2>&1; then
echo "Antigravity CLI already installed, skipping."
else
if ask_yes_no "Install Google Antigravity CLI (agy) and log in?"; then
if curl -fsSL https://antigravity.google/cli/install.sh | bash; then
echo "Launching 'agy' once to complete sign-in (exit with /logout or Ctrl+D when done)..."
agy || echo "Sign-in didn't complete. Retry any time by running: agy"
else
echo "Antigravity CLI install failed. Retry later with: curl -fsSL https://antigravity.google/cli/install.sh | bash"
fi
fi
fi
# --- Claude Code CLI ---
if command -v claude >/dev/null 2>&1; then
echo "Claude Code CLI already installed, skipping."
elif command -v bun >/dev/null 2>&1; then
if ask_yes_no "Install Claude Code CLI and log in?"; then
if bun install -g @anthropic-ai/claude-code; then
echo "Launching 'claude' once to complete sign-in (use /login if not prompted; Ctrl+C to exit when done)..."
claude || echo "Sign-in didn't complete. Retry any time by running: claude"
else
echo "Claude Code CLI install failed. Retry later with: bun install -g @anthropic-ai/claude-code"
fi
fi
else
echo "Skipping Claude Code CLI: bun not found on this workspace image."
fi
echo ""
if ask_yes_no "Mark setup wizard as complete so it stops asking on new terminals?"; then
touch "$WIZARD_DONE_FILE"
echo "Done. Re-run any time with: bash /opt/coder/cli-setup-wizard.sh --force"
else
echo "OK, this'll ask again next time you open a terminal."
fi
return 0 2>/dev/null || exit 0
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Installs this repo's Agent Skills (extensions/{awesome-skills-plugin,
# custom-specialty-plugin}/skills/*, each a SKILL.md-based skill directory)
# into every AI CLI's personal skills directory:
#
# Claude Code CLI -> ~/.claude/skills/<name>/
# GitHub Copilot CLI -> ~/.copilot/skills/<name>/
# Antigravity CLI -> ~/.gemini/config/skills/<name>/ (per antigravity.google/docs/skills;
# worth a spot-check if agy doesn't pick these up, some third-party
# docs disagree on the exact path)
#
# Run once at workspace startup via coder_script. Pulls this repo fresh from
# Gitea rather than embedding ~2.5MB of skill files into Terraform state.
#
# Env vars:
# SPECIALTY_SKILLS - optional space-separated skill names from
# extensions/custom-specialty-plugin/skills/ to install in addition to
# the common awesome-skills-plugin bundle (every env gets that one).
set -e
REPO_ZIP_URL="https://git.octoturge.com/octoturge/Profiles-for-Coder/archive/main.zip"
ZIP_PATH="/tmp/coder-skills-src.zip"
WORK_DIR="/tmp/coder-skills-src"
rm -rf "$WORK_DIR" "$ZIP_PATH"
curl -fsSL "$REPO_ZIP_URL" -o "$ZIP_PATH"
mkdir -p "$WORK_DIR"
unzip -q -o "$ZIP_PATH" -d "$WORK_DIR"
INNER_DIR=$(find "$WORK_DIR" -mindepth 1 -maxdepth 1 -type d | head -n1)
if [ -z "$INNER_DIR" ]; then
echo "install-skills: couldn't find extracted repo contents, skipping." >&2
rm -rf "$WORK_DIR" "$ZIP_PATH"
exit 0
fi
TARGET_DIRS=("$HOME/.claude/skills" "$HOME/.copilot/skills" "$HOME/.gemini/config/skills")
for dir in "${TARGET_DIRS[@]}"; do
mkdir -p "$dir"
done
# Common skill bundle, installed for every environment.
COMMON_SKILLS_SRC="$INNER_DIR/extensions/awesome-skills-plugin/skills"
if [ -d "$COMMON_SKILLS_SRC" ]; then
for dir in "${TARGET_DIRS[@]}"; do
cp -r "$COMMON_SKILLS_SRC/." "$dir/"
done
echo "install-skills: installed common skill bundle into ${TARGET_DIRS[*]}"
else
echo "install-skills: common skill bundle not found at $COMMON_SKILLS_SRC, skipping." >&2
fi
# Environment-specific specialty skills, if any were requested.
for skill in ${SPECIALTY_SKILLS:-}; do
SRC="$INNER_DIR/extensions/custom-specialty-plugin/skills/$skill"
if [ -d "$SRC" ]; then
for dir in "${TARGET_DIRS[@]}"; do
cp -r "$SRC" "$dir/$skill"
done
echo "install-skills: installed specialty skill '$skill'"
else
echo "install-skills: specialty skill '$skill' not found at $SRC, skipping." >&2
fi
done
rm -rf "$WORK_DIR" "$ZIP_PATH"
echo "install-skills: done."
+314
View File
@@ -0,0 +1,314 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
docker = {
source = "kreuzwerker/docker"
}
}
}
locals {
env_name = "3D Printing & Engineering"
profile = jsondecode(file("${path.module}/../../profile-templates/3D.code-profile"))
settings_raw = jsondecode(local.profile.settings).settings
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
}
variable "docker_socket" {
default = ""
description = "(Optional) Docker socket URI"
type = string
}
provider "docker" {
# Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default
host = var.docker_socket != "" ? var.docker_socket : null
}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
startup_script = <<-EOT
set -e
# Prepare user home with default files on first start.
if [ ! -f ~/.init_done ]; then
cp -rT /etc/skel ~
touch ~/.init_done
fi
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
EOT
# These environment variables allow you to make Git commits right away after creating a
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
# You can remove this block if you'd prefer to configure Git manually or using
# dotfiles. (see docs/dotfiles.md)
env = {
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
}
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
# For basic 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
# `extensions` is populated at template-push time from this env's
# profile-templates/*.code-profile file, so no interactive prompt is
# needed for VS Code extensions - Terraform handles it declaratively.
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
# Pass the target folder here natively
folder = "/home/coder/workspace"
extensions = local.extensions
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "~> 1.1"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
}
resource "docker_volume" "home_volume" {
name = "coder-${data.coder_workspace.me.id}-home"
# Protect the volume from being deleted due to changes in attributes.
lifecycle {
ignore_changes = all
}
# Add labels in Docker to keep track of orphan resources.
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
# This field becomes outdated if the workspace is renamed but can
# be useful for debugging or cleaning out dangling volumes.
labels {
label = "coder.workspace_name_at_creation"
value = data.coder_workspace.me.name
}
}
resource "docker_container" "workspace" {
count = data.coder_workspace.me.start_count
image = "codercom/enterprise-base:ubuntu"
# Uses lower() to avoid Docker restriction on container names.
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# Hostname makes the shell more user friendly: coder@my-workspace:~$
hostname = data.coder_workspace.me.name
# Use the docker gateway if the access URL is 127.0.0.1
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
host {
host = "host.docker.internal"
ip = "host-gateway"
}
host {
host = "code.octoturge.com"
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
}
labels {
label = "coder.profile"
value = local.env_name
}
}
# Writes this env's VS Code settings.json, sourced straight from the
# matching profile-templates/*.code-profile file at template-push time.
resource "coder_script" "apply_settings" {
agent_id = coder_agent.main.id
display_name = "Apply ${local.env_name} VS Code Settings"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p "$HOME/workspace"
mkdir -p "$HOME/.local/share/code-server/User"
echo '${base64encode(local.settings_raw)}' | base64 -d > "$HOME/.local/share/code-server/User/settings.json"
EOT
}
# Drops the shared CLI setup wizard onto the workspace and hooks it into
# every new interactive shell (via .bashrc) until the user completes it.
# See ../../scripts/cli-setup-wizard.sh for what it actually asks.
resource "coder_script" "cli_setup_wizard" {
agent_id = coder_agent.main.id
display_name = "Install CLI Setup Wizard"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p /opt/coder
echo '${base64encode(file("${path.module}/../../scripts/cli-setup-wizard.sh"))}' | base64 -d > /opt/coder/cli-setup-wizard.sh
chmod +x /opt/coder/cli-setup-wizard.sh
MARKER="# >>> coder cli setup wizard >>>"
if ! grep -qF "$MARKER" "$HOME/.bashrc" 2>/dev/null; then
{
echo ""
echo "$MARKER"
echo 'export PATH="$HOME/.local/bin:$PATH"'
echo 'source /opt/coder/cli-setup-wizard.sh'
echo "# <<< coder cli setup wizard <<<"
} >> "$HOME/.bashrc"
fi
EOT
}
# Installs Bun and uses it (instead of npm) for the CLI installs the wizard
# script runs. The installer doesn't reliably add ~/.bun/bin to PATH in
# non-interactive shells, so that's hooked into .bashrc explicitly here.
resource "coder_script" "install_bun" {
agent_id = coder_agent.main.id
display_name = "Install Bun"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
export BUN_INSTALL="$HOME/.bun"
if [ ! -x "$BUN_INSTALL/bin/bun" ]; then
curl -fsSL https://bun.sh/install | bash
fi
MARKER="# >>> coder bun path >>>"
if ! grep -qF "$MARKER" "$HOME/.bashrc" 2>/dev/null; then
{
echo ""
echo "$MARKER"
echo 'export BUN_INSTALL="$HOME/.bun"'
echo 'export PATH="$BUN_INSTALL/bin:$PATH"'
echo "# <<< coder bun path <<<"
} >> "$HOME/.bashrc"
fi
EOT
}
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
# Antigravity CLI's skills directories, plus the openscad-parametric skill
# from extensions/custom-specialty-plugin. See ../../scripts/install-skills.sh.
resource "coder_script" "install_skills" {
agent_id = coder_agent.main.id
display_name = "Install Agent Skills"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p /opt/coder
echo '${base64encode(file("${path.module}/../../scripts/install-skills.sh"))}' | base64 -d > /opt/coder/install-skills.sh
chmod +x /opt/coder/install-skills.sh
SPECIALTY_SKILLS="openscad-parametric" /opt/coder/install-skills.sh
EOT
}
+314
View File
@@ -0,0 +1,314 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
docker = {
source = "kreuzwerker/docker"
}
}
}
locals {
env_name = "COBOL Modern Mainframe"
profile = jsondecode(file("${path.module}/../../profile-templates/COBOL.code-profile"))
settings_raw = jsondecode(local.profile.settings).settings
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
}
variable "docker_socket" {
default = ""
description = "(Optional) Docker socket URI"
type = string
}
provider "docker" {
# Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default
host = var.docker_socket != "" ? var.docker_socket : null
}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
startup_script = <<-EOT
set -e
# Prepare user home with default files on first start.
if [ ! -f ~/.init_done ]; then
cp -rT /etc/skel ~
touch ~/.init_done
fi
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
EOT
# These environment variables allow you to make Git commits right away after creating a
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
# You can remove this block if you'd prefer to configure Git manually or using
# dotfiles. (see docs/dotfiles.md)
env = {
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
}
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
# For basic 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
# `extensions` is populated at template-push time from this env's
# profile-templates/*.code-profile file, so no interactive prompt is
# needed for VS Code extensions - Terraform handles it declaratively.
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
# Pass the target folder here natively
folder = "/home/coder/workspace"
extensions = local.extensions
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "~> 1.1"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
}
resource "docker_volume" "home_volume" {
name = "coder-${data.coder_workspace.me.id}-home"
# Protect the volume from being deleted due to changes in attributes.
lifecycle {
ignore_changes = all
}
# Add labels in Docker to keep track of orphan resources.
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
# This field becomes outdated if the workspace is renamed but can
# be useful for debugging or cleaning out dangling volumes.
labels {
label = "coder.workspace_name_at_creation"
value = data.coder_workspace.me.name
}
}
resource "docker_container" "workspace" {
count = data.coder_workspace.me.start_count
image = "codercom/enterprise-base:ubuntu"
# Uses lower() to avoid Docker restriction on container names.
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# Hostname makes the shell more user friendly: coder@my-workspace:~$
hostname = data.coder_workspace.me.name
# Use the docker gateway if the access URL is 127.0.0.1
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
host {
host = "host.docker.internal"
ip = "host-gateway"
}
host {
host = "code.octoturge.com"
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
}
labels {
label = "coder.profile"
value = local.env_name
}
}
# Writes this env's VS Code settings.json, sourced straight from the
# matching profile-templates/*.code-profile file at template-push time.
resource "coder_script" "apply_settings" {
agent_id = coder_agent.main.id
display_name = "Apply ${local.env_name} VS Code Settings"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p "$HOME/workspace"
mkdir -p "$HOME/.local/share/code-server/User"
echo '${base64encode(local.settings_raw)}' | base64 -d > "$HOME/.local/share/code-server/User/settings.json"
EOT
}
# Drops the shared CLI setup wizard onto the workspace and hooks it into
# every new interactive shell (via .bashrc) until the user completes it.
# See ../../scripts/cli-setup-wizard.sh for what it actually asks.
resource "coder_script" "cli_setup_wizard" {
agent_id = coder_agent.main.id
display_name = "Install CLI Setup Wizard"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p /opt/coder
echo '${base64encode(file("${path.module}/../../scripts/cli-setup-wizard.sh"))}' | base64 -d > /opt/coder/cli-setup-wizard.sh
chmod +x /opt/coder/cli-setup-wizard.sh
MARKER="# >>> coder cli setup wizard >>>"
if ! grep -qF "$MARKER" "$HOME/.bashrc" 2>/dev/null; then
{
echo ""
echo "$MARKER"
echo 'export PATH="$HOME/.local/bin:$PATH"'
echo 'source /opt/coder/cli-setup-wizard.sh'
echo "# <<< coder cli setup wizard <<<"
} >> "$HOME/.bashrc"
fi
EOT
}
# Installs Bun and uses it (instead of npm) for the CLI installs the wizard
# script runs. The installer doesn't reliably add ~/.bun/bin to PATH in
# non-interactive shells, so that's hooked into .bashrc explicitly here.
resource "coder_script" "install_bun" {
agent_id = coder_agent.main.id
display_name = "Install Bun"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
export BUN_INSTALL="$HOME/.bun"
if [ ! -x "$BUN_INSTALL/bin/bun" ]; then
curl -fsSL https://bun.sh/install | bash
fi
MARKER="# >>> coder bun path >>>"
if ! grep -qF "$MARKER" "$HOME/.bashrc" 2>/dev/null; then
{
echo ""
echo "$MARKER"
echo 'export BUN_INSTALL="$HOME/.bun"'
echo 'export PATH="$BUN_INSTALL/bin:$PATH"'
echo "# <<< coder bun path <<<"
} >> "$HOME/.bashrc"
fi
EOT
}
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
# Antigravity CLI's skills directories, plus the cobol-teacher skill from
# extensions/custom-specialty-plugin. See ../../scripts/install-skills.sh.
resource "coder_script" "install_skills" {
agent_id = coder_agent.main.id
display_name = "Install Agent Skills"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p /opt/coder
echo '${base64encode(file("${path.module}/../../scripts/install-skills.sh"))}' | base64 -d > /opt/coder/install-skills.sh
chmod +x /opt/coder/install-skills.sh
SPECIALTY_SKILLS="cobol-teacher" /opt/coder/install-skills.sh
EOT
}
+315
View File
@@ -0,0 +1,315 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
docker = {
source = "kreuzwerker/docker"
}
}
}
locals {
env_name = "Default"
profile = jsondecode(file("${path.module}/../../profile-templates/Default.code-profile"))
settings_raw = jsondecode(local.profile.settings).settings
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
}
variable "docker_socket" {
default = ""
description = "(Optional) Docker socket URI"
type = string
}
provider "docker" {
# Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default
host = var.docker_socket != "" ? var.docker_socket : null
}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
startup_script = <<-EOT
set -e
# Prepare user home with default files on first start.
if [ ! -f ~/.init_done ]; then
cp -rT /etc/skel ~
touch ~/.init_done
fi
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
EOT
# These environment variables allow you to make Git commits right away after creating a
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
# You can remove this block if you'd prefer to configure Git manually or using
# dotfiles. (see docs/dotfiles.md)
env = {
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
}
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
# For basic 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
# `extensions` is populated at template-push time from this env's
# profile-templates/*.code-profile file, so no interactive prompt is
# needed for VS Code extensions - Terraform handles it declaratively.
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
# Pass the target folder here natively
folder = "/home/coder/workspace"
extensions = local.extensions
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "~> 1.1"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
}
resource "docker_volume" "home_volume" {
name = "coder-${data.coder_workspace.me.id}-home"
# Protect the volume from being deleted due to changes in attributes.
lifecycle {
ignore_changes = all
}
# Add labels in Docker to keep track of orphan resources.
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
# This field becomes outdated if the workspace is renamed but can
# be useful for debugging or cleaning out dangling volumes.
labels {
label = "coder.workspace_name_at_creation"
value = data.coder_workspace.me.name
}
}
resource "docker_container" "workspace" {
count = data.coder_workspace.me.start_count
image = "codercom/enterprise-base:ubuntu"
# Uses lower() to avoid Docker restriction on container names.
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# Hostname makes the shell more user friendly: coder@my-workspace:~$
hostname = data.coder_workspace.me.name
# Use the docker gateway if the access URL is 127.0.0.1
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
host {
host = "host.docker.internal"
ip = "host-gateway"
}
host {
host = "code.octoturge.com"
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
}
labels {
label = "coder.profile"
value = local.env_name
}
}
# Writes this env's VS Code settings.json, sourced straight from the
# matching profile-templates/*.code-profile file at template-push time.
resource "coder_script" "apply_settings" {
agent_id = coder_agent.main.id
display_name = "Apply ${local.env_name} VS Code Settings"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p "$HOME/workspace"
mkdir -p "$HOME/.local/share/code-server/User"
echo '${base64encode(local.settings_raw)}' | base64 -d > "$HOME/.local/share/code-server/User/settings.json"
EOT
}
# Drops the shared CLI setup wizard onto the workspace and hooks it into
# every new interactive shell (via .bashrc) until the user completes it.
# See ../../scripts/cli-setup-wizard.sh for what it actually asks.
resource "coder_script" "cli_setup_wizard" {
agent_id = coder_agent.main.id
display_name = "Install CLI Setup Wizard"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p /opt/coder
echo '${base64encode(file("${path.module}/../../scripts/cli-setup-wizard.sh"))}' | base64 -d > /opt/coder/cli-setup-wizard.sh
chmod +x /opt/coder/cli-setup-wizard.sh
MARKER="# >>> coder cli setup wizard >>>"
if ! grep -qF "$MARKER" "$HOME/.bashrc" 2>/dev/null; then
{
echo ""
echo "$MARKER"
echo 'export PATH="$HOME/.local/bin:$PATH"'
echo 'source /opt/coder/cli-setup-wizard.sh'
echo "# <<< coder cli setup wizard <<<"
} >> "$HOME/.bashrc"
fi
EOT
}
# Installs Bun and uses it (instead of npm) for the CLI installs the wizard
# script runs. The installer doesn't reliably add ~/.bun/bin to PATH in
# non-interactive shells, so that's hooked into .bashrc explicitly here.
resource "coder_script" "install_bun" {
agent_id = coder_agent.main.id
display_name = "Install Bun"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
export BUN_INSTALL="$HOME/.bun"
if [ ! -x "$BUN_INSTALL/bin/bun" ]; then
curl -fsSL https://bun.sh/install | bash
fi
MARKER="# >>> coder bun path >>>"
if ! grep -qF "$MARKER" "$HOME/.bashrc" 2>/dev/null; then
{
echo ""
echo "$MARKER"
echo 'export BUN_INSTALL="$HOME/.bun"'
echo 'export PATH="$BUN_INSTALL/bin:$PATH"'
echo "# <<< coder bun path <<<"
} >> "$HOME/.bashrc"
fi
EOT
}
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
# Antigravity CLI's skills directories. See ../../scripts/install-skills.sh.
# Default has no matching entry in extensions/custom-specialty-plugin/skills,
# so it only gets the common awesome-skills-plugin bundle.
resource "coder_script" "install_skills" {
agent_id = coder_agent.main.id
display_name = "Install Agent Skills"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p /opt/coder
echo '${base64encode(file("${path.module}/../../scripts/install-skills.sh"))}' | base64 -d > /opt/coder/install-skills.sh
chmod +x /opt/coder/install-skills.sh
SPECIALTY_SKILLS="" /opt/coder/install-skills.sh
EOT
}
+315
View File
@@ -0,0 +1,315 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
docker = {
source = "kreuzwerker/docker"
}
}
}
locals {
env_name = "Python Engineering"
profile = jsondecode(file("${path.module}/../../profile-templates/Python.code-profile"))
settings_raw = jsondecode(local.profile.settings).settings
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
}
variable "docker_socket" {
default = ""
description = "(Optional) Docker socket URI"
type = string
}
provider "docker" {
# Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default
host = var.docker_socket != "" ? var.docker_socket : null
}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
startup_script = <<-EOT
set -e
# Prepare user home with default files on first start.
if [ ! -f ~/.init_done ]; then
cp -rT /etc/skel ~
touch ~/.init_done
fi
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
EOT
# These environment variables allow you to make Git commits right away after creating a
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
# You can remove this block if you'd prefer to configure Git manually or using
# dotfiles. (see docs/dotfiles.md)
env = {
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
}
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
# For basic 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
# `extensions` is populated at template-push time from this env's
# profile-templates/*.code-profile file, so no interactive prompt is
# needed for VS Code extensions - Terraform handles it declaratively.
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
# Pass the target folder here natively
folder = "/home/coder/workspace"
extensions = local.extensions
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "~> 1.1"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
}
resource "docker_volume" "home_volume" {
name = "coder-${data.coder_workspace.me.id}-home"
# Protect the volume from being deleted due to changes in attributes.
lifecycle {
ignore_changes = all
}
# Add labels in Docker to keep track of orphan resources.
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
# This field becomes outdated if the workspace is renamed but can
# be useful for debugging or cleaning out dangling volumes.
labels {
label = "coder.workspace_name_at_creation"
value = data.coder_workspace.me.name
}
}
resource "docker_container" "workspace" {
count = data.coder_workspace.me.start_count
image = "codercom/enterprise-base:ubuntu"
# Uses lower() to avoid Docker restriction on container names.
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# Hostname makes the shell more user friendly: coder@my-workspace:~$
hostname = data.coder_workspace.me.name
# Use the docker gateway if the access URL is 127.0.0.1
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
host {
host = "host.docker.internal"
ip = "host-gateway"
}
host {
host = "code.octoturge.com"
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
}
labels {
label = "coder.profile"
value = local.env_name
}
}
# Writes this env's VS Code settings.json, sourced straight from the
# matching profile-templates/*.code-profile file at template-push time.
resource "coder_script" "apply_settings" {
agent_id = coder_agent.main.id
display_name = "Apply ${local.env_name} VS Code Settings"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p "$HOME/workspace"
mkdir -p "$HOME/.local/share/code-server/User"
echo '${base64encode(local.settings_raw)}' | base64 -d > "$HOME/.local/share/code-server/User/settings.json"
EOT
}
# Drops the shared CLI setup wizard onto the workspace and hooks it into
# every new interactive shell (via .bashrc) until the user completes it.
# See ../../scripts/cli-setup-wizard.sh for what it actually asks.
resource "coder_script" "cli_setup_wizard" {
agent_id = coder_agent.main.id
display_name = "Install CLI Setup Wizard"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p /opt/coder
echo '${base64encode(file("${path.module}/../../scripts/cli-setup-wizard.sh"))}' | base64 -d > /opt/coder/cli-setup-wizard.sh
chmod +x /opt/coder/cli-setup-wizard.sh
MARKER="# >>> coder cli setup wizard >>>"
if ! grep -qF "$MARKER" "$HOME/.bashrc" 2>/dev/null; then
{
echo ""
echo "$MARKER"
echo 'export PATH="$HOME/.local/bin:$PATH"'
echo 'source /opt/coder/cli-setup-wizard.sh'
echo "# <<< coder cli setup wizard <<<"
} >> "$HOME/.bashrc"
fi
EOT
}
# Installs Bun and uses it (instead of npm) for the CLI installs the wizard
# script runs. The installer doesn't reliably add ~/.bun/bin to PATH in
# non-interactive shells, so that's hooked into .bashrc explicitly here.
resource "coder_script" "install_bun" {
agent_id = coder_agent.main.id
display_name = "Install Bun"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
export BUN_INSTALL="$HOME/.bun"
if [ ! -x "$BUN_INSTALL/bin/bun" ]; then
curl -fsSL https://bun.sh/install | bash
fi
MARKER="# >>> coder bun path >>>"
if ! grep -qF "$MARKER" "$HOME/.bashrc" 2>/dev/null; then
{
echo ""
echo "$MARKER"
echo 'export BUN_INSTALL="$HOME/.bun"'
echo 'export PATH="$BUN_INSTALL/bin:$PATH"'
echo "# <<< coder bun path <<<"
} >> "$HOME/.bashrc"
fi
EOT
}
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
# Antigravity CLI's skills directories. See ../../scripts/install-skills.sh.
# Python has no matching entry in extensions/custom-specialty-plugin/skills,
# so it only gets the common awesome-skills-plugin bundle.
resource "coder_script" "install_skills" {
agent_id = coder_agent.main.id
display_name = "Install Agent Skills"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p /opt/coder
echo '${base64encode(file("${path.module}/../../scripts/install-skills.sh"))}' | base64 -d > /opt/coder/install-skills.sh
chmod +x /opt/coder/install-skills.sh
SPECIALTY_SKILLS="" /opt/coder/install-skills.sh
EOT
}
+315
View File
@@ -0,0 +1,315 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
docker = {
source = "kreuzwerker/docker"
}
}
}
locals {
env_name = "TTRPG & Lore Building"
profile = jsondecode(file("${path.module}/../../profile-templates/TTRPG.code-profile"))
settings_raw = jsondecode(local.profile.settings).settings
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
}
variable "docker_socket" {
default = ""
description = "(Optional) Docker socket URI"
type = string
}
provider "docker" {
# Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default
host = var.docker_socket != "" ? var.docker_socket : null
}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
startup_script = <<-EOT
set -e
# Prepare user home with default files on first start.
if [ ! -f ~/.init_done ]; then
cp -rT /etc/skel ~
touch ~/.init_done
fi
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
EOT
# These environment variables allow you to make Git commits right away after creating a
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
# You can remove this block if you'd prefer to configure Git manually or using
# dotfiles. (see docs/dotfiles.md)
env = {
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
}
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
# For basic 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
# `extensions` is populated at template-push time from this env's
# profile-templates/*.code-profile file, so no interactive prompt is
# needed for VS Code extensions - Terraform handles it declaratively.
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
# Pass the target folder here natively
folder = "/home/coder/workspace"
extensions = local.extensions
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "~> 1.1"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
}
resource "docker_volume" "home_volume" {
name = "coder-${data.coder_workspace.me.id}-home"
# Protect the volume from being deleted due to changes in attributes.
lifecycle {
ignore_changes = all
}
# Add labels in Docker to keep track of orphan resources.
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
# This field becomes outdated if the workspace is renamed but can
# be useful for debugging or cleaning out dangling volumes.
labels {
label = "coder.workspace_name_at_creation"
value = data.coder_workspace.me.name
}
}
resource "docker_container" "workspace" {
count = data.coder_workspace.me.start_count
image = "codercom/enterprise-base:ubuntu"
# Uses lower() to avoid Docker restriction on container names.
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# Hostname makes the shell more user friendly: coder@my-workspace:~$
hostname = data.coder_workspace.me.name
# Use the docker gateway if the access URL is 127.0.0.1
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
host {
host = "host.docker.internal"
ip = "host-gateway"
}
host {
host = "code.octoturge.com"
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
}
labels {
label = "coder.profile"
value = local.env_name
}
}
# Writes this env's VS Code settings.json, sourced straight from the
# matching profile-templates/*.code-profile file at template-push time.
resource "coder_script" "apply_settings" {
agent_id = coder_agent.main.id
display_name = "Apply ${local.env_name} VS Code Settings"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p "$HOME/workspace"
mkdir -p "$HOME/.local/share/code-server/User"
echo '${base64encode(local.settings_raw)}' | base64 -d > "$HOME/.local/share/code-server/User/settings.json"
EOT
}
# Drops the shared CLI setup wizard onto the workspace and hooks it into
# every new interactive shell (via .bashrc) until the user completes it.
# See ../../scripts/cli-setup-wizard.sh for what it actually asks.
resource "coder_script" "cli_setup_wizard" {
agent_id = coder_agent.main.id
display_name = "Install CLI Setup Wizard"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p /opt/coder
echo '${base64encode(file("${path.module}/../../scripts/cli-setup-wizard.sh"))}' | base64 -d > /opt/coder/cli-setup-wizard.sh
chmod +x /opt/coder/cli-setup-wizard.sh
MARKER="# >>> coder cli setup wizard >>>"
if ! grep -qF "$MARKER" "$HOME/.bashrc" 2>/dev/null; then
{
echo ""
echo "$MARKER"
echo 'export PATH="$HOME/.local/bin:$PATH"'
echo 'source /opt/coder/cli-setup-wizard.sh'
echo "# <<< coder cli setup wizard <<<"
} >> "$HOME/.bashrc"
fi
EOT
}
# Installs Bun and uses it (instead of npm) for the CLI installs the wizard
# script runs. The installer doesn't reliably add ~/.bun/bin to PATH in
# non-interactive shells, so that's hooked into .bashrc explicitly here.
resource "coder_script" "install_bun" {
agent_id = coder_agent.main.id
display_name = "Install Bun"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
export BUN_INSTALL="$HOME/.bun"
if [ ! -x "$BUN_INSTALL/bin/bun" ]; then
curl -fsSL https://bun.sh/install | bash
fi
MARKER="# >>> coder bun path >>>"
if ! grep -qF "$MARKER" "$HOME/.bashrc" 2>/dev/null; then
{
echo ""
echo "$MARKER"
echo 'export BUN_INSTALL="$HOME/.bun"'
echo 'export PATH="$BUN_INSTALL/bin:$PATH"'
echo "# <<< coder bun path <<<"
} >> "$HOME/.bashrc"
fi
EOT
}
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
# Antigravity CLI's skills directories, plus the foundryvtt-modding and
# ttrpg-lore-weaver skills from extensions/custom-specialty-plugin.
# See ../../scripts/install-skills.sh.
resource "coder_script" "install_skills" {
agent_id = coder_agent.main.id
display_name = "Install Agent Skills"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p /opt/coder
echo '${base64encode(file("${path.module}/../../scripts/install-skills.sh"))}' | base64 -d > /opt/coder/install-skills.sh
chmod +x /opt/coder/install-skills.sh
SPECIALTY_SKILLS="foundryvtt-modding ttrpg-lore-weaver" /opt/coder/install-skills.sh
EOT
}
+315
View File
@@ -0,0 +1,315 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
docker = {
source = "kreuzwerker/docker"
}
}
}
locals {
env_name = "Web Applications"
profile = jsondecode(file("${path.module}/../../profile-templates/Web.code-profile"))
settings_raw = jsondecode(local.profile.settings).settings
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
}
variable "docker_socket" {
default = ""
description = "(Optional) Docker socket URI"
type = string
}
provider "docker" {
# Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default
host = var.docker_socket != "" ? var.docker_socket : null
}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
startup_script = <<-EOT
set -e
# Prepare user home with default files on first start.
if [ ! -f ~/.init_done ]; then
cp -rT /etc/skel ~
touch ~/.init_done
fi
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
EOT
# These environment variables allow you to make Git commits right away after creating a
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
# You can remove this block if you'd prefer to configure Git manually or using
# dotfiles. (see docs/dotfiles.md)
env = {
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
}
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
# For basic 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
# `extensions` is populated at template-push time from this env's
# profile-templates/*.code-profile file, so no interactive prompt is
# needed for VS Code extensions - Terraform handles it declaratively.
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
# Pass the target folder here natively
folder = "/home/coder/workspace"
extensions = local.extensions
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "~> 1.1"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
}
resource "docker_volume" "home_volume" {
name = "coder-${data.coder_workspace.me.id}-home"
# Protect the volume from being deleted due to changes in attributes.
lifecycle {
ignore_changes = all
}
# Add labels in Docker to keep track of orphan resources.
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
# This field becomes outdated if the workspace is renamed but can
# be useful for debugging or cleaning out dangling volumes.
labels {
label = "coder.workspace_name_at_creation"
value = data.coder_workspace.me.name
}
}
resource "docker_container" "workspace" {
count = data.coder_workspace.me.start_count
image = "codercom/enterprise-base:ubuntu"
# Uses lower() to avoid Docker restriction on container names.
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# Hostname makes the shell more user friendly: coder@my-workspace:~$
hostname = data.coder_workspace.me.name
# Use the docker gateway if the access URL is 127.0.0.1
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
host {
host = "host.docker.internal"
ip = "host-gateway"
}
host {
host = "code.octoturge.com"
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
}
labels {
label = "coder.profile"
value = local.env_name
}
}
# Writes this env's VS Code settings.json, sourced straight from the
# matching profile-templates/*.code-profile file at template-push time.
resource "coder_script" "apply_settings" {
agent_id = coder_agent.main.id
display_name = "Apply ${local.env_name} VS Code Settings"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p "$HOME/workspace"
mkdir -p "$HOME/.local/share/code-server/User"
echo '${base64encode(local.settings_raw)}' | base64 -d > "$HOME/.local/share/code-server/User/settings.json"
EOT
}
# Drops the shared CLI setup wizard onto the workspace and hooks it into
# every new interactive shell (via .bashrc) until the user completes it.
# See ../../scripts/cli-setup-wizard.sh for what it actually asks.
resource "coder_script" "cli_setup_wizard" {
agent_id = coder_agent.main.id
display_name = "Install CLI Setup Wizard"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p /opt/coder
echo '${base64encode(file("${path.module}/../../scripts/cli-setup-wizard.sh"))}' | base64 -d > /opt/coder/cli-setup-wizard.sh
chmod +x /opt/coder/cli-setup-wizard.sh
MARKER="# >>> coder cli setup wizard >>>"
if ! grep -qF "$MARKER" "$HOME/.bashrc" 2>/dev/null; then
{
echo ""
echo "$MARKER"
echo 'export PATH="$HOME/.local/bin:$PATH"'
echo 'source /opt/coder/cli-setup-wizard.sh'
echo "# <<< coder cli setup wizard <<<"
} >> "$HOME/.bashrc"
fi
EOT
}
# Installs Bun and uses it (instead of npm) for the CLI installs the wizard
# script runs. The installer doesn't reliably add ~/.bun/bin to PATH in
# non-interactive shells, so that's hooked into .bashrc explicitly here.
resource "coder_script" "install_bun" {
agent_id = coder_agent.main.id
display_name = "Install Bun"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
export BUN_INSTALL="$HOME/.bun"
if [ ! -x "$BUN_INSTALL/bin/bun" ]; then
curl -fsSL https://bun.sh/install | bash
fi
MARKER="# >>> coder bun path >>>"
if ! grep -qF "$MARKER" "$HOME/.bashrc" 2>/dev/null; then
{
echo ""
echo "$MARKER"
echo 'export BUN_INSTALL="$HOME/.bun"'
echo 'export PATH="$BUN_INSTALL/bin:$PATH"'
echo "# <<< coder bun path <<<"
} >> "$HOME/.bashrc"
fi
EOT
}
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
# Antigravity CLI's skills directories. See ../../scripts/install-skills.sh.
# Web has no matching entry in extensions/custom-specialty-plugin/skills,
# so it only gets the common awesome-skills-plugin bundle.
resource "coder_script" "install_skills" {
agent_id = coder_agent.main.id
display_name = "Install Agent Skills"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
mkdir -p /opt/coder
echo '${base64encode(file("${path.module}/../../scripts/install-skills.sh"))}' | base64 -d > /opt/coder/install-skills.sh
chmod +x /opt/coder/install-skills.sh
SPECIALTY_SKILLS="" /opt/coder/install-skills.sh
EOT
}