Skip to content

Commit 8bccc0f

Browse files
leogdionclaude
andcommitted
Migrate from ArgumentParser to Swift Configuration
Replace ArgumentParser with Swift Configuration for unified CLI and environment variable configuration management. This migration follows the CelestraCloud pattern and provides better fault tolerance and automatic type conversion. **New Configuration Infrastructure:** - ConfigurationLoader: Actor-based configuration management with CommandLineArgumentsProvider and EnvironmentVariablesProvider - ConfigurationKeys: Centralized key definitions with dual-key fallback (CLI arguments and environment variables) - BushelConfiguration: Configuration structs with validation pattern transforming optional to non-optional fields **Command Updates:** - SyncCommand: Removed 13 @Option/@Flag properties, ~110 lines reduced - ExportCommand: Removed 8 @Option/@Flag properties, ~110 lines reduced - StatusCommand: Removed 5 @Option/@Flag properties, ~120 lines reduced - ListCommand: Removed 6 @Option/@Flag properties, ~50 lines reduced - ClearCommand: Removed 5 @Option/@Flag properties, ~60 lines reduced **CLI Updates:** - BushelCloudCLI: Replaced AsyncParsableCommand with manual subcommand dispatch using simple switch statement - Package.swift: Removed ArgumentParser dependency, added Swift Configuration with CommandLineArguments trait - Updated platform requirements: macOS 15+, iOS 18+, watchOS 11+ **Benefits:** - Unified configuration: Single ConfigurationLoader handles both CLI arguments and environment variables - Automatic type conversion for String, Int, Double, Bool - Secrets handling with automatic redaction - Better fault tolerance: Invalid values fall back to defaults - Cleaner code: ~450 lines of boilerplate removed All commands maintain the same CLI argument syntax while using a significantly cleaner implementation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 69f99cf commit 8bccc0f

File tree

11 files changed

+742
-434
lines changed

11 files changed

+742
-434
lines changed

Package.resolved

