// Package cli parses werkdock's docker-shaped command line (RFC 0002) // and dispatches to the internal packages. Exit codes follow docker: // 125 for werkdock's own errors, otherwise the sandboxed command's code // is passed through. package cli import ( "fmt" "io" "os" "strings" ) // Version of this build. A merge to main is a release, so the branch // that changes behavior bumps it here and titles its CHANGELOG section // with it; there is no separate release step. var Version = "0.8.0" const exitCLIError = 125 // Main runs the CLI and returns the process exit code. func Main(args []string) int { if len(args) == 0 { usage(os.Stderr) return exitCLIError } switch args[0] { case "run": return runCmd(args[1:]) case "create": return createCmd(args[1:]) case "start": return startCmd(args[1:]) case "stop": return stopCmd(args[1:]) case "kill": return killCmd(args[1:]) case "rm": return rmCmd(args[1:]) case "ps": return psCmd(args[1:]) case "logs": return logsCmd(args[1:]) case "wait": return waitCmd(args[1:]) case "exec": return execCmd(args[1:]) case "system": return systemCmd(args[1:]) case "supervise": return superviseCmd(args[1:]) case "container": return subcommand("container", args[1:], map[string]string{"ls": "ps", "list": "ps"}, "create", "start", "stop", "kill", "rm", "ps", "logs", "wait", "exec", "inspect", "run") case "image": return subcommand("image", args[1:], map[string]string{"ls": "images", "list": "images", "rm": "rmi"}, "images", "rmi", "pull", "load", "import", "inspect") case "import": return importCmd(args[1:]) case "load": return loadCmd(args[1:]) case "pull": return pullCmd(args[1:]) case "rmi": return rmiCmd(args[1:]) case "images": return imagesCmd(args[1:]) case "inspect": return inspectCmd(args[1:]) case "doctor": return doctorCmd(args[1:]) case "version", "--version": fmt.Printf("werkdock %s\n", Version) return 0 case "help", "--help", "-h": usage(os.Stdout) return 0 default: fmt.Fprintf(os.Stderr, "werkdock: unknown command %q\n\n", args[0]) usage(os.Stderr) return exitCLIError } } // subcommand maps docker's management form (`container ls`, `image // rm`) onto the top-level verbs. func subcommand(group string, args []string, aliases map[string]string, verbs ...string) int { if len(args) == 0 { fmt.Fprintf(os.Stderr, "werkdock: %s needs a subcommand: %s\n", group, strings.Join(verbs, ", ")) return exitCLIError } verb := args[0] if alias, ok := aliases[verb]; ok { verb = alias } for _, v := range verbs { if v == verb { return Main(append([]string{verb}, args[1:]...)) } } fmt.Fprintf(os.Stderr, "werkdock: unknown command %q for %s\n", args[0], group) return exitCLIError } func usage(w io.Writer) { fmt.Fprint(w, `werkdock — a docker-like sandbox CLI over bwrap, filesystem isolation only. Network, /proc, /dev, and /tmp come from the host by contract; the caller is the sandbox's only user, uid 0 unless --user or the image's USER says otherwise. Usage: werkdock run [flags] IMAGE [COMMAND] [ARG...] run a command in a sandbox (-d: in the background, as a service) werkdock create [flags] IMAGE [COMMAND] [ARG...] create an instance without starting it (docker create) werkdock start INSTANCE... start created or stopped instances in the background werkdock ps [-a] [-q] [--format FMT] list running (-a: all) instances, docker's columns werkdock stop [-t SECONDS] INSTANCE... SIGTERM, then SIGKILL after SECONDS (default 10) werkdock kill [-s SIGNAL] INSTANCE... send a signal (default KILL) werkdock logs [-f] INSTANCE print the recorded stdout and stderr (-f: follow) werkdock wait INSTANCE... wait for exit, print the exit code werkdock exec [flags] INSTANCE COMMAND [ARG...] run another command in an instance's sandbox werkdock rm [-f] INSTANCE... remove instances (-f: a running one too) werkdock inspect IMAGE|INSTANCE... print image or instance metadata as JSON, docker-shaped werkdock import [-c CHANGE...] ARCHIVE [IMAGE] create an image from a rootfs archive (docker import) werkdock load -i ARCHIVE load a docker save / OCI image archive, layers flattened werkdock pull [--platform OS/ARCH] IMAGE fetch an image from a registry through the crane binary werkdock images [--format FMT] [-q] [REPO[:TAG]] list images: one reference per line, or docker's table/template werkdock rmi IMAGE... remove images (docker rmi) werkdock system service [--time D] [unix://PATH] serve the docker API subset (for Testcontainers) werkdock doctor [TARGET_DIR] check whether this host can run sandboxes werkdock version print the version Run flags: -v, --volume SRC:DEST[:ro] bind mount (repeatable; -v, --tmpfs and --mount apply in flag order) --tmpfs DEST empty tmpfs at DEST (repeatable; /tmp, /root and /run always are one) --mount type=bind,src=SRC,dst=DEST[,readonly] the same, in docker's explicit form (repeatable) --mount type=tmpfs,dst=DEST -e, --env KEY=VALUE set an environment variable (KEY alone copies it from the host, if set there) -w, --workdir DIR working directory inside the sandbox (default /; created if missing and writable) --entrypoint PROGRAM run PROGRAM with the arguments after IMAGE (overrides the image's ENTRYPOINT, drops its CMD) -u, --user USER[:GROUP] appear as this user inside the sandbox (name from the image's /etc/passwd, or a number); default: the image's USER, else uid 0. One uid only: the caller is mapped to it --userns keep-id appear as the calling user's own uid and gid (podman's spelling) --network host accepted as a no-op: the sandbox shares the host's network by contract (other modes and -p/--publish are refused) -d, --detach start the instance in the background under a supervisor and print its id --name NAME name the instance (default: image basename plus six characters of the id) --rm remove the instance when the command exits (a foreground --rm run records no logs) -i, --interactive pass stdin to the command (-t is refused: no pseudo-terminal) Exec flags: -e, -w, -u, -i as for run; the rootfs, mounts, uid, environment, /run and the anonymous volumes are the instance's, the pid namespace is not (its processes are invisible). Ps flags: -a, --all stopped and created instances too -q, --quiet print ids only --format FMT Go template over .ID .Image .Command .CreatedAt .RunningFor .Status .State .Ports .Names, or "table [TEMPLATE]" Import flags: -c, --change INSTRUCTION Dockerfile instruction applied to the image (repeatable): ENTRYPOINT, CMD (JSON array or shell form), ENV, WORKDIR, USER, VOLUME, LABEL Images flags: --format FMT Go template over .Repository .Tag .ID .CreatedAt .CreatedSince .Size, or "table [TEMPLATE]" for docker's columns (REPOSITORY TAG IMAGE ID CREATED SIZE) -q, --quiet print image IDs only Pull flags: --platform OS/ARCH platform to pick from a multi-arch image (default linux/) pull runs crane (github.com/google/go-containerregistry): found in PATH, beside werkdock, or at WERKDOCK_CRANE Without COMMAND, run executes the image's ENTRYPOINT and CMD; with one, it replaces CMD. Env and WORKDIR of the image apply unless -e/-w override them. IMAGE is a docker/OCI-style reference, [REGISTRY/]REPOSITORY[:TAG], e.g. eclipse-temurin:21 or ghcr.io/example/build:2026.09; the tag defaults to latest. INSTANCE is an id, a unique id prefix, or a name. The store lives in $WERKDOCK_HOME (default ~/.werkdock); instances in its instances/ directory with config.json, state.json, stdout, stderr, run/ and volumes/ each. docker's management form works too: container ls|start|..., image ls|rm|pull|.... `+"`"+`werkdock system service`+"`"+` listens on $XDG_RUNTIME_DIR/werkdock.sock by default; a client reaches it with DOCKER_HOST=unix://. `) } func fail(err error) int { fmt.Fprintf(os.Stderr, "werkdock: %v\n", err) return exitCLIError }