Local development environment framework for web applications. Go CLI that shells out to the docker CLI (never the Docker SDK - keeps the door open for Podman). Single command startup, shared infrastructure (Traefik, Mailpit, Adminer), project isolation via Docker networks.
About this file: agent guidance only - decisions, conventions, and gotchas that can't be inferred from code. Every line biases behavior and competes for attention. When editing, ask per line: "Would removing this cause an agent to make a mistake?" If not, cut it. Don't add file listings, stack summaries, or anything agents discover by grepping.
- Run
make testbefore every commit (fast, mock runtime). - Run
make test-integrationbefore releases, or after changing project lifecycle / routing / Mutagen / runtime code (spins up real Docker, takes minutes). - Never commit, push, or tag without explicit user confirmation. Show the proposed commit message and wait for approval before
git commit,git push, orgit tag. Never add "Co-Authored-By" lines.
Release process:
- Add
## vX.Y.Zsection at the top ofCHANGELOG.md. git add CHANGELOG.md && git commit -m "Release vX.Y.Z"git tag vX.Y.Z && git push origin main && git push origin vX.Y.Z- CI builds darwin/linux (arm64/amd64) binaries and creates the GitHub Release from the changelog.
Never use em-dashes (—). Use regular hyphens (-) everywhere: code, copy, comments, docs.
- No top-level
volumes:in project config. Unlike Docker Compose, named volumes don't need separate declaration - anything in a servicevolumes:entry that doesn't start with/or.is auto-discovered as a named volume (parseVolumeMount()). - Config
variables:are NOT env vars. They're${VAR}placeholders substituted at config-load time (second pass ofLoadProject(), afterPROJECTNAMEresolves). They don't reach containers - that's whatenvironment:is for. - Justfile commands live in
.scdev/commands/<name>.just, not a single Justfile. Resolution order: built-in > justfile > error. - Mutagen auto-detection: enabled on macOS, disabled on Linux. Controlled by
~/.scdev/global-config.yaml, not project config. routing.domainon a service enables a per-service custom domain (HTTP/HTTPS only). Without it, services share the project domain. Useful for frontend + backend splits.- Default domain
scalecommerce.siteis wildcard DNS resolving to 127.0.0.1 - not a real site, just a resolver trick. - Framework progress messages use
ui.StatusStep(), notfmt.Println- two blank lines + cyan▶+ bold text, so they stand out from verbose nested command output. Mirrored by thescdev step <message>subcommand for template justfiles (templates should call@scdev step "..."instead of@echo "..."for top-level progress markers).
internal/config/defaults.gois the single source of truth for images, versions, and the default domain. Change once, everything picks it up.buildContainerConfig()ininternal/project/project.gois the single source of truth for container configuration. It stamps anscdev.config-hashlabel covering image, env, volumes, command, working dir, routing labels, ports, and network aliases.scdev updaterecreates any service whose stamped hash differs. Any new service config field that should shape a container must flow throughbuildContainerConfig- otherwisescdev updatewon't detect changes to it.runtime.ComputeConfigHash/runtime.StampConfigHashininternal/runtime/confighash.goare the shared hash helpers. ProjectbuildContainerConfigand every shared-service*ContainerConfigfunction (internal/services/adminer.go,mail.go,redis_insights.go,router.go) stamp the samescdev.config-hashlabel.services.Manager.startServicecompares the running container's hash to the freshly built expected config on every call and recreates on mismatch - this is what makesscdev services startpick up SSL/image/domain changes instead of silently starting a stale container. Any new shared-service config field that should shape the container must flow through the*ContainerConfigfunction so it ends up in the hash; don't add per-field comparators. The router has a port-superset carve-out:StartRouterhashes against the UNION of the running container's ports and state-required ports, so extra ports from a now-removed project don't force a recreate. Intentional port shrinking still happens viaRefreshRouteron project removal.ContainerNameFor(service, project)builds container names without a loadedProject. Use it instead offmt.Sprintf("%s.%s.scdev", ...).- Link networks are runtime relationships between projects, stored in global state (
~/.scdev/state.yaml), not project config. Each creates ascdev_link_<name>network. Containers resolve each other by container name via Docker's embedded DNS. - Template repos follow the naming convention
scdev-template-<name>(matters forscdev createresolution).
Use withProject(timeout, fn) or withDocker(timeout, fn) from cmd/shared.go. They handle the context, requireDocker check, and (for withProject) project.Load in one line. Without the Docker check users get cryptic low-level failures instead of a clear "Docker isn't running" message.
Easy-to-miss steps when wiring a new shared service:
- Container name constant in
internal/services/<service>.go. <Service>ContainerConfig(...)function that stampsruntime.StampConfigHash- same pattern asmail.go/redis_insights.go.Start<Service>/Stop<Service>/<Service>Statusonmanager.go.Connect<Service>ToProjectmust pass network aliases so project containers can resolve it by short name.- Add an entry to
AllSharedServices()ininternal/services/registry.go- this single registry drives CLI start/stop/status,services recreate, and per-project connect/disconnect. Do not add parallel registries incmd/orinternal/project/. - Add the image constant to
internal/config/defaults.go. - Add the per-project opt-in flag to
ProjectSharedConfigininternal/config/config.goand reference it in the registry'sProjectEnabledclosure.
- Project domains don't work for inter-container communication.
*.scalecommerce.siteresolves to 127.0.0.1, which inside a container points at the container itself, not Traefik. Cross-project containers must use container names (app.project-b.scdev) - this is whyscdev linkuses Docker DNS, not routing. - Mutagen ignored paths are not synced either way.
node_modulesand.pnpm-storemust be ignored for Node.js projects so they stay inside the container at native speed. IDE autocomplete still works via the host's ownpnpm install/ hostnode_modules. .pnpm-storeMUST be in the mutagen ignore list for pnpm projects. pnpm builds a ~500MB content-addressable store with platform-specific native binaries (glibc vs musl) inside the project dir. Without ignoring it, syncing those binaries to the host breaks the next time the container image changes.- Only directory bind mounts sync via Mutagen. Single-file mounts stay as regular bind mounts.
- Sync-ready gate:
buildContainerConfigwraps commands with a wait on/.scdev-sync-readywhen Mutagen is enabled for that service. Don't add your ownwhile [ ! -f ... ]workaround - it's already there. scdev renamemigrates volumes via a temp container using a project service image (guaranteed present locally). Docker has no native volume rename. SeeCopyVolumeon the Runtime interface. All copies happen before any old volumes are removed, to bound blast radius on failure.- The docs page (
docs.shared.<domain>) is also Traefik's 404 catch-all - unmatched URLs land there, not a generic error page. - Integration tests that tear down shared services (router, mail, db, redis) must snapshot beforehand and restore afterward via
snapshotSharedServices/restoreSharedServices. Forgetting this silently breaks the developer's running environment.
README.md- user-facing docs and marketing; needs updating for any user-visible change (new commands, config options, shared services, CLI flags).templates/README.md- template authoring guide; update alongsideREADME.mdwhen changing config options, variables, Mutagen behavior, or the create/setup workflow.CONTRIBUTING.md- developer onboarding (structure, testing strategy, how to add commands/services). Update it when architectural decisions or test patterns change.Completo-Briefing.md- context for Completo's AI features. Regenerate with/completo-briefing.