{
  "format" : 1,
  "manifest" : {
    "accent" : "cyan",
    "capabilities" : [
      {
        "run" : {
          "commands" : [
            "docker"
          ]
        }
      }
    ],
    "homepage" : "https://github.com/navbytes/stackbar-plugins/tree/main/Developer/Docker",
    "id" : "io.navbytes.stackbar.plugins.docker",
    "minimumStackbar" : "0.17.0",
    "name" : "Docker",
    "summary" : "Your containers, with start, stop, restart and logs a click away.",
    "surfaces" : [
      "menuBar",
      "panel",
      "widget",
      "settings"
    ],
    "symbol" : "shippingbox.fill",
    "triggers" : [
      {
        "launch" : {

        }
      },
      {
        "every" : {
          "seconds" : 15
        }
      },
      {
        "wake" : {

        }
      }
    ],
    "version" : "1.4.0"
  },
  "publisher" : {
    "name" : "navbytes",
    "publicKey" : "4dpLtXEpFXdi+6cviY/KdYuUulJPbEmCxxIK0EEN2/w="
  },
  "signature" : "IkEOUYo2DM/yz6q6mS8jr3W7d/KDRtYUqE3/pXl4R3b3tg72Z7gHbx6vEgNwUZ4BLeaoi9AIWoy8IP426xf+Bw==",
  "source" : "import StackbarKit\nimport Foundation\n\n/// Your Docker containers: what is running, with start, stop, restart, logs\n/// and cleanup a click away.\n///\n/// It runs the `docker` CLI through Stackbar (Docker Desktop, OrbStack,\n/// Colima and Rancher Desktop all install one), every 15 seconds while a\n/// container runs and every minute otherwise. Nothing runs in a shell.\n/// When a container crashes, turns unhealthy or keeps restarting, it says\n/// so with a notification and a red menu bar.\n@main\nfinal class Docker: StackbarPlugin {\n    static let manifest = Manifest(\n        id: \"io.navbytes.stackbar.plugins.docker\",\n        name: \"Docker\",\n        version: \"1.4.0\",\n        summary: \"Your containers, with start, stop, restart and logs a click away.\",\n        surfaces: [.menuBar, .panel, .widget, .settings],\n        triggers: [.launch, .every(.seconds(15)), .wake],\n        capabilities: [.run(commands: [\"docker\"])],\n        minimumStackbar: \"0.17.0\",\n        homepage: \"https://github.com/navbytes/stackbar-plugins/tree/main/Developer/Docker\",\n        accent: .cyan,\n        symbol: \"shippingbox.fill\"\n    )\n\n    // MARK: Previews\n\n    static let previews: [PreviewScene<Docker>] = [\n        PreviewScene(\"hero\", \"Every container, grouped by Compose project\") { plugin in\n            plugin.containers = samples\n            plugin.loaded = true\n        },\n        PreviewScene(\"trouble\", \"Catch the container that keeps crashing\") { plugin in\n            plugin.containers = samples\n            plugin.crashedIDs = [\"c6\"]\n            plugin.loaded = true\n        },\n        PreviewScene(\"widget\", \"Your stack's health on the desktop\", surface: .widget) { plugin in\n            plugin.containers = samples\n            plugin.loaded = true\n        },\n    ]\n\n    static let samples: [Container] = {\n        func container(_ id: String, _ name: String, _ image: String, _ state: String, _ status: String, _ ports: String?, project: String?) -> Container {\n            Container(id: id, names: name, image: image, state: state, status: status, ports: ports,\n                      labels: project.map { \"com.docker.compose.project=\\($0),com.docker.compose.service=\\(name)\" })\n        }\n        return [\n            container(\"c1\", \"shop-web\", \"node:22-alpine\", \"running\", \"Up 3 hours\", \"0.0.0.0:3000->3000/tcp\", project: \"shop\"),\n            container(\"c2\", \"shop-api\", \"shop/api:dev\", \"running\", \"Up 3 hours (healthy)\", \"0.0.0.0:8080->8080/tcp\", project: \"shop\"),\n            container(\"c3\", \"shop-db\", \"postgres:17\", \"running\", \"Up 3 hours (healthy)\", \"0.0.0.0:5432->5432/tcp\", project: \"shop\"),\n            container(\"c4\", \"shop-cache\", \"redis:7\", \"running\", \"Up 3 hours\", \"6379/tcp\", project: \"shop\"),\n            container(\"c5\", \"blog-site\", \"ghost:5\", \"running\", \"Up 25 minutes\", \"0.0.0.0:2368->2368/tcp\", project: \"blog\"),\n            container(\"c6\", \"blog-worker\", \"blog/worker:latest\", \"exited\", \"Exited (1) 4 minutes ago\", nil, project: \"blog\"),\n            container(\"c7\", \"minio\", \"minio/minio:latest\", \"running\", \"Up 2 days\", \"0.0.0.0:9000-9001->9000-9001/tcp\", project: nil),\n            container(\"c8\", \"mailpit\", \"axllent/mailpit\", \"exited\", \"Exited (0) 2 hours ago\", nil, project: nil),\n        ]\n    }()\n\n    /// One line of `docker ps --format '{{json .}}'`.\n    struct Container: Decodable, Equatable, Identifiable {\n        let id: String\n        let names: String\n        let image: String\n        let state: String\n        let status: String\n        let ports: String?\n        let labels: String?\n\n        enum CodingKeys: String, CodingKey {\n            case id = \"ID\", names = \"Names\", image = \"Image\", state = \"State\", status = \"Status\", ports = \"Ports\", labels = \"Labels\"\n        }\n\n        var name: String { names.split(separator: \",\").first.map(String.init) ?? names }\n        var isRunning: Bool { state == \"running\" || state == \"restarting\" }\n        var isUnhealthy: Bool { status.contains(\"(unhealthy)\") }\n        /// The code in `Exited (1) 5 minutes ago`.\n        var exitCode: Int? {\n            guard state == \"exited\", let open = status.firstIndex(of: \"(\"), let close = status.firstIndex(of: \")\"), open < close else { return nil }\n            return Int(status[status.index(after: open)..<close])\n        }\n        /// Exited on its own with an error. 0 is a clean exit, and 130, 137\n        /// and 143 are what `docker stop` and Ctrl-C leave.\n        var crashed: Bool { exitCode.map { ![0, 130, 137, 143].contains($0) } ?? false }\n        /// `nginx:latest`, without Docker Hub's `docker.io/library/`.\n        var shortImage: String {\n            image.hasPrefix(\"docker.io/library/\") ? String(image.dropFirst(\"docker.io/library/\".count)) : image\n        }\n        /// The compose project, from its label.\n        var project: String? {\n            labels?.split(separator: \",\").first { $0.hasPrefix(\"com.docker.compose.project=\") }\n                .map { String($0.dropFirst(\"com.docker.compose.project=\".count)) }\n        }\n        /// `0.0.0.0:8080->80/tcp` becomes `http://localhost:8080`.\n        var localURLs: [String] {\n            (ports ?? \"\").split(separator: \",\").compactMap { mapping -> String? in\n                let part = mapping.trimmingCharacters(in: .whitespaces)\n                guard let arrow = part.range(of: \"->\"), part[arrow.upperBound...].hasSuffix(\"/tcp\") else { return nil }\n                guard let port = part[..<arrow.lowerBound].split(separator: \":\").last, Int(port) != nil else { return nil }\n                return \"http://localhost:\\(port)\"\n            }.reduce(into: [String]()) { if !$0.contains($1) { $0.append($1) } }\n        }\n    }\n\n    var containers: [Container] = []\n    var loaded = false\n    var problem: String?\n    var busy: Set<String> = []\n    /// What is running now, like \"Restarting api…\".\n    var busyText: String?\n    var logs: [String: String] = [:]\n    var lastSlowCheck = Date.distantPast\n\n    @Stored(\"showStopped\") var showStopped = true\n    @Stored(\"groupByProject\") var groupByProject = true\n    @Stored(\"notifyTrouble\") var notifyTrouble = true\n    /// Containers that crashed since they last ran, by id, so the menu bar\n    /// stays red until they are started again or removed.\n    var crashedIDs: Set<String> = []\n    /// Containers this plugin is stopping, so their exit isn't a crash.\n    var stopping: Set<String> = []\n\n    var running: [Container] { containers.filter(\\.isRunning) }\n    var shown: [Container] { showStopped ? containers : running }\n\n    func refresh() async throws {\n        // Every 15 s while something runs; once a minute otherwise.\n        if running.isEmpty, loaded, problem == nil, Date().timeIntervalSince(lastSlowCheck) < 55 { return }\n        await load()\n    }\n\n    func load() async {\n        lastSlowCheck = Date()\n        do {\n            let result = try await Stackbar.run(\"docker\", [\"ps\", \"--all\", \"--no-trunc\", \"--format\", \"{{json .}}\"], timeout: .seconds(20))\n            guard result.succeeded else {\n                // The last list stays on screen under a banner.\n                problem = Self.explain(result)\n                loaded = true\n                return\n            }\n            let decoder = JSONDecoder()\n            let fresh = result.text.split(separator: \"\\n\").compactMap { try? decoder.decode(Container.self, from: Data($0.utf8)) }\n                .sorted { ($0.isRunning ? 0 : 1, $0.name) < ($1.isRunning ? 0 : 1, $1.name) }\n            if loaded, problem == nil { noticeTrouble(from: containers, to: fresh) }\n            containers = fresh\n            crashedIDs = crashedIDs.filter { id in fresh.contains { $0.id == id && !$0.isRunning } }\n            problem = nil\n        } catch {\n            problem = \"\\(error)\".contains(\"not installed\")\n                ? \"The docker command isn't installed. Install Docker Desktop, OrbStack or Colima, then refresh.\"\n                : \"\\(error)\"\n        }\n        loaded = true\n    }\n\n    /// Compares two lists and speaks up about what went wrong in between:\n    /// a crash, a container turning unhealthy, or one stuck restarting.\n    func noticeTrouble(from old: [Container], to new: [Container]) {\n        let before = Dictionary(old.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first })\n        var lines: [String] = []\n        for container in new {\n            guard let was = before[container.id] else { continue }\n            if was.isRunning, container.crashed, !stopping.contains(container.id) {\n                crashedIDs.insert(container.id)\n                lines.append(\"\\(container.name) crashed (exit \\(container.exitCode ?? 0))\")\n            } else if container.isUnhealthy, !was.isUnhealthy {\n                lines.append(\"\\(container.name) is unhealthy\")\n            } else if container.state == \"restarting\", was.state != \"restarting\" {\n                lines.append(\"\\(container.name) keeps restarting\")\n            }\n        }\n        guard notifyTrouble, !lines.isEmpty else { return }\n        Stackbar.notify(lines.count == 1 ? lines[0] : \"\\(lines.count) containers need a look\", body: lines.count == 1 ? \"Open Docker in the menu bar for its logs.\" : lines.joined(separator: \"\\n\"))\n    }\n\n    /// Containers that need a look: unhealthy, restarting, or crashed.\n    var troubled: [Container] {\n        containers.filter { $0.isUnhealthy || $0.state == \"restarting\" || crashedIDs.contains($0.id) }\n    }\n\n    static func explain(_ result: RunResult) -> String {\n        let text = result.errorText\n        if text.contains(\"Cannot connect to the Docker daemon\") || text.contains(\"docker daemon running\") || text.contains(\"connect: no such file\") {\n            return \"Docker isn't running. Start Docker Desktop, OrbStack or Colima.\"\n        }\n        if result.timedOut { return \"docker took too long to answer\" }\n        return \"docker failed (\\(result.status)): \" + (text.split(separator: \"\\n\").first.map(String.init) ?? \"\")\n    }\n\n    func act(_ verb: String, _ container: Container) async throws {\n        busy.insert(container.id)\n        busyText = \"\\([\"stop\": \"Stopping\", \"start\": \"Starting\", \"restart\": \"Restarting\", \"rm\": \"Removing\"][verb] ?? verb) \\(container.name)…\"\n        if verb == \"stop\" || verb == \"restart\" { stopping.insert(container.id) }\n        defer {\n            busy.remove(container.id)\n            busyText = nil\n            stopping.remove(container.id)\n        }\n        let result = try await Stackbar.run(\"docker\", [verb, container.id], timeout: .seconds(90))\n        guard result.succeeded else { throw HostError(\"\\(verb) \\(container.name): \" + Self.explain(result)) }\n        await load()\n    }\n\n    func loadLogs(_ id: String, lines: Int = 200) async {\n        do {\n            let result = try await Stackbar.run(\"docker\", [\"logs\", \"--tail\", \"\\(lines)\", \"--timestamps\", id], timeout: .seconds(20))\n            // docker logs writes the container's stderr to stderr.\n            let text = [result.text, result.errorText].filter { !$0.isEmpty }.joined(separator: \"\\n\")\n            logs[id] = result.succeeded ? (text.isEmpty ? \"(no output)\" : text) : Self.explain(result)\n        } catch {\n            logs[id] = \"\\(error)\"\n        }\n    }\n\n    // MARK: Surfaces\n\n    let theme = Theme.cyan\n\n    var projects: Int { Set(containers.compactMap(\\.project)).count }\n\n    var menuBar: ViewContent {\n        if problem != nil, loaded {\n            Label(\"\", symbol: \"shippingbox\").role(.warning)\n        } else if !troubled.isEmpty {\n            Label(\"\\(running.count) · \\(troubled.count)!\", symbol: \"shippingbox.fill\").role(.critical)\n        } else if running.isEmpty {\n            Label(\"\", symbol: \"shippingbox\").role(.secondary)\n        } else {\n            Label(\"\\(running.count)\", symbol: \"shippingbox.fill\")\n        }\n    }\n\n    var panel: ViewContent {\n        VStack(spacing: Space.m) {\n            if !loaded {\n                PanelHeader(\"Containers\", subtitle: \"Checking…\", symbol: \"shippingbox.fill\", theme: theme)\n            } else if let problem, containers.isEmpty {\n                let missing = problem.contains(\"isn't installed\")\n                EmptyState(missing ? \"Docker isn't installed\" : \"Docker isn't running\", symbol: missing ? \"shippingbox\" : \"exclamationmark.triangle.fill\",\n                           message: problem.hasPrefix(\"Docker isn't running. \") ? String(problem.dropFirst(\"Docker isn't running. \".count)) : problem, theme: missing ? theme : .critical) {\n                    ActionButton(\"Try Again\", symbol: \"arrow.clockwise\", theme: theme) { await self.load() }\n                }\n                if missing { Link(\"Get OrbStack\", url: \"https://orbstack.dev\") }\n            } else {\n                PanelHeader(running.isEmpty ? \"Nothing running\" : \"\\(running.count) running\", subtitle: summary, symbol: \"shippingbox.fill\", theme: theme) {\n                    Picker(\"Show\", options: [\"Running\", \"All\"], selection: showStopped ? \"All\" : \"Running\", style: .segmented) { self.showStopped = $0 == \"All\" }\n                        .frame(width: 124)\n                }\n                if !containers.isEmpty {\n                    let unhealthy = containers.filter(\\.isUnhealthy).count\n                    HeroCard(theme: theme, look: .glow) {\n                        HStack(spacing: Space.l) {\n                            HeroStat(\"Running\", value: \"\\(running.count)\", theme: theme, live: !running.isEmpty)\n                            HeroStat(\"Stopped\", value: \"\\(containers.count - running.count)\", theme: .neutral, symbol: \"stop.fill\")\n                            if unhealthy > 0 {\n                                HeroStat(\"Unhealthy\", value: \"\\(unhealthy)\", theme: .critical, symbol: \"cross.case.fill\")\n                            } else if projects > 0 {\n                                HeroStat(\"Projects\", value: \"\\(projects)\", theme: theme, symbol: \"square.stack.3d.up.fill\")\n                            }\n                            Spacer()\n                        }\n                    }\n                }\n                if let problem { Banner(problem) }\n                if !troubled.isEmpty {\n                    Banner(troubled.map { self.trouble($0) }.joined(separator: \" · \"), symbol: \"exclamationmark.triangle.fill\", theme: .critical)\n                }\n                if let busyText { Banner(busyText, symbol: \"arrow.triangle.2.circlepath\", theme: .info) }\n                if containers.isEmpty {\n                    EmptyState(\"No containers yet\", symbol: \"shippingbox\", message: \"Containers you create or start with Compose show up here.\", theme: theme)\n                } else if shown.isEmpty {\n                    EmptyState(\"Nothing running\", symbol: \"moon.zzz\", message: \"\\(containers.count) stopped.\", theme: theme) {\n                        ActionButton(\"Show All\", theme: theme) { self.showStopped = true }\n                    }\n                } else if groupByProject {\n                    let groups = Dictionary(grouping: shown) { $0.project ?? \"\" }\n                    VStack(spacing: Space.s) {\n                        ForEach(groups.keys.sorted { ($0.isEmpty ? 1 : 0, $0) < ($1.isEmpty ? 1 : 0, $1) }) { project in\n                            let members = groups[project] ?? []\n                            let up = containers.filter { ($0.project ?? \"\") == project && $0.isRunning }.count\n                            let all = containers.filter { ($0.project ?? \"\") == project }.count\n                            Section(project.isEmpty ? (groups.count > 1 ? \"Other containers\" : \"Containers\") : \"\\(project) · \\(up)/\\(all) running\") {\n                                ForEach(members) { container in self.row(container) }\n                            }\n                        }\n                    }\n                } else {\n                    List(shown, searchable: shown.count > 8) { container in self.row(container) }\n                }\n            }\n        }\n    }\n\n    func trouble(_ container: Container) -> String {\n        if crashedIDs.contains(container.id) { return \"\\(container.name) crashed (exit \\(container.exitCode ?? 0))\" }\n        if container.state == \"restarting\" { return \"\\(container.name) keeps restarting\" }\n        return \"\\(container.name) is unhealthy\"\n    }\n\n    var summary: String {\n        let count = \"\\(containers.count) container\\(containers.count == 1 ? \"\" : \"s\")\"\n        return projects > 0 ? \"\\(count) · \\(projects) project\\(projects == 1 ? \"\" : \"s\")\" : count\n    }\n\n    var widget: ViewContent {\n        if problem != nil, containers.isEmpty {\n            VStack(alignment: .center, spacing: Space.xs) {\n                IconTile(\"shippingbox\", theme: .neutral, size: 32)\n                Text(\"Docker isn't running\").role(.secondary).typography(.caption)\n            }\n            .frame(fillWidth: true, fillHeight: true)\n        } else {\n            VStack(spacing: Space.s) {\n                HStack(spacing: Space.s) {\n                    StatTile(\"Running\", value: \"\\(running.count)\", symbol: \"play.fill\", theme: theme, emphasized: !running.isEmpty)\n                    StatTile(\"Stopped\", value: \"\\(containers.count - running.count)\", symbol: \"stop.fill\")\n                    let unhealthy = containers.filter(\\.isUnhealthy).count\n                    StatTile(\"Unhealthy\", value: \"\\(unhealthy)\", symbol: \"cross.case.fill\", theme: unhealthy > 0 ? .critical : .neutral, emphasized: unhealthy > 0)\n                }\n                ForEach(running.prefix(2)) { container in\n                    HStack(spacing: Space.xs + 2) {\n                        StatusDot(container.isUnhealthy ? .critical : .success, size: 6)\n                        Text(container.name).typography(.caption)\n                        Spacer()\n                        Text(container.status).role(.secondary).typography(.caption)\n                    }\n                }\n            }\n        }\n    }\n\n    func row(_ container: Container) -> Row {\n        var row = Row(container.name, subtitle: \"\\(container.shortImage) · \\(container.status.replacingOccurrences(of: \" (unhealthy)\", with: \"\").replacingOccurrences(of: \" (healthy)\", with: \"\"))\", symbol: container.isRunning ? \"circle.fill\" : \"circle\", key: container.id)\n        if busy.contains(container.id) {\n            row = row.badge(\"…\")\n        } else if container.isUnhealthy {\n            row = row.badge(\"unhealthy\", role: .critical)\n        } else if container.state == \"restarting\" {\n            row = row.badge(\"restarting\", role: .warning)\n        } else if crashedIDs.contains(container.id) {\n            row = row.badge(\"crashed\", role: .critical)\n        } else if container.state == \"paused\" {\n            row = row.badge(\"paused\")\n        }\n        if container.isRunning, let url = container.localURLs.first {\n            row = row.badge(String(url.dropFirst(\"http://\".count)))\n        }\n        if let url = container.localURLs.first {\n            row = row.action(\"Open \\(url.dropFirst(\"http://\".count))\") { Stackbar.open(url) }\n        }\n        if container.isRunning {\n            row = row\n                .action(\"Stop\", shortcut: \"cmd+.\") { try await self.act(\"stop\", container) }\n                .action(\"Restart\", shortcut: \"cmd+r\") { try await self.act(\"restart\", container) }\n        } else {\n            row = row.action(\"Start\", shortcut: \"cmd+return\") { try await self.act(\"start\", container) }\n        }\n        return row\n            .navigate(\"Logs\", to: \"logs\", arguments: [\"container\": container.id], shortcut: \"cmd+l\")\n            .action(\"Copy ID\", shortcut: \"cmd+shift+c\") { Stackbar.copy(String(container.id.prefix(12))) }\n            .action(\"Copy Exec Command\") { Stackbar.copy(\"docker exec -it \\(container.name) sh\") }\n            .action(\"Remove\", shortcut: \"ctrl+x\", role: .critical) {\n                guard !container.isRunning else { throw HostError(\"Stop \\(container.name) before removing it\") }\n                try await self.act(\"rm\", container)\n            }\n    }\n\n    var settings: ViewContent {\n        Section(\"Show\") {\n            Toggle(\"Group by Compose project\", isOn: groupByProject) { self.groupByProject = $0 }\n        }\n        Section(\"Notify\") {\n            Toggle(\"When a container crashes, turns unhealthy or keeps restarting\", isOn: notifyTrouble) { self.notifyTrouble = $0 }\n        }\n        Section(\"Cleanup\") {\n            Button(\"Remove Stopped Containers\", symbol: \"trash\", role: .critical) { _ = try await self.prune() }\n            Text(\"Runs docker container prune. Images and volumes are left alone.\").style(.caption)\n        }\n    }\n\n    func prune() async throws -> String {\n        let result = try await Stackbar.run(\"docker\", [\"container\", \"prune\", \"--force\"], timeout: .seconds(120))\n        guard result.succeeded else { throw HostError(Self.explain(result)) }\n        await load()\n        return result.text.split(separator: \"\\n\").last.map(String.init) ?? \"Removed stopped containers\"\n    }\n\n    // MARK: Commands\n\n    func find(_ text: String?) -> Container? {\n        guard let text, !text.isEmpty else { return nil }\n        return containers.first { $0.id.hasPrefix(text) || $0.name == text } ?? containers.first { $0.name.localizedCaseInsensitiveContains(text) }\n    }\n\n    var commands: [Command] {\n        Command(\"containers\", title: \"Docker Containers\", symbol: \"shippingbox\", keywords: [\"docker\", \"container\", \"compose\"])\n            .view(search: { _ in await self.load() }) { _ in\n                if let problem = self.problem {\n                    ContentUnavailableView(\"Docker isn't available\", symbol: \"shippingbox\", message: problem)\n                } else {\n                    List(self.shown, searchable: true) { container in self.row(container) }\n                }\n            }\n        Command(\"logs\", title: \"Docker Logs\", symbol: \"text.alignleft\", keywords: [\"docker\"])\n            .argument(\"container\", placeholder: \"Container\")\n            .view(search: { invocation in\n                if self.containers.isEmpty { await self.load() }\n                if let container = self.find(invocation[\"container\"]) { await self.loadLogs(container.id) }\n            }) { invocation in\n                if let container = self.find(invocation[\"container\"]) {\n                    HStack {\n                        Button(\"Refresh\", symbol: \"arrow.clockwise\") { await self.loadLogs(container.id) }\n                        Button(\"Copy\", symbol: \"doc.on.doc\") { Stackbar.copy(self.logs[container.id] ?? \"\") }\n                    }\n                    Section(\"\\(container.name) · last 200 lines\") {\n                        Text(String((self.logs[container.id] ?? \"Loading…\").suffix(20_000))).style(.monospaced)\n                    }\n                } else {\n                    ContentUnavailableView(\"No container \\(invocation[\"container\"] ?? \"\")\", symbol: \"shippingbox\")\n                }\n            }\n            .stackbarOnly()\n        Command(\"prune\", title: \"Remove Stopped Docker Containers\", symbol: \"trash\", keywords: [\"docker\", \"prune\", \"clean\"]) { _ in\n            .message(try await self.prune())\n        }\n        .stackbarOnly()\n    }\n\n    var tools: [AgentTool] {\n        AgentTool(\"containers\", description: \"Docker containers with name, image, state and status\") { _ in\n            if self.containers.isEmpty { await self.load() }\n            if let problem = self.problem { return problem }\n            return self.containers.map { \"\\($0.name)\\t\\($0.image)\\t\\($0.state)\\t\\($0.status)\" }.joined(separator: \"\\n\")\n        }\n    }\n}\n",
  "sourceDigest" : "200b64f72a830decd7689db870c5b86fea69bee2f58ff6fd362f3c07e448b542"
}