Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6de3196faa | |||
| 730af0a335 | |||
| d60e44508a | |||
| 93721b9096 | |||
| 013567f02a | |||
| e8a8f93f9d | |||
| 557d6a3d84 | |||
| 354536dad5 | |||
| 1be7c00b84 | |||
| 52cc84650a | |||
| 0c53bb6554 | |||
| a302e21609 | |||
| 60e63edd61 | |||
| 22429aa4e3 | |||
| 5b32539d65 |
@@ -13,19 +13,53 @@ name: Provision Coder Templates
|
|||||||
# CODER_URL e.g. https://code.octoturge.com
|
# CODER_URL e.g. https://code.octoturge.com
|
||||||
# CODER_SESSION_TOKEN a token from `coder tokens create`, ideally under a
|
# CODER_SESSION_TOKEN a token from `coder tokens create`, ideally under a
|
||||||
# dedicated service account rather than a personal one
|
# dedicated service account rather than a personal one
|
||||||
|
# GITEA_PACKAGE_TOKEN a Gitea access token (user Settings > Applications)
|
||||||
|
# with write:package scope, for pushing templates/web's
|
||||||
|
# image to this instance's container registry. Only
|
||||||
|
# the octoturge account's own token is used - login()
|
||||||
|
# hardcodes that username to match.
|
||||||
|
#
|
||||||
|
# templates/web builds its Docker image here (build-web-image, on the
|
||||||
|
# dedicated "docker-build" runner - see templates/web/main.tf for why: that
|
||||||
|
# runner is scoped to this repo only and has host Docker socket access that
|
||||||
|
# the shared runner-1 deliberately doesn't). provision then just pulls the
|
||||||
|
# tag build-web-image produced, instead of building it itself at
|
||||||
|
# `terraform apply` time - keeps the slow Rust toolchain compile off of
|
||||||
|
# "someone is waiting to create a workspace".
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- "templates/**"
|
- "templates/**"
|
||||||
- "scripts/**"
|
|
||||||
- "profile-templates/**"
|
|
||||||
- ".gitea/workflows/coder-templates.yml"
|
- ".gitea/workflows/coder-templates.yml"
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
build-web-image:
|
||||||
|
runs-on: docker-build
|
||||||
|
container:
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build and push templates/web's image (skips if the tag already exists)
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
TAG="$(sha1sum templates/web/Dockerfile | cut -d' ' -f1)"
|
||||||
|
IMAGE="git.octoturge.com/octo-tech/profiles-web:${TAG}"
|
||||||
|
echo "${{ secrets.GITEA_PACKAGE_TOKEN }}" | docker login git.octoturge.com -u octoturge --password-stdin
|
||||||
|
if docker manifest inspect "$IMAGE" >/dev/null 2>&1; then
|
||||||
|
echo "$IMAGE already in the registry (Dockerfile unchanged), skipping build."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
docker build -t "$IMAGE" templates/web
|
||||||
|
docker push "$IMAGE"
|
||||||
|
|
||||||
provision:
|
provision:
|
||||||
|
needs: build-web-image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
CODER_URL: ${{ secrets.CODER_URL }}
|
CODER_URL: ${{ secrets.CODER_URL }}
|
||||||
@@ -63,7 +97,7 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
OLD_DIRS="$(git ls-tree -d --name-only "$PREV_SHA" -- templates 2>/dev/null | xargs -n1 basename 2>/dev/null || true)"
|
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
|
if [ -z "$OLD_DIRS" ]; then
|
||||||
echo "No templates/ directory at $PREV_SHA, nothing to diff. Skipping."
|
echo "No templates/ directory at $PREV_SHA, nothing to diff. Skipping."
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ name: Rotate Coder API Token
|
|||||||
# it replaced.
|
# it replaced.
|
||||||
#
|
#
|
||||||
# One-time bootstrap (see README "Auto-provisioning" section): a
|
# One-time bootstrap (see README "Auto-provisioning" section): a
|
||||||
# GITEA_ROTATION_TOKEN secret holding a Gitea personal access token
|
# ROTATION_PAT secret holding a Gitea personal access token (write:repository
|
||||||
# (write:repository scope, no expiration) with permission to write this
|
# scope, no expiration) with permission to write this repo's Actions secrets.
|
||||||
# repo's Actions secrets. Nothing else needs to touch this ever again.
|
# 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:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
@@ -23,7 +25,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CODER_URL: ${{ secrets.CODER_URL }}
|
CODER_URL: ${{ secrets.CODER_URL }}
|
||||||
CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }}
|
CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }}
|
||||||
GITEA_ROTATION_TOKEN: ${{ secrets.GITEA_ROTATION_TOKEN }}
|
ROTATION_PAT: ${{ secrets.ROTATION_PAT }}
|
||||||
GITEA_API_URL: ${{ github.server_url }}/api/v1
|
GITEA_API_URL: ${{ github.server_url }}/api/v1
|
||||||
GITEA_REPO_PATH: ${{ github.repository }}
|
GITEA_REPO_PATH: ${{ github.repository }}
|
||||||
steps:
|
steps:
|
||||||
@@ -56,7 +58,7 @@ jobs:
|
|||||||
BODY="$(jq -n --arg data "$NEW_TOKEN" '{data:$data}')"
|
BODY="$(jq -n --arg data "$NEW_TOKEN" '{data:$data}')"
|
||||||
HTTP_STATUS="$(curl -s -o /tmp/put-secret.out -w '%{http_code}' \
|
HTTP_STATUS="$(curl -s -o /tmp/put-secret.out -w '%{http_code}' \
|
||||||
-X PUT \
|
-X PUT \
|
||||||
-H "Authorization: token ${GITEA_ROTATION_TOKEN}" \
|
-H "Authorization: token ${ROTATION_PAT}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$BODY" \
|
-d "$BODY" \
|
||||||
"${GITEA_API_URL}/repos/${GITEA_REPO_PATH}/actions/secrets/CODER_SESSION_TOKEN")"
|
"${GITEA_API_URL}/repos/${GITEA_REPO_PATH}/actions/secrets/CODER_SESSION_TOKEN")"
|
||||||
|
|||||||
@@ -9,25 +9,58 @@ do and which didn't actually work.
|
|||||||
|
|
||||||
```
|
```
|
||||||
templates/
|
templates/
|
||||||
default/main.tf # Standard Default Dev
|
default/
|
||||||
3d-printing/main.tf # 3D Printing & Engineering
|
main.tf # Standard Default Dev
|
||||||
cobol/main.tf # COBOL Modern Mainframe
|
profile.code-profile # VS Code .code-profile export for this env
|
||||||
python/main.tf # Python Engineering
|
cli-setup-wizard.sh # first-run wizard, see below
|
||||||
ttrpg/main.tf # TTRPG & Lore Building
|
install-skills.sh # Agent Skills installer, see below
|
||||||
web/main.tf # Web Applications
|
3d-printing/ (same 4 files) # 3D Printing & Engineering
|
||||||
scripts/
|
cobol/ (same 4 files) # COBOL Modern Mainframe
|
||||||
cli-setup-wizard.sh # shared first-run wizard, see below
|
python/ (same 4 files) # Python Engineering
|
||||||
install-skills.sh # shared Agent Skills installer, see below
|
ttrpg/ (same 4 files) # TTRPG & Lore Building
|
||||||
profile-templates/ # VS Code .code-profile exports, one per env
|
web/ (same 4 files, plus Dockerfile) # Web Applications
|
||||||
extensions/ # Agent Skills bundles, installed per env (see below)
|
extensions/ # Agent Skills bundles, installed per env (see below)
|
||||||
```
|
```
|
||||||
|
|
||||||
Each `templates/<env>/main.tf` is a complete, independent Coder template
|
### Web Applications: baked-in toolchain image
|
||||||
(agent, docker container, code-server, JetBrains). They're deliberately not
|
|
||||||
built from a shared Terraform module - only their `locals` block differs
|
Every other template pulls `codercom/enterprise-base:ubuntu` straight from
|
||||||
(which profile file to read). The naming convention is `profiles-<dir>`
|
Docker Hub and installs what little it needs (just Bun) via a `coder_script`
|
||||||
(matches what `.gitea/workflows/coder-templates.yml` does automatically -
|
at workspace start. `templates/web/` doesn't do that: it needs a large,
|
||||||
see below). To push by hand:
|
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
|
```sh
|
||||||
coder templates push profiles-default -d templates/default
|
coder templates push profiles-default -d templates/default
|
||||||
@@ -40,7 +73,7 @@ coder templates push profiles-web -d templates/web
|
|||||||
|
|
||||||
### How the VS Code profile gets applied
|
### How the VS Code profile gets applied
|
||||||
|
|
||||||
`profile-templates/*.code-profile` is a real VS Code Profile export: a JSON
|
`templates/<env>/profile.code-profile` is a real VS Code Profile export: a JSON
|
||||||
file whose `settings` and `extensions` fields are themselves JSON-encoded
|
file whose `settings` and `extensions` fields are themselves JSON-encoded
|
||||||
strings (double/triple-nested). Each template's `main.tf` reads and decodes
|
strings (double/triple-nested). Each template's `main.tf` reads and decodes
|
||||||
its matching file **at `terraform apply`/push time** (via `file()` +
|
its matching file **at `terraform apply`/push time** (via `file()` +
|
||||||
@@ -62,22 +95,40 @@ never really worked.
|
|||||||
|
|
||||||
### Bun
|
### Bun
|
||||||
|
|
||||||
Every template installs [Bun](https://bun.sh) via a `coder_script`
|
Every template except `web` installs [Bun](https://bun.sh) via a
|
||||||
(`curl -fsSL https://bun.sh/install | bash`) and hooks `~/.bun/bin` onto
|
`coder_script` (`curl -fsSL https://bun.sh/install | bash`) and hooks
|
||||||
`PATH` in `~/.bashrc` (the installer doesn't reliably do this itself in a
|
`~/.bun/bin` onto `PATH` in `~/.bashrc` (the installer doesn't reliably do
|
||||||
non-interactive/scripted shell). The CLI setup wizard below uses
|
this itself in a non-interactive/scripted shell). `web` instead bakes Bun
|
||||||
`bun install -g <pkg>` instead of `npm install -g <pkg>` for everything it
|
into its Dockerfile with `BUN_INSTALL`/`PATH` set as image `ENV` - see "Web
|
||||||
installs.
|
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
|
### CLI setup wizard
|
||||||
|
|
||||||
`scripts/cli-setup-wizard.sh` is dropped onto every workspace and hooked
|
`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
|
into `~/.bashrc`. It runs in every new interactive terminal - until the user
|
||||||
finishes it - and offers to install + log into:
|
finishes it - and offers to install + log into:
|
||||||
|
|
||||||
- **GitHub Copilot CLI** (`bun install -g @github/copilot`, then `copilot login`)
|
- **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`)
|
- **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`)
|
- **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
|
It does **not** ask about VS Code extensions, since those are handled by
|
||||||
Terraform (see above). Once the user confirms completion it writes a
|
Terraform (see above). Once the user confirms completion it writes a
|
||||||
@@ -92,7 +143,7 @@ root `main.tf` copied this folder straight into code-server's VS Code
|
|||||||
extensions directory, which never worked since these aren't VS Code
|
extensions directory, which never worked since these aren't VS Code
|
||||||
extension packages.
|
extension packages.
|
||||||
|
|
||||||
Each template now runs `scripts/install-skills.sh` (via a `coder_script`,
|
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
|
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
|
into Terraform state) to install skills into all three AI CLIs' personal
|
||||||
skills directories:
|
skills directories:
|
||||||
@@ -117,13 +168,13 @@ the `SPECIALTY_SKILLS` env var passed to the script from each template's
|
|||||||
### Auto-provisioning (Gitea Actions)
|
### Auto-provisioning (Gitea Actions)
|
||||||
|
|
||||||
`.gitea/workflows/coder-templates.yml` keeps Coder in sync with this repo on
|
`.gitea/workflows/coder-templates.yml` keeps Coder in sync with this repo on
|
||||||
every push to `main` that touches `templates/**`, `scripts/**`, or
|
every push to `main` that touches `templates/**`:
|
||||||
`profile-templates/**`:
|
|
||||||
|
|
||||||
- **Add** a new `templates/<env>/` directory -> next push creates a new
|
- **Add** a new `templates/<env>/` directory -> next push creates a new
|
||||||
Coder template `profiles-<env>` automatically. No workflow edits needed.
|
Coder template `profiles-<env>` automatically. No workflow edits needed.
|
||||||
- **Edit** an existing `templates/<env>/main.tf` (or a shared script/profile
|
- **Edit** an existing `templates/<env>/main.tf` (or its `profile.code-profile`,
|
||||||
it references) -> next push updates that template with a new version.
|
`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
|
- **Remove** a `templates/<env>/` directory -> next push deletes
|
||||||
`profiles-<env>` from Coder. `coder templates delete` refuses if the
|
`profiles-<env>` from Coder. `coder templates delete` refuses if the
|
||||||
template still has active workspaces, so this fails loudly instead of
|
template still has active workspaces, so this fails loudly instead of
|
||||||
@@ -201,12 +252,14 @@ workflow write to its own repo's secrets):
|
|||||||
1. Create a Gitea personal access token with **write:repository** scope and
|
1. Create a Gitea personal access token with **write:repository** scope and
|
||||||
**no expiration** (Settings -> Applications -> Generate New Token). This
|
**no expiration** (Settings -> Applications -> Generate New Token). This
|
||||||
one doesn't rotate itself, so give it a long life up front.
|
one doesn't rotate itself, so give it a long life up front.
|
||||||
2. Add it as a repo/org Actions secret named `GITEA_ROTATION_TOKEN` (same
|
2. Add it as a repo/org Actions secret named `ROTATION_PAT` (same
|
||||||
Settings -> Actions -> Secrets page as above - if that tab is missing,
|
Settings -> Actions -> Secrets page as above - if that tab is missing,
|
||||||
see the nav-link workaround / API fallback in the "Auto-provisioning"
|
see the nav-link workaround / API fallback in the "Auto-provisioning"
|
||||||
section above, same `curl -X PUT .../actions/secrets/<name>` pattern,
|
section above, same `curl -X PUT .../actions/secrets/<name>` pattern,
|
||||||
just with `GITEA_ROTATION_TOKEN` as the secret name and the PAT itself as
|
just with `ROTATION_PAT` as the secret name and the PAT itself as the
|
||||||
the value).
|
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
|
After that, `CODER_SESSION_TOKEN` never needs manual attention again - you
|
||||||
can also trigger a rotation on demand from Gitea's Actions tab
|
can also trigger a rotation on demand from Gitea's Actions tab
|
||||||
|
|||||||
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
@@ -1,107 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Coder workspace first-run CLI setup wizard.
|
|
||||||
#
|
|
||||||
# Meant to be `source`d from a new interactive shell (e.g. via .bashrc). It asks,
|
|
||||||
# once per user per workspace, whether to install and log into a few optional
|
|
||||||
# AI coding CLIs. It re-runs on every new terminal until the user lets it finish
|
|
||||||
# (or explicitly skips it for good), then gets out of the way.
|
|
||||||
#
|
|
||||||
# VS Code / code-server extensions are intentionally NOT asked about here -
|
|
||||||
# they're installed declaratively by the Coder template itself (the
|
|
||||||
# `code-server` module's `extensions` input, populated from the matching
|
|
||||||
# profile-templates/*.code-profile file at template-push time).
|
|
||||||
#
|
|
||||||
# Manual re-run: bash /opt/coder/cli-setup-wizard.sh --force
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
WIZARD_DONE_FILE="${HOME}/.cache/coder-cli-wizard/done"
|
|
||||||
FORCE=0
|
|
||||||
[ "${1:-}" = "--force" ] && FORCE=1
|
|
||||||
|
|
||||||
export BUN_INSTALL="${HOME}/.bun"
|
|
||||||
export PATH="${BUN_INSTALL}/bin:${HOME}/.local/bin:${PATH}"
|
|
||||||
|
|
||||||
# Only bother interactive shells with a real terminal attached, and only until
|
|
||||||
# the user marks the wizard as done.
|
|
||||||
if [ "$FORCE" -ne 1 ]; then
|
|
||||||
case "$-" in
|
|
||||||
*i*) : ;;
|
|
||||||
*) return 0 2>/dev/null || exit 0 ;;
|
|
||||||
esac
|
|
||||||
[ -t 0 ] || { return 0 2>/dev/null || exit 0; }
|
|
||||||
[ -f "$WIZARD_DONE_FILE" ] && { return 0 2>/dev/null || exit 0; }
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$(dirname "$WIZARD_DONE_FILE")"
|
|
||||||
|
|
||||||
ask_yes_no() {
|
|
||||||
local prompt="$1" reply
|
|
||||||
read -r -p "$prompt [y/N] " reply
|
|
||||||
case "$reply" in
|
|
||||||
[Yy]*) return 0 ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "==================================================================="
|
|
||||||
echo " Coder workspace setup wizard"
|
|
||||||
echo " Runs once per new terminal until you finish it. Ctrl+C any time"
|
|
||||||
echo " to skip for now - it'll ask again next terminal."
|
|
||||||
echo "==================================================================="
|
|
||||||
|
|
||||||
# --- GitHub Copilot CLI ---
|
|
||||||
if command -v copilot >/dev/null 2>&1; then
|
|
||||||
echo "GitHub Copilot CLI already installed, skipping."
|
|
||||||
elif command -v bun >/dev/null 2>&1; then
|
|
||||||
if ask_yes_no "Install GitHub Copilot CLI and log in?"; then
|
|
||||||
if bun install -g @github/copilot; then
|
|
||||||
copilot login || echo "Install succeeded but login didn't complete. Retry any time with: copilot login"
|
|
||||||
else
|
|
||||||
echo "Copilot CLI install failed. Retry later with: bun install -g @github/copilot && copilot login"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Skipping GitHub Copilot CLI: bun not found on this workspace image."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Google Antigravity CLI (agy) ---
|
|
||||||
if command -v agy >/dev/null 2>&1; then
|
|
||||||
echo "Antigravity CLI already installed, skipping."
|
|
||||||
else
|
|
||||||
if ask_yes_no "Install Google Antigravity CLI (agy) and log in?"; then
|
|
||||||
if curl -fsSL https://antigravity.google/cli/install.sh | bash; then
|
|
||||||
echo "Launching 'agy' once to complete sign-in (exit with /logout or Ctrl+D when done)..."
|
|
||||||
agy || echo "Sign-in didn't complete. Retry any time by running: agy"
|
|
||||||
else
|
|
||||||
echo "Antigravity CLI install failed. Retry later with: curl -fsSL https://antigravity.google/cli/install.sh | bash"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Claude Code CLI ---
|
|
||||||
if command -v claude >/dev/null 2>&1; then
|
|
||||||
echo "Claude Code CLI already installed, skipping."
|
|
||||||
elif command -v bun >/dev/null 2>&1; then
|
|
||||||
if ask_yes_no "Install Claude Code CLI and log in?"; then
|
|
||||||
if bun install -g @anthropic-ai/claude-code; then
|
|
||||||
echo "Launching 'claude' once to complete sign-in (use /login if not prompted; Ctrl+C to exit when done)..."
|
|
||||||
claude || echo "Sign-in didn't complete. Retry any time by running: claude"
|
|
||||||
else
|
|
||||||
echo "Claude Code CLI install failed. Retry later with: bun install -g @anthropic-ai/claude-code"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Skipping Claude Code CLI: bun not found on this workspace image."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
if ask_yes_no "Mark setup wizard as complete so it stops asking on new terminals?"; then
|
|
||||||
touch "$WIZARD_DONE_FILE"
|
|
||||||
echo "Done. Re-run any time with: bash /opt/coder/cli-setup-wizard.sh --force"
|
|
||||||
else
|
|
||||||
echo "OK, this'll ask again next time you open a terminal."
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0 2>/dev/null || exit 0
|
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
#!/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 whoami` succeeds regardless of how the login was done (personal
|
||||||
|
# access token or OAuth) - more reliable than parsing tea's own
|
||||||
|
# config.yml, whose indentation and fields (no plaintext `token:` at all
|
||||||
|
# for an OAuth login) vary by auth method.
|
||||||
|
command -v tea >/dev/null 2>&1 && tea whoami >/dev/null 2>&1 && DID_GITEA=1
|
||||||
|
|
||||||
|
# --- 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
|
||||||
|
if tea ssh-keys add "${SSH_KEY}.pub" --title "coder-$(hostname)" >/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 with: tea ssh-keys add ${SSH_KEY}.pub"
|
||||||
|
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
|
||||||
|
if gpg --armor --export "$GPG_KEY_ID" | tea api -X POST /user/gpg_keys -F armored_public_key=@- >/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: gpg --armor --export $GPG_KEY_ID | tea api -X POST /user/gpg_keys -F armored_public_key=@-"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif command -v gh >/dev/null 2>&1 || command -v tea >/dev/null 2>&1; then
|
||||||
|
# gh and/or tea are installed but neither is actually logged in yet (login
|
||||||
|
# was declined, failed, or never completed) - say why the key-generation
|
||||||
|
# question above got skipped instead of just silently not asking.
|
||||||
|
echo "Skipping SSH/GPG key setup: not logged in to GitHub or Gitea yet."
|
||||||
|
echo "Log in (gh auth login / tea login add) then re-run: bash /opt/coder/cli-setup-wizard.sh --force"
|
||||||
|
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
|
||||||
@@ -6,12 +6,22 @@ terraform {
|
|||||||
docker = {
|
docker = {
|
||||||
source = "kreuzwerker/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 {
|
locals {
|
||||||
env_name = "3D Printing & Engineering"
|
env_name = "3D Printing & Engineering"
|
||||||
profile = jsondecode(file("${path.module}/../../profile-templates/3D.code-profile"))
|
profile = jsondecode(file("${path.module}/profile.code-profile"))
|
||||||
settings_raw = jsondecode(local.profile.settings).settings
|
settings_raw = jsondecode(local.profile.settings).settings
|
||||||
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
|
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
|
||||||
}
|
}
|
||||||
@@ -43,6 +53,14 @@ resource "coder_agent" "main" {
|
|||||||
touch ~/.init_done
|
touch ~/.init_done
|
||||||
fi
|
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
|
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
|
||||||
EOT
|
EOT
|
||||||
|
|
||||||
@@ -141,17 +159,6 @@ module "code-server" {
|
|||||||
order = 1
|
order = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# See https://registry.coder.com/modules/coder/jetbrains
|
|
||||||
module "jetbrains" {
|
|
||||||
count = data.coder_workspace.me.start_count
|
|
||||||
source = "registry.coder.com/coder/jetbrains/coder"
|
|
||||||
version = "~> 1.1"
|
|
||||||
agent_id = coder_agent.main.id
|
|
||||||
agent_name = "main"
|
|
||||||
folder = "/home/coder"
|
|
||||||
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "docker_volume" "home_volume" {
|
resource "docker_volume" "home_volume" {
|
||||||
name = "coder-${data.coder_workspace.me.id}-home"
|
name = "coder-${data.coder_workspace.me.id}-home"
|
||||||
# Protect the volume from being deleted due to changes in attributes.
|
# Protect the volume from being deleted due to changes in attributes.
|
||||||
@@ -243,7 +250,7 @@ resource "coder_script" "apply_settings" {
|
|||||||
|
|
||||||
# Drops the shared CLI setup wizard onto the workspace and hooks it into
|
# Drops the shared CLI setup wizard onto the workspace and hooks it into
|
||||||
# every new interactive shell (via .bashrc) until the user completes it.
|
# every new interactive shell (via .bashrc) until the user completes it.
|
||||||
# See ../../scripts/cli-setup-wizard.sh for what it actually asks.
|
# See ./cli-setup-wizard.sh for what it actually asks.
|
||||||
resource "coder_script" "cli_setup_wizard" {
|
resource "coder_script" "cli_setup_wizard" {
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
display_name = "Install CLI Setup Wizard"
|
display_name = "Install CLI Setup Wizard"
|
||||||
@@ -251,8 +258,9 @@ resource "coder_script" "cli_setup_wizard" {
|
|||||||
script = <<-EOT
|
script = <<-EOT
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
mkdir -p /opt/coder
|
sudo mkdir -p /opt/coder
|
||||||
echo '${base64encode(file("${path.module}/../../scripts/cli-setup-wizard.sh"))}' | base64 -d > /opt/coder/cli-setup-wizard.sh
|
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
|
chmod +x /opt/coder/cli-setup-wizard.sh
|
||||||
|
|
||||||
MARKER="# >>> coder cli setup wizard >>>"
|
MARKER="# >>> coder cli setup wizard >>>"
|
||||||
@@ -298,7 +306,7 @@ resource "coder_script" "install_bun" {
|
|||||||
|
|
||||||
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
|
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
|
||||||
# Antigravity CLI's skills directories, plus the openscad-parametric skill
|
# Antigravity CLI's skills directories, plus the openscad-parametric skill
|
||||||
# from extensions/custom-specialty-plugin. See ../../scripts/install-skills.sh.
|
# from extensions/custom-specialty-plugin. See ./install-skills.sh.
|
||||||
resource "coder_script" "install_skills" {
|
resource "coder_script" "install_skills" {
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
display_name = "Install Agent Skills"
|
display_name = "Install Agent Skills"
|
||||||
@@ -306,8 +314,9 @@ resource "coder_script" "install_skills" {
|
|||||||
script = <<-EOT
|
script = <<-EOT
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
mkdir -p /opt/coder
|
sudo mkdir -p /opt/coder
|
||||||
echo '${base64encode(file("${path.module}/../../scripts/install-skills.sh"))}' | base64 -d > /opt/coder/install-skills.sh
|
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
|
chmod +x /opt/coder/install-skills.sh
|
||||||
SPECIALTY_SKILLS="openscad-parametric" /opt/coder/install-skills.sh
|
SPECIALTY_SKILLS="openscad-parametric" /opt/coder/install-skills.sh
|
||||||
EOT
|
EOT
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,249 @@
|
|||||||
|
#!/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 whoami` succeeds regardless of how the login was done (personal
|
||||||
|
# access token or OAuth) - more reliable than parsing tea's own
|
||||||
|
# config.yml, whose indentation and fields (no plaintext `token:` at all
|
||||||
|
# for an OAuth login) vary by auth method.
|
||||||
|
command -v tea >/dev/null 2>&1 && tea whoami >/dev/null 2>&1 && DID_GITEA=1
|
||||||
|
|
||||||
|
# --- 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
|
||||||
|
if tea ssh-keys add "${SSH_KEY}.pub" --title "coder-$(hostname)" >/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 with: tea ssh-keys add ${SSH_KEY}.pub"
|
||||||
|
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
|
||||||
|
if gpg --armor --export "$GPG_KEY_ID" | tea api -X POST /user/gpg_keys -F armored_public_key=@- >/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: gpg --armor --export $GPG_KEY_ID | tea api -X POST /user/gpg_keys -F armored_public_key=@-"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif command -v gh >/dev/null 2>&1 || command -v tea >/dev/null 2>&1; then
|
||||||
|
# gh and/or tea are installed but neither is actually logged in yet (login
|
||||||
|
# was declined, failed, or never completed) - say why the key-generation
|
||||||
|
# question above got skipped instead of just silently not asking.
|
||||||
|
echo "Skipping SSH/GPG key setup: not logged in to GitHub or Gitea yet."
|
||||||
|
echo "Log in (gh auth login / tea login add) then re-run: bash /opt/coder/cli-setup-wizard.sh --force"
|
||||||
|
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."
|
||||||
+27
-18
@@ -6,12 +6,22 @@ terraform {
|
|||||||
docker = {
|
docker = {
|
||||||
source = "kreuzwerker/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 {
|
locals {
|
||||||
env_name = "COBOL Modern Mainframe"
|
env_name = "COBOL Modern Mainframe"
|
||||||
profile = jsondecode(file("${path.module}/../../profile-templates/COBOL.code-profile"))
|
profile = jsondecode(file("${path.module}/profile.code-profile"))
|
||||||
settings_raw = jsondecode(local.profile.settings).settings
|
settings_raw = jsondecode(local.profile.settings).settings
|
||||||
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
|
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
|
||||||
}
|
}
|
||||||
@@ -43,6 +53,14 @@ resource "coder_agent" "main" {
|
|||||||
touch ~/.init_done
|
touch ~/.init_done
|
||||||
fi
|
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
|
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
|
||||||
EOT
|
EOT
|
||||||
|
|
||||||
@@ -141,17 +159,6 @@ module "code-server" {
|
|||||||
order = 1
|
order = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# See https://registry.coder.com/modules/coder/jetbrains
|
|
||||||
module "jetbrains" {
|
|
||||||
count = data.coder_workspace.me.start_count
|
|
||||||
source = "registry.coder.com/coder/jetbrains/coder"
|
|
||||||
version = "~> 1.1"
|
|
||||||
agent_id = coder_agent.main.id
|
|
||||||
agent_name = "main"
|
|
||||||
folder = "/home/coder"
|
|
||||||
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "docker_volume" "home_volume" {
|
resource "docker_volume" "home_volume" {
|
||||||
name = "coder-${data.coder_workspace.me.id}-home"
|
name = "coder-${data.coder_workspace.me.id}-home"
|
||||||
# Protect the volume from being deleted due to changes in attributes.
|
# Protect the volume from being deleted due to changes in attributes.
|
||||||
@@ -243,7 +250,7 @@ resource "coder_script" "apply_settings" {
|
|||||||
|
|
||||||
# Drops the shared CLI setup wizard onto the workspace and hooks it into
|
# Drops the shared CLI setup wizard onto the workspace and hooks it into
|
||||||
# every new interactive shell (via .bashrc) until the user completes it.
|
# every new interactive shell (via .bashrc) until the user completes it.
|
||||||
# See ../../scripts/cli-setup-wizard.sh for what it actually asks.
|
# See ./cli-setup-wizard.sh for what it actually asks.
|
||||||
resource "coder_script" "cli_setup_wizard" {
|
resource "coder_script" "cli_setup_wizard" {
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
display_name = "Install CLI Setup Wizard"
|
display_name = "Install CLI Setup Wizard"
|
||||||
@@ -251,8 +258,9 @@ resource "coder_script" "cli_setup_wizard" {
|
|||||||
script = <<-EOT
|
script = <<-EOT
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
mkdir -p /opt/coder
|
sudo mkdir -p /opt/coder
|
||||||
echo '${base64encode(file("${path.module}/../../scripts/cli-setup-wizard.sh"))}' | base64 -d > /opt/coder/cli-setup-wizard.sh
|
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
|
chmod +x /opt/coder/cli-setup-wizard.sh
|
||||||
|
|
||||||
MARKER="# >>> coder cli setup wizard >>>"
|
MARKER="# >>> coder cli setup wizard >>>"
|
||||||
@@ -298,7 +306,7 @@ resource "coder_script" "install_bun" {
|
|||||||
|
|
||||||
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
|
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
|
||||||
# Antigravity CLI's skills directories, plus the cobol-teacher skill from
|
# Antigravity CLI's skills directories, plus the cobol-teacher skill from
|
||||||
# extensions/custom-specialty-plugin. See ../../scripts/install-skills.sh.
|
# extensions/custom-specialty-plugin. See ./install-skills.sh.
|
||||||
resource "coder_script" "install_skills" {
|
resource "coder_script" "install_skills" {
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
display_name = "Install Agent Skills"
|
display_name = "Install Agent Skills"
|
||||||
@@ -306,8 +314,9 @@ resource "coder_script" "install_skills" {
|
|||||||
script = <<-EOT
|
script = <<-EOT
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
mkdir -p /opt/coder
|
sudo mkdir -p /opt/coder
|
||||||
echo '${base64encode(file("${path.module}/../../scripts/install-skills.sh"))}' | base64 -d > /opt/coder/install-skills.sh
|
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
|
chmod +x /opt/coder/install-skills.sh
|
||||||
SPECIALTY_SKILLS="cobol-teacher" /opt/coder/install-skills.sh
|
SPECIALTY_SKILLS="cobol-teacher" /opt/coder/install-skills.sh
|
||||||
EOT
|
EOT
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,249 @@
|
|||||||
|
#!/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 whoami` succeeds regardless of how the login was done (personal
|
||||||
|
# access token or OAuth) - more reliable than parsing tea's own
|
||||||
|
# config.yml, whose indentation and fields (no plaintext `token:` at all
|
||||||
|
# for an OAuth login) vary by auth method.
|
||||||
|
command -v tea >/dev/null 2>&1 && tea whoami >/dev/null 2>&1 && DID_GITEA=1
|
||||||
|
|
||||||
|
# --- 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
|
||||||
|
if tea ssh-keys add "${SSH_KEY}.pub" --title "coder-$(hostname)" >/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 with: tea ssh-keys add ${SSH_KEY}.pub"
|
||||||
|
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
|
||||||
|
if gpg --armor --export "$GPG_KEY_ID" | tea api -X POST /user/gpg_keys -F armored_public_key=@- >/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: gpg --armor --export $GPG_KEY_ID | tea api -X POST /user/gpg_keys -F armored_public_key=@-"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif command -v gh >/dev/null 2>&1 || command -v tea >/dev/null 2>&1; then
|
||||||
|
# gh and/or tea are installed but neither is actually logged in yet (login
|
||||||
|
# was declined, failed, or never completed) - say why the key-generation
|
||||||
|
# question above got skipped instead of just silently not asking.
|
||||||
|
echo "Skipping SSH/GPG key setup: not logged in to GitHub or Gitea yet."
|
||||||
|
echo "Log in (gh auth login / tea login add) then re-run: bash /opt/coder/cli-setup-wizard.sh --force"
|
||||||
|
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."
|
||||||
+27
-18
@@ -6,12 +6,22 @@ terraform {
|
|||||||
docker = {
|
docker = {
|
||||||
source = "kreuzwerker/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 {
|
locals {
|
||||||
env_name = "Default"
|
env_name = "Default"
|
||||||
profile = jsondecode(file("${path.module}/../../profile-templates/Default.code-profile"))
|
profile = jsondecode(file("${path.module}/profile.code-profile"))
|
||||||
settings_raw = jsondecode(local.profile.settings).settings
|
settings_raw = jsondecode(local.profile.settings).settings
|
||||||
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
|
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
|
||||||
}
|
}
|
||||||
@@ -43,6 +53,14 @@ resource "coder_agent" "main" {
|
|||||||
touch ~/.init_done
|
touch ~/.init_done
|
||||||
fi
|
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
|
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
|
||||||
EOT
|
EOT
|
||||||
|
|
||||||
@@ -141,17 +159,6 @@ module "code-server" {
|
|||||||
order = 1
|
order = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# See https://registry.coder.com/modules/coder/jetbrains
|
|
||||||
module "jetbrains" {
|
|
||||||
count = data.coder_workspace.me.start_count
|
|
||||||
source = "registry.coder.com/coder/jetbrains/coder"
|
|
||||||
version = "~> 1.1"
|
|
||||||
agent_id = coder_agent.main.id
|
|
||||||
agent_name = "main"
|
|
||||||
folder = "/home/coder"
|
|
||||||
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "docker_volume" "home_volume" {
|
resource "docker_volume" "home_volume" {
|
||||||
name = "coder-${data.coder_workspace.me.id}-home"
|
name = "coder-${data.coder_workspace.me.id}-home"
|
||||||
# Protect the volume from being deleted due to changes in attributes.
|
# Protect the volume from being deleted due to changes in attributes.
|
||||||
@@ -243,7 +250,7 @@ resource "coder_script" "apply_settings" {
|
|||||||
|
|
||||||
# Drops the shared CLI setup wizard onto the workspace and hooks it into
|
# Drops the shared CLI setup wizard onto the workspace and hooks it into
|
||||||
# every new interactive shell (via .bashrc) until the user completes it.
|
# every new interactive shell (via .bashrc) until the user completes it.
|
||||||
# See ../../scripts/cli-setup-wizard.sh for what it actually asks.
|
# See ./cli-setup-wizard.sh for what it actually asks.
|
||||||
resource "coder_script" "cli_setup_wizard" {
|
resource "coder_script" "cli_setup_wizard" {
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
display_name = "Install CLI Setup Wizard"
|
display_name = "Install CLI Setup Wizard"
|
||||||
@@ -251,8 +258,9 @@ resource "coder_script" "cli_setup_wizard" {
|
|||||||
script = <<-EOT
|
script = <<-EOT
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
mkdir -p /opt/coder
|
sudo mkdir -p /opt/coder
|
||||||
echo '${base64encode(file("${path.module}/../../scripts/cli-setup-wizard.sh"))}' | base64 -d > /opt/coder/cli-setup-wizard.sh
|
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
|
chmod +x /opt/coder/cli-setup-wizard.sh
|
||||||
|
|
||||||
MARKER="# >>> coder cli setup wizard >>>"
|
MARKER="# >>> coder cli setup wizard >>>"
|
||||||
@@ -297,7 +305,7 @@ resource "coder_script" "install_bun" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
|
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
|
||||||
# Antigravity CLI's skills directories. See ../../scripts/install-skills.sh.
|
# Antigravity CLI's skills directories. See ./install-skills.sh.
|
||||||
# Default has no matching entry in extensions/custom-specialty-plugin/skills,
|
# Default has no matching entry in extensions/custom-specialty-plugin/skills,
|
||||||
# so it only gets the common awesome-skills-plugin bundle.
|
# so it only gets the common awesome-skills-plugin bundle.
|
||||||
resource "coder_script" "install_skills" {
|
resource "coder_script" "install_skills" {
|
||||||
@@ -307,8 +315,9 @@ resource "coder_script" "install_skills" {
|
|||||||
script = <<-EOT
|
script = <<-EOT
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
mkdir -p /opt/coder
|
sudo mkdir -p /opt/coder
|
||||||
echo '${base64encode(file("${path.module}/../../scripts/install-skills.sh"))}' | base64 -d > /opt/coder/install-skills.sh
|
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
|
chmod +x /opt/coder/install-skills.sh
|
||||||
SPECIALTY_SKILLS="" /opt/coder/install-skills.sh
|
SPECIALTY_SKILLS="" /opt/coder/install-skills.sh
|
||||||
EOT
|
EOT
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
#!/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 whoami` succeeds regardless of how the login was done (personal
|
||||||
|
# access token or OAuth) - more reliable than parsing tea's own
|
||||||
|
# config.yml, whose indentation and fields (no plaintext `token:` at all
|
||||||
|
# for an OAuth login) vary by auth method.
|
||||||
|
command -v tea >/dev/null 2>&1 && tea whoami >/dev/null 2>&1 && DID_GITEA=1
|
||||||
|
|
||||||
|
# --- 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
|
||||||
|
if tea ssh-keys add "${SSH_KEY}.pub" --title "coder-$(hostname)" >/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 with: tea ssh-keys add ${SSH_KEY}.pub"
|
||||||
|
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
|
||||||
|
if gpg --armor --export "$GPG_KEY_ID" | tea api -X POST /user/gpg_keys -F armored_public_key=@- >/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: gpg --armor --export $GPG_KEY_ID | tea api -X POST /user/gpg_keys -F armored_public_key=@-"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif command -v gh >/dev/null 2>&1 || command -v tea >/dev/null 2>&1; then
|
||||||
|
# gh and/or tea are installed but neither is actually logged in yet (login
|
||||||
|
# was declined, failed, or never completed) - say why the key-generation
|
||||||
|
# question above got skipped instead of just silently not asking.
|
||||||
|
echo "Skipping SSH/GPG key setup: not logged in to GitHub or Gitea yet."
|
||||||
|
echo "Log in (gh auth login / tea login add) then re-run: bash /opt/coder/cli-setup-wizard.sh --force"
|
||||||
|
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."
|
||||||
+27
-18
@@ -6,12 +6,22 @@ terraform {
|
|||||||
docker = {
|
docker = {
|
||||||
source = "kreuzwerker/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 {
|
locals {
|
||||||
env_name = "Python Engineering"
|
env_name = "Python Engineering"
|
||||||
profile = jsondecode(file("${path.module}/../../profile-templates/Python.code-profile"))
|
profile = jsondecode(file("${path.module}/profile.code-profile"))
|
||||||
settings_raw = jsondecode(local.profile.settings).settings
|
settings_raw = jsondecode(local.profile.settings).settings
|
||||||
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
|
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
|
||||||
}
|
}
|
||||||
@@ -43,6 +53,14 @@ resource "coder_agent" "main" {
|
|||||||
touch ~/.init_done
|
touch ~/.init_done
|
||||||
fi
|
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
|
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
|
||||||
EOT
|
EOT
|
||||||
|
|
||||||
@@ -141,17 +159,6 @@ module "code-server" {
|
|||||||
order = 1
|
order = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# See https://registry.coder.com/modules/coder/jetbrains
|
|
||||||
module "jetbrains" {
|
|
||||||
count = data.coder_workspace.me.start_count
|
|
||||||
source = "registry.coder.com/coder/jetbrains/coder"
|
|
||||||
version = "~> 1.1"
|
|
||||||
agent_id = coder_agent.main.id
|
|
||||||
agent_name = "main"
|
|
||||||
folder = "/home/coder"
|
|
||||||
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "docker_volume" "home_volume" {
|
resource "docker_volume" "home_volume" {
|
||||||
name = "coder-${data.coder_workspace.me.id}-home"
|
name = "coder-${data.coder_workspace.me.id}-home"
|
||||||
# Protect the volume from being deleted due to changes in attributes.
|
# Protect the volume from being deleted due to changes in attributes.
|
||||||
@@ -243,7 +250,7 @@ resource "coder_script" "apply_settings" {
|
|||||||
|
|
||||||
# Drops the shared CLI setup wizard onto the workspace and hooks it into
|
# Drops the shared CLI setup wizard onto the workspace and hooks it into
|
||||||
# every new interactive shell (via .bashrc) until the user completes it.
|
# every new interactive shell (via .bashrc) until the user completes it.
|
||||||
# See ../../scripts/cli-setup-wizard.sh for what it actually asks.
|
# See ./cli-setup-wizard.sh for what it actually asks.
|
||||||
resource "coder_script" "cli_setup_wizard" {
|
resource "coder_script" "cli_setup_wizard" {
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
display_name = "Install CLI Setup Wizard"
|
display_name = "Install CLI Setup Wizard"
|
||||||
@@ -251,8 +258,9 @@ resource "coder_script" "cli_setup_wizard" {
|
|||||||
script = <<-EOT
|
script = <<-EOT
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
mkdir -p /opt/coder
|
sudo mkdir -p /opt/coder
|
||||||
echo '${base64encode(file("${path.module}/../../scripts/cli-setup-wizard.sh"))}' | base64 -d > /opt/coder/cli-setup-wizard.sh
|
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
|
chmod +x /opt/coder/cli-setup-wizard.sh
|
||||||
|
|
||||||
MARKER="# >>> coder cli setup wizard >>>"
|
MARKER="# >>> coder cli setup wizard >>>"
|
||||||
@@ -297,7 +305,7 @@ resource "coder_script" "install_bun" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
|
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
|
||||||
# Antigravity CLI's skills directories. See ../../scripts/install-skills.sh.
|
# Antigravity CLI's skills directories. See ./install-skills.sh.
|
||||||
# Python has no matching entry in extensions/custom-specialty-plugin/skills,
|
# Python has no matching entry in extensions/custom-specialty-plugin/skills,
|
||||||
# so it only gets the common awesome-skills-plugin bundle.
|
# so it only gets the common awesome-skills-plugin bundle.
|
||||||
resource "coder_script" "install_skills" {
|
resource "coder_script" "install_skills" {
|
||||||
@@ -307,8 +315,9 @@ resource "coder_script" "install_skills" {
|
|||||||
script = <<-EOT
|
script = <<-EOT
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
mkdir -p /opt/coder
|
sudo mkdir -p /opt/coder
|
||||||
echo '${base64encode(file("${path.module}/../../scripts/install-skills.sh"))}' | base64 -d > /opt/coder/install-skills.sh
|
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
|
chmod +x /opt/coder/install-skills.sh
|
||||||
SPECIALTY_SKILLS="" /opt/coder/install-skills.sh
|
SPECIALTY_SKILLS="" /opt/coder/install-skills.sh
|
||||||
EOT
|
EOT
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,249 @@
|
|||||||
|
#!/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 whoami` succeeds regardless of how the login was done (personal
|
||||||
|
# access token or OAuth) - more reliable than parsing tea's own
|
||||||
|
# config.yml, whose indentation and fields (no plaintext `token:` at all
|
||||||
|
# for an OAuth login) vary by auth method.
|
||||||
|
command -v tea >/dev/null 2>&1 && tea whoami >/dev/null 2>&1 && DID_GITEA=1
|
||||||
|
|
||||||
|
# --- 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
|
||||||
|
if tea ssh-keys add "${SSH_KEY}.pub" --title "coder-$(hostname)" >/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 with: tea ssh-keys add ${SSH_KEY}.pub"
|
||||||
|
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
|
||||||
|
if gpg --armor --export "$GPG_KEY_ID" | tea api -X POST /user/gpg_keys -F armored_public_key=@- >/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: gpg --armor --export $GPG_KEY_ID | tea api -X POST /user/gpg_keys -F armored_public_key=@-"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif command -v gh >/dev/null 2>&1 || command -v tea >/dev/null 2>&1; then
|
||||||
|
# gh and/or tea are installed but neither is actually logged in yet (login
|
||||||
|
# was declined, failed, or never completed) - say why the key-generation
|
||||||
|
# question above got skipped instead of just silently not asking.
|
||||||
|
echo "Skipping SSH/GPG key setup: not logged in to GitHub or Gitea yet."
|
||||||
|
echo "Log in (gh auth login / tea login add) then re-run: bash /opt/coder/cli-setup-wizard.sh --force"
|
||||||
|
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."
|
||||||
+27
-18
@@ -6,12 +6,22 @@ terraform {
|
|||||||
docker = {
|
docker = {
|
||||||
source = "kreuzwerker/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 {
|
locals {
|
||||||
env_name = "TTRPG & Lore Building"
|
env_name = "TTRPG & Lore Building"
|
||||||
profile = jsondecode(file("${path.module}/../../profile-templates/TTRPG.code-profile"))
|
profile = jsondecode(file("${path.module}/profile.code-profile"))
|
||||||
settings_raw = jsondecode(local.profile.settings).settings
|
settings_raw = jsondecode(local.profile.settings).settings
|
||||||
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
|
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
|
||||||
}
|
}
|
||||||
@@ -43,6 +53,14 @@ resource "coder_agent" "main" {
|
|||||||
touch ~/.init_done
|
touch ~/.init_done
|
||||||
fi
|
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
|
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
|
||||||
EOT
|
EOT
|
||||||
|
|
||||||
@@ -141,17 +159,6 @@ module "code-server" {
|
|||||||
order = 1
|
order = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# See https://registry.coder.com/modules/coder/jetbrains
|
|
||||||
module "jetbrains" {
|
|
||||||
count = data.coder_workspace.me.start_count
|
|
||||||
source = "registry.coder.com/coder/jetbrains/coder"
|
|
||||||
version = "~> 1.1"
|
|
||||||
agent_id = coder_agent.main.id
|
|
||||||
agent_name = "main"
|
|
||||||
folder = "/home/coder"
|
|
||||||
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "docker_volume" "home_volume" {
|
resource "docker_volume" "home_volume" {
|
||||||
name = "coder-${data.coder_workspace.me.id}-home"
|
name = "coder-${data.coder_workspace.me.id}-home"
|
||||||
# Protect the volume from being deleted due to changes in attributes.
|
# Protect the volume from being deleted due to changes in attributes.
|
||||||
@@ -243,7 +250,7 @@ resource "coder_script" "apply_settings" {
|
|||||||
|
|
||||||
# Drops the shared CLI setup wizard onto the workspace and hooks it into
|
# Drops the shared CLI setup wizard onto the workspace and hooks it into
|
||||||
# every new interactive shell (via .bashrc) until the user completes it.
|
# every new interactive shell (via .bashrc) until the user completes it.
|
||||||
# See ../../scripts/cli-setup-wizard.sh for what it actually asks.
|
# See ./cli-setup-wizard.sh for what it actually asks.
|
||||||
resource "coder_script" "cli_setup_wizard" {
|
resource "coder_script" "cli_setup_wizard" {
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
display_name = "Install CLI Setup Wizard"
|
display_name = "Install CLI Setup Wizard"
|
||||||
@@ -251,8 +258,9 @@ resource "coder_script" "cli_setup_wizard" {
|
|||||||
script = <<-EOT
|
script = <<-EOT
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
mkdir -p /opt/coder
|
sudo mkdir -p /opt/coder
|
||||||
echo '${base64encode(file("${path.module}/../../scripts/cli-setup-wizard.sh"))}' | base64 -d > /opt/coder/cli-setup-wizard.sh
|
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
|
chmod +x /opt/coder/cli-setup-wizard.sh
|
||||||
|
|
||||||
MARKER="# >>> coder cli setup wizard >>>"
|
MARKER="# >>> coder cli setup wizard >>>"
|
||||||
@@ -299,7 +307,7 @@ resource "coder_script" "install_bun" {
|
|||||||
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
|
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
|
||||||
# Antigravity CLI's skills directories, plus the foundryvtt-modding and
|
# Antigravity CLI's skills directories, plus the foundryvtt-modding and
|
||||||
# ttrpg-lore-weaver skills from extensions/custom-specialty-plugin.
|
# ttrpg-lore-weaver skills from extensions/custom-specialty-plugin.
|
||||||
# See ../../scripts/install-skills.sh.
|
# See ./install-skills.sh.
|
||||||
resource "coder_script" "install_skills" {
|
resource "coder_script" "install_skills" {
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
display_name = "Install Agent Skills"
|
display_name = "Install Agent Skills"
|
||||||
@@ -307,8 +315,9 @@ resource "coder_script" "install_skills" {
|
|||||||
script = <<-EOT
|
script = <<-EOT
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
mkdir -p /opt/coder
|
sudo mkdir -p /opt/coder
|
||||||
echo '${base64encode(file("${path.module}/../../scripts/install-skills.sh"))}' | base64 -d > /opt/coder/install-skills.sh
|
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
|
chmod +x /opt/coder/install-skills.sh
|
||||||
SPECIALTY_SKILLS="foundryvtt-modding ttrpg-lore-weaver" /opt/coder/install-skills.sh
|
SPECIALTY_SKILLS="foundryvtt-modding ttrpg-lore-weaver" /opt/coder/install-skills.sh
|
||||||
EOT
|
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,249 @@
|
|||||||
|
#!/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 whoami` succeeds regardless of how the login was done (personal
|
||||||
|
# access token or OAuth) - more reliable than parsing tea's own
|
||||||
|
# config.yml, whose indentation and fields (no plaintext `token:` at all
|
||||||
|
# for an OAuth login) vary by auth method.
|
||||||
|
command -v tea >/dev/null 2>&1 && tea whoami >/dev/null 2>&1 && DID_GITEA=1
|
||||||
|
|
||||||
|
# --- 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
|
||||||
|
if tea ssh-keys add "${SSH_KEY}.pub" --title "coder-$(hostname)" >/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 with: tea ssh-keys add ${SSH_KEY}.pub"
|
||||||
|
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
|
||||||
|
if gpg --armor --export "$GPG_KEY_ID" | tea api -X POST /user/gpg_keys -F armored_public_key=@- >/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: gpg --armor --export $GPG_KEY_ID | tea api -X POST /user/gpg_keys -F armored_public_key=@-"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif command -v gh >/dev/null 2>&1 || command -v tea >/dev/null 2>&1; then
|
||||||
|
# gh and/or tea are installed but neither is actually logged in yet (login
|
||||||
|
# was declined, failed, or never completed) - say why the key-generation
|
||||||
|
# question above got skipped instead of just silently not asking.
|
||||||
|
echo "Skipping SSH/GPG key setup: not logged in to GitHub or Gitea yet."
|
||||||
|
echo "Log in (gh auth login / tea login add) then re-run: bash /opt/coder/cli-setup-wizard.sh --force"
|
||||||
|
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."
|
||||||
+58
-46
@@ -6,12 +6,22 @@ terraform {
|
|||||||
docker = {
|
docker = {
|
||||||
source = "kreuzwerker/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 {
|
locals {
|
||||||
env_name = "Web Applications"
|
env_name = "Web Applications"
|
||||||
profile = jsondecode(file("${path.module}/../../profile-templates/Web.code-profile"))
|
profile = jsondecode(file("${path.module}/profile.code-profile"))
|
||||||
settings_raw = jsondecode(local.profile.settings).settings
|
settings_raw = jsondecode(local.profile.settings).settings
|
||||||
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
|
extensions = [for e in jsondecode(local.profile.extensions) : e.identifier.id]
|
||||||
}
|
}
|
||||||
@@ -43,6 +53,14 @@ resource "coder_agent" "main" {
|
|||||||
touch ~/.init_done
|
touch ~/.init_done
|
||||||
fi
|
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
|
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
|
||||||
EOT
|
EOT
|
||||||
|
|
||||||
@@ -50,11 +68,18 @@ resource "coder_agent" "main" {
|
|||||||
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
|
# 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
|
# You can remove this block if you'd prefer to configure Git manually or using
|
||||||
# dotfiles. (see docs/dotfiles.md)
|
# 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 = {
|
env = {
|
||||||
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
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_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_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||||
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
|
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
|
# The following metadata blocks are optional. They are used to display
|
||||||
@@ -141,17 +166,6 @@ module "code-server" {
|
|||||||
order = 1
|
order = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# See https://registry.coder.com/modules/coder/jetbrains
|
|
||||||
module "jetbrains" {
|
|
||||||
count = data.coder_workspace.me.start_count
|
|
||||||
source = "registry.coder.com/coder/jetbrains/coder"
|
|
||||||
version = "~> 1.1"
|
|
||||||
agent_id = coder_agent.main.id
|
|
||||||
agent_name = "main"
|
|
||||||
folder = "/home/coder"
|
|
||||||
tooltip = "You need to [install JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) to use this app."
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "docker_volume" "home_volume" {
|
resource "docker_volume" "home_volume" {
|
||||||
name = "coder-${data.coder_workspace.me.id}-home"
|
name = "coder-${data.coder_workspace.me.id}-home"
|
||||||
# Protect the volume from being deleted due to changes in attributes.
|
# Protect the volume from being deleted due to changes in attributes.
|
||||||
@@ -179,9 +193,24 @@ resource "docker_volume" "home_volume" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Pulls the full Web Applications toolchain (Rust/Tauri 2, Bun/Node/pnpm,
|
||||||
|
# Python CV/ONNX, DB clients - see ./Dockerfile) instead of building it here.
|
||||||
|
# The image is built and pushed by .gitea/workflows/coder-templates.yml's
|
||||||
|
# build-web-image job, tagged with the same Dockerfile hash this resource
|
||||||
|
# computes - so a Dockerfile edit always resolves to the matching image, and
|
||||||
|
# an unchanged Dockerfile resolves to one already built (CI skips rebuilding
|
||||||
|
# it, and this pull is normally a same-host cache hit rather than a real
|
||||||
|
# network pull, since CI and this Coder deployment share one Docker daemon).
|
||||||
|
# Moves the ~15-20min Rust toolchain compile off of "someone is waiting to
|
||||||
|
# create a workspace" and onto CI, where it runs once per Dockerfile change.
|
||||||
|
resource "docker_image" "web" {
|
||||||
|
name = "git.octoturge.com/octo-tech/profiles-web:${filesha1("${path.module}/Dockerfile")}"
|
||||||
|
keep_locally = true
|
||||||
|
}
|
||||||
|
|
||||||
resource "docker_container" "workspace" {
|
resource "docker_container" "workspace" {
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
image = "codercom/enterprise-base:ubuntu"
|
image = docker_image.web.image_id
|
||||||
# Uses lower() to avoid Docker restriction on container names.
|
# Uses lower() to avoid Docker restriction on container names.
|
||||||
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
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 makes the shell more user friendly: coder@my-workspace:~$
|
||||||
@@ -243,7 +272,7 @@ resource "coder_script" "apply_settings" {
|
|||||||
|
|
||||||
# Drops the shared CLI setup wizard onto the workspace and hooks it into
|
# Drops the shared CLI setup wizard onto the workspace and hooks it into
|
||||||
# every new interactive shell (via .bashrc) until the user completes it.
|
# every new interactive shell (via .bashrc) until the user completes it.
|
||||||
# See ../../scripts/cli-setup-wizard.sh for what it actually asks.
|
# See ./cli-setup-wizard.sh for what it actually asks.
|
||||||
resource "coder_script" "cli_setup_wizard" {
|
resource "coder_script" "cli_setup_wizard" {
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
display_name = "Install CLI Setup Wizard"
|
display_name = "Install CLI Setup Wizard"
|
||||||
@@ -251,8 +280,9 @@ resource "coder_script" "cli_setup_wizard" {
|
|||||||
script = <<-EOT
|
script = <<-EOT
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
mkdir -p /opt/coder
|
sudo mkdir -p /opt/coder
|
||||||
echo '${base64encode(file("${path.module}/../../scripts/cli-setup-wizard.sh"))}' | base64 -d > /opt/coder/cli-setup-wizard.sh
|
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
|
chmod +x /opt/coder/cli-setup-wizard.sh
|
||||||
|
|
||||||
MARKER="# >>> coder cli setup wizard >>>"
|
MARKER="# >>> coder cli setup wizard >>>"
|
||||||
@@ -268,36 +298,17 @@ resource "coder_script" "cli_setup_wizard" {
|
|||||||
EOT
|
EOT
|
||||||
}
|
}
|
||||||
|
|
||||||
# Installs Bun and uses it (instead of npm) for the CLI installs the wizard
|
# Bun (and the rest of the toolchain - Rust, Node/pnpm/yarn, Python CV/ONNX
|
||||||
# script runs. The installer doesn't reliably add ~/.bun/bin to PATH in
|
# packages, DB clients) is baked into the workspace image at build time (see
|
||||||
# non-interactive shells, so that's hooked into .bashrc explicitly here.
|
# Dockerfile) rather than installed here on every start. Docker populates a
|
||||||
resource "coder_script" "install_bun" {
|
# fresh, empty named volume from the image's directory contents on first
|
||||||
agent_id = coder_agent.main.id
|
# mount, so $HOME/.bun, $HOME/.cargo, $HOME/.rustup etc. land in the
|
||||||
display_name = "Install Bun"
|
# persistent home_volume automatically the first time a workspace boots -
|
||||||
run_on_start = true
|
# same mechanism the /etc/skel copy above relies on. BUN_INSTALL and PATH
|
||||||
script = <<-EOT
|
# are set as image ENV plus restated on coder_agent.env above.
|
||||||
#!/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
|
# Installs this repo's Agent Skills into Claude Code, GitHub Copilot CLI, and
|
||||||
# Antigravity CLI's skills directories. See ../../scripts/install-skills.sh.
|
# Antigravity CLI's skills directories. See ./install-skills.sh.
|
||||||
# Web has no matching entry in extensions/custom-specialty-plugin/skills,
|
# Web has no matching entry in extensions/custom-specialty-plugin/skills,
|
||||||
# so it only gets the common awesome-skills-plugin bundle.
|
# so it only gets the common awesome-skills-plugin bundle.
|
||||||
resource "coder_script" "install_skills" {
|
resource "coder_script" "install_skills" {
|
||||||
@@ -307,8 +318,9 @@ resource "coder_script" "install_skills" {
|
|||||||
script = <<-EOT
|
script = <<-EOT
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
mkdir -p /opt/coder
|
sudo mkdir -p /opt/coder
|
||||||
echo '${base64encode(file("${path.module}/../../scripts/install-skills.sh"))}' | base64 -d > /opt/coder/install-skills.sh
|
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
|
chmod +x /opt/coder/install-skills.sh
|
||||||
SPECIALTY_SKILLS="" /opt/coder/install-skills.sh
|
SPECIALTY_SKILLS="" /opt/coder/install-skills.sh
|
||||||
EOT
|
EOT
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user