Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions .claude/skills/devstack-templates/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
---
name: devstack-templates
description: >-
Use when authoring or editing a devstack service template — a directory
holding template.yaml, an optional build/ tree and golden.yaml — or when
asked to add support for a new database, engine, language or framework to
devstack. Covers the engine-versus-app split, the [[ ]] delimiters and the
deterministic FuncMap, params/extends/provides/exports, the rule that
metadata keys are never templated, list-merge semantics, and the template
new → lint → test loop.
license: Apache-2.0
allowed-tools: Bash(devstack:*)
---

Commands below are written as `devstack`. If this machine installed an alias
(`rq`, `uranus`), substitute it.

## What a template is

A template is a **directory** that renders one compose service. Every service in
a devstack workspace comes from one.

```
<name>/
template.yaml REQUIRED — metadata plus the compose service fragment
build/ optional — Dockerfile, entrypoint.sh, nginx.conf, rendered verbatim
golden.yaml optional — a byte-for-byte fixture asserted by `devstack template test`
post_init.yaml optional — merged after the whole extends chain
```

The directory name is the template ref. Dots are allowed and are how families are
named: `php.nginx`, `php.laravel.nginx`.

Templates resolve from three sources, first match wins: an OCI-pinned remote
template, then `~/.devstack/templates/<name>/`, then the built-ins compiled into
the binary. Dropping a directory into `~/.devstack/templates/postgres/` therefore
shadows the built-in `postgres` for every workspace on the machine.

## Engines and apps are different things

This is a hard branch, not a style preference.

**An engine** is shared infrastructure (postgres, redis, minio, kafka). It uses
`image:`, declares `provides:` and `exports:` and `defaultPort:`, and usually
declares a named volume. It must **never** have a `build:` key — generation
rejects a shared service that tries to build.

**An app** is a project service (node.next, php.laravel.nginx). It uses
`build: { context: build, dockerfile: Dockerfile }` with a `build/` tree, and
must **never** declare `provides:` — `provides:` is what marks a template as
usable in `workspace.yaml`'s `shared:` block.

## Read a real one first

The built-ins are correct, current, and the best possible reference. Before
authoring, read one of the same kind:

```bash
devstack template list --json # every template with its metadata
devstack ai docs guide/templates # the full authoring guide
```

## The manifest

```yaml
schemaVersion: 1
extends: php.nginx # optional parent ref
description: "One line describing the service."
provides: postgres # ENGINES ONLY — the capability it satisfies
exports: [host, port, user, password, database] # attrs consumers may import
defaultPort: 5432 # the in-network port ${ref:...port} resolves to
params:
version:
type: string # string | int | bool (advisory in v1)
default: "18"
required: false
description: "Image tag."
service: # the compose service fragment
image: "postgres:[[ .params.version ]]"
volumes: # top-level named volumes
pgdata: {}
```

## Templating: `[[ ]]`, not `{{ }}`

The engine is Go's `text/template` with the delimiters changed to `[[` and `]]`.
That is deliberate: it lets shell `${VAR}`, Dockerfile `$TAG` and compose
`${VAR:-default}` pass through untouched, so a `build/Dockerfile` can use both
syntaxes at once.

The only data in scope is `.params`:

```yaml
image: "postgres:[[ .params.version ]]"
```

`missingkey=error` is set, so referencing an undeclared param is a hard failure,
never a silent empty string.

**Three rules that will bite you:**

1. **Metadata keys are parsed UNRENDERED.** `schemaVersion`, `extends`,
`description`, `provides`, `exports`, `defaultPort` and `params` are read
before any templating runs. A `[[ ]]` action in any of them is silently
meaningless — so the linter makes it a hard error. Only `service:` and
`volumes:` are rendered.
2. **The FuncMap is deterministic on purpose.** There is no `now`, no `uuid`, no
`randAlphaNum`, no environment access, because byte-identical output is a
CI-asserted requirement.
3. **Argument order is pipeline-style — the data comes last**, which is the
opposite of the `strings` package: `trimPrefix "v" .params.tag`,
`replace "-" "_" .params.name`, `contains "alpine" .params.image`,
`join "," .params.list`, `indent 4 .params.block`.

