feature: add tb-devctl local dev orchestrator#6
Merged
Conversation
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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ilucin
approved these changes
Mar 27, 2026
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
repos/, with rbenv/nvm version detectiontb-devctl.tomltb-devctl doctor)env,env_docker,env_local)Test plan
tb-devctl doctor— verifies environment healthtb-devctl infra up && tb-devctl start api,frontend --docker— Docker modetb-devctl start frontend --local --bg— local modecargo fmt --check && cargo clippy --workspace -- -D warnings— CI gates