|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## What this is |
| 6 | + |
| 7 | +`squarectl` is a Crystal CLI that wraps `docker compose`, `docker swarm` (configs/secrets), |
| 8 | +and `kubectl`/`kompose`. It reads a single YAML config (`squarectl.yml` by default) describing |
| 9 | +one or more **environments** (development, staging, production, ...) and renders the right |
| 10 | +env vars, compose file lists, networks, SSL certs, and deploy artifacts before shelling out to |
| 11 | +the underlying tool. The user picks a **target** (`compose`, `swarm`, `kubernetes`) and an |
| 12 | +**environment**; squarectl assembles the command and `exec`s it. |
| 13 | + |
| 14 | +## Toolchain & commands |
| 15 | + |
| 16 | +The dev environment is managed by [mise](https://mise.jdx.dev) (`mise.toml`), which pins |
| 17 | +Crystal and defines all tasks. Run tasks with `mise <task>`: |
| 18 | + |
| 19 | +- `mise dev:deps` — `shards install` |
| 20 | +- `mise dev:build` — compile a dev binary to `bin/squarectl` |
| 21 | +- `mise dev:spec` — run the test suite (`crystal spec`) |
| 22 | +- `mise dev:ameba` — static analysis (`bin/ameba`) |
| 23 | +- `mise dev:format` — `crystal tool format src/` |
| 24 | +- `mise release:build` / `mise release:static` — release binaries (static build runs via Docker Buildx / `docker-bake.hcl`) |
| 25 | + |
| 26 | +Run a single spec file / example directly: |
| 27 | +```sh |
| 28 | +crystal spec spec/squarectl/squarectl/task_factory_spec.cr |
| 29 | +crystal spec spec/squarectl/squarectl/task_factory_spec.cr:42 # by line |
| 30 | +``` |
| 31 | + |
| 32 | +Tests use **spectator** (not Crystal's built-in spec DSL) — see `spec/spec_helper.cr`. |
| 33 | +`Spectator.reset` calls `Squarectl.reset_config!` after each example because config is held in |
| 34 | +class-level state on the `Squarectl` module (`@@config`, `@@environments`). Any test that loads |
| 35 | +config relies on that reset. |
| 36 | + |
| 37 | +Note: on macOS, `mise dev:fix-shards-command` is a required workaround for a Crystal packaging |
| 38 | +bug (crystal-lang/crystal#16746) before `shards` works — CI runs it before installing deps. |
| 39 | + |
| 40 | +## Request flow (the important architecture) |
| 41 | + |
| 42 | +Understanding one command path explains all of them — every subcommand follows the same 4-layer |
| 43 | +pipeline. Example: `squarectl compose up staging`. |
| 44 | + |
| 45 | +1. **`src/squarectl/cli/*.cr`** — Admiral command tree. `CLI` (`cli.cr`) registers top-level |
| 46 | + subcommands (`compose`, `swarm`, `configs`, `secrets`, `kube`, `info`, `completion`), each of |
| 47 | + which registers its own leaf commands. Every leaf `run` does the same three steps: |
| 48 | + `Squarectl.load_config(flags.config)` → `Squarectl.find_environment(env, TARGET)` → |
| 49 | + `TaskFactory.build(...)` → hand off to a `Tasks::*` class. The `SQUARECTL_TARGET` constant on |
| 50 | + each command class ("compose"/"swarm"/"kubernetes") is what selects the target. |
| 51 | + |
| 52 | +2. **`src/squarectl/task_factory.cr`** — the merge engine. Builds a `Task` by combining the |
| 53 | + selected environment with the special `"all"` environment (global defaults). Env vars, domains, |
| 54 | + compose files, networks, SSL certs, setup commands, configs, and secrets are each resolved by |
| 55 | + selecting entries whose `target:` matches (via `find_matching_target`, which honors `"all"`) |
| 56 | + and merging global-then-env. Heavy use of macros (`define_method_hash` / `define_method_array`) |
| 57 | + generates the per-attribute resolvers. Also derives `_DOMAIN`/`_SCHEME` vars from any `_URL` |
| 58 | + var (`decompose_urls`) and namespaces config/secret keys as `<app>-<envshort>__<key>`. |
| 59 | + |
| 60 | +3. **`src/squarectl/tasks/*.cr`** — thin orchestration per target. Class methods like |
| 61 | + `Tasks::Compose.up` decide the sequence of side effects (e.g. `up` creates SSL certs + networks |
| 62 | + first, then execs; `clean` tears them down). These call methods mixed into `Task`. |
| 63 | + |
| 64 | +4. **`src/squarectl/commands/*.cr`** — the actual command builders, mixed into `Task` (see the |
| 65 | + `include` list in `task.cr`). `build_docker_compose_command` assembles argv, splits user args |
| 66 | + into pre-args (before the action) vs post-args via the `PRE_ARGS_*` allowlists in |
| 67 | + `commands/compose.cr`, and handles compose v1 (`docker-compose`) vs v2 (`docker compose`). |
| 68 | + Everything ultimately runs through **`Executor`** (`executor.cr`): `run_command` (wait), |
| 69 | + `exec_command` (replace process), or `capture_output`. Set `SQUARECTL_DEBUG=true` to print the |
| 70 | + full command line + env instead of guessing what ran. |
| 71 | + |
| 72 | +## Config model & conventions |
| 73 | + |
| 74 | +- `src/squarectl/config/*.cr` are `YAML::Serializable` structs mapping the `squarectl.yml` schema |
| 75 | + (`SquarectlConfig` → `app`, `compose.version`, `environments[]`; each `SquarectlEnvironment` has |
| 76 | + `server`, `env_vars`, `networks`, `compose_files`, `domains`, `ssl_certificates`, |
| 77 | + `setup_commands`, `config_files`, `secret_files`). |
| 78 | +- Config is a **Crinja (Jinja) template** rendered with `ENV` before parsing — `Squarectl.load_config` |
| 79 | + runs `Crinja.render` first, so `{{ ENV.FOO }}` works in `squarectl.yml`. |
| 80 | +- Directory conventions relative to the project (`Squarectl.root_dir` = cwd): |
| 81 | + `squarectl/base.yml`, `squarectl/targets/<target>/{common,<env>}.yml`, |
| 82 | + `squarectl/targets/common/`, and `kubernetes/<env>/`. These base compose files are always |
| 83 | + prepended to the resolved compose file list (see `compose_file_*_for` in `squarectl_environment.cr`). |
| 84 | +- The environment named **`all`** is global defaults merged into every other environment. An entry |
| 85 | + with `target: all` (or a target array) applies across targets. |
| 86 | +- `development` environment is restricted to the `compose` target only (`find_environment` raises otherwise). |
| 87 | +- Runtime vars injected into every task: `SQUARECTL_CWD/APP/TARGET/ENV/ENV_SHORT` (see `Task#runtime_env_vars`). |
| 88 | + |
| 89 | +## Conventions |
| 90 | + |
| 91 | +- `ameba:disable` inline comments are used deliberately (e.g. for `not_nil!` and Admiral's |
| 92 | + `Lint/UselessAssign` on flag definitions) — keep them when editing those lines. |
| 93 | +- Commit messages follow Conventional Commits (`commitlint.config.js`): `feat:`, `fix:`, `build:`, |
| 94 | + `ci:`, etc. |
| 95 | +- crystalline (the Crystal LSP, in `mise.toml [tools]`) is for editors only and is disabled in CI |
| 96 | + via `MISE_DISABLE_TOOLS` because its macOS binary needs Homebrew LLVM. |
0 commit comments