Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93721b9096 | |||
| 013567f02a | |||
| e8a8f93f9d | |||
| 557d6a3d84 | |||
| 354536dad5 | |||
| 1be7c00b84 | |||
| 52cc84650a | |||
| 0c53bb6554 | |||
| a302e21609 | |||
| 60e63edd61 | |||
| 22429aa4e3 | |||
| 5b32539d65 | |||
| 9424746305 | |||
| a5a760b24a | |||
| a9583535f0 | |||
| f6c432d21d | |||
| 1beaac6e4f |
@@ -0,0 +1,4 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.tf text eol=lf
|
||||||
|
*.code-profile -text
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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/**"
|
||||||
|
- ".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
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
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
|
||||||
|
# ROTATION_PAT secret holding a Gitea personal access token (write:repository
|
||||||
|
# scope, no expiration) with permission to write this repo's Actions secrets.
|
||||||
|
# Not named GITEA_ROTATION_TOKEN because Gitea reserves the GITEA_ prefix for
|
||||||
|
# its own automatic tokens/variables and rejects secrets with that prefix.
|
||||||
|
# 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 }}
|
||||||
|
ROTATION_PAT: ${{ secrets.ROTATION_PAT }}
|
||||||
|
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 ${ROTATION_PAT}" \
|
||||||
|
-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
|
||||||
@@ -1,2 +1,266 @@
|
|||||||
# Profiles-for-Coder
|
# 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
|
||||||
|
profile.code-profile # VS Code .code-profile export for this env
|
||||||
|
cli-setup-wizard.sh # first-run wizard, see below
|
||||||
|
install-skills.sh # Agent Skills installer, see below
|
||||||
|
3d-printing/ (same 4 files) # 3D Printing & Engineering
|
||||||
|
cobol/ (same 4 files) # COBOL Modern Mainframe
|
||||||
|
python/ (same 4 files) # Python Engineering
|
||||||
|
ttrpg/ (same 4 files) # TTRPG & Lore Building
|
||||||
|
web/ (same 4 files, plus Dockerfile) # Web Applications
|
||||||
|
extensions/ # Agent Skills bundles, installed per env (see below)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Applications: baked-in toolchain image
|
||||||
|
|
||||||
|
Every other template pulls `codercom/enterprise-base:ubuntu` straight from
|
||||||
|
Docker Hub and installs what little it needs (just Bun) via a `coder_script`
|
||||||
|
at workspace start. `templates/web/` doesn't do that: it needs a large,
|
||||||
|
slow-to-install native toolchain (Rust/rustup with cross targets, the Tauri 2
|
||||||
|
/ WebKit GUI dev libraries, Node + Bun + pnpm/yarn, Python with OpenCV/ONNX/
|
||||||
|
CPU-torch, Postgres/Redis/SQLite CLI clients, protoc, clang/llvm) that would
|
||||||
|
make every workspace start take many minutes if installed on the fly.
|
||||||
|
|
||||||
|
Instead `templates/web/main.tf` builds `templates/web/Dockerfile` at
|
||||||
|
apply/push time via the `docker` provider's `docker_image` resource (`build
|
||||||
|
{ context = path.module }`, same directory as `main.tf` so no `file()`
|
||||||
|
reaches outside the template per the constraint above) and runs the
|
||||||
|
container from that image instead of the enterprise-base one. The image tag
|
||||||
|
embeds `filesha1(Dockerfile)`, so editing the Dockerfile forces a rebuild on
|
||||||
|
the next apply/push while an unchanged Dockerfile reuses Docker's build
|
||||||
|
cache. Because the toolchain lives under `/home/coder` (rustup, cargo, bun),
|
||||||
|
and that path is a fresh *named* Docker volume on a workspace's first boot,
|
||||||
|
Docker's own "populate an empty volume from the image's directory contents"
|
||||||
|
behavior copies all of it into the persistent volume automatically - no
|
||||||
|
extra `coder_script` needed, matching how the `/etc/skel` copy in every
|
||||||
|
template's `startup_script` already relies on that same mechanism.
|
||||||
|
|
||||||
|
Each `templates/<env>/` is a complete, independent Coder template (agent,
|
||||||
|
docker container, code-server) with its own copy of everything `main.tf`
|
||||||
|
needs. They're deliberately not built from a shared Terraform
|
||||||
|
module or shared files elsewhere in the repo: `coder templates push -d
|
||||||
|
templates/<env>` only uploads that one directory to the Coder server, so any
|
||||||
|
`file()` reference reaching outside it (e.g. the old shared
|
||||||
|
`scripts/`/`profile-templates/` layout) fails at push/apply time with
|
||||||
|
`Invalid function argument: ... this function works only with files that
|
||||||
|
are distributed as part of the configuration source code` - discovered the
|
||||||
|
hard way once `coder-templates.yml` actually ran end-to-end. `main.tf`,
|
||||||
|
`profile.code-profile`, `cli-setup-wizard.sh`, and `install-skills.sh` are
|
||||||
|
duplicated per template rather than shared for this reason; only their
|
||||||
|
`locals` block and profile file differ meaningfully. 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
|
||||||
|
|
||||||
|
`templates/<env>/profile.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 except `web` 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). `web` instead bakes Bun
|
||||||
|
into its Dockerfile with `BUN_INSTALL`/`PATH` set as image `ENV` - see "Web
|
||||||
|
Applications: baked-in toolchain image" above. Either way, the CLI setup
|
||||||
|
wizard below uses `bun install -g <pkg>` instead of `npm install -g <pkg>`
|
||||||
|
for everything it installs.
|
||||||
|
|
||||||
|
### CLI setup wizard
|
||||||
|
|
||||||
|
`templates/<env>/cli-setup-wizard.sh` (identical across envs) 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`)
|
||||||
|
- **GitHub CLI** (`gh`, via the official apt repo, then `gh auth login`)
|
||||||
|
- **Gitea CLI** (`tea`, official binary release downloaded to `~/.local/bin`, then `tea login add`)
|
||||||
|
|
||||||
|
`git` and `gnupg` themselves aren't part of this opt-in flow - every
|
||||||
|
template's `coder_agent` startup script installs them unconditionally as
|
||||||
|
base packages (a no-op where they're already present, e.g. baked into
|
||||||
|
`templates/web`'s image). If the wizard just authenticated GitHub and/or
|
||||||
|
Gitea above (skipped entirely for "local git only" - neither set up), it
|
||||||
|
asks once more whether to auto-generate an ed25519 SSH key and an ed25519
|
||||||
|
GPG signing key and register them with whichever host(s) got set up: `gh
|
||||||
|
ssh-key add` / `gh gpg-key add` for GitHub, a direct call against Gitea's
|
||||||
|
`/api/v1/user/keys` and `/api/v1/user/gpg_keys` (using the token `tea
|
||||||
|
login add` already stored) for Gitea. Either upload failing just prints
|
||||||
|
the manual command/URL to finish it yourself - never blocks the rest of
|
||||||
|
the wizard.
|
||||||
|
|
||||||
|
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 its own copy of `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/**`:
|
||||||
|
|
||||||
|
- **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 its `profile.code-profile`,
|
||||||
|
`cli-setup-wizard.sh`, or `install-skills.sh`) -> 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 `ROTATION_PAT` (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 `ROTATION_PAT` as the secret name and the PAT itself as the
|
||||||
|
value). Not named `GITEA_ROTATION_TOKEN` - Gitea reserves the `GITEA_`
|
||||||
|
prefix for its own automatic tokens/variables and rejects secrets with
|
||||||
|
that prefix (`Error: invalid variable or secret name`).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,250 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
# Tracks whether the user actually ended up authenticated against GitHub
|
||||||
|
# and/or Gitea below, so the SSH/GPG key step can ask about exactly the
|
||||||
|
# host(s) in play (and stay silent - "local git only" - if neither).
|
||||||
|
DID_GITHUB=0
|
||||||
|
DID_GITEA=0
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- GitHub CLI (gh) ---
|
||||||
|
if command -v gh >/dev/null 2>&1; then
|
||||||
|
echo "GitHub CLI already installed, skipping."
|
||||||
|
else
|
||||||
|
if ask_yes_no "Install GitHub CLI (gh) and log in?"; then
|
||||||
|
if (sudo mkdir -p -m 755 /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg >/dev/null \
|
||||||
|
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||||
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null \
|
||||||
|
&& sudo apt-get update -qq && sudo apt-get install -y gh); then
|
||||||
|
gh auth login || echo "Install succeeded but login didn't complete. Retry any time with: gh auth login"
|
||||||
|
else
|
||||||
|
echo "GitHub CLI install failed. Retry later with: gh auth login (once gh is installed)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1 && DID_GITHUB=1
|
||||||
|
|
||||||
|
# --- Gitea CLI (tea) ---
|
||||||
|
if command -v tea >/dev/null 2>&1; then
|
||||||
|
echo "Gitea CLI already installed, skipping."
|
||||||
|
else
|
||||||
|
if ask_yes_no "Install Gitea CLI (tea) and log in?"; then
|
||||||
|
TEA_ARCH="$(uname -m)"
|
||||||
|
case "$TEA_ARCH" in
|
||||||
|
x86_64) TEA_ARCH="amd64" ;;
|
||||||
|
aarch64) TEA_ARCH="arm64" ;;
|
||||||
|
esac
|
||||||
|
TEA_VERSION="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/tea/releases/latest | grep -o '"tag_name":[^,]*' | grep -o 'v[0-9][^"]*')"
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
if [ -n "$TEA_VERSION" ] \
|
||||||
|
&& curl -fsSL "https://gitea.com/gitea/tea/releases/download/${TEA_VERSION}/tea-${TEA_VERSION#v}-linux-${TEA_ARCH}" -o "$HOME/.local/bin/tea" \
|
||||||
|
&& chmod +x "$HOME/.local/bin/tea"; then
|
||||||
|
echo "Add this Gitea instance now (e.g. https://git.octoturge.com)..."
|
||||||
|
tea login add || echo "Login didn't complete. Retry any time with: tea login add"
|
||||||
|
else
|
||||||
|
echo "Gitea CLI install failed. Retry later from: https://gitea.com/gitea/tea/releases"
|
||||||
|
rm -f "$HOME/.local/bin/tea"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
|
||||||
|
if command -v tea >/dev/null 2>&1 && [ -f "$TEA_CONFIG" ]; then
|
||||||
|
TEA_URL="$(awk '/^logins:/{f=1} f && /^ url:/{print $2; exit}' "$TEA_CONFIG")"
|
||||||
|
TEA_TOKEN="$(awk '/^logins:/{f=1} f && /^ token:/{print $2; exit}' "$TEA_CONFIG")"
|
||||||
|
[ -n "$TEA_URL" ] && [ -n "$TEA_TOKEN" ] && DID_GITEA=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- SSH + GPG keys for the external git host(s) selected above ---
|
||||||
|
# Only asks if the user actually set up GitHub and/or Gitea just now -
|
||||||
|
# stays silent for "local git only" (neither was set up).
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ] && [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
KEY_HOSTS_DESC="GitHub and Gitea"
|
||||||
|
elif [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
KEY_HOSTS_DESC="GitHub"
|
||||||
|
else
|
||||||
|
KEY_HOSTS_DESC="Gitea"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ask_yes_no "Auto-generate an SSH key and a GPG signing key, and register them with $KEY_HOSTS_DESC?"; then
|
||||||
|
KEY_NAME="${GIT_AUTHOR_NAME:-$(whoami)}"
|
||||||
|
KEY_EMAIL="${GIT_AUTHOR_EMAIL:-$(whoami)@$(hostname)}"
|
||||||
|
|
||||||
|
# SSH key: ed25519, no passphrase (disposable dev workspace convenience;
|
||||||
|
# add one manually afterwards with `ssh-keygen -p` if you want one).
|
||||||
|
SSH_KEY="$HOME/.ssh/id_ed25519"
|
||||||
|
if [ ! -f "$SSH_KEY" ]; then
|
||||||
|
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
|
||||||
|
ssh-keygen -t ed25519 -N "" -C "$KEY_EMAIL" -f "$SSH_KEY" -q
|
||||||
|
echo "Generated SSH key: ${SSH_KEY}.pub"
|
||||||
|
else
|
||||||
|
echo "SSH key already exists at ${SSH_KEY}.pub, reusing it."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
if gh ssh-key add "${SSH_KEY}.pub" --title "coder-$(hostname)" 2>/dev/null; then
|
||||||
|
echo "SSH key added to GitHub."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the SSH key to GitHub automatically (may already be added). Add manually: gh ssh-key add ${SSH_KEY}.pub"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
SSH_PUB_JSON="$(sed 's/\\/\\\\/g; s/"/\\"/g' "${SSH_KEY}.pub")"
|
||||||
|
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/keys" \
|
||||||
|
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"title\":\"coder-$(hostname)\",\"key\":\"${SSH_PUB_JSON}\"}" >/dev/null 2>&1; then
|
||||||
|
echo "SSH key added to Gitea."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# GPG key: ed25519 signing key, no passphrase, no expiry.
|
||||||
|
if gpg --list-secret-keys --with-colons "$KEY_EMAIL" 2>/dev/null | grep -q '^sec'; then
|
||||||
|
echo "GPG key for $KEY_EMAIL already exists, reusing it."
|
||||||
|
else
|
||||||
|
mkdir -p "$HOME/.gnupg" && chmod 700 "$HOME/.gnupg"
|
||||||
|
grep -qF "allow-loopback-pinentry" "$HOME/.gnupg/gpg-agent.conf" 2>/dev/null \
|
||||||
|
|| echo "allow-loopback-pinentry" >> "$HOME/.gnupg/gpg-agent.conf"
|
||||||
|
gpgconf --kill gpg-agent 2>/dev/null
|
||||||
|
if gpg --batch --pinentry-mode loopback --passphrase '' --quick-gen-key "$KEY_NAME <$KEY_EMAIL>" ed25519 sign 0 2>/dev/null; then
|
||||||
|
echo "Generated GPG signing key for $KEY_EMAIL."
|
||||||
|
else
|
||||||
|
echo "GPG key generation failed. Generate manually with: gpg --quick-gen-key \"$KEY_NAME <$KEY_EMAIL>\" ed25519 sign 0"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
GPG_KEY_ID="$(gpg --list-secret-keys --with-colons "$KEY_EMAIL" 2>/dev/null | awk -F: '/^sec/{print $5; exit}')"
|
||||||
|
|
||||||
|
if [ -n "$GPG_KEY_ID" ]; then
|
||||||
|
git config --global user.signingkey "$GPG_KEY_ID"
|
||||||
|
git config --global commit.gpgsign true
|
||||||
|
echo "Configured git to sign commits with this key."
|
||||||
|
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
if gpg --armor --export "$GPG_KEY_ID" | gh gpg-key add - 2>/dev/null; then
|
||||||
|
echo "GPG key added to GitHub."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the GPG key to GitHub automatically (may already be added). Add manually: gpg --armor --export $GPG_KEY_ID | gh gpg-key add -"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
GPG_ARMORED_JSON="$(gpg --armor --export "$GPG_KEY_ID" | awk '{printf "%s\\n", $0}')"
|
||||||
|
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/gpg_keys" \
|
||||||
|
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"armored_public_key\":\"${GPG_ARMORED_JSON}\"}" >/dev/null 2>&1; then
|
||||||
|
echo "GPG key added to Gitea."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
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
|
||||||
@@ -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."
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
}
|
||||||
|
docker = {
|
||||||
|
source = "kreuzwerker/docker"
|
||||||
|
}
|
||||||
|
# Not used directly in this config. Existing workspace state from before
|
||||||
|
# the jetbrains module was removed still has resources tagged under this
|
||||||
|
# provider (the module used it internally to fetch IDE metadata) -
|
||||||
|
# terraform init only installs providers the current config declares, so
|
||||||
|
# without this, plan/apply fails with "Missing required provider" while
|
||||||
|
# trying to reconcile/destroy those leftover state entries. Safe to drop
|
||||||
|
# once every workspace has updated past the jetbrains-module version.
|
||||||
|
http = {
|
||||||
|
source = "hashicorp/http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
env_name = "3D Printing & Engineering"
|
||||||
|
profile = jsondecode(file("${path.module}/profile.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
|
||||||
|
|
||||||
|
# Ensure git and gnupg (commit signing) are present as base packages -
|
||||||
|
# not every base image ships gnupg by default. No-op once both are
|
||||||
|
# present (e.g. templates/web already bakes them into its image).
|
||||||
|
if ! command -v git >/dev/null 2>&1 || ! command -v gpg >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y --no-install-recommends git gnupg
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ./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
|
||||||
|
sudo mkdir -p /opt/coder
|
||||||
|
sudo chown "$(id -u):$(id -g)" /opt/coder
|
||||||
|
echo '${base64encode(file("${path.module}/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 ./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
|
||||||
|
sudo mkdir -p /opt/coder
|
||||||
|
sudo chown "$(id -u):$(id -g)" /opt/coder
|
||||||
|
echo '${base64encode(file("${path.module}/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
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,250 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
# Tracks whether the user actually ended up authenticated against GitHub
|
||||||
|
# and/or Gitea below, so the SSH/GPG key step can ask about exactly the
|
||||||
|
# host(s) in play (and stay silent - "local git only" - if neither).
|
||||||
|
DID_GITHUB=0
|
||||||
|
DID_GITEA=0
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- GitHub CLI (gh) ---
|
||||||
|
if command -v gh >/dev/null 2>&1; then
|
||||||
|
echo "GitHub CLI already installed, skipping."
|
||||||
|
else
|
||||||
|
if ask_yes_no "Install GitHub CLI (gh) and log in?"; then
|
||||||
|
if (sudo mkdir -p -m 755 /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg >/dev/null \
|
||||||
|
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||||
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null \
|
||||||
|
&& sudo apt-get update -qq && sudo apt-get install -y gh); then
|
||||||
|
gh auth login || echo "Install succeeded but login didn't complete. Retry any time with: gh auth login"
|
||||||
|
else
|
||||||
|
echo "GitHub CLI install failed. Retry later with: gh auth login (once gh is installed)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1 && DID_GITHUB=1
|
||||||
|
|
||||||
|
# --- Gitea CLI (tea) ---
|
||||||
|
if command -v tea >/dev/null 2>&1; then
|
||||||
|
echo "Gitea CLI already installed, skipping."
|
||||||
|
else
|
||||||
|
if ask_yes_no "Install Gitea CLI (tea) and log in?"; then
|
||||||
|
TEA_ARCH="$(uname -m)"
|
||||||
|
case "$TEA_ARCH" in
|
||||||
|
x86_64) TEA_ARCH="amd64" ;;
|
||||||
|
aarch64) TEA_ARCH="arm64" ;;
|
||||||
|
esac
|
||||||
|
TEA_VERSION="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/tea/releases/latest | grep -o '"tag_name":[^,]*' | grep -o 'v[0-9][^"]*')"
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
if [ -n "$TEA_VERSION" ] \
|
||||||
|
&& curl -fsSL "https://gitea.com/gitea/tea/releases/download/${TEA_VERSION}/tea-${TEA_VERSION#v}-linux-${TEA_ARCH}" -o "$HOME/.local/bin/tea" \
|
||||||
|
&& chmod +x "$HOME/.local/bin/tea"; then
|
||||||
|
echo "Add this Gitea instance now (e.g. https://git.octoturge.com)..."
|
||||||
|
tea login add || echo "Login didn't complete. Retry any time with: tea login add"
|
||||||
|
else
|
||||||
|
echo "Gitea CLI install failed. Retry later from: https://gitea.com/gitea/tea/releases"
|
||||||
|
rm -f "$HOME/.local/bin/tea"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
|
||||||
|
if command -v tea >/dev/null 2>&1 && [ -f "$TEA_CONFIG" ]; then
|
||||||
|
TEA_URL="$(awk '/^logins:/{f=1} f && /^ url:/{print $2; exit}' "$TEA_CONFIG")"
|
||||||
|
TEA_TOKEN="$(awk '/^logins:/{f=1} f && /^ token:/{print $2; exit}' "$TEA_CONFIG")"
|
||||||
|
[ -n "$TEA_URL" ] && [ -n "$TEA_TOKEN" ] && DID_GITEA=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- SSH + GPG keys for the external git host(s) selected above ---
|
||||||
|
# Only asks if the user actually set up GitHub and/or Gitea just now -
|
||||||
|
# stays silent for "local git only" (neither was set up).
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ] && [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
KEY_HOSTS_DESC="GitHub and Gitea"
|
||||||
|
elif [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
KEY_HOSTS_DESC="GitHub"
|
||||||
|
else
|
||||||
|
KEY_HOSTS_DESC="Gitea"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ask_yes_no "Auto-generate an SSH key and a GPG signing key, and register them with $KEY_HOSTS_DESC?"; then
|
||||||
|
KEY_NAME="${GIT_AUTHOR_NAME:-$(whoami)}"
|
||||||
|
KEY_EMAIL="${GIT_AUTHOR_EMAIL:-$(whoami)@$(hostname)}"
|
||||||
|
|
||||||
|
# SSH key: ed25519, no passphrase (disposable dev workspace convenience;
|
||||||
|
# add one manually afterwards with `ssh-keygen -p` if you want one).
|
||||||
|
SSH_KEY="$HOME/.ssh/id_ed25519"
|
||||||
|
if [ ! -f "$SSH_KEY" ]; then
|
||||||
|
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
|
||||||
|
ssh-keygen -t ed25519 -N "" -C "$KEY_EMAIL" -f "$SSH_KEY" -q
|
||||||
|
echo "Generated SSH key: ${SSH_KEY}.pub"
|
||||||
|
else
|
||||||
|
echo "SSH key already exists at ${SSH_KEY}.pub, reusing it."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
if gh ssh-key add "${SSH_KEY}.pub" --title "coder-$(hostname)" 2>/dev/null; then
|
||||||
|
echo "SSH key added to GitHub."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the SSH key to GitHub automatically (may already be added). Add manually: gh ssh-key add ${SSH_KEY}.pub"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
SSH_PUB_JSON="$(sed 's/\\/\\\\/g; s/"/\\"/g' "${SSH_KEY}.pub")"
|
||||||
|
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/keys" \
|
||||||
|
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"title\":\"coder-$(hostname)\",\"key\":\"${SSH_PUB_JSON}\"}" >/dev/null 2>&1; then
|
||||||
|
echo "SSH key added to Gitea."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# GPG key: ed25519 signing key, no passphrase, no expiry.
|
||||||
|
if gpg --list-secret-keys --with-colons "$KEY_EMAIL" 2>/dev/null | grep -q '^sec'; then
|
||||||
|
echo "GPG key for $KEY_EMAIL already exists, reusing it."
|
||||||
|
else
|
||||||
|
mkdir -p "$HOME/.gnupg" && chmod 700 "$HOME/.gnupg"
|
||||||
|
grep -qF "allow-loopback-pinentry" "$HOME/.gnupg/gpg-agent.conf" 2>/dev/null \
|
||||||
|
|| echo "allow-loopback-pinentry" >> "$HOME/.gnupg/gpg-agent.conf"
|
||||||
|
gpgconf --kill gpg-agent 2>/dev/null
|
||||||
|
if gpg --batch --pinentry-mode loopback --passphrase '' --quick-gen-key "$KEY_NAME <$KEY_EMAIL>" ed25519 sign 0 2>/dev/null; then
|
||||||
|
echo "Generated GPG signing key for $KEY_EMAIL."
|
||||||
|
else
|
||||||
|
echo "GPG key generation failed. Generate manually with: gpg --quick-gen-key \"$KEY_NAME <$KEY_EMAIL>\" ed25519 sign 0"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
GPG_KEY_ID="$(gpg --list-secret-keys --with-colons "$KEY_EMAIL" 2>/dev/null | awk -F: '/^sec/{print $5; exit}')"
|
||||||
|
|
||||||
|
if [ -n "$GPG_KEY_ID" ]; then
|
||||||
|
git config --global user.signingkey "$GPG_KEY_ID"
|
||||||
|
git config --global commit.gpgsign true
|
||||||
|
echo "Configured git to sign commits with this key."
|
||||||
|
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
if gpg --armor --export "$GPG_KEY_ID" | gh gpg-key add - 2>/dev/null; then
|
||||||
|
echo "GPG key added to GitHub."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the GPG key to GitHub automatically (may already be added). Add manually: gpg --armor --export $GPG_KEY_ID | gh gpg-key add -"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
GPG_ARMORED_JSON="$(gpg --armor --export "$GPG_KEY_ID" | awk '{printf "%s\\n", $0}')"
|
||||||
|
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/gpg_keys" \
|
||||||
|
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"armored_public_key\":\"${GPG_ARMORED_JSON}\"}" >/dev/null 2>&1; then
|
||||||
|
echo "GPG key added to Gitea."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
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
|
||||||
@@ -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."
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
}
|
||||||
|
docker = {
|
||||||
|
source = "kreuzwerker/docker"
|
||||||
|
}
|
||||||
|
# Not used directly in this config. Existing workspace state from before
|
||||||
|
# the jetbrains module was removed still has resources tagged under this
|
||||||
|
# provider (the module used it internally to fetch IDE metadata) -
|
||||||
|
# terraform init only installs providers the current config declares, so
|
||||||
|
# without this, plan/apply fails with "Missing required provider" while
|
||||||
|
# trying to reconcile/destroy those leftover state entries. Safe to drop
|
||||||
|
# once every workspace has updated past the jetbrains-module version.
|
||||||
|
http = {
|
||||||
|
source = "hashicorp/http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
env_name = "COBOL Modern Mainframe"
|
||||||
|
profile = jsondecode(file("${path.module}/profile.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
|
||||||
|
|
||||||
|
# Ensure git and gnupg (commit signing) are present as base packages -
|
||||||
|
# not every base image ships gnupg by default. No-op once both are
|
||||||
|
# present (e.g. templates/web already bakes them into its image).
|
||||||
|
if ! command -v git >/dev/null 2>&1 || ! command -v gpg >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y --no-install-recommends git gnupg
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ./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
|
||||||
|
sudo mkdir -p /opt/coder
|
||||||
|
sudo chown "$(id -u):$(id -g)" /opt/coder
|
||||||
|
echo '${base64encode(file("${path.module}/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 ./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
|
||||||
|
sudo mkdir -p /opt/coder
|
||||||
|
sudo chown "$(id -u):$(id -g)" /opt/coder
|
||||||
|
echo '${base64encode(file("${path.module}/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
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,250 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
# Tracks whether the user actually ended up authenticated against GitHub
|
||||||
|
# and/or Gitea below, so the SSH/GPG key step can ask about exactly the
|
||||||
|
# host(s) in play (and stay silent - "local git only" - if neither).
|
||||||
|
DID_GITHUB=0
|
||||||
|
DID_GITEA=0
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- GitHub CLI (gh) ---
|
||||||
|
if command -v gh >/dev/null 2>&1; then
|
||||||
|
echo "GitHub CLI already installed, skipping."
|
||||||
|
else
|
||||||
|
if ask_yes_no "Install GitHub CLI (gh) and log in?"; then
|
||||||
|
if (sudo mkdir -p -m 755 /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg >/dev/null \
|
||||||
|
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||||
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null \
|
||||||
|
&& sudo apt-get update -qq && sudo apt-get install -y gh); then
|
||||||
|
gh auth login || echo "Install succeeded but login didn't complete. Retry any time with: gh auth login"
|
||||||
|
else
|
||||||
|
echo "GitHub CLI install failed. Retry later with: gh auth login (once gh is installed)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1 && DID_GITHUB=1
|
||||||
|
|
||||||
|
# --- Gitea CLI (tea) ---
|
||||||
|
if command -v tea >/dev/null 2>&1; then
|
||||||
|
echo "Gitea CLI already installed, skipping."
|
||||||
|
else
|
||||||
|
if ask_yes_no "Install Gitea CLI (tea) and log in?"; then
|
||||||
|
TEA_ARCH="$(uname -m)"
|
||||||
|
case "$TEA_ARCH" in
|
||||||
|
x86_64) TEA_ARCH="amd64" ;;
|
||||||
|
aarch64) TEA_ARCH="arm64" ;;
|
||||||
|
esac
|
||||||
|
TEA_VERSION="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/tea/releases/latest | grep -o '"tag_name":[^,]*' | grep -o 'v[0-9][^"]*')"
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
if [ -n "$TEA_VERSION" ] \
|
||||||
|
&& curl -fsSL "https://gitea.com/gitea/tea/releases/download/${TEA_VERSION}/tea-${TEA_VERSION#v}-linux-${TEA_ARCH}" -o "$HOME/.local/bin/tea" \
|
||||||
|
&& chmod +x "$HOME/.local/bin/tea"; then
|
||||||
|
echo "Add this Gitea instance now (e.g. https://git.octoturge.com)..."
|
||||||
|
tea login add || echo "Login didn't complete. Retry any time with: tea login add"
|
||||||
|
else
|
||||||
|
echo "Gitea CLI install failed. Retry later from: https://gitea.com/gitea/tea/releases"
|
||||||
|
rm -f "$HOME/.local/bin/tea"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
|
||||||
|
if command -v tea >/dev/null 2>&1 && [ -f "$TEA_CONFIG" ]; then
|
||||||
|
TEA_URL="$(awk '/^logins:/{f=1} f && /^ url:/{print $2; exit}' "$TEA_CONFIG")"
|
||||||
|
TEA_TOKEN="$(awk '/^logins:/{f=1} f && /^ token:/{print $2; exit}' "$TEA_CONFIG")"
|
||||||
|
[ -n "$TEA_URL" ] && [ -n "$TEA_TOKEN" ] && DID_GITEA=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- SSH + GPG keys for the external git host(s) selected above ---
|
||||||
|
# Only asks if the user actually set up GitHub and/or Gitea just now -
|
||||||
|
# stays silent for "local git only" (neither was set up).
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ] && [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
KEY_HOSTS_DESC="GitHub and Gitea"
|
||||||
|
elif [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
KEY_HOSTS_DESC="GitHub"
|
||||||
|
else
|
||||||
|
KEY_HOSTS_DESC="Gitea"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ask_yes_no "Auto-generate an SSH key and a GPG signing key, and register them with $KEY_HOSTS_DESC?"; then
|
||||||
|
KEY_NAME="${GIT_AUTHOR_NAME:-$(whoami)}"
|
||||||
|
KEY_EMAIL="${GIT_AUTHOR_EMAIL:-$(whoami)@$(hostname)}"
|
||||||
|
|
||||||
|
# SSH key: ed25519, no passphrase (disposable dev workspace convenience;
|
||||||
|
# add one manually afterwards with `ssh-keygen -p` if you want one).
|
||||||
|
SSH_KEY="$HOME/.ssh/id_ed25519"
|
||||||
|
if [ ! -f "$SSH_KEY" ]; then
|
||||||
|
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
|
||||||
|
ssh-keygen -t ed25519 -N "" -C "$KEY_EMAIL" -f "$SSH_KEY" -q
|
||||||
|
echo "Generated SSH key: ${SSH_KEY}.pub"
|
||||||
|
else
|
||||||
|
echo "SSH key already exists at ${SSH_KEY}.pub, reusing it."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
if gh ssh-key add "${SSH_KEY}.pub" --title "coder-$(hostname)" 2>/dev/null; then
|
||||||
|
echo "SSH key added to GitHub."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the SSH key to GitHub automatically (may already be added). Add manually: gh ssh-key add ${SSH_KEY}.pub"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
SSH_PUB_JSON="$(sed 's/\\/\\\\/g; s/"/\\"/g' "${SSH_KEY}.pub")"
|
||||||
|
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/keys" \
|
||||||
|
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"title\":\"coder-$(hostname)\",\"key\":\"${SSH_PUB_JSON}\"}" >/dev/null 2>&1; then
|
||||||
|
echo "SSH key added to Gitea."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# GPG key: ed25519 signing key, no passphrase, no expiry.
|
||||||
|
if gpg --list-secret-keys --with-colons "$KEY_EMAIL" 2>/dev/null | grep -q '^sec'; then
|
||||||
|
echo "GPG key for $KEY_EMAIL already exists, reusing it."
|
||||||
|
else
|
||||||
|
mkdir -p "$HOME/.gnupg" && chmod 700 "$HOME/.gnupg"
|
||||||
|
grep -qF "allow-loopback-pinentry" "$HOME/.gnupg/gpg-agent.conf" 2>/dev/null \
|
||||||
|
|| echo "allow-loopback-pinentry" >> "$HOME/.gnupg/gpg-agent.conf"
|
||||||
|
gpgconf --kill gpg-agent 2>/dev/null
|
||||||
|
if gpg --batch --pinentry-mode loopback --passphrase '' --quick-gen-key "$KEY_NAME <$KEY_EMAIL>" ed25519 sign 0 2>/dev/null; then
|
||||||
|
echo "Generated GPG signing key for $KEY_EMAIL."
|
||||||
|
else
|
||||||
|
echo "GPG key generation failed. Generate manually with: gpg --quick-gen-key \"$KEY_NAME <$KEY_EMAIL>\" ed25519 sign 0"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
GPG_KEY_ID="$(gpg --list-secret-keys --with-colons "$KEY_EMAIL" 2>/dev/null | awk -F: '/^sec/{print $5; exit}')"
|
||||||
|
|
||||||
|
if [ -n "$GPG_KEY_ID" ]; then
|
||||||
|
git config --global user.signingkey "$GPG_KEY_ID"
|
||||||
|
git config --global commit.gpgsign true
|
||||||
|
echo "Configured git to sign commits with this key."
|
||||||
|
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
if gpg --armor --export "$GPG_KEY_ID" | gh gpg-key add - 2>/dev/null; then
|
||||||
|
echo "GPG key added to GitHub."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the GPG key to GitHub automatically (may already be added). Add manually: gpg --armor --export $GPG_KEY_ID | gh gpg-key add -"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
GPG_ARMORED_JSON="$(gpg --armor --export "$GPG_KEY_ID" | awk '{printf "%s\\n", $0}')"
|
||||||
|
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/gpg_keys" \
|
||||||
|
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"armored_public_key\":\"${GPG_ARMORED_JSON}\"}" >/dev/null 2>&1; then
|
||||||
|
echo "GPG key added to Gitea."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
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
|
||||||
@@ -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."
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
}
|
||||||
|
docker = {
|
||||||
|
source = "kreuzwerker/docker"
|
||||||
|
}
|
||||||
|
# Not used directly in this config. Existing workspace state from before
|
||||||
|
# the jetbrains module was removed still has resources tagged under this
|
||||||
|
# provider (the module used it internally to fetch IDE metadata) -
|
||||||
|
# terraform init only installs providers the current config declares, so
|
||||||
|
# without this, plan/apply fails with "Missing required provider" while
|
||||||
|
# trying to reconcile/destroy those leftover state entries. Safe to drop
|
||||||
|
# once every workspace has updated past the jetbrains-module version.
|
||||||
|
http = {
|
||||||
|
source = "hashicorp/http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
env_name = "Default"
|
||||||
|
profile = jsondecode(file("${path.module}/profile.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
|
||||||
|
|
||||||
|
# Ensure git and gnupg (commit signing) are present as base packages -
|
||||||
|
# not every base image ships gnupg by default. No-op once both are
|
||||||
|
# present (e.g. templates/web already bakes them into its image).
|
||||||
|
if ! command -v git >/dev/null 2>&1 || ! command -v gpg >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y --no-install-recommends git gnupg
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ./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
|
||||||
|
sudo mkdir -p /opt/coder
|
||||||
|
sudo chown "$(id -u):$(id -g)" /opt/coder
|
||||||
|
echo '${base64encode(file("${path.module}/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 ./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
|
||||||
|
sudo mkdir -p /opt/coder
|
||||||
|
sudo chown "$(id -u):$(id -g)" /opt/coder
|
||||||
|
echo '${base64encode(file("${path.module}/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
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
# Tracks whether the user actually ended up authenticated against GitHub
|
||||||
|
# and/or Gitea below, so the SSH/GPG key step can ask about exactly the
|
||||||
|
# host(s) in play (and stay silent - "local git only" - if neither).
|
||||||
|
DID_GITHUB=0
|
||||||
|
DID_GITEA=0
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- GitHub CLI (gh) ---
|
||||||
|
if command -v gh >/dev/null 2>&1; then
|
||||||
|
echo "GitHub CLI already installed, skipping."
|
||||||
|
else
|
||||||
|
if ask_yes_no "Install GitHub CLI (gh) and log in?"; then
|
||||||
|
if (sudo mkdir -p -m 755 /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg >/dev/null \
|
||||||
|
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||||
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null \
|
||||||
|
&& sudo apt-get update -qq && sudo apt-get install -y gh); then
|
||||||
|
gh auth login || echo "Install succeeded but login didn't complete. Retry any time with: gh auth login"
|
||||||
|
else
|
||||||
|
echo "GitHub CLI install failed. Retry later with: gh auth login (once gh is installed)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1 && DID_GITHUB=1
|
||||||
|
|
||||||
|
# --- Gitea CLI (tea) ---
|
||||||
|
if command -v tea >/dev/null 2>&1; then
|
||||||
|
echo "Gitea CLI already installed, skipping."
|
||||||
|
else
|
||||||
|
if ask_yes_no "Install Gitea CLI (tea) and log in?"; then
|
||||||
|
TEA_ARCH="$(uname -m)"
|
||||||
|
case "$TEA_ARCH" in
|
||||||
|
x86_64) TEA_ARCH="amd64" ;;
|
||||||
|
aarch64) TEA_ARCH="arm64" ;;
|
||||||
|
esac
|
||||||
|
TEA_VERSION="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/tea/releases/latest | grep -o '"tag_name":[^,]*' | grep -o 'v[0-9][^"]*')"
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
if [ -n "$TEA_VERSION" ] \
|
||||||
|
&& curl -fsSL "https://gitea.com/gitea/tea/releases/download/${TEA_VERSION}/tea-${TEA_VERSION#v}-linux-${TEA_ARCH}" -o "$HOME/.local/bin/tea" \
|
||||||
|
&& chmod +x "$HOME/.local/bin/tea"; then
|
||||||
|
echo "Add this Gitea instance now (e.g. https://git.octoturge.com)..."
|
||||||
|
tea login add || echo "Login didn't complete. Retry any time with: tea login add"
|
||||||
|
else
|
||||||
|
echo "Gitea CLI install failed. Retry later from: https://gitea.com/gitea/tea/releases"
|
||||||
|
rm -f "$HOME/.local/bin/tea"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
|
||||||
|
if command -v tea >/dev/null 2>&1 && [ -f "$TEA_CONFIG" ]; then
|
||||||
|
TEA_URL="$(awk '/^logins:/{f=1} f && /^ url:/{print $2; exit}' "$TEA_CONFIG")"
|
||||||
|
TEA_TOKEN="$(awk '/^logins:/{f=1} f && /^ token:/{print $2; exit}' "$TEA_CONFIG")"
|
||||||
|
[ -n "$TEA_URL" ] && [ -n "$TEA_TOKEN" ] && DID_GITEA=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- SSH + GPG keys for the external git host(s) selected above ---
|
||||||
|
# Only asks if the user actually set up GitHub and/or Gitea just now -
|
||||||
|
# stays silent for "local git only" (neither was set up).
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ] && [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
KEY_HOSTS_DESC="GitHub and Gitea"
|
||||||
|
elif [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
KEY_HOSTS_DESC="GitHub"
|
||||||
|
else
|
||||||
|
KEY_HOSTS_DESC="Gitea"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ask_yes_no "Auto-generate an SSH key and a GPG signing key, and register them with $KEY_HOSTS_DESC?"; then
|
||||||
|
KEY_NAME="${GIT_AUTHOR_NAME:-$(whoami)}"
|
||||||
|
KEY_EMAIL="${GIT_AUTHOR_EMAIL:-$(whoami)@$(hostname)}"
|
||||||
|
|
||||||
|
# SSH key: ed25519, no passphrase (disposable dev workspace convenience;
|
||||||
|
# add one manually afterwards with `ssh-keygen -p` if you want one).
|
||||||
|
SSH_KEY="$HOME/.ssh/id_ed25519"
|
||||||
|
if [ ! -f "$SSH_KEY" ]; then
|
||||||
|
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
|
||||||
|
ssh-keygen -t ed25519 -N "" -C "$KEY_EMAIL" -f "$SSH_KEY" -q
|
||||||
|
echo "Generated SSH key: ${SSH_KEY}.pub"
|
||||||
|
else
|
||||||
|
echo "SSH key already exists at ${SSH_KEY}.pub, reusing it."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
if gh ssh-key add "${SSH_KEY}.pub" --title "coder-$(hostname)" 2>/dev/null; then
|
||||||
|
echo "SSH key added to GitHub."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the SSH key to GitHub automatically (may already be added). Add manually: gh ssh-key add ${SSH_KEY}.pub"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
SSH_PUB_JSON="$(sed 's/\\/\\\\/g; s/"/\\"/g' "${SSH_KEY}.pub")"
|
||||||
|
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/keys" \
|
||||||
|
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"title\":\"coder-$(hostname)\",\"key\":\"${SSH_PUB_JSON}\"}" >/dev/null 2>&1; then
|
||||||
|
echo "SSH key added to Gitea."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# GPG key: ed25519 signing key, no passphrase, no expiry.
|
||||||
|
if gpg --list-secret-keys --with-colons "$KEY_EMAIL" 2>/dev/null | grep -q '^sec'; then
|
||||||
|
echo "GPG key for $KEY_EMAIL already exists, reusing it."
|
||||||
|
else
|
||||||
|
mkdir -p "$HOME/.gnupg" && chmod 700 "$HOME/.gnupg"
|
||||||
|
grep -qF "allow-loopback-pinentry" "$HOME/.gnupg/gpg-agent.conf" 2>/dev/null \
|
||||||
|
|| echo "allow-loopback-pinentry" >> "$HOME/.gnupg/gpg-agent.conf"
|
||||||
|
gpgconf --kill gpg-agent 2>/dev/null
|
||||||
|
if gpg --batch --pinentry-mode loopback --passphrase '' --quick-gen-key "$KEY_NAME <$KEY_EMAIL>" ed25519 sign 0 2>/dev/null; then
|
||||||
|
echo "Generated GPG signing key for $KEY_EMAIL."
|
||||||
|
else
|
||||||
|
echo "GPG key generation failed. Generate manually with: gpg --quick-gen-key \"$KEY_NAME <$KEY_EMAIL>\" ed25519 sign 0"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
GPG_KEY_ID="$(gpg --list-secret-keys --with-colons "$KEY_EMAIL" 2>/dev/null | awk -F: '/^sec/{print $5; exit}')"
|
||||||
|
|
||||||
|
if [ -n "$GPG_KEY_ID" ]; then
|
||||||
|
git config --global user.signingkey "$GPG_KEY_ID"
|
||||||
|
git config --global commit.gpgsign true
|
||||||
|
echo "Configured git to sign commits with this key."
|
||||||
|
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
if gpg --armor --export "$GPG_KEY_ID" | gh gpg-key add - 2>/dev/null; then
|
||||||
|
echo "GPG key added to GitHub."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the GPG key to GitHub automatically (may already be added). Add manually: gpg --armor --export $GPG_KEY_ID | gh gpg-key add -"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
GPG_ARMORED_JSON="$(gpg --armor --export "$GPG_KEY_ID" | awk '{printf "%s\\n", $0}')"
|
||||||
|
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/gpg_keys" \
|
||||||
|
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"armored_public_key\":\"${GPG_ARMORED_JSON}\"}" >/dev/null 2>&1; then
|
||||||
|
echo "GPG key added to Gitea."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
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
|
||||||
@@ -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."
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
}
|
||||||
|
docker = {
|
||||||
|
source = "kreuzwerker/docker"
|
||||||
|
}
|
||||||
|
# Not used directly in this config. Existing workspace state from before
|
||||||
|
# the jetbrains module was removed still has resources tagged under this
|
||||||
|
# provider (the module used it internally to fetch IDE metadata) -
|
||||||
|
# terraform init only installs providers the current config declares, so
|
||||||
|
# without this, plan/apply fails with "Missing required provider" while
|
||||||
|
# trying to reconcile/destroy those leftover state entries. Safe to drop
|
||||||
|
# once every workspace has updated past the jetbrains-module version.
|
||||||
|
http = {
|
||||||
|
source = "hashicorp/http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
env_name = "Python Engineering"
|
||||||
|
profile = jsondecode(file("${path.module}/profile.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
|
||||||
|
|
||||||
|
# Ensure git and gnupg (commit signing) are present as base packages -
|
||||||
|
# not every base image ships gnupg by default. No-op once both are
|
||||||
|
# present (e.g. templates/web already bakes them into its image).
|
||||||
|
if ! command -v git >/dev/null 2>&1 || ! command -v gpg >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y --no-install-recommends git gnupg
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ./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
|
||||||
|
sudo mkdir -p /opt/coder
|
||||||
|
sudo chown "$(id -u):$(id -g)" /opt/coder
|
||||||
|
echo '${base64encode(file("${path.module}/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 ./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
|
||||||
|
sudo mkdir -p /opt/coder
|
||||||
|
sudo chown "$(id -u):$(id -g)" /opt/coder
|
||||||
|
echo '${base64encode(file("${path.module}/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
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,250 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
# Tracks whether the user actually ended up authenticated against GitHub
|
||||||
|
# and/or Gitea below, so the SSH/GPG key step can ask about exactly the
|
||||||
|
# host(s) in play (and stay silent - "local git only" - if neither).
|
||||||
|
DID_GITHUB=0
|
||||||
|
DID_GITEA=0
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- GitHub CLI (gh) ---
|
||||||
|
if command -v gh >/dev/null 2>&1; then
|
||||||
|
echo "GitHub CLI already installed, skipping."
|
||||||
|
else
|
||||||
|
if ask_yes_no "Install GitHub CLI (gh) and log in?"; then
|
||||||
|
if (sudo mkdir -p -m 755 /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg >/dev/null \
|
||||||
|
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||||
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null \
|
||||||
|
&& sudo apt-get update -qq && sudo apt-get install -y gh); then
|
||||||
|
gh auth login || echo "Install succeeded but login didn't complete. Retry any time with: gh auth login"
|
||||||
|
else
|
||||||
|
echo "GitHub CLI install failed. Retry later with: gh auth login (once gh is installed)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1 && DID_GITHUB=1
|
||||||
|
|
||||||
|
# --- Gitea CLI (tea) ---
|
||||||
|
if command -v tea >/dev/null 2>&1; then
|
||||||
|
echo "Gitea CLI already installed, skipping."
|
||||||
|
else
|
||||||
|
if ask_yes_no "Install Gitea CLI (tea) and log in?"; then
|
||||||
|
TEA_ARCH="$(uname -m)"
|
||||||
|
case "$TEA_ARCH" in
|
||||||
|
x86_64) TEA_ARCH="amd64" ;;
|
||||||
|
aarch64) TEA_ARCH="arm64" ;;
|
||||||
|
esac
|
||||||
|
TEA_VERSION="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/tea/releases/latest | grep -o '"tag_name":[^,]*' | grep -o 'v[0-9][^"]*')"
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
if [ -n "$TEA_VERSION" ] \
|
||||||
|
&& curl -fsSL "https://gitea.com/gitea/tea/releases/download/${TEA_VERSION}/tea-${TEA_VERSION#v}-linux-${TEA_ARCH}" -o "$HOME/.local/bin/tea" \
|
||||||
|
&& chmod +x "$HOME/.local/bin/tea"; then
|
||||||
|
echo "Add this Gitea instance now (e.g. https://git.octoturge.com)..."
|
||||||
|
tea login add || echo "Login didn't complete. Retry any time with: tea login add"
|
||||||
|
else
|
||||||
|
echo "Gitea CLI install failed. Retry later from: https://gitea.com/gitea/tea/releases"
|
||||||
|
rm -f "$HOME/.local/bin/tea"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
|
||||||
|
if command -v tea >/dev/null 2>&1 && [ -f "$TEA_CONFIG" ]; then
|
||||||
|
TEA_URL="$(awk '/^logins:/{f=1} f && /^ url:/{print $2; exit}' "$TEA_CONFIG")"
|
||||||
|
TEA_TOKEN="$(awk '/^logins:/{f=1} f && /^ token:/{print $2; exit}' "$TEA_CONFIG")"
|
||||||
|
[ -n "$TEA_URL" ] && [ -n "$TEA_TOKEN" ] && DID_GITEA=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- SSH + GPG keys for the external git host(s) selected above ---
|
||||||
|
# Only asks if the user actually set up GitHub and/or Gitea just now -
|
||||||
|
# stays silent for "local git only" (neither was set up).
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ] && [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
KEY_HOSTS_DESC="GitHub and Gitea"
|
||||||
|
elif [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
KEY_HOSTS_DESC="GitHub"
|
||||||
|
else
|
||||||
|
KEY_HOSTS_DESC="Gitea"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ask_yes_no "Auto-generate an SSH key and a GPG signing key, and register them with $KEY_HOSTS_DESC?"; then
|
||||||
|
KEY_NAME="${GIT_AUTHOR_NAME:-$(whoami)}"
|
||||||
|
KEY_EMAIL="${GIT_AUTHOR_EMAIL:-$(whoami)@$(hostname)}"
|
||||||
|
|
||||||
|
# SSH key: ed25519, no passphrase (disposable dev workspace convenience;
|
||||||
|
# add one manually afterwards with `ssh-keygen -p` if you want one).
|
||||||
|
SSH_KEY="$HOME/.ssh/id_ed25519"
|
||||||
|
if [ ! -f "$SSH_KEY" ]; then
|
||||||
|
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
|
||||||
|
ssh-keygen -t ed25519 -N "" -C "$KEY_EMAIL" -f "$SSH_KEY" -q
|
||||||
|
echo "Generated SSH key: ${SSH_KEY}.pub"
|
||||||
|
else
|
||||||
|
echo "SSH key already exists at ${SSH_KEY}.pub, reusing it."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
if gh ssh-key add "${SSH_KEY}.pub" --title "coder-$(hostname)" 2>/dev/null; then
|
||||||
|
echo "SSH key added to GitHub."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the SSH key to GitHub automatically (may already be added). Add manually: gh ssh-key add ${SSH_KEY}.pub"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
SSH_PUB_JSON="$(sed 's/\\/\\\\/g; s/"/\\"/g' "${SSH_KEY}.pub")"
|
||||||
|
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/keys" \
|
||||||
|
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"title\":\"coder-$(hostname)\",\"key\":\"${SSH_PUB_JSON}\"}" >/dev/null 2>&1; then
|
||||||
|
echo "SSH key added to Gitea."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# GPG key: ed25519 signing key, no passphrase, no expiry.
|
||||||
|
if gpg --list-secret-keys --with-colons "$KEY_EMAIL" 2>/dev/null | grep -q '^sec'; then
|
||||||
|
echo "GPG key for $KEY_EMAIL already exists, reusing it."
|
||||||
|
else
|
||||||
|
mkdir -p "$HOME/.gnupg" && chmod 700 "$HOME/.gnupg"
|
||||||
|
grep -qF "allow-loopback-pinentry" "$HOME/.gnupg/gpg-agent.conf" 2>/dev/null \
|
||||||
|
|| echo "allow-loopback-pinentry" >> "$HOME/.gnupg/gpg-agent.conf"
|
||||||
|
gpgconf --kill gpg-agent 2>/dev/null
|
||||||
|
if gpg --batch --pinentry-mode loopback --passphrase '' --quick-gen-key "$KEY_NAME <$KEY_EMAIL>" ed25519 sign 0 2>/dev/null; then
|
||||||
|
echo "Generated GPG signing key for $KEY_EMAIL."
|
||||||
|
else
|
||||||
|
echo "GPG key generation failed. Generate manually with: gpg --quick-gen-key \"$KEY_NAME <$KEY_EMAIL>\" ed25519 sign 0"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
GPG_KEY_ID="$(gpg --list-secret-keys --with-colons "$KEY_EMAIL" 2>/dev/null | awk -F: '/^sec/{print $5; exit}')"
|
||||||
|
|
||||||
|
if [ -n "$GPG_KEY_ID" ]; then
|
||||||
|
git config --global user.signingkey "$GPG_KEY_ID"
|
||||||
|
git config --global commit.gpgsign true
|
||||||
|
echo "Configured git to sign commits with this key."
|
||||||
|
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
if gpg --armor --export "$GPG_KEY_ID" | gh gpg-key add - 2>/dev/null; then
|
||||||
|
echo "GPG key added to GitHub."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the GPG key to GitHub automatically (may already be added). Add manually: gpg --armor --export $GPG_KEY_ID | gh gpg-key add -"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
GPG_ARMORED_JSON="$(gpg --armor --export "$GPG_KEY_ID" | awk '{printf "%s\\n", $0}')"
|
||||||
|
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/gpg_keys" \
|
||||||
|
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"armored_public_key\":\"${GPG_ARMORED_JSON}\"}" >/dev/null 2>&1; then
|
||||||
|
echo "GPG key added to Gitea."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
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
|
||||||
@@ -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."
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
}
|
||||||
|
docker = {
|
||||||
|
source = "kreuzwerker/docker"
|
||||||
|
}
|
||||||
|
# Not used directly in this config. Existing workspace state from before
|
||||||
|
# the jetbrains module was removed still has resources tagged under this
|
||||||
|
# provider (the module used it internally to fetch IDE metadata) -
|
||||||
|
# terraform init only installs providers the current config declares, so
|
||||||
|
# without this, plan/apply fails with "Missing required provider" while
|
||||||
|
# trying to reconcile/destroy those leftover state entries. Safe to drop
|
||||||
|
# once every workspace has updated past the jetbrains-module version.
|
||||||
|
http = {
|
||||||
|
source = "hashicorp/http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
env_name = "TTRPG & Lore Building"
|
||||||
|
profile = jsondecode(file("${path.module}/profile.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
|
||||||
|
|
||||||
|
# Ensure git and gnupg (commit signing) are present as base packages -
|
||||||
|
# not every base image ships gnupg by default. No-op once both are
|
||||||
|
# present (e.g. templates/web already bakes them into its image).
|
||||||
|
if ! command -v git >/dev/null 2>&1 || ! command -v gpg >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y --no-install-recommends git gnupg
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ./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
|
||||||
|
sudo mkdir -p /opt/coder
|
||||||
|
sudo chown "$(id -u):$(id -g)" /opt/coder
|
||||||
|
echo '${base64encode(file("${path.module}/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 ./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
|
||||||
|
sudo mkdir -p /opt/coder
|
||||||
|
sudo chown "$(id -u):$(id -g)" /opt/coder
|
||||||
|
echo '${base64encode(file("${path.module}/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
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# Web Applications workspace image: Rust (Tauri 2 / gRPC), Bun + Node/pnpm,
|
||||||
|
# Python + CV/ONNX prototyping, and DB CLI clients baked in at build time so
|
||||||
|
# workspace start doesn't pay for a from-scratch toolchain install.
|
||||||
|
#
|
||||||
|
# Built by templates/web/main.tf via the docker provider's `build` block
|
||||||
|
# (context = this directory), not pulled from a registry.
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
locales sudo ca-certificates gnupg curl wget \
|
||||||
|
&& locale-gen en_US.UTF-8 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV LANG=en_US.UTF-8 \
|
||||||
|
LANGUAGE=en_US:en \
|
||||||
|
LC_ALL=en_US.UTF-8
|
||||||
|
|
||||||
|
# Google Chrome (stable), for the Browse Lite VS Code extension's embedded
|
||||||
|
# browser preview. Ubuntu's own `chromium-browser` apt package is just a
|
||||||
|
# snap wrapper and doesn't work in a container (no snapd) - Google's own
|
||||||
|
# .deb is the reliable way to get a real Chrome binary here. amd64 only
|
||||||
|
# (Google doesn't publish a Chrome .deb for arm64), which matches this
|
||||||
|
# repo's single x86_64 Docker host.
|
||||||
|
RUN curl -fsSL https://dl.google.com/linux/linux_signing_key.pub \
|
||||||
|
| gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg \
|
||||||
|
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" \
|
||||||
|
> /etc/apt/sources.list.d/google-chrome.list \
|
||||||
|
&& apt-get update && apt-get install -y google-chrome-stable \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Core build toolchain, crypto/DB headers, Tauri 2 / WebKit GUI prerequisites,
|
||||||
|
# X11 dev libs, DB CLI clients, Python + OpenCV, protobuf compiler.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential pkg-config cmake clang llvm \
|
||||||
|
git git-lfs jq unzip tar file htop tree tmux zsh openssh-client \
|
||||||
|
libssl-dev libpq-dev libsqlite3-dev \
|
||||||
|
libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev \
|
||||||
|
libgtk-3-dev libsoup-3.0-dev \
|
||||||
|
libx11-dev libxext-dev libxrender-dev libxtst-dev libxi-dev \
|
||||||
|
postgresql-client redis-tools sqlite3 \
|
||||||
|
python3 python3-pip python3-venv python3-dev \
|
||||||
|
libopencv-dev \
|
||||||
|
protobuf-compiler \
|
||||||
|
&& git lfs install --system \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Node.js LTS (22.x) plus npm/pnpm/yarn as root so global bins land on the
|
||||||
|
# system PATH for every user.
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& npm install -g pnpm yarn \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Global Python prototyping packages: CV, ONNX runtime, CPU-only torch wheel.
|
||||||
|
# Ubuntu 24.04's system Python is PEP 668 externally-managed; this is a
|
||||||
|
# throwaway container image, so --break-system-packages is the right call
|
||||||
|
# instead of forcing every user into a venv for basic prototyping. Not
|
||||||
|
# upgrading pip itself first: the Debian-packaged pip 24.0 has no RECORD
|
||||||
|
# file (dpkg-installed, not pip-installed), so `pip install --upgrade pip`
|
||||||
|
# fails trying to uninstall it in place - and it's unneeded anyway, the
|
||||||
|
# packages below install fine under the stock version.
|
||||||
|
RUN python3 -m pip install --break-system-packages --no-cache-dir \
|
||||||
|
numpy opencv-python-headless onnxruntime \
|
||||||
|
&& python3 -m pip install --break-system-packages --no-cache-dir \
|
||||||
|
torch --index-url https://download.pytorch.org/whl/cpu
|
||||||
|
|
||||||
|
# Standard non-root dev user with passwordless sudo. Ubuntu 24.04's base
|
||||||
|
# image already ships a default `ubuntu` user/group at uid/gid 1000, which
|
||||||
|
# collides with the explicit --uid 1000 below - drop it first so `coder`
|
||||||
|
# can take that uid.
|
||||||
|
RUN userdel -r ubuntu 2>/dev/null; \
|
||||||
|
groupdel ubuntu 2>/dev/null; \
|
||||||
|
useradd --uid 1000 --create-home --shell /bin/bash coder \
|
||||||
|
&& echo "coder ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/coder \
|
||||||
|
&& chmod 0440 /etc/sudoers.d/coder
|
||||||
|
|
||||||
|
ENV RUST_BACKTRACE=1 \
|
||||||
|
RUSTUP_HOME=/home/coder/.rustup \
|
||||||
|
CARGO_HOME=/home/coder/.cargo \
|
||||||
|
BUN_INSTALL=/home/coder/.bun \
|
||||||
|
PNPM_HOME=/home/coder/.local/share/pnpm \
|
||||||
|
PATH=/home/coder/.cargo/bin:/home/coder/.bun/bin:/home/coder/.local/share/pnpm:/home/coder/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
|
||||||
|
USER coder
|
||||||
|
WORKDIR /home/coder
|
||||||
|
|
||||||
|
# Rust via rustup: stable toolchain, rust-analyzer/clippy/rustfmt/rust-src,
|
||||||
|
# native + musl targets for x86_64/aarch64, and cargo helper utilities.
|
||||||
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
|
||||||
|
--default-toolchain stable --profile default \
|
||||||
|
&& rustup component add rustfmt clippy rust-analyzer rust-src \
|
||||||
|
&& rustup target add \
|
||||||
|
x86_64-unknown-linux-gnu \
|
||||||
|
x86_64-unknown-linux-musl \
|
||||||
|
aarch64-unknown-linux-gnu \
|
||||||
|
aarch64-unknown-linux-musl \
|
||||||
|
&& cargo install --locked cargo-watch cargo-edit cross bacon
|
||||||
|
|
||||||
|
# Bun: global runtime for the ElysiaJS backend and fast scripting.
|
||||||
|
RUN curl -fsSL https://bun.sh/install | bash
|
||||||
|
|
||||||
|
RUN mkdir -p /home/coder/workspace
|
||||||
|
WORKDIR /home/coder/workspace
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
# Tracks whether the user actually ended up authenticated against GitHub
|
||||||
|
# and/or Gitea below, so the SSH/GPG key step can ask about exactly the
|
||||||
|
# host(s) in play (and stay silent - "local git only" - if neither).
|
||||||
|
DID_GITHUB=0
|
||||||
|
DID_GITEA=0
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- GitHub CLI (gh) ---
|
||||||
|
if command -v gh >/dev/null 2>&1; then
|
||||||
|
echo "GitHub CLI already installed, skipping."
|
||||||
|
else
|
||||||
|
if ask_yes_no "Install GitHub CLI (gh) and log in?"; then
|
||||||
|
if (sudo mkdir -p -m 755 /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg >/dev/null \
|
||||||
|
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||||
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null \
|
||||||
|
&& sudo apt-get update -qq && sudo apt-get install -y gh); then
|
||||||
|
gh auth login || echo "Install succeeded but login didn't complete. Retry any time with: gh auth login"
|
||||||
|
else
|
||||||
|
echo "GitHub CLI install failed. Retry later with: gh auth login (once gh is installed)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1 && DID_GITHUB=1
|
||||||
|
|
||||||
|
# --- Gitea CLI (tea) ---
|
||||||
|
if command -v tea >/dev/null 2>&1; then
|
||||||
|
echo "Gitea CLI already installed, skipping."
|
||||||
|
else
|
||||||
|
if ask_yes_no "Install Gitea CLI (tea) and log in?"; then
|
||||||
|
TEA_ARCH="$(uname -m)"
|
||||||
|
case "$TEA_ARCH" in
|
||||||
|
x86_64) TEA_ARCH="amd64" ;;
|
||||||
|
aarch64) TEA_ARCH="arm64" ;;
|
||||||
|
esac
|
||||||
|
TEA_VERSION="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/tea/releases/latest | grep -o '"tag_name":[^,]*' | grep -o 'v[0-9][^"]*')"
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
if [ -n "$TEA_VERSION" ] \
|
||||||
|
&& curl -fsSL "https://gitea.com/gitea/tea/releases/download/${TEA_VERSION}/tea-${TEA_VERSION#v}-linux-${TEA_ARCH}" -o "$HOME/.local/bin/tea" \
|
||||||
|
&& chmod +x "$HOME/.local/bin/tea"; then
|
||||||
|
echo "Add this Gitea instance now (e.g. https://git.octoturge.com)..."
|
||||||
|
tea login add || echo "Login didn't complete. Retry any time with: tea login add"
|
||||||
|
else
|
||||||
|
echo "Gitea CLI install failed. Retry later from: https://gitea.com/gitea/tea/releases"
|
||||||
|
rm -f "$HOME/.local/bin/tea"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
|
||||||
|
if command -v tea >/dev/null 2>&1 && [ -f "$TEA_CONFIG" ]; then
|
||||||
|
TEA_URL="$(awk '/^logins:/{f=1} f && /^ url:/{print $2; exit}' "$TEA_CONFIG")"
|
||||||
|
TEA_TOKEN="$(awk '/^logins:/{f=1} f && /^ token:/{print $2; exit}' "$TEA_CONFIG")"
|
||||||
|
[ -n "$TEA_URL" ] && [ -n "$TEA_TOKEN" ] && DID_GITEA=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- SSH + GPG keys for the external git host(s) selected above ---
|
||||||
|
# Only asks if the user actually set up GitHub and/or Gitea just now -
|
||||||
|
# stays silent for "local git only" (neither was set up).
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ] && [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
KEY_HOSTS_DESC="GitHub and Gitea"
|
||||||
|
elif [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
KEY_HOSTS_DESC="GitHub"
|
||||||
|
else
|
||||||
|
KEY_HOSTS_DESC="Gitea"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ask_yes_no "Auto-generate an SSH key and a GPG signing key, and register them with $KEY_HOSTS_DESC?"; then
|
||||||
|
KEY_NAME="${GIT_AUTHOR_NAME:-$(whoami)}"
|
||||||
|
KEY_EMAIL="${GIT_AUTHOR_EMAIL:-$(whoami)@$(hostname)}"
|
||||||
|
|
||||||
|
# SSH key: ed25519, no passphrase (disposable dev workspace convenience;
|
||||||
|
# add one manually afterwards with `ssh-keygen -p` if you want one).
|
||||||
|
SSH_KEY="$HOME/.ssh/id_ed25519"
|
||||||
|
if [ ! -f "$SSH_KEY" ]; then
|
||||||
|
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
|
||||||
|
ssh-keygen -t ed25519 -N "" -C "$KEY_EMAIL" -f "$SSH_KEY" -q
|
||||||
|
echo "Generated SSH key: ${SSH_KEY}.pub"
|
||||||
|
else
|
||||||
|
echo "SSH key already exists at ${SSH_KEY}.pub, reusing it."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
if gh ssh-key add "${SSH_KEY}.pub" --title "coder-$(hostname)" 2>/dev/null; then
|
||||||
|
echo "SSH key added to GitHub."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the SSH key to GitHub automatically (may already be added). Add manually: gh ssh-key add ${SSH_KEY}.pub"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
SSH_PUB_JSON="$(sed 's/\\/\\\\/g; s/"/\\"/g' "${SSH_KEY}.pub")"
|
||||||
|
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/keys" \
|
||||||
|
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"title\":\"coder-$(hostname)\",\"key\":\"${SSH_PUB_JSON}\"}" >/dev/null 2>&1; then
|
||||||
|
echo "SSH key added to Gitea."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# GPG key: ed25519 signing key, no passphrase, no expiry.
|
||||||
|
if gpg --list-secret-keys --with-colons "$KEY_EMAIL" 2>/dev/null | grep -q '^sec'; then
|
||||||
|
echo "GPG key for $KEY_EMAIL already exists, reusing it."
|
||||||
|
else
|
||||||
|
mkdir -p "$HOME/.gnupg" && chmod 700 "$HOME/.gnupg"
|
||||||
|
grep -qF "allow-loopback-pinentry" "$HOME/.gnupg/gpg-agent.conf" 2>/dev/null \
|
||||||
|
|| echo "allow-loopback-pinentry" >> "$HOME/.gnupg/gpg-agent.conf"
|
||||||
|
gpgconf --kill gpg-agent 2>/dev/null
|
||||||
|
if gpg --batch --pinentry-mode loopback --passphrase '' --quick-gen-key "$KEY_NAME <$KEY_EMAIL>" ed25519 sign 0 2>/dev/null; then
|
||||||
|
echo "Generated GPG signing key for $KEY_EMAIL."
|
||||||
|
else
|
||||||
|
echo "GPG key generation failed. Generate manually with: gpg --quick-gen-key \"$KEY_NAME <$KEY_EMAIL>\" ed25519 sign 0"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
GPG_KEY_ID="$(gpg --list-secret-keys --with-colons "$KEY_EMAIL" 2>/dev/null | awk -F: '/^sec/{print $5; exit}')"
|
||||||
|
|
||||||
|
if [ -n "$GPG_KEY_ID" ]; then
|
||||||
|
git config --global user.signingkey "$GPG_KEY_ID"
|
||||||
|
git config --global commit.gpgsign true
|
||||||
|
echo "Configured git to sign commits with this key."
|
||||||
|
|
||||||
|
if [ "$DID_GITHUB" -eq 1 ]; then
|
||||||
|
if gpg --armor --export "$GPG_KEY_ID" | gh gpg-key add - 2>/dev/null; then
|
||||||
|
echo "GPG key added to GitHub."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the GPG key to GitHub automatically (may already be added). Add manually: gpg --armor --export $GPG_KEY_ID | gh gpg-key add -"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DID_GITEA" -eq 1 ]; then
|
||||||
|
GPG_ARMORED_JSON="$(gpg --armor --export "$GPG_KEY_ID" | awk '{printf "%s\\n", $0}')"
|
||||||
|
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/gpg_keys" \
|
||||||
|
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"armored_public_key\":\"${GPG_ARMORED_JSON}\"}" >/dev/null 2>&1; then
|
||||||
|
echo "GPG key added to Gitea."
|
||||||
|
else
|
||||||
|
echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
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
|
||||||
@@ -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."
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
}
|
||||||
|
docker = {
|
||||||
|
source = "kreuzwerker/docker"
|
||||||
|
}
|
||||||
|
# Not used directly in this config. Existing workspace state from before
|
||||||
|
# the jetbrains module was removed still has resources tagged under this
|
||||||
|
# provider (the module used it internally to fetch IDE metadata) -
|
||||||
|
# terraform init only installs providers the current config declares, so
|
||||||
|
# without this, plan/apply fails with "Missing required provider" while
|
||||||
|
# trying to reconcile/destroy those leftover state entries. Safe to drop
|
||||||
|
# once every workspace has updated past the jetbrains-module version.
|
||||||
|
http = {
|
||||||
|
source = "hashicorp/http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
env_name = "Web Applications"
|
||||||
|
profile = jsondecode(file("${path.module}/profile.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
|
||||||
|
|
||||||
|
# Ensure git and gnupg (commit signing) are present as base packages -
|
||||||
|
# not every base image ships gnupg by default. No-op once both are
|
||||||
|
# present (e.g. templates/web already bakes them into its image).
|
||||||
|
if ! command -v git >/dev/null 2>&1 || ! command -v gpg >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y --no-install-recommends git gnupg
|
||||||
|
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)
|
||||||
|
#
|
||||||
|
# RUST_BACKTRACE/PNPM_HOME/BUN_INSTALL are also baked in as image ENV (see
|
||||||
|
# Dockerfile) so every process picks them up; restated here so they surface
|
||||||
|
# on the workspace dashboard too.
|
||||||
|
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}"
|
||||||
|
RUST_BACKTRACE = "1"
|
||||||
|
PNPM_HOME = "/home/coder/.local/share/pnpm"
|
||||||
|
BUN_INSTALL = "/home/coder/.bun"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Builds the full Web Applications toolchain (Rust/Tauri 2, Bun/Node/pnpm,
|
||||||
|
# Python CV/ONNX, DB clients - see ./Dockerfile) from this template's own
|
||||||
|
# directory, so no external registry push is required. The tag embeds the
|
||||||
|
# Dockerfile's hash so a Dockerfile edit forces a rebuild on next apply/push,
|
||||||
|
# while an unchanged Dockerfile reuses the cached image.
|
||||||
|
resource "docker_image" "web" {
|
||||||
|
name = "coder-profiles-web:${filesha1("${path.module}/Dockerfile")}"
|
||||||
|
build {
|
||||||
|
context = path.module
|
||||||
|
}
|
||||||
|
keep_locally = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "docker_container" "workspace" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
image = docker_image.web.image_id
|
||||||
|
# Uses lower() to avoid Docker restriction on container names.
|
||||||
|
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||||
|
# Hostname makes the shell more user friendly: coder@my-workspace:~$
|
||||||
|
hostname = data.coder_workspace.me.name
|
||||||
|
# Use the docker gateway if the access URL is 127.0.0.1
|
||||||
|
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
|
||||||
|
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
|
||||||
|
host {
|
||||||
|
host = "host.docker.internal"
|
||||||
|
ip = "host-gateway"
|
||||||
|
}
|
||||||
|
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 ./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
|
||||||
|
sudo mkdir -p /opt/coder
|
||||||
|
sudo chown "$(id -u):$(id -g)" /opt/coder
|
||||||
|
echo '${base64encode(file("${path.module}/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
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bun (and the rest of the toolchain - Rust, Node/pnpm/yarn, Python CV/ONNX
|
||||||
|
# packages, DB clients) is baked into the workspace image at build time (see
|
||||||
|
# Dockerfile) rather than installed here on every start. Docker populates a
|
||||||
|
# fresh, empty named volume from the image's directory contents on first
|
||||||
|
# mount, so $HOME/.bun, $HOME/.cargo, $HOME/.rustup etc. land in the
|
||||||
|
# persistent home_volume automatically the first time a workspace boots -
|
||||||
|
# same mechanism the /etc/skel copy above relies on. BUN_INSTALL and PATH
|
||||||
|
# are set as image ENV plus restated on coder_agent.env above.
|
||||||
|
|
||||||
|
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
|
||||||
|
# Antigravity CLI's skills directories. See ./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
|
||||||
|
sudo mkdir -p /opt/coder
|
||||||
|
sudo chown "$(id -u):$(id -g)" /opt/coder
|
||||||
|
echo '${base64encode(file("${path.module}/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
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user