112 lines
6.2 KiB
Swift
112 lines
6.2 KiB
Swift
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")
|
|
}
|
|
}
|