Package boundaries
zook/
├── main.go # entry point: calls cli.Run
├── internal/
│ ├── cli/ # command parsing, human-readable output
│ │ ├── cli.go # command dispatch (deploy/rollback/status/releases/list/logs)
│ │ └── status.go # status table + logs display
│ ├── core/ # release logic, state — runtime-agnostic
│ │ ├── engine.go # Deploy + Rollback orchestration
│ │ └── state.go # State, HistoryEntry, LoadState, SaveState
│ ├── runtime/
│ │ ├── runtime.go # Runtime interface
│ │ └── docker/ # DockerRuntime: shells out to `docker compose`
│ ├── config/ # stack discovery, FindStack, DiscoverStacks
│ └── exec/ # Runner interface + OSRunner (thin shell-out wrapper)
The Runtime interface
The Runtime interface is the central seam in Zook's design. Everything above
it is runtime-agnostic; everything below it is implementation-specific.
type Runtime interface {
Preflight(ctx, stack, log) (missingHealthchecks []string, err error)
Pull(ctx, stack, version, log) error
Up(ctx, stack, version, timeout, log) error
Health(ctx, stack) (healthy bool, err error)
}
DockerRuntime (V1) implements this interface by shelling out to the docker
CLI. It calls:
docker compose config --services+docker compose config --format jsonfor preflightdocker compose pullfor pulldocker compose up -d --wait --wait-timeout <secs>for updocker compose ps --format jsonfor health queries
A later NativeRuntime (V2) will implement the same interface using systemd
units and release directories — no changes required in core, cli, or
config.
The core engine
core.Engine owns the release logic. It is the reusable value of the tool.
Engine.Deploy sequence:
- Load
state.json(missing file = zero state, not an error). - Open a log file for this operation.
- Call
rt.Preflight. If any services are missing healthchecks, refuse and return without touching the running state. - Call
rt.Pull. - Call
rt.Up. If it succeeds, callstate.recordSuccessand save. - If
rt.Upfails and apreviousversion exists, callrt.Upagain withprevious. If that succeeds, callstate.recordRolledBackand return an error describing what happened. If the rollback also fails, callstate.recordFailed, save, and return an error demanding manual intervention.
Engine.Rollback is a simplified path: load state, verify previous exists,
call rt.Up, record the outcome.
The CLI as a thin transport
The cli package is one transport over the engine. It:
- Reads
ZOOK_STACKS_ROOTandZOOK_TIMEOUTfrom the environment. - Resolves the stack directory using
config.FindStack. - Calls
Engine.DeployorEngine.Rollback. - Formats output for a human terminal.
A future zook serve daemon would wrap the same core.Engine with an HTTP
handler — zero rework to the engine, state management, or Runtime interface.
Atomic state writes
SaveState writes state.json by creating a temp file in the same .zook/
directory and then renaming it over the target path. On POSIX systems, rename is
atomic: a crash mid-write leaves the old state.json intact.
Exec abstraction
The exec.Runner interface wraps the actual shell-out. OSRunner uses
os/exec. In tests, a fake runner is injected so that no real Docker daemon is
required. This keeps the test suite fast and hermetic.