diff --git a/.agents/interfaces/.gitkeep b/.agents/interfaces/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/interfaces/macos/Resettable.swiftinterface b/.agents/interfaces/macos/Resettable.swiftinterface
new file mode 100644
index 0000000..3a489b8
--- /dev/null
+++ b/.agents/interfaces/macos/Resettable.swiftinterface
@@ -0,0 +1,296 @@
+// swift-interface-format-version: 1.0
+// swift-compiler-version: Apple Swift version 6.4 (swiftlang-6.4.0.25.4 clang-2100.3.25.1)
+// swift-module-flags: -target arm64-apple-macos12.0 -enable-objc-interop -language-mode 6 -O -enable-experimental-feature DebugDescriptionMacro -module-name Resettable -package-name swift_resettable
+// swift-module-flags-ignorable: -formal-cxx-interoperability-mode=off -interface-compiler-version 6.4
+import KeyPathMapping
+import KeyPathsExtensions
+import Swift
+import SwiftMarkerProtocols
+import _Concurrency
+import _StringProcessing
+import _SwiftConcurrencyShims
+extension Resettable::Resettable where Base : Swift::Collection {
+ public var collection: Resettable::Resettable.Resettable::WritableKeyPathContainer {
+ get
+ }
+}
+extension Resettable::Resettable.Resettable::WritableKeyPathContainer where Value : Swift::Collection {
+ @discardableResult
+ public func swapAt(_ idx1: Swift::Array.Swift::Index, _ idx2: Swift::Array.Swift::Index, operation: Resettable::Resettable.Resettable::OperationBehavior = .default) -> Resettable::Resettable where Value == [T]
+ @discardableResult
+ public func remove(at idx: Swift::Array.Swift::Index, operation: Resettable::Resettable.Resettable::OperationBehavior = .default) -> Resettable::Resettable where Value == [T]
+ @discardableResult
+ public func insert(_ element: Swift::Array.Swift::Element, at idx: Swift::Array.Swift::Index, operation: Resettable::Resettable.Resettable::OperationBehavior = .default) -> Resettable::Resettable where Value == [T]
+ @discardableResult
+ public func append(_ element: Swift::Array.Swift::Element, operation: Resettable::Resettable.Resettable::OperationBehavior = .default) -> Resettable::Resettable where Value == [T]
+}
+extension Resettable::Resettable {
+ public enum OperationBehavior {
+ case `default`
+ case amend
+ case insert
+ case inject
+ @_semantics("derived_enum_equals") @_implements(Swift::Equatable, ==(_:_:)) public static func __derived_enum_equals(_ a: Resettable::Resettable.Resettable::OperationBehavior, _ b: Resettable::Resettable.Resettable::OperationBehavior) -> Swift::Bool
+ public func hash(into hasher: inout Swift::Hasher)
+ public var hashValue: Swift::Int {
+ get
+ }
+ }
+}
+@propertyWrapper @dynamicMemberLookup public class Resettable {
+ @available(*, deprecated, renamed: "Base")
+ public typealias Object = Base
+ convenience public init(wrappedValue: Base)
+ public init(_ base: Base)
+ @usableFromInline
+ internal var base: Base
+ @inlinable public var wrappedValue: Base {
+ _read { yield base }
+ }
+ @inlinable public var projectedValue: Resettable::Resettable {
+ get { self }
+ }
+ @discardableResult
+ public func undo() -> Resettable::Resettable
+ @discardableResult
+ public func redo() -> Resettable::Resettable
+ @discardableResult
+ @inlinable public func undo(_ count: Swift::Int) -> Resettable::Resettable {
+ for _ in 0.. Resettable::Resettable {
+ for _ in 0.. Resettable::Resettable
+ @discardableResult
+ public func redoAll() -> Resettable::Resettable
+ @discardableResult
+ public func _modify(operation: Resettable::Resettable.Resettable::OperationBehavior = .default, _ keyPath: Swift::WritableKeyPath, using action: @escaping (inout Value) -> Swift::Void) -> Resettable::Resettable
+ @discardableResult
+ public func _modify(operation: Resettable::Resettable.Resettable::OperationBehavior = .default, _ keyPath: Swift::WritableKeyPath, using action: @escaping (inout Value) -> Swift::Void, undo: @escaping (inout Value) -> Swift::Void) -> Resettable::Resettable
+ @discardableResult
+ public func _modify(operation: Resettable::Resettable.Resettable::OperationBehavior = .default, using action: @escaping (inout Base) -> Swift::Void, undo: @escaping (inout Base) -> Swift::Void) -> Resettable::Resettable
+ @inlinable public subscript(dynamicMember keyPath: Swift::WritableKeyPath) -> Resettable::Resettable.Resettable::WritableKeyPathContainer {
+ get {
+ WritableKeyPathContainer(
+ resettable: self,
+ keyPath: keyPath
+ )
+ }
+ }
+ @inlinable public subscript(dynamicMember keyPath: Swift::KeyPath) -> Resettable::Resettable.Resettable::KeyPathContainer {
+ get {
+ KeyPathContainer(
+ resettable: self,
+ keyPath: keyPath
+ )
+ }
+ }
+ @objc deinit
+}
+extension Resettable::Resettable {
+ @dynamicMemberLookup public struct KeyPathContainer {
+ @usableFromInline
+ internal let resettable: Resettable::Resettable
+ @usableFromInline
+ internal let keyPath: Swift::KeyPath
+ @usableFromInline
+ internal init(resettable: Resettable::Resettable, keyPath: Swift::KeyPath)
+ @inlinable public subscript(dynamicMember keyPath: Swift::ReferenceWritableKeyPath) -> Resettable::Resettable.Resettable::WritableKeyPathContainer {
+ get {
+ WritableKeyPathContainer(
+ resettable: resettable,
+ keyPath: self.keyPath.appending(path: keyPath)
+ )
+ }
+ }
+ @inlinable public subscript(dynamicMember keyPath: Swift::KeyPath) -> Resettable::Resettable.Resettable::KeyPathContainer {
+ get {
+ KeyPathContainer(
+ resettable: resettable,
+ keyPath: self.keyPath.appending(path: keyPath)
+ )
+ }
+ }
+ }
+ @dynamicMemberLookup public struct WritableKeyPathContainer {
+ @usableFromInline
+ internal let resettable: Resettable::Resettable
+ @usableFromInline
+ internal let keyPath: Swift::WritableKeyPath
+ @usableFromInline
+ internal init(resettable: Resettable::Resettable, keyPath: Swift::WritableKeyPath)
+ @discardableResult
+ @inlinable public func callAsFunction(_ value: Value, operation: Resettable::Resettable.Resettable::OperationBehavior = .default) -> Resettable::Resettable {
+ return self.callAsFunction(operation) { $0 = value }
+ }
+ @discardableResult
+ @inlinable public func callAsFunction(_ operation: Resettable::Resettable.Resettable::OperationBehavior = .default, _ action: @escaping (inout Value) -> Swift::Void) -> Resettable::Resettable {
+ return resettable._modify(
+ operation: operation,
+ keyPath,
+ using: action
+ )
+ }
+ @discardableResult
+ @inlinable public func callAsFunction(_ operation: Resettable::Resettable.Resettable::OperationBehavior = .default, _ action: @escaping (inout Value) -> Swift::Void, undo: @escaping (inout Value) -> Swift::Void) -> Resettable::Resettable {
+ return resettable._modify(
+ operation: operation,
+ keyPath,
+ using: action,
+ undo: undo
+ )
+ }
+ @inlinable public subscript(dynamicMember keyPath: Swift::WritableKeyPath) -> Resettable::Resettable.Resettable::WritableKeyPathContainer {
+ get {
+ WritableKeyPathContainer(
+ resettable: resettable,
+ keyPath: self.keyPath.appending(path: keyPath)
+ )
+ }
+ }
+ @inlinable public subscript(dynamicMember keyPath: Swift::KeyPath) -> Resettable::Resettable.Resettable::KeyPathContainer {
+ get {
+ KeyPathContainer(
+ resettable: resettable,
+ keyPath: self.keyPath.appending(path: keyPath)
+ )
+ }
+ }
+ }
+ @dynamicMemberLookup public struct IfLetKeyPathContainer {
+ public typealias Value = Wrapped?
+ @usableFromInline
+ internal let resettable: Resettable::Resettable
+ @usableFromInline
+ internal let keyPath: Swift::KeyPath.Resettable::IfLetKeyPathContainer.Resettable::Value>
+ @usableFromInline
+ internal init(resettable: Resettable::Resettable, keyPath: Swift::KeyPath.Resettable::IfLetKeyPathContainer.Resettable::Value>)
+ @inlinable public subscript(dynamicMember keyPath: Swift::ReferenceWritableKeyPath) -> Resettable::Resettable.Resettable::IfLetWritableKeyPathContainer {
+ get {
+ IfLetWritableKeyPathContainer(
+ resettable: resettable,
+ keyPath: self.keyPath.appending(path: keyPath)
+ )
+ }
+ }
+ @inlinable public subscript(dynamicMember keyPath: Swift::KeyPath) -> Resettable::Resettable.Resettable::IfLetKeyPathContainer {
+ get {
+ IfLetKeyPathContainer(
+ resettable: resettable,
+ keyPath: self.keyPath.appending(path: keyPath)
+ )
+ }
+ }
+ }
+ @dynamicMemberLookup public struct IfLetWritableKeyPathContainer {
+ public typealias Value = Wrapped?
+ @usableFromInline
+ internal let resettable: Resettable::Resettable
+ @usableFromInline
+ internal let keyPath: Swift::WritableKeyPath.Resettable::IfLetWritableKeyPathContainer.Resettable::Value>
+ @usableFromInline
+ internal init(resettable: Resettable::Resettable, keyPath: Swift::WritableKeyPath.Resettable::IfLetWritableKeyPathContainer.Resettable::Value>)
+ @discardableResult
+ @inlinable public func callAsFunction(_ value: Wrapped, operation: Resettable::Resettable.Resettable::OperationBehavior = .default) -> Resettable::Resettable {
+ guard resettable.wrappedValue[keyPath: keyPath] != nil else { return resettable }
+ return self.callAsFunction(operation) { $0 = value }
+ }
+ @discardableResult
+ @inlinable public func callAsFunction(_ operation: Resettable::Resettable.Resettable::OperationBehavior = .default, _ action: @escaping (inout Wrapped) -> Swift::Void) -> Resettable::Resettable {
+ guard let value = resettable.wrappedValue[keyPath: keyPath] else { return resettable }
+ return resettable._modify(
+ operation: operation,
+ keyPath.unwrapped(with: value),
+ using: action
+ )
+ }
+ @discardableResult
+ @inlinable public func callAsFunction(_ operation: Resettable::Resettable.Resettable::OperationBehavior = .default, _ action: @escaping (inout Wrapped) -> Swift::Void, undo: @escaping (inout Wrapped) -> Swift::Void) -> Resettable::Resettable {
+ guard let value = resettable.wrappedValue[keyPath: keyPath] else { return resettable }
+ return resettable._modify(
+ operation: operation,
+ keyPath.unwrapped(with: value),
+ using: action,
+ undo: undo
+ )
+ }
+ @inlinable public subscript(dynamicMember keyPath: Swift::WritableKeyPath) -> Resettable::Resettable.Resettable::IfLetWritableKeyPathContainer {
+ get {
+ IfLetWritableKeyPathContainer(
+ resettable: resettable,
+ keyPath: self.keyPath.appending(path: keyPath)
+ )
+ }
+ }
+ @inlinable public subscript(dynamicMember keyPath: Swift::KeyPath) -> Resettable::Resettable.Resettable::IfLetKeyPathContainer {
+ get {
+ IfLetKeyPathContainer(
+ resettable: resettable,
+ keyPath: self.keyPath.appending(path: keyPath)
+ )
+ }
+ }
+ }
+}
+extension Resettable::Resettable where Base : SwiftMarkerProtocols::_OptionalProtocol {
+ public var ifLet: Resettable::Resettable.Resettable::IfLetWritableKeyPathContainer {
+ get
+ }
+}
+extension Resettable::Resettable {
+ public func ifLet(_ keyPath: Swift::WritableKeyPath) -> Resettable::Resettable.Resettable::IfLetWritableKeyPathContainer
+ public func ifLet(_ keyPath: Swift::KeyPath) -> Resettable::Resettable.Resettable::IfLetKeyPathContainer
+}
+extension Resettable::Resettable.Resettable::WritableKeyPathContainer where Value : SwiftMarkerProtocols::_OptionalProtocol {
+ public var ifLet: Resettable::Resettable.Resettable::IfLetWritableKeyPathContainer {
+ get
+ }
+ public func ifNil(_ value: Value, operation: Resettable::Resettable.Resettable::OperationBehavior = .default) -> Resettable::Resettable
+}
+extension Resettable::Resettable.Resettable::KeyPathContainer where Value : SwiftMarkerProtocols::_OptionalProtocol {
+ public var ifLet: Resettable::Resettable.Resettable::IfLetKeyPathContainer {
+ get
+ }
+}
+extension Resettable::Resettable.Resettable::IfLetWritableKeyPathContainer where Wrapped : SwiftMarkerProtocols::_OptionalProtocol {
+ public var ifLet: Resettable::Resettable.Resettable::IfLetWritableKeyPathContainer {
+ get
+ }
+}
+extension Resettable::Resettable.Resettable::IfLetKeyPathContainer where Wrapped : SwiftMarkerProtocols::_OptionalProtocol {
+ public var ifLet: Resettable::Resettable.Resettable::IfLetKeyPathContainer {
+ get
+ }
+}
+extension Resettable::Resettable.Resettable::WritableKeyPathContainer {
+ public func ifLet(_ keyPath: Swift::WritableKeyPath) -> Resettable::Resettable.Resettable::IfLetWritableKeyPathContainer
+ public func ifLet(_ keyPath: Swift::KeyPath) -> Resettable::Resettable.Resettable::IfLetKeyPathContainer
+}
+extension Resettable::Resettable.Resettable::KeyPathContainer {
+ public func ifLet(_ keyPath: Swift::ReferenceWritableKeyPath) -> Resettable::Resettable.Resettable::IfLetWritableKeyPathContainer
+ public func ifLet(_ keyPath: Swift::KeyPath) -> Resettable::Resettable.Resettable::IfLetKeyPathContainer
+}
+extension Resettable::Resettable.Resettable::IfLetWritableKeyPathContainer {
+ public func ifLet(_ keyPath: Swift::WritableKeyPath) -> Resettable::Resettable.Resettable::IfLetWritableKeyPathContainer
+ public func ifLet(_ keyPath: Swift::KeyPath) -> Resettable::Resettable.Resettable::IfLetKeyPathContainer
+}
+extension Resettable::Resettable.Resettable::IfLetKeyPathContainer {
+ public func ifLet(_ keyPath: Swift::ReferenceWritableKeyPath) -> Resettable::Resettable.Resettable::IfLetWritableKeyPathContainer
+ public func ifLet(_ keyPath: Swift::KeyPath) -> Resettable::Resettable.Resettable::IfLetKeyPathContainer
+}
+@usableFromInline
+@discardableResult
+internal func modification(of object: T, with action: (inout T) -> Swift::Void) -> T
+extension Resettable::Resettable : Swift::Identifiable where Base : Swift::Identifiable {
+ @inlinable public var id: Base.ID {
+ get { wrappedValue.id }
+ }
+ public typealias ID = Base.ID
+}
+extension Resettable::Resettable.Resettable::OperationBehavior : Swift::Equatable {}
+extension Resettable::Resettable.Resettable::OperationBehavior : Swift::Hashable {}
diff --git a/.agents/interfaces/macos/_ResettableDebugging.swiftinterface b/.agents/interfaces/macos/_ResettableDebugging.swiftinterface
new file mode 100644
index 0000000..279645e
--- /dev/null
+++ b/.agents/interfaces/macos/_ResettableDebugging.swiftinterface
@@ -0,0 +1,29 @@
+// swift-interface-format-version: 1.0
+// swift-compiler-version: Apple Swift version 6.4 (swiftlang-6.4.0.25.4 clang-2100.3.25.1)
+// swift-module-flags: -target arm64-apple-macos12.0 -enable-objc-interop -language-mode 6 -O -enable-experimental-feature DebugDescriptionMacro -module-name _ResettableDebugging -package-name swift_resettable
+// swift-module-flags-ignorable: -formal-cxx-interoperability-mode=off -interface-compiler-version 6.4
+import CustomDump
+@_exported import Resettable
+import Swift
+import _Concurrency
+import _StringProcessing
+import _SwiftConcurrencyShims
+extension Resettable::Resettable {
+ @available(*, deprecated, message: "Might be removed due to lack of Value type constraint, which makes the behavior unstable, consider re-implementing this feature locally if needed")
+ public struct ValuesDump {
+ @usableFromInline
+ internal init(items: [Resettable::Resettable.Resettable::Object], currentIndex: Swift::Int)
+ public let items: [Resettable::Resettable.Resettable::Object]
+ public let currentIndex: Swift::Int
+ }
+}
+extension Resettable::Resettable {
+ @available(*, deprecated, message: "Might be removed due to lack of Value type constraint, which makes the behavior unstable, consider re-implementing this feature locally if needed")
+ public func valuesDump() -> Resettable::Resettable._ResettableDebugging::ValuesDump
+ @inlinable public func dump() -> Swift::String {
+ var buffer = ""
+ self.dump(to: &buffer)
+ return buffer
+ }
+ public func dump(to stream: inout TargetStream) where TargetStream : Swift::TextOutputStream
+}
diff --git a/.github/workflows/generate-swiftinterface.yml b/.github/workflows/generate-swiftinterface.yml
new file mode 100644
index 0000000..a7a4239
--- /dev/null
+++ b/.github/workflows/generate-swiftinterface.yml
@@ -0,0 +1,120 @@
+name: Generate Swift interface
+run-name: Generate ${{ inputs.platform }} interfaces
+
+on:
+ workflow_dispatch:
+ inputs:
+ platform:
+ description: Normalized platform name (macos, ios, linux, windows, wasi, ...)
+ required: true
+ type: string
+ target_branch:
+ description: Branch that receives the generated-interface PR (defaults to the dispatched branch)
+ required: false
+ default: ""
+ type: string
+ workflow_call:
+ inputs:
+ platform:
+ required: true
+ type: string
+ target_branch:
+ required: true
+ type: string
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ generate:
+ name: Generate ${{ inputs.platform }}
+ runs-on: ${{ (inputs.platform == 'linux' || inputs.platform == 'wasi') && 'ubuntu-latest' || inputs.platform == 'windows' && 'windows-latest' || 'macos-latest' }}
+ env:
+ GH_TOKEN: ${{ github.token }}
+ INPUT_TARGET_BRANCH: ${{ inputs.target_branch }}
+ PLATFORM: ${{ inputs.platform }}
+ steps:
+ - name: Validate request
+ shell: bash
+ run: |
+ if [[ ! "$PLATFORM" =~ ^[a-z0-9][a-z0-9._-]*$ ]]; then
+ echo "Platform must be a normalized lowercase directory name: $PLATFORM" >&2
+ exit 1
+ fi
+
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ ref: ${{ inputs.target_branch != '' && inputs.target_branch || github.ref_name }}
+
+ - name: Set up Swift outside Xcode
+ if: runner.os != 'macOS'
+ uses: SwiftyLab/setup-swift@v1
+ with:
+ swift-version: "6"
+ sdks: ${{ inputs.platform == 'wasi' && 'wasm' || '' }}
+
+ - name: Prepare generation branch
+ shell: bash
+ run: |
+ target_branch="${INPUT_TARGET_BRANCH:-$GITHUB_REF_NAME}"
+ interface_branch="automation/swiftinterfaces/$GITHUB_RUN_ID/$PLATFORM"
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git switch -C "$interface_branch"
+ echo "TARGET_BRANCH=$target_branch" >> "$GITHUB_ENV"
+ echo "INTERFACE_BRANCH=$interface_branch" >> "$GITHUB_ENV"
+
+ - name: Select WASI SDK
+ if: inputs.platform == 'wasi'
+ shell: bash
+ run: |
+ swift_sdk="$(swift sdk list | sed -nE '/(_wasm|wasi)/ { s/^[[:space:]]*([^[:space:]]+).*/\1/p; q; }')"
+ if [[ -z "$swift_sdk" ]]; then
+ echo "No installed WASI Swift SDK was found" >&2
+ swift sdk list >&2 || true
+ exit 1
+ fi
+ echo "SWIFT_SDK=$swift_sdk" >> "$GITHUB_ENV"
+
+ - name: Generate interface
+ shell: bash
+ run: |
+ interface_dir=".agents/interfaces/$PLATFORM"
+ if [[ -d "$interface_dir" ]]; then
+ find "$interface_dir" -maxdepth 1 -type f -name '*.swiftinterface' -delete
+ fi
+ if [[ -n "${SWIFT_SDK:-}" ]]; then
+ make swiftinterface platform="$PLATFORM" swift_sdk="$SWIFT_SDK"
+ else
+ make swiftinterface platform="$PLATFORM"
+ fi
+
+ - name: Commit and push interface
+ id: commit
+ shell: bash
+ run: |
+ git add --all -- ".agents/interfaces/$PLATFORM"
+ if git diff --cached --quiet; then
+ echo "changed=false" >> "$GITHUB_OUTPUT"
+ echo "No $PLATFORM interface changes" >> "$GITHUB_STEP_SUMMARY"
+ exit 0
+ fi
+ git commit -m "Update $PLATFORM Swift interfaces"
+ git push --force-with-lease --set-upstream origin "$INTERFACE_BRANCH"
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+
+ - name: Open platform PR
+ if: steps.commit.outputs.changed == 'true'
+ shell: bash
+ run: |
+ pr_url="$(gh pr list --state open --base "$TARGET_BRANCH" --head "$INTERFACE_BRANCH" --json url --jq '.[0].url')"
+ if [[ -z "$pr_url" ]]; then
+ pr_url="$(gh pr create \
+ --base "$TARGET_BRANCH" \
+ --head "$INTERFACE_BRANCH" \
+ --title "chore: update $PLATFORM Swift interfaces" \
+ --body "Generated from \`$TARGET_BRANCH\` by workflow run $GITHUB_RUN_ID. Merge this PR into the aggregation branch after reviewing the platform-specific API snapshot.")"
+ fi
+ echo "$pr_url" >> "$GITHUB_STEP_SUMMARY"
diff --git a/.github/workflows/update-swiftinterfaces.yml b/.github/workflows/update-swiftinterfaces.yml
new file mode 100644
index 0000000..27fc7c9
--- /dev/null
+++ b/.github/workflows/update-swiftinterfaces.yml
@@ -0,0 +1,85 @@
+name: Update Swift interfaces
+run-name: Update Swift interfaces from ${{ github.ref_name }}
+
+on:
+ workflow_dispatch:
+ inputs:
+ platforms:
+ description: JSON array of independently generated platforms
+ required: true
+ default: '["macos","ios","tvos","watchos","maccatalyst"]'
+ type: string
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ prepare:
+ name: Create aggregation branch
+ runs-on: ubuntu-latest
+ outputs:
+ update_branch: ${{ steps.branch.outputs.update_branch }}
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ ref: ${{ github.ref_name }}
+
+ - name: Push aggregation branch
+ id: branch
+ shell: bash
+ run: |
+ update_branch="automation/swiftinterfaces/$GITHUB_RUN_ID"
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git switch -C "$update_branch"
+ git commit --allow-empty -m "Prepare Swift interface update"
+ git push --force-with-lease --set-upstream origin "$update_branch"
+ echo "update_branch=$update_branch" >> "$GITHUB_OUTPUT"
+
+ generate:
+ name: Generate ${{ matrix.platform }}
+ needs: prepare
+ strategy:
+ fail-fast: false
+ matrix:
+ platform: ${{ fromJSON(inputs.platforms) }}
+ permissions:
+ contents: write
+ pull-requests: write
+ uses: ./.github/workflows/generate-swiftinterface.yml
+ with:
+ platform: ${{ matrix.platform }}
+ target_branch: ${{ needs.prepare.outputs.update_branch }}
+
+ finalize:
+ name: Open draft aggregation PR
+ if: always() && needs.prepare.result == 'success'
+ needs:
+ - prepare
+ - generate
+ runs-on: ubuntu-latest
+ env:
+ GH_TOKEN: ${{ github.token }}
+ TARGET_BRANCH: ${{ github.ref_name }}
+ UPDATE_BRANCH: ${{ needs.prepare.outputs.update_branch }}
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ ref: ${{ github.ref_name }}
+
+ - name: Open draft PR
+ shell: bash
+ run: |
+ pr_url="$(gh pr list --state open --base "$TARGET_BRANCH" --head "$UPDATE_BRANCH" --json url --jq '.[0].url')"
+ if [[ -z "$pr_url" ]]; then
+ pr_url="$(gh pr create \
+ --draft \
+ --base "$TARGET_BRANCH" \
+ --head "$UPDATE_BRANCH" \
+ --title "chore: update platform Swift interfaces" \
+ --body "Aggregation PR for workflow run $GITHUB_RUN_ID. Merge the successful platform PRs into \`$UPDATE_BRANCH\`; failed platform jobs remain visible in the workflow and can be fixed or intentionally ignored. Matrix result: \`${{ needs.generate.result }}\`.")"
+ fi
+ echo "$pr_url" >> "$GITHUB_STEP_SUMMARY"
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c9eab41
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,7 @@
+output ?= .agents/interfaces
+platform ?= host
+swift_sdk ?=
+
+.PHONY: swiftinterface
+swiftinterface:
+ @./scripts/generate-swiftinterfaces.sh --package-path . --output "$(output)" --platform "$(platform)" $(if $(swift_sdk),--swift-sdk "$(swift_sdk)")
diff --git a/scripts/generate-swiftinterfaces.sh b/scripts/generate-swiftinterfaces.sh
new file mode 100755
index 0000000..b028e55
--- /dev/null
+++ b/scripts/generate-swiftinterfaces.sh
@@ -0,0 +1,364 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
+package_path="."
+output_path=".agents/interfaces"
+configuration="release"
+include_tests=false
+list_declared_platforms=false
+platform=""
+triple=""
+sdk=""
+swift_sdk=""
+scratch_path=""
+declare -a selected_targets=()
+declare -a build_arguments=()
+
+usage() {
+ sed -n '1,$p' <<'USAGE'
+Generate public Swift interfaces for one SwiftPM platform.
+
+Usage:
+ generate-swiftinterfaces.sh --platform [options]
+ generate-swiftinterfaces.sh --list-declared-platforms [options]
+
+Options:
+ --package-path Package root (default: .)
+ --output Platform-parent destination, relative to the package root
+ unless absolute (default: .agents/interfaces)
+ --platform Output/build platform, for example macos, ios, linux,
+ windows, or wasi; host resolves to the current platform
+ --list-declared-platforms Print the Apple/custom platforms declared by Package.swift
+ --configuration debug or release (default: release)
+ --include-tests Include Swift test targets
+ --target Generate one target; repeat to select several
+ --triple Forward a target triple to SwiftPM
+ --sdk Forward an SDK path to SwiftPM
+ --swift-sdk Forward an installed Swift SDK name to SwiftPM
+ --scratch-path Use a specific SwiftPM scratch directory
+ --build-arg Forward one additional argument to `swift build`; repeatable
+ -h, --help Show this help
+
+The platform name becomes an output directory. For example, --platform ios writes
+