runner

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 2, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

Documentation

Overview

Package runner builds the system prompt that devcell injects into agent CLIs (claude, opencode, codex) and the cell serve HTTP server.

The prompt has two distinct conceptual layers, always concatenated in order — see ContainerContext and ResolveSystemPrompt — and a third per-request layer that lives outside this package (cell serve merges per-request `instructions` / `system` role from the API body into the user prompt directly).

Index

Constants

View Source
const DefaultNixhomeGitRef = "feature/wip"

DefaultNixhomeGitRef is the github branch/tag used when no local nixhome is available and the cell binary doesn't carry a release version (v0.0.0 / dev builds). Set to feature/wip while DIMM-198 lives off main; flip back to "main" when the pure path lands.

View Source
const (
	// DefaultRegistry is the fallback registry prefix for devcell images.
	DefaultRegistry = "public.ecr.aws/w1l3v2k8/devcell"
)

Variables

View Source
var Modules []string

Modules is the list of extra nix modules composed on top of the stack. Set from CellConfig at startup.

View Source
var PerSessionImage bool

PerSessionImage tags user images per tmux session instead of per stack. Set from CellConfig at startup; defaults to false (stack-based).

View Source
var Registry = DefaultRegistry

Registry is the active container registry. Set via cfg.ResolvedRegistry() at startup; defaults to DefaultRegistry.

View Source
var Stack = "base"

Stack is the resolved nix stack name (e.g. "ultimate", "go"). Set from CellConfig at startup; defaults to "base".

Functions

func AcquireImage added in v0.7.0

func AcquireImage(ctx context.Context, d AcquireDeps) error

AcquireImage walks the action sequence, invoking the matching closure for each effective action and stopping at the first one that returns nil. If every action fails the returned error joins all attempts so the user sees the full chain.

func AssembleSystemPrompt added in v0.6.0

func AssembleSystemPrompt(c config.Config, cellCfg cfg.CellConfig, opts ResolveOpts) (string, error)

AssembleSystemPrompt is the single entry point callers should use to build the string passed to claude's --append-system-prompt (or any future agent's equivalent). It prepends ContainerContext to the resolved prompt with a blank-line separator. When the resolved prompt is empty, returns just ContainerContext.

func BaseImageTag

func BaseImageTag() string

BaseImageTag returns the base image tag used in scaffold FROM, allowing override via DEVCELL_BASE_IMAGE env var (local dev, CI, tests).

func BuildArgv

func BuildArgv(spec RunSpec, fs FS, lookPath func(string) (string, error)) []string

BuildArgv constructs the full docker run argv for the given spec. It is pure given injectable FS and LookPath.

func BuildImage

func BuildImage(ctx context.Context, configDir string, noCache bool, verbose bool, out io.Writer) error

BuildImage runs docker build to build UserImageTag from configDir. Legacy Dockerfile path; reached via `cell build --impure` after DIMM-213. verbose=true streams plain-text output to out; verbose=false suppresses all docker output (quiet mode) and captures stderr to out for error replay. --pull is always passed so Docker checks for a newer base image digest and busts the layer cache when the upstream image has been updated.

func BuildImagePure added in v0.7.0

func BuildImagePure(ctx context.Context, spec PureBuildSpec, tag string, verbose bool, out io.Writer) error

BuildImagePure runs `nix build` for the pure-image flake output and loads the result into the Docker daemon as UserImageTag(). Strictly nix2container — never falls back to docker build.

func BuildPrunePrompt added in v0.7.0

func BuildPrunePrompt(opts PruneOpts) string

BuildPrunePrompt returns the user-facing confirmation warning text for the given prune mode. All prompts share the format:

⚠  This will delete ALL <specific-list>.
   Target: <host-specific-detail>
   Continue? [y/N]

The exact list and target line are mode-specific, per DIMM-200.

func BuildVagrantSSHArgv added in v0.5.0

func BuildVagrantSSHArgv(spec VagrantSpec) []string

BuildVagrantSSHArgv constructs the remote-command argv for:

vagrant ssh -- -t bash -l -c "cd ~/project && [env KEY=VAL ...] <binary> <defaultFlags...> <userArgs...>"

The remote command is wrapped in `bash -l -c "..."` so that the login shell sources ~/.profile and ~/.nix-profile/etc/profile.d/nix.sh, putting home-manager-installed binaries (claude, codex, etc.) on PATH.

When ProjectDir is set, the command cds into ~/basename(ProjectDir) first, mirroring Docker's --workdir behaviour. The post-up rsync trigger syncs the project there, so the agent sees the correct working directory.

The caller is responsible for running the command with its working directory set to VagrantDir (via cmd.Dir) so vagrant finds the correct Vagrantfile. It is a pure function: no I/O, no exec.

func ChangedBuildFiles added in v0.4.0

func ChangedBuildFiles(configDir string) ([]string, bool)

ChangedBuildFiles returns which build context files are newer than the image. Returns the list of changed file names and true if any changed.

func ConfirmDestructive added in v0.7.0

func ConfirmDestructive(out io.Writer, in io.Reader, skipYes bool, isTTY bool, warning string) bool

ConfirmDestructive prints `warning` to out, reads one line from in, and returns true iff the user typed y/Y/yes/YES (case-insensitive, trimmed).

skipYes=true bypasses the prompt entirely (used for `-y` / `--yes`). The warning is not printed in this case — the user already opted in.

isTTY=false without skipYes refuses to proceed (returns false after printing a refusal message that mentions --yes). This prevents accidental destruction when stdin is piped (e.g. `echo y | cell build prune --force`).

func ContainerContext added in v0.6.0

func ContainerContext(c config.Config, cellCfg cfg.CellConfig) string