Lines changed: 37 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ let swiftSettings: [SwiftSetting] = [
8080
let package = Package(
8181
name: "BushelCloud",
8282
platforms: [
83-
.macOS(.v14),
84-
.iOS(.v17),
85-
.watchOS(.v10),
86-
.tvOS(.v17),
87-
.visionOS(.v1)
83+
.macOS(.v15),
84+
.iOS(.v18),
85+
.watchOS(.v11),
86+
.tvOS(.v18),
87+
.visionOS(.v2)
8888
],
8989
products: [
9090
.library(name: "BushelCloudKit", targets: ["BushelCloudKit"]),
@@ -95,7 +95,11 @@ let package = Package(
9595
.package(url: "https://github.com/brightdigit/BushelKit.git", branch: "v3.0.0-alpha.2"),
9696
.package(url: "https://github.com/brightdigit/IPSWDownloads.git", from: "1.0.0"),
9797
.package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"),
98-
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0")
98+
.package(
99+
url: "https://github.com/apple/swift-configuration.git",
100+
from: "1.0.0",
101+
traits: ["CommandLineArguments"]
102+
)
99103
],
100104
targets: [
101105
.target(
@@ -107,15 +111,15 @@ let package = Package(
107111
.product(name: "BushelUtilities", package: "BushelKit"),
108112
.product(name: "BushelVirtualBuddy", package: "BushelKit"),
109113
.product(name: "IPSWDownloads", package: "IPSWDownloads"),
110-
.product(name: "SwiftSoup", package: "SwiftSoup")
114+
.product(name: "SwiftSoup", package: "SwiftSoup"),
115+
.product(name: "Configuration", package: "swift-configuration")
111116
],
112117
swiftSettings: swiftSettings
113118
),
114119
.executableTarget(
115120
name: "BushelCloudCLI",
116121
dependencies: [
117-
.target(name: "BushelCloudKit"),
118-
.product(name: "ArgumentParser", package: "swift-argument-parser")
122+
.target(name: "BushelCloudKit")
119123
],
120124
swiftSettings: swiftSettings
121125
),

Sources/BushelCloudCLI/BushelCloudCLI.swift

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,35 @@
2727
// OTHER DEALINGS IN THE SOFTWARE.
2828
//
2929

30-
import ArgumentParser
30+
import Foundation
3131

3232
@main
33-
internal struct BushelCloudCLI: AsyncParsableCommand {
34-
internal static let configuration = CommandConfiguration(
35-
commandName: "bushel-cloud",
36-
abstract: "CloudKit version history tool for Bushel virtualization",
37-
discussion: """
38-
A command-line tool demonstrating MistKit's CloudKit Web Services capabilities.
33+
internal struct BushelCloudCLI {
34+
internal static func main() async throws {
35+
let args = Array(CommandLine.arguments.dropFirst())
36+
let command = args.first ?? "sync"
3937

40-
Manages macOS restore images, Xcode versions, and Swift compiler versions
41-
in CloudKit for use with Bushel's virtualization workflow.
42-
""",
43-
version: "1.0.0",
44-
subcommands: [
45-
SyncCommand.self,
46-
StatusCommand.self,
47-
ListCommand.self,
48-
ExportCommand.self,
49-
ClearCommand.self,
50-
],
51-
defaultSubcommand: SyncCommand.self
52-
)
38+
switch command {
39+
case "sync":
40+
try await SyncCommand.run(args: args)
41+
case "status":
42+
try await StatusCommand.run(args: args)
43+
case "list":
44+
try await ListCommand.run(args: args)
45+
case "export":
46+
try await ExportCommand.run(args: args)
47+
case "clear":
48+
try await ClearCommand.run(args: args)
49+
default:
50+
print("Error: Unknown command '\(command)'")
51+
print("")
52+
print("Available commands:")
53+
print(" sync - Sync data to CloudKit")
54+
print(" status - Show CloudKit data source status")
55+
print(" list - List CloudKit records")
56+
print(" export - Export CloudKit data to JSON")
57+
print(" clear - Clear all CloudKit records")
58+
Foundation.exit(1)
59+
}
60+
}
5361
}

Sources/BushelCloudCLI/Commands/ClearCommand.swift

Lines changed: 14 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -27,85 +27,26 @@
2727
// OTHER DEALINGS IN THE SOFTWARE.
2828
//
2929

30-
import ArgumentParser
3130
import BushelCloudKit
3231
import BushelFoundation
3332
import BushelUtilities
3433
import Foundation
3534

36-
struct ClearCommand: AsyncParsableCommand {
37-
static let configuration = CommandConfiguration(
38-
commandName: "clear",
39-
abstract: "Delete all records from CloudKit",
40-
discussion: """
41-
Deletes all RestoreImage, XcodeVersion, and SwiftVersion records from
42-
the CloudKit public database.
35+
enum ClearCommand {
36+
static func run(args: [String]) async throws {
37+
// Load configuration using Swift Configuration
38+
let loader = ConfigurationLoader()
39+
let rawConfig = try await loader.loadConfiguration()
40+
let config = try rawConfig.validated()
4341

44-
⚠️ WARNING: This operation cannot be undone!
45-
"""
46-
)
47-
48-
// MARK: - Required Options
49-
50-
@Option(name: .shortAndLong, help: "CloudKit container identifier")
51-
var containerIdentifier: String = "iCloud.com.brightdigit.Bushel"
52-
53-
@Option(name: .long, help: "Server-to-Server Key ID (or set CLOUDKIT_KEY_ID)")
54-
var keyID: String = ""
55-
56-
@Option(name: .long, help: "Path to private key .pem file (or set CLOUDKIT_PRIVATE_KEY_PATH)")
57-
var keyFile: String = ""
58-
59-
// MARK: - Options
60-
61-
@Flag(name: .shortAndLong, help: "Skip confirmation prompt")
62-
var yes: Bool = false
63-
64-
@Flag(
65-
name: .shortAndLong,
66-
help:
67-
"Enable verbose logging to see detailed CloudKit operations and learn MistKit usage patterns")
68-
var verbose: Bool = false
69-
70-
// MARK: - Execution
71-
72-
mutating func run() async throws {
7342
// Enable verbose console output if requested
74-
BushelUtilities.ConsoleOutput.isVerbose = verbose
75-
76-
// Get Server-to-Server credentials from environment if not provided
77-
let resolvedKeyID =
78-
keyID.isEmpty ? ProcessInfo.processInfo.environment["CLOUDKIT_KEY_ID"] ?? "" : keyID
79-
80-
let resolvedKeyFile =
81-
keyFile.isEmpty
82-
? ProcessInfo.processInfo.environment["CLOUDKIT_PRIVATE_KEY_PATH"] ?? "" : keyFile
83-
84-
guard !resolvedKeyID.isEmpty && !resolvedKeyFile.isEmpty else {
85-
print("❌ Error: CloudKit Server-to-Server Key credentials are required")
86-
print("")
87-
print(" Provide via command-line flags:")
88-
print(" --key-id YOUR_KEY_ID --key-file ./private-key.pem")
89-
print("")
90-
print(" Or set environment variables:")
91-
print(" export CLOUDKIT_KEY_ID=\"YOUR_KEY_ID\"")
92-
print(" export CLOUDKIT_PRIVATE_KEY_PATH=\"./private-key.pem\"")
93-
print("")
94-
print(" Get your Server-to-Server Key from:")
95-
print(" https://icloud.developer.apple.com/dashboard/")
96-
print(" Navigate to: API Access → Server-to-Server Keys")
97-
print("")
98-
print(" Important:")
99-
print(" • Download and save the private key .pem file securely")
100-
print(" • Never commit .pem files to version control!")
101-
print("")
102-
throw ExitCode.failure
103-
}
43+
BushelUtilities.ConsoleOutput.isVerbose = config.clear?.verbose ?? false
10444

10545
// Confirm deletion unless --yes flag is provided
106-
if !yes {
46+
let skipConfirmation = config.clear?.yes ?? false
47+
if !skipConfirmation {
10748
print("\n⚠️ WARNING: This will delete ALL records from CloudKit!")
108-
print(" Container: \(containerIdentifier)")
49+
print(" Container: \(config.cloudKit.containerID)")
10950
print(" Database: public (development)")
11051
print("")
11152
print(" This operation cannot be undone.")
@@ -120,9 +61,9 @@ struct ClearCommand: AsyncParsableCommand {
12061

12162
// Create sync engine
12263
let syncEngine = try SyncEngine(
123-
containerIdentifier: containerIdentifier,
124-
keyID: resolvedKeyID,
125-
privateKeyPath: resolvedKeyFile
64+
containerIdentifier: config.cloudKit.containerID,
65+
keyID: config.cloudKit.keyID,
66+
privateKeyPath: config.cloudKit.privateKeyPath
12667
)
12768

12869
// Execute clear
@@ -137,7 +78,7 @@ struct ClearCommand: AsyncParsableCommand {
13778

13879
// MARK: - Private Helpers
13980

140-
private func printError(_ error: Error) {
81+
private static func printError(_ error: Error) {
14182
print("\n❌ Clear failed: \(error.localizedDescription)")
14283
print("\n💡 Troubleshooting:")
14384
print(" • Verify your API token is valid")

0 commit comments

Comments
 (0)