| name | sake-command-patterns |
| description | Use when writing or modifying Sake commands, CommandGroups, SakeApp files, or SakeApp/ directory code. Covers Command struct, dependencies, skipIf, context, storage, subprocess patterns, argument parsing. |
Sake Command Patterns
Reference for writing commands in the Sake framework.
Command Struct
Command is a Sendable struct with async/throws closures:
public struct Command: Sendable {
let description: String?
let dependencies: [Command]
let runDependenciesConcurrently: Bool
let skipIf: @Sendable (Context) async throws -> Bool
let run: @Sendable (Context) async throws -> Void
}
Visibility Rules
Only public static var of type Command or Sake.Command are exposed to CLI. Non-public or non-static commands work as internal dependencies but aren't callable from terminal. Typealiases don't work.
public static var build: Command { ... }
static var cleanIfNeeded: Command { ... }
CommandGroup & @CommandGroup Macro
@CommandGroup
struct BuildCommands {
public static var build: Command { ... }
}
Macro generates CommandGroup conformance with commands: [String: Command] dict. Register groups in main struct:
@main @CommandGroup
struct Commands: SakeApp {
static var configuration: SakeAppConfiguration {
SakeAppConfiguration(commandGroups: [BuildCommands.self, TestCommands.self])
}
public static var lint: Command { ... }
}
Dependencies
Command(dependencies: [clean, fetch], run: { _ in ... })
Command(dependencies: [a, b, c], runDependenciesConcurrently: true, run: { _ in ... })
Command(run: { context in
try await CommandRunner(command: otherCommand, context: context).run()
})
Conditional Execution (skipIf)
Command(
skipIf: { context in
let args = try TestArguments.parse(context.arguments)
return args.skipBuild
},
run: { _ in ... }
)
When skipIf returns true, the command AND its dependencies are skipped entirely.
Context
Properties: arguments: [String], environment: [String: String], appDirectory: String, runDirectory: String, storage: Storage, interruptionHandler: InterruptionHandler.
Context Extensions
extension Command.Context {
var projectRoot: String { "\(appDirectory)/.." }
}
Sharing Data via Storage
Thread-safe key-value store (NSRecursiveLock). Shared across command dependency chain:
context.storage["build-output"] = outputPath
let path = context.storage["build-output"] as? String
Context Mapping for Dependencies
dependencies: [
buildTests,
unitTests.mapArguments { $0 + ["--skip-build"] },
deploy.mapEnvironment { env in
var e = env; e["TARGET"] = "prod"; return e
},
]
Argument Parsing
Use ArgumentParser ParsableArguments inside commands:
Command(run: { context in
struct Args: ParsableArguments {
@Flag(name: .long) var clean: Bool = false
@Flag(name: .long) var skipBuild: Bool = false
}
let args = try Args.parse(context.arguments)
...
})
Subprocess Execution
Project uses helper functions wrapping swift-subprocess (see SakeApp/SubprocessPresets.swift):
try await runAndPrint("swift", "build", "--build-tests")
let result = try await run("mise", "which", "swiftformat")
if result.terminationStatus.isSuccess { ... }
Interruption Handling
Command(run: { context in
context.interruptionHandler.register {
print("Cleanup on Ctrl+C...")
}
})
Common Pattern: Ensure Tool Installed
static var ensureSwiftFormatInstalled: Command {
Command(
description: "Ensure swiftformat is installed",
skipIf: { _ in
try await run("mise", "which", "swiftformat").terminationStatus.isSuccess
},
run: { _ in
try await runAndPrint("mise", "install", "swiftformat")
},
)
}