Skip to content

feature: add tb-devctl local dev orchestrator#6

Merged
trogulja merged 33 commits intomainfrom
feature/devctl
Mar 27, 2026
Merged

feature: add tb-devctl local dev orchestrator#6
trogulja merged 33 commits intomainfrom
feature/devctl

Conversation

@trogulja
Copy link
Copy Markdown
Collaborator

@trogulja trogulja commented Mar 25, 2026

Summary

Adds tb-devctl — a new CLI tool for orchestrating Productive's local dev environment. Manages services in Docker or locally, handles shared infrastructure (MySQL, Redis, Meilisearch, Memcached), and provides a unified interface for start/stop/init/logs/doctor across all repos.

Key capabilities:

  • Docker mode: multiple services in one container via overmind
  • Local mode: services running on host from repos/, with rbenv/nvm version detection
  • Hybrid: mix Docker and local freely
  • Dynamic docker-compose generation from tb-devctl.toml
  • Health checks: Docker, Caddy, AWS SSO, infra, repos, secrets (tb-devctl doctor)
  • Presets for common service combinations
  • Per-service env vars split by mode (env, env_docker, env_local)
  • Local requirements checking (ruby, node, chrome versions)

Test plan

  • tb-devctl doctor — verifies environment health
  • tb-devctl infra up && tb-devctl start api,frontend --docker — Docker mode
  • tb-devctl start frontend --local --bg — local mode
  • cargo fmt --check && cargo clippy --workspace -- -D warnings — CI gates

trogulja and others added 30 commits March 25, 2026 18:12
Scaffolds the devctl CLI tool with:
- Config loading: walks up directory tree to find devctl.toml, parses
  all services/infra/docker config
- State tracking: JSON state file in .devctl/ for service mode/PID tracking
- Health checks: TCP port probing, Docker daemon check, Caddy check,
  compose project status, port owner detection via lsof
- `devctl status`: shows all services with mode, running state, and URL,
  plus infra status with per-service port probes
- `devctl infra up/down/status`: manages shared infrastructure via
  docker compose, auto-creates volumes

Phase 1 of devctl — replaces session.sh incrementally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- `devctl start <svc,...> --docker`: full Docker mode start with port
  conflict detection, repo/secrets validation, auto-infra startup,
  Procfile generation (with runtime version wrappers), container
  lifecycle, healthcheck waiting, state tracking, env capture
- `devctl stop`: stops Docker container, clears state
- `devctl restart <service>`: restarts individual service via overmind
- `devctl logs <service>`: captures logs from overmind tmux pane
  (app services) or docker compose logs (infra services)
