new features

This commit is contained in:
Mark Gregory
2026-11-08 17:28:29 +00:00
parent 8c34e965fd
commit ecfa74a35f
30 changed files with 1651 additions and 27 deletions
+111
View File
@@ -0,0 +1,111 @@
import Foundation
@main
struct AppleComposeCLI {
static func main() {
do { try execute() }
catch { fputs("accompose: \(error)\n", stderr); exit(1) }
}
static func execute() throws {
var arguments = Array(CommandLine.arguments.dropFirst())
let help = """
accompose — Apple Container Compose CLI
Usage: accompose [-f FILE] [-p PROJECT] COMMAND [OPTIONS]
Commands:
init Create a starter compose.yaml
doctor Check Apple Container runtime
config Validate Compose file and display configuration
up [-d|--detach] [--build] Start; attach to logs unless detached
down [--volumes] Stop and delete project containers
ps List containers
logs [SERVICE] Display service logs (snapshot only)
pull Pull service images
build Build services with Dockerfiles
start [SERVICE] Start containers
stop [SERVICE] Stop containers
restart [SERVICE] Restart containers
exec SERVICE CMD Execute a command in a container
clean SERVICE Delete one managed container
clean --all Delete all containers of current project
clean --dry-run Preview managed container deletion
prune --images Ask runtime to remove unused images
import PATH Validate a Compose file or project directory
Global options:
-f, --file FILE Compose file path
-p, --project-name N Override project name
-v, -vv, -vvv Increase diagnostics (independent of detach)
-h, --help Display help
Supported: basic images, Dockerfile builds, ports, directory binds,
named volumes, environment, dependencies, networks, commands.
Other service keys are rejected instead of silently ignored.
This is NOT a complete Docker Compose implementation.
"""
if arguments.contains("--help") || arguments.contains("-h") || arguments.isEmpty {
print(help)
return
}
var file: String?; var name: String?
var verbosity = 0
var index = 0
while index < arguments.count {
if ["-f", "--file", "-p", "--project-name"].contains(arguments[index]) {
guard index + 1 < arguments.count else { throw ComposeError.message("Missing value for \(arguments[index])") }
if arguments[index] == "-f" || arguments[index] == "--file" { file = arguments[index + 1] } else { name = arguments[index + 1] }
arguments.removeSubrange(index...index + 1)
} else if ["-v", "-vv", "-vvv"].contains(arguments[index]) {
verbosity = min(3, verbosity + arguments[index].count - 1)
arguments.remove(at: index)
} else {
// exec's command arguments belong to the invoked program.
if arguments[index] == "exec" { break }
index += 1
}
}
guard let command = arguments.first else { throw ComposeError.message("Missing command") }
if command == "doctor" && file == nil && (try? ComposeProject.discover()) == nil {
print("Apple Container Compose diagnostics")
guard let binary = Shell.executable("container") else { throw ComposeError.message("Apple container CLI not found. Install from https://github.com/apple/container/releases") }
print("CLI: \(binary)")
print(try Shell.checked("container", ["--version"]))
let result = try Shell.run("container", ["system", "status"])
print(result.output)
if result.status != 0 { throw ComposeError.message("Container runtime not ready. Try: container system start") }
return
}
if command == "import" {
guard arguments.count >= 2 else { throw ComposeError.message("Usage: accompose import PATH") }
let url = URL(fileURLWithPath: arguments[1]).standardizedFileURL
var isDir: ObjCBool = false
guard FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir) else { throw ComposeError.message("Path not found: \(url.path)") }
let input: URL
if isDir.boolValue {
let candidates = ["compose.yaml", "compose.yml", "docker-compose.yaml", "docker-compose.yml"]
guard let match = candidates.map({ url.appendingPathComponent($0) }).first(where: { FileManager.default.fileExists(atPath: $0.path) }) else { throw ComposeError.message("No Compose file found in \(url.path)") }
input = match
} else { input = url }
let project = try ComposeProject(file: input, overrideName: name)
print("Validated \(project.services.count) services from \(input.path). No files modified.")
return
}
if command == "init" {
let target = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent("compose.yaml")
guard !FileManager.default.fileExists(atPath: target.path) else { throw ComposeError.message("compose.yaml already exists") }
try "services:\n web:\n image: docker.io/library/nginx:alpine\n ports:\n - \"8080:80\"\n".write(to: target, atomically: true, encoding: .utf8)
print("Created \(target.path)"); return
}
let commandArgs = Array(arguments.dropFirst())
if command == "up" {
let supported: Set<String> = ["-d", "--detach", "--build"]
if let unknown = commandArgs.first(where: { !supported.contains($0) }) {
throw ComposeError.message("Unsupported up option: \(unknown)")
}
}
let project = try ComposeProject(file: ComposeProject.discover(file), overrideName: name)
let attached = command == "up" && !commandArgs.contains("-d") && !commandArgs.contains("--detach")
let output = try ComposeEngine(project).execute(command, args: commandArgs, attached: attached, verbosity: verbosity)
print(output, terminator: output.hasSuffix("\n") ? "" : "\n")
}
}