ContainerContext returns the auto-generated filesystem/runtime preamble — bind mounts, host path mappings, hard constraints — describing the devcell container the agent is running inside. Pure container facts; no user-controllable content.

This is what makes the agent file-aware: when the user mentions a host path, the agent can translate it to the matching container path. Every surface that ships a system prompt (cell claude, cell serve) prepends this so the agent reasons correctly about its filesystem.

func DiffBuildFile added in v0.4.0

func DiffBuildFile(configDir, name string) string

DiffBuildFile returns a unified diff between the local build context file and the version baked into the image. Returns "" if the file isn't in the image (e.g. Dockerfile) or if they're identical. Uses docker cp to extract.

func DiscoverStacks added in v0.4.0

func DiscoverStacks(ctx context.Context, configDir string, out io.Writer) ([]string, error)

DiscoverStacks runs nix flake lock + discovers available stacks from the locked devcell input inside a Docker container. Returns stack names (e.g. "base", "go"). Falls back to nil on error (caller should use hardcoded defaults).

func DockerfileChanged

func DockerfileChanged(configDir string) bool

DockerfileChanged reports whether any build-input file in configDir (Dockerfile, flake.nix) is newer than the user image. Returns true when the user image doesn't exist or inspect fails.

func EnsureNetwork

func EnsureNetwork(ctx context.Context) error

EnsureNetwork creates the devcell-network docker network if it doesn't exist.

func FormatImageVersionUserExport added in v0.7.0

func FormatImageVersionUserExport(m ImageMetadata) string

FormatImageVersionUserExport is the test seam for formatImageVersionUser.

func FormatJSON added in v0.7.0

func FormatJSON(snap DFSnapshot, opts FormatOpts, w io.Writer) error

FormatJSON emits a stable schema for scripting. Keys are camelCase so `jq` recipes can be pinned across releases. The order of entries is the same as FormatTable — sorted by ReclaimBytes desc — so a single `jq '.entries[0]'` always picks the biggest reclaimable thing.

func FormatTable added in v0.7.0

func FormatTable(snap DFSnapshot, opts FormatOpts, w io.Writer) error

FormatTable renders the ranked entries as a human-readable table with a totals footer and bottom reclaim hint. Mirrors the layout shown in the `cell build df` design (DIMM-221).

func HumanBytes added in v0.7.0

func HumanBytes(n int64) string

HumanBytes formats a byte count as "1.4 GB" / "237 MB" / "812 KB" / "42 B". Uses 1024-based units (KiB/MiB/GiB semantics) but the conventional abbreviations users expect from `docker images`.

func ImageExists

func ImageExists(ctx context.Context, tag string) bool

ImageExists returns true if a Docker image with the given tag exists locally.

func ImageVersions added in v0.3.0

func ImageVersions(ctx context.Context) (base, user string)

ImageVersions reads build metadata from the user image. Returns (base, user) strings for backward compatibility with callers. Format: user = "<commit> built <date>" when both known,

"built <date>" when only date,
"<commit>" when only commit,
"" otherwise (falls through to legacy file read).

func LocalImageID

func LocalImageID(ctx context.Context) (string, error)

LocalImageID returns the full image ID (sha256:...) of the user image. Used to pin the running container to the exact image just built, rather than the mutable tag which could race with a concurrent build.

func LocalImageIDFor added in v0.7.0

func LocalImageIDFor(ctx context.Context, tag string) (string, error)

LocalImageIDFor returns the image ID for an arbitrary tag. Used with UserImageTagPure() to pin pure-built runtime containers.

func LocalImageSize added in v0.7.0

func LocalImageSize(ctx context.Context, tag string) int64

LocalImageSize returns the on-daemon size in bytes of the local image with the given tag. Used post-build to show "Loaded: 1.4 GB" alongside the spinner success — skopeo's per-blob progress is empty when stdout isn't a TTY, so we synthesize a summary from `docker image inspect` instead.

Returns 0 on any error; callers treat 0 as "size unknown, skip the summary".

func NixSystemFor added in v0.7.0

func NixSystemFor(goos, goarch string) string

NixSystemFor maps a Go (GOOS, GOARCH) pair to nix's system identifier (e.g. "aarch64-darwin", "x86_64-linux"). Pure function for testability.

This identifies the HOST running `cell`, not the image target. The flake exposes `packages.<hostSystem>.devcell-<stack>-pure-image` for every supported host: on Darwin hosts that package wires n2c with darwin pkgs (so IFD helpers and the bundled copy-to-docker-daemon are darwin-runnable) while keeping image content (copyToRoot) at aarch64-linux. On Linux hosts host == target, so everything is Linux.

func ParseSize added in v0.7.0

func ParseSize(s string) (int64, error)

ParseSize handles docker's CLI size format. Examples:

"29.8GB" → 29_800_000_000   (decimal GB, not GiB — docker convention)
"813.6kB" → 813_600         (lowercase k, like docker prints)
"0B" → 0
"N/A" or "" → -1            (volume with no size info)

Returns -1 for unknown so the caller can distinguish "small but real" from "size not reported."

func ParseVagrantGlobalStatus added in v0.5.0

func ParseVagrantGlobalStatus(output string) map[string]string

ParseVagrantGlobalStatus extracts running devcell VM entries from `vagrant global-status` output. Returns projectBasename → machineID for running VMs.

Only VMs whose directory ends in ".devcell" are considered devcell cells. UTM reports state as "started"; other providers use "running" — both are accepted.

Output format:

id       name    provider  state    directory
abc1234  default utm       started  /Users/dmitry/dev/myproject/.devcell

func ParseVagrantPortOutput added in v0.5.0

func ParseVagrantPortOutput(output, guestPort string) (string, bool)

ParseVagrantPortOutput extracts the host port for guestPort from `vagrant port --machine-readable` output. Line format: timestamp,target,forwarded_port,guestPort,hostPort

