diff --git a/.claude/skills/swiftyshell.md b/.claude/skills/swiftyshell.md index 397b34d..ff5e2e4 100644 --- a/.claude/skills/swiftyshell.md +++ b/.claude/skills/swiftyshell.md @@ -31,13 +31,15 @@ Before writing any code, follow this decision tree: → Use `Swift` 8. Is this a GitHub CLI operation (`gh pr`, `gh repo`, `gh workflow`, `gh api`, `gh copilot`, `gh skill`, ...)? → Use `Gh` -9. Does the operation need typed output, structured results, or conditional follow-up? +9. Is this a Docker CLI operation (`docker buildx`, `docker compose`, `docker debug`, `docker mcp`, `docker scout`, ...)? + → Use `Docker` +10. Does the operation need typed output, structured results, or conditional follow-up? → Use the appropriate typed client -10. Are two or more commands chained by pipe? +11. Are two or more commands chained by pipe? → Use `.pipe(to:)` to build a `Pipeline` -11. Does the command write output to a file? +12. Does the command write output to a file? → Use `.stdout(.file(path:append:))` on the command -12. Is this any other command? +13. Is this any other command? → Use `Command` ### API Reference @@ -851,6 +853,105 @@ public struct Gh: RunnableCommandFamily { } ``` +#### Docker CLI + +```swift +public enum DockerSubcommand: String, Sendable, Equatable, Hashable { + case builder + case buildx + case checkpoint + case compose + case config + case container + case context + case debug + case desktop + case dhi + case image + case initialize + case inspect + case login + case logout + case manifest + case mcp + case model + case network + case node + case offload + case pass + case plugin + case sandbox + case scout + case search + case secret + case service + case stack + case swarm + case system + case trust + case version + case volume +} + +public enum DockerBuildProgress: String, Sendable, Equatable, Hashable { + case auto + case plain + case tty + case rawjson +} + +public struct Docker: RunnableCommandFamily { + public init(context: ShellContext = .init()) + public func subcommand(_ value: DockerSubcommand) -> Self + public func subcommand(_ value: String) -> Self + public func subcommand(_ value: DockerSubcommand, _ nested: String) -> Self + public func subcommand(_ value: String, _ nested: String) -> Self + public func buildx(_ nested: String? = nil) -> Self + public func compose(_ nested: String? = nil) -> Self + public func container(_ nested: String? = nil) -> Self + public func image(_ nested: String? = nil) -> Self + public func initialize() -> Self + public func debug(_ target: String? = nil) -> Self + public func mcp(_ nested: String? = nil) -> Self + public func scout(_ nested: String? = nil) -> Self + public func system(_ nested: String? = nil) -> Self + public func version() -> Self + public func configPath(_ path: String) -> Self + public func context(_ name: String) -> Self + public func host(_ value: String) -> Self + public func logLevel(_ value: String) -> Self + public func debugMode(_ enabled: Bool = true) -> Self + public func tls(_ enabled: Bool = true) -> Self + public func tlsVerify(_ enabled: Bool = true) -> Self + public func platform(_ value: String) -> Self + public func file(_ path: String) -> Self + public func tag(_ name: String) -> Self + public func tags(_ names: [String]) -> Self + public func buildArg(_ value: String) -> Self + public func buildArgs(_ values: [String]) -> Self + public func progress(_ value: DockerBuildProgress) -> Self + public func push(_ enabled: Bool = true) -> Self + public func load(_ enabled: Bool = true) -> Self + public func pull(_ enabled: Bool = true) -> Self + public func name(_ value: String) -> Self + public func removeWhenDone(_ enabled: Bool = true) -> Self + public func detach(_ enabled: Bool = true) -> Self + public func interactive(_ enabled: Bool = true) -> Self + public func tty(_ enabled: Bool = true) -> Self + public func commandString(_ value: String) -> Self + public func shell(_ value: String) -> Self + public func format(_ value: String) -> Self + public func option(_ name: String) -> Self + public func option(_ name: String, _ value: String) -> Self + public func argument(_ value: String) -> Self + public func arguments(_ values: [String]) -> Self + public func positionalArgument(_ value: String) -> Self + public func positionalArguments(_ values: [String]) -> Self + public func command() -> Command + public func run() async throws -> ShellOutput +} +``` + #### Archives (Tar / Zip / Unzip) ```swift @@ -1520,7 +1621,7 @@ SwiftyShell uses [SwiftPM Package Traits](https://github.com/swiftlang/swift-evo Declared in `Package.swift`: -- **Per-family** — `Git`, `Brew`, `Grep`, `Fzf`, `Rg`, `Swift`, `Gh`, `Ls`, `Cp`, `Mkdir`, `Chmod`, `Rm`, `Mv`, `Pwd`, `Jq`, `Rsync`, `Tar`, `Zip`, `Unzip`. One trait per family directory; for `Common/`, one trait per file. +- **Per-family** — `Git`, `Brew`, `Grep`, `Fzf`, `Rg`, `Swift`, `Gh`, `Docker`, `Ls`, `Cp`, `Mkdir`, `Chmod`, `Rm`, `Mv`, `Pwd`, `Jq`, `Rsync`, `Tar`, `Zip`, `Unzip`. One trait per family directory; for `Common/`, one trait per file. - **Umbrellas** — `CommonUtilities` (every `Common/*` family), `All` (every command family). Consumers select families with `traits:` on `.package(...)`: @@ -1533,7 +1634,7 @@ Consumers select families with `traits:` on `.package(...)`: The contract is enforced by `Scripts/validate-traits.swift` and CI: -1. Every `.swift` file under a gated source directory (`Git/`, `Brew/`, `Grep/`, `Fzf/`, `Rg/`, `Swift/`, `Gh/`, and each file in `Common/`) is wrapped top-to-bottom in `#if ... #endif`. +1. Every `.swift` file under a gated source directory (`Git/`, `Brew/`, `Grep/`, `Fzf/`, `Rg/`, `Swift/`, `Gh/`, `Docker/`, and each file in `Common/`) is wrapped top-to-bottom in `#if ... #endif`. 2. Every test file targeting a gated family is wrapped the same way. Cross-family tests use combined guards (`#if Git && Grep`). 3. Every family directory (or `Common/*.swift` file) has a matching `.trait(name:)` entry in `Package.swift`. 4. The `All` umbrella's `enabledTraits` transitively enables every per-family trait. The `CommonUtilities` umbrella enables every `Common/*` trait. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9329172..4160811 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: - name: Compute affected traits id: compute run: | - FULL='["", "Git", "Brew", "Grep", "Fzf", "Rg", "Swift", "Gh", "Rsync", "Tar", "Zip", "Unzip", "CommonUtilities", "All"]' + FULL='["", "Git", "Brew", "Grep", "Fzf", "Rg", "Swift", "Gh", "Docker", "Rsync", "Tar", "Zip", "Unzip", "CommonUtilities", "All"]' CHANGED=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD") echo "Changed files:" @@ -55,6 +55,7 @@ jobs: echo "$CHANGED" | grep -qE '^(Sources/SwiftyShell/Rg/|Tests/SwiftyShellTests/Rg/)' && family_traits+=("Rg") echo "$CHANGED" | grep -qE '^(Sources/SwiftyShell/Swift/|Tests/SwiftyShellTests/Swift/)' && family_traits+=("Swift") echo "$CHANGED" | grep -qE '^(Sources/SwiftyShell/Gh/|Tests/SwiftyShellTests/Gh/)' && family_traits+=("Gh") + echo "$CHANGED" | grep -qE '^(Sources/SwiftyShell/Docker/|Tests/SwiftyShellTests/Docker/)' && family_traits+=("Docker") echo "$CHANGED" | grep -qE '^(Sources/SwiftyShell/Common/|Tests/SwiftyShellTests/Common/)' && family_traits+=("CommonUtilities") traits+=("${family_traits[@]}") diff --git a/.github/workflows/reusable-ci.yml b/.github/workflows/reusable-ci.yml index 183b21b..f3b0b0d 100644 --- a/.github/workflows/reusable-ci.yml +++ b/.github/workflows/reusable-ci.yml @@ -25,7 +25,7 @@ on: Defaults to the full matrix when omitted. required: false type: string - default: '["", "Git", "Brew", "Grep", "Fzf", "Rg", "Swift", "Gh", "Rsync", "Tar", "Zip", "Unzip", "CommonUtilities", "All"]' + default: '["", "Git", "Brew", "Grep", "Fzf", "Rg", "Swift", "Gh", "Docker", "Rsync", "Tar", "Zip", "Unzip", "CommonUtilities", "All"]' jobs: validate-traits: diff --git a/AGENTS.md b/AGENTS.md index 2587abf..d683654 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,10 @@ Typed wrapper for the Swift toolchain: `Swift`, `SwiftSubcommand`, and `SwiftBui Typed wrapper for the GitHub CLI: `Gh` and `GhSubcommand`. +### `Sources/SwiftyShell/Docker/` + +Typed wrapper for the Docker CLI: `Docker`, `DockerSubcommand`, and `DockerBuildProgress`. + ### `Sources/SwiftyShell/Common/` Typed wrappers for frequently used shell utilities: `Ls`, `Cp`, `Mkdir`, `Chmod`, `Rm`, `Mv`, `Pwd`, `Jq`, `JqArgument`, `Rsync`, `Tar`, `TarOperation`, `TarCompression`, `Zip`, `ZipCompressionLevel`, `Unzip`, and `UnzipEntry`. Each follows the same fluent builder conventions as all other command families. @@ -48,7 +52,7 @@ DocC documentation catalog. `SwiftyShell.md` is the top-level landing page. Arti ### `Tests/SwiftyShellTests/` -Test suite. Sub-folders mirror the source layout: `Brew/`, `Common/`, `Core/`, `Fzf/`, `Gh/`, `Git/`, `Grep/`, `Pipelines/`, `Rg/`, and `Swift/`. Test files for gated families are wrapped in `#if ` so the test target compiles under any trait selection. `Common/` has one test file per family (`LsTests.swift`, `CpTests.swift`, …) plus `CommonTestSupport.swift` (shared helpers, ungated). +Test suite. Sub-folders mirror the source layout: `Brew/`, `Common/`, `Core/`, `Docker/`, `Fzf/`, `Gh/`, `Git/`, `Grep/`, `Pipelines/`, `Rg/`, and `Swift/`. Test files for gated families are wrapped in `#if ` so the test target compiles under any trait selection. `Common/` has one test file per family (`LsTests.swift`, `CpTests.swift`, …) plus `CommonTestSupport.swift` (shared helpers, ungated). ### `Scripts/` @@ -169,7 +173,7 @@ SwiftyShell uses [SwiftPM Package Traits](https://github.com/swiftlang/swift-evo **Trait inventory (declared in `Package.swift`):** -- Per-family: `Git`, `Brew`, `Grep`, `Fzf`, `Rg`, `Swift`, `Gh`, `Ls`, `Cp`, `Mkdir`, `Chmod`, `Rm`, `Mv`, `Pwd`, `Jq`, `Rsync`, `Tar`, `Zip`, `Unzip` (one trait per family directory; for `Common/`, one trait per file). +- Per-family: `Git`, `Brew`, `Grep`, `Fzf`, `Rg`, `Swift`, `Gh`, `Docker`, `Ls`, `Cp`, `Mkdir`, `Chmod`, `Rm`, `Mv`, `Pwd`, `Jq`, `Rsync`, `Tar`, `Zip`, `Unzip` (one trait per family directory; for `Common/`, one trait per file). - Umbrellas: `CommonUtilities` (all `Common/*`), `All` (every family). **The wiring contract** — enforced by `Scripts/validate-traits.swift` and CI: diff --git a/Package.swift b/Package.swift index 1c57250..c1921ac 100644 --- a/Package.swift +++ b/Package.swift @@ -23,6 +23,7 @@ let package = Package( .trait(name: "Rg", description: "Typed wrapper for ripgrep (rg)."), .trait(name: "Swift", description: "Typed wrapper for the Swift toolchain CLI."), .trait(name: "Gh", description: "Typed wrapper for the GitHub CLI."), + .trait(name: "Docker", description: "Typed wrapper for the Docker CLI."), .trait(name: "Ls", description: "Typed wrapper for ls."), .trait(name: "Cp", description: "Typed wrapper for cp."), .trait(name: "Mkdir", description: "Typed wrapper for mkdir."), @@ -45,7 +46,7 @@ let package = Package( .trait( name: "All", description: "Enables every command family shipped by SwiftyShell.", - enabledTraits: ["Git", "Brew", "Grep", "Fzf", "Rg", "Swift", "Gh", "CommonUtilities"] + enabledTraits: ["Git", "Brew", "Grep", "Fzf", "Rg", "Swift", "Gh", "Docker", "CommonUtilities"] ), // Default is intentionally empty: consumers opt in to the families they want. .default(enabledTraits: []), diff --git a/README.md b/README.md index 8dc3b25..2704dc6 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ SwiftyShell ships typed wrappers for common tools. Each family is gated behind a | `Fzf` | `fzf` | `Fzf` | Fuzzy finder options for interactive and filter-mode pipelines | | `Swift` | `swift` | `Swift` | SwiftPM build, test, run, package commands, traits, compiler flags | | `Gh` | `gh` | `Gh` | GitHub CLI automation for PRs, repos, workflows, Copilot, skills, API calls | +| `Docker` | `docker` | `Docker` | Docker automation for Buildx, Compose, Debug, MCP, Scout, images, containers | | `Ls` | `ls` | `Ls` | All flags, recursive, human-readable sizes | | `Cp` | `cp` | `Cp` | Recursive, force | | `Mkdir` | `mkdir` | `Mkdir` | Parent directories, permissions | diff --git a/Sources/SwiftyShell/Docker/Docker.swift b/Sources/SwiftyShell/Docker/Docker.swift new file mode 100644 index 0000000..ed5c716 --- /dev/null +++ b/Sources/SwiftyShell/Docker/Docker.swift @@ -0,0 +1,565 @@ +#if Docker +import Foundation + +/// The top-level Docker CLI command or command group to invoke. +public enum DockerSubcommand: String, Sendable, Equatable, Hashable { + /// `docker builder` — manage builds. + case builder + /// `docker buildx` — use Docker Buildx and BuildKit workflows. + case buildx + /// `docker checkpoint` — manage checkpoints. + case checkpoint + /// `docker compose` — run Docker Compose commands. + case compose + /// `docker config` — manage Swarm configs. + case config + /// `docker container` — manage containers. + case container + /// `docker context` — manage Docker contexts. + case context + /// `docker debug` — debug containers or images with a toolbox shell. + case debug + /// `docker desktop` — manage Docker Desktop. + case desktop + /// `docker dhi` — manage Docker Hardened Images. + case dhi + /// `docker image` — manage images. + case image + /// `docker init` — create Docker starter files for a project. + case initialize = "init" + /// `docker inspect` — inspect Docker objects. + case inspect + /// `docker login` — authenticate to a registry. + case login + /// `docker logout` — log out from a registry. + case logout + /// `docker manifest` — manage image manifests. + case manifest + /// `docker mcp` — manage MCP servers and clients. + case mcp + /// `docker model` — use Docker Model Runner commands. + case model + /// `docker network` — manage networks. + case network + /// `docker node` — manage Swarm nodes. + case node + /// `docker offload` — control Docker Offload. + case offload + /// `docker pass` — manage local OS keychain secrets. + case pass + /// `docker plugin` — manage plugins. + case plugin + /// `docker sandbox` — use Docker Sandbox commands. + case sandbox + /// `docker scout` — use Docker Scout commands. + case scout + /// `docker search` — search Docker Hub. + case search + /// `docker secret` — manage Swarm secrets. + case secret + /// `docker service` — manage Swarm services. + case service + /// `docker stack` — manage Swarm stacks. + case stack + /// `docker swarm` — manage Swarm. + case swarm + /// `docker system` — manage Docker system resources. + case system + /// `docker trust` — manage image trust. + case trust + /// `docker version` — show Docker version information. + case version + /// `docker volume` — manage volumes. + case volume +} + +/// The progress output mode used by BuildKit-based build commands. +public enum DockerBuildProgress: String, Sendable, Equatable, Hashable { + /// Automatically choose progress output based on the terminal. + case auto + /// Plain text progress suitable for CI logs. + case plain + /// TTY progress output. + case tty + /// Raw JSON progress output. + case rawjson +} + +/// A fluent wrapper for the Docker CLI (`docker`). +/// +/// ``Docker`` focuses on automation-friendly Docker workflows: Buildx, Compose, container and +/// image management, Docker Debug, Docker MCP, Docker Scout, and project initialization. It models +/// common global flags and high-value command options while preserving raw escape hatches with +/// ``argument(_:)``, ``arguments(_:)``, and ``option(_:_:)``. +/// +/// ```swift +/// let output = try await Docker(context: context) +/// .buildx("build") +/// .tag("owner/app:latest") +/// .file("Dockerfile") +/// .progress(.plain) +/// .positionalArgument(".") +/// .run() +/// ``` +public struct Docker: RunnableCommandFamily { + private let state: State + + /// The shell context used when running this command family. + public var context: ShellContext { state.config.context } + + /// Creates a Docker CLI command family bound to a shell context. + /// + /// The default invocation is `docker version`, which is read-only but may still contact the + /// configured daemon. Select daemon-free help output with ``argument(_:)`` if needed. + /// + /// - Parameter context: The shell context whose executor, search paths, environment, and + /// defaults will be used. Defaults to a freshly constructed ``ShellContext``. + public init(context: ShellContext = .init()) { + self.state = State(config: ToolConfiguration(context: context)) + } + + private init(state: State) { + self.state = state + } + + /// Returns a copy with updated shared tool configuration. + public func updatingConfiguration( + _ update: (ToolConfiguration) -> ToolConfiguration + ) -> Self { + copy(config: update(state.config)) + } + + /// Returns a copy that routes the built `docker` command's stdout to the given destination. + public func settingStdoutDestination(_ destination: OutputDestination) -> Self { + copy(stdoutDestination: destination) + } + + /// Returns a copy that routes the built `docker` command's stderr to the given destination. + public func settingStderrDestination(_ destination: OutputDestination) -> Self { + copy(stderrDestination: destination) + } + + /// Returns a copy that selects a top-level Docker command or command group. + public func subcommand(_ value: DockerSubcommand) -> Self { + copy(subcommand: .some(value.rawValue), nestedSubcommand: .some(nil)) + } + + /// Returns a copy that selects a raw top-level Docker command or command group. + public func subcommand(_ value: String) -> Self { + copy(subcommand: .some(value), nestedSubcommand: .some(nil)) + } + + /// Returns a copy that selects a top-level Docker command group and nested command. + public func subcommand(_ value: DockerSubcommand, _ nested: String) -> Self { + copy(subcommand: .some(value.rawValue), nestedSubcommand: .some(nested)) + } + + /// Returns a copy that selects a raw top-level Docker command group and nested command. + public func subcommand(_ value: String, _ nested: String) -> Self { + copy(subcommand: .some(value), nestedSubcommand: .some(nested)) + } + + /// Returns a copy that configures a `docker buildx` command. + public func buildx(_ nested: String? = nil) -> Self { commandGroup(.buildx, nested) } + + /// Returns a copy that configures a `docker compose` command. + public func compose(_ nested: String? = nil) -> Self { commandGroup(.compose, nested) } + + /// Returns a copy that configures a `docker container` command. + public func container(_ nested: String? = nil) -> Self { commandGroup(.container, nested) } + + /// Returns a copy that configures a `docker image` command. + public func image(_ nested: String? = nil) -> Self { commandGroup(.image, nested) } + + /// Returns a copy that configures `docker init`. + public func initialize() -> Self { commandGroup(.initialize, nil) } + + /// Returns a copy that configures a `docker debug` command for a target container or image. + public func debug(_ target: String? = nil) -> Self { + var result = commandGroup(.debug, nil) + if let target { result = result.positionalArgument(target) } + return result + } + + /// Returns a copy that configures a `docker mcp` command. + public func mcp(_ nested: String? = nil) -> Self { commandGroup(.mcp, nested) } + + /// Returns a copy that configures a `docker scout` command. + public func scout(_ nested: String? = nil) -> Self { commandGroup(.scout, nested) } + + /// Returns a copy that configures a `docker system` command. + public func system(_ nested: String? = nil) -> Self { commandGroup(.system, nested) } + + /// Returns a copy that configures a `docker version` command. + public func version() -> Self { commandGroup(.version, nil) } + + /// Returns a copy that passes `--config ` before the Docker subcommand. + public func configPath(_ path: String) -> Self { copy(configPath: path) } + + /// Returns a copy that passes `--context ` before the Docker subcommand. + public func context(_ name: String) -> Self { copy(contextName: name) } + + /// Returns a copy that passes `--host ` before the Docker subcommand. + public func host(_ value: String) -> Self { copy(hostValue: value) } + + /// Returns a copy that passes `--log-level ` before the Docker subcommand. + public func logLevel(_ value: String) -> Self { copy(logLevel: value) } + + /// Returns a copy that passes `--debug` before the Docker subcommand. + public func debugMode(_ enabled: Bool = true) -> Self { copy(debugEnabled: enabled) } + + /// Returns a copy that passes `--tls` before the Docker subcommand. + public func tls(_ enabled: Bool = true) -> Self { copy(tlsEnabled: enabled) } + + /// Returns a copy that passes `--tlsverify` before the Docker subcommand. + public func tlsVerify(_ enabled: Bool = true) -> Self { copy(tlsVerifyEnabled: enabled) } + + /// Returns a copy that passes `--platform `. + public func platform(_ value: String) -> Self { copy(platformValue: value) } + + /// Returns a copy that passes `--file `. + public func file(_ path: String) -> Self { copy(filePath: path) } + + /// Returns a copy that passes `--tag `. + public func tag(_ name: String) -> Self { copy(tags: state.tags + [name]) } + + /// Returns a copy that passes multiple `--tag` values. + public func tags(_ names: [String]) -> Self { copy(tags: state.tags + names) } + + /// Returns a copy that passes `--build-arg `. + public func buildArg(_ value: String) -> Self { copy(buildArgs: state.buildArgs + [value]) } + + /// Returns a copy that passes multiple `--build-arg` values. + public func buildArgs(_ values: [String]) -> Self { copy(buildArgs: state.buildArgs + values) } + + /// Returns a copy that passes `--progress `. + public func progress(_ value: DockerBuildProgress) -> Self { copy(progressMode: value) } + + /// Returns a copy that passes `--push`. + public func push(_ enabled: Bool = true) -> Self { copy(pushes: enabled) } + + /// Returns a copy that passes `--load`. + public func load(_ enabled: Bool = true) -> Self { copy(loads: enabled) } + + /// Returns a copy that passes `--pull`. + public func pull(_ enabled: Bool = true) -> Self { copy(pulls: enabled) } + + /// Returns a copy that passes `--name `. + public func name(_ value: String) -> Self { copy(nameValue: value) } + + /// Returns a copy that passes `--rm`. + public func removeWhenDone(_ enabled: Bool = true) -> Self { copy(removesWhenDone: enabled) } + + /// Returns a copy that passes `--detach`. + public func detach(_ enabled: Bool = true) -> Self { copy(detaches: enabled) } + + /// Returns a copy that passes `--interactive`. + public func interactive(_ enabled: Bool = true) -> Self { copy(isInteractive: enabled) } + + /// Returns a copy that passes `--tty`. + public func tty(_ enabled: Bool = true) -> Self { copy(allocatesTTY: enabled) } + + /// Returns a copy that passes `--command ` for commands like `docker debug`. + public func commandString(_ value: String) -> Self { copy(commandValue: value) } + + /// Returns a copy that passes `--shell ` for `docker debug`. + public func shell(_ value: String) -> Self { copy(shellValue: value) } + + /// Returns a copy that passes `--format