This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Build
swift build # Debug build
swift build -c release # Release build
# Test
swift test # All tests
swift test --filter 'AppTests' # Tests matching a pattern
swift test --enable-code-coverage # With coverage
# Run
swift run asc <args>
make run ARGS="apps list"Three strict layers with a unidirectional dependency flow: ASCCommand → Infrastructure → Domain
Sources/
├── Domain/ # Pure value types, @Mockable protocols — zero I/O
├── Infrastructure/# Implements Domain protocols via appstoreconnect-swift-sdk
└── ASCCommand/ # CLI entry point, output formatting, TUI
All models are public struct + Sendable + Equatable + Codable. The JSON encoding is the public schema. Models with optional text fields use custom Codable with encodeIfPresent to omit nil values from JSON output.
Design rules:
- Every model carries its parent ID (e.g.
AppStoreVersion.appId,AppScreenshot.setId) — the App Store Connect API doesn't return parent IDs, so Infrastructure injects them - State enums expose semantic booleans (
isLive,isEditable,isPending,isComplete) for agent decision-making - All repositories and providers are
@Mockableprotocols
Adapts appstoreconnect-swift-sdk to Domain protocols. The critical pattern: mappers always inject the parent ID from the request parameter into every mapped response object.
ASC.swift—@mainentry, registers all subcommandsGlobalOptions.swift—--output(default: json),--pretty,--timeoutOutputFormatter.swift— JSON/table/markdown rendering;formatAgentItems()merges affordancesClientProvider.swift— factory wiring auth → authenticated repositories
CLI equivalent of REST HATEOAS. Every response includes an affordances field with ready-to-run CLI commands so an AI agent can navigate without knowing the command tree. Affordances are state-aware — e.g. submitForReview only appears when isEditable == true.
All domain models implement AffordanceProviding:
protocol AffordanceProviding {
var affordances: [String: String] { get }
}OutputFormatter.formatAgentItems() merges affordances into the encoded JSON output.
Commands mirror the App Store Connect API hierarchy exactly:
App → AppStoreVersion → AppStoreVersionLocalization → AppScreenshotSet → AppScreenshot
App → AppInfo → AppInfoLocalization
App → AppInfo → AgeRatingDeclaration
AppCategory (top-level, not nested under App)
App → CustomerReview → CustomerReviewResponse
App → Build → BetaBuildLocalization
App → BuildUpload
App → TestFlight (BetaGroup → BetaTester)
App → CiProduct (XcodeCloud) → CiWorkflow → CiBuildRun
AppStoreVersion → VersionReadiness
AppStoreVersion → AppStoreReviewDetail
CodeSigning: BundleID → Profile
App → PerformanceMetric (via perfPowerMetrics)
Build → PerformanceMetric (via perfPowerMetrics)
Build → DiagnosticSignatureInfo → DiagnosticLogEntry
Domain folders are nested to mirror the resource hierarchy:
Domain/
├── Apps/ → App, AppRepository
│ ├── Versions/ → AppStoreVersion, AppStoreVersionState, VersionReadiness,
│ │ │ VersionRepository, ReviewDetailRepository,
│ │ │ AppStoreReviewDetail, ReviewDetailUpdate
│ │ └── Localizations/ → AppStoreVersionLocalization, VersionLocalizationRepository
│ │ └── ScreenshotSets/ → AppScreenshotSet, ScreenshotDisplayType, ScreenshotRepository
│ │ └── Screenshots/ → AppScreenshot
│ ├── AppInfos/ → AppInfo, AppInfoLocalization, AppInfoRepository,
│ │ AppCategory, AppCategoryRepository,
│ │ AgeRatingDeclaration, AgeRatingDeclarationRepository
│ ├── Reviews/ → CustomerReview, CustomerReviewResponse, ReviewResponseState,
│ │ CustomerReviewRepository
│ ├── Builds/ → Build, BuildUpload, BetaBuildLocalization,
│ │ BuildRepository, BuildUploadRepository, BetaBuildLocalizationRepository
│ ├── Pricing/ → PricingRepository
│ ├── TestFlight/ → BetaGroup, BetaTester, TestFlightRepository
│ └── Performance/ → PerformanceMetric, PerformanceMetricCategory, DiagnosticSignatureInfo,
│ DiagnosticType, DiagnosticLogEntry, PerfMetricsRepository, DiagnosticsRepository
├── CodeSigning/ → BundleID, Certificate, Device, Profile + their repositories
│ ├── BundleIDs/ → BundleID, BundleIDRepository
│ ├── Certificates/ → Certificate, CertificateRepository
│ ├── Devices/ → Device, DeviceRepository
│ └── Profiles/ → Profile, ProfileRepository
├── Submissions/ → ReviewSubmission, ReviewSubmissionState, SubmissionRepository
├── Auth/ → AuthCredentials, AuthProvider, AuthStatus, AuthStorage, CredentialSource, AuthError
├── Projects/ → ProjectConfig, ProjectConfigStorage
├── Skills/ → Skill, SkillCheckResult, SkillConfig, SkillRepository, SkillConfigStorage
└── Shared/ → AffordanceProviding, APIError, OutputFormat, PaginatedResponse
Infrastructure and test folders mirror this exact structure.
asc init saves the app ID, name, and bundle ID to .asc/project.json in the current directory:
asc init # auto-detect from *.xcodeproj bundle ID
asc init --name "X" # search by name
asc init --app-id <id>FileProjectConfigStorage (Infrastructure) reads/writes .asc/project.json relative to cwd. ProjectConfig (Domain) carries appId, appName, bundleId + CAEOAS affordances.
We follow the Chicago School of TDD — state-based, not interaction-based. Tests should verify what domain objects return and compute, rather than how they call their collaborators.
ALWAYS write tests first, then implement. Never write implementation code without a failing test. This is non-negotiable.
- If code is difficult to test, treat that as a design problem, not an exception to testing.
- The proper TDD workflow:
- Think from user's mental model: How would the user describe this behavior? What would they expect to see? For example: "a version is live when its state is readyForSale", "submit is only available when the version is editable".
- Write the test: Name it after the user's expectation. Assert the exact output values (e.g.
"IOS","READY_FOR_SALE","expired": true). - Run the test: It must fail (red). If it passes, the test is not testing new behaviour.
- Implement: Write just enough code to make the test pass (green).
- Refactor: Clean up while keeping tests green.
- Test cases should reflect the user's mental model — describe what the user sees and expects, not internal implementation details.
- Never modify a test to make it pass — if a test fails unexpectedly, the specification (step 1) was wrong. Fix the thinking, not the test.
- If you find yourself writing implementation before tests, stop and reverse course.
- Framework: Apple's
@Testingmacro (not XCTest) - Mocking:
@Mockableannotation on protocols +given().willReturn()in tests - Test naming: backtick style —
func `version is live when state is readyForSale`() Tests/DomainTests/TestHelpers/MockRepositoryFactory.swift— shared test data factory
The codebase has two distinct localization concepts with separate repositories:
| Type | Domain folder | Repository | Commands | Data |
|---|---|---|---|---|
AppStoreVersionLocalization |
Domain/Localizations/ |
VersionLocalizationRepository |
asc version-localizations * |
whatsNew, description, keywords, screenshots |
AppInfoLocalization |
Domain/AppInfos/ |
AppInfoRepository |
asc app-info-localizations * |
name, subtitle, privacyPolicyUrl, privacyChoicesUrl, privacyPolicyText |
ScreenshotRepository (in Domain/ScreenshotSets/) handles screenshot sets and screenshot images — no localization methods.
After every code change — new feature, improvement, or bug fix — update all affected docs before considering the task done.
| Change type | Files to update |
|---|---|
| New feature / command | docs/features/<feature>.md (create), CHANGELOG.md ([Unreleased]), README.md (feature list + CLI examples), skills/ (relevant skill files) |
| Improvement / enhancement | docs/features/<feature>.md (update affected sections), CHANGELOG.md ([Unreleased]) |
| Bug fix | CHANGELOG.md ([Unreleased]) |
| Architecture / API change | CLAUDE.md (update architecture / patterns sections), docs/features/<feature>.md |
| Auth / config change | CLAUDE.md (Authentication section), README.md |
docs/features/<feature>.md — write from actual code (read files first, never from memory). Structure:
- CLI Usage — flags table + examples + output samples (json + table)
- Typical Workflow — end-to-end bash script showing the happy path
- Architecture — three-layer ASCII diagram + dependency note
- Domain Models — every public struct/enum/protocol with fields, computed properties, affordances
- File Map —
Sources/andTests/trees + wiring files table - API Reference — endpoint → SDK call → repository method
- Testing — representative test snippet +
swift testcommand - Extending — natural next steps with stub code
CHANGELOG.md — add entry under [Unreleased] using Keep a Changelog format:
### Addedfor new features/commands### Changedfor improvements to existing behaviour### Fixedfor bug fixes
README.md — update the feature/command table and any usage examples that changed.
skills/<feature> — always use the /skill-creator skill to create or update feature skills
Key skills to keep in sync:
implement-feature/SKILL.md— workflow + checklistasc-cli/references/commands.md— command reference- Feature-specific skills (
asc-testflight,asc-beta-review,asc-builds-upload,asc-code-signing,asc-check-readiness,asc-app-previews,asc-app-shots,asc-review-detail,asc-plugins, etc.)
CLAUDE.md — update when architecture patterns, file locations, or design rules change.
Option A — Persistent login (recommended):
asc auth login --key-id <id> --issuer-id <id> --private-key-path ~/.asc/AuthKey_XXXXXX.p8 [--vendor-number <number>]
asc auth update --vendor-number <number> # add vendor number to existing account
asc auth logout # remove saved credentials
asc auth check # verify credentials; shows source: "file" or "environment"Credentials saved to ~/.asc/credentials.json. Vendor number (optional) is used by sales-reports and finance-reports commands — auto-resolved from the active account when --vendor-number is omitted.
Option B — Environment variables:
export ASC_KEY_ID="YOUR_KEY_ID"
export ASC_ISSUER_ID="YOUR_ISSUER_ID"
export ASC_PRIVATE_KEY_PATH="~/.asc/AuthKey_XXXXXX.p8"
# OR use ASC_PRIVATE_KEY with the PEM content directlyResolution order: ~/.asc/credentials.json → environment variables, handled by CompositeAuthProvider in Infrastructure. EnvironmentAuthProvider is the fallback.