func PickImageTag added in v0.7.0

func PickImageTag(impure bool) string

PickImageTag is the single seam every runtime caller (`cell claude`, `cell shell`, `cell codex`, `cell gemini`, …) uses to decide which local image variant to exec into.

After the 2026-05-15 flip (DIMM-204) + DIMM-213 vocab rename, pure is the default and `impure` (was `debian`) is the opt-in legacy path:

impure=false (default):    UserImageTagPure() (nix2container, devcell-user:<stack>-pure)
impure=true  (--impure):   UserImageTag()    (bare Dockerfile-built, devcell-user:<stack>)

func PickSkopeoBin added in v0.7.0

func PickSkopeoBin(printOutPathsOutput string, exists func(string) bool) (string, error)

PickSkopeoBin walks the multi-line output of `nix build --print-out-paths` and returns the first store path that actually contains `bin/skopeo`.

Why this exists: skopeo (and skopeo-nix2container, which inherits skopeo's outputs) is a multi-output derivation — at least `out` and `man`, sometimes more. `--print-out-paths` prints one path per output, one per line. Naively joining the whole stdout with `/bin/skopeo` produces a path with an embedded newline; the kernel then rejects it with "fork/exec ...-man\n/nix/store/...".

The `exists` predicate is injected so this is testable without touching the real store; production callers pass a wrapper around os.Stat.

func PreflightNixBuilder added in v0.7.0

func PreflightNixBuilder(stack string) error

PreflightNixBuilder reports whether the host can usefully invoke a pure nix build (`nix build path:#packages.aarch64-linux.<...>-pure-image`).

Returns nil on hosts that can build; returns an actionable error otherwise. On a non-Linux host that lacks a Linux remote builder (or extra-platforms), the returned error explains exactly which probe path failed and how to fix it (nix-darwin linux-builder, container build, or DEVCELL_PURE_SKIP_PREFLIGHT bypass).

Callers can run this independently (before invoking BuildImagePure) to decide whether the host can usefully attempt a pure build at all.

func PreflightNixBuilderFromProbe added in v0.7.0

func PreflightNixBuilderFromProbe(probe LinuxBuilderProbe, stack string) error

PreflightNixBuilderFromProbe is the testable seam — tests inject a LinuxBuilderProbe directly so the macOS-failure branch can be exercised on any host (including the Linux CI runner).

func PullAndTagImpure added in v0.7.0

func PullAndTagImpure(ctx context.Context, stack string, verbose bool) error

PullAndTagImpure pulls the registry's impure (Dockerfile-built) image for <stack> and tags it under BOTH UserImageTag() (so an explicit --impure caller finds it) AND UserImageTagPure() (so the next default-path launch's LocalExists check is satisfied without re-pulling). Carrying the second tag is what lets the impure-pull truly serve as a fallback for the pure path on nix-less hosts.

func PullAndTagPure added in v0.7.0

func PullAndTagPure(ctx context.Context, stack string, verbose bool) error

PullAndTagPure pulls the registry's pure image for <stack> and re-tags it as UserImageTagPure() so the local-exists check finds it on the next launch.

Returns nil on success. Either step failing returns the underlying error — the launcher is expected to treat any error as "fall back to build".

func PullImage added in v0.4.0

func PullImage(ctx context.Context, tag string, verbose bool) error

PullImage attempts to pull a Docker image. Returns nil on success. When verbose is true, docker pull output is streamed to os.Stderr.

func PureBuildArgv added in v0.7.0

func PureBuildArgv(spec PureBuildSpec) []string

PureBuildArgv composes the `nix build` argv targeting the per-stack pure-image flake output. Pure function — does not invoke nix.

func RemoveOrphanedContainer

func RemoveOrphanedContainer(ctx context.Context, name string) error

RemoveOrphanedContainer removes a stopped container with the given name if it exists. Returns nil if the container doesn't exist or was successfully removed. Returns an error if the container is currently running.

func ResolveSystemPrompt added in v0.6.0

func ResolveSystemPrompt(opts ResolveOpts) (string, error)

ResolveSystemPrompt walks the seven-tier source chain in order — flags, env, TOML — returning the first match. Within a tier, setting both the file and inline form is rejected as ambiguous so the caller never has to guess which one won. Across tiers, higher silently shadows lower: the layering is the whole point of having multiple sources.

Returns ("", nil) when no source is set — callers concatenate this with ContainerContext via AssembleSystemPrompt.

Resolution order (first match wins):

  1. opts.FlagFile (--system-prompt-file)
  2. opts.FlagInline (--system-prompt)
  3. opts.EnvFile (DEVCELL_SYSTEM_PROMPT_FILE)
  4. opts.EnvInline (DEVCELL_SYSTEM_PROMPT)
  5. CellCfg.LLM.SystemPromptFile ([llm].system_prompt_file)
  6. CellCfg.LLM.SystemPrompt ([llm].system_prompt)
  7. ""

func RunDF added in v0.7.0

func RunDF(a RunDFArgs) error

RunDF orchestrates: collect → parse → format. The pure-data steps live in df.go; this function just wires them.

func RunPrune added in v0.7.0

func RunPrune(a RunPruneArgs) error

RunPrune orchestrates: build the step plan, prompt the user, execute the steps. DryRun steps are printed but not executed. IgnoreError steps don't abort the loop on failure. Returns nil if the prompt was rejected (not an error — user intent).

func StackImageTagDebian deprecated added in v0.7.0

func StackImageTagDebian(stack string) string

StackImageTagDebian is a deprecated alias for StackImageTagImpure, kept for one release while internal callers migrate. Returns the same `-impure`- suffixed tag.

Deprecated: use StackImageTagImpure.