Available functions: `default` `coalesce` · `upper` `lower` `title` · `trim`
`trimPrefix` `trimSuffix` · `replace` `contains` `hasPrefix` `hasSuffix` ·
`join` `split` · `quote` `squote` · `indent` `nindent` `repeat` · `atoi`.

`atoi` parses the *leading* integer (`"9.6"` → 9), which is what makes
version-conditional fragments work with the builtin `lt`/`ge`:

```yaml
volumes:
- "pgdata:[[ if lt (atoi .params.version) 18 ]]/var/lib/postgresql/data[[ else ]]/var/lib/postgresql[[ end ]]"
```

## extends and the merge

`extends` renders the parent, then deep-merges the child over it. Order is:
parent → child `template.yaml` → child `post_init.yaml` → the project's overrides.

**Lists REPLACE by default.** A child declaring `volumes:` replaces the parent's
list entirely. Opt into appending with `$merge: append`. This is the single most
surprising merge behavior; check it whenever a parent's list vanishes.

`provides`, `exports` and `defaultPort` inherit leaf-wins. Files in `build/`
merge by path, so a child's `build/Dockerfile` replaces the parent's.

## The authoring loop

Use the builder rather than hand-writing the directory — it emits a deterministic,
correct skeleton for the kind you pick:

```bash
devstack template new mysvc --kind engine --print-spec > spec.yaml # inspect the plan
devstack template new mysvc --kind engine --from spec.yaml # materialize it
devstack template lint <dir> --show # lints + rendered compose
devstack template test <dir> # compare against golden.yaml
```

`--print-spec` → `--from` round-trips byte-stably, so an agent can generate the
spec, show it to a human, and materialize exactly what was reviewed.

`lint` runs three checks and then validates the rendered service through
`compose-go`:

| Check | Severity | Meaning |
|---|---|---|
| meta-templating | **error** | a `[[ ]]` action outside `service:`/`volumes:` |
| param-type | warning | a `default` that does not parse as its declared `type` |
| delimiter-collision | warning | a `build/` file containing a literal `[[` that is not a valid action |

## Using a template

```yaml
# workspace.yaml — engines only (templates that declare provides:)
shared:
postgres: { template: postgres, params: { version: "18" } }
```

```yaml
# devstack.yaml — apps
services:
api:
template: node.next
params: { nodeVersion: "22" }
uses: [workspace.shared.postgres]
```
66 changes: 66 additions & 0 deletions .claude/skills/devstack-troubleshooting/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
name: devstack-troubleshooting
description: >-
Use when a devstack command fails or a service is unhealthy — the Docker
daemon is unreachable, the devstack_shared network is missing, a host port
is in use, generate --check reports drift, config errors point at
file:line:col, secret:// resolution fails, shared-service ref counts look
wrong, or file watching misbehaves on WSL2. Maps each symptom to the
diagnostic command and the fix.
license: Apache-2.0
allowed-tools: Bash(devstack:*)
---

Commands below are written as `devstack`. If this machine installed an alias
(`rq`, `uranus`), substitute it.

## Start here

```bash
devstack doctor # the host preflight matrix: docker, compose, git, ports, paths
devstack doctor --fix # repair what is safely repairable
devstack status # per-service health + the shared-service ref graph
devstack logs <service> # the actual error, usually
```

Add `--debug` to any command for structured logs on stderr, including the exact
external command devstack ran and its exit code.

## Symptom → diagnose → fix

