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
- Variables
- func AcquireImage(ctx context.Context, d AcquireDeps) error
- func AssembleOverlayPrompt(c config.Config, cellCfg cfg.CellConfig, opts ResolveOpts) (string, error)
- func BaseImageTag() string
- func BuildArgv(spec RunSpec, fs FS, lookPath func(string) (string, error)) []string
- func BuildCleanupPrompt(closures []LiveClosure) string
- func BuildCleanupScript(closures []LiveClosure) string
- func BuildExecArgv(spec ExecSpec) []string
- func BuildImage(ctx context.Context, configDir string, noCache bool, verbose bool, ...) error
- func BuildImagePure(ctx context.Context, spec PureBuildSpec, tag string, verbose bool, ...) error
- func BuildLabel(prefix, stack string, explicit bool) string
- func BuildPrunePrompt(opts PruneOpts) string
- func BuildVagrantSSHArgv(spec VagrantSpec) []string
- func ChangedBuildFiles(configDir string) ([]string, bool)
- func ClosureAliveArgv(volume, image string) []string
- func ClosureDeadWarning(alive bool) (warning string, dead bool)
- func CollectRunningContainers(ctx context.Context) ([]string, error)
- func CollectVolumeMounts(ctx context.Context) map[string][]string
- func ConfirmDestructive(out io.Writer, in io.Reader, skipYes bool, isTTY bool, warning string) bool
- func ConfirmProceed(out io.Writer, in io.Reader, isTTY bool, warning string) bool
- func ConsumeBootEvents(events <-chan BootEvent)
- func ContainerContext(c config.Config, cellCfg cfg.CellConfig) string
- func ContainerRunning(ctx context.Context, name string) bool
- func DebugArgv(argv []string) string
- func DetectArch() string
- func DiffBuildFile(configDir, name string) string
- func DiscoverStacks(ctx context.Context, configDir string, out io.Writer) ([]string, error)
- func DockerBindProbeArgv(image, volume, source, marker string) []string
- func DockerDaemonReachable(ctx context.Context) error
- func DockerHostPath(p string) string
- func DockerPlatform(arch string) string
- func DockerResolveContainerLink(ctx context.Context, container, link string) (string, error)
- func DockerRunningDevcellContainers(ctx context.Context) ([]string, error)
- func DockerVolumeDebug(ctx context.Context, volume string) string
- func DockerfileChanged(configDir string) bool
- func EnsureNetwork(ctx context.Context) error
- func FlakeNixImage() string
- func FormatImageVersionUserExport(m ImageMetadata) string
- func FormatJSON(snap DFSnapshot, opts FormatOpts, w io.Writer) error
- func FormatMCPLoadedBanner(nixMCPs, userMCPs []string) string
- func FormatNixStoreSection(r NixStoreReport, w io.Writer)
- func FormatTable(snap DFSnapshot, opts FormatOpts, w io.Writer) error
- func FormatTableWithVM(snap DFSnapshot, opts FormatOpts, vm VMDiskInfo, containers []string, ...) error
- func HumanBytes(n int64) string
- func ImageExists(ctx context.Context, tag string) bool
- func ImageExistsForPlatform(ctx context.Context, tag, platform string) bool
- func ImageVersions(ctx context.Context) (base, user string)
- func LocalImageID(ctx context.Context) (string, error)
- func LocalImageIDFor(ctx context.Context, tag string) (string, error)
- func LocalImageSize(ctx context.Context, tag string) int64
- func LookPathNix() (string, error)
- func NixDFReportArgv(volume string) []string
- func NixHealthProbeArgv(volume string) []string
- func NixSystemFor(goos, goarch string) string
- func ParseClosureAliveResult(stdout string, err error) (resolvedPath string, alive bool)
- func ParseSentinelName(filename string) (component, state string, ok bool)
- func ParseSize(s string) (int64, error)
- func ParseVagrantGlobalStatus(output string) map[string]string
- func ParseVagrantPortOutput(output, guestPort string) (string, bool)
- func PickImageTag(impure bool) string
- func PickImageTagThin() string
- func PickSkopeoBin(printOutPathsOutput string, exists func(string) bool) (string, error)
- func PreflightNixBuilder(stack string) error
- func PreflightNixBuilderFromProbe(probe LinuxBuilderProbe, stack string) error
- func PreflightPlatformCheck(ctx context.Context, nixhomeFlakeRef, targetSystem string) error
- func PreflightPlatformCheckWithLookPath(ctx context.Context, nixhomeFlakeRef, targetSystem string, ...) error
- func PrepareWireguard(cellHome string, cellCfg cfg.CellConfig) error
- func ProbeDockerBind(ctx context.Context, image, volume, source, marker string) (string, error)
- func ProjectNixpkgsRev(baseDir string) (string, error)
- func PullAndTagImpure(ctx context.Context, stack string, verbose bool) error
- func PullAndTagPure(ctx context.Context, stack string, verbose bool) error
- func PullImage(ctx context.Context, tag string, verbose bool) error
- func PullImageForPlatform(ctx context.Context, tag, platform string, verbose bool) error
- func PureBuildArgv(spec PureBuildSpec) []string
- func RemoveOrphanedContainer(ctx context.Context, name string) error
- func RenderModulesOverlay(modules []string) string
- func ResolveAppendPrompt(opts ResolveOpts) (string, error)
- func ResolveBuildTag(custom, derived string) string
- func ResolveNixhomeRef(ver string) string
- func ResolveSystemPrompt(opts ResolveOpts) (string, error)
- func RunCleanup(a RunCleanupArgs) error
- func RunDF(a RunDFArgs) error
- func RunPrune(a RunPruneArgs) error
- func StackImageTagImpure(stack string) string
- func StackImageTagPure(stack string) string
- func StaleCellWarning(projectRev string, h NixStoreHealth) (string, bool)
- func StampRootsScript(closures []LiveClosure) string
- func ThinBuildArgv(...) []string
- func ThinBuildArgvFull(...) []string
- func ThinStoreVolume() string
- func TranslateError(err error) string
- func UpdateFlakeLock(ctx context.Context, configDir string, lockOnly bool, verbose bool, ...) error
- func UpstreamFlakeRef(ref string) string
- func UpstreamFlakeRefNoVersion() string
- func UserImageTag() string
- func UserImageTagPure() string
- func UserImageTagThin() string
- func VagrantBinaryExists(ctx context.Context, vagrantDir, binary string) bool
- func VagrantEnsureGUI(ctx context.Context, vagrantDir string, dryRun bool) error
- func VagrantEnsureUp(ctx context.Context, vagrantDir, provider string, dryRun bool) error
- func VagrantIsRunning(vagrantDir string) bool
- func VagrantMachineCreated(vagrantDir string) bool
- func VagrantMachinePort(machineID, guestPort string) (string, bool)
- func VagrantProvision(ctx context.Context, vagrantDir string, dryRun bool) error
- func VagrantReadForwardedPort(vagrantDir, portID string) (string, bool)
- func VagrantRunningCells() map[string]string
- func VagrantStatusRunning(output string) bool
- func VagrantUploadNixhome(ctx context.Context, vagrantDir, nixhomePath string, dryRun bool) error
- func VolumeContains(ctx context.Context, volumeName, sentinelPath string) bool
- func VolumeContainsArgv(volumeName, sentinelPath string) []string
- func VolumeExists(ctx context.Context, name string) bool
- func VolumeHydrated(volumeName, sentinelPath string, exists func(string) bool, ...) bool
- func WarnThickDeprecation()
- func WriteBasePrompt(c config.Config, opts ResolveOpts) (string, error)
- func WriteModulesOverlay(modules []string, outPath string) error
- func WriteOverlayPrompt(c config.Config, cellCfg cfg.CellConfig, opts ResolveOpts) (string, error)
- func WritePromptFile(c config.Config, name, content string) (string, error)
- func WriteThinBuildContext(w io.Writer, buildDir string) error
- type AcquireDeps
- type BootDirWatcher
- type BootEvent
- type BuildDebugInfo
- type BuildLimits
- type Catalog
- type DFCache
- type DFCollector
- type DFContainer
- type DFImage
- type DFOpts
- type DFSnapshot
- type DFVolume
- type DockerCapacity
- type DockerDebugInfo
- type EntryKind
- type EphemeralRegistry
- type ExecCollector
- type ExecSpec
- type FS
- type FSFunc
- type FormatOpts
- type ImageMetadata
- type LaunchAction
- type LaunchInputs
- type LayerCounter
- type LayerStats
- type LinuxBuilderProbe
- type LiveClosure
- type ModuleMeta
- type NixRootEntry
- type NixRootMeta
- type NixStoreHealth
- type NixStoreReport
- type PruneOpts
- type PruneStep
- type PureBuildSpec
- type PureNixhomeInputs
- type PureNixhomeRef
- type RankedEntry
- type ResolveOpts
- type RunCleanupArgs
- type RunDFArgs
- type RunPruneArgs
- type RunSpec
- type VMDiskInfo
- type VagrantSpec
Constants ¶
const ( ContainerProfileLink = "/opt/devcell/.local/state/nix/profiles/profile" ContainerGenerationLink = "/opt/devcell/.local/state/nix/profiles/home-manager" )
Symlinks inside a running container that resolve to its closure's volume-resident store paths. Resolved with `docker exec <c> readlink -f`.
const ( UpstreamOwner = "devcell-sh" UpstreamRepo = "community-home" // UpstreamSubdir is empty since the nixhome moved to its own repo // (community-home); the flake now lives at the repo root. UpstreamSubdir = "" )
Canonical upstream nixhome source — single source of truth across the CLI. Previously these constants were re-encoded in 4 separate fmt.Sprintf calls (pure_nixhome_resolver, cmd/modules, scaffold templates). Centralised here so a fork/rename is a one-line change.
const BasePromptFile = "system-prompt.md"
BasePromptFile is the generated file carrying the base prompt — the one that REPLACES Claude Code's built-in prompt.
const ( // DefaultBuildCPUs is "0" — no CFS quota by default. The memory ceiling // plus the max-jobs derived from it are the OOM guard; a CPU quota only // starves per-job cores (nproc ignores it anyway) and slows the build. DefaultBuildCPUs = "0" )
Resource ceiling for the thin-build container (CELL-359).
The builder used to run uncapped, so a `nix build` spike was arbitrated by the VM-wide OOM killer only after the whole VM was already starved. Capping trades that for a deterministic cgroup kill confined to the builder: sibling cells survive and the failure names its own cause.
These are ceilings, NOT reservations, and they are only emitted when they actually constrain the daemon — see clampBuildLimits. A ceiling at or above what the daemon has protects nothing, and dockerd rejects a --cpus larger than its CPU count outright ("range of CPUs is from 0.01 to 2.00", exit 125), which is fatal on a stock 2-CPU Colima VM.
Two properties make a ceiling safe to emit, and the first cut of CELL-359 shipped with neither:
nix's own concurrency must be derived from the ceiling. `--cpus` is a CFS bandwidth quota, not a core count: nproc inside a `--cpus 4` container still reports every host CPU, so `max-jobs = auto` kept scheduling one job per host CPU. Eight concurrent derivations under an 8 GiB ceiling is ~1 GiB each, and `npm ci` over a large dependency tree needs several — the cgroup OOM killer took it out mid-install ("Killed", exit 137).
The ceiling cannot lean on swap as a cushion. Leaving --memory-swap unset gives Docker's 2x default, but Lima and Docker Desktop VMs both run swapless in practice (SwapTotal: 0), so the ceiling is hard at --memory and the concurrency derived from it is the only thing keeping the build inside it.
const DefaultNixhomeGitRef = "main"
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). Points at community-home's default branch.
const (
// DefaultRegistry is the fallback registry prefix for devcell images.
DefaultRegistry = "ghcr.io/devcell-sh/devcell"
)
const DefaultThinStoreVolume = "devcell-nix-store"
DefaultThinStoreVolume is the named Docker volume that holds the thin-mode /nix store across builds and cell runs. Single shared volume by default so stack rebuilds reuse the existing nix store.
const NixCoreImage = "nixos/nix:2.34.7"
NixCoreImage is the default nix base image for thin builds. Prefer cfg.DefaultNixImage for the canonical constant; this alias exists for call sites inside runner that don't take a config parameter.
const NixDFReportScript = NixHealthProbeScript + "\n" + nixRootsListScript
NixDFReportScript is the health probe plus the root/meta listing — still purely read-only.
const NixGCRootReportScript = `` /* 1045-byte string literal not displayed */
NixGCRootReportScript prints the current GC root state: how many roots, how many unique hashes, and details from each -meta file. Used by the prune preflight to give drift visibility before destructive operations. Exported for test assertions.
const NixHealthProbeScript = `` /* 1582-byte string literal not displayed */
NixHealthProbeScript emits one machine-readable line:
total=N stale=N hashes=N generations=N orphaned=N
total/stale: devcell root symlinks and how many dangle. hashes: distinct profile hashes (>1 means config/lock drift). generations/orphaned: per-user profile generations and how many no devcell root protects (reclaimable by `cell build prune --pure`).
const OverlayPromptFile = "additional-systemprompt.md"
OverlayPromptFile is the generated file carrying container context plus the resolved operator prompt — everything that layers *on top of* whichever base prompt is in effect.
const ProfilePath = "/opt/devcell/.local/state/nix/profiles/profile"
ProfilePath is the canonical location of the home-manager profile baked into every thin image by thin_build.go.
const SafeNixGCScript = `` /* 1451-byte string literal not displayed */
SafeNixGCScript is the shell script that performs project-aware nix garbage collection. It removes only orphaned profile generations that aren't protected by project-scoped GC roots, then runs nix-store --gc.
CELL-334: also reads *-meta files to identify and clean up stale roots (roots whose metadata exists but whose profile target is dangling). Reports stale root count for drift visibility.
Exported for test assertions.
const ThinEntrypointSentinel = "/nix/var/nix/profiles/devcell-tools/bin/tini"
ThinEntrypointSentinel is the path the thin image's ENTRYPOINT references (built by thin_build.go:259). Used as the hydration probe in cmd/root.go's auto-build gate (CELL-38).
Variables ¶
var ErrDockerDaemonUnreachable = errors.New("Docker is not running. Please start Docker and retry")
ErrDockerDaemonUnreachable is returned by DockerDaemonReachable when the docker daemon doesn't respond. Stable so callers can match on it.
var Modules []string
Modules is the list of extra nix modules composed on top of the stack. Set from CellConfig at startup.
var PerCellImage bool
PerCellImage tags user images per cell instead of per stack. Set from CellConfig at startup; defaults to false (stack-based).
var PollInterval = 25 * time.Millisecond
PollInterval is how often the watcher re-reads the boot dir looking for new sentinel files. 25ms keeps timing resolution fine enough that fragments writing their two sentinels (`X.starting` + `X.ready`) within a single fragment body usually land in separate poll cycles — visible to the user as distinct elapsed-time deltas. Cost: ~40 getdents/sec during the ~30-second boot window. Negligible.
History: started at 100ms, but 8+ rows ended up sharing identical cumulative-time stamps because a single poll cycle picked them all up. Dropping to 25ms gave each fragment its own visible time bucket. Var rather than const so tests can dial it down further for speed.
var Registry = DefaultRegistry
Registry is the active container registry. Set via cfg.ResolvedRegistry() at startup; defaults to DefaultRegistry.
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 AssembleOverlayPrompt ¶ added in v0.9.0
func AssembleOverlayPrompt(c config.Config, cellCfg cfg.CellConfig, opts ResolveOpts) (string, error)
AssembleOverlayPrompt builds the overlay: the auto-generated container context followed by the resolved append prompt. This is what reaches claude via --append-system-prompt-file.
Container context lives on the overlay rather than the base deliberately — it is regenerated per run from live container facts, so a user-supplied base prompt must not be able to displace it.
When the append layer resolves empty, the container context alone is returned; it is never empty, so the overlay file is always written.
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 ¶
BuildArgv constructs the full docker run argv for the given spec. It is pure given injectable FS and LookPath.
func BuildCleanupPrompt ¶ added in v0.9.0
func BuildCleanupPrompt(closures []LiveClosure) string
BuildCleanupPrompt is the confirmation text for `cell cleanup`. States the retention rule (running-only) and how many containers stay protected — the primary non-debug UX for the reaper.
func BuildCleanupScript ¶ added in v0.9.0
func BuildCleanupScript(closures []LiveClosure) string
BuildCleanupScript emits the reaper shell: remove gcroots/devcell/ entries whose hash is not in the live set. Only root symlinks and their -meta files are touched — never auto/ roots (namespace-local, CELL-330) and never the store itself (that's `cell build prune`'s job).
func BuildExecArgv ¶ added in v0.9.0
BuildExecArgv constructs a docker exec argv for attaching to a running container.
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 CELL-165. 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 BuildLabel ¶ added in v0.8.0
BuildLabel renders a progress label for an image build. When the stack was explicitly chosen by the user (TOML `stack = "..."` or `--stack`/env override), the qualifier is surfaced so they know what's about to run. Otherwise it's omitted because stacks are deprecated (CELL-6/337) and surfacing the resolved-default misleads — implies an opt-in that wasn't made.
func BuildPrunePrompt ¶ added in v0.7.0
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 CELL-101.
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
ChangedBuildFiles returns which build context files are newer than the image. Returns the list of changed file names and true if any changed.
func ClosureAliveArgv ¶ added in v0.9.0
ClosureAliveArgv returns the docker argv for the closure-alive probe. Runs inside the target image with the nix volume mounted so the profile's /nix/store/... symlink target can be resolved.
func ClosureDeadWarning ¶ added in v0.9.0
ClosureDeadWarning returns a user-facing warning and dead=true when the closure probe reports the image's nix paths are gone.
func CollectRunningContainers ¶ added in v0.8.0
CollectRunningContainers returns `docker ps` output for display.
func CollectVolumeMounts ¶ added in v0.8.0
CollectVolumeMounts returns a map of volume name → container names that mount it.
func ConfirmDestructive ¶ added in v0.7.0
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 ConfirmProceed ¶ added in v0.9.0
ConfirmProceed is the default-YES twin of ConfirmDestructive: a nudge, not a gate. Prints the warning; bare Enter or anything except n/no proceeds. Non-TTY prints the warning and proceeds unconditionally — automation is never blocked by a hygiene nudge.
func ConsumeBootEvents ¶ added in v0.8.0
func ConsumeBootEvents(events <-chan BootEvent)
ConsumeBootEvents reads BootDirWatcher events and renders each as a permanent ✓ row directly via ux.SuccessMsg. CELL-264.
Wire format matches the host-side PhaseRunner (CELL-262) so the rendered checklist reads as one continuous boot story:
✓ Cell ready ← from PhaseRunner.Seal on the host
✓ Loading mise tools ← from sentinel mise.starting
✓ Mise ready ← from sentinel mise.ready
✓ GUI ready ← from sentinel gui.ready
← boot.ready stops the consumer; TTY handoff
Why no spinner ¶
The CELL-262 PhaseRunner uses ProgressSpinner for host-side phases because the host is actively *doing* work while the spinner ticks. Here, the events represent completed milestones from inside the container — by the time the host sees the sentinel file, the work is already done. A spinner adds no information AND introduces a race: the spinner's clear-on-stop can land on the wrong terminal row when the container's stdout writes concurrently to the same TTY (visible as a stuck `⠋ GUI ready 80ms` ghost row preceding the sealed ✓).
Direct permanent-line emission avoids the race entirely. No spinner goroutine, no clear, no chance of fighting the container TTY.
Title lookup precedence ¶
- event.Title — set by BootDirWatcher from the host-side titles map
- event.Component when Title is empty AND component != "boot" — falls back so a fragment introducing a new component still produces a visible row (just with the raw name) instead of being dropped.
boot.ready ¶
The terminal `boot.ready` event (sentinel emitted by entrypoint.sh after all fragments) carries Title="" by design — the consumer treats it as the seal trigger, NOT as a row to render. Returns immediately when seen.
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 ContainerRunning ¶ added in v0.9.0
ContainerRunning checks if a container with the given name is currently running.
func DebugArgv ¶ added in v0.9.0
DebugArgv renders an argv for --debug output. Multi-line elements (embedded `sh -c` scripts) are elided to a one-line placeholder — dumping the probe script made the health check look like it printed the script instead of running it.
func DetectArch ¶ added in v0.8.0
func DetectArch() string
DetectArch returns "aarch64" or "x86_64". Respects DEVCELL_ARCH env override ("amd64"→"x86_64", "arm64"→"aarch64") for cross-architecture builds.
func DiffBuildFile ¶ added in v0.4.0
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
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 DockerBindProbeArgv ¶ added in v0.9.0
DockerBindProbeArgv composes the non-mutating debug probe command.
func DockerDaemonReachable ¶ added in v0.8.0
DockerDaemonReachable probes the daemon with a single `docker info` call. Cheap (~30ms warm) and avoids the confusing fallout (`Pulling core image nixos/nix:latest failed`) when the socket is unreachable. Call once at the top of any path that will later invoke docker (CELL-44).
func DockerHostPath ¶ added in v0.8.0
DockerHostPath translates container-local paths (e.g. /devcell-256/nixhome) to Docker-accessible host paths when running inside a devcell container. Uses DEVCELL_HOST_PROJECT_DIR env var set by the entrypoint.
func DockerPlatform ¶ added in v0.9.0
DockerPlatform returns the docker --platform value for the given nix arch.
func DockerResolveContainerLink ¶ added in v0.9.0
DockerResolveContainerLink resolves a symlink inside a running container to its final target — the only namespace where /opt/devcell resolves.
func DockerRunningDevcellContainers ¶ added in v0.9.0
DockerRunningDevcellContainers lists RUNNING devcell containers by the devcell.basedir label every `cell` launch stamps (runner.go BuildArgv). Running-only on purpose: `docker ps` without -a (CELL-334 retention decision, 2026-08-01).
func DockerVolumeDebug ¶ added in v0.9.0
DockerVolumeDebug returns daemon-side metadata for a named volume.
func DockerfileChanged ¶
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 ¶
EnsureNetwork creates the devcell-network docker network if it doesn't exist.
func FlakeNixImage ¶ added in v0.9.0
func FlakeNixImage() string
FlakeNixImage returns the lightweight nixos/nix image used for flake lock/update and stack discovery. Decoupled from the binary version so dev builds with -dirty tags don't require a registry push.
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 FormatMCPLoadedBanner ¶ added in v0.8.0
FormatMCPLoadedBanner returns a single-line banner the CLI prints after the cell starts but before handing off to the agent UI. Its job is to prove to the user that:
- Their `~/.claude/mcp.json` MCPs carried over into the cell.
- The nix-managed (DevCell-bundled) MCPs are wired in.
Format example:
✓ MCP loaded — nix: opentofu, patchright (2) yours: github, linear (2)
Both lists are sorted alphabetically for a stable, scannable output. If neither slice has entries, the banner reports the empty state honestly so the user can spot a config-mount failure immediately.
func FormatNixStoreSection ¶ added in v0.9.0
func FormatNixStoreSection(r NixStoreReport, w io.Writer)
FormatNixStoreSection renders the nix block of `cell build df`. An empty report (no volume data) renders nothing.
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 (CELL-98).
func FormatTableWithVM ¶ added in v0.8.0
func FormatTableWithVM(snap DFSnapshot, opts FormatOpts, vm VMDiskInfo, containers []string, volMounts map[string][]string, w io.Writer) error
FormatTableWithVM renders the ranked entries table plus Docker VM disk info, running containers, volume-to-container mapping, and cleanup instructions.
func HumanBytes ¶ added in v0.7.0
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 ¶
ImageExists returns true if a Docker image with the given tag exists locally.
func ImageExistsForPlatform ¶ added in v0.9.0
ImageExistsForPlatform returns true if a Docker image with the given tag exists locally AND matches the requested platform (e.g. "linux/amd64"). Empty platform falls back to ImageExists (host default).
func ImageVersions ¶ added in v0.3.0
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 ¶
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
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
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 LookPathNix ¶ added in v0.9.0
LookPathNix is a test helper that exposes exec.LookPath("nix").
func NixDFReportArgv ¶ added in v0.9.0
NixDFReportArgv wraps the report script in a docker-run of the volume.
func NixHealthProbeArgv ¶ added in v0.9.0
NixHealthProbeArgv wraps the probe in a docker-run of the given volume.
func NixSystemFor ¶ added in v0.7.0
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 ParseClosureAliveResult ¶ added in v0.9.0
ParseClosureAliveResult interprets the probe's stdout + error. Returns the resolved store path and whether the closure is alive.
func ParseSentinelName ¶ added in v0.8.0
ParseSentinelName decomposes a sentinel filename into (component, state). The split is on the LAST '.' so component names containing dots (e.g. "nix.daemon") split correctly. Returns ok=false for empty input, missing '.', or empty halves on either side.
func ParseSize ¶ added in v0.7.0
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
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
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
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 (CELL-183) + CELL-165 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 PickImageTagThin ¶ added in v0.8.0
func PickImageTagThin() string
PickImageTagThin returns the thin image tag for runtime callers.
func PickSkopeoBin ¶ added in v0.7.0
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
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 PreflightPlatformCheck ¶ added in v0.9.0
PreflightPlatformCheck runs `nix eval` on the flake's platformStrictCheck attribute to verify that all packages (including transitive dependencies) are compatible with the target system. Returns nil on success.
Skips gracefully (returns nil) when nix is not in PATH — this allows the thin Docker build path to work on hosts without nix installed.
targetSystem is a nix system string like "aarch64-linux" or "aarch64-darwin".
func PreflightPlatformCheckWithLookPath ¶ added in v0.9.0
func PreflightPlatformCheckWithLookPath(ctx context.Context, nixhomeFlakeRef, targetSystem string, lookPath func(string) (string, error)) error
PreflightPlatformCheckWithLookPath is the testable seam — tests inject a custom lookPath to simulate nix-not-found without modifying PATH.
func PrepareWireguard ¶ added in v0.9.0
func PrepareWireguard(cellHome string, cellCfg cfg.CellConfig) error
PrepareWireguard writes WireGuard config files for each enabled entry to <cellHome>/.wg/<name>.conf. PrivateKey lines are stripped from the config; a PostUp directive loads the key from /run/secrets/wg-private-key at runtime. No-op when no entries are enabled.
func ProbeDockerBind ¶ added in v0.9.0
ProbeDockerBind verifies both that the daemon accepts source and that marker is visible inside a container. It is intended for --debug diagnostics only.
func ProjectNixpkgsRev ¶ added in v0.9.0
ProjectNixpkgsRev reads the scaffolded flake.lock under <baseDir>/.devcell/flake.lock and returns nodes.nixpkgs.locked.rev. A missing or unparsable lock returns "" without error — the warning simply won't fire (never fatal, never blocking).
func PullAndTagImpure ¶ added in v0.7.0
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
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
PullImage attempts to pull a Docker image. Returns nil on success. When verbose is true, docker pull output is streamed to os.Stderr.
func PullImageForPlatform ¶ added in v0.9.0
PullImageForPlatform pulls a Docker image for a specific platform. Empty platform uses Docker's default (host architecture).
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 ¶
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 RenderModulesOverlay ¶ added in v0.8.0
RenderModulesOverlay returns a Nix module that flips `devcell.modules.<name>.enable = true` for each name in `modules`.
Output is always a parseable nix attrset, even for an empty list (in which case the body is empty — home-manager treats it as a no-op).
Module names are emitted as quoted strings (`"name"`) so that names with dashes ("yahoo-finance", "project-management") round-trip safely.
The output is deterministic: same input → byte-identical output. This matters for build-cache hash stability — re-running `devcell build` with an unchanged `.devcell.toml` must not invalidate the previous nix store.
func ResolveAppendPrompt ¶ added in v0.9.0
func ResolveAppendPrompt(opts ResolveOpts) (string, error)
ResolveAppendPrompt walks the same tier chain over the *append* sources. It never reads the base sources: after the split, [llm].system_prompt replaces Claude Code's built-in prompt while the append layer stacks on top of whichever base ends up in effect.
Resolution order (first match wins):
- opts.AppendFlagFile (--append-system-prompt-file)
- opts.AppendFlagInline (--append-system-prompt)
- opts.AppendEnvFile (DEVCELL_APPEND_SYSTEM_PROMPT_FILE)
- opts.AppendEnvInline (DEVCELL_APPEND_SYSTEM_PROMPT)
- CellCfg.LLM.AppendSystemPromptFile ([llm].append_system_prompt_file)
- CellCfg.LLM.AppendSystemPrompt ([llm].append_system_prompt)
- ""
func ResolveBuildTag ¶ added in v0.8.0
ResolveBuildTag returns the tag a `cell build` invocation should use: custom (typically from --image) when non-empty, falling back to the auto-derived stack tag. Trims whitespace on custom to forgive copy-paste.
func ResolveNixhomeRef ¶ added in v0.9.0
ResolveNixhomeRef returns the nixhome source to use for builds. Precedence: DEVCELL_NIXHOME > DEVCELL_NIXHOME_PATH (legacy) > default upstream flake ref. Accepts local paths, github: flake refs, or https:// git URLs.
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, which is the signal that no base is configured and Claude Code's built-in prompt must stay in effect.
Resolution order (first match wins):
- opts.FlagFile (--system-prompt-file)
- opts.FlagInline (--system-prompt)
- opts.EnvFile (DEVCELL_SYSTEM_PROMPT_FILE)
- opts.EnvInline (DEVCELL_SYSTEM_PROMPT)
- CellCfg.LLM.SystemPromptFile ([llm].system_prompt_file)
- CellCfg.LLM.SystemPrompt ([llm].system_prompt)
- ""
func RunCleanup ¶ added in v0.9.0
func RunCleanup(a RunCleanupArgs) error
RunCleanup orchestrates the reaper: build the plan, prompt, execute. Rejection is a clean no-op (user intent, not an error).
func RunDF ¶ added in v0.7.0
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 StackImageTagImpure ¶ added in v0.7.0
StackImageTagImpure returns the registry tag for the multi-arch Dockerfile- built (impure) stack image. The registry uses the explicit `-impure` suffix (CELL-165 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
StackImageTagPure returns the registry tag for a pre-built pure stack image. Example: StackImageTagPure("ultimate") → "<registry>:v<ver>-ultimate-pure"
func StaleCellWarning ¶ added in v0.9.0
func StaleCellWarning(projectRev string, h NixStoreHealth) (string, bool)
StaleCellWarning decides whether this project's closure is behind the newest rev live on the volume, and renders the warning if so. Unknown revs on either side mean silence — a nudge must never fire on guesswork.
func StampRootsScript ¶ added in v0.9.0
func StampRootsScript(closures []LiveClosure) string
StampRootsScript emits the shell that stamps hash-named GC roots for every live closure. Idempotent — re-stamping the same closure is a no-op (`ln -sfT` to the identical target). Only creates; never deletes, never GCs. Empty input yields an empty script (nothing to stamp).
func ThinBuildArgv ¶ added in v0.8.0
func ThinBuildArgv(coreImage, containerName, volumeName, nixhomeRef, thinTag, stackName, arch string) []string
ThinBuildArgv composes the docker run argv for the thin build. Runs nixos/nix with the nix store volume + docker socket. Inside: home-manager switch (uses volume cache), then docker build to produce the thin image (nix-core + config, no /nix/store baked in).
nixhomeRef accepts EITHER:
- a filesystem path (e.g. /home/bob/nixhome) — the caller streams a tar archive to stdin, which is extracted at /opt/nixhome before home-manager
- a flake reference (e.g. github:devcell-sh/community-home/main) — no archive; home-manager runs against `<ref>#devcell-<stack><arch>` directly, letting nix fetch and cache under /nix/store. This is the clean-machine path (CELL-38) — no local nixhome required.
Detected by prefix: anything starting with a flake-scheme (github:, git+, path:, http:, etc.) is treated as remote; everything else is treated as a local filesystem path.
func ThinBuildArgvFull ¶ added in v0.8.0
func ThinBuildArgvFull(coreImage, containerName, volumeName, nixhomeRef, thinTag, hmTarget, arch, stack, modules, projectName string) []string
ThinBuildArgvFull is the canonical builder argv. hmTarget is the home-manager flake target name (typically "local" for thin); stack is the user-facing stack name written to DEVCELL_STACK/metadata.json; modules is a CSV of module names written to DEVCELL_MODULES (CELL-41); projectName is the project directory basename used to scope GC roots so different projects don't overwrite each other's nix store references (CELL-320).
func ThinStoreVolume ¶ added in v0.8.0
func ThinStoreVolume() string
ThinStoreVolume returns the volume name to use for the thin /nix store. Reads DEVCELL_NIX_VOLUME (trimmed); falls back to DefaultThinStoreVolume. When DEVCELL_ARCH requests a non-native architecture (e.g. amd64 on an ARM host), the default volume name gets an arch suffix so cross-arch builds use a separate nix store — mixing architectures on a single volume wastes space and causes dangling-symlink issues on every build.
func TranslateError ¶ added in v0.8.0
TranslateError converts a raw subprocess error from nix/docker/home-manager into a user-friendly one-liner with a next-action hint. Unknown errors pass through verbatim (no false-positive translation).
Used by CLI command handlers when wrapping `nix eval`, `docker run`, and `home-manager switch` failures. The agent's first 10 minutes should never dump a raw nix/docker traceback at the user.
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 lightweight nixos/nix container with configDir bind-mounted.
func UpstreamFlakeRef ¶ added in v0.8.0
UpstreamFlakeRef returns the canonical github flake reference for the devcell nixhome, pinned to `ref`. Empty / "v0.0.0" / dev-version coerces to DefaultNixhomeGitRef so dev builds always point at a real branch.
Example: UpstreamFlakeRef("v1.0.0") → "github:devcell-sh/community-home/v1.0.0"
func UpstreamFlakeRefNoVersion ¶ added in v0.8.0
func UpstreamFlakeRefNoVersion() string
UpstreamFlakeRefNoVersion returns the unpinned ref — used by introspection commands (`cell modules list`) that want the catalog as it exists upstream right now, not pinned to the CLI binary's compile-time version.
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_cell_image=true): devcell-user:<cell>
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 (CELL-189). Same repo as UserImageTag with a "-pure" suffix.
Example: UserImageTag()="devcell-user:ultimate" → UserImageTagPure()="devcell-user:ultimate-pure".
func UserImageTagThin ¶ added in v0.8.0
func UserImageTagThin() string
UserImageTagThin returns the local tag for thin images — nix store lives on a Docker named volume, not baked into the image.
Resolution order:
- DEVCELL_USER_IMAGE_THIN — legacy explicit override
- DEVCELL_USER_IMAGE — modern unified override; used as-is (no suffix). CELL-286 prep: every devcell image we publish is thin, so the `-thin` suffix on the env-driven path is redundant. CI sets `DEVCELL_USER_IMAGE=ghcr.io/devcell-sh/devcell:v<ver>-<arch>` and expects this exact tag at runtime (no suffix appended).
- UserImageTag() + "-thin" — local-dev fallback, preserves the suffix convention so `devcell-user:<stack>-thin` doesn't collide with `-pure`/`-impure` legacy local tags.
Example (no env): UserImageTag()="devcell-user:ultimate" → "devcell-user:ultimate-thin".
func VagrantBinaryExists ¶ added in v0.5.0
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
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
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
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
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
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
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
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
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
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
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.
func VolumeContains ¶ added in v0.8.0
VolumeContains spawns a throwaway busybox container (~5 MB) to test whether a sentinel path exists inside the named volume. sentinelPath must be the absolute in-container path the image's ENTRYPOINT references (e.g. /nix/var/nix/profiles/devcell-tools/bin/tini); see VolumeContainsArgv for the mount-layout rationale.
func VolumeContainsArgv ¶ added in v0.8.0
VolumeContainsArgv returns the docker argv used by VolumeContains. Pure so the mount layout can be unit-tested without spawning docker.
The volume MUST be mounted at /nix (not /probe). Nix profile entries are absolute symlinks rooted at /nix/store/...; mounting elsewhere makes `test -e` follow a dangling target and report MISSING for a perfectly hydrated volume (CELL-45). Mounting at /nix matches what the builder and runner already do, so absolute symlinks resolve in-container.
func VolumeExists ¶ added in v0.8.0
VolumeExists shells out to `docker volume inspect` — returns true on exit 0.
func VolumeHydrated ¶ added in v0.8.0
func VolumeHydrated(volumeName, sentinelPath string, exists func(string) bool, probe func(string, string) bool) bool
VolumeHydrated reports whether a named Docker volume exists AND contains the given sentinel path. Use to gate auto-build in `cell shell`/`claude`/... so a stale or pruned `/nix` volume triggers a rebuild instead of failing later at the kernel's `exec: tini: no such file or directory` (CELL-40/332).
Pure function — caller injects the two probes so unit tests don't need docker. For runtime use, see VolumeExists and VolumeContains below.
func WarnThickDeprecation ¶ added in v0.8.0
func WarnThickDeprecation()
WarnThickDeprecation emits a one-time deprecation warning when the user opts out of thin mode (`--no-thin`, `--thick`, `thin = false` in TOML, or `DEVCELL_THIN=0`). Thin mode (Docker volume nix store) is the canonical path post-Modules-2.0; non-thin / "thick" image builds are kept for backwards compatibility and will be removed in a future release.
Safe to call from multiple sites (cmd/root.go, cmd/build.go); the message fires at most once per process via sync.Once.
func WriteBasePrompt ¶ added in v0.9.0
func WriteBasePrompt(c config.Config, opts ResolveOpts) (string, error)
WriteBasePrompt materializes the base prompt, returning the container path for --system-prompt-file — or "" when no base is configured.
The empty return is the switch that keeps this opt-in: with no base set, the caller emits no flag and Claude Code's stock prompt stays in effect. The content is written verbatim; container context belongs on the overlay.
func WriteModulesOverlay ¶ added in v0.8.0
WriteModulesOverlay renders the overlay and writes it to outPath, replacing any existing file. Caller is responsible for creating outPath's parent dir.
func WriteOverlayPrompt ¶ added in v0.9.0
func WriteOverlayPrompt(c config.Config, cellCfg cfg.CellConfig, opts ResolveOpts) (string, error)
WriteOverlayPrompt assembles the overlay and materializes it, returning the container path for --append-system-prompt-file.
Container context is always present, so this file is always written even when no append prompt is configured.
func WritePromptFile ¶ added in v0.9.0
WritePromptFile materializes prompt content to disk and returns the path the *container* will read it at.
Prompts used to travel as a single argv element. That capped them at MAX_ARG_STRLEN (128 KiB on Linux) and published their full text to `ps aux` and `docker inspect`. Writing a file and passing its path removes both limits — claude reads it via --system-prompt-file / --append-system-prompt-file.
Files are namespaced per cell. .devcell/ is per project, but a container is per (cell, project) pair and ContainerContext embeds the cell name, so a shared path would let one cell boot with another cell's container context.
The returned path is the container-side path: the project is bind-mounted at /<AppName>, so translating is a prefix swap of BaseDir for that root.
func WriteThinBuildContext ¶ added in v0.9.0
WriteThinBuildContext writes the local nixhome overlay needed by the thin builder. Streaming this through the Docker API avoids asking a VM-backed daemon to resolve a host path for a second-generation container.
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 BootDirWatcher ¶ added in v0.8.0
type BootDirWatcher struct {
// contains filtered or unexported fields
}
BootDirWatcher polls the boot dir for new sentinel files and emits BootEvents on a buffered channel. Zero-value is usable; call Start to begin polling and Close to stop.
We POLL rather than use fsnotify because Docker Desktop on macOS (and, historically, Windows) doesn't forward inotify events across bind-mounts through gRPC-FUSE / virtiofs. The container's `touch` writes ARE forwarded (data is visible cross-boundary), but the kernel-event channel is not. Polling sidesteps that entirely — we just keep asking `what's in this dir?` and emit a BootEvent for each new filename.
Cost: one `getdents` syscall per PollInterval, looking at ~10 small filenames. Negligible compared to the multi-second container boot.
func (*BootDirWatcher) Close ¶ added in v0.8.0
func (w *BootDirWatcher) Close() error
Close stops the polling goroutine. Sentinel files are intentionally NOT removed — they're useful for post-mortem debugging (`ls ~/.devcell/<cell>/boot/`). Safe to call multiple times.
func (*BootDirWatcher) Start ¶ added in v0.8.0
func (w *BootDirWatcher) Start(dir string) (<-chan BootEvent, error)
Start ensures dir exists, then spawns the polling goroutine. Returns the events channel.
The dir is created (mode 0755) if missing — cmd/root.go can call Start without pre-creating, and the bind-mount sees a present directory.
type BootEvent ¶ added in v0.8.0
BootEvent is one parsed sentinel-file CREATE event. Title is the host-side human label looked up from the titles table — empty string if the component isn't yet known to the title registry (consumer should fall back to using Component verbatim).
type BuildDebugInfo ¶ added in v0.7.0
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 BuildLimits ¶ added in v0.9.0
type BuildLimits struct {
Memory string // --memory value, "" to omit
CPUs string // --cpus value, "" to omit
MaxJobs int // nix max-jobs, 0 to leave at "auto"
Cores int // nix cores, 0 to leave at nix's own default
}
BuildLimits is what the builder may use: the docker ceilings plus the nix concurrency derived from them. A zero field means "emit nothing".
func ResolveBuildLimits ¶ added in v0.9.0
func ResolveBuildLimits() BuildLimits
ResolveBuildLimits returns the build resource limits that will be applied. Exported for debug logging in cmd/build.go.
type Catalog ¶ added in v0.8.0
type Catalog map[string]ModuleMeta
Catalog is the full catalog keyed by module name (e.g. "electronics", "yahoo-finance").
func ParseCatalogJSON ¶ added in v0.8.0
ParseCatalogJSON decodes raw JSON output from `nix eval .#devcellModules --json` into a typed Catalog. Returns an error if the JSON is malformed.
func ReadCatalogFromFlake ¶ added in v0.8.0
ReadCatalogFromFlake invokes `nix eval --json <flakeRef>#devcellModules` and parses the result. `flakeRef` is something like `path:./nixhome` or `github:devcell/modules?ref=v1.0`.
Returns an error if the nix subprocess fails or the JSON is malformed. The CLI wraps this error with user-language guidance in cmd/.
type DFCollector ¶ added in v0.7.0
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 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 DockerCapacity ¶ added in v0.9.0
DockerCapacity is what the build daemon advertises for itself. Note this is the daemon's own ceiling, which on macOS is the Colima/Docker Desktop VM — not the host Mac's specs, and not necessarily the daemon this process runs under if contexts differ.
type DockerDebugInfo ¶ added in v0.9.0
type DockerDebugInfo struct {
Context string
Endpoint string
DockerHostEnv string
Runtime string
Name string
ServerVersion string
OperatingOS string
Architecture string
RootDir string
CPUs int
MemoryBytes int64
Socket string
SocketTarget string
}
DockerDebugInfo describes the client-to-daemon connection used by Cell. It deliberately excludes registry and authentication configuration.
func CollectDockerDebugInfo ¶ added in v0.9.0
func CollectDockerDebugInfo(ctx context.Context) (DockerDebugInfo, error)
CollectDockerDebugInfo reports the actual daemon selected by the current Docker CLI environment. This matters inside a cell, where /var/run/docker.sock was fixed when the outer container was created.
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.
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 FormatOpts ¶ added in v0.7.0
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
type LayerStats ¶ added in v0.7.0
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 LiveClosure ¶ added in v0.9.0
type LiveClosure struct {
Container string
ProfilePath string // /nix/store/<hash>-home-manager-path
GenerationPath string // /nix/store/<hash>-home-manager-generation
}
LiveClosure is one running container's protected store paths.
func CollectLiveClosures ¶ added in v0.9.0
func CollectLiveClosures( ps func() ([]string, error), resolve func(container, link string) (string, error), logf func(format string, args ...any), ) ([]LiveClosure, error)
CollectLiveClosures enumerates running devcell containers via ps and resolves each one's profile + generation store paths via resolve. Dependency-injected for tests; the runtime call site wires `docker ps` and `docker exec readlink -f`.
A running container whose closure cannot be resolved fails the whole collection: an unknown closure means neither the reaper nor the prune gate can guarantee safety, so both must refuse — naming the cell.
logf, when non-nil, receives one line per datapoint (--debug visibility).
type ModuleMeta ¶ added in v0.8.0
type ModuleMeta struct {
Description string `json:"description"`
MCPServers []string `json:"mcpServers"`
SizeMB int `json:"sizeMb"`
}
ModuleMeta is one entry from the `devcellModules` flake catalog. Mirrors the shape of `nix eval .#devcellModules --json`.
type NixRootEntry ¶ added in v0.9.0
NixRootEntry is one GC root symlink on the volume.
type NixRootMeta ¶ added in v0.9.0
NixRootMeta is the parsed -meta file for one root hash.
type NixStoreHealth ¶ added in v0.9.0
type NixStoreHealth struct {
TotalRoots int
StaleRoots int
ProfileHashes int
Generations int
OrphanedGenerations int
// Lock-drift datapoints from the *-meta files stamped at container
// start (CELL-332). Zero values on pre-CELL-332 volumes. CELL-391
// consumes these for the stale-cell warning.
DistinctRevs int // distinct nixpkgs revs across metas
NewestRev string // rev with the most recent stamped= timestamp
NewestProjects int // how many metas sit on NewestRev
}
NixStoreHealth is the parsed probe result.
func ParseNixStoreHealth ¶ added in v0.9.0
func ParseNixStoreHealth(out string) (NixStoreHealth, error)
ParseNixStoreHealth finds the datapoint line in probe output (docker may prepend image-pull noise) and parses its key=value fields. Missing keys are zero values (older probes / pre-CELL-332 volumes emit fewer fields). No datapoint line means the probe degraded — callers treat that as "skip silently", never as fatal.
func (NixStoreHealth) Summary ¶ added in v0.9.0
func (h NixStoreHealth) Summary() (string, bool)
Summary renders the one-line non-debug UX for the "Nix store" phase row. The bool return signals whether the result is a warning (should render ⚠ instead of ✓).
type NixStoreReport ¶ added in v0.9.0
type NixStoreReport struct {
Volume string
Health NixStoreHealth
Roots []NixRootEntry
Metas []NixRootMeta
}
NixStoreReport is the full parsed df report for the nix volume.
func CollectNixStore ¶ added in v0.9.0
func CollectNixStore(ctx context.Context) (NixStoreReport, bool)
CollectNixStore runs the report against the thin store volume. Any failure returns ok=false — the df section is simply omitted, matching the CELL-390 degrade-to-silence contract.
func ParseNixStoreReport ¶ added in v0.9.0
func ParseNixStoreReport(volume, out string) (NixStoreReport, error)
ParseNixStoreReport parses combined probe + listing output.
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
// LiveClosures are the running containers' resolved store paths
// (CollectLiveClosures). When set, the pure prune plan stamps GC roots
// for them before the safe GC step, so "running implies rooted" holds
// by construction instead of being assumed (CELL-334 preflight gate).
LiveClosures []LiveClosure
}
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 BuildCleanupSteps ¶ added in v0.9.0
func BuildCleanupSteps(closures []LiveClosure) []PruneStep
BuildCleanupSteps wraps the reaper script in a docker-run step mounting the nix volume — the only namespace where the devcell roots and their volume-resident targets both resolve (CELL-333 lesson).
func BuildDockerPruneSteps ¶ added in v0.7.0
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
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:devcell-sh/community-home/main").
// 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
// Thin builds a thin image (no /nix/store layers). The nix store is
// expected to be provided at runtime via a Docker named volume.
Thin bool
}
PureBuildSpec describes a pure-image build invocation.
type PureNixhomeInputs ¶ added in v0.7.0
type PureNixhomeInputs struct {
// TomlNixhome is the resolved [nix].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:
- inputs.TomlNixhome (explicit user setting via .devcell.toml / env)
- inputs.BaseDir + "/nixhome" on disk
- github:devcell-sh/community-home/<Version> (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
// AppendFlagFile / AppendFlagInline are the --append-system-prompt-file /
// --append-system-prompt CLI flags. Currently exposed only on `cell serve`.
AppendFlagFile, AppendFlagInline string
// AppendEnvFile / AppendEnvInline are DEVCELL_APPEND_SYSTEM_PROMPT_FILE /
// DEVCELL_APPEND_SYSTEM_PROMPT. Read by every surface.
AppendEnvFile, AppendEnvInline string
// CellCfg supplies [llm].system_prompt / system_prompt_file (base) and
// [llm].append_system_prompt / append_system_prompt_file (overlay) 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 RunCleanupArgs ¶ added in v0.9.0
type RunCleanupArgs struct {
Closures []LiveClosure
Exec func(step PruneStep) error
Out io.Writer
In io.Reader
SkipYes bool
IsTTY bool
}
RunCleanupArgs bundles inputs to RunCleanup, mirroring RunPruneArgs.
type RunDFArgs ¶ added in v0.7.0
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
SkipFlake bool // pass DEVCELL_SKIP_FLAKE=1 into the container
TrustFlake bool // pass DEVCELL_FLAKE_TRUST=1 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
ThinImage bool // when true, mount devcell-nix-store volume for /nix
BootDir string // CELL-264: host-side boot dir for fsnotify sentinels; empty disables the bind-mount
TTY bool // allocate a pseudo-TTY (-it); set from isatty check on stdin
Detach bool // run container in detached mode (-d); set by `cell start`
}
RunSpec holds everything needed to build the docker run argv.
type VMDiskInfo ¶ added in v0.8.0
VMDiskInfo holds Docker VM filesystem info from `docker run alpine df`.
func CollectVMDisk ¶ added in v0.8.0
func CollectVMDisk(ctx context.Context) (VMDiskInfo, error)
CollectVMDisk probes the Docker VM filesystem size.
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.
Source Files
¶
- acquire_image.go
- banner.go
- boot_consumer.go
- boot_watcher.go
- build_debug.go
- build_label.go
- catalog.go
- cleanup.go
- cleanup_docker.go
- closure_alive.go
- df.go
- df_collect.go
- df_nix.go
- docker_alive.go
- docker_debug.go
- errors.go
- exec.go
- launch_decision.go
- nix_health.go
- overlay.go
- platform_preflight.go
- preflight.go
- promptfile.go
- prune.go
- pure_build.go
- pure_build_heartbeat.go
- pure_nixhome_resolver.go
- registry.go
- runner.go
- stale_cell.go
- systemprompt.go
- thick_deprecation.go
- thin_build.go
- thin_context.go
- thin_volume.go
- upstream.go
- vagrant.go
- volume_hydrated.go