From a89ea72ab2ad3a192e02c3023c2047272f439d6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 May 2026 17:49:25 +0000 Subject: [PATCH 1/7] Extract compose file flag into shared option group --- .../Commands/ComposeDown.swift | 8 +++-- .../Commands/ComposeFileOptions.swift | 24 ++++++++++++++ .../Commands/ComposeUp.swift | 8 +++-- .../ComposeCommandParsingTests.swift | 33 +++++++++++++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 Sources/Container-Compose/Commands/ComposeFileOptions.swift create mode 100644 Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift diff --git a/Sources/Container-Compose/Commands/ComposeDown.swift b/Sources/Container-Compose/Commands/ComposeDown.swift index 6770d11d..9bbf1ec6 100644 --- a/Sources/Container-Compose/Commands/ComposeDown.swift +++ b/Sources/Container-Compose/Commands/ComposeDown.swift @@ -43,8 +43,12 @@ public struct ComposeDown: AsyncParsableCommand { private var cwd: String { process.cwd ?? FileManager.default.currentDirectoryPath } - @Option(name: [.customShort("f"), .customLong("file")], help: "The path to your Docker Compose file") - var composeFilename: String? + @OptionGroup + var composeFileOptions: ComposeFileOptions + + private var composeFilename: String? { + composeFileOptions.composeFilename + } private static let supportedComposeFilenames = [ "compose.yml", diff --git a/Sources/Container-Compose/Commands/ComposeFileOptions.swift b/Sources/Container-Compose/Commands/ComposeFileOptions.swift new file mode 100644 index 00000000..4384a9aa --- /dev/null +++ b/Sources/Container-Compose/Commands/ComposeFileOptions.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Morris Richman and the Container-Compose project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ArgumentParser + +public struct ComposeFileOptions: ParsableArguments, Sendable { + public init() {} + + @Option(name: [.customShort("f"), .customLong("file")], help: "The path to your Docker Compose file") + var composeFilename: String? +} diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index ebb6360f..3be42227 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -46,8 +46,12 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { help: "Detaches from container logs. Note: If you do NOT detach, killing this process will NOT kill the container. To kill the container, run container-compose down") var detach: Bool = false - @Option(name: [.customShort("f"), .customLong("file")], help: "The path to your Docker Compose file") - var composeFilename: String? + @OptionGroup + var composeFileOptions: ComposeFileOptions + + private var composeFilename: String? { + composeFileOptions.composeFilename + } private static let supportedComposeFilenames = [ "compose.yml", diff --git a/Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift b/Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift new file mode 100644 index 00000000..ad12defd --- /dev/null +++ b/Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Morris Richman and the Container-Compose project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Testing +@testable import ContainerComposeCore + +@Suite("Compose command parsing") +struct ComposeCommandParsingTests { + @Test("ComposeUp command accepts -f flag for compose file") + func composeUpCommandAcceptsFileFlag() throws { + let cmd = try ComposeUp.parse(["-f", "my-compose.yaml"]) + #expect(cmd.composeFileOptions.composeFilename == "my-compose.yaml") + } + + @Test("ComposeDown command accepts -f flag for compose file") + func composeDownCommandAcceptsFileFlag() throws { + let cmd = try ComposeDown.parse(["-f", "my-compose.yaml"]) + #expect(cmd.composeFileOptions.composeFilename == "my-compose.yaml") + } +} From 5608ece4c34f2aeb89dadd63ffb8679bb1cbcfc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 May 2026 17:52:46 +0000 Subject: [PATCH 2/7] Make compose file option group field public --- Sources/Container-Compose/Commands/ComposeFileOptions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Container-Compose/Commands/ComposeFileOptions.swift b/Sources/Container-Compose/Commands/ComposeFileOptions.swift index 4384a9aa..4b93c8a4 100644 --- a/Sources/Container-Compose/Commands/ComposeFileOptions.swift +++ b/Sources/Container-Compose/Commands/ComposeFileOptions.swift @@ -20,5 +20,5 @@ public struct ComposeFileOptions: ParsableArguments, Sendable { public init() {} @Option(name: [.customShort("f"), .customLong("file")], help: "The path to your Docker Compose file") - var composeFilename: String? + public var composeFilename: String? } From a2111b41a4979985fa13f4dd2c7e5d7938385729 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 May 2026 17:53:44 +0000 Subject: [PATCH 3/7] Address validation review nits for compose option group --- Sources/Container-Compose/Commands/ComposeDown.swift | 6 +----- .../Container-Compose/Commands/ComposeFileOptions.swift | 2 -- Sources/Container-Compose/Commands/ComposeUp.swift | 8 ++------ 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/Sources/Container-Compose/Commands/ComposeDown.swift b/Sources/Container-Compose/Commands/ComposeDown.swift index 9bbf1ec6..30240c65 100644 --- a/Sources/Container-Compose/Commands/ComposeDown.swift +++ b/Sources/Container-Compose/Commands/ComposeDown.swift @@ -46,10 +46,6 @@ public struct ComposeDown: AsyncParsableCommand { @OptionGroup var composeFileOptions: ComposeFileOptions - private var composeFilename: String? { - composeFileOptions.composeFilename - } - private static let supportedComposeFilenames = [ "compose.yml", "compose.yaml", @@ -62,7 +58,7 @@ public struct ComposeDown: AsyncParsableCommand { } private var composePath: String { - if let composeFilename { + if let composeFilename = composeFileOptions.composeFilename { return resolvedPath(for: composeFilename, relativeTo: cwdURL) } diff --git a/Sources/Container-Compose/Commands/ComposeFileOptions.swift b/Sources/Container-Compose/Commands/ComposeFileOptions.swift index 4b93c8a4..c45ce290 100644 --- a/Sources/Container-Compose/Commands/ComposeFileOptions.swift +++ b/Sources/Container-Compose/Commands/ComposeFileOptions.swift @@ -17,8 +17,6 @@ import ArgumentParser public struct ComposeFileOptions: ParsableArguments, Sendable { - public init() {} - @Option(name: [.customShort("f"), .customLong("file")], help: "The path to your Docker Compose file") public var composeFilename: String? } diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index 3be42227..b63ce227 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -49,10 +49,6 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { @OptionGroup var composeFileOptions: ComposeFileOptions - private var composeFilename: String? { - composeFileOptions.composeFilename - } - private static let supportedComposeFilenames = [ "compose.yml", "compose.yaml", @@ -65,7 +61,7 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { } private var composePath: String { - if let composeFilename { + if let composeFilename = composeFileOptions.composeFilename { return resolvedPath(for: composeFilename, relativeTo: cwdURL) } @@ -528,7 +524,7 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { runCommandArgs.append(networkToConnect) } print( - "Info: Service '\(serviceName)' is configured to connect to networks: \(serviceNetworks.joined(separator: ", ")) ascertained from networks attribute in \(composeFilename)." + "Info: Service '\(serviceName)' is configured to connect to networks: \(serviceNetworks.joined(separator: ", ")) ascertained from networks attribute in \(composeFileOptions.composeFilename)." ) print( "Note: This tool assumes custom networks are defined at the top-level 'networks' key or are pre-existing. This tool does not create implicit networks for services if not explicitly defined at the top-level." From 8cabcdaf42062e1262c4957fa393ede965072a12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 May 2026 17:54:18 +0000 Subject: [PATCH 4/7] Use resolved compose path in network log message --- Sources/Container-Compose/Commands/ComposeUp.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index b63ce227..3a874f08 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -524,7 +524,7 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { runCommandArgs.append(networkToConnect) } print( - "Info: Service '\(serviceName)' is configured to connect to networks: \(serviceNetworks.joined(separator: ", ")) ascertained from networks attribute in \(composeFileOptions.composeFilename)." + "Info: Service '\(serviceName)' is configured to connect to networks: \(serviceNetworks.joined(separator: ", ")) ascertained from networks attribute in \(composePath)." ) print( "Note: This tool assumes custom networks are defined at the top-level 'networks' key or are pre-existing. This tool does not create implicit networks for services if not explicitly defined at the top-level." From 36afd13ef75e6ddb7030b69822da0f6c20aba9bf Mon Sep 17 00:00:00 2001 From: Morris Richman <81453549+Mcrich23@users.noreply.github.com> Date: Sat, 30 May 2026 12:02:40 -0700 Subject: [PATCH 5/7] move build command to option group --- Sources/Container-Compose/Commands/ComposeBuild.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Container-Compose/Commands/ComposeBuild.swift b/Sources/Container-Compose/Commands/ComposeBuild.swift index 8a2ee1b2..5abde194 100644 --- a/Sources/Container-Compose/Commands/ComposeBuild.swift +++ b/Sources/Container-Compose/Commands/ComposeBuild.swift @@ -39,8 +39,8 @@ public struct ComposeBuild: AsyncParsableCommand, @unchecked Sendable { @Argument(help: "Services to build (builds all if omitted)") var services: [String] = [] - @Option(name: [.customShort("f"), .customLong("file")], help: "The path to your Docker Compose file") - var composeFilename: String? + @OptionGroup + var composeFileOptions: ComposeFileOptions @Flag(name: .long, help: "Do not use cache when building") var noCache: Bool = false @@ -63,7 +63,7 @@ public struct ComposeBuild: AsyncParsableCommand, @unchecked Sendable { ] private var composePath: String { - if let composeFilename { + if let composeFilename = composeFileOptions.composeFilename { return resolvedPath(for: composeFilename, relativeTo: cwdURL) } for filename in Self.supportedComposeFilenames { From 2df826e4297ce75acb22421d95104d7a2b9fd637 Mon Sep 17 00:00:00 2001 From: Morris Richman <81453549+Mcrich23@users.noreply.github.com> Date: Sat, 30 May 2026 12:02:42 -0700 Subject: [PATCH 6/7] compile fixes and validation --- Sources/Container-Compose/Application.swift | 3 +++ Sources/Container-Compose/Commands/ComposeFileOptions.swift | 2 ++ 2 files changed, 5 insertions(+) diff --git a/Sources/Container-Compose/Application.swift b/Sources/Container-Compose/Application.swift index 2266e1aa..ad56c566 100644 --- a/Sources/Container-Compose/Application.swift +++ b/Sources/Container-Compose/Application.swift @@ -33,6 +33,9 @@ public struct Main: AsyncParsableCommand { ComposeBuild.self, Version.self ]) + + @OptionGroup + var composeFileOptions: ComposeFileOptions public init() {} } diff --git a/Sources/Container-Compose/Commands/ComposeFileOptions.swift b/Sources/Container-Compose/Commands/ComposeFileOptions.swift index c45ce290..e27782af 100644 --- a/Sources/Container-Compose/Commands/ComposeFileOptions.swift +++ b/Sources/Container-Compose/Commands/ComposeFileOptions.swift @@ -17,6 +17,8 @@ import ArgumentParser public struct ComposeFileOptions: ParsableArguments, Sendable { + public init() {} + @Option(name: [.customShort("f"), .customLong("file")], help: "The path to your Docker Compose file") public var composeFilename: String? } From 5d4b3b8d2b779e70a08a9e66e94aca65ee5eec5a Mon Sep 17 00:00:00 2001 From: Morris Richman <81453549+Mcrich23@users.noreply.github.com> Date: Sat, 30 May 2026 12:08:13 -0700 Subject: [PATCH 7/7] testing fixes --- .../ComposeBuildParsingTests.swift | 2 +- .../ComposeCommandParsingTests.swift | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Tests/Container-Compose-StaticTests/ComposeBuildParsingTests.swift b/Tests/Container-Compose-StaticTests/ComposeBuildParsingTests.swift index 208d6a9f..1355fde9 100644 --- a/Tests/Container-Compose-StaticTests/ComposeBuildParsingTests.swift +++ b/Tests/Container-Compose-StaticTests/ComposeBuildParsingTests.swift @@ -163,6 +163,6 @@ struct ComposeBuildParsingTests { @Test("ComposeBuild command accepts -f flag for compose file") func composeBuildCommandAcceptsFileFlag() throws { let cmd = try ComposeBuild.parse(["-f", "my-compose.yaml"]) - #expect(cmd.composeFilename == "my-compose.yaml") + #expect(cmd.composeFileOptions.composeFilename == "my-compose.yaml") } } diff --git a/Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift b/Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift index ad12defd..121e1b0d 100644 --- a/Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift +++ b/Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift @@ -19,15 +19,9 @@ import Testing @Suite("Compose command parsing") struct ComposeCommandParsingTests { - @Test("ComposeUp command accepts -f flag for compose file") + @Test("Main+ComposeUp command accepts -f flag for compose file from root") func composeUpCommandAcceptsFileFlag() throws { - let cmd = try ComposeUp.parse(["-f", "my-compose.yaml"]) - #expect(cmd.composeFileOptions.composeFilename == "my-compose.yaml") - } - - @Test("ComposeDown command accepts -f flag for compose file") - func composeDownCommandAcceptsFileFlag() throws { - let cmd = try ComposeDown.parse(["-f", "my-compose.yaml"]) + let cmd = try Main.parseAsRoot(["-f", "my-compose.yaml", "up"]) as! ComposeUp #expect(cmd.composeFileOptions.composeFilename == "my-compose.yaml") } }