| Symptom | Diagnose | Fix |
|---|---|---|
| `Cannot connect to the Docker daemon` | `devstack doctor` | Start Docker. On WSL2 confirm which daemon you mean — Desktop and an in-distro `dockerd` are separate contexts with separate ledgers. |
| `network devstack_shared not found` | `docker network ls` | `devstack up` recreates it. Never `docker network rm` it yourself. |
| A host port is already in use | `devstack ports` | `devstack expose --off`, or let devstack allocate a different port. On Windows, an excluded port range can also be the cause. |
| `generate --check` reports drift | `devstack generate --check` | Run `devstack generate`. If drift returns immediately, something is editing `.devstack/` by hand. |
| A config error with `file:line:col` | `devstack config validate` | Read the position — it points at the exact YAML node. `devstack config schema` gives the full field contract. |
| `unknown interpolation ${...}` | — | `${ref:...}` takes a **colon**; `${env.NAME}` and `${self.attr}` take a **dot**. |
| A shared service will not start | `devstack logs shared-<engine>` | Often a volume from an older major version. Check the template's `params.version`. |
| A service cannot reach Postgres | `devstack status` | Connect to the alias `shared-postgres`, not `localhost` and not the bare service name. Both containers must be on the shared network. |
| Ref counts look wrong | `devstack shared status` | `devstack shared doctor` reconciles from live containers; `devstack shared gc` releases orphans. |
| A secret will not resolve | `devstack secrets status` | Confirm the provider is declared in `workspace.yaml` and that you are logged in (`devstack secrets login <provider>`). |
| A template change has no effect | `devstack template lint <dir> --show` | A `[[ ]]` action in a metadata key is a hard lint error — only `service:` and `volumes:` are rendered. |
| A parent template's list disappeared | — | Deep-merge **replaces** lists. Use `$merge: append`. |
| File watching does not fire on WSL2 | — | The app templates set polling env vars for this. Confirm the repo is on the Linux filesystem — `/mnt/*` working directories are refused. |
| Everything is confusing | `devstack doctor --json` | Escalate: `doctor --fix` → `shared doctor` → `shared gc` → as a last resort `workspace destroy` (destructive, needs `--yes`). |

## Never do these

- **Do not `docker compose ...` against a devstack stack.** The project name,
labels and external network are tool-owned; you will fork a parallel stack.
- **Do not hand-edit `.devstack/`.** It is generated output.
- **Do not `docker network rm devstack_shared`.**
- **Do not pass `--yes` to a destructive verb on the user's behalf** without
asking. `workspace destroy`, `db drop`, `db reset`, `s3 rb` and friends are not
reversible.
- **Do not run two mutating devstack commands concurrently.** They coordinate
through a cross-process lock; racing them is what corrupts ref counts.

## Reading further

```bash
devstack ai docs guide/recovery # doctor --fix, gc, teardown
devstack ai docs troubleshooting # the top-level troubleshooting page
devstack ai docs --search "<error text>"
```
127 changes: 127 additions & 0 deletions .claude/skills/devstack/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
name: devstack
description: >-
Use when working in a repository that contains workspace.yaml or
devstack.yaml, or when asked to start, stop or inspect a local development
environment, shared Postgres/Redis/MinIO/Kafka/NATS, tenant databases,
buckets, queues or topics. devstack runs many project stacks against one
warm shared infrastructure stack on a tool-owned Docker network. Covers the
two-file config model, the up/down/status lifecycle, and the rules for
driving the CLI safely — including that compose files under .devstack/ are
generated and must never be hand-edited.
license: Apache-2.0
allowed-tools: Bash(devstack:*)
---

Commands below are written as `devstack`. If this machine installed an alias
(`rq`, `uranus`), substitute it — the command tree is identical.

## What devstack is

devstack runs Docker development environments where **infrastructure is shared
across projects**. One warm Postgres, one Redis, one MinIO on a tool-owned Docker
network serve every repo in the workspace, and each project still gets its own
database, role and bucket. That is the whole point: eight microservices, one
Postgres container, eight isolated databases.

Two committed files describe everything:

- **`workspace.yaml`** at the workspace root — what infrastructure is *provided*,
and which repos belong to the workspace.
- **`devstack.yaml`**, one per repo — what that project *consumes*.

Everything else is generated. `devstack up` renders the compose files, starts the
shared engines once, provisions each project's isolated data, and brings the
project stacks up on the shared network.