- `devctl doctor`: comprehensive diagnostic — checks Docker, Caddy,
  infra ports, repo presence, secrets, and reports issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Status now queries overmind inside the container for Docker service
  state instead of TCP port probing (which gave false positives due
  to Docker's static port bindings)
- Infra status now uses docker compose ps instead of host port probing
  (works even when ports aren't published to host)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pad text before colorizing to prevent invisible escape codes from
throwing off column widths. Also return (text, color) tuples from
state detection for cleaner separation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per decisions.md: "No --skip-setup — Always run setup steps. Fast when
nothing changed. Removes a decision point that adds no value."

Setup always runs on container start. Init (first-time secrets, schema,
seeding) is separate via `devctl init`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- `devctl init <service>`: runs init steps from devctl.toml (secrets,
  schema:load, seeding). Runs inside Docker container if running,
  otherwise on host in repos/<service>.
- `devctl start <service> --local [--dir <path>] [--bg]`: local mode
  start with setup steps (git pull with dirty guard, deps, migrate,
  git restore), port conflict check, secrets validation, auto-infra,
  PID cleanup. Foreground (default) or background with log file.
- `devctl stop <service>`: stops local background service by PID.
- Updated CLI: --docker and --local are mutually exclusive flags,
  --dir and --bg require --local.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Direct commits to main are no longer allowed. Use feature branches
and draft PRs for review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Init checks AWS SSO session before running steps that need
  secrets-manager (pattern match on step content)
- Doctor reports AWS SSO status as a warning
- health::aws_sso_is_valid() calls aws sts get-caller-identity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Doctor shows full SSO status with time remaining (warns at <30min).
Status only shows SSO info when expiring soon or expired — no clutter
when session is healthy.

Reads expiry from ~/.aws/sso/cache/*.json (same approach as paws).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Static dev-compose.yml bound all 14 ports regardless of which services
were running, blocking local mode for any service whose port was mapped.

devctl now generates docker-compose.yml at runtime with only the ports
and mounts needed for the requested Docker services. This enables the
core hybrid mode: Docker for some services, local for others.

Tested: api in Docker + frontend locally, both accessible via Caddy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bundle install needs write access to RVM gem directories which are
owned by root. Init steps run as root (same as setup.sh), matching
the container's privilege model.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When running init steps as root inside the container, the AWS SDK
looks for ~/.aws in /root (root's home) instead of /home/dev (where
the host's ~/.aws is mounted). Setting HOME=/home/dev in the docker
exec environment fixes credential resolution.

Tested: secrets-manager, bundle install, rails db:create, and
schema:load safety guard all work correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes from testing:

1. Declarative Docker start now stops existing container BEFORE checking
   port conflicts. Previously it checked ports first, failed because our
   own container was using them.

2. State is written immediately after container starts (before waiting for
   healthcheck). This ensures `devctl status` works during boot, even if
   the healthcheck takes a long time (e.g., Ruby 4.0.1 compilation).

3. Healthcheck timeout increased from 2 to 10 minutes to handle first-time
   setup with multi-Ruby compilation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
From bloat detector review:
- Remove dead `repos` BTreeSet, use collected repo names for
  SELECTED_REPOS env var (was passing service names, not repo names)
- Remove dead port-check block in doctor (TCP connect with no effect)
- Extract health::infra_is_running() helper (was duplicated 5 times)
- Move default Ruby/Node versions to module-level consts
- Remove unused _config parameter from procfile_entry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- container_is_running now uses ^name$ anchor for exact match
  (docker ps filter does substring by default, causing false positives)
- Removed hardcoded empty BUGSNAG_AUTH_TOKEN from generated compose
  (was overriding the value from .env.session)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The hardcoded fallback AWS_DEFAULT_REGION=eu-central-1 was overriding
~/.aws/config (which has region=us-east-1). This caused secrets-manager
pull to look in the wrong region inside the container.

Now only forwards AWS env vars if explicitly set on the host. The AWS
SDK reads the correct region from the mounted ~/.aws/config.

Tested: secrets-manager pull works inside the container.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CI=true suppresses pnpm's interactive "reinstall modules?" prompt
  that blocks in non-interactive Docker environments
- COREPACK_ENABLE_DOWNLOAD_PROMPT=0 and COREPACK_ENABLE_AUTO_PIN=0
  prevent corepack from prompting when downloading pnpm versions

Discovered during live testing with frontend service.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Exporter reads redis_host (lowercase) from process.env, not REDIS_URL.
Added redis_host=productive-dev-redis to generated compose environment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Points Puppeteer at the arm64 Chromium binary from Playwright's CDN,
installed in /opt/chromium in the Docker image.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bash -lc doesn't put rbenv shims first in PATH, causing system Ruby
to be used instead of the version in .ruby-version. Fix: prepend
`eval "$(rbenv init - bash)"` when .ruby-version exists, and source
nvm when .node-version/.nvmrc exists.

Also: resolve --dir paths relative to project root, not cwd.

Tested: devportal from worktree with Ruby 4.0.1 via rbenv.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Services can now declare environment variables in devctl.toml:

  [services.ai-agent]
  env = { NODE_EXTRA_CA_CERTS = "/etc/pki/tls/certs/ca-bundle.crt" }

Docker mode: added to generated compose environment block.
Local mode: exported before the service command in bash.

This bridges the gap between production Dockerfiles (which set their
own env vars) and the shared dev container (which uses a common image).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Services can now declare mode-specific environment variables:
- env: shared across all modes
- env_docker: Docker mode only (overrides env)
- env_local: local mode only (overrides env)

Example: ai-agent needs NODE_EXTRA_CA_CERTS but the path differs
between Docker (/etc/pki/tls/certs/ca-bundle.crt) and macOS
(/etc/ssl/cert.pem).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Presets are named configurations in devctl.toml that expand to
services + mode + env vars:

  devctl start --preset frontend-only
  devctl start --preset ai-dev

Preset runs services in the configured mode (local/docker), with
env vars set before launching. For local mode, all services except
the last run in background; the last runs in foreground.

Also adds --version flag (standard CLI convention).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Set CI=true in shell_cmd to suppress pnpm interactive prompts
  (same as Docker mode)
- nvm init no longer breaks the command chain if nvm isn't installed
  (uses semicolons + `true` fallback instead of &&)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Doctor now shows a per-service readiness matrix (LOCAL/DOCKER columns)
with a detailed issues section below. Each service in devctl.toml can
declare local hard dependencies via `requires = ["ruby", "node", ...]`.

Runtime checks (ruby, node, python3) auto-detect the version manager
(rbenv/rvm, nvm/fnm/volta, pyenv) and verify the required version from
.ruby-version/.node-version/.python-version is installed. `n` is not
supported as a Node version manager (single active version limitation).

Companion services (e.g. sidekiq) show as "(companion of api)" instead
of independent checks. Chromium checks for actual binaries in the
Puppeteer cache. SSO cache parsing no longer aborts on a single bad file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@trogulja trogulja marked this pull request as ready for review March 27, 2026 10:25
@trogulja trogulja requested a review from ilucin March 27, 2026 10:27
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
trogulja and others added 2 commits March 27, 2026 13:17
- Crate directory: crates/devctl/ → crates/tb-devctl/
- Package and binary name: devctl → tb-devctl
- Config file on disk: devctl.toml → tb-devctl.toml
- State/log dir on disk: .devctl/ → .tb-devctl/
- All user-facing messages, help text, generated comments
- Tooling: bump.sh, install.sh, release.yml, publish skill

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Feature planning docs don't belong in this repo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@trogulja trogulja changed the title Add devctl: local dev environment orchestrator feature: add tb-devctl local dev orchestrator Mar 27, 2026
@trogulja trogulja merged commit 1c29fc7 into main Mar 27, 2026
1 check passed
@trogulja trogulja deleted the feature/devctl branch March 27, 2026 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants