A command-line tool demonstrating MistKit's CloudKit Web Services capabilities by syncing macOS restore images, Xcode versions, and Swift compiler versions to CloudKit.
📖 Tutorial-Friendly Demo - This example is designed for developers learning CloudKit and MistKit. Use the
--verboseflag to see detailed explanations of CloudKit operations and MistKit usage patterns.
This demo teaches practical CloudKit development patterns:
- ✅ Server-to-Server Authentication - How to authenticate CloudKit operations from command-line tools and servers
- ✅ Batch Record Operations - Handling CloudKit's 200-operation-per-request limit efficiently
- ✅ Record Relationships - Using CKReference to create relationships between records
- ✅ Multi-Source Data Integration - Fetching, deduplicating, and merging data from multiple APIs
- ✅ Modern Swift Patterns - async/await, Sendable types, and Swift 6 concurrency
New to CloudKit? Start with the Quick Start Guide below, then explore verbose mode to see how everything works under the hood.
Bushel is a comprehensive demo application showcasing how to use MistKit to:
- Fetch data from multiple sources (ipsw.me, AppleDB.dev, xcodereleases.com, swift.org, MESU, Mr. Macintosh)
- Transform data into CloudKit-compatible record structures
- Batch upload records to CloudKit using the Web Services REST API
- Handle relationships between records using CloudKit References
- Export data for analysis or backup
In Apple's virtualization framework, restore images are used to boot virtual Macintosh systems. These images are essential for running macOS VMs and are distributed through Apple's software update infrastructure. Bushel collects and organizes information about these images along with related Xcode and Swift versions, making it easier to manage virtualization environments.
The demo integrates with multiple data sources to gather comprehensive version information:
-
IPSW.me API (via IPSWDownloads)
- macOS restore images for VirtualMac2,1
- Build numbers, release dates, signatures, file sizes
-
AppleDB.dev
- Comprehensive macOS restore image database
- Device-specific signing status
- VirtualMac2,1 compatibility information
- Maintained by LittleByte Organization
-
XcodeReleases.com
- Xcode versions and build numbers
- Release dates and prerelease status
- Download URLs and SDK information
-
Swift.org
- Swift compiler versions
- Release dates and download links
- Official Swift toolchain information
-
Apple MESU Catalog (Mobile Equipment Software Update)
- Official macOS restore image catalog
- Asset metadata and checksums
-
Mr. Macintosh's Restore Image Archive
- Historical restore image information
- Community-maintained release data
BushelCloud/
├── DataSources/ # Data fetchers for external APIs
│ ├── IPSWFetcher.swift
│ ├── XcodeReleasesFetcher.swift
│ ├── SwiftVersionFetcher.swift
│ ├── MESUFetcher.swift
│ ├── MrMacintoshFetcher.swift
│ └── DataSourcePipeline.swift
├── Models/ # Data models
│ ├── RestoreImageRecord.swift
│ ├── XcodeVersionRecord.swift
│ └── SwiftVersionRecord.swift
├── CloudKit/ # CloudKit integration
│ ├── BushelCloudKitService.swift
│ ├── RecordBuilder.swift
│ └── SyncEngine.swift
└── Commands/ # CLI commands
├── BushelCloudCLI.swift
├── SyncCommand.swift
└── ExportCommand.swift
BushelCloud uses BushelKit as its modular foundation, providing:
Core Modules:
- BushelFoundation - Domain models (RestoreImageRecord, XcodeVersionRecord, SwiftVersionRecord)
- BushelUtilities - Formatting helpers, JSON decoding, console output
- BushelLogging - Unified logging abstractions
Current Integration:
- Git subrepo at
Packages/BushelKit/for rapid development - Local path dependency during migration phase
Future:
- After BushelKit v2.0 stable release → versioned remote dependency
- BushelKit will support VM management features
Documentation: BushelKit Docs
✅ Public API Usage
- Uses only public MistKit APIs (no internal OpenAPI types)
- Demonstrates proper abstraction layer design
✅ Record Operations
- Creating records with
RecordOperation.create() - Batch operations with
modifyRecords() - Proper field value mapping with
FieldValueenum
✅ Data Type Support
- Strings, integers, booleans, dates
- CloudKit References (relationships between records)
- Proper date/timestamp conversion (milliseconds since epoch)
✅ Batch Processing
- CloudKit's 200-operation-per-request limit handling
- Progress reporting during sync
- Error handling for partial failures
✅ Authentication
- Server-to-Server Key authentication with
ServerToServerAuthManager - ECDSA P-256 private key signing
- Container and environment configuration
✅ Strict Concurrency
- All types conform to
Sendable - Async/await throughout
- No data races
✅ Modern Error Handling
- Typed errors with
CloudKitError - Proper error propagation with
throws
✅ Value Semantics
- Immutable structs for data models
- No reference types in concurrent contexts
- CloudKit Container - Create a container in CloudKit Dashboard
- Server-to-Server Key - Generate from CloudKit Dashboard → API Access
- Private Key File - Download the
.pemfile when creating the key
For detailed setup instructions, run swift package generate-documentation and view the CloudKit Setup guide in the generated documentation.
# From Bushel directory
swift build
# Run the demo
.build/debug/bushel-cloud --helpRun with --verbose to see educational explanations of what's happening:
export CLOUDKIT_KEY_ID="YOUR_KEY_ID"
export CLOUDKIT_KEY_FILE="./path/to/private-key.pem"
# Optional: Enable VirtualBuddy TSS signing status
export VIRTUALBUDDY_API_KEY="YOUR_VIRTUALBUDDY_API_KEY"
# Sync with verbose logging to learn how MistKit works
.build/debug/bushel-cloud sync --verbose
# Or do a dry run first to see what would be synced
.build/debug/bushel-cloud sync --dry-run --verboseWhat the verbose flag shows:
- 🔍 How MistKit authenticates with Server-to-Server keys
- 💡 CloudKit batch processing (200 operations/request limit)
- 📊 Data source fetching and deduplication
- ⚙️ Record dependency ordering
- 🌐 Actual CloudKit API calls and responses
Clone and build the project:
git clone https://github.com/brightdigit/BushelCloud.git
cd BushelCloud
swift build -c release
.build/release/bushel-cloud --helpBuild and install to /usr/local/bin:
git clone https://github.com/brightdigit/BushelCloud.git
cd BushelCloud
make installThis makes bushel-cloud available globally.
Run without local Swift installation:
git clone https://github.com/brightdigit/BushelCloud.git
cd BushelCloud
make docker-runBefore running any sync operations, you'll need:
- CloudKit container (create in CloudKit Dashboard)
- Server-to-Server Key (generate from API Access section)
- Private key
.pemfile (downloaded when creating key)
See Authentication Setup for detailed instructions.
Fetch data from all sources and upload to CloudKit:
# Basic usage
bushel-cloud sync \
--container-id "iCloud.com.brightdigit.Bushel" \
--key-id "YOUR_KEY_ID" \
--key-file ./path/to/private-key.pem
# With verbose logging (recommended for learning)
bushel-cloud sync --verbose
# Dry run (fetch data but don't upload to CloudKit)
bushel-cloud sync --dry-run
# Selective sync
bushel-cloud sync --restore-images-only
bushel-cloud sync --xcode-only
bushel-cloud sync --swift-only
bushel-cloud sync --no-betas # Exclude beta/RC releases
# Use environment variables (recommended)
export CLOUDKIT_KEY_ID="YOUR_KEY_ID"
export CLOUDKIT_KEY_FILE="./path/to/private-key.pem"
bushel-cloud sync --verboseQuery and export CloudKit data to JSON file:
# Export to file
bushel-cloud export \
--container-id "iCloud.com.brightdigit.Bushel" \
--key-id "YOUR_KEY_ID" \
--key-file ./path/to/private-key.pem \
--output ./bushel-data.json
# With verbose logging
bushel-cloud export --verbose --output ./bushel-data.json
# Pretty-print JSON
bushel-cloud export --pretty --output ./bushel-data.json
# Export to stdout for piping
bushel-cloud export --pretty | jq '.restoreImages | length'bushel-cloud --help
bushel-cloud sync --help
bushel-cloud export --helpFor Xcode setup and debugging instructions, see the "Xcode Development Setup" section in CLAUDE.md.
The demo uses three record types with relationships:
SwiftVersion
↑
| (reference)
|
RestoreImage ← XcodeVersion
↑ ↑
| (reference) |
|______________|
- XcodeVersion → RestoreImage: Links Xcode to minimum macOS version required
- XcodeVersion → SwiftVersion: Links Xcode to included Swift compiler version
- Fetch Swift 6.0.3 → Create SwiftVersion record
- Fetch macOS 15.2 restore image → Create RestoreImage record
- Fetch Xcode 16.2 → Create XcodeVersion record with references to both
Shows how to convert domain models to CloudKit records using the CloudKitRecord protocol:
extension RestoreImageRecord: CloudKitRecord {
static var cloudKitRecordType: String { "RestoreImage" }
func toCloudKitFields() -> [String: FieldValue] {
var fields: [String: FieldValue] = [
"version": .string(version),
"buildNumber": .string(buildNumber),
"releaseDate": .date(releaseDate),
"fileSize": .int64(Int(fileSize)),
"isSigned": .boolean(isSigned),
// ... more fields
]
return fields
}
static func from(recordInfo: RecordInfo) -> Self? {
// Parse CloudKit record into domain model
guard let fields = recordInfo.fields else { return nil }
// ... field extraction
return RestoreImageRecord(/* ... */)
}
}Demonstrates efficient CloudKit batch operations:
private func executeBatchOperations(
_ operations: [RecordOperation],
recordType: String
) async throws {
let batchSize = 200 // CloudKit limit
let batches = operations.chunked(into: batchSize)
for (index, batch) in batches.enumerated() {
print(" Batch \(index + 1)/\(batches.count)...")
_ = try await service.modifyRecords(batch)
}
}Shows async/await parallel data fetching:
struct DataSourcePipeline: Sendable {
func fetchAllData() async throws -> (
restoreImages: [RestoreImageRecord],
xcodeVersions: [XcodeVersionRecord],
swiftVersions: [SwiftVersionRecord]
) {
async let restoreImages = ipswFetcher.fetch()
async let xcodeVersions = xcodeReleasesFetcher.fetch()
async let swiftVersions = swiftVersionFetcher.fetch()
return try await (restoreImages, xcodeVersions, swiftVersions)
}
}- macOS 14.0+ (for demonstration purposes; MistKit supports macOS 11.0+)
- Swift 6.2+
- Xcode 16.2+ (for development)
- CloudKit container with appropriate schema (see setup below)
- CloudKit Server-to-Server Key (Key ID + private .pem file)
Before running the sync command, you need to set up the CloudKit schema. The schema will be created at the container level, but Bushel writes all records to the public database for worldwide accessibility.
You have two options:
Use cktool to automatically import the schema:
# Save your CloudKit management token
xcrun cktool save-token
# Set environment variables
export CLOUDKIT_CONTAINER_ID="iCloud.com.yourcompany.Bushel"
export CLOUDKIT_TEAM_ID="YOUR_TEAM_ID"
# Run the setup script
cd Examples/Bushel
./Scripts/setup-cloudkit-schema.shRun the automated setup script: ./Scripts/setup-cloudkit-schema.sh or view the CloudKit Setup guide in the documentation.
Create the record types manually in CloudKit Dashboard.
See the "CloudKit Schema Field Reference" section in CLAUDE.md for complete field definitions.
After setting up your CloudKit schema, you need to create a Server-to-Server Key for authentication:
- Go to CloudKit Dashboard
- Select your container
- Navigate to API Access → Server-to-Server Keys
- Click Create a Server-to-Server Key
- Enter a key name (e.g., "Bushel Demo Key")
- Click Create
- Download the private key .pem file - You won't be able to download it again!
- Note the Key ID displayed (e.g., "abc123def456")
- Store the
.pemfile in a secure location (e.g.,~/.cloudkit/bushel-private-key.pem) - Never commit .pem files to version control (already in
.gitignore) - Use appropriate file permissions:
chmod 600 ~/.cloudkit/bushel-private-key.pem - Consider using environment variables for the key path
Method 1: Command-line flags
bushel-cloud sync \
--key-id "YOUR_KEY_ID" \
--key-file ~/.cloudkit/bushel-private-key.pemMethod 2: Environment variables (recommended for frequent use)
# Add to your ~/.zshrc or ~/.bashrc
export CLOUDKIT_KEY_ID="YOUR_KEY_ID"
export CLOUDKIT_KEY_FILE="$HOME/.cloudkit/bushel-private-key.pem"
# Optional: VirtualBuddy TSS signing status (get from https://tss.virtualbuddy.app/)
export VIRTUALBUDDY_API_KEY="YOUR_VIRTUALBUDDY_API_KEY"
# Then simply run
bushel-cloud sync- MistKit - CloudKit Web Services client (local path dependency)
- IPSWDownloads - ipsw.me API wrapper
- SwiftSoup - HTML parsing for web scraping
- ArgumentParser - CLI argument parsing
- Swift 6.1 or later
- macOS 14.0+ (for full CloudKit functionality)
- Mint (for linting tools):
brew install mint
Develop with Linux and test multiple Swift versions using VS Code Dev Containers:
Available configurations:
- Swift 6.1 (Ubuntu Jammy)
- Swift 6.2 (Ubuntu Jammy)
- Swift 6.2 (Ubuntu Noble) - Default
Usage:
- Install VS Code Dev Containers extension
- Open project in VS Code
- Click "Reopen in Container" or use Command Palette:
Dev Containers: Reopen in Container - Select desired Swift version when prompted
Or use directly with Docker:
# Swift 6.2 on Ubuntu Noble
docker run -it -v $PWD:/workspace -w /workspace swift:6.2-noble bash
# Run tests
docker run -v $PWD:/workspace -w /workspace swift:6.2-noble swift testmake build # Build the project
make test # Run tests
make lint # Run linting
make format # Format code
make xcode # Generate Xcode project
make install # Install to /usr/local/bin
make help # Show all targetsswift build
# Or with make:
make buildswift test
# Or with make:
make test./Scripts/lint.sh
# Or with make:
make lintThis will:
- Format code with swift-format
- Check style with SwiftLint
- Verify code compiles
- Add copyright headers
make docker-build # Build in Docker
make docker-test # Test in Docker
make docker-run # Interactive shellGenerate Xcode project using XcodeGen:
make xcode
# Or directly:
mint run xcodegen generateThis creates BushelCloud.xcodeproj from project.yml. The project file is gitignored and regenerated as needed.
Targets included:
- BushelCloud - Main executable
- BushelCloudTests - Unit tests
- Linting - Aggregate target that runs SwiftLint
This project uses GitHub Actions for continuous integration:
- Multi-platform builds: Ubuntu (Noble, Jammy), Windows (2022, 2025), macOS 15
- Swift versions: 6.1, 6.2, 6.2-nightly
- Xcode versions: 16.3, 16.4, 26.0
- Linting: SwiftLint, swift-format, periphery
- Security: CodeQL static analysis
- Coverage: Codecov integration
- AI Review: Claude Code for automated PR reviews
See .github/workflows/ for workflow configurations.
Managed via Mint (see Mintfile):
swift-format@602.0.0- Code formattingSwiftLint@0.62.2- Style and convention lintingperiphery@3.2.0- Unused code detection
Configuration files:
.swiftlint.yml- 90+ opt-in rules, strict mode.swift-format- 2-space indentation, 100-char lines
Bushel fetches data from multiple external sources including:
- ipsw.me - macOS restore images for VirtualMac devices
- AppleDB.dev - Comprehensive restore image database with device-specific signing information
- xcodereleases.com - Xcode versions and build information
- swift.org - Swift compiler versions
- Apple MESU - Official restore image metadata
- Mr. Macintosh - Community-maintained release archive
- VirtualBuddy TSS API (optional) - Real-time TSS signing status verification (requires API key from tss.virtualbuddy.app)
The sync command fetches from all sources, deduplicates records, and uploads to CloudKit.
The export command queries existing records from your CloudKit database and exports them to JSON format.
⚠️ No duplicate detection (will create duplicate records on repeated syncs)⚠️ No incremental sync (always fetches all data)⚠️ No conflict resolution for concurrent updates⚠️ Limited error recovery in batch operations
- Add
--updatemode to update existing records instead of creating new ones - Implement incremental sync with change tracking
- Add record deduplication logic
- Support for querying existing records before sync
- Progress bar for long-running operations
- Retry logic for transient network errors
- Validation of record references before upload
- Support for CloudKit zones for better organization
❌ "Private key file not found"
✅ Solution: Check that your .pem file path is correct
export CLOUDKIT_KEY_FILE="$HOME/.cloudkit/bushel-private-key.pem"
ls -la "$CLOUDKIT_KEY_FILE" # Verify file exists❌ "Authentication failed" or "Invalid signature"
✅ Solutions:
1. Verify Key ID matches the key in CloudKit Dashboard
2. Check that .pem file is the correct private key (not certificate)
3. Ensure key hasn't been revoked in CloudKit Dashboard
4. Try regenerating the key if issues persist
❌ "Record type not found" errors
✅ Solution: Set up CloudKit schema first
cd Examples/Bushel
./Scripts/setup-cloudkit-schema.sh
# Or manually create record types in CloudKit Dashboard❌ Seeing duplicate records after re-sync
✅ This is expected behavior - Bushel creates new records each sync
See "Limitations" section for details on incremental sync
❌ "Operation failed" with no details
✅ Solution: Use --verbose flag to see CloudKit error details
bushel-cloud sync --verbose
# Look for serverErrorCode and reason in outputFor verbose logging:
- Always run with
--verboseflag when troubleshooting - Check the console for 🔍 (verbose), 💡 (explanations), and
⚠️ (warnings)
For CloudKit errors:
- Review CloudKit Dashboard for schema configuration
- Verify Server-to-Server key is active
- Check container identifier matches your CloudKit container
For MistKit issues:
- See MistKit repository for documentation
- Check MistKit's test suite for usage examples
Start Here:
- Run
bushel-cloud sync --dry-run --verboseto see what happens without uploading - Review the code in
SyncEngine.swiftto understand the flow - Check
BushelCloudKitService.swiftfor MistKit usage patterns - Explore
RecordBuilder.swiftto see CloudKit record construction
Key Files to Study:
BushelCloudKitService.swift- Server-to-Server authentication and batch operationsSyncEngine.swift- Overall sync orchestrationRecordBuilder.swift- CloudKit record field mappingDataSourcePipeline.swift- Multi-source data integration
This demo is designed to be reusable for your own CloudKit projects:
✅ Copy the authentication pattern from BushelCloudKitService.swift
- Shows how to load private keys from disk
- Demonstrates ServerToServerAuthManager setup
- Handles all ECDSA signing automatically
✅ Adapt the batch processing from executeBatchOperations()
- Handles CloudKit's 200-operation limit
- Shows progress reporting
- Demonstrates error handling for partial failures
✅ Use the logging pattern from Logger.swift
- os.Logger with subsystems for organization
- Verbose mode for development/debugging
- Educational explanations for documentation
✅ Reference record building from RecordBuilder.swift
- Shows CloudKit field mapping
- Demonstrates CKReference relationships
- Handles date conversion (milliseconds since epoch)
- MistKit Repository
- See main repository's CLAUDE.md for development guidelines
- IPSWDownloads - ipsw.me API client
- XcodeReleases.com - Xcode version tracking
- Celestra (coming soon) - RSS aggregator using MistKit (sibling demo)
This is a demonstration project. For contributions to MistKit itself, please see the main repository.
Same as MistKit - MIT License. See main repository LICENSE file.
For issues specific to this demo:
- Check the "Xcode Development Setup" section in CLAUDE.md for configuration help
- Review CloudKit Dashboard for schema and authentication issues
For MistKit issues:
- Open an issue in the main MistKit repository
- Include relevant code snippets and error messages