## Orient before acting

In an unfamiliar workspace, run these first. They are read-only and fast.

```bash
devstack context # active workspace, project, docker context
devstack status # service health + the shared-service ref graph
devstack config show --json # the resolved configuration
devstack ai commands # every command this binary has
```

If `devstack config validate` reports an error it will be a `file:line:col`
pointing at the exact node. Read it; do not guess.

## The rules that matter

Violating any of these produces a broken or confusing workspace, and most are not
guessable from Docker experience alone.

1. **Never hand-edit anything under `.devstack/`.** Those compose files and
Dockerfiles are generated and are overwritten on the next `generate` or `up`.
Change `workspace.yaml` / `devstack.yaml` and regenerate.
2. **Never run `docker compose` against a devstack stack.** The project name,
labels and the external network are tool-owned; running compose directly forks
a second, parallel stack that shares nothing with the workspace.
3. **Never `docker network rm devstack_shared`.** Compose refuses to manage
`external: true` networks, so devstack owns creating and removing it.
4. **Shared services are reached by DNS alias** — `shared-postgres`,
`shared-redis`, `shared-minio` — never by bare service name. By default
nothing publishes a host port; run `devstack expose` when a GUI client on the
host needs one.
5. **Let devstack perform mutations.** Anything that changes the ledger or the
shared stack takes a cross-process lock. Do not run two `up`s concurrently.
6. **Destructive verbs need `--yes` under `--json`** (`workspace destroy`,
`uninstall`, `db drop`/`reset`/`restore`/`gc`, `resource rm`/`gc`, `s3 rb`,
`queue`/`topic`/`stream rm`). Never pass `--yes` on the user's behalf without
asking them first.
7. **`secret://` values never land in a generated file.** A secret reaches a
container because the compose file lists the variable name with no value and
devstack passes the value through the process environment. Do not try to
inline a resolved secret.
8. **`${ref:...}` uses a colon. `${env.NAME}` and `${self.attr}` use a dot.**
This asymmetry is the single most common config typo.
9. **Deep-merge replaces lists by default.** Opt into appending with
`$merge: append`.

## The commands that cover most work

```bash
devstack up [project...] # network → shared engines → provision → compose up
devstack down [project...] # stop this workspace's stacks, keep the data
devstack status # health + ref graph
devstack logs [service...] # streamed across project and shared stacks
devstack shell [service] # a shell inside a service container
devstack run <task...> # the project's tasks: graph, dependency-ordered
devstack generate --check # is anything stale? (exit non-zero if so)
```

Every headline command supports `--json` and `--quiet`. Use `--json` when you
need to parse the result.

## Common tasks

| Goal | Do this |
|---|---|
| Add a repo to the workspace | `devstack project new <name> --path <dir>` |
| Add a service to a repo | add an entry under `services:` in its `devstack.yaml`, then `devstack generate` |
| Add a shared engine | add an entry under `shared:` in `workspace.yaml`, then `devstack up` |
| Give a project a database | declare it under `resources:`, or `devstack db create <name>` |
| Reach a shared engine from the host | `devstack expose` then `devstack ports` |
| Set an env var on a service | `devstack env set KEY=VALUE --service <svc>` |
| See why a service is unhealthy | `devstack status`, then `devstack logs <service>` |
| Check the host is set up | `devstack doctor` (add `--fix` to repair) |

## Reading further

The whole documentation corpus is compiled into the binary. Do not guess at
behavior — read it:

```bash
devstack ai docs # list every document
devstack ai docs guide/templates # authoring service templates
devstack ai docs guide/config-reference # every config field and grammar
devstack ai docs guide/lifecycle # up / down / status / logs / shell
devstack ai docs guide/shared-services # the shared engines and host access
devstack ai docs guide/databases # the db group
devstack ai docs guide/secrets # secret:// and .env ingestion
devstack ai docs --search "<question>" # search titles and bodies
```

`reference.md` next to this file is a condensed config and flag reference.
Loading
Loading