func StackImageTagImpure added in v0.7.0

func StackImageTagImpure(stack string) string

StackImageTagImpure returns the registry tag for the multi-arch Dockerfile- built (impure) stack image. The registry uses the explicit `-impure` suffix (DIMM-213 vocabulary rename — was `-debian`) so the namespace is symmetric with the pure variant.

Used by scaffold's FROM-image fallback for the legacy Dockerfile build path.

Example: StackImageTagImpure("ultimate") → "<registry>:v<ver>-ultimate-impure"

func StackImageTagPure added in v0.7.0

func StackImageTagPure(stack string) string

StackImageTagPure returns the registry tag for a pre-built pure stack image. Example: StackImageTagPure("ultimate") → "<registry>:v<ver>-ultimate-pure"

func UpdateFlakeLock added in v0.3.0

func UpdateFlakeLock(ctx context.Context, configDir string, lockOnly bool, verbose bool, out io.Writer) error

UpdateFlakeLock runs nix flake lock (or update) inside a temp base container with configDir bind-mounted. When lockOnly is true, runs "nix flake lock" (resolves inputs, generates lock if missing, doesn't update existing pins). When lockOnly is false, runs "nix flake update" (pulls latest for all inputs).

func UserImageTag

func UserImageTag() string

UserImageTag returns the user's local image tag — the bare-name local devcell image. Unchanged across the 2026-05-15 flip: this name is the user's "current image" concept and existing local images keep working.

Default (stack-based): devcell-user:<stack> or devcell-user:<stack>-<mod1>-<mod2>-<sha8> Legacy (per_session_image=true): devcell-user:<session>

Used by:

  • `cell <agent> --impure` (PickImageTag(true) → bare local tag)
  • `cell build --impure` (docker build → tags this name)

Override with DEVCELL_USER_IMAGE env var.

func UserImageTagPure added in v0.7.0

func UserImageTagPure() string

UserImageTagPure returns the local tag for nix2container-built (pure) images — the DEFAULT path after the 2026-05-15 flip (DIMM-202). Same repo as UserImageTag with a "-pure" suffix.

Example: UserImageTag()="devcell-user:ultimate" → UserImageTagPure()="devcell-user:ultimate-pure".

func VagrantBinaryExists added in v0.5.0

func VagrantBinaryExists(ctx context.Context, vagrantDir, binary string) bool

VagrantBinaryExists checks whether a binary is reachable and executable in the VM's login shell. Used for auto-detect: if the binary is missing, the caller should provision before running.

func VagrantEnsureGUI added in v0.5.0

func VagrantEnsureGUI(ctx context.Context, vagrantDir string, dryRun bool) error

VagrantEnsureGUI starts GUI services (Xvfb, fluxbox, x11vnc, xrdp) inside the VM if they are not already running. Idempotent — pgrep guards prevent double-start. Called when the cell stack includes the desktop module and GUI is enabled.

func VagrantEnsureUp added in v0.5.0

func VagrantEnsureUp(ctx context.Context, vagrantDir, provider string, dryRun bool) error

VagrantEnsureUp brings the VM up if it is not already running. In dry-run mode prints the would-be command and returns.

func VagrantIsRunning added in v0.5.0

func VagrantIsRunning(vagrantDir string) bool

VagrantIsRunning checks whether the vagrant VM in vagrantDir is currently running. Returns false quickly when vagrantDir has no Vagrantfile (no subprocess needed). When vagrant CLI is unavailable, falls back to VagrantMachineCreated which checks whether the machine has been provisioned at least once (id file present).

func VagrantMachineCreated added in v0.5.0

func VagrantMachineCreated(vagrantDir string) bool

VagrantMachineCreated returns true if the vagrant VM in vagrantDir has been created at least once — i.e. .vagrant/machines/default/<provider>/id exists. Used as a fallback when vagrant CLI is not available in the current environment.

func VagrantMachinePort added in v0.5.0

func VagrantMachinePort(machineID, guestPort string) (string, bool)

VagrantMachinePort returns the host port mapped from guestPort for the VM identified by machineID. Uses `vagrant port <id> --machine-readable` — no file-system access needed, works regardless of where the Vagrantfile lives on disk.

func VagrantProvision added in v0.5.0

func VagrantProvision(ctx context.Context, vagrantDir string, dryRun bool) error

VagrantProvision runs `vagrant provision` to (re-)apply the nixhome flake. In dry-run mode prints the would-be command and returns.

func VagrantReadForwardedPort added in v0.5.0

func VagrantReadForwardedPort(vagrantDir, portID string) (string, bool)

VagrantReadForwardedPort reads the Vagrantfile in vagrantDir and returns the host port for the forwarded_port entry with the given id ("rdp" or "vnc"). Looks for lines of the form:

config.vm.network "forwarded_port", guest: 3389, host: 36289, id: "rdp"

func VagrantRunningCells added in v0.5.0

func VagrantRunningCells() map[string]string

VagrantRunningCells parses `vagrant global-status` and returns a map of projectBasename → machineID for all running devcell VMs. Returns an empty map (not an error) if vagrant is not installed or has no VMs.

func VagrantStatusRunning added in v0.5.0

func VagrantStatusRunning(output string) bool

VagrantStatusRunning parses `vagrant status --machine-readable` output and returns true if the machine state is "running" (libvirt/virtualbox) or "started" (UTM).

Machine-readable format: timestamp,target,type,data (CSV, 4 fields) We look for a record where type=="state" and data is "running" or "started".

func VagrantUploadNixhome added in v0.5.0

func VagrantUploadNixhome(ctx context.Context, vagrantDir, nixhomePath string, dryRun bool) error

VagrantUploadNixhome uploads a local nixhome directory into the VM at ~/nixhome using `vagrant upload <src> nixhome`. No-op when nixhomePath is empty. The provisioner checks $HOME/nixhome first (set by this upload), then falls back to GitHub.

Types

type AcquireDeps added in v0.7.0

type AcquireDeps struct {
	Inputs      LaunchInputs
	PullPure    func(context.Context) error
	PullImpure  func(context.Context) error
	BuildPure   func(context.Context) error
	BuildImpure func(context.Context) error
}

AcquireDeps bundles the side-effectful work AcquireImage needs to satisfy each effective LaunchAction in the sequence returned by DecideLaunchActionsPure. UseLocal and DryRun are no-ops at this layer and are handled internally — callers only supply the four closures that may fail.

type BuildDebugInfo added in v0.7.0

type BuildDebugInfo struct {
	Tag        string
	ID         string
	Created    string
	SizeBytes  int64
	LayerCount int
}

BuildDebugInfo bundles the post-build inspect data shown by `cell build --debug`.

func InspectImageDebug added in v0.7.0

func InspectImageDebug(ctx context.Context, tag string) (BuildDebugInfo, error)

InspectImageDebug calls `docker image inspect <tag> --format '{{json .}}'` once and extracts the fields shown in the --debug summary.

type DFCache added in v0.7.0

type DFCache struct {
	ID           string
	Description  string
	SizeBytes    int64
	InUse        bool
	Shared       bool
	UsageCount   int
	CreatedSince string
}

type DFCollector added in v0.7.0

type DFCollector interface {
	CollectSystemDF(ctx context.Context) ([]byte, error)
}

DFCollector is the test seam for `cell build df` — production wires ExecCollector, which shells to `docker system df -v --format json`. Tests inject a fake that returns a canned JSON fixture.

type DFContainer added in v0.7.0

type DFContainer struct {
	ID        string
	Names     string
	ImageRef  string // "sha256:..." — cross-ref to DFImage.ID
	State     string
	SizeBytes int64 // writable layer size
}

type DFImage added in v0.7.0

type DFImage struct {
	ID           string // full "sha256:..." form, as docker returns it
	Repository   string
	Tag          string
	Containers   int   // pin count (0 = orphan, candidate for `docker image rm`)
	SizeBytes    int64 // total size
	UniqueBytes  int64 // bytes that would actually free on removal
	SharedBytes  int64
	CreatedSince string
}

type DFOpts added in v0.7.0

type DFOpts struct {
	TopN  int         // 0 = all rows
	Kinds []EntryKind // empty = all kinds
	JSON  bool        // true → FormatJSON, false → FormatTable
}

DFOpts controls a single `cell build df` invocation.

type DFSnapshot added in v0.7.0

type DFSnapshot struct {
	Images     []DFImage
	Containers []DFContainer
	Volumes    []DFVolume
	BuildCache []DFCache
}

DFSnapshot is the typed shape of `docker system df -v --format json`. All numeric fields arrive from docker as strings ("29.8GB", "3", "N/A") and are parsed once via parseSize / strconv.Atoi at unmarshal time.

func ParseSystemDF added in v0.7.0

func ParseSystemDF(raw []byte) (DFSnapshot, error)

ParseSystemDF unmarshals the JSON blob produced by `docker system df -v --format json` into typed structs with parsed sizes.

type DFVolume added in v0.7.0

type DFVolume struct {
	Name      string
	Links     int   // pin count
	SizeBytes int64 // -1 when docker reports "N/A"
}

type EntryKind added in v0.7.0

type EntryKind string

EntryKind tags a RankedEntry's origin so the formatter can render the right reclaim hint and column heading.

const (
	EntryKindImage     EntryKind = "image"
	EntryKindContainer EntryKind = "container"
	EntryKindVolume    EntryKind = "volume"
	EntryKindCache     EntryKind = "cache"
)

type EphemeralRegistry added in v0.7.0

type EphemeralRegistry struct {
	Port int
	// contains filtered or unexported fields
}

EphemeralRegistry is a transient OCI registry backed by a filesystem blob store. It enables layer-level dedup when loading nix2container images into the Docker daemon: unchanged layers are served from disk cache instead of being re-copied every build.

func (*EphemeralRegistry) Addr added in v0.7.0

func (r *EphemeralRegistry) Addr() string

Addr returns the host:port string for use in docker:// and skopeo references.

func (*EphemeralRegistry) Start added in v0.7.0

func (r *EphemeralRegistry) Start(cacheDir string) error

Start launches the registry on a random localhost port. cacheDir is created if it doesn't exist and used to persist blobs across invocations.

func (*EphemeralRegistry) Stop added in v0.7.0

func (r *EphemeralRegistry) Stop() error

Stop gracefully shuts down the registry with a 5s timeout.

type ExecCollector added in v0.7.0

type ExecCollector struct{}

ExecCollector is the real collector. It runs docker once and returns the raw JSON for ParseSystemDF to handle.

func (ExecCollector) CollectSystemDF added in v0.7.0

func (ExecCollector) CollectSystemDF(ctx context.Context) ([]byte, error)

type FS

type FS interface {
	Stat(path string) error
}

FS abstracts filesystem stat for testability.

var OsFS FS = FSFunc(func(path string) error {
	_, err := os.Stat(path)
	return err
})

OsFS is the real filesystem implementation.

type FSFunc

type FSFunc func(string) error

FSFunc is a function that implements FS.

func (FSFunc) Stat

func (f FSFunc) Stat(path string) error

type FormatOpts added in v0.7.0

type FormatOpts struct {
	TopN  int         // 0 means "all"
	Kinds []EntryKind // empty means all kinds
}

FormatOpts controls FormatTable / FormatJSON output.

type ImageMetadata added in v0.4.0

type ImageMetadata struct {
	BaseImage string   `json:"base_image"`
	Stack     string   `json:"stack"`
	Modules   []string `json:"modules"`
	GitCommit string   `json:"git_commit"`
	BuildDate string   `json:"build_date"`
	Packages  int      `json:"packages"`
}

ImageMetadata holds structured build metadata from /etc/devcell/metadata.json.

func ImageMetadataFromContainer added in v0.4.0

func ImageMetadataFromContainer(ctx context.Context) ImageMetadata

ImageMetadataFromContainer reads build metadata for the current launch's image.

Source-of-truth flip (2026-05-16): the date/rev now come from the OCI image manifest (labels + Created field) rather than /etc/devcell/metadata.json inside the image. Why: when metadata.json carried a real per-build timestamp, every `cell build` invocation perturbed homeRoot's tar hash and forced skopeo to re-push the ~3.9GB customization layer even when no source had changed. Pinning metadata.json to static placeholders eliminates that; the date moves to OCI manifest labels (which only affect the tiny manifest blob, not layer content).

Reads:

  • .Created — OCI manifest creation timestamp (= our buildDate)
  • .Config.Labels — devcell.stack, org.opencontainers.image.revision, etc.
  • .Config.Env — DEVCELL_PROFILE for stack fallback

Falls back to a runtime `cat /etc/devcell/metadata.json` for older images that predate the manifest-based stamping.

func ImageMetadataFromInspectExport added in v0.7.0

func ImageMetadataFromInspectExport(created string, labels map[string]string, env []string) ImageMetadata

ImageMetadataFromInspectExport is the test seam for the pure helper — exported so package_test.go can drive it without docker.

func ParseImageMetadata added in v0.4.0

func ParseImageMetadata(data []byte) ImageMetadata

ParseImageMetadata parses JSON into ImageMetadata. Returns zero value on error.

type LaunchAction added in v0.7.0

type LaunchAction int

LaunchAction is one image-acquisition step the launcher may run before exec'ing the container. DecideLaunchActionsPure orders them into a fallback sequence; AcquireImage walks that sequence at runtime.

const (
	// ActionUseLocal: image is present locally; exec immediately.
	ActionUseLocal LaunchAction = iota

	// ActionDryRun: no image work; render argv only.
	ActionDryRun

	// ActionPullPure: pull the pure (nix2container) registry tag for the
	// active stack and retag locally as UserImageTagPure().
	ActionPullPure

	// ActionPullImpure: pull the impure (Dockerfile-built) registry tag and
	// retag locally so the next LocalExists check finds it. Used as the
	// second fallback when ActionPullPure fails — the impure tag carries
	// the same effective contents but is reachable by hosts without nix.
	ActionPullImpure

	// ActionBuildPure: build via nix2container against the resolved nixhome
	// flake. Requires a usable host nix (and on macOS, a Linux remote
	// builder — see PreflightNixBuilder).
	ActionBuildPure

	// ActionBuildImpure: build via `docker build` against the scaffolded
	// Dockerfile. Nix runs inside the build, so the host does not need a
	// nix binary. Final fallback for nix-less hosts.
	ActionBuildImpure
)

func DecideLaunchActionsPure added in v0.7.0

func DecideLaunchActionsPure(in LaunchInputs) []LaunchAction

DecideLaunchActionsPure returns the ordered fallback sequence AcquireImage should walk. The first action that succeeds wins; on the last action's failure AcquireImage surfaces a chain error.

Sequences:

LocalExists           → [UseLocal]
DryRun                → [DryRun]
ExplicitBuild+HasNix  → [BuildPure]
ExplicitBuild+no nix  → [BuildImpure]
cold start + HasNix   → [PullPure, PullImpure, BuildPure]
cold start + no nix   → [PullPure, PullImpure, BuildImpure]

type LaunchInputs added in v0.7.0

type LaunchInputs struct {
	// DryRun is true when --dry-run is set. Highest precedence.
	DryRun bool

	// ExplicitBuild is true when --build is set. Forces a rebuild regardless
	// of local image presence.
	ExplicitBuild bool

	// LocalExists is true when the locally-tagged pure image
	// (UserImageTagPure()) is present in the Docker daemon.
	LocalExists bool

	// HasNix is true when the host can usefully run a pure nix build —
	// nix is on PATH AND (on macOS) a Linux remote builder is configured.
	// When false the decision skips ActionBuildPure and prefers
	// ActionBuildImpure so docker can complete the build instead.
	HasNix bool
}

LaunchInputs are the inputs to DecideLaunchActionsPure.

type LayerCounter added in v0.7.0

type LayerCounter struct {
	// contains filtered or unexported fields
}

LayerCounter wraps an io.Writer, mirrors all bytes through unchanged, and tallies skopeo's "Copying blob ... done" / "Copying blob ... skipped" lines. Use it during the registry-push step of BuildImagePure to surface cache hit-rate to `cell build --debug`.

Lines are buffered until '\n' so io.Copy chunking can't split a match. "Copying config" lines are intentionally ignored — that's the image config descriptor, not a layer.

BuildImagePure invokes skopeo twice (nix→registry, registry→daemon), so the same blob ID appears in both passes. We dedupe by ID and only count each blob's FIRST classification — that line reflects the build-side cache state (was this layer in the local nix store / registry cache?), which is what the user means by "cached vs new".

func NewLayerCounter added in v0.7.0

func NewLayerCounter(w io.Writer) *LayerCounter

func (*LayerCounter) Stats added in v0.7.0

func (lc *LayerCounter) Stats() LayerStats

func (*LayerCounter) Write added in v0.7.0

func (lc *LayerCounter) Write(p []byte) (int, error)

type LayerStats added in v0.7.0

type LayerStats struct {
	New    int
	Cached int
}

LayerStats counts skopeo's per-blob outcomes from a single image copy. "New" = blobs transferred to the destination; "Cached" = blobs the destination already had (cache hit / dedup). Sum should equal the image's layer count for a clean push (the config blob is filtered out).

type LinuxBuilderProbe added in v0.7.0

type LinuxBuilderProbe struct {
	OK             bool   // can we build aarch64-linux / x86_64-linux?
	Source         string // "env", "linux-host", "nix-config-show", "nix.conf", "machines-file", "none"
	ConfigCmd      string // the nix command we ran (or "" if we didn't)
	ConfigErr      string // error from running nix config show (if any)
	BuildersLine   string // exact `builders = ...` line nix reported
	ExtraPlatforms string // exact `extra-platforms = ...` line nix reported
	NixConfPath    string // path to nix.conf if read as fallback
	MachinesFile   string // path to /etc/nix/machines if BuildersLine references it
	MachinesLines  string // first ~3 non-comment lines from machines file
}

LinuxBuilderProbe captures the result of inspecting the local nix config for a Linux remote builder. Populated by CheckNixLinuxBuilder so the caller can surface the diagnostic context in user-facing errors.

func CheckNixLinuxBuilder added in v0.7.0

func CheckNixLinuxBuilder() LinuxBuilderProbe

CheckNixLinuxBuilder probes nix to determine whether aarch64-linux is buildable and returns full diagnostic info for the error message.

type PruneOpts added in v0.7.0

type PruneOpts struct {
	// GOOS is the target operating system: "darwin" or "linux".
	// Tests pass this explicitly; the runtime call site passes runtime.GOOS.
	GOOS string

	// Force enables nuclear cleanup mode (docker desktop wipe / qcow nuke /
	// aggressive nix GC). Default mode is the standard prune sequence.
	Force bool

	// Pure selects the nix path instead of the docker path.
	Pure bool

	// Rootless indicates a rootless Docker daemon on Linux. Only relevant
	// when GOOS == "linux" && !Pure && Force. The runtime call site
	// auto-detects this via `docker info`.
	Rootless bool

	// NixOS indicates /etc/NIXOS is present. Only relevant when
	// GOOS == "linux" && Pure && Force; triggers system-profile cleanup.
	NixOS bool

	// HomeDir is the resolved user home (e.g. /home/dmitry). Used to
	// compose absolute paths in macOS Docker Desktop wipe and the
	// `~/.cache/nix` wipe on Linux. Tests pass a fixed value.
	HomeDir string

	// LinuxBuilderHost is the SSH target for the macOS linux-builder VM.
	// Default: "builder@linux-builder".
	LinuxBuilderHost string
}

PruneOpts describes a `cell build prune` invocation.

type PruneStep added in v0.7.0

type PruneStep struct {
	// Argv is the command and arguments. For shell-substituted commands
	// like `docker rm -f $(docker ps -aq)`, this is ["sh", "-c", "<script>"].
	Argv []string

	// IgnoreError lets the step fail without aborting the plan
	// (e.g. `docker rm` with no containers exits non-zero).
	IgnoreError bool

	// DryRun, when true, signals the runtime to print the argv as a
	// `# (dry-run)` comment instead of executing. Used for the macOS
	// linux-builder qcow nuke until paths are verified on a live setup.
	DryRun bool
}

PruneStep is one command in a prune plan.

func BuildDockerPruneSteps added in v0.7.0

func BuildDockerPruneSteps(opts PruneOpts) []PruneStep

BuildDockerPruneSteps composes the docker prune plan. Default mode is the cleandocker sequence (identical on Darwin and Linux). Force mode branches by OS.

func BuildNixPruneSteps added in v0.7.0

func BuildNixPruneSteps(opts PruneOpts) []PruneStep

BuildNixPruneSteps composes the nix prune plan. Default mode runs `nix-collect-garbage -d` + `nix-store --optimise` (via ssh on macOS, locally on Linux native). Force mode branches by OS (Darwin: dry-run qcow nuke plan; Linux: aggressive local GC).

type PureBuildSpec added in v0.7.0

type PureBuildSpec struct {
	// FlakeRef is the full flake reference to build against (e.g.
	// "path:/abs/nixhome" or "github:DimmKirr/devcell/main?dir=nixhome").
	// When set, takes precedence over NixhomePath — the per-stack output
	// suffix is appended directly. This is the seam that lets the pure
	// path fall back to a remote flake when no local nixhome exists,
	// mirroring the docker path's flake input fallback (scaffold.go:130-140).
	FlakeRef string
	// NixhomePath is a local directory containing the nixhome flake.
	// Used only when FlakeRef is empty (legacy/back-compat callers).
	NixhomePath string
	// StackName is the devcell stack to build (e.g. "base", "python", "ultimate").
	StackName string
	// Arch is the nix system identifier (e.g. "aarch64-linux", "x86_64-linux").
	// If empty, detected from runtime.GOARCH.
	Arch string
	// OutLink is the symlink target for the build output. Optional; defaults
	// to a path under the nixhome dir.
	OutLink string
	// Verbose enables -L (print build logs) and -v on the nix invocation,
	// so users running `cell <agent> --pure --debug` see real progress
	// instead of just a spinner.
	Verbose bool
}

PureBuildSpec describes a pure-image build invocation.

type PureNixhomeInputs added in v0.7.0

type PureNixhomeInputs struct {
	// TomlNixhome is the resolved [cell].nixhome value (project TOML →
	// global TOML → DEVCELL_NIXHOME_PATH env, merged by cfg.LoadFromOS).
	// Empty if unset.
	TomlNixhome string

	// BaseDir is the project working directory — used to check for a local
	// "<BaseDir>/nixhome" as the second-level fallback.
	BaseDir string

	// Version is the cell binary version (version.Version). Used to pin the
	// remote github ref. "v0.0.0" and "" are coerced to DefaultNixhomeGitRef
	// (dev builds).
	Version string

	// StatFunc lets tests inject a synthetic filesystem. When nil, real
	// os.Stat is used.
	StatFunc func(path string) error
}

PureNixhomeInputs is the input to ResolvePureNixhomeRef. Mirrors the data flow already present for the docker path (scaffold.go:130-140), but emits a flake reference instead of editing a generated flake.nix.

type PureNixhomeRef added in v0.7.0

type PureNixhomeRef struct {
	// FlakeRef is the value to pass through to PureBuildSpec.FlakeRef.
	// Format: "path:<abs>" for local sources, "github:..." for remote.
	FlakeRef string

	// LocalPath is the absolute on-disk path when FlakeRef is "path:" form;
	// empty when remote. Caller uses this to decide whether to sync into
	// BuildDir (only local sources need staging).
	LocalPath string

	// Remote is true when FlakeRef is a network URL (github:, git+https:, …).
	// Convenience flag — equivalent to LocalPath == "".
	Remote bool
}

PureNixhomeRef is the resolved flake reference plus metadata the caller needs to decide whether to SyncNixhome into BuildDir.

func ResolvePureNixhomeRef added in v0.7.0

func ResolvePureNixhomeRef(inputs PureNixhomeInputs) PureNixhomeRef

ResolvePureNixhomeRef applies the docker path's nixhome resolution chain to produce a flake reference for the pure build.

Precedence:

  1. inputs.TomlNixhome (explicit user setting via .devcell.toml / env)
  2. inputs.BaseDir + "/nixhome" on disk
  3. github:DimmKirr/devcell/<Version>?dir=nixhome (Version coerced to DefaultNixhomeGitRef when empty or "v0.0.0")

Pure function — fs lookups go through inputs.StatFunc so tests don't depend on real disk state.

type RankedEntry added in v0.7.0

type RankedEntry struct {
	Kind         EntryKind
	ID           string
	Label        string // e.g. "devcell-user:ultimate-pure" or "nix profile install (truncated)"
	SizeBytes    int64
	ReclaimBytes int64
	PinCount     int // >0 means "in use" — formatter hides reclaim hint
}

RankedEntry is the unified row type used by ranking + formatting. ReclaimBytes is the sort key — for images that's UniqueBytes; for everything else it's the full SizeBytes.

func RankEntries added in v0.7.0

func RankEntries(snap DFSnapshot, topN int) []RankedEntry

RankEntries flattens the snapshot into a single list sorted by ReclaimBytes desc. topN <= 0 returns all entries.

type ResolveOpts added in v0.6.0

type ResolveOpts struct {
	// FlagFile / FlagInline are the --system-prompt-file / --system-prompt
	// CLI flags. Currently exposed only on `cell serve`.
	FlagFile, FlagInline string
	// EnvFile / EnvInline are the DEVCELL_SYSTEM_PROMPT_FILE /
	// DEVCELL_SYSTEM_PROMPT env vars. Read by every surface.
	EnvFile, EnvInline string
	// CellCfg supplies [llm].system_prompt and [llm].system_prompt_file
	// from the merged devcell.toml.
	CellCfg cfg.CellConfig
	// CfgBaseDir is the project base dir, used to resolve a relative
	// `[llm].system_prompt_file` path. Empty disables relative resolution
	// (absolute paths still work).
	CfgBaseDir string
}

ResolveOpts bundles every input source the system-prompt resolver looks at. Surfaces wire only the inputs they have — `cell claude` leaves the flag fields empty; `cell serve` populates everything.

type RunDFArgs added in v0.7.0

type RunDFArgs struct {
	Ctx       context.Context
	Collector DFCollector
	Opts      DFOpts
	Out       io.Writer
}

RunDFArgs bundles inputs to RunDF — keeps the call site readable (matches the pattern of RunPruneArgs).

type RunPruneArgs added in v0.7.0

type RunPruneArgs struct {
	Opts    PruneOpts
	Exec    func(step PruneStep) error
	Out     io.Writer
	In      io.Reader
	SkipYes bool // --yes / -y
	IsTTY   bool // term.IsTerminal(int(os.Stdin.Fd()))
}

RunPruneArgs bundles inputs to RunPrune. Keeps the call site readable when callers wire in real stdin/stdout/exec.

type RunSpec

type RunSpec struct {
	Config       config.Config
	CellCfg      cfg.CellConfig
	Binary       string
	DefaultFlags []string
	UserArgs     []string
	Debug        bool                // pass DEVCELL_DEBUG=true into the container
	NixDaemon    bool                // pass DEVCELL_NIX_DAEMON=true into the container
	Image        string              // image ID or tag to run; defaults to UserImageTag
	ExtraEnv     map[string]string   // additional env vars injected by the command handler
	InheritEnv   []string            // env var names to inherit from host (passed as -e KEY with no value)
	Getenv       func(string) string // env lookup; defaults to os.Getenv when nil
}

RunSpec holds everything needed to build the docker run argv.

type VagrantSpec added in v0.5.0

type VagrantSpec struct {
	Config       config.Config
	CellCfg      cfg.CellConfig
	Binary       string   // agent binary to run inside the VM (e.g. "claude")
	DefaultFlags []string // flags always passed to the binary
	UserArgs     []string // additional args from the user
	VagrantDir   string   // directory containing the Vagrantfile
	Provider     string   // vagrant provider ("utm" or "libvirt")
	EnvVars      []string // KEY=VALUE pairs to set inside the VM via `env`
	ProjectDir   string   // host project directory — basename is used as workdir in VM
}

VagrantSpec holds everything needed to build a vagrant ssh argv.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL