From 8f83af77da72c7f9161f8bd521404727fb7c8397 Mon Sep 17 00:00:00 2001 From: slamhan Date: Wed, 23 Sep 2026 18:30:02 +0800 Subject: [PATCH 01/14] docs: add documentation design (Skill-style progressive disclosure) Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/{desgin.md => design.md} | 0 docs/documentation-design/README.md | 240 +++++++++++++++++++++ docs/documentation-design/layout.md | 309 ++++++++++++++++++++++++++++ 3 files changed, 549 insertions(+) rename docs/{desgin.md => design.md} (100%) create mode 100644 docs/documentation-design/README.md create mode 100644 docs/documentation-design/layout.md diff --git a/docs/desgin.md b/docs/design.md similarity index 100% rename from docs/desgin.md rename to docs/design.md diff --git a/docs/documentation-design/README.md b/docs/documentation-design/README.md new file mode 100644 index 00000000..805e0326 --- /dev/null +++ b/docs/documentation-design/README.md @@ -0,0 +1,240 @@ +--- +description: How asc-cli docs are layered (Skill-style progressive disclosure), where each kind of content lives, and the limits CI enforces. Read before adding or restructuring any doc. +--- + +# Documentation Design + +> Status: **proposed**, 2026-09-23. Not implemented yet. See [Migration](#migration). + +## Goal + +**Reduce accidental complexity so docs are easy to change.** + +Every other rule in this doc follows from that goal. Each fact has exactly one home, so a code change touches at most one doc. When choosing between two options, pick the one where *the next change touches fewer places*. + +## Problem + +| File | Today | Accidental complexity | +|---|---|---| +| `README.md` | 727 lines | Hand-written command reference (~400 lines) repeats `docs/features/*`, so one flag change means two edits | +| `CHANGELOG.md` | 1049 lines | Bullets average 750–1,575 chars and name internal types, file paths and SDK calls, which repeats the PR | +| `docs/features/*.md` | 45 files, ~16.9k lines | Architecture, File Map, Domain Models, Testing and Extending (~16.5k lines) restate the source and go stale when code changes | +| `CLAUDE.md` | 277 lines, loaded every session | The domain folder tree repeats `ls Sources/Domain`; feature-only checklists are loaded on every task | +| Links | 7 broken | Nothing checks them | + +**Root cause:** the doc rules make every feature copy the same facts into several places (README list + README examples + 9-section doc + long changelog bullet). Copies have to be kept in sync by hand, and nothing checks that they are. + +## Principle: docs load like Skills + +Agent Skills stay cheap by loading content in three tiers, where each tier only *points* to the next: + +| Skill tier | Loaded | asc-cli docs equivalent | +|---|---|---| +| **1. Metadata** (`name` + `description`) | Always | `README.md`, `CLAUDE.md`, the `docs/README.md` index, CHANGELOG lines | +| **2. Instructions** (`SKILL.md` body) | When needed | `docs/features//README.md`: how to use one feature | +| **3. Resources** (`references/`, `scripts/`) | On demand, by link | Topic files next to it in `docs/features//`, generated command reference | + +Rules: + +1. **One home per fact; tiers link, they don't copy.** Any copy is a future inconsistency. +2. **Don't write what code or `--help` already says.** Flags, file trees and model fields are read from the source, like Skill scripts that run outside the context window. A doc can't go stale about something it doesn't contain. +3. **Same shape everywhere; nothing empty.** Every feature gets the same folder shape, but no empty subfolders and no metadata fields that nothing reads. + +The CLI already does this at runtime: CAEOAS affordances show only the next legal commands. The docs follow the same shape. + +## Layout + +``` +README.md tier 1: pitch, install, quick start, feature table → links +CLAUDE.md tier 1: agent rules only (TDD gate, layers, pointers) +CONTRIBUTING.md tier 1: build, test, where things go → links +CHANGELOG.md tier 1: [Unreleased] + current minor only, one line per change → links +docs/ +├── README.md tier 1: index, GENERATED from feature descriptions +├── documentation-design/ this design (README.md) + layout.md +├── design.md CAEOAS + architecture reasons (renamed from desgin.md) +├── library.md use as a Swift package (moved from README) +├── release.md end-to-end release workflow (moved from README) +├── commands.md tier 3: GENERATED from swift-argument-parser, never hand-edited +├── changelog/ tier 3: one file per past minor, moved as-is, never edited +│ ├── 0.17.md +│ └── 0.1.md +└── features/ one folder per feature, shaped like a Skill + ├── testflight/ + │ └── README.md tier 2: description + user guide (≤200 lines); most features stop here + └── iap-subscriptions/ + ├── README.md tier 2 + ├── pricing.md tier 3: topic files, added only when README outgrows 200 lines + ├── offer-codes.md + └── assets/ only if the feature has images +``` + +File-by-file detail, written from the reader's side with a mockup of each file: [layout.md](layout.md). + +Every feature is a folder with a `README.md`, in the same way every skill is a folder with a `SKILL.md`: + +- **One predictable path.** A feature's docs are always at `docs/features//`, next to its skill at `skills/asc-/`. Readers and agents never have to check whether a feature is a file, a folder or both. +- **Growing never moves anything.** A deep dive becomes a new file next to `README.md`. The feature doc's own path and links stay the same. +- **GitHub shows it on open.** Browsing `docs/features/testflight/` renders the README. +- **Topic files sit next to `README.md`, with no `references/` level.** That matches the `pdf` skill (`forms.md` and `reference.md` next to `SKILL.md`). `assets/` only exists when there are images. + +## Tier 1: metadata + +### Frontmatter: one field + +```yaml +--- +description: Manage beta groups and testers, import/export CSV, submit builds for beta review. Use when distributing a build to testers. +--- +``` + +- `description` works like a Skill's: one sentence saying *what* and *when*, ≤250 chars. +- There is no `name` field, because the filename is the name. There are no other fields until something reads them. +- `make docs` builds `docs/README.md` from these lines. Adding a feature means writing one file, and the index updates itself. + +### README.md (≤150 lines) + +Keeps: logo/badges, one-paragraph pitch, install, 4-command quick start, one CAEOAS JSON example, the Features table (each row links to its feature doc), links to Docs index / Contributing / Changelog, Sponsors, License. + +| Section today | New home | +|---|---| +| Command Reference (~400 lines) | Removed; `docs/commands.md` is generated | +| Optional iris sign-in | `docs/features/iris/README.md` | +| Authentication details | `docs/features/asc-auth/README.md` | +| Release Workflow | `docs/release.md` | +| Feature Guides list | `docs/README.md` (generated) | +| Use as a Swift Package | `docs/library.md` | +| Development | `CONTRIBUTING.md` | + +### CLAUDE.md (≤100 lines) + +Keeps: the TDD gate, build/test commands, the three-layer rule, a one-paragraph CAEOAS summary, and pointers. + +- **Moves to the `implement-feature` skill** (loaded only while building a feature): the REST exposure checklist, doc-update rules, domain model design rules. +- **Deleted** because the code already says it: the domain folder tree and the resource hierarchy list. + +### CHANGELOG.md + +Answers the upgrader's questions in order: *will this break me → is my bug fixed → what's new*. Going forward: + +- `Removed` / `Changed` come before `Added`; anything incompatible starts with `Breaking:` and says what to use instead. +- Each bullet starts with what the user types, says the effect rather than the implementation, and is ≤300 chars. Related fixes are merged into one bullet. +- Links point only to files that already exist. The changelog never gets detail files of its own: + + | Link | Points to | Reader gets | When | + |---|---|---|---| + | `→ [docs](docs/features//README.md#
)` | The feature doc, deep-linked to the section (e.g. `#gotchas`) | How to use it now | `Added`, `Changed`, `Breaking` — or a fix with a Gotcha to read | + | `([#123](https://github.com/tddworks/asc-cli/pull/123))` | The PR | Why and how it changed: types, SDK calls, tests | Every bullet | + + The PR link is where the implementation detail that used to fill bullets now lives. URLs don't count toward the 300 chars. +- REST is mentioned only when a route is new, changed or removed. + +```markdown +### Added +- `asc experiments`, `experiment-treatments` and `experiment-treatment-localizations` create, start and stop product-page A/B tests. REST: `/api/v1/apps/:appId/experiments`. → [docs](docs/features/product-page-optimization/README.md) ([#NN](https://github.com/tddworks/asc-cli/pull/NN)) +``` + +Real before → after rewrites are in [layout.md](layout.md#changelogmd). + +Type names, repository methods, SDK calls and file paths go in the PR, their one home, reached from the bullet's PR link. + +#### Size: the current minor stays, older minors roll off + +`CHANGELOG.md` is tier 1, so it has to stay small. Humans only read the top, but an agent adding an entry loads the whole file, and at 1,049 lines that means paying for 0.1.0 on every change. + +- `CHANGELOG.md` holds `[Unreleased]` plus the **current minor** (today 0.18.x, ~50 lines), then an "Older releases" list linking each `docs/changelog/.md`. +- **When a new minor is released** (0.18 → 0.19), the 0.18.x sections move unchanged into `docs/changelog/0.18.md` and one link is added. This happens once per minor, not every release, and `scripts/changelog-rollover.py`, run by `scripts/promote-changelog.sh` in the release workflow, does it, so nobody decides anything. +- **Archived sections are moved, never rewritten.** Past entries keep their original wording, and one version's history lives in exactly one place. +- Git history and GitHub Releases keep everything too. The archive is for people who browse, not a second source of truth. + +The initial split turns today's 1,049 lines into ~60 in `CHANGELOG.md`, with `docs/changelog/0.17.md`, `0.16.md` and `0.1.md` holding the rest. The one mis-numbered heading, `0.1.74` (dated between 0.17.3 and 0.17.5, so really 0.17.4), goes into `0.17.md` with its heading unchanged. The 0.1.x line ends at 0.1.68, after which numbering continued as 0.16.9. + +## Tier 2: feature doc (≤200 lines) + +Written for someone *using* the feature, human or agent. It contains only what `--help` and the code can't say: + +1. **Title + one-line summary** +2. **Quick start**: the 2–4 commands of the happy path +3. **Workflows**: end-to-end scripts for common jobs, with a JSON sample only where the shape isn't obvious +4. **REST endpoints**: path table + CLI flag → query param mapping + one curl example +5. **Gotchas**: Apple API limits, iris requirements, state rules ("`start` only appears when approved"), why a workaround exists +6. **See also**: `docs/commands.md#` for all flags, topic files in the same folder, related features + +There are **no flag tables**: flags change with the code, and `docs/commands.md` is generated from it. Also none of the following: +- architecture diagrams, which live once in `docs/design.md` +- file maps +- domain model field lists +- test snippets +- "Extending" stubs + +If a README outgrows 200 lines, move a topic into `docs/features//.md` next to it and link to it. + +## Tier 3: resources + +- **`docs/commands.md`**: generated by `make docs` from `asc --experimental-dump-help` (or the `generate-manual` plugin). It is the only complete flag list. `skills/asc-cli/references/commands.md` links to it instead of copying it. +- **`docs/features//.md`**: deep dives that are too long for the feature doc, such as IAP pricing or the iris SRP flow. +- **Deleted, not moved**: File Map, Domain Models, API Reference tables, Testing, Extending. The code is their one home. Anyone reading at that depth is already in the source. + +## Reader paths + +| Reader | Path | +|---|---| +| New user | README → quick start | +| Feature user | README table / `docs/README.md` → feature doc | +| Needs every flag | feature doc → `docs/commands.md` or `asc --help` | +| Agent using the CLI | Skill → feature doc → CAEOAS affordances at runtime | +| Contributor | CONTRIBUTING → `docs/design.md` → code | +| Agent building a feature | CLAUDE.md → `implement-feature` skill → its references | + +## Update rules (replace the table in CLAUDE.md) + +| Change | Touch | Nothing else | +|---|---|---| +| New feature | `docs/features//README.md` with a `description`, one CHANGELOG line, `make docs` | README only if it's a new Features-table category | +| New or changed flag | Nothing; `make docs` regenerates `commands.md` | | +| Improvement | The affected feature doc section, one CHANGELOG line | | +| Bug fix | One CHANGELOG line; a Gotchas entry if users could hit it again | | +| Architecture rule change | `docs/design.md`; CLAUDE.md only if agents must follow it on every task | | + +Skills (the `skills/` submodule) follow the same rule: they carry agent workflow and link to feature docs instead of copying command tables. + +## Enforcement + +`scripts/check-docs.py` in CI (`--strict` fails the build). It is kept small because it is code that also has to be maintained. + +| Check | Limit | +|---|---| +| Line budgets | README ≤150, CLAUDE.md ≤100, `docs/features/*/README.md` ≤200 | +| CHANGELOG bullet length in `[Unreleased]` | ≤300 chars | +| `CHANGELOG.md` holds only `[Unreleased]` + one minor | fails once a second minor appears (rollover forgotten) | +| Every `docs/features/*/README.md` has a `description` | required | +| Relative links resolve (links inside code fences are skipped, since mockups use future paths) | all `.md` files | +| Generated files are current | `make docs && git diff --exit-code` | + +When a doc goes over budget, split it. Don't raise the limit. + +## Migration + +Each step is its own PR, and the docs stay valid after each one. + +1. **Hygiene**: fix the 7 broken links, rename `desgin.md` → `design.md`, split `CHANGELOG.md` into the current minor + `docs/changelog/`, add `scripts/check-docs.py` in report-only mode. +2. **Generation**: `make docs` builds `docs/commands.md` and `docs/README.md`. +3. **Tier 1**: slim README; add `CONTRIBUTING.md`, `docs/library.md`, `docs/release.md`. +4. **Move**: `git mv docs/features/.md docs/features//README.md` for all 45 features, merging into the existing `iap-subscriptions/` and `iris/` folders. A script rewrites the links, and `scripts/check-docs.py` proves none broke. This is one mechanical PR with no content changes, so it's easy to review. +5. **Trim**: add `description`, delete flag tables and internals sections, move leftover design reasons into Gotchas. This can be parallelised per feature. +6. **CLAUDE.md**: move checklists into the `implement-feature` skill and replace the update-rules table. +7. **Enforce**: turn the CI checks from report-only into failing. + +## Decisions + +Each one is judged by the goal: *does the next change touch fewer places?* + +| Question | Decision | Why | +|---|---|---| +| Internals sections (File Map, Domain Models, Testing, Extending) | **Delete** | They copy the code, so every refactor would need a doc edit that nobody makes | +| Flag tables in feature docs | **Delete; generate `docs/commands.md`** | Flags change with the code; generated means zero manual edits | +| Old CHANGELOG entries | **Keep the wording; new style going forward** | Rewriting history is work with no gain for future changes | +| One CHANGELOG file vs rolling archive | **Current minor in `CHANGELOG.md`, older minors in `docs/changelog/.md`** | Tier 1 must stay small for readers and agents. The rollover is one scripted move per minor, each version still lives in exactly one file, and CI catches a forgotten rollover. | +| Folder per feature vs flat file | **Folder per feature, `README.md` inside; no empty subfolders** | The rename happens once and is checked by CI. After that, every feature has one path and growing never moves a file. Mixing flat files with sibling folders would spread a feature over two places forever. | +| Frontmatter fields | **`description` only** | Every extra field has to be kept correct; the filename is already the name | +| Hand-written vs generated index | **Generated** | Adding a feature touches one file instead of two | diff --git a/docs/documentation-design/layout.md b/docs/documentation-design/layout.md new file mode 100644 index 00000000..ef935fbb --- /dev/null +++ b/docs/documentation-design/layout.md @@ -0,0 +1,309 @@ +--- +description: File-by-file detail of the documentation layout, written from the reader's side. For each file: who opens it, the question they bring, a mockup, and what stays out. +--- + +# Layout in Detail + +Companion to [the design](README.md). Each file below is described from the **reader's side**: who opens it, what question they have, and what they should see. Anything that doesn't answer that question belongs in a different file. + +## Which file answers my question? + +| I am… | …and I want to know | I open | +|---|---|---| +| Evaluating the tool | What is this? Can I try it in a minute? | `README.md` | +| Upgrading | What changed? Will my scripts break? | `CHANGELOG.md` | +| Looking for a feature | Does asc do X? Where is it documented? | `docs/README.md` | +| Using a feature | How do I do the job? What will bite me? | `docs/features//README.md` | +| Writing a script | What is the exact flag name? | `docs/commands.md` or `asc --help` | +| Scripting a release | What's the end-to-end order? | `docs/release.md` | +| Embedding in Swift | How do I use it as a package? | `docs/library.md` | +| Contributing | How do I build, test, and where does code go? | `CONTRIBUTING.md` → `docs/design.md` | +| An agent working in this repo | What must I never do? | `CLAUDE.md` | + +--- + +## `CHANGELOG.md` + +**Who:** someone upgrading from version N to N+k, a script owner after CI broke, or someone checking whether their bug is fixed. + +**Their questions, in order:** +1. **Will this break me?** They need to see anything removed or changed incompatibly first, and what to do about it. +2. **Is my bug fixed?** They skim for the command they use. +3. **What's new that I could use?** +4. **Where do I learn more?** One link. + +They don't care which Swift type, repository method or SDK call changed; that's for reviewers and belongs in the PR. + +### Rules + +| Rule | Why (reader's view) | +|---|---| +| Keep a Changelog headings; **`Removed` and `Changed` come before `Added`** | Question 1 is answered before anything else | +| Anything that breaks existing use starts with **`Breaking:`** and says what to do instead | Script owners can grep for "Breaking" across versions | +| Each bullet starts with **what the user types** (`` `asc builds list` ``, `` `GET /api/v1/…` ``) | They're scanning for *their* command | +| Say the **effect**, not the implementation: "returns every page", not "uses `requestAllPages`" | They can check the effect; they can't check the implementation | +| One bullet per user-visible change, **≤300 chars**; merge related fixes into one bullet | Nine one-line bullets about pagination are the same news | +| Every bullet ends with its PR link; bullets that change how you use asc also get `→ [docs](…#section)` | Two ways down: *how do I use it* (feature doc) and *why did it change* (PR). No changelog-only files. | +| Mention REST only when a route is **new, changed or removed** | Every feature has REST; saying so every time is noise | +| `## [Unreleased]` is always at the top; releasing means renaming that heading | One edit per release | +| Only the current minor lives here; each past minor is in `docs/changelog/.md`, linked under "Older releases" | The file an upgrader or agent opens stays ~50–150 lines. Anyone upgrading across minors follows one link per minor. | + +### Before → after (real entries) + +**Before (0.17.8):** a removed REST route is hidden at the end of an 1,100-char `Added` bullet: + +> - **`asc iris iap-submissions delete --submission-id `** — iris-side dequeue of an IAP submission (`DELETE /iris/v1/inAppPurchaseSubmissions/{id}`). Iris-queued submissions don't round-trip through the public-SDK delete; they can only be removed via this iris-cookie-authed path. New `IrisClient.delete`, new `IrisInAppPurchaseSubmissionRepository.deleteSubmission(session:submissionId:)`. REST equivalent: … `RESTPathResolver.resourcePath` now converts spaces … Removed the short-lived public-SDK alias `POST /api/v1/iap/:iapId/unsubmit` (incorrect — …). + +A script calling `POST /api/v1/iap/:iapId/unsubmit` broke, and the only notice was the last sentence of a bullet about something else. + +**After:** + +```markdown +## [0.17.8] - 2026-04-29 + +### Removed +- **Breaking:** `POST /api/v1/iap/:iapId/unsubmit` is gone — it never worked for iris-queued submissions. Use `DELETE /api/v1/iris/iap-submissions/:id`. + +### Added +- `asc iris iap-submissions delete --submission-id ` removes an IAP from the next version's review queue; the `removeFromNextVersion` affordance now runs it. → [docs](docs/features/iap-subscriptions/submission-iris-parity.md) ([#PR](…)) +``` + +**Before (0.18.4):** six `Fixed` bullets, about 3,900 chars, full of `limit=200`, `SubscriptionPriceInlineCreate` and `APIClient.requestAllPages`. + +**After:** + +```markdown +## [0.18.4] - 2026-09-23 + +### Fixed +- List commands no longer stop at Apple's first 20–50 results — e.g. `builds uploads list` (50 → 122 on a real account), `bundle-ids list`, `profiles list`, and every per-territory price list. +- `asc subscriptions prices set-batch` sends one all-or-nothing request instead of one per territory (175 for a full price list), so a failure no longer leaves prices half-applied. +- `asc subscription-price-schedule get` shows the correct custom price for every territory, not just the first 50. +- Offer `prices list` commands now include each price's territory and price point. +- `asc profiles list` shows each profile's bundle ID again. +``` + +Same news for the reader, about a quarter of the length. The implementation detail isn't lost: each bullet's PR link leads to it. + +**Old entries:** wording left as it is and moved unchanged to `docs/changelog/.md`. The new style applies going forward (see Decisions). + +### What the file looks like + +```markdown +# Changelog +Format: Keep a Changelog · Versioning: SemVer + +## [Unreleased] + +## [0.18.4] - 2026-09-23 +### Fixed +- List commands no longer stop at Apple's first 20–50 results — … ([#NN](…)) + +## [0.18.3] - 2026-09-22 +… +## [0.18.0] - 2026-05-13 +… + +## Older releases +[0.17](docs/changelog/0.17.md) · [0.16](docs/changelog/0.16.md) · [0.1](docs/changelog/0.1.md) +``` + +--- + +## `README.md` (≤150 lines) + +**Who:** someone who found the repo, from a link, Homebrew search or an agent recommendation. +**Their question:** *What is this, is it for me, and can I see it work in a minute?* + +```markdown +# asc-cli ← logo + 4 badges +App Store Connect from your terminal, CI, or AI agent. JSON output with +next-step commands built in. ← 2-line pitch + +## Quick start ← ~12 lines +brew install asccli +asc auth login --key-id … --issuer-id … --private-key-path … +asc apps list +asc init --app-id + +## Built for agents ← ~20 lines +One JSON example with `affordances`, 2 sentences on CAEOAS → docs/design.md + +## What it covers ← the existing Features table, +| Area | What you can do | each row links to its feature doc +| TestFlight | Beta groups, testers, CSV import… → [docs](docs/features/testflight/README.md) | +… + +## More ← 5 links, nothing else +Docs index · Release workflow · Use as a Swift package · Contributing · Changelog + +## Sponsors · App Wall · License +``` + +**Not here:** per-command examples, the auth account model, iris sign-in, the SPM API or the development setup. Each has one home, listed in [the design](README.md#readmemd-150-lines). + +--- + +## `docs/README.md` (generated) + +**Who:** a user who knows *what job* they have but not which command does it. +**Their question:** *Does asc do X, and where is it documented?* + +Built by `make docs` from each feature doc's `description`. It is never edited by hand; CI fails if it's stale. + +```markdown +# asc-cli docs + + +Start with the [README](../README.md). For exact flags, see [commands](commands.md). + +| Feature | What it's for | +|---|---| +| [age-rating](features/age-rating/README.md) | Set the age-rating questionnaire answers. Use before first submission or when content changes. | +| [app-clips](features/app-clips/README.md) | … | +| [testflight](features/testflight/README.md) | Manage beta groups and testers, import/export CSV. Use when distributing a build to testers. | +… + +Guides: [release](release.md) · [library](library.md) · [design](design.md) +``` + +**Alphabetical, not grouped.** Grouping by task would need a `group:` field in every doc plus a rule for which group each feature belongs to, which is more to maintain. Browser find (Ctrl-F) across 45 one-line descriptions is enough. Add grouping only if people actually get lost. + +--- + +## `docs/features//README.md` (≤200 lines) + +**Who:** a user (or an agent following a skill) about to do a job with one feature. +**Their question:** *How do I get this done, and what will surprise me?* + +They **don't** need flag tables, which `--help` already has, or architecture, which lives in the source. Mockup of today's `testflight.md` (465 lines) after it moves to `testflight/README.md` and is trimmed: + +````markdown +--- +description: Manage beta groups and testers, import/export CSV. Use when distributing a build to testers. +--- + +# TestFlight + +Beta groups and testers: create groups, invite people, move testers between groups with CSV. +Every flag: [commands.md#testflight](../../commands.md#asc-testflight). + +## Quick start +```bash +asc testflight groups list --app-id 6450406024 +asc testflight testers add --beta-group-id g-abc123 --email jane@example.com +``` + +## Workflows + +### Copy testers from one group to another +```bash +asc testflight testers export --beta-group-id g-old > testers.csv +asc testflight testers import --beta-group-id g-new --file testers.csv +``` + +### Open a public beta +```bash +asc testflight groups create --app-id 6450406024 --name "Public Beta" --public-link-enabled +``` + +## REST +| Method | Path | CLI | +|---|---|---| +| GET | `/api/v1/apps/:appId/testflight` | `asc testflight groups list --app-id` | + +## Gotchas +- CSV must have the header `email,firstName,lastName`; `export` writes exactly that, so export → import round-trips. +- `--public-link-enabled` and `--feedback-enabled` only apply to external groups (no `--internal`). +- Only group listing is exposed over REST today; tester changes are CLI-only. + +## See also +[beta-review](../beta-review/README.md) · [builds-upload](../builds-upload/README.md) · [asc-testflight skill](../../../skills/asc-testflight/SKILL.md) +```` + +About 45 lines instead of 465. Everything removed is either in `--help` (flags, defaults), in the source (domain models, file map, tests), or was a copy of an affordance the JSON already shows. + +**When it outgrows 200 lines:** move one topic into `docs/features//.md` next to the README and link to it from "See also", as `iap-subscriptions/pricing.md` already does. The README's own path doesn't change. + +--- + +## `docs/commands.md` (generated) + +**Who:** a script author who needs the exact flag, or an agent checking a flag without a shell. +**Their question:** *What's the exact spelling, and is it required?* + +Generated by `make docs` from `asc --experimental-dump-help`, so it is exactly what the binary accepts. One section per command, with stable anchors for feature docs to link to: + +````markdown + +# Command reference — asc 0.18.4 + +## asc testflight testers import +Import beta testers from a CSV file (columns: email,firstName,lastName) + +``` +asc testflight testers import --beta-group-id --file [--output ] [--pretty] [--timeout ] +``` + +| Flag | Required | Description | +|---|---|---| +| `--beta-group-id` | yes | Beta group ID | +| `--file` | yes | Path to CSV file with columns: email,firstName,lastName | +```` + +Global flags (`--output`, `--pretty`, `--timeout`) are listed once at the top, not repeated on every command. To improve a description, edit the `help:` string in Swift. That one edit fixes `--help`, this file and every link to it. + +--- + +## `docs/release.md` + +**Who:** someone shipping a version, by hand or in CI. +**Their question:** *What's the order, and what do I check before submitting?* + +The numbered README "Release Workflow" moved here (upload → TestFlight → version → What's New → check-readiness → submit). Each step is one command plus a link to its feature doc. The per-feature detail stays in the feature docs. + +## `docs/library.md` + +**Who:** a Swift developer embedding asc in their own tool. +**Their question:** *How do I add the package and get a repository?* + +The README's "Use as a Swift Package" section moved here: the dependency snippet, a 20-line example and the `ClientFactory` list. + +## `docs/design.md` + +**Who:** a contributor or curious user asking *why* it's built this way. +**Their question:** *What is CAEOAS, and why three layers?* + +Today's `desgin.md`, renamed. It's the **one** home for the architecture diagram, which is why feature docs no longer draw their own. + +## `CONTRIBUTING.md` + +**Who:** a first-time contributor. +**Their question:** *How do I build, run the tests, and open a PR that gets merged?* + +```markdown +# Contributing +## Build & test swift build · swift test · make run ARGS="apps list" +## How code is organised 3 lines + link to docs/design.md +## Rules TDD first (link), every command ships CLI + REST (link) +## Docs the update-rules table from docs/documentation-design/, by link +``` + +## `CLAUDE.md` (≤100 lines) + +**Who:** an AI agent at the start of *every* session in this repo. +**Its question:** *What must I always or never do here?* + +```markdown +# CLAUDE.md +## TDD is non-negotiable ← unchanged gate (~20 lines) +## Commands ← build / test / run (~10 lines) +## Architecture ← 3 layers + dependency rule + CAEOAS in 1 paragraph (~15 lines) +## When you are… ← pointers, loaded only when relevant +- adding a feature → implement-feature skill (REST checklist, domain rules, docs) +- improving one → improvement skill +- touching docs → docs/documentation-design/ +## Authentication ← 5 lines + link to docs/features/asc-auth/README.md +``` + +**Moved out:** the REST exposure checklist, the domain folder tree, the doc-update table and the localization-types table. The first goes to the `implement-feature` skill, the tree is deleted (`ls Sources/Domain`), and the rest go to the skill or the feature doc. From e2a2ab7d037e6a51fcc87d999daa1eaff3db6fb5 Mon Sep 17 00:00:00 2001 From: slamhan Date: Wed, 23 Sep 2026 18:30:02 +0800 Subject: [PATCH 02/14] docs: fix broken links, roll old changelog minors into docs/changelog - CHANGELOG.md keeps [Unreleased] + 0.18.x; 0.17, 0.16 and 0.1 move unchanged to docs/changelog/.md (no lines lost) - scripts/changelog-rollover.py runs from promote-changelog.sh so each new minor release rolls the previous one off automatically - scripts/check-docs.py reports broken links, size budgets, missing descriptions and changelog bullet length (report-only for now) - rename docs/desgin.md to docs/design.md; fix 7 broken links Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/release.yml | 2 +- CHANGELOG.md | 989 +---------------------- README.md | 10 +- docs/changelog/0.1.md | 728 +++++++++++++++++ docs/changelog/0.16.md | 18 + docs/changelog/0.17.md | 247 ++++++ docs/features/simulators.md | 4 +- docs/features/web-server-architecture.md | 2 +- scripts/changelog-rollover.py | 116 +++ scripts/check-docs.py | 115 +++ scripts/promote-changelog.sh | 3 + 11 files changed, 1238 insertions(+), 996 deletions(-) create mode 100644 docs/changelog/0.1.md create mode 100644 docs/changelog/0.16.md create mode 100644 docs/changelog/0.17.md create mode 100755 scripts/changelog-rollover.py create mode 100755 scripts/check-docs.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b3c4a23..01cea34c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,7 +116,7 @@ jobs: fi git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add CHANGELOG.md + git add CHANGELOG.md docs/changelog git diff --cached --quiet \ && echo "No CHANGELOG changes to commit" \ || git commit -m "chore(changelog): promote [Unreleased] to v${{ steps.version.outputs.version }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ae2651b..55a29095 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,930 +68,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **`asc versions update` now accepts `--copyright`, `--release-type`, and `--earliest-release-date`** — closes the App Store submission gap where the version's copyright line, release type (`MANUAL` / `AFTER_APPROVAL` / `SCHEDULED`), and earliest release date could only be set via the web. The underlying `VersionRepository.updateVersion(...)` already supported these fields; the CLI now wires them through. `--version` is now optional, so any subset of these fields can be patched independently (e.g. `asc versions update --version-id v-1 --copyright "© 2026 Acme"`). ---- - -## [0.17.9] - 2026-05-09 - -### Added -- **`asc builds set-encryption-compliance --build-id --uses-non-exempt-encryption ` — set Apple's export-compliance answer on a build** — closes the second TestFlight-blocking gap: when an IPA is uploaded without `ITSAppUsesNonExemptEncryption` in Info.plist, ASC marks the build "Missing Compliance" and external testing is blocked. The new command PATCHes `/v1/builds/{id}` with `usesNonExemptEncryption` so the answer can be supplied post-upload from CI scripts that don't control the Info.plist. `Build` domain model now carries `usesNonExemptEncryption: Bool?` and a `isMissingEncryptionCompliance` semantic boolean (`true` when nil); usable builds in the missing state advertise a `setEncryptionCompliance` affordance with the ready-to-run command. Build's `tableRow` gains an "Encryption" column showing `uses` / `exempt` / `missing`. REST equivalent: `PATCH /api/v1/builds/:buildId/encryption-compliance` body `{"usesNonExemptEncryption": true|false}`. New `BuildRepository.updateBuildEncryptionCompliance(buildId:usesNonExemptEncryption:)`. See `docs/features/builds-upload.md`. -- **`asc beta-app-localizations {list,get,create,update,delete}` — TestFlight Beta App Description per locale** — closes the parity gap that blocked TestFlight submissions: Apple's `BetaAppLocalizations` resource holds the per-locale beta description, tester feedback email, marketing URL, and privacy policy URL shown in TestFlight before external testing can be enabled. Distinct from the existing `asc builds update-beta-notes` (which writes per-build "What to Test" notes) and `asc beta-review` (which writes review contact info). New `BetaAppLocalization` domain model + `@Mockable BetaAppLocalizationRepository` (Domain/Apps/TestFlight/), `SDKBetaAppLocalizationRepository` (Infrastructure) backed by `/v1/apps/{id}/betaAppLocalizations` (list), `/v1/betaAppLocalizations/{id}` (get/update/delete), and `/v1/betaAppLocalizations` (create). Affordances: `delete`, `get`, `listSiblings`, `update`. REST equivalents: `GET /api/v1/apps/:appId/beta-app-localizations` and `GET /api/v1/beta-app-localizations/:id` via `BetaAppLocalizationsController`. New REST path resolver entry for `beta-app-localizations` parented under `apps`. See `docs/features/beta-app-localizations.md`. - ---- - -## [0.17.8] - 2026-04-29 - -### Added -- **`asc iris iap-submissions delete --submission-id `** — iris-side dequeue of an IAP submission (`DELETE /iris/v1/inAppPurchaseSubmissions/{id}`). Iris-queued submissions don't round-trip through the public-SDK delete; they can only be removed via this iris-cookie-authed path. New `IrisClient.delete`, new `IrisInAppPurchaseSubmissionRepository.deleteSubmission(session:submissionId:)`. REST equivalent: `DELETE /api/v1/iris/iap-submissions/:submissionId` on `IrisIAPSubmissionsController`. **`removeFromNextVersion` affordance now resolves end-to-end through iris** (CLI: `asc iris iap-submissions delete --submission-id `; REST: `DELETE /api/v1/iris/iap-submissions/`). `RESTPathResolver.resourcePath` now converts spaces in segment names to slashes so multi-word commands like `iris iap-submissions` produce clean URL segments. Removed the short-lived public-SDK alias `POST /api/v1/iap/:iapId/unsubmit` (incorrect — public DELETE doesn't accept iris-queued submissions). -- **`removeFromNextVersion` affordance on queued IAPs** — when an IAP is in `READY_TO_SUBMIT` and Apple's iris listing reports `submitWithNextAppStoreVersion: true` (queued to ride along with the next app version), `InAppPurchase.structuredAffordances` surfaces `removeFromNextVersion` → `asc iap unsubmit --submission-id ` and **suppresses** the `submit` affordance (mutually exclusive — Apple rejects re-submits on already-queued IAPs). The dequeue uses the existing public-SDK `DELETE /v1/inAppPurchaseSubmissions/{id}` (Apple's iris flow keys the submission resource by parent IAP id, so `--submission-id ` works). Underpinned by a new best-effort iris enrichment: `IrisSDKInAppPurchaseStateRepository` calls `GET /iris/v1/apps/:appId/inAppPurchasesV2?fields[inAppPurchases]=submitWithNextAppStoreVersion&filter[state]=READY_TO_SUBMIT`, mapping each IAP to its queued bit. `SDKInAppPurchaseRepository.listInAppPurchases` accepts an optional `irisFlagsProvider` closure; the factory wires it from `IrisCookieProvider` + `IrisSDKInAppPurchaseStateRepository`. CI scripts using API-key auth keep their existing JSON output (flag stays `false`, no `removeFromNextVersion` affordance). New `InAppPurchase.submitWithNextAppStoreVersion: Bool` field, encoded only when `true` to avoid snapshot churn. New `IrisInAppPurchaseStateRepository` `@Mockable` Domain protocol. - -### Changed -- **`InAppPurchase` submission affordances reorganized into iris-queue family vs. public-SDK direct submit.** Two inverse pairs, all gated on `state == .readyToSubmit`: - - `addToNextVersion` (NEW) → `asc iris iap-submissions create --iap-id ` — queues the IAP to ride along with the next App Store version submission. Always available for ready, not-yet-queued IAPs. - - `removeFromNextVersion` → `asc iap unsubmit --submission-id ` — dequeues. Mutually exclusive with the others; emitted only when iris listing reports `submitWithNextAppStoreVersion: true`. - - `submit` → `asc iap submit --iap-id ` — public-SDK direct submit (standalone review). Now emitted **only** when `isFirstTimeSubmission == false` (Apple rejects the direct path for the first IAP for an app). - - Established-app ready IAPs see **both** `submit` and `addToNextVersion`; agent picks based on release strategy. - - Replaces the previous behavior where `submit` quietly auto-dispatched between iris and public SDK paths — keys are now explicit about what each call does. New `InAppPurchaseState.hasBeenApproved` semantic boolean. New `iris iap-submissions` REST route registered with the path resolver so `addToNextVersion` `_links` resolve to `/api/v1/iris/iap//submissions`. - -### Added -- **`asc iris iap-submissions create --iap-id ` — IAP submission via the iris private API** — closes the parity gap with the public SDK's `POST /v1/inAppPurchaseSubmissions`, which has no `attributes` field and therefore can't carry `submitWithNextAppStoreVersion`. The new command posts to `POST /iris/v1/inAppPurchaseSubmissions` with that attribute set, which is required for first-time IAP submissions (Apple binds the first IAP to the next App Store version). Defaults to `--with-next-version`; pass `--no-with-next-version` to opt out. Lives under `asc iris …` because it needs iris cookies (either browser cookies or a session from `asc iris auth login`). Surfaced via `IrisStatus.affordances.submitIAP` so `asc iris status` advertises the path when authenticated. Existing `asc iap submit` stays untouched — CI scripts using key-auth keep working unchanged. See `docs/features/iap-subscriptions/submission-iris-parity.md`. -- **REST: `POST /api/v1/iris/iap/:iapId/submissions`** — body `{"submitWithNextAppStoreVersion": true}` (default true); returns the submission resource with `_links.viewIAP` pointing back at the public IAP endpoint. - -### Fixed -- **iris 2FA `verify-code` no longer 401s with a valid code** — `IdmsaAPIClient.applyHeaders` was sending `X-Apple-OAuth-Client-Id`, `X-Apple-OAuth-Redirect-URI`, `X-Apple-OAuth-Response-Mode`, `X-Apple-OAuth-Response-Type`, `Origin`, and `Referer` on `signin/init` and `signin/complete`. None of `XcodesOrg/XcodesApp`, `XcodesOrg/XcodesLoginKit`, or the `rorkai/App-Store-Connect-CLI` Go reference send those — they taint the SRP session so the post-409 `GET /appleauth/auth` returns 401, rotates `scnt`, and the subsequent `POST /verify/trusteddevice/securitycode` is rejected as out-of-sequence. Header set is now narrowed to `Accept`, `Content-Type`, `X-Requested-With`, `X-Apple-Widget-Key`, `scnt`, `X-Apple-ID-Session-Id` (+ `X-Apple-HC` on `signin/complete`). `signinComplete` also flips to `?isRememberMeEnabled=false` with body `rememberMe: false` to match the same references — `true` puts Apple in the "trust this browser" path, which mismatches a CLI session. - -### Added -- **`asc iris auth login` / `verify-code` / `logout` — Apple ID SRP login for the iris private API** — until now iris features (`asc iris apps list`, `asc iris status`) only worked when the user had logged into App Store Connect in their browser; we'd borrow cookies from there. Headless CI had no path. The new command performs a full Apple SRP-6a handshake against `idmsa.apple.com`, handles 2FA (trusted-device push or phone code), then persists the resulting session to `~/.asc/iris/session.json` (0600 perms). New `CompositeIrisCookieProvider` chains SRP-stored cookies before browser cookies, so any existing iris command immediately picks up the SRP session without code changes elsewhere. See `docs/features/iris/iris-srp-login.md`. - - `asc iris auth login --apple-id user@example.com` — prompts for password on stdin, runs SRP, persists session on success or pending-2FA state on 409. - - `asc iris auth verify-code 123456` — submits the 2FA code, calls `2sv/trust`, fetches `olympus/v1/session` for team metadata, persists the full session. - - `asc iris auth logout` — deletes the persisted session. - - `ASC_IRIS_DEBUG=1` — dumps every idmsa request/response to stderr with `a`/`m1`/`m2` redacted, so failures self-capture safely-shareable test data. - - Built on `adam-fowler/swift-srp` (Apache 2.0) for RFC 5054 group-2048 constants, BigNum, and key generation. Apple's PBKDF2-derived `x` is layered on top in `AppleSRPClient`. PBKDF2-HMAC-SHA256 implementation cross-checked against RFC 7914 §11 vectors. - - **Caveat**: while every layer is unit-tested with mocked HTTP and synthesized vectors, real-world correctness against Apple's specific `M1` shape gets validated only on the first real login attempt. If Apple's protocol shifts, expect churn here. - ---- - -## [0.17.7] - 2026-04-29 - -### Added -- **REST `POST` / `PATCH` / `DELETE` for subscription group localizations** — `SubscriptionGroupLocalizationsController` previously exposed only `GET`, so `POST /api/v1/subscription-groups/:groupId/subscription-group-localizations` (the call the AppNexus web UI emits when creating a new locale) returned 404. Now wired: - - `POST /api/v1/subscription-groups/:groupId/subscription-group-localizations` — body `{locale, name, customAppName?}`. - - `PATCH /api/v1/subscription-group-localizations/:localizationId` — body `{name?, customAppName?}`. - - `DELETE /api/v1/subscription-group-localizations/:localizationId` — returns `{"deleted": true}`. -- **`uploadReviewScreenshot` and `uploadImage` affordances on `InAppPurchase` and `Subscription`** — until now an agent looking at an IAP or subscription with no review screenshot had no `_links` path to discover the upload command. The four new affordances close that gap (CLI surface, REST `_links`, REST endpoints). -- **REST upload endpoints (raw body)** — `POST /api/v1/iap/:iapId/review-screenshot`, `POST /api/v1/iap/:iapId/images`, `POST /api/v1/subscriptions/:subscriptionId/review-screenshot`, `POST /api/v1/subscriptions/:subscriptionId/images`. Send the image bytes as the request body (Content-Type `image/png`, `image/jpeg`, etc.); body is spooled to a temp file and uploaded through the existing `uploadReviewScreenshot` / `uploadImage` repository methods. 20MB body ceiling. Same path as the `GET` collection — verb selects read vs. upload. - -### Fixed -- **`uploadImage` / `uploadReviewScreenshot` now wait for ASC processing before returning** — ASC's PATCH-commit response carries an empty `imageAsset` because processing is async; clients were getting `{templateURL: "", width: 0, height: 0}` and had no signal of when the asset URL would appear. Both `SDKInAppPurchaseReviewRepository` and `SDKSubscriptionReviewRepository` now mirror the iOS SDK pattern (`AppStoreSdk` `verifyImageUpload` / `verifyReviewScreenshotUpload`): after the commit PATCH, poll `GET /v1/inAppPurchaseImages/{id}` (or the screenshot equivalent) until `state ∈ {PREPARE_FOR_SUBMISSION, WAITING_FOR_REVIEW, APPROVED}` for images / `assetDeliveryState == COMPLETE` for screenshots. Defaults: 15 attempts × 2s = 30s ceiling; failure states (`FAILED`, `REJECTED`) throw immediately. The mapper additionally treats empty `templateURL` / zero dimensions as nil so any leftover not-ready shape is omitted from JSON via `encodeIfPresent` rather than leaking. Result: `POST /api/v1/iap/:iapId/images` and the three sibling upload endpoints return a record whose `imageAsset` is fully populated and ready to render — no client-side polling needed. - -### Changed -- **`upload` action resolves to bare collection path in REST** — `RESTPathResolver` now treats `upload` like `get/update/delete` (no extra suffix) so an upload `_links` href is `/api/v1/iap/:iapId/review-screenshot` (POST), not `/...review-screenshot/upload`. Standard REST: the verb carries the intent, the path names the resource. -- **`reviewNote` now read-side on `InAppPurchase` and `Subscription`** — the field was previously write-only via `asc iap update --review-note` / `asc subscriptions update --review-note`. List/get responses now expose `reviewNote: String?` so an agent (or web UI like AppNexus's IAP/Subscription review tabs) can read the existing value without a separate fetch. SDK mappers thread `attributes.reviewNote` from `InAppPurchaseV2` and `Subscription` SDK responses; nil values are omitted from JSON output via `encodeIfPresent`. Existing JSON snapshots stay byte-identical when no review note is set. - ---- - -## [0.17.6] - 2026-04-28 - -### Fixed -- **`PATCH /api/v1/iap/{iapId}/availability` returned 404** — the route was never registered, so the web frontend's "save territories" call failed. `IAPAvailabilityController` now serves `PATCH` with body `{territoryIds, availableInNewTerritories}` and routes through `repo.createAvailability` (ASC's `POST /v1/inAppPurchaseAvailabilities` upserts — there is no separate update endpoint). - ---- - -## [0.17.5] - 2026-04-28 - -### Added -- **Production / sandbox split for offer codes** — `InAppPurchaseOfferCode` and `SubscriptionOfferCode` now expose `productionCodeCount` and `sandboxCodeCount` (Apple returns these alongside the existing total). Visible in `asc iap-offer-codes list`, `asc subscription-offer-codes list`, and on the existing `GET /api/v1/iap/:iapId/offer-codes` and `GET /api/v1/subscriptions/:subscriptionId/offer-codes` endpoints (flow through Codable). -- **`environment` on one-time-use code batches** — `InAppPurchaseOfferCodeOneTimeUseCode` and `SubscriptionOfferCodeOneTimeUseCode` now carry `environment: OfferCodeEnvironment?` (PRODUCTION or SANDBOX). Apple separates redemption by environment; sandbox batches redeem against sandbox tester accounts and have a smaller per-quarter ceiling. -- **`--environment` flag on one-time-codes create** — `asc iap-offer-code-one-time-codes create --environment sandbox` and `asc subscription-offer-code-one-time-codes create --environment sandbox`. Default is `production`, matching prior behaviour. Maps to the SDK's `Attributes(environment:)` field on the create-request body so sandbox batches can be generated from CLI. -- **REST endpoints for one-time-codes** — `GET/POST /api/v1/iap-offer-codes/:offerCodeId/one-time-codes`, `PATCH /api/v1/iap-offer-code-one-time-codes/:oneTimeCodeId`, and the subscription mirrors. POST body accepts `{numberOfCodes, expirationDate, environment?}` so REST clients can generate sandbox redemption batches without dropping to CLI. Each batch row's `_links` resolves `listOneTimeCodes` to the nested parent path. -- **`createOfferCode` affordance + POST endpoints** — `InAppPurchase` and `Subscription` now advertise `createOfferCode` in `_links`, mirroring the iOS app's "+ New offer code" affordance. `POST /api/v1/iap/:iapId/offer-codes` accepts `{name, customerEligibilities[]}`; `POST /api/v1/subscriptions/:subscriptionId/offer-codes` accepts `{name, duration, mode, periods, customerEligibilities[], offerEligibility}`. Previously these were CLI-only. -- **Per-territory `prices` on offer-code create (IAP + Subscription)** — both CLI and REST now accept the full territory price list at creation time, matching ASC's actual contract. CLI: `--price =` (repeatable, paid) and `--free-territory ` (repeatable). REST: `prices: [{territory, pricePointId?}]` in POST body — omit `pricePointId` for free. New shared `OfferCodePriceInput` domain type. SDK adapters compose the `included[InAppPurchaseOfferPriceInlineCreate]` / `included[SubscriptionOfferCodePriceInlineCreate]` payload with `${local-price-N}` placeholder ids referenced from `relationships.prices.data` — same pattern as the existing IAP price-schedule create. **Fixes a long-standing bug**: previous `createOfferCode` calls sent an empty `prices` relationship, so every offer code created via asc-cli was missing per-territory pricing (and ASC's `prices` is read-only post-create — there was no recovery path). -- **`--auto-renew` on subscription offer-code create** — CLI flag (default `true`) and REST body field `isAutoRenewEnabled` (also accepts `autoRenew`). Setting it `false` creates a non-renewing/one-time offer; ASC only accepts `--mode FREE_TRIAL` in that case. Previously not exposed. - ---- - -## [0.1.74] - 2026-04-27 - -### Added -- **HATEOAS `_links` for IAPs and Subscriptions** — `GET /api/v1/apps/{id}/iap` and `GET /api/v1/subscription-groups/{id}/subscriptions` now embed `_links` per item so an agent can navigate to localizations, availability, offer codes, price points, review screenshots, promotional offers, win-back offers (subscriptions only), and intro offers without knowing the URL conventions. -- **REST controllers for IAP details** — `GET /api/v1/iap/:id/localizations`, `/availability`, `/offer-codes`, `/price-points`. Each returns the agent-first `data: [...]` envelope with `_links` already populated. -- **REST controllers for Subscription details** — `GET /api/v1/subscriptions/:id/localizations`, `/availability`, `/offer-codes`, `/introductory-offers`. -- **`InAppPurchasePriceSchedule` enriched with `baseTerritory` + `territoryPrices`** — `GET /api/v1/iap/:id/price-schedule` now returns the developer-set base territory plus all auto-equalized per-territory prices (currency + customerPrice + proceeds) so the iOS Pricing tab has data to render. Composes three ASC API calls under the hood: schedule (with base territory), manual prices, and equalizations. -- **IAP equalizations primitive** — `asc iap-equalizations list --price-point-id [--limit N]` and `GET /api/v1/iap-price-points/:id/equalizations`. Exposes `GET /v1/inAppPurchasePricePoints/{id}/equalizations` (~175 territory prices auto-derived from a manual base price). -- **Subscription price schedule** — new `SubscriptionPriceSchedule` domain type with `territoryPrices` and `price(for:)` lookup (no base territory — subscriptions are per-territory). `asc subscription-price-schedule get --subscription-id ` and `GET /api/v1/subscriptions/:id/price-schedule`. Composes `GET /v1/subscriptions/{id}/prices` + equalizations. -- **Subscription equalizations primitive** — `asc subscription-equalizations list --price-point-id ` and `GET /api/v1/subscription-price-points/:id/equalizations`. -- **Multi-territory subscription `setPrices` (batch)** — `asc subscriptions prices set-batch --subscription-id --price USA=spp-1 --price JPN=spp-2 ...`. Mirrors iOS `setPrices(prices:)`. Returns the post-write schedule. -- **Subscription promotional images** — `asc subscription-images list|upload|delete` and `GET /api/v1/subscriptions/:id/images`. New `SubscriptionPromotionalImage` domain type with `delete` suppression while `state.isPendingReview` (mirrors IAP images). -- **`asc subscriptions update --period `** — change billing period (ONE_WEEK, ONE_MONTH, …, ONE_YEAR) on an existing subscription. Mirrors the iOS app's `subscription.update(subscriptionPeriod:)`. -- **`setPrice` affordance + `POST /api/v1/iap/{id}/prices/set`** — IAPs now advertise how to set or change their base price/territory. Calling with a new `baseTerritory` is how the iOS app implements "Change Base Territory" — ASC replaces the schedule and re-equalizes other territories. -- **`setPrices` affordance + `POST /api/v1/subscriptions/{id}/prices/set-batch`** — subscriptions now advertise the batch multi-territory price-set operation. Body accepts `{ prices: [{territory, pricePointId, startDate?, preserveCurrentPrice?}] }` (preferred) or a flat `{ "USA": "spp-1", "JPN": "spp-2" }` shorthand. -- **Cursor pagination for IAP/Subscription price points** — `listPricePoints` now accepts `limit` + `cursor` and returns `PaginatedResponse { data, nextCursor, totalCount }`. CLI: `asc iap price-points list --iap-id X --limit 200 --cursor `. REST: `GET /api/v1/iap/{id}/price-points?territory=CHN&limit=200&cursor=` returns `{ data, nextCursor, totalCount }`. Frontends loop until `nextCursor` is absent. Mirrors iOS's `loadPricePoints(territory:limit:cursor:) → PaginatedResult`. - -### Fixed -- **`POST/PATCH /api/v1/iap/{id}/localizations` returned bare 500 on ASC validation failures + dropped over-limit content** — when the description exceeded ASC's 45-character cap (or name exceeded 30), Hummingbird's default error path returned 500 with no body, browsers showed "Failed to fetch", and the user lost the typed text. The controller now (1) trims `name` to 30 grapheme clusters and `description` to 45 before sending, matching the iOS app's `LocaleCard` `limit:` constants, and (2) catches any remaining ASC error (e.g., invalid locale) and surfaces it as `400 Bad Request` with the underlying message so the UI can render a meaningful inline error. -- **IAP/Subscription availability returned only ~10 territories instead of all ~175** — the parent endpoint's `include=availableTerritories` truncates the relationship to a single page. `getAvailability` now issues two parallel calls (attributes + dedicated `/availableTerritories?limit=200`) so the full territory list reaches the frontend's Availability tab. Matches the iOS SDK's `fetchAvailability` composition. -- **`SubscriptionGroup._links` was empty** — `GET /api/v1/apps/{id}/subscription-groups` items now embed populated `_links` for `listSubscriptions`, `listLocalizations`, `createSubscription`, `createLocalization`, `update`, `delete`. Migrated `SubscriptionGroup` from raw `affordances` to `structuredAffordances` so `apiLinks` auto-derives, and registered `_subscriptionGroupLocalizationRoutes` in `RESTPathResolver.ensureInitialized` so the nested localizations path resolves. -- **`/api/v1/subscription-groups/{id}/subscriptions` returned 404** — the `_links.listSubscriptions` URL had no controller. Added `SubscriptionsController` and wired it in `RESTRoutes.swift`. Each subscription's `_links` already advertise the full child surface (localizations, availability, price-schedule, offers, etc.). -- **Review screenshots and promotional images returned no image URL** — the responses included `id`/`fileName`/`fileSize`/`assetState` but nothing renderable. Added `imageAsset: ImageAsset?` to `SubscriptionReviewScreenshot`, `InAppPurchaseReviewScreenshot`, `SubscriptionPromotionalImage`, `InAppPurchasePromotionalImage` — populated from the SDK's `attributes.imageAsset` (templateUrl + width + height). Frontend calls `imageAsset.url(maxSize:format:)` to substitute the `{w}/{h}/{f}` placeholders and produce a real CDN URL. -- **`SubscriptionPrice._links` was empty** — migrated `SubscriptionPrice` from raw `affordances` to `structuredAffordances` so `apiLinks` auto-derives. `listPricePoints` now resolves to `/api/v1/subscriptions/{id}/price-points` over REST. -- **`POST /iap/{id}/prices/set` returned 500** — the inline manual-price payload omitted the `inAppPurchaseV2` back-reference and used a plain `"p1"` placeholder, both of which ASC rejects. The request now mirrors the iOS app shape: `${local-manual-price-1}` placeholder + `inAppPurchaseV2` relationship pointing back at the IAP. Also wrapped `IAPPricesController` in try/catch so future failures log the underlying ASC error to stderr and return a JSON error body instead of an empty 500. -- **`POST /api/v1/apps/{id}/iap` returned 404** — the route wasn't registered. `IAPController` now serves `POST` (create — accepts `referenceName`, `productId`, and `inAppPurchaseType` in either CLI-style `non-consumable` or raw enum `NON_CONSUMABLE`), `PATCH /iap/{id}` (update), and `DELETE /iap/{id}`. Mirrors `VersionsController`'s POST/PATCH conventions. -- **`POST /api/v1/apps/{id}/subscription-groups` returned 404** — same fix for subscription groups. `SubscriptionGroupsController` now serves `POST` (create — `referenceName`), `PATCH /subscription-groups/{id}` (rename), and `DELETE /subscription-groups/{id}`. -- **`GET /api/v1/iap/{id}/availability` returned 500 for newly-created IAPs** — ASC returns 404 until the developer creates the availability resource. `getAvailability(iapId:)` and `getAvailability(subscriptionId:)` now return `Optional` and tolerate the 404 by returning nil; the SDK adapter mirrors iOS's `refreshTerritoryStatuses` 404 tolerance. -- **Availability tab showed empty list on fresh IAP/subscription** — iOS shows all 175 territories preselected as the default UX; the web frontend got `data: []` and rendered nothing. `IAPAvailabilityController` and `SubscriptionAvailabilityController` now synthesize a default-all-territories availability (with `isAvailableInNewTerritories: true`) by fetching `/v1/territories` whenever no resource exists. Frontend renders the same 175-territory grid iOS does. - -### Changed -- **`RESTPathResolver` resolves singleton-under-parent `get` to nested path** — when an action is not `list`/`create`, the singularized own-id is missing, and a registered route's parent param matches one in `params`, the resolver now returns the nested `/parent/{id}/segment` path. This corrects `_links.getReviewScreenshot` for IAP and Subscription (was `/api/v1/iap-review-screenshot/{id}` → now `/api/v1/iap/{id}/review-screenshot`) and similar singletons (availability, age-rating). -- **`AgeRatingController`** now serves the canonical nested path `/api/v1/app-infos/{appInfoId}/age-rating` matching the resolver's `_links`. The flat `/api/v1/age-rating/{id}` remains for back-compat. - ---- - -## [0.17.3] - 2026-04-26 - -### Added -- **State-aware affordances on the new aggregates** — affordances no longer suggest illegal next actions: - - `PromotedPurchase` exposes `state.isLocked` (true while `WAITING_FOR_REVIEW` / `IN_REVIEW`); `update` and `delete` are suppressed while locked, so an agent following affordances won't issue a 409 against App Review. - - `InAppPurchasePromotionalImage` suppresses `delete` while `state.isPendingReview` is true. - - `InAppPurchaseReviewScreenshot.AssetState` and `SubscriptionReviewScreenshot.AssetState` gain `isComplete` / `hasFailed` semantic booleans. `delete` is offered once the asset is reachable (`uploadComplete` / `complete` / `failed`); while `awaitingUpload`, only `upload` is offered as the recovery path. -- **IAP & Subscription review screenshots + IAP promotional images** — closes the upload-heavy feature parity gap. New CLI commands implement the standard ASC reserve → upload chunks → commit-with-MD5 protocol on top of `URLSession`: - - `asc iap-review-screenshot get|upload|delete` — single review screenshot per IAP. `get` returns an empty `data: []` array when no screenshot is present, mirroring CAEOAS conventions. - - `asc iap-images list|upload|delete` — 1024×1024 promotional images for an IAP, with state semantic booleans (`isApproved`, `isPendingReview`). - - `asc subscription-review-screenshot get|upload|delete` — single review screenshot per subscription. - - New domain types: `InAppPurchaseReviewScreenshot`, `InAppPurchasePromotionalImage` (with `ImageState` enum), `SubscriptionReviewScreenshot`. New repository protocols `InAppPurchaseReviewRepository` + `SubscriptionReviewRepository`, both `@Mockable`. - - `InAppPurchase` now advertises `getReviewScreenshot` + `listImages`; `Subscription` advertises `getReviewScreenshot`. -- **Promoted purchases** — App Store product page promoted slot CRUD. New CLI commands under `asc promoted-purchases`: - - `list --app-id `, `create --app-id (--iap-id | --subscription-id ) [--visible|--hidden] [--enabled|--disabled]`, `update --promoted-id ...`, `delete --promoted-id `. - - New `PromotedPurchase` domain type carrying `appId` + either `inAppPurchaseId` or `subscriptionId` (mutually exclusive at create time, validated in the command). State enum `PromotedPurchaseState` with raw values matching ASC plus `isLocked` / `isApproved` semantic booleans. - - Backed by `GET/POST /v1/apps/{id}/promotedPurchases` and `PATCH/DELETE /v1/promotedPurchases/{id}`. -- **Win-back offers** — full CRUD with eligibility rules, priority, promotion intent, and per-territory pricing. New CLI command tree under `asc win-back-offers`: - - `list --subscription-id `, `delete --offer-id `, `update --offer-id [--priority HIGH|NORMAL] [--start-date ...] [--end-date ...] [--paid-months n] [--since-min n] [--since-max n] [--wait-months n] [--promotion-intent ...]`, `prices list --offer-id `. - - `create --subscription-id --reference-name --offer-id --duration --mode --periods --paid-months --since-min --since-max --start-date --priority HIGH|NORMAL [--end-date ...] [--wait-months n] [--promotion-intent ...] [--price USA=spp-1 --price GBR=spp-2 ...]` — bypasses the generated SDK's incomplete `WinBackOfferPriceInlineCreate` (missing relationships) by encoding the body manually with type-erased `AnyCodable`. - - Domain types: `WinBackOffer` (with `customerEligibilityTimeSinceLastSubscribed` min/max, `WinBackOfferPriority`, `WinBackOfferPromotionIntent`), `WinBackOfferPrice`, `WinBackOfferPriceInput`. `Subscription` advertises `listWinBackOffers`. -- **Subscription promotional offers** — CRUD plus per-territory inline price creation. New CLI commands under `asc subscription-promotional-offers`: - - `list --subscription-id `, `delete --offer-id `, `prices list --offer-id `. - - `create --subscription-id --name --offer-code --duration --mode --periods [--price USA=spp-1 ...]` — uses `${newPromoOfferPrice-N}` 1-based local IDs in the `included` array, matching the ASC web UI request shape. - - Domain types: `SubscriptionPromotionalOffer`, `SubscriptionPromotionalOfferPrice`, shared `PromotionalOfferPriceInput`. `Subscription` advertises `createPromotionalOffer` + `listPromotionalOffers`. -- **Subscription group localizations** — per-locale display name and Custom App Name for a subscription group. New CLI command tree under `asc subscription-group-localizations`: - - `list --group-id `, `create --group-id --locale --name [--custom-app-name ]`, `update --localization-id [--name ] [--custom-app-name ]`, and `delete --localization-id `. - - New `SubscriptionGroupLocalization` domain type ({id, groupId, locale, name?, customAppName?, state?}) with `listSiblings`/`update`/`delete` affordances. New `SubscriptionGroupLocalizationRepository` `@Mockable` protocol, SDK adapter on `/v1/subscriptionGroupLocalizations`. - - `SubscriptionGroup` now advertises `createLocalization`/`listLocalizations` so an agent navigating from a group can discover the localization tree. -- **Offer code prices + one-time code values** — completes the offer-code feature so an agent can read back per-territory pricing and download distributable redemption codes: - - `asc iap-offer-codes prices list --offer-code-id ` and `asc subscription-offer-codes prices list --offer-code-id ` — returns each price with `territory` and `pricePointId` (IAP) or `subscriptionPricePointId` (Subscription). Backed by `GET /v1/inAppPurchaseOfferCodes/{id}/prices` and `GET /v1/subscriptionOfferCodes/{id}/prices`. - - `asc iap-offer-code-one-time-codes values --one-time-code-id ` and `asc subscription-offer-code-one-time-codes values --one-time-code-id ` — fetches the raw CSV body of one-time-use redemption codes for distribution. Backed by `GET /v1/.../oneTimeUseCodes/{id}/values` (`Request`). - - Two new domain types: `InAppPurchaseOfferCodePrice { id, offerCodeId, territory?, pricePointId? }` and `SubscriptionOfferCodePrice { id, offerCodeId, territory?, subscriptionPricePointId? }`. Each advertises a `listPrices` affordance that points back at its parent offer code. - - Repository protocols extended with `listPrices(offerCodeId:)` and `fetchOneTimeUseCodeValues(oneTimeCodeId:) -> String`; SDK adapters implement them on top of the appstoreconnect-swift-sdk. -- **Subscription pricing parity** — subscriptions now match IAP for browsing tiers and committing per-territory prices. Subscriptions don't have a base territory (Apple auto-equalizes), so the model has `proceedsYear2` and `prices set` is per-territory rather than per-base. New CLI commands (mirrored under `asc subscriptions`): - - `asc subscriptions price-points list --subscription-id [--territory ]` — list `SubscriptionPricePoint`s with optional territory filter. Each result advertises `setPrice` only when a territory is attached. - - `asc subscriptions prices set --subscription-id --territory --price-point-id [--start-date YYYY-MM-DD] [--preserve-current-price]` — POST `/v1/subscriptionPrices` to commit a price. - - New domain models: `SubscriptionPricePoint` (id, subscriptionId, territory, customerPrice, proceeds, **proceedsYear2**) and `SubscriptionPrice` (id, subscriptionId). - - New `SubscriptionPriceRepository` `@Mockable` protocol with `listPricePoints` + `setPrice`; SDK adapter wires `GET /v1/subscriptions/{id}/pricePoints` and `POST /v1/subscriptionPrices`. -- **IAP & Subscription lifecycle parity** — symmetric update/delete/unsubmit across the in-app-purchase and subscription aggregates so an agent can drive the full lifecycle, not just create-then-submit. New CLI commands: - - `asc iap-localizations update --localization-id [--name ] [--description ]` and `asc iap-localizations delete --localization-id `. - - `asc subscription-localizations update --localization-id [--name ] [--description ]` and `asc subscription-localizations delete --localization-id `. - - `asc iap update --iap-id [--reference-name ] [--review-note ] [--family-sharable | --not-family-sharable]`, `asc iap delete --iap-id `, and `asc iap unsubmit --submission-id ` (the SDK lacks a generated DELETE, so it goes through a manual `Request`). - - `asc subscriptions update --subscription-id [--name ] [--family-sharable | --not-family-sharable] [--group-level ] [--review-note ]`, `asc subscriptions delete --subscription-id `, and `asc subscriptions unsubmit --submission-id ` (manual DELETE same as IAP). - - `asc subscription-groups update --group-id --reference-name ` and `asc subscription-groups delete --group-id `. - - `asc subscription-offers delete --offer-id ` to drop an introductory offer. -- Affordance updates so the new commands surface in the JSON output of every list/create/submit response: `InAppPurchase` now advertises `update`/`delete`, `Subscription` advertises `update`/`delete`, `SubscriptionGroup` advertises `update`/`delete`, both localization models advertise `update`/`delete`, both submission models advertise `unsubmit`, and `SubscriptionIntroductoryOffer` advertises `delete`. - ---- - -## [0.17.2] - 2026-04-25 - -### Added -- **Plugin update workflow (Sparkle-style)** — check for and apply plugin updates via CLI or REST: - - `asc plugins updates` (CLI) and `GET /api/v1/plugins/updates` (REST) — list every installed plugin where the marketplace has a newer version. Each entry is a `PluginUpdate { name, installedVersion, latestVersion, repositoryURL?, downloadURL? }` with affordances pointing at `asc plugins update --name X` (CLI) and `POST /api/v1/plugins/:name/update` (REST). - - `asc plugins update --name X` (CLI) and `POST /api/v1/plugins/:name/update` (REST) — uninstall the named plugin and reinstall the latest marketplace version. Returns the freshly installed `Plugin`. - - `Plugin.affordances` adds `checkUpdate → asc plugins updates` for installed plugins so frontends can wire a "Check for updates" entry without hard-coding the path. - - New `PluginRepository.listOutdated()` and `update(name:)` methods on the repository protocol; implemented in `PluginMarketRepository` by zipping `listInstalled()` with `listAvailable()` on `name`. -- **Plugins REST install/uninstall/search** — `PluginsController` now mirrors the full `asc plugins` CLI: - - `POST /api/v1/plugins` — install a plugin from the marketplace. Body: `{ "name": "Hello.plugin" }`. Returns the installed `Plugin` with `isInstalled: true`. - - `DELETE /api/v1/plugins/:name` — uninstall by name (or slug). Returns `204 No Content`. - - `GET /api/v1/plugins/market?q=` — search the marketplace. Without `q`, behaves like the existing list. With `q`, calls `PluginRepository.searchAvailable`. Same response shape as `GET /api/v1/plugins/market`. - - These endpoints back the install/uninstall/search affordances already advertised on `Plugin.affordances` — frontends following affordances no longer hit a 404. -- **Auth REST endpoints** — `asc auth` is now drivable from a web client. New `AuthController` exposes: - - `POST /api/v1/auth/accounts` (login) — body `{ keyId, issuerId, privateKeyPEM, name?, vendorNumber? }`. Saves to `~/.asc/credentials.json` and marks active. Returns the new `AuthStatus`. - - `GET /api/v1/auth/accounts` (list) — returns all saved `ConnectAccount` records. - - `GET /api/v1/auth/accounts/active` (check) — returns the active account's `AuthStatus`. - - `PATCH /api/v1/auth/accounts/active` (use) — body `{ name }` switches the active account, returns updated `AuthStatus`. - - `PATCH /api/v1/auth/accounts/:name` (update) — body `{ vendorNumber }` updates a stored account. - - `DELETE /api/v1/auth/accounts/active` and `DELETE /api/v1/auth/accounts/:name` (logout) — return `204 No Content`. - - **Security:** the controller writes API key PEMs to disk via `AuthStorage`. Run `asc web-server` bound to loopback only when this controller is enabled; the request body is sensitive. - - `AuthStatus` and `ConnectAccount` now conform to `Presentable` so REST responses share the `{"data":[…]}` shape used elsewhere. - ---- - -## [0.17.1] - 2026-04-24 - -### Added -- **`createVersion` affordance on `App`** — every app response now advertises `asc versions create --app-id ` (REST: `POST /api/v1/apps/{appId}/versions`). Frontends driven by affordances (e.g. the command center UI) can use the presence of this key to enable a "Create Version" action without hard-coding capabilities. -- **`updateVersion` affordance on editable `AppStoreVersion`** — versions in `prepareForSubmission` state expose `asc versions update --version-id ` (REST: `PATCH /api/v1/versions/{id}`). Live and pending versions omit it, giving the UI a state-aware signal for the edit dialog. -- **`asc versions update --version-id --version `** — new CLI command to update an existing App Store version's version string. Backed by new `VersionRepository.updateVersion(id:versionString:)` which maps to the ASC SDK `AppStoreVersionUpdateRequest`. -- **`POST /api/v1/apps/{appId}/versions`** and **`PATCH /api/v1/versions/{versionId}`** — REST endpoints for version create/update on `VersionsController`. Request body: `{ "versionString": "...", "platform": "IOS" }` for create, `{ "versionString": "..." }` for update. -- **`createLocalization` affordance on `AppInfo`** — every app-info response now advertises the locale-add endpoint. Unblocks the frontend's "+ Add Locale" button, which previously had no link to POST against. -- **`POST /api/v1/app-infos/{appInfoId}/localizations`** — create an `AppInfoLocalization` via REST. Body: `{ "locale": "fr-FR", "name": "..." }`. Returns the new row with its `_links` (`updateLocalization`, `delete`, `listLocalizations`). -- **`PATCH /api/v1/app-info-localizations/{localizationId}`** — update `name`, `subtitle`, `privacyPolicyUrl`, `privacyChoicesUrl`, `privacyPolicyText`. Missing keys mean "don't change"; sending an empty string clears the field. Fixes the `404` the frontend was seeing when saving per-locale name / subtitle / privacy URLs. -- **`DELETE /api/v1/app-info-localizations/{localizationId}`** — delete a locale, returns `204 No Content`. Backs the "trash" button on the localization row. -- **`App.contentRightsDeclaration` + `ContentRightsDeclaration` enum** — apps now carry the third-party-content declaration (`USES_THIRD_PARTY_CONTENT` / `DOES_NOT_USE_THIRD_PARTY_CONTENT`). Field is optional and omitted from JSON when unset. This is the ASC-accurate mapping — the declaration lives on `App`, not on `AppInfo`. -- **`asc apps update --app-id --content-rights-declaration `** + **`PATCH /api/v1/apps/{appId}`** — update content rights declaration via CLI or REST. Backed by new `AppRepository.updateContentRights(appId:declaration:)` which maps to the ASC SDK `AppUpdateRequest`. -- **`updateContentRights` affordance on `App`** — every app response advertises the new PATCH endpoint so frontends can wire the declaration switch without hard-coding the URL. -- **`PATCH /api/v1/age-rating/{declarationId}`** — update an age rating declaration via REST. Body accepts any subset of the `AgeRatingDeclarationUpdate` fields (boolean flags, `ContentIntensity` values, `kidsAgeBand`, `ageRatingOverride`, `koreaAgeRatingOverride`). Fixes the `404` the frontend was seeing when following the `update` `_link` on an `AgeRatingDeclaration` response. Matches the affordance key already advertised by `AgeRatingDeclaration.structuredAffordances`. - ---- - -## [0.17.0] - 2026-04-21 - -### Added -- **`GET /api/v1/apps?include=icon`** — enriches each app in the response with its primary build's `iconAsset` (`templateUrl`, `width`, `height`). Icon fetch is opt-in to keep the default list-apps path fast. Without `?include=icon`, behaviour is unchanged. Template URL placeholders (`{w}`, `{h}`, `{f}`) can be substituted client-side to render at any size (e.g. `120x120bb.png`). -- **Domain types `ImageAsset`, `App.iconAsset`** — new optional `iconAsset` on `App` (omitted from JSON when nil), new `ImageAsset` value type under `Domain/Shared/` with `url(maxSize:format:)` helper. Populated from SDK `Build.iconAssetToken` via `/v1/apps/{id}/appStoreVersions?include=build` -- **`AppRepository.fetchAppIcon(appId:)`** — returns `ImageAsset?` by joining the latest app version to its build. Returns `nil` when no version has an attached build -- **`GET /api/v1/apps/{appId}/app-infos`** — new REST route backed by `AppInfoRepository`. Previously the affordance on `App` advertised this path but the controller returned 404. `AppInfo` is now `Presentable` and uses `structuredAffordances`, so responses include `_links` to app-info localizations, age rating, and the enclosing app-infos list -- **`AppInfo.appStoreState` / `AppInfo.state`** — app-infos responses now include lifecycle fields (`appStoreState` uses legacy ASC version states like `READY_FOR_SALE`; `state` uses the newer `AppInfo.State` enum like `READY_FOR_DISTRIBUTION`, `PREPARE_FOR_SUBMISSION`). Agents can use these to pick the live-version app-info vs the version-being-prepared. Computed booleans `isLive` and `isEditable` expose the common decisions -- **`AppInfo.appStoreAgeRating`** — computed App Store age rating (e.g. `FOUR_PLUS`, `NINE_PLUS`, `TWELVE_PLUS`, `SEVENTEEN_PLUS`) now surfaced on app-infos responses. This is what the App Store listing page displays; cheaper than fetching the full `ageRatingDeclaration` when only the label is needed -- **`asc app-categories get --category-id `** + **`GET /api/v1/app-categories/{id}`** — fetch a single App Store category by ID. `AppInfo` responses now include `getPrimaryCategory`, `getSecondaryCategory`, and four subcategory affordances (conditional on the respective ID being set), each pointing at the new endpoint. Backed by new `AppCategoryRepository.getCategory(id:)` -- **`GET /api/v1/app-categories`** — list categories exposed at the REST layer (previously CLI-only). Optional `?platform=` query param mirrors the CLI flag -- **`GET /api/v1/app-infos/{appInfoId}/localizations`** — new REST route backed by `AppInfoRepository.listLocalizations`. `AppInfoLocalization` is now `Presentable` with `structuredAffordances` including update/delete links -- **Unified REST path resolver** — removed the global `{param → segment}` alias table from `RESTPathResolver`. New single rule: for resource actions (get/update/delete/submit/…), the command name IS the REST segment. CLI flag aliases (e.g. `--localization-id`, `--product-id`) are presentation concerns and no longer drive REST routing. Fixes ambiguity where the same CLI flag belonged to multiple resources -- **`GET /api/v1/age-rating/{appInfoId}`** — fetch the age rating declaration for an app-info. Previously only `asc age-rating get` worked; the REST route was advertised via `_links.getAgeRating` but returned 404. `AgeRatingDeclaration` is now `Presentable` with `structuredAffordances` -- **`PATCH /api/v1/app-infos/{appInfoId}`** — update app-info categories (primary, secondary, up to two subcategories each) via REST. JSON body accepts `primaryCategoryId`, `primarySubcategoryOneId`, `primarySubcategoryTwoId`, `secondaryCategoryId`, `secondarySubcategoryOneId`, `secondarySubcategoryTwoId` — any subset. Returns the updated `AppInfo` with `_links` that now include `getPrimaryCategory` / `getSecondaryCategory` pointing at the new values -- **`GET /api/v1/version-localizations/{localizationId}/screenshot-sets`** and **`GET /api/v1/screenshot-sets/{setId}/screenshots`** — screenshot-related `_links` on `AppStoreVersionLocalization` and `AppScreenshotSet` responses now resolve to working REST endpoints. `AppStoreVersionLocalization`, `AppScreenshotSet`, and `AppScreenshot` now use `structuredAffordances` (replacing raw `affordances`); the latter two also gained `Presentable` conformance - -### Changed -- **REST controllers split by resource (SRP/OCP refactor)** — the former `AppsController` was a god controller accumulating 15+ routes across 8 unrelated resources (apps, versions, localizations, screenshot-sets, screenshots, builds, testflight, reviews, iap, subscription-groups, app-infos, app-info-localizations, app-categories, age-rating). Now split into 13 focused controllers, one per REST resource type — `AppsController`, `VersionsController`, `VersionLocalizationsController`, `ScreenshotSetsController`, `ScreenshotsController`, `BuildsController`, `TestFlightController`, `CustomerReviewsController`, `IAPController`, `SubscriptionGroupsController`, `AppInfosController`, `AppCategoriesController`, `AgeRatingController`. Each takes only the 1–2 repositories it actually needs. `RESTRoutes.configure` composes them with one line per resource. Adding a new resource no longer requires editing an existing controller - -### Fixed -- **`RESTPathResolver.ensureInitialized` race** — the `initialized` flag was being set *before* the domain route registrations ran, so concurrent callers could observe `initialized == true` while the `routes` dictionary was still empty. Now uses a separate `initLock` and sets the flag only after all `_*Routes` registrations complete. Fixes intermittent test failures where screenshot/localization `_links` resolved to top-level paths under parallel test execution -- **`include=primaryCategory,…` on app-infos request** — added to `SDKAppInfoRepository.listAppInfos` alongside `fields[appInfos]`. The ASC API returns only relationship `links` with `fields[]` alone; `include=` is required to populate `relationships.primaryCategory.data.id` so the mapper can surface `primaryCategoryId` etc. - -### Fixed -- **`AppInfo.primaryCategoryId` / secondary categories missing from `/v1/apps/{id}/appInfos`** — the ASC API returns relationship `data` sparsely unless the client requests the explicit sparse fieldset. `SDKAppInfoRepository.listAppInfos` now passes `fields[appInfos]=primaryCategory,primarySubcategoryOne,primarySubcategoryTwo,secondaryCategory,secondarySubcategoryOne,secondarySubcategoryTwo`, so the mapped `AppInfo` carries the category IDs - ---- - -## [0.16.9] - 2026-04-20 - -### Added -- **`asc review-submissions list`** — list App Store review submissions for an app. Required `--app-id`; optional `--state ` (e.g. `WAITING_FOR_REVIEW,IN_REVIEW,READY_FOR_REVIEW` or `UNRESOLVED_ISSUES`) and `--limit`. Backed by `SubmissionRepository.listSubmissions(appId:states:limit:)` -- **`asc certificates list` filtering flags** — `--limit` (server-side), `--expired-only` (client-side, drops unexpired certs), `--before ` (client-side, keeps certs with expirationDate strictly before the cutoff) -- **REST endpoint `GET /api/v1/apps/{appId}/review-submissions`** — new `ReviewSubmissionsController` in the `asc web-server`. Supports `?state=…&limit=…` query params; mirrors the CLI. **No fleet route** — Apple's OpenAPI spec marks `filter[app]` as required, so review submissions can only be listed per-app -- **REST endpoint `GET /api/v1/certificates` query params** — now honours `?type=&limit=&expired-only=&before=` (previously ignored). `before` accepts both full ISO8601 (`2026-11-01T00:00:00Z`) and date-only (`2026-11-01`, interpreted as midnight UTC). Same filter semantics as the CLI -- **REST endpoint `GET /api/v1/builds`** — fleet listing with optional `?app-id=&platform=&version=&limit=` query params. `/api/v1/apps/{appId}/builds` still works for the nested form - -### Changed -- **`CertificateRepository.listCertificates`** signature now takes `(certificateType:limit:)` — forwards `limit` to the SDK -- **`ReviewSubmission` REST links** — affordance migrated from raw `affordances` dictionary to `structuredAffordances`, so REST responses now render `_links` correctly (e.g. `/api/v1/apps/{appId}/versions`) - ---- - -## [0.1.68] - 2026-04-14 - -### Added -- **`asc testflight groups create`** — create external or internal TestFlight beta groups (`--internal` flag toggles internal; `--public-link-enabled` / `--feedback-enabled` for external groups) -- **`asc init` review contact flags** — `--contact-first-name`, `--contact-last-name`, `--contact-phone`, `--contact-email` save review contact info to `.asc/project.json` for reuse across versions -- **`ProjectConfig` review contact fields** — optional `contactFirstName`, `contactLastName`, `contactPhone`, `contactEmail` with `hasReviewContact` computed property -- **`setReviewContact` / `updateReviewContact` affordances** — `ProjectConfig` now suggests setting review contact when missing - ---- - -## [0.1.67] - 2026-04-13 - -### Changed -- Bug fixes and improvements. - ---- - -## [0.1.66] - 2026-04-09 - -### Added -- **`ThemeDesign` domain model** — composes from Gallery-native types (`GalleryPalette` + `[Decoration]`) for structured theme output. Generated by AI once, applied deterministically to all screenshots -- **`ThemeDesignApplier`** — re-renders through `GalleryHTMLRenderer.renderScreen()` pipeline with overridden palette and merged decorations (no HTML patching) -- **`GalleryPalette.textColor`** — optional explicit text color, overrides the auto-detect heuristic -- **`Decoration.label()` shape** — text/emoji decorative elements (e.g. `Decoration(shape: .label("✨"), ...)`) -- **`DecorationAnimation`** — float, drift, pulse, spin, twinkle animations for decorations -- **`ScreenLayout.withDecorations()`** — creates a copy with additional decorations -- **`GalleryHTMLRenderer.renderDecorations()`** — renders `ScreenLayout.decorations` (previously dead code) using `cqi` units -- **`buildDesignContext()` on `ScreenTheme`** — prompt method that instructs AI to return `ThemeDesign` JSON -- **`design()` on `ThemeProvider`/`ThemeRepository`** — generate a ThemeDesign from AI in one call -- **`--design-only` / `--apply-design` CLI flags** — batch theme workflow -- **REST endpoints** — `POST /app-shots/themes/design` and `POST /app-shots/themes/apply-design` - -### Changed -- **`GalleryHTMLRenderer` refactored to Mustache templates** — all HTML extracted from Swift into 7 `.mustache` template files using [swift-mustache](https://github.com/hummingbird-project/swift-mustache). The renderer only builds context dictionaries; all HTML, CSS colors (via CSS custom properties in `theme-vars.mustache`), and keyframe animations live in templates. Templates are pre-compiled at startup via `MustacheLibrary` for performance. Preview rendering is cached per template ID. -- **`DecorationShape.displayCharacter`** — computed property on the model instead of renderer logic -- **`GalleryPalette.isLight` + `headlineColor`** — theme detection and text color derivation moved from renderer to palette -- **`Decoration` extended** — new optional fields: `color`, `background`, `borderRadius`, `animation` -- **Theme selection no longer requires auto-compose** — clicking a theme applies immediately to slides with existing preview HTML via `ThemeDesign` (1 AI call for design, then deterministic apply to all slides) -- **Blitz plugin: `design()` implemented** — generates `ThemeDesign` via compose bridge `mode: "design"`, enabling the fast design→apply-design flow - ---- - -## [0.1.65] - 2026-04-06 - -### Changed -- **`Presentable` protocol** — domain models own their table headers and row values; eliminates `headers:`/`rowMapper:` boilerplate from 37 list commands -- **REST controllers with DI** — routes rewritten as controller structs (`AppsController`, `CodeSigningController`, etc.) with injected repo dependencies; repos created once at server startup, not per request -- **No more `Command.parse([])` in REST layer** — controllers call domain repos and operations directly; CLI and REST are equal thin adapters -- **`AffordanceRegistry` uses structured `Affordance`** — plugin affordances now render to both CLI commands and REST `_links` (previously CLI-only) -- **`ScreenshotTemplate` domain operations** — `apply(content:)`, `renderFragment(content:)` as rich domain methods; `ThemedPage` value type for page wrapping -- **`ScreenshotTemplate` Codable includes `previewHTML` and `deviceCount`** — REST consumers get preview data without special handling - ---- - -## [0.1.64] - 2026-04-05 - -### Added -- **REST API with HATEOAS** — `GET /api/v1` entry point plus 12 resource endpoints (apps, versions, builds, testflight, certificates, bundle-ids, devices, profiles, simulators, plugins, territories) calling domain repositories directly (in-process, no subprocess), returning JSON with `_links` for agent navigation -- **`APIRoot` model** — HATEOAS entry point at `GET /api/v1` listing all available top-level resources with navigable `_links` -- **Structured `Affordance` type** — single source of truth for both CLI commands and REST links; models define affordances once, rendered to either format by `OutputFormatter` -- **`APILink` and `AffordanceMode`** — domain types supporting dual-mode affordance rendering (`.cli` → `"affordances"`, `.rest` → `"_links"`) -- **`RESTPathResolver`** — resolves CLI command + params into REST API paths using a route table covering 35+ resource types across the full domain hierarchy -- **HTML-to-PNG export** — `--preview image` option on `asc app-shots templates apply` and `asc app-shots themes apply` renders composed HTML to PNG via WebKit, with `--image-output` for custom output path -- **`HTMLRenderer` protocol** — `@Mockable` domain protocol for HTML-to-image rendering, implemented by `WebKitHTMLRenderer` using WKWebView snapshot - ---- - -## [0.1.63] - 2026-04-02 - -### Added -- **Plugin Marketplace** — `asc plugins market list` and `asc plugins market search --query X` to browse and search plugins from [tddworks/asc-registry](https://github.com/tddworks/asc-registry) -- **Multi-source plugin registry** — `PluginSource` protocol with `GitHubPluginSource` implementation; composable sources in `PluginMarketRepository` -- **`asc plugins install --name X`** — download and install `.plugin` bundles from the marketplace -- **`asc plugins uninstall --name X`** — remove installed plugin bundles (matches by slug or registry ID) -- **Plugins page in Command Center** — web UI with Installed and Marketplace tabs, stats bar, install/uninstall with loading spinners -- **Enriched plugin manifest** — `manifest.json` now supports `description`, `author`, `repositoryURL`, `categories`; installed plugins display the same rich info as marketplace listings -- **Centralized plugin affordance merging** — `AffordanceRegistry` affordances merged automatically in `OutputFormatter` via `WithPluginAffordances`; domain models no longer need manual registry calls -- **Example plugin** — `examples/hello-plugin/` demonstrates `AffordanceRegistry`, server routes, and UI affordance handlers -- **Plugin registry repo** — [tddworks/asc-registry](https://github.com/tddworks/asc-registry) with `registry.json` and release assets for ASC Pro + Hello Plugin - -### Changed -- **Unified `Plugin` model** — merged `Plugin` and `MarketPlugin` into a single model with `isInstalled`, `downloadURL?`, `slug?` fields -- **Refactored plugin system** — replaced event-based script plugins with dylib `.plugin` bundle management -- **Removed event plugin system** — deleted `PluginEvent`, `PluginEventPayload`, `PluginResult`, `PluginRunner`, `PluginEventBus` and all infrastructure -- **Removed event bus from commands** — `BuildsUpload` and `VersionsSubmit` no longer emit plugin events -- **Apps page affordance buttons** — plugin affordances (e.g. `greet` from Hello Plugin) render as clickable buttons via `appAffordanceHandlers` registry - ---- - -## [0.1.62] - 2026-04-02 - -### Added -- **`asc web-server`** — Swift/Hummingbird HTTP+WebSocket server replacing Node.js. Single binary, zero external dependencies, HTTPS with self-signed cert -- **Plugin architecture** — `.plugin` bundles in `~/.asc/plugins/` extend the CLI with routes, commands, affordances, and UI. Dylibs built with `dynamic_lookup` (~300KB) -- **`AffordanceRegistry`** — plugins extend domain model affordances at runtime (e.g. pro plugin adds `stream` to booted simulators) -- **Affordance-driven UI** — web app renders buttons from CAEOAS affordances, not hardcoded features. Plugins register handlers via `window.simAffordanceHandlers` -- **Command Center Simulators page** — device list with search/filter, stats cards, affordance-driven actions - ---- - -## [0.1.61] - 2026-03-31 - -### Added -- **Screenshot image URLs** — `asc screenshots list` now returns `imageUrl` for each screenshot, so you can view and render real App Store images directly from CLI output -- **Screenshot platform filtering** — filter and browse screenshots by platform (iPhone, iPad, etc.) in the web UI -- **Simulator screenshot capture** — capture and browse simulator screenshots in the web gallery - ---- - -## [0.1.60] - 2026-03-31 - -### Added -- **`asc simulators list [--booted]`** — list available iOS simulators with state-aware affordances -- **`asc simulators boot --udid `** — boot a simulator -- **`asc simulators shutdown --udid `** — shut down a simulator -- **`asc simulators list/boot/shutdown`** — manage local iOS simulators with state-aware affordances - ---- - -## [0.1.59] - 2026-03-30 - -### Added -- **`asc builds list --platform`** — filter builds by platform (ios, macos, tvos, visionos) -- **`asc builds list --version`** — filter builds by marketing version (e.g. 1.0.0) -- **`asc builds next-number`** — get the next build number for a version/platform, ideal for CI/CD automation (resolves #13) -- **Build platform field** — builds now include platform from preReleaseVersion -- **Build number populated** — `buildNumber` field now maps from `Build.attributes.version` (was always nil) - -### Fixed -- **Global options (`--pretty`, `--output`, `--timeout`) now accepted by all commands** — previously 10 leaf commands (`builds add-beta-group`, `builds remove-beta-group`, `builds uploads delete`, `versions set-build`, `reviews responses delete`, `app-clip-experiences delete`, `app-clip-experience-localizations delete`, `analytics-reports delete`, `auth logout`, `auth use`) rejected these flags with "Unknown option" - -### Changed -- **`asc builds list`** — table output now includes Build Number and Platform columns -- **Build version semantics** — `version` field now holds the marketing version (from PreReleaseVersion) when available; `buildNumber` holds the build string -- **Builds sorted by newest first** — list results default to `-uploadedDate` sort order - ---- - -## [0.1.58] - 2026-03-23 - -### Changed -- Bug fixes and improvements. - ---- - -## [0.1.57] - 2026-03-23 - -### Changed -- **`asc web-server`** — `/command-center/`, `/console/`, and `/` now redirect (302) to the hosted apps at `asccli.app/command-center` and `asccli.app/console` instead of serving local files that no longer exist; startup banner updated to show hosted URLs - ---- - -## [0.1.56] - 2026-03-21 - -### Changed -- Bug fixes and improvements. - ---- - -## [0.1.55] - 2026-03-21 - -### Added -- **Iris private API support** — new `asc iris` command namespace for cookie-based App Store Connect private API access -- **`asc iris status`** — check iris cookie session availability (browser or environment source) -- **`asc iris apps list`** — list apps via iris `/v1/appBundles` endpoint -- **`asc iris apps create`** — create a new app with initial version, supports multi-platform (`--platforms IOS MAC_OS`) -- **Cookie-based authentication** — auto-extracts `myacinfo` and session cookies from Chrome/Safari/Firefox via SweetCookieKit, with `ASC_IRIS_COOKIES` env var fallback for CI/CD -- **SweetCookieKit dependency** — browser cookie extraction for iris authentication - ---- - -## [0.1.53] - 2026-03-18 - -### Changed -- **`asc web` → `asc web-server`** — renamed command to reflect its role as a local API proxy, no longer serves static files -- **Removed Hummingbird dependency** — web server replaced with embedded Node.js proxy (`apps/server.js`), reducing binary size and build dependencies -- **Unified web apps under `apps/`** — moved dashboard from `Sources/ASCCommand/Resources/web/` to `apps/asc-web-command-center/`, console from `homepage/asc-web-console/` to `apps/asc-web-console/` -- **Dual-mode API detection** — both web apps auto-detect local proxy (tries relative `/api/run`, then `localhost:8420`, then falls back to mock mode) - -### Added -- **SPM build plugin (`Plugins/EmbedServerJS/`)** — auto-generates Swift source from `apps/server.js` at build time, embedding the API proxy in the binary with `apps/server.js` as single source of truth -- **Command Center nav link on homepage** — added "Command Center" navigation to homepage template with translations in all 12 languages -- **GitHub Pages deployment for web apps** — deploy workflow now copies `apps/asc-web-*` to homepage for static hosting - -### Removed -- `WebServer.swift` (Hummingbird-based static file server) -- `homepage/asc-web-console/server.py` (Python dev server) -- Hummingbird package dependency - ---- - -## [0.1.52] - 2026-03-18 - -### Changed -- Bug fixes and improvements. - ---- - -## [0.1.51] - 2026-03-17 - -### Added -- `asc web` management dashboard — complete redesign with professional light/dark theme, Plus Jakarta Sans typography, and self-contained CSS (no Tailwind CDN dependency) -- App context selector in sidebar — select an app from the Apps page and all other sections (Versions, Builds, TestFlight, Reviews, IAP, Subscriptions) use that app context automatically -- Rich domain models in web UI — every entity carries parent IDs, semantic booleans (`isLive`, `isEditable`, `isPending`, `isUsable`), and state-aware CAEOAS affordances matching the Swift Domain layer exactly -- Mock/CLI dual data mode — web UI auto-detects the `asc web` backend; falls back to built-in mock data for offline development and demos; toggle via header button -- Dark mode with system preference detection and localStorage persistence -- 15 management pages: Command Center, Apps, Versions, Builds, TestFlight, Submissions, App Info, Screenshots, Reviews, In-App Purchases, Subscriptions, Reports, Code Signing, Xcode Cloud, Users & Roles -- Command Center with release pipeline timeline, quick actions, and stats overview -- Command log modal showing every CLI command executed by the UI -- Standalone development server (`apps/asc-web-command-center/server.js`) for working on the web UI outside of `asc web` - ---- - -## [0.1.50] - 2026-03-15 - -### Added -- `asc territories list` — list all available App Store territories with currency codes -- `asc iap-availability get --iap-id ` — get territory availability for an in-app purchase (includes currency per territory) -- `asc iap-availability create --iap-id --available-in-new-territories --territory USA --territory CHN` — create territory availability for an IAP -- `asc subscription-availability get --subscription-id ` — get territory availability for a subscription (includes currency per territory) -- `asc subscription-availability create --subscription-id --available-in-new-territories --territory USA` — create territory availability for a subscription -- `Territory` domain model with id and currency, shared across availability responses -- `getAvailability` affordance on `InAppPurchase` and `Subscription` models for CAEOAS navigation -- `listTerritories` affordance on availability models for territory discovery -- `asc app-availability get --app-id ` — get per-territory availability with `isAvailable`, `contentStatuses` (blocking reasons), `releaseDate`, and `isPreOrderEnabled` for every territory - ---- +## Older releases -## [0.1.49] - 2026-03-14 - -### Added -- `asc skills list` — list available skills from the asc-cli repository via `npx skills add tddworks/asc-cli --list` -- `asc skills install --name ` — install a specific skill; `--all` to install all available skills -- `asc skills installed` — show skills installed in `~/.claude/skills/` with CAEOAS affordances -- `asc skills uninstall --name ` — remove an installed skill from `~/.claude/skills/` -- `asc skills check` — check for available skill updates via `npx skills check` -- `asc skills update` — update installed skills via `npx skills update` -- Auto-update checker — non-blocking skill update check on every `asc` command (24h cooldown, CI-aware, disable with `ASC_SKIP_SKILL_CHECK=true`) -- `--app-id` flag on `asc app-wall submit` — accepts App Store Connect app IDs directly, auto-constructs App Store URLs (repeatable, combinable with `--app` and `--developer-id`) - ---- - -## [0.1.48] - 2026-03-13 - -### Changed -- `--developer` on `asc app-wall submit` is now optional — when omitted, the homepage card uses the iTunes artist name instead -- `asc beta-review submissions list --build-id ` — list beta app review submissions for a build -- `asc beta-review submissions create --build-id ` — submit a build for beta (TestFlight external) review -- `asc beta-review submissions get --submission-id ` — get a specific beta review submission -- `asc beta-review detail get --app-id ` — get beta review contact info and demo account details -- `asc beta-review detail update --detail-id ` — update beta review contact info, demo account, and notes - ---- - -## [0.1.47] - 2026-03-13 - -### Changed -- Bug fixes and improvements. - ---- - -## [0.1.46] - 2026-03-13 - -### Added -- `asc builds archive --scheme ` — archive and export Xcode projects locally via `xcodebuild`, auto-detecting workspace/project from the current directory; supports `--platform`, `--configuration`, `--export-method` (app-store/ad-hoc/development/enterprise), and `--output-dir` -- `--upload` flag on `asc builds archive` chains the exported IPA/PKG directly into App Store Connect upload, combining archive + export + upload in a single command - ---- - -## [0.1.45] - 2026-03-12 - -### Changed -- Subscription offer codes: `asc subscription-offer-codes list/create/update`, `asc subscription-offer-code-custom-codes list/create/update`, `asc subscription-offer-code-one-time-codes list/create/update` — manage offer codes, custom redeemable codes, and one-time use code batches for auto-renewable subscriptions -- IAP offer codes: `asc iap-offer-codes list/create/update`, `asc iap-offer-code-custom-codes list/create/update`, `asc iap-offer-code-one-time-codes list/create/update` — manage offer codes, custom redeemable codes, and one-time use code batches for in-app purchases -- `InAppPurchase` and `Subscription` affordances now include `listOfferCodes` for direct navigation to offer codes - ---- - -## [0.1.44] - 2026-03-12 - -### Added -- Power & performance metrics: `asc perf-metrics list --app-id ` and `asc perf-metrics list --build-id ` — download launch time, hang rate, memory, disk, battery, termination, and animation metrics with `--metric-type` filter -- Diagnostic signatures: `asc diagnostics list --build-id ` — list hang, disk write, and launch diagnostic hotspots with `--diagnostic-type` filter -- Diagnostic logs: `asc diagnostic-logs list --signature-id ` — view call stack metadata for a specific diagnostic signature - ---- - -## [0.1.43] - 2026-03-11 - -### Added -- HTML screenshot generation: `asc app-shots html --plan composition-plan.json` — deterministic App Store screenshot generation with real device mockup frames, no AI or API keys needed -- CompositionPlan format: normalized 0-1 coordinates, multiple devices per slide, text overlays with alignment, gradient backgrounds, per-screen color themes -- Device mockup system: bundled iPhone 17 Pro Max frame with `mockups.json` config; users can add custom mockups to `~/.asc/mockups/` -- TextOverlay `textAlign` property: supports `left`, `center`, `right` alignment for text positioning in composition plans -- Client-side PNG export via html-to-image CDN in generated HTML pages - ---- - -## [0.1.42] - 2026-03-11 - -### Added -- Customer reviews: `asc reviews list --app-id ` and `asc reviews get --review-id ` — list and view customer reviews with rating, title, body, reviewer nickname, and territory -- Review responses: `asc review-responses create --review-id --response-body "text"`, `asc review-responses get --review-id `, `asc review-responses delete --response-id ` — manage developer responses to customer reviews - -### Changed -- Bug fixes and improvements. - ---- - -## [0.1.41] - 2026-03-10 - -### Added -- Sales reports: `asc sales-reports download` with support for 10 report types, 5 sub-types, and 4 frequencies -- Finance reports: `asc finance-reports download` with financial and finance detail report types -- Gzip decompression and TSV parsing infrastructure for App Store Connect report downloads -- Analytics reports: `asc analytics-reports request/list/delete/reports/instances/segments` — multi-step analytics workflow with 5 report categories and 3 granularity levels -- `asc auth login --vendor-number ` — save vendor number with account credentials -- `asc auth update --vendor-number ` — add or update vendor number on an existing account -- Vendor number auto-resolution for `sales-reports download` and `finance-reports download` — `--vendor-number` is now optional when saved on the active account - ---- - -## [0.1.40] - 2026-03-09 - -### Changed -- Bug fixes and improvements. - ---- - -## [0.1.39] - 2026-03-08 - -### Added -- `asc app-clips list --app-id ` — list App Clips for an app; `AppClip` carries `appId` (injected), `bundleId?`; affordances: `listAppClips`, `listExperiences` -- `asc app-clip-experiences list --app-clip-id ` — list default experiences for an App Clip; `AppClipDefaultExperience` carries `appClipId` (injected), `action?` (OPEN/VIEW/PLAY); affordances: `delete`, `listExperiences`, `listLocalizations` -- `asc app-clip-experiences create --app-clip-id [--action OPEN|VIEW|PLAY]` — create a default experience -- `asc app-clip-experiences delete --experience-id ` — delete a default experience -- `asc app-clip-experience-localizations list --experience-id ` — list localizations; `AppClipDefaultExperienceLocalization` carries `experienceId` (injected), `locale`, `subtitle?`; affordances: `delete`, `listLocalizations` -- `asc app-clip-experience-localizations create --experience-id --locale [--subtitle "..."]` — create a localization -- `asc app-clip-experience-localizations delete --localization-id ` — delete a localization -- `AppClipRepository` `@Mockable` protocol with 7 methods covering all CRUD operations -- `asc game-center detail get --app-id ` — get Game Center configuration for an app; `GameCenterDetail` carries `appId` (injected), `isArcadeEnabled`; affordances: `getDetail`, `listAchievements`, `listLeaderboards` -- `asc game-center achievements list --detail-id ` — list Game Center achievements; `GameCenterAchievement` carries `gameCenterDetailId` (injected), `referenceName`, `vendorIdentifier`, `points`, `isShowBeforeEarned`, `isRepeatable`, `isArchived`; affordances: `listAchievements`, `delete` -- `asc game-center achievements create --detail-id --reference-name --vendor-identifier --points [--show-before-earned] [--repeatable]` — create a new achievement -- `asc game-center achievements delete --achievement-id ` — delete an achievement -- `asc game-center leaderboards list --detail-id ` — list Game Center leaderboards; `GameCenterLeaderboard` carries `gameCenterDetailId` (injected), `referenceName`, `vendorIdentifier`, `scoreSortType` (ASC/DESC), `submissionType` (BEST_SCORE/MOST_RECENT_SCORE), `isArchived`; affordances: `listLeaderboards`, `delete` -- `asc game-center leaderboards create --detail-id --reference-name --vendor-identifier --score-sort-type ASC|DESC [--submission-type BEST_SCORE|MOST_RECENT_SCORE]` — create a new leaderboard -- `asc game-center leaderboards delete --leaderboard-id ` — delete a leaderboard -- `ScoreSortType` and `LeaderboardSubmissionType` domain enums -- `GameCenterRepository` `@Mockable` protocol with 7 methods covering all CRUD operations - ---- - -## [0.1.38] - 2026-03-07 - -### Added -- `asc xcode-cloud products list [--app-id ]` — list Xcode Cloud products; `XcodeCloudProduct` carries `appId` (injected from relationship), `name`, `productType`; affordances: `listWorkflows`, `listProducts` -- `asc xcode-cloud workflows list --product-id ` — list CI workflows for a product; `XcodeCloudWorkflow` carries `productId` (injected), `name`, `description`, `isEnabled`, `isLockedForEditing`; affordances: `listBuildRuns`, `listWorkflows` always, `startBuild` only when `isEnabled` -- `asc xcode-cloud builds list --workflow-id ` — list build runs for a workflow; `XcodeCloudBuildRun` carries `workflowId` (injected), `number`, `executionProgress` (PENDING/RUNNING/COMPLETE), `completionStatus` (SUCCEEDED/FAILED/ERRORED/CANCELED/SKIPPED), `startReason`; semantic booleans: `isPending`, `isRunning`, `isComplete`, `isSucceeded`, `hasFailed` -- `asc xcode-cloud builds get --build-run-id ` — get a specific build run by ID -- `asc xcode-cloud builds start --workflow-id [--clean]` — start a new build run; `--clean` flag performs a clean build removing derived data - ---- - -## [0.1.37] - 2026-03-06 - -### Changed -- Bug fixes and improvements. - ---- - -## [0.1.36] - 2026-03-05 - -### Added -- `asc users list/update/remove` — manage App Store Connect team members; list users filtered by role, update a member's roles (replaces all current roles), or revoke access immediately; `TeamMember` carries `username`, `firstName`, `lastName`, `roles`, `isAllAppsVisible`, `isProvisioningAllowed`; affordances: `remove`, `updateRoles` (pre-filled with current roles) -- `asc user-invitations list/invite/cancel` — manage pending team invitations; invite a new member with one or more roles (`--role DEVELOPER --role APP_MANAGER`); `UserInvitationRecord` carries `email`, `roles`, `expirationDate`; affordance: `cancel`; supports 13 roles; `--all-apps-visible` flag on invite grants access to all apps -- `asc auth list` — list all saved App Store Connect accounts with active status and per-account affordances (`use`, `logout`) -- `asc auth use ` — switch the active App Store Connect account; all subsequent commands use the newly active account -- `asc auth login --name ` — optional `--name` flag to save credentials under a human-readable alias (defaults to `"default"`); first saved account becomes active automatically; subsequent logins with new names are saved alongside existing accounts; account names must not contain spaces (use hyphens or underscores) - -### Changed -- `asc auth logout` — now accepts optional `--name ` flag; removes the named account or the active account if `--name` is omitted -- `asc auth check` — now shows the account `name` field when credentials come from a saved account (omitted for environment-variable credentials) -- `~/.asc/credentials.json` — upgraded to multi-account format `{ "active": "name", "accounts": { ... } }`; old single-credential files are auto-migrated to a `"default"` named account on first use - ---- - -## [0.1.35] - 2026-03-04 - -### Added -- `asc app-wall submit` — submit your app to the community app wall at asccli.app by opening a GitHub pull request against `tddworks/asc-cli`; forks the repo, adds your entry to `homepage/apps.json`, and creates a PR automatically; supports `--developer-id` (auto-fetches all your App Store apps), `--app` (specific App Store URLs, repeatable), `--github`, `--x`; GitHub token resolved from `--github-token` flag, `$GITHUB_TOKEN`, or `gh auth token` - ---- - -## [0.1.34] - 2026-03-02 - -### Fixed -- `asc app-shots translate` — no longer regenerates the visual design (background, colors, device mockup, layout) from the original plan specs; now sends a simple "edit this image, translate only the text overlays" prompt so the existing generated screenshot is preserved exactly - ---- - -## [0.1.33] - 2026-03-02 - -### Added -- `asc init` — initialise project context by saving the app ID, name, and bundle ID to `.asc/project.json` in the current directory; supports `--app-id` (direct), `--name` (search by name), or auto-detect from `.xcodeproj` bundle identifier; output includes CAEOAS affordances for common next steps - ---- - -## [0.1.32] - 2026-03-01 - -### Changed -- Bug fixes and improvements. - ---- - -## [0.1.31] - 2026-03-01 - -### Added -- **Plugin system** — users can install executable plugins in `~/.asc/plugins//` to extend the CLI with custom event handlers (e.g., Slack/Telegram notifications) -- `asc plugins list` — list all installed plugins with name, version, enabled status, and subscribed events -- `asc plugins install ` — install a plugin from a local directory containing `manifest.json` and a `run` executable -- `asc plugins uninstall --name ` — remove an installed plugin -- `asc plugins enable --name ` — re-enable a disabled plugin -- `asc plugins disable --name ` — disable a plugin without removing it -- `asc plugins run --name --event ` — manually invoke a plugin for testing; supports `--app-id`, `--version-id`, `--build-id` payload flags -- Auto-event emission: `asc builds upload` fires `build.uploaded` after a successful upload; `asc versions submit` fires `version.submitted` after a successful submission -- Plugin protocol: plugins receive a JSON event payload on stdin and write a `{"success": bool, "message": "..."}` result to stdout -- `version-review-detail get --version-id ` — fetch the App Store review contact info and demo account settings for a version -- `asc version-review-detail update --version-id [flags]` — upsert review info (creates if none exists, patches if already set); supports `--contact-first-name`, `--contact-last-name`, `--contact-phone`, `--contact-email`, `--demo-account-required`, `--demo-account-name`, `--demo-account-password`, `--notes` -- `notes` field added to `AppStoreReviewDetail` domain model -- `getReviewDetail` affordance added to `AppStoreVersion` for agent navigation - ---- - -## [0.1.30] - 2026-03-01 - -### Added -- `asc age-rating get --app-info-id ` — fetch the full age rating declaration for an app info, including all content intensity ratings, boolean flags, kids age band, and region overrides -- `asc age-rating update --declaration-id [flags]` — update individual age rating fields via PATCH; supports all 9 boolean flags (`--advertising`, `--gambling`, `--loot-box`, etc.) and 13 intensity ratings (`--violence-realistic`, `--profanity`, `--sexual-content`, etc.) plus `--kids-age-band`, `--age-rating-override`, and `--korea-age-rating-override` -- `getAgeRating` affordance added to `AppInfo` for agent navigation -- `--privacy-choices-url` and `--privacy-policy-text` flags on `asc app-info-localizations update` — expose the two remaining updatable privacy fields from the App Store Connect API -- `asc app-info-localizations delete --localization-id ` — remove a per-locale metadata entry; `delete` affordance added to `AppInfoLocalization` -- `asc app-infos update --app-info-id [--primary-category] [--primary-subcategory-one] [--primary-subcategory-two] [--secondary-category] [--secondary-subcategory-one] [--secondary-subcategory-two]` — set or update all 6 category relationship fields on an AppInfo; `updateCategories` affordance added to `AppInfo` -- `asc app-categories list [--platform IOS|MAC_OS|TV_OS]` — list all available App Store categories and subcategories; returns a flat list combining top-level (`data[]`) and subcategories (`included[]`) from the API; `AppCategory` domain model with `parentId` for subcategory identification - ---- - -## [0.1.29] - 2026-03-01 - -### Added -- `--style-reference ` flag on `asc app-shots generate` and `asc app-shots translate` — pass any PNG/JPEG as a visual style guide; Gemini replicates the reference's colors, typography, gradients, and layout patterns without copying its content; the reference image is sent as the first part in the Gemini request followed by an explicit style-guide instruction - ---- - -## [0.1.28] - 2026-02-28 - -### Added -- `--device-type` flag on `asc app-shots generate` and `asc app-shots translate` — accepts named App Store display type constants (`APP_IPHONE_69`, `APP_IPHONE_67`, `APP_IPAD_PRO_129`, etc.) and automatically sets the correct `--output-width`/`--output-height`; overrides explicit dimension flags when both are provided; all 16 device types supported across iPhone, iPad, Apple TV, Mac, and Apple Vision Pro - -### Fixed -- `asc app-shots generate` and `asc app-shots translate` now upscale Gemini output to the correct App Store dimensions using CoreGraphics. Gemini returns ~704×1520; the required iPhone 6.9" size is 1320×2868. New `--output-width` (default `1320`) and `--output-height` (default `2868`) flags control the target size for all other device types. - ---- - -## [0.1.27] - 2026-02-28 - -### Added -- `asc app-shots translate` — one-shot localization of generated screenshots; reads the English plan + existing `screen-{n}.png` files, sends them to Gemini with per-locale translation instructions, writes `{output-dir}/{locale}/screen-{n}.png`; supports multiple locales in one invocation (`--to zh --to ja --to ko`); locales processed in parallel via `TaskGroup` -- `asc app-shots generate` — AI-powered App Store screenshot generation using Gemini; reads a `ScreenPlan` JSON + screenshot images, calls Gemini image generation API, writes `screen-{index}.png` files; `--plan` defaults to `.asc/app-shots/app-shots-plan.json`, `--output-dir` defaults to `.asc/app-shots/output`, screenshots auto-discovered from plan directory when not provided — zero-argument happy path: `asc app-shots generate` -- `asc app-shots config` — persistent Gemini API key management; `--gemini-api-key` saves to `~/.asc/app-shots-config.json`, bare invocation shows current key + source (file/env), `--remove` deletes it; `generate` resolves key from flag → env var → stored config -- `ScreenPlan`, `ScreenConfig`, `ScreenTone`, `LayoutMode`, `ScreenColors`, `AppShotsConfig` domain models -- `ScreenshotGenerationRepository` + `AppShotsConfigStorage` `@Mockable` protocols -- `GeminiScreenshotGenerationRepository` — native Gemini `generateContent` API with `responseModalities: ["TEXT","IMAGE"]`, parallel TaskGroup generation -- `FileAppShotsConfigStorage` — saves Gemini API key to `~/.asc/app-shots-config.json` - ---- - -## [0.1.25] - 2026-02-27 - -### Added -- `asc subscriptions submit` — submit a subscription for App Store review; `submit` affordance on `Subscription` appears only when `state == READY_TO_SUBMIT` -- `asc subscription-offers list` — list introductory offers for a subscription -- `asc subscription-offers create` — create a `FREE_TRIAL`, `PAY_AS_YOU_GO`, or `PAY_UP_FRONT` introductory offer; validates that `--price-point-id` is provided for paid modes -- `SubscriptionIntroductoryOffer` domain model with `SubscriptionOfferDuration` and `SubscriptionOfferMode` enums; `requiresPricePoint` semantic boolean on `SubscriptionOfferMode` -- `createIntroductoryOffer` and `listIntroductoryOffers` CAEOAS affordances on `Subscription` -- `asc iap list` — list in-app purchases (consumable, non-consumable, non-renewing subscription) for an app -- `asc iap create` — create a new in-app purchase with reference name, product ID, and type -- `asc iap-localizations list` — list localizations for an in-app purchase -- `asc iap-localizations create` — create a per-locale name and description for an in-app purchase -- `asc subscription-groups list` — list subscription groups for an app -- `asc subscription-groups create` — create a new subscription group -- `asc subscriptions list` — list subscriptions within a group -- `asc subscriptions create` — create a subscription with period (ONE_WEEK–ONE_YEAR), family sharing, and group level -- `asc subscription-localizations list` — list per-locale metadata for a subscription -- `asc subscription-localizations create` — create a per-locale name and description for a subscription -- `asc iap submit` — submit an in-app purchase for App Store review -- `asc iap price-points list` — list available price tiers for an IAP, optionally filtered by territory -- `asc iap prices set` — set the price schedule for an IAP (base territory + auto-pricing for all others) -- `InAppPurchaseSubmission`, `InAppPurchasePricePoint`, `InAppPurchasePriceSchedule` domain models with CAEOAS affordances -- `InAppPurchaseState` and `SubscriptionState` enums with semantic booleans (`isApproved`, `isLive`, `isEditable`, `isPendingReview`) -- CAEOAS affordances on all new models linking to sibling and child commands -- `asc app-preview-sets list` — list App Store video preview sets for a version localization -- `asc app-preview-sets create` — create a new preview set for a specific device type -- `asc app-previews list` — list video previews in a preview set -- `asc app-previews upload` — upload a video file (`.mp4`, `.mov`, `.m4v`) to a preview set using a 3-step upload flow (reserve → PUT chunks → PATCH with MD5) -- `PreviewType` enum with 16 device cases (iPhone, iPad, Mac, Apple TV, Apple Vision Pro) -- `AppPreview.VideoDeliveryState` with 5 states including `PROCESSING` (unique to video) -- Renamed `--group-id` to `--beta-group-id` in all `asc testflight testers` commands for consistency with `asc builds add-beta-group` - ---- - -## [0.1.24] - 2026-02-27 - ---- - -## [0.1.23] - 2026-02-26 - ---- - -## [0.1.22] - 2026-02-26 - ---- - -## [0.1.21] - 2026-02-26 - ---- - -## [0.1.20] - 2026-02-26 - ---- - -## [0.1.9] - 2026-02-25 - ---- - -## [0.1.8] - 2026-02-25 - ---- - -## [0.1.7] - 2026-02-25 - ---- - -## [0.1.6] - 2026-02-24 - ---- - -## [0.1.5] - 2026-02-24 - ---- - -## [0.1.4] - 2026-02-24 - -### Added -- **Persistent Auth Login**: Save API key credentials to `~/.asc/credentials.json` so environment variables are not required on every session. - - `asc auth login --key-id --issuer-id --private-key-path ` — save credentials to disk - - `asc auth logout` — remove saved credentials - - `asc auth check` — verify credentials and show their source (`file` or `environment`) - - Credential resolution order: `~/.asc/credentials.json` → environment variables (fully backwards-compatible) - - `asc auth check` now outputs JSON with an `affordances` field (same agent-first format as all other commands) - -### Technical -- Added `AuthStorage` `@Mockable` protocol (`save`, `load`, `delete`) in Domain -- Added `AuthStatus` domain model (`keyID`, `issuerID`, `source: CredentialSource`) with `AffordanceProviding` -- Added `CredentialSource` enum (`.file` / `.environment`) as `Codable` String raw value -- `AuthCredentials` gains `Codable` conformance for JSON file serialization -- `FileAuthStorage` (Infrastructure) reads/writes `~/.asc/credentials.json` using `JSONEncoder`/`JSONDecoder` -- `FileAuthProvider` (Infrastructure) implements `AuthProvider` backed by `FileAuthStorage` -- `CompositeAuthProvider` (Infrastructure) tries file credentials first, falls back to `EnvironmentAuthProvider` -- `ClientProvider` updated to use `CompositeAuthProvider` — all existing commands gain file-based auth transparently - ---- - -## [0.1.3] - 2026-02-24 - -### Added -- **Version Localization Update**: Set What's New text, description, keywords, marketing URL, support URL, and promotional text for any locale directly from the CLI. - - `asc version-localizations update --localization-id --whats-new "text"` — update What's New - - `asc version-localizations update --localization-id --description "text" --keywords "a,b,c"` — update other fields - - All content fields are optional — only provided fields are sent to the API -- **App Info Localizations**: Manage per-locale app metadata (name, subtitle, privacy policy) directly from the CLI. Each locale's App Store listing information is now fully writable. - - `asc app-infos list --app-id ` — list AppInfo records for an app - - `asc app-info-localizations list --app-info-id ` — list all locale entries - - `asc app-info-localizations create --app-info-id --locale --name ` — add a new locale - - `asc app-info-localizations update --localization-id [--name] [--subtitle] [--privacy-policy-url]` — patch one or more fields - -### Technical -- Introduced `VersionLocalizationRepository` (`@Mockable` protocol) with `listLocalizations`, `createLocalization`, `updateLocalization` — separate from `ScreenshotRepository` which now handles only screenshot sets and images -- Added `SDKLocalizationRepository` implementing `VersionLocalizationRepository`; converts `URL?` ↔ `String?` for `marketingURL`/`supportURL` SDK fields -- `AppStoreVersionLocalization` gains 6 optional text fields with nil-safe Codable (`encodeIfPresent`) — nil fields are omitted from JSON output -- `AppStoreVersionLocalization.affordances` now includes `updateLocalization` command -- `ScreenshotsImport` updated to accept `localizationRepo` + `screenshotRepo` as separate parameters -- Added `AppInfo` and `AppInfoLocalization` domain models carrying parent IDs for agent navigation -- Added `AppInfoRepository` `@Mockable` protocol with `listAppInfos`, `listLocalizations`, `createLocalization`, `updateLocalization` -- Added `SDKAppInfoRepository` mapping `privacyPolicyURL`/`privacyChoicesURL` SDK fields; extracts `appInfoId` from PATCH response relationships -- Updated `App.affordances` to include `listAppInfos` command -- Added `docs/features/version-localizations.md`, `docs/features/app-info-localizations.md` - ---- - -## [0.1.1] - 2026-02-22 - -### Added -- **App Store Version Management**: Browse and create versions for any app across all platforms (iOS, macOS, tvOS, watchOS, visionOS). - - `asc versions list --app-id ` - - `asc versions create --app-id --version --platform ` -- **Localization Management**: List and create App Store version localizations. - - `asc version-localizations list --version-id ` - - `asc version-localizations create --version-id --locale ` -- **Screenshot Sets**: List and create screenshot sets for a localization (one set per display type: iPhone 6.7", iPad Pro 12.9", Mac, etc.). - - `asc screenshot-sets list --localization-id ` - - `asc screenshot-sets create --localization-id --display-type ` -- **Screenshots**: List and upload screenshots within a set. - - `asc screenshots list --set-id ` — with file size, dimensions, and delivery state - - `asc screenshots upload --set-id --file ` — three-step ASC flow (reserve → S3 upload → commit) handled automatically -- **Submit for Review**: Submit an App Store version for review in one command. Reuses an existing open review submission if one is already pending. - - `asc versions submit --version-id ` -- **TestFlight**: List beta groups and testers. - - `asc testflight groups [--app-id ]` - - `asc testflight testers --group-id ` -- **TUI Screenshot Browser**: Interactive terminal UI now supports the full screenshot hierarchy — app → version → platform → locale → screenshot set. -- **Agent-First Output (CAEOAS)**: JSON responses include an `affordances` field with ready-to-run follow-up commands so AI agents can navigate the full resource tree without knowing the command structure. - ```json - { "data": [{ "id": "...", "affordances": { "listVersions": "asc versions list --app-id ..." } }] } - ``` -- **Homebrew Tap**: Install via Homebrew with native architecture binaries (arm64 and x86_64). - ```bash - brew install asc-tools/tap/asc - ``` -- **Homepage**: Static landing page at [asccli.app](https://asccli.app) with English, Chinese, and Japanese localizations. - -### Fixed -- Submission flow now correctly includes the app relationship in the version request, preventing API 422 errors. -- Submission flow reuses an unresolved (open) review submission instead of always creating a new one. - -### Technical -- `AppStoreVersion`, `AppStoreVersionState`, `AppStorePlatform` models with semantic booleans (`isLive`, `isEditable`, `isPending`) -- `AppStoreVersionLocalization`, `AppScreenshotSet`, `AppScreenshot` models; all carry parent IDs -- `ScreenshotDisplayType` enum with 39 display types, device categories, and human-readable names -- `AffordanceProviding` protocol; `OutputFormatter.formatAgentItems()` merges affordances into JSON -- `SubmissionRepository` orchestrating 4-step review submission API flow -- `ReviewSubmission` domain model with `ReviewSubmissionState` and `isWaitingForReview` -- `SequencedStubAPIClient` for multi-step API flow tests -- `APIClient` protocol abstraction enabling `StubAPIClient` for infrastructure tests -- Automated GitHub Actions release workflow with binary artifacts and Homebrew formula update -- CI/CD Codecov coverage reporting - ---- - -## [0.1.0] - 2026-02-10 - -### Added -- **Apps**: List and get apps on your App Store Connect account. - - `asc apps list` -- **Builds**: List builds for an app with processing state, version, and expiry. - - `asc builds list [--app-id ]` -- **TUI**: Interactive terminal UI (`asc tui`) for browsing apps and builds without remembering IDs. -- **Auth Check**: Verify environment credentials are valid. - - `asc auth check` -- **Output Formats**: All commands support `--output json` (default), `--output table`, and `--output markdown`. Use `--pretty` for formatted JSON. -- **Environment Auth**: Configure via `ASC_KEY_ID`, `ASC_ISSUER_ID`, and `ASC_PRIVATE_KEY_PATH` (or `ASC_PRIVATE_KEY` for inline PEM). - -### Technical -- Three-layer architecture: `ASCCommand → Infrastructure → Domain` -- `appstoreconnect-swift-sdk` adapter with clean domain model separation -- `App`, `Build`, `BetaGroup`, `BetaTester` domain models (`Sendable`, `Equatable`, `Identifiable`, `Codable`) -- `AppRepository`, `BuildRepository`, `TestFlightRepository` `@Mockable` protocols -- `ClientFactory` + `ClientProvider` dependency injection wiring -- Apple `@Testing` macro test suite following Chicago School TDD - ---- +[0.17](docs/changelog/0.17.md) · [0.16](docs/changelog/0.16.md) · [0.1](docs/changelog/0.1.md) [Unreleased]: https://github.com/tddworks/asc-cli/compare/v0.18.4...HEAD [0.18.4]: https://github.com/tddworks/asc-cli/compare/v0.18.3...v0.18.4 @@ -999,67 +78,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [0.18.2]: https://github.com/tddworks/asc-cli/compare/v0.18.1...v0.18.2 [0.18.1]: https://github.com/tddworks/asc-cli/compare/v0.18.0...v0.18.1 [0.18.0]: https://github.com/tddworks/asc-cli/compare/v0.17.9...v0.18.0 -[0.17.9]: https://github.com/tddworks/asc-cli/compare/v0.17.8...v0.17.9 -[0.17.8]: https://github.com/tddworks/asc-cli/compare/v0.17.7...v0.17.8 -[0.17.7]: https://github.com/tddworks/asc-cli/compare/v0.1.74...v0.17.7 -[0.1.74]: https://github.com/tddworks/asc-cli/compare/v0.17.3...v0.1.74 -[0.17.3]: https://github.com/tddworks/asc-cli/compare/v0.17.2...v0.17.3 -[0.17.2]: https://github.com/tddworks/asc-cli/compare/v0.17.1...v0.17.2 -[0.17.1]: https://github.com/tddworks/asc-cli/compare/v0.17.0...v0.17.1 -[0.17.0]: https://github.com/tddworks/asc-cli/compare/v0.16.9...v0.17.0 -[0.16.9]: https://github.com/tddworks/asc-cli/compare/v0.1.68...v0.16.9 -[0.1.68]: https://github.com/tddworks/asc-cli/compare/v0.1.67...v0.1.68 -[0.1.67]: https://github.com/tddworks/asc-cli/compare/v0.1.66...v0.1.67 -[0.1.66]: https://github.com/tddworks/asc-cli/compare/v0.1.65...v0.1.66 -[0.1.65]: https://github.com/tddworks/asc-cli/compare/v0.1.64...v0.1.65 -[0.1.64]: https://github.com/tddworks/asc-cli/compare/v0.1.63...v0.1.64 -[0.1.63]: https://github.com/tddworks/asc-cli/compare/v0.1.62...v0.1.63 -[0.1.62]: https://github.com/tddworks/asc-cli/compare/v0.1.61...v0.1.62 -[0.1.61]: https://github.com/tddworks/asc-cli/compare/v0.1.60...v0.1.61 -[0.1.60]: https://github.com/tddworks/asc-cli/compare/v0.1.59...v0.1.60 -[0.1.59]: https://github.com/tddworks/asc-cli/compare/v0.1.58...v0.1.59 -[0.1.58]: https://github.com/tddworks/asc-cli/compare/v0.1.57...v0.1.58 -[0.1.57]: https://github.com/tddworks/asc-cli/compare/v0.1.56...v0.1.57 -[0.1.56]: https://github.com/tddworks/asc-cli/compare/v0.1.55...v0.1.56 -[0.1.55]: https://github.com/tddworks/asc-cli/compare/v0.1.53...v0.1.55 -[0.1.53]: https://github.com/tddworks/asc-cli/compare/v0.1.52...v0.1.53 -[0.1.52]: https://github.com/tddworks/asc-cli/compare/v0.1.51...v0.1.52 -[0.1.51]: https://github.com/tddworks/asc-cli/compare/v0.1.50...v0.1.51 -[0.1.50]: https://github.com/tddworks/asc-cli/compare/v0.1.49...v0.1.50 -[0.1.49]: https://github.com/tddworks/asc-cli/compare/v0.1.48...v0.1.49 -[0.1.48]: https://github.com/tddworks/asc-cli/compare/v0.1.47...v0.1.48 -[0.1.47]: https://github.com/tddworks/asc-cli/compare/v0.1.46...v0.1.47 -[0.1.46]: https://github.com/tddworks/asc-cli/compare/v0.1.45...v0.1.46 -[0.1.45]: https://github.com/tddworks/asc-cli/compare/v0.1.43...v0.1.45 -[0.1.43]: https://github.com/tddworks/asc-cli/compare/v0.1.42...v0.1.43 -[0.1.42]: https://github.com/tddworks/asc-cli/compare/v0.1.41...v0.1.42 -[0.1.41]: https://github.com/tddworks/asc-cli/compare/v0.1.40...v0.1.41 -[0.1.40]: https://github.com/tddworks/asc-cli/compare/v0.1.39...v0.1.40 -[0.1.39]: https://github.com/tddworks/asc-cli/compare/v0.1.38...v0.1.39 -[0.1.38]: https://github.com/tddworks/asc-cli/compare/v0.1.37...v0.1.38 -[0.1.37]: https://github.com/tddworks/asc-cli/compare/v0.1.36...v0.1.37 -[0.1.36]: https://github.com/tddworks/asc-cli/compare/v0.1.35...v0.1.36 -[0.1.35]: https://github.com/tddworks/asc-cli/compare/v0.1.34...v0.1.35 -[0.1.34]: https://github.com/tddworks/asc-cli/compare/v0.1.33...v0.1.34 -[0.1.33]: https://github.com/tddworks/asc-cli/compare/v0.1.32...v0.1.33 -[0.1.32]: https://github.com/tddworks/asc-cli/compare/v0.1.31...v0.1.32 -[0.1.31]: https://github.com/tddworks/asc-cli/compare/v0.1.30...v0.1.31 -[0.1.30]: https://github.com/tddworks/asc-cli/compare/v0.1.29...v0.1.30 -[0.1.29]: https://github.com/tddworks/asc-cli/compare/v0.1.28...v0.1.29 -[0.1.28]: https://github.com/tddworks/asc-cli/compare/v0.1.27...v0.1.28 -[0.1.27]: https://github.com/tddworks/asc-cli/compare/v0.1.25...v0.1.27 -[0.1.25]: https://github.com/tddworks/asc-cli/compare/v0.1.24...v0.1.25 -[0.1.24]: https://github.com/tddworks/asc-cli/compare/v0.1.23...v0.1.24 -[0.1.23]: https://github.com/tddworks/asc-cli/compare/v0.1.22...v0.1.23 -[0.1.22]: https://github.com/tddworks/asc-cli/compare/v0.1.21...v0.1.22 -[0.1.21]: https://github.com/tddworks/asc-cli/compare/v0.1.20...v0.1.21 -[0.1.20]: https://github.com/tddworks/asc-cli/compare/v0.1.9...v0.1.20 -[0.1.9]: https://github.com/tddworks/asc-cli/compare/v0.1.8...v0.1.9 -[0.1.8]: https://github.com/tddworks/asc-cli/compare/v0.1.7...v0.1.8 -[0.1.7]: https://github.com/tddworks/asc-cli/compare/v0.1.6...v0.1.7 -[0.1.6]: https://github.com/tddworks/asc-cli/compare/v0.1.5...v0.1.6 -[0.1.5]: https://github.com/tddworks/asc-cli/compare/v0.1.4...v0.1.5 -[0.1.4]: https://github.com/tddworks/asc-cli/compare/v0.1.3...v0.1.4 -[0.1.3]: https://github.com/tddworks/asc-cli/compare/v0.1.1...v0.1.3 -[0.1.1]: https://github.com/tddworks/asc-cli/compare/v0.1.0...v0.1.1 -[0.1.0]: https://github.com/tddworks/asc-cli/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 07621f59..6512a8f2 100644 --- a/README.md +++ b/README.md @@ -514,7 +514,7 @@ asc simulators shutdown --udid asc web-server ``` -Streaming and interaction available via the [ASC Pro plugin](docs/features/plugin-ui-architecture.md). +Streaming and interaction available via the [ASC Pro plugin](docs/features/plugins.md). ### Output & TUI @@ -579,15 +579,15 @@ Detailed documentation for each feature: - [App Shots Themes](docs/features/app-shots-themes.md) — plugin-provided visual themes with two-step ThemeDesign workflow - [Plugins](docs/features/plugins.md) — custom event handlers (Slack, Telegram, webhooks) - [App Wall](docs/features/app-wall.md) — community showcase; `apps.json` format and architecture -- [Users & Roles](docs/features/users.md) — team member management, role assignment, invitation lifecycle; directory integration for automated access control +- [Users & Roles](docs/features/asc-users.md) — team member management, role assignment, invitation lifecycle; directory integration for automated access control - [Customer Reviews](docs/features/customer-reviews.md) — list reviews, respond to feedback, manage review responses - [Game Center](docs/features/game-center.md) — achievements (list, create, delete) and leaderboards (list, create, delete) - [Power & Performance](docs/features/performance.md) — performance metrics (app/build), diagnostic signatures, diagnostic logs - [Reports](docs/features/reports.md) — sales, finance, and analytics reports; TSV parsing, multi-step analytics workflow -- [Iris (Private API)](docs/features/iris.md) — cookie-based auth; create apps, list apps via the iris private API +- [Iris (Private API)](docs/features/iris/iris.md) — cookie-based auth; create apps, list apps via the iris private API - [Simulators](docs/features/simulators.md) — list, boot, shutdown; streaming via plugin - [Web Server](docs/features/web-server-architecture.md) — Hummingbird API server with plugin system -- [Plugins](docs/features/plugin-ui-architecture.md) — extend CLI with routes, commands, affordances, and UI +- [Plugins](docs/features/plugins.md) — extend CLI with routes, commands, affordances, and UI ## Design: CAEOAS @@ -611,7 +611,7 @@ $ asc versions list --app-id app-abc } ``` -Affordances are state-aware — `submitForReview` only appears when `isEditable == true`. See [docs/design.md](docs/desgin.md) for the full pattern. +Affordances are state-aware — `submitForReview` only appears when `isEditable == true`. See [docs/design.md](docs/design.md) for the full pattern. ## Use as a Swift Package (SPM) diff --git a/docs/changelog/0.1.md b/docs/changelog/0.1.md new file mode 100644 index 00000000..7f5378d1 --- /dev/null +++ b/docs/changelog/0.1.md @@ -0,0 +1,728 @@ +# Changelog — 0.1.x + +Archived unchanged from [CHANGELOG.md](../../CHANGELOG.md), which holds the current releases. + +## [0.1.68] - 2026-04-14 + +### Added +- **`asc testflight groups create`** — create external or internal TestFlight beta groups (`--internal` flag toggles internal; `--public-link-enabled` / `--feedback-enabled` for external groups) +- **`asc init` review contact flags** — `--contact-first-name`, `--contact-last-name`, `--contact-phone`, `--contact-email` save review contact info to `.asc/project.json` for reuse across versions +- **`ProjectConfig` review contact fields** — optional `contactFirstName`, `contactLastName`, `contactPhone`, `contactEmail` with `hasReviewContact` computed property +- **`setReviewContact` / `updateReviewContact` affordances** — `ProjectConfig` now suggests setting review contact when missing + +--- + +## [0.1.67] - 2026-04-13 + +### Changed +- Bug fixes and improvements. + +--- + +## [0.1.66] - 2026-04-09 + +### Added +- **`ThemeDesign` domain model** — composes from Gallery-native types (`GalleryPalette` + `[Decoration]`) for structured theme output. Generated by AI once, applied deterministically to all screenshots +- **`ThemeDesignApplier`** — re-renders through `GalleryHTMLRenderer.renderScreen()` pipeline with overridden palette and merged decorations (no HTML patching) +- **`GalleryPalette.textColor`** — optional explicit text color, overrides the auto-detect heuristic +- **`Decoration.label()` shape** — text/emoji decorative elements (e.g. `Decoration(shape: .label("✨"), ...)`) +- **`DecorationAnimation`** — float, drift, pulse, spin, twinkle animations for decorations +- **`ScreenLayout.withDecorations()`** — creates a copy with additional decorations +- **`GalleryHTMLRenderer.renderDecorations()`** — renders `ScreenLayout.decorations` (previously dead code) using `cqi` units +- **`buildDesignContext()` on `ScreenTheme`** — prompt method that instructs AI to return `ThemeDesign` JSON +- **`design()` on `ThemeProvider`/`ThemeRepository`** — generate a ThemeDesign from AI in one call +- **`--design-only` / `--apply-design` CLI flags** — batch theme workflow +- **REST endpoints** — `POST /app-shots/themes/design` and `POST /app-shots/themes/apply-design` + +### Changed +- **`GalleryHTMLRenderer` refactored to Mustache templates** — all HTML extracted from Swift into 7 `.mustache` template files using [swift-mustache](https://github.com/hummingbird-project/swift-mustache). The renderer only builds context dictionaries; all HTML, CSS colors (via CSS custom properties in `theme-vars.mustache`), and keyframe animations live in templates. Templates are pre-compiled at startup via `MustacheLibrary` for performance. Preview rendering is cached per template ID. +- **`DecorationShape.displayCharacter`** — computed property on the model instead of renderer logic +- **`GalleryPalette.isLight` + `headlineColor`** — theme detection and text color derivation moved from renderer to palette +- **`Decoration` extended** — new optional fields: `color`, `background`, `borderRadius`, `animation` +- **Theme selection no longer requires auto-compose** — clicking a theme applies immediately to slides with existing preview HTML via `ThemeDesign` (1 AI call for design, then deterministic apply to all slides) +- **Blitz plugin: `design()` implemented** — generates `ThemeDesign` via compose bridge `mode: "design"`, enabling the fast design→apply-design flow + +--- + +## [0.1.65] - 2026-04-06 + +### Changed +- **`Presentable` protocol** — domain models own their table headers and row values; eliminates `headers:`/`rowMapper:` boilerplate from 37 list commands +- **REST controllers with DI** — routes rewritten as controller structs (`AppsController`, `CodeSigningController`, etc.) with injected repo dependencies; repos created once at server startup, not per request +- **No more `Command.parse([])` in REST layer** — controllers call domain repos and operations directly; CLI and REST are equal thin adapters +- **`AffordanceRegistry` uses structured `Affordance`** — plugin affordances now render to both CLI commands and REST `_links` (previously CLI-only) +- **`ScreenshotTemplate` domain operations** — `apply(content:)`, `renderFragment(content:)` as rich domain methods; `ThemedPage` value type for page wrapping +- **`ScreenshotTemplate` Codable includes `previewHTML` and `deviceCount`** — REST consumers get preview data without special handling + +--- + +## [0.1.64] - 2026-04-05 + +### Added +- **REST API with HATEOAS** — `GET /api/v1` entry point plus 12 resource endpoints (apps, versions, builds, testflight, certificates, bundle-ids, devices, profiles, simulators, plugins, territories) calling domain repositories directly (in-process, no subprocess), returning JSON with `_links` for agent navigation +- **`APIRoot` model** — HATEOAS entry point at `GET /api/v1` listing all available top-level resources with navigable `_links` +- **Structured `Affordance` type** — single source of truth for both CLI commands and REST links; models define affordances once, rendered to either format by `OutputFormatter` +- **`APILink` and `AffordanceMode`** — domain types supporting dual-mode affordance rendering (`.cli` → `"affordances"`, `.rest` → `"_links"`) +- **`RESTPathResolver`** — resolves CLI command + params into REST API paths using a route table covering 35+ resource types across the full domain hierarchy +- **HTML-to-PNG export** — `--preview image` option on `asc app-shots templates apply` and `asc app-shots themes apply` renders composed HTML to PNG via WebKit, with `--image-output` for custom output path +- **`HTMLRenderer` protocol** — `@Mockable` domain protocol for HTML-to-image rendering, implemented by `WebKitHTMLRenderer` using WKWebView snapshot + +--- + +## [0.1.63] - 2026-04-02 + +### Added +- **Plugin Marketplace** — `asc plugins market list` and `asc plugins market search --query X` to browse and search plugins from [tddworks/asc-registry](https://github.com/tddworks/asc-registry) +- **Multi-source plugin registry** — `PluginSource` protocol with `GitHubPluginSource` implementation; composable sources in `PluginMarketRepository` +- **`asc plugins install --name X`** — download and install `.plugin` bundles from the marketplace +- **`asc plugins uninstall --name X`** — remove installed plugin bundles (matches by slug or registry ID) +- **Plugins page in Command Center** — web UI with Installed and Marketplace tabs, stats bar, install/uninstall with loading spinners +- **Enriched plugin manifest** — `manifest.json` now supports `description`, `author`, `repositoryURL`, `categories`; installed plugins display the same rich info as marketplace listings +- **Centralized plugin affordance merging** — `AffordanceRegistry` affordances merged automatically in `OutputFormatter` via `WithPluginAffordances`; domain models no longer need manual registry calls +- **Example plugin** — `examples/hello-plugin/` demonstrates `AffordanceRegistry`, server routes, and UI affordance handlers +- **Plugin registry repo** — [tddworks/asc-registry](https://github.com/tddworks/asc-registry) with `registry.json` and release assets for ASC Pro + Hello Plugin + +### Changed +- **Unified `Plugin` model** — merged `Plugin` and `MarketPlugin` into a single model with `isInstalled`, `downloadURL?`, `slug?` fields +- **Refactored plugin system** — replaced event-based script plugins with dylib `.plugin` bundle management +- **Removed event plugin system** — deleted `PluginEvent`, `PluginEventPayload`, `PluginResult`, `PluginRunner`, `PluginEventBus` and all infrastructure +- **Removed event bus from commands** — `BuildsUpload` and `VersionsSubmit` no longer emit plugin events +- **Apps page affordance buttons** — plugin affordances (e.g. `greet` from Hello Plugin) render as clickable buttons via `appAffordanceHandlers` registry + +--- + +## [0.1.62] - 2026-04-02 + +### Added +- **`asc web-server`** — Swift/Hummingbird HTTP+WebSocket server replacing Node.js. Single binary, zero external dependencies, HTTPS with self-signed cert +- **Plugin architecture** — `.plugin` bundles in `~/.asc/plugins/` extend the CLI with routes, commands, affordances, and UI. Dylibs built with `dynamic_lookup` (~300KB) +- **`AffordanceRegistry`** — plugins extend domain model affordances at runtime (e.g. pro plugin adds `stream` to booted simulators) +- **Affordance-driven UI** — web app renders buttons from CAEOAS affordances, not hardcoded features. Plugins register handlers via `window.simAffordanceHandlers` +- **Command Center Simulators page** — device list with search/filter, stats cards, affordance-driven actions + +--- + +## [0.1.61] - 2026-03-31 + +### Added +- **Screenshot image URLs** — `asc screenshots list` now returns `imageUrl` for each screenshot, so you can view and render real App Store images directly from CLI output +- **Screenshot platform filtering** — filter and browse screenshots by platform (iPhone, iPad, etc.) in the web UI +- **Simulator screenshot capture** — capture and browse simulator screenshots in the web gallery + +--- + +## [0.1.60] - 2026-03-31 + +### Added +- **`asc simulators list [--booted]`** — list available iOS simulators with state-aware affordances +- **`asc simulators boot --udid `** — boot a simulator +- **`asc simulators shutdown --udid `** — shut down a simulator +- **`asc simulators list/boot/shutdown`** — manage local iOS simulators with state-aware affordances + +--- + +## [0.1.59] - 2026-03-30 + +### Added +- **`asc builds list --platform`** — filter builds by platform (ios, macos, tvos, visionos) +- **`asc builds list --version`** — filter builds by marketing version (e.g. 1.0.0) +- **`asc builds next-number`** — get the next build number for a version/platform, ideal for CI/CD automation (resolves #13) +- **Build platform field** — builds now include platform from preReleaseVersion +- **Build number populated** — `buildNumber` field now maps from `Build.attributes.version` (was always nil) + +### Fixed +- **Global options (`--pretty`, `--output`, `--timeout`) now accepted by all commands** — previously 10 leaf commands (`builds add-beta-group`, `builds remove-beta-group`, `builds uploads delete`, `versions set-build`, `reviews responses delete`, `app-clip-experiences delete`, `app-clip-experience-localizations delete`, `analytics-reports delete`, `auth logout`, `auth use`) rejected these flags with "Unknown option" + +### Changed +- **`asc builds list`** — table output now includes Build Number and Platform columns +- **Build version semantics** — `version` field now holds the marketing version (from PreReleaseVersion) when available; `buildNumber` holds the build string +- **Builds sorted by newest first** — list results default to `-uploadedDate` sort order + +--- + +## [0.1.58] - 2026-03-23 + +### Changed +- Bug fixes and improvements. + +--- + +## [0.1.57] - 2026-03-23 + +### Changed +- **`asc web-server`** — `/command-center/`, `/console/`, and `/` now redirect (302) to the hosted apps at `asccli.app/command-center` and `asccli.app/console` instead of serving local files that no longer exist; startup banner updated to show hosted URLs + +--- + +## [0.1.56] - 2026-03-21 + +### Changed +- Bug fixes and improvements. + +--- + +## [0.1.55] - 2026-03-21 + +### Added +- **Iris private API support** — new `asc iris` command namespace for cookie-based App Store Connect private API access +- **`asc iris status`** — check iris cookie session availability (browser or environment source) +- **`asc iris apps list`** — list apps via iris `/v1/appBundles` endpoint +- **`asc iris apps create`** — create a new app with initial version, supports multi-platform (`--platforms IOS MAC_OS`) +- **Cookie-based authentication** — auto-extracts `myacinfo` and session cookies from Chrome/Safari/Firefox via SweetCookieKit, with `ASC_IRIS_COOKIES` env var fallback for CI/CD +- **SweetCookieKit dependency** — browser cookie extraction for iris authentication + +--- + +## [0.1.53] - 2026-03-18 + +### Changed +- **`asc web` → `asc web-server`** — renamed command to reflect its role as a local API proxy, no longer serves static files +- **Removed Hummingbird dependency** — web server replaced with embedded Node.js proxy (`apps/server.js`), reducing binary size and build dependencies +- **Unified web apps under `apps/`** — moved dashboard from `Sources/ASCCommand/Resources/web/` to `apps/asc-web-command-center/`, console from `homepage/asc-web-console/` to `apps/asc-web-console/` +- **Dual-mode API detection** — both web apps auto-detect local proxy (tries relative `/api/run`, then `localhost:8420`, then falls back to mock mode) + +### Added +- **SPM build plugin (`Plugins/EmbedServerJS/`)** — auto-generates Swift source from `apps/server.js` at build time, embedding the API proxy in the binary with `apps/server.js` as single source of truth +- **Command Center nav link on homepage** — added "Command Center" navigation to homepage template with translations in all 12 languages +- **GitHub Pages deployment for web apps** — deploy workflow now copies `apps/asc-web-*` to homepage for static hosting + +### Removed +- `WebServer.swift` (Hummingbird-based static file server) +- `homepage/asc-web-console/server.py` (Python dev server) +- Hummingbird package dependency + +--- + +## [0.1.52] - 2026-03-18 + +### Changed +- Bug fixes and improvements. + +--- + +## [0.1.51] - 2026-03-17 + +### Added +- `asc web` management dashboard — complete redesign with professional light/dark theme, Plus Jakarta Sans typography, and self-contained CSS (no Tailwind CDN dependency) +- App context selector in sidebar — select an app from the Apps page and all other sections (Versions, Builds, TestFlight, Reviews, IAP, Subscriptions) use that app context automatically +- Rich domain models in web UI — every entity carries parent IDs, semantic booleans (`isLive`, `isEditable`, `isPending`, `isUsable`), and state-aware CAEOAS affordances matching the Swift Domain layer exactly +- Mock/CLI dual data mode — web UI auto-detects the `asc web` backend; falls back to built-in mock data for offline development and demos; toggle via header button +- Dark mode with system preference detection and localStorage persistence +- 15 management pages: Command Center, Apps, Versions, Builds, TestFlight, Submissions, App Info, Screenshots, Reviews, In-App Purchases, Subscriptions, Reports, Code Signing, Xcode Cloud, Users & Roles +- Command Center with release pipeline timeline, quick actions, and stats overview +- Command log modal showing every CLI command executed by the UI +- Standalone development server (`apps/asc-web-command-center/server.js`) for working on the web UI outside of `asc web` + +--- + +## [0.1.50] - 2026-03-15 + +### Added +- `asc territories list` — list all available App Store territories with currency codes +- `asc iap-availability get --iap-id ` — get territory availability for an in-app purchase (includes currency per territory) +- `asc iap-availability create --iap-id --available-in-new-territories --territory USA --territory CHN` — create territory availability for an IAP +- `asc subscription-availability get --subscription-id ` — get territory availability for a subscription (includes currency per territory) +- `asc subscription-availability create --subscription-id --available-in-new-territories --territory USA` — create territory availability for a subscription +- `Territory` domain model with id and currency, shared across availability responses +- `getAvailability` affordance on `InAppPurchase` and `Subscription` models for CAEOAS navigation +- `listTerritories` affordance on availability models for territory discovery +- `asc app-availability get --app-id ` — get per-territory availability with `isAvailable`, `contentStatuses` (blocking reasons), `releaseDate`, and `isPreOrderEnabled` for every territory + +--- + +## [0.1.49] - 2026-03-14 + +### Added +- `asc skills list` — list available skills from the asc-cli repository via `npx skills add tddworks/asc-cli --list` +- `asc skills install --name ` — install a specific skill; `--all` to install all available skills +- `asc skills installed` — show skills installed in `~/.claude/skills/` with CAEOAS affordances +- `asc skills uninstall --name ` — remove an installed skill from `~/.claude/skills/` +- `asc skills check` — check for available skill updates via `npx skills check` +- `asc skills update` — update installed skills via `npx skills update` +- Auto-update checker — non-blocking skill update check on every `asc` command (24h cooldown, CI-aware, disable with `ASC_SKIP_SKILL_CHECK=true`) +- `--app-id` flag on `asc app-wall submit` — accepts App Store Connect app IDs directly, auto-constructs App Store URLs (repeatable, combinable with `--app` and `--developer-id`) + +--- + +## [0.1.48] - 2026-03-13 + +### Changed +- `--developer` on `asc app-wall submit` is now optional — when omitted, the homepage card uses the iTunes artist name instead +- `asc beta-review submissions list --build-id ` — list beta app review submissions for a build +- `asc beta-review submissions create --build-id ` — submit a build for beta (TestFlight external) review +- `asc beta-review submissions get --submission-id ` — get a specific beta review submission +- `asc beta-review detail get --app-id ` — get beta review contact info and demo account details +- `asc beta-review detail update --detail-id ` — update beta review contact info, demo account, and notes + +--- + +## [0.1.47] - 2026-03-13 + +### Changed +- Bug fixes and improvements. + +--- + +## [0.1.46] - 2026-03-13 + +### Added +- `asc builds archive --scheme ` — archive and export Xcode projects locally via `xcodebuild`, auto-detecting workspace/project from the current directory; supports `--platform`, `--configuration`, `--export-method` (app-store/ad-hoc/development/enterprise), and `--output-dir` +- `--upload` flag on `asc builds archive` chains the exported IPA/PKG directly into App Store Connect upload, combining archive + export + upload in a single command + +--- + +## [0.1.45] - 2026-03-12 + +### Changed +- Subscription offer codes: `asc subscription-offer-codes list/create/update`, `asc subscription-offer-code-custom-codes list/create/update`, `asc subscription-offer-code-one-time-codes list/create/update` — manage offer codes, custom redeemable codes, and one-time use code batches for auto-renewable subscriptions +- IAP offer codes: `asc iap-offer-codes list/create/update`, `asc iap-offer-code-custom-codes list/create/update`, `asc iap-offer-code-one-time-codes list/create/update` — manage offer codes, custom redeemable codes, and one-time use code batches for in-app purchases +- `InAppPurchase` and `Subscription` affordances now include `listOfferCodes` for direct navigation to offer codes + +--- + +## [0.1.44] - 2026-03-12 + +### Added +- Power & performance metrics: `asc perf-metrics list --app-id ` and `asc perf-metrics list --build-id ` — download launch time, hang rate, memory, disk, battery, termination, and animation metrics with `--metric-type` filter +- Diagnostic signatures: `asc diagnostics list --build-id ` — list hang, disk write, and launch diagnostic hotspots with `--diagnostic-type` filter +- Diagnostic logs: `asc diagnostic-logs list --signature-id ` — view call stack metadata for a specific diagnostic signature + +--- + +## [0.1.43] - 2026-03-11 + +### Added +- HTML screenshot generation: `asc app-shots html --plan composition-plan.json` — deterministic App Store screenshot generation with real device mockup frames, no AI or API keys needed +- CompositionPlan format: normalized 0-1 coordinates, multiple devices per slide, text overlays with alignment, gradient backgrounds, per-screen color themes +- Device mockup system: bundled iPhone 17 Pro Max frame with `mockups.json` config; users can add custom mockups to `~/.asc/mockups/` +- TextOverlay `textAlign` property: supports `left`, `center`, `right` alignment for text positioning in composition plans +- Client-side PNG export via html-to-image CDN in generated HTML pages + +--- + +## [0.1.42] - 2026-03-11 + +### Added +- Customer reviews: `asc reviews list --app-id ` and `asc reviews get --review-id ` — list and view customer reviews with rating, title, body, reviewer nickname, and territory +- Review responses: `asc review-responses create --review-id --response-body "text"`, `asc review-responses get --review-id `, `asc review-responses delete --response-id ` — manage developer responses to customer reviews + +### Changed +- Bug fixes and improvements. + +--- + +## [0.1.41] - 2026-03-10 + +### Added +- Sales reports: `asc sales-reports download` with support for 10 report types, 5 sub-types, and 4 frequencies +- Finance reports: `asc finance-reports download` with financial and finance detail report types +- Gzip decompression and TSV parsing infrastructure for App Store Connect report downloads +- Analytics reports: `asc analytics-reports request/list/delete/reports/instances/segments` — multi-step analytics workflow with 5 report categories and 3 granularity levels +- `asc auth login --vendor-number ` — save vendor number with account credentials +- `asc auth update --vendor-number ` — add or update vendor number on an existing account +- Vendor number auto-resolution for `sales-reports download` and `finance-reports download` — `--vendor-number` is now optional when saved on the active account + +--- + +## [0.1.40] - 2026-03-09 + +### Changed +- Bug fixes and improvements. + +--- + +## [0.1.39] - 2026-03-08 + +### Added +- `asc app-clips list --app-id ` — list App Clips for an app; `AppClip` carries `appId` (injected), `bundleId?`; affordances: `listAppClips`, `listExperiences` +- `asc app-clip-experiences list --app-clip-id ` — list default experiences for an App Clip; `AppClipDefaultExperience` carries `appClipId` (injected), `action?` (OPEN/VIEW/PLAY); affordances: `delete`, `listExperiences`, `listLocalizations` +- `asc app-clip-experiences create --app-clip-id [--action OPEN|VIEW|PLAY]` — create a default experience +- `asc app-clip-experiences delete --experience-id ` — delete a default experience +- `asc app-clip-experience-localizations list --experience-id ` — list localizations; `AppClipDefaultExperienceLocalization` carries `experienceId` (injected), `locale`, `subtitle?`; affordances: `delete`, `listLocalizations` +- `asc app-clip-experience-localizations create --experience-id --locale [--subtitle "..."]` — create a localization +- `asc app-clip-experience-localizations delete --localization-id ` — delete a localization +- `AppClipRepository` `@Mockable` protocol with 7 methods covering all CRUD operations +- `asc game-center detail get --app-id ` — get Game Center configuration for an app; `GameCenterDetail` carries `appId` (injected), `isArcadeEnabled`; affordances: `getDetail`, `listAchievements`, `listLeaderboards` +- `asc game-center achievements list --detail-id ` — list Game Center achievements; `GameCenterAchievement` carries `gameCenterDetailId` (injected), `referenceName`, `vendorIdentifier`, `points`, `isShowBeforeEarned`, `isRepeatable`, `isArchived`; affordances: `listAchievements`, `delete` +- `asc game-center achievements create --detail-id --reference-name --vendor-identifier --points [--show-before-earned] [--repeatable]` — create a new achievement +- `asc game-center achievements delete --achievement-id ` — delete an achievement +- `asc game-center leaderboards list --detail-id ` — list Game Center leaderboards; `GameCenterLeaderboard` carries `gameCenterDetailId` (injected), `referenceName`, `vendorIdentifier`, `scoreSortType` (ASC/DESC), `submissionType` (BEST_SCORE/MOST_RECENT_SCORE), `isArchived`; affordances: `listLeaderboards`, `delete` +- `asc game-center leaderboards create --detail-id --reference-name --vendor-identifier --score-sort-type ASC|DESC [--submission-type BEST_SCORE|MOST_RECENT_SCORE]` — create a new leaderboard +- `asc game-center leaderboards delete --leaderboard-id ` — delete a leaderboard +- `ScoreSortType` and `LeaderboardSubmissionType` domain enums +- `GameCenterRepository` `@Mockable` protocol with 7 methods covering all CRUD operations + +--- + +## [0.1.38] - 2026-03-07 + +### Added +- `asc xcode-cloud products list [--app-id ]` — list Xcode Cloud products; `XcodeCloudProduct` carries `appId` (injected from relationship), `name`, `productType`; affordances: `listWorkflows`, `listProducts` +- `asc xcode-cloud workflows list --product-id ` — list CI workflows for a product; `XcodeCloudWorkflow` carries `productId` (injected), `name`, `description`, `isEnabled`, `isLockedForEditing`; affordances: `listBuildRuns`, `listWorkflows` always, `startBuild` only when `isEnabled` +- `asc xcode-cloud builds list --workflow-id ` — list build runs for a workflow; `XcodeCloudBuildRun` carries `workflowId` (injected), `number`, `executionProgress` (PENDING/RUNNING/COMPLETE), `completionStatus` (SUCCEEDED/FAILED/ERRORED/CANCELED/SKIPPED), `startReason`; semantic booleans: `isPending`, `isRunning`, `isComplete`, `isSucceeded`, `hasFailed` +- `asc xcode-cloud builds get --build-run-id ` — get a specific build run by ID +- `asc xcode-cloud builds start --workflow-id [--clean]` — start a new build run; `--clean` flag performs a clean build removing derived data + +--- + +## [0.1.37] - 2026-03-06 + +### Changed +- Bug fixes and improvements. + +--- + +## [0.1.36] - 2026-03-05 + +### Added +- `asc users list/update/remove` — manage App Store Connect team members; list users filtered by role, update a member's roles (replaces all current roles), or revoke access immediately; `TeamMember` carries `username`, `firstName`, `lastName`, `roles`, `isAllAppsVisible`, `isProvisioningAllowed`; affordances: `remove`, `updateRoles` (pre-filled with current roles) +- `asc user-invitations list/invite/cancel` — manage pending team invitations; invite a new member with one or more roles (`--role DEVELOPER --role APP_MANAGER`); `UserInvitationRecord` carries `email`, `roles`, `expirationDate`; affordance: `cancel`; supports 13 roles; `--all-apps-visible` flag on invite grants access to all apps +- `asc auth list` — list all saved App Store Connect accounts with active status and per-account affordances (`use`, `logout`) +- `asc auth use ` — switch the active App Store Connect account; all subsequent commands use the newly active account +- `asc auth login --name ` — optional `--name` flag to save credentials under a human-readable alias (defaults to `"default"`); first saved account becomes active automatically; subsequent logins with new names are saved alongside existing accounts; account names must not contain spaces (use hyphens or underscores) + +### Changed +- `asc auth logout` — now accepts optional `--name ` flag; removes the named account or the active account if `--name` is omitted +- `asc auth check` — now shows the account `name` field when credentials come from a saved account (omitted for environment-variable credentials) +- `~/.asc/credentials.json` — upgraded to multi-account format `{ "active": "name", "accounts": { ... } }`; old single-credential files are auto-migrated to a `"default"` named account on first use + +--- + +## [0.1.35] - 2026-03-04 + +### Added +- `asc app-wall submit` — submit your app to the community app wall at asccli.app by opening a GitHub pull request against `tddworks/asc-cli`; forks the repo, adds your entry to `homepage/apps.json`, and creates a PR automatically; supports `--developer-id` (auto-fetches all your App Store apps), `--app` (specific App Store URLs, repeatable), `--github`, `--x`; GitHub token resolved from `--github-token` flag, `$GITHUB_TOKEN`, or `gh auth token` + +--- + +## [0.1.34] - 2026-03-02 + +### Fixed +- `asc app-shots translate` — no longer regenerates the visual design (background, colors, device mockup, layout) from the original plan specs; now sends a simple "edit this image, translate only the text overlays" prompt so the existing generated screenshot is preserved exactly + +--- + +## [0.1.33] - 2026-03-02 + +### Added +- `asc init` — initialise project context by saving the app ID, name, and bundle ID to `.asc/project.json` in the current directory; supports `--app-id` (direct), `--name` (search by name), or auto-detect from `.xcodeproj` bundle identifier; output includes CAEOAS affordances for common next steps + +--- + +## [0.1.32] - 2026-03-01 + +### Changed +- Bug fixes and improvements. + +--- + +## [0.1.31] - 2026-03-01 + +### Added +- **Plugin system** — users can install executable plugins in `~/.asc/plugins//` to extend the CLI with custom event handlers (e.g., Slack/Telegram notifications) +- `asc plugins list` — list all installed plugins with name, version, enabled status, and subscribed events +- `asc plugins install ` — install a plugin from a local directory containing `manifest.json` and a `run` executable +- `asc plugins uninstall --name ` — remove an installed plugin +- `asc plugins enable --name ` — re-enable a disabled plugin +- `asc plugins disable --name ` — disable a plugin without removing it +- `asc plugins run --name --event ` — manually invoke a plugin for testing; supports `--app-id`, `--version-id`, `--build-id` payload flags +- Auto-event emission: `asc builds upload` fires `build.uploaded` after a successful upload; `asc versions submit` fires `version.submitted` after a successful submission +- Plugin protocol: plugins receive a JSON event payload on stdin and write a `{"success": bool, "message": "..."}` result to stdout +- `version-review-detail get --version-id ` — fetch the App Store review contact info and demo account settings for a version +- `asc version-review-detail update --version-id [flags]` — upsert review info (creates if none exists, patches if already set); supports `--contact-first-name`, `--contact-last-name`, `--contact-phone`, `--contact-email`, `--demo-account-required`, `--demo-account-name`, `--demo-account-password`, `--notes` +- `notes` field added to `AppStoreReviewDetail` domain model +- `getReviewDetail` affordance added to `AppStoreVersion` for agent navigation + +--- + +## [0.1.30] - 2026-03-01 + +### Added +- `asc age-rating get --app-info-id ` — fetch the full age rating declaration for an app info, including all content intensity ratings, boolean flags, kids age band, and region overrides +- `asc age-rating update --declaration-id [flags]` — update individual age rating fields via PATCH; supports all 9 boolean flags (`--advertising`, `--gambling`, `--loot-box`, etc.) and 13 intensity ratings (`--violence-realistic`, `--profanity`, `--sexual-content`, etc.) plus `--kids-age-band`, `--age-rating-override`, and `--korea-age-rating-override` +- `getAgeRating` affordance added to `AppInfo` for agent navigation +- `--privacy-choices-url` and `--privacy-policy-text` flags on `asc app-info-localizations update` — expose the two remaining updatable privacy fields from the App Store Connect API +- `asc app-info-localizations delete --localization-id ` — remove a per-locale metadata entry; `delete` affordance added to `AppInfoLocalization` +- `asc app-infos update --app-info-id [--primary-category] [--primary-subcategory-one] [--primary-subcategory-two] [--secondary-category] [--secondary-subcategory-one] [--secondary-subcategory-two]` — set or update all 6 category relationship fields on an AppInfo; `updateCategories` affordance added to `AppInfo` +- `asc app-categories list [--platform IOS|MAC_OS|TV_OS]` — list all available App Store categories and subcategories; returns a flat list combining top-level (`data[]`) and subcategories (`included[]`) from the API; `AppCategory` domain model with `parentId` for subcategory identification + +--- + +## [0.1.29] - 2026-03-01 + +### Added +- `--style-reference ` flag on `asc app-shots generate` and `asc app-shots translate` — pass any PNG/JPEG as a visual style guide; Gemini replicates the reference's colors, typography, gradients, and layout patterns without copying its content; the reference image is sent as the first part in the Gemini request followed by an explicit style-guide instruction + +--- + +## [0.1.28] - 2026-02-28 + +### Added +- `--device-type` flag on `asc app-shots generate` and `asc app-shots translate` — accepts named App Store display type constants (`APP_IPHONE_69`, `APP_IPHONE_67`, `APP_IPAD_PRO_129`, etc.) and automatically sets the correct `--output-width`/`--output-height`; overrides explicit dimension flags when both are provided; all 16 device types supported across iPhone, iPad, Apple TV, Mac, and Apple Vision Pro + +### Fixed +- `asc app-shots generate` and `asc app-shots translate` now upscale Gemini output to the correct App Store dimensions using CoreGraphics. Gemini returns ~704×1520; the required iPhone 6.9" size is 1320×2868. New `--output-width` (default `1320`) and `--output-height` (default `2868`) flags control the target size for all other device types. + +--- + +## [0.1.27] - 2026-02-28 + +### Added +- `asc app-shots translate` — one-shot localization of generated screenshots; reads the English plan + existing `screen-{n}.png` files, sends them to Gemini with per-locale translation instructions, writes `{output-dir}/{locale}/screen-{n}.png`; supports multiple locales in one invocation (`--to zh --to ja --to ko`); locales processed in parallel via `TaskGroup` +- `asc app-shots generate` — AI-powered App Store screenshot generation using Gemini; reads a `ScreenPlan` JSON + screenshot images, calls Gemini image generation API, writes `screen-{index}.png` files; `--plan` defaults to `.asc/app-shots/app-shots-plan.json`, `--output-dir` defaults to `.asc/app-shots/output`, screenshots auto-discovered from plan directory when not provided — zero-argument happy path: `asc app-shots generate` +- `asc app-shots config` — persistent Gemini API key management; `--gemini-api-key` saves to `~/.asc/app-shots-config.json`, bare invocation shows current key + source (file/env), `--remove` deletes it; `generate` resolves key from flag → env var → stored config +- `ScreenPlan`, `ScreenConfig`, `ScreenTone`, `LayoutMode`, `ScreenColors`, `AppShotsConfig` domain models +- `ScreenshotGenerationRepository` + `AppShotsConfigStorage` `@Mockable` protocols +- `GeminiScreenshotGenerationRepository` — native Gemini `generateContent` API with `responseModalities: ["TEXT","IMAGE"]`, parallel TaskGroup generation +- `FileAppShotsConfigStorage` — saves Gemini API key to `~/.asc/app-shots-config.json` + +--- + +## [0.1.25] - 2026-02-27 + +### Added +- `asc subscriptions submit` — submit a subscription for App Store review; `submit` affordance on `Subscription` appears only when `state == READY_TO_SUBMIT` +- `asc subscription-offers list` — list introductory offers for a subscription +- `asc subscription-offers create` — create a `FREE_TRIAL`, `PAY_AS_YOU_GO`, or `PAY_UP_FRONT` introductory offer; validates that `--price-point-id` is provided for paid modes +- `SubscriptionIntroductoryOffer` domain model with `SubscriptionOfferDuration` and `SubscriptionOfferMode` enums; `requiresPricePoint` semantic boolean on `SubscriptionOfferMode` +- `createIntroductoryOffer` and `listIntroductoryOffers` CAEOAS affordances on `Subscription` +- `asc iap list` — list in-app purchases (consumable, non-consumable, non-renewing subscription) for an app +- `asc iap create` — create a new in-app purchase with reference name, product ID, and type +- `asc iap-localizations list` — list localizations for an in-app purchase +- `asc iap-localizations create` — create a per-locale name and description for an in-app purchase +- `asc subscription-groups list` — list subscription groups for an app +- `asc subscription-groups create` — create a new subscription group +- `asc subscriptions list` — list subscriptions within a group +- `asc subscriptions create` — create a subscription with period (ONE_WEEK–ONE_YEAR), family sharing, and group level +- `asc subscription-localizations list` — list per-locale metadata for a subscription +- `asc subscription-localizations create` — create a per-locale name and description for a subscription +- `asc iap submit` — submit an in-app purchase for App Store review +- `asc iap price-points list` — list available price tiers for an IAP, optionally filtered by territory +- `asc iap prices set` — set the price schedule for an IAP (base territory + auto-pricing for all others) +- `InAppPurchaseSubmission`, `InAppPurchasePricePoint`, `InAppPurchasePriceSchedule` domain models with CAEOAS affordances +- `InAppPurchaseState` and `SubscriptionState` enums with semantic booleans (`isApproved`, `isLive`, `isEditable`, `isPendingReview`) +- CAEOAS affordances on all new models linking to sibling and child commands +- `asc app-preview-sets list` — list App Store video preview sets for a version localization +- `asc app-preview-sets create` — create a new preview set for a specific device type +- `asc app-previews list` — list video previews in a preview set +- `asc app-previews upload` — upload a video file (`.mp4`, `.mov`, `.m4v`) to a preview set using a 3-step upload flow (reserve → PUT chunks → PATCH with MD5) +- `PreviewType` enum with 16 device cases (iPhone, iPad, Mac, Apple TV, Apple Vision Pro) +- `AppPreview.VideoDeliveryState` with 5 states including `PROCESSING` (unique to video) +- Renamed `--group-id` to `--beta-group-id` in all `asc testflight testers` commands for consistency with `asc builds add-beta-group` + +--- + +## [0.1.24] - 2026-02-27 + +--- + +## [0.1.23] - 2026-02-26 + +--- + +## [0.1.22] - 2026-02-26 + +--- + +## [0.1.21] - 2026-02-26 + +--- + +## [0.1.20] - 2026-02-26 + +--- + +## [0.1.9] - 2026-02-25 + +--- + +## [0.1.8] - 2026-02-25 + +--- + +## [0.1.7] - 2026-02-25 + +--- + +## [0.1.6] - 2026-02-24 + +--- + +## [0.1.5] - 2026-02-24 + +--- + +## [0.1.4] - 2026-02-24 + +### Added +- **Persistent Auth Login**: Save API key credentials to `~/.asc/credentials.json` so environment variables are not required on every session. + - `asc auth login --key-id --issuer-id --private-key-path ` — save credentials to disk + - `asc auth logout` — remove saved credentials + - `asc auth check` — verify credentials and show their source (`file` or `environment`) + - Credential resolution order: `~/.asc/credentials.json` → environment variables (fully backwards-compatible) + - `asc auth check` now outputs JSON with an `affordances` field (same agent-first format as all other commands) + +### Technical +- Added `AuthStorage` `@Mockable` protocol (`save`, `load`, `delete`) in Domain +- Added `AuthStatus` domain model (`keyID`, `issuerID`, `source: CredentialSource`) with `AffordanceProviding` +- Added `CredentialSource` enum (`.file` / `.environment`) as `Codable` String raw value +- `AuthCredentials` gains `Codable` conformance for JSON file serialization +- `FileAuthStorage` (Infrastructure) reads/writes `~/.asc/credentials.json` using `JSONEncoder`/`JSONDecoder` +- `FileAuthProvider` (Infrastructure) implements `AuthProvider` backed by `FileAuthStorage` +- `CompositeAuthProvider` (Infrastructure) tries file credentials first, falls back to `EnvironmentAuthProvider` +- `ClientProvider` updated to use `CompositeAuthProvider` — all existing commands gain file-based auth transparently + +--- + +## [0.1.3] - 2026-02-24 + +### Added +- **Version Localization Update**: Set What's New text, description, keywords, marketing URL, support URL, and promotional text for any locale directly from the CLI. + - `asc version-localizations update --localization-id --whats-new "text"` — update What's New + - `asc version-localizations update --localization-id --description "text" --keywords "a,b,c"` — update other fields + - All content fields are optional — only provided fields are sent to the API +- **App Info Localizations**: Manage per-locale app metadata (name, subtitle, privacy policy) directly from the CLI. Each locale's App Store listing information is now fully writable. + - `asc app-infos list --app-id ` — list AppInfo records for an app + - `asc app-info-localizations list --app-info-id ` — list all locale entries + - `asc app-info-localizations create --app-info-id --locale --name ` — add a new locale + - `asc app-info-localizations update --localization-id [--name] [--subtitle] [--privacy-policy-url]` — patch one or more fields + +### Technical +- Introduced `VersionLocalizationRepository` (`@Mockable` protocol) with `listLocalizations`, `createLocalization`, `updateLocalization` — separate from `ScreenshotRepository` which now handles only screenshot sets and images +- Added `SDKLocalizationRepository` implementing `VersionLocalizationRepository`; converts `URL?` ↔ `String?` for `marketingURL`/`supportURL` SDK fields +- `AppStoreVersionLocalization` gains 6 optional text fields with nil-safe Codable (`encodeIfPresent`) — nil fields are omitted from JSON output +- `AppStoreVersionLocalization.affordances` now includes `updateLocalization` command +- `ScreenshotsImport` updated to accept `localizationRepo` + `screenshotRepo` as separate parameters +- Added `AppInfo` and `AppInfoLocalization` domain models carrying parent IDs for agent navigation +- Added `AppInfoRepository` `@Mockable` protocol with `listAppInfos`, `listLocalizations`, `createLocalization`, `updateLocalization` +- Added `SDKAppInfoRepository` mapping `privacyPolicyURL`/`privacyChoicesURL` SDK fields; extracts `appInfoId` from PATCH response relationships +- Updated `App.affordances` to include `listAppInfos` command +- Added `docs/features/version-localizations.md`, `docs/features/app-info-localizations.md` + +--- + +## [0.1.1] - 2026-02-22 + +### Added +- **App Store Version Management**: Browse and create versions for any app across all platforms (iOS, macOS, tvOS, watchOS, visionOS). + - `asc versions list --app-id ` + - `asc versions create --app-id --version --platform ` +- **Localization Management**: List and create App Store version localizations. + - `asc version-localizations list --version-id ` + - `asc version-localizations create --version-id --locale ` +- **Screenshot Sets**: List and create screenshot sets for a localization (one set per display type: iPhone 6.7", iPad Pro 12.9", Mac, etc.). + - `asc screenshot-sets list --localization-id ` + - `asc screenshot-sets create --localization-id --display-type ` +- **Screenshots**: List and upload screenshots within a set. + - `asc screenshots list --set-id ` — with file size, dimensions, and delivery state + - `asc screenshots upload --set-id --file ` — three-step ASC flow (reserve → S3 upload → commit) handled automatically +- **Submit for Review**: Submit an App Store version for review in one command. Reuses an existing open review submission if one is already pending. + - `asc versions submit --version-id ` +- **TestFlight**: List beta groups and testers. + - `asc testflight groups [--app-id ]` + - `asc testflight testers --group-id ` +- **TUI Screenshot Browser**: Interactive terminal UI now supports the full screenshot hierarchy — app → version → platform → locale → screenshot set. +- **Agent-First Output (CAEOAS)**: JSON responses include an `affordances` field with ready-to-run follow-up commands so AI agents can navigate the full resource tree without knowing the command structure. + ```json + { "data": [{ "id": "...", "affordances": { "listVersions": "asc versions list --app-id ..." } }] } + ``` +- **Homebrew Tap**: Install via Homebrew with native architecture binaries (arm64 and x86_64). + ```bash + brew install asc-tools/tap/asc + ``` +- **Homepage**: Static landing page at [asccli.app](https://asccli.app) with English, Chinese, and Japanese localizations. + +### Fixed +- Submission flow now correctly includes the app relationship in the version request, preventing API 422 errors. +- Submission flow reuses an unresolved (open) review submission instead of always creating a new one. + +### Technical +- `AppStoreVersion`, `AppStoreVersionState`, `AppStorePlatform` models with semantic booleans (`isLive`, `isEditable`, `isPending`) +- `AppStoreVersionLocalization`, `AppScreenshotSet`, `AppScreenshot` models; all carry parent IDs +- `ScreenshotDisplayType` enum with 39 display types, device categories, and human-readable names +- `AffordanceProviding` protocol; `OutputFormatter.formatAgentItems()` merges affordances into JSON +- `SubmissionRepository` orchestrating 4-step review submission API flow +- `ReviewSubmission` domain model with `ReviewSubmissionState` and `isWaitingForReview` +- `SequencedStubAPIClient` for multi-step API flow tests +- `APIClient` protocol abstraction enabling `StubAPIClient` for infrastructure tests +- Automated GitHub Actions release workflow with binary artifacts and Homebrew formula update +- CI/CD Codecov coverage reporting + +--- + +## [0.1.0] - 2026-02-10 + +### Added +- **Apps**: List and get apps on your App Store Connect account. + - `asc apps list` +- **Builds**: List builds for an app with processing state, version, and expiry. + - `asc builds list [--app-id ]` +- **TUI**: Interactive terminal UI (`asc tui`) for browsing apps and builds without remembering IDs. +- **Auth Check**: Verify environment credentials are valid. + - `asc auth check` +- **Output Formats**: All commands support `--output json` (default), `--output table`, and `--output markdown`. Use `--pretty` for formatted JSON. +- **Environment Auth**: Configure via `ASC_KEY_ID`, `ASC_ISSUER_ID`, and `ASC_PRIVATE_KEY_PATH` (or `ASC_PRIVATE_KEY` for inline PEM). + +### Technical +- Three-layer architecture: `ASCCommand → Infrastructure → Domain` +- `appstoreconnect-swift-sdk` adapter with clean domain model separation +- `App`, `Build`, `BetaGroup`, `BetaTester` domain models (`Sendable`, `Equatable`, `Identifiable`, `Codable`) +- `AppRepository`, `BuildRepository`, `TestFlightRepository` `@Mockable` protocols +- `ClientFactory` + `ClientProvider` dependency injection wiring +- Apple `@Testing` macro test suite following Chicago School TDD + +[0.1.68]: https://github.com/tddworks/asc-cli/compare/v0.1.67...v0.1.68 +[0.1.67]: https://github.com/tddworks/asc-cli/compare/v0.1.66...v0.1.67 +[0.1.66]: https://github.com/tddworks/asc-cli/compare/v0.1.65...v0.1.66 +[0.1.65]: https://github.com/tddworks/asc-cli/compare/v0.1.64...v0.1.65 +[0.1.64]: https://github.com/tddworks/asc-cli/compare/v0.1.63...v0.1.64 +[0.1.63]: https://github.com/tddworks/asc-cli/compare/v0.1.62...v0.1.63 +[0.1.62]: https://github.com/tddworks/asc-cli/compare/v0.1.61...v0.1.62 +[0.1.61]: https://github.com/tddworks/asc-cli/compare/v0.1.60...v0.1.61 +[0.1.60]: https://github.com/tddworks/asc-cli/compare/v0.1.59...v0.1.60 +[0.1.59]: https://github.com/tddworks/asc-cli/compare/v0.1.58...v0.1.59 +[0.1.58]: https://github.com/tddworks/asc-cli/compare/v0.1.57...v0.1.58 +[0.1.57]: https://github.com/tddworks/asc-cli/compare/v0.1.56...v0.1.57 +[0.1.56]: https://github.com/tddworks/asc-cli/compare/v0.1.55...v0.1.56 +[0.1.55]: https://github.com/tddworks/asc-cli/compare/v0.1.53...v0.1.55 +[0.1.53]: https://github.com/tddworks/asc-cli/compare/v0.1.52...v0.1.53 +[0.1.52]: https://github.com/tddworks/asc-cli/compare/v0.1.51...v0.1.52 +[0.1.51]: https://github.com/tddworks/asc-cli/compare/v0.1.50...v0.1.51 +[0.1.50]: https://github.com/tddworks/asc-cli/compare/v0.1.49...v0.1.50 +[0.1.49]: https://github.com/tddworks/asc-cli/compare/v0.1.48...v0.1.49 +[0.1.48]: https://github.com/tddworks/asc-cli/compare/v0.1.47...v0.1.48 +[0.1.47]: https://github.com/tddworks/asc-cli/compare/v0.1.46...v0.1.47 +[0.1.46]: https://github.com/tddworks/asc-cli/compare/v0.1.45...v0.1.46 +[0.1.45]: https://github.com/tddworks/asc-cli/compare/v0.1.43...v0.1.45 +[0.1.43]: https://github.com/tddworks/asc-cli/compare/v0.1.42...v0.1.43 +[0.1.42]: https://github.com/tddworks/asc-cli/compare/v0.1.41...v0.1.42 +[0.1.41]: https://github.com/tddworks/asc-cli/compare/v0.1.40...v0.1.41 +[0.1.40]: https://github.com/tddworks/asc-cli/compare/v0.1.39...v0.1.40 +[0.1.39]: https://github.com/tddworks/asc-cli/compare/v0.1.38...v0.1.39 +[0.1.38]: https://github.com/tddworks/asc-cli/compare/v0.1.37...v0.1.38 +[0.1.37]: https://github.com/tddworks/asc-cli/compare/v0.1.36...v0.1.37 +[0.1.36]: https://github.com/tddworks/asc-cli/compare/v0.1.35...v0.1.36 +[0.1.35]: https://github.com/tddworks/asc-cli/compare/v0.1.34...v0.1.35 +[0.1.34]: https://github.com/tddworks/asc-cli/compare/v0.1.33...v0.1.34 +[0.1.33]: https://github.com/tddworks/asc-cli/compare/v0.1.32...v0.1.33 +[0.1.32]: https://github.com/tddworks/asc-cli/compare/v0.1.31...v0.1.32 +[0.1.31]: https://github.com/tddworks/asc-cli/compare/v0.1.30...v0.1.31 +[0.1.30]: https://github.com/tddworks/asc-cli/compare/v0.1.29...v0.1.30 +[0.1.29]: https://github.com/tddworks/asc-cli/compare/v0.1.28...v0.1.29 +[0.1.28]: https://github.com/tddworks/asc-cli/compare/v0.1.27...v0.1.28 +[0.1.27]: https://github.com/tddworks/asc-cli/compare/v0.1.25...v0.1.27 +[0.1.25]: https://github.com/tddworks/asc-cli/compare/v0.1.24...v0.1.25 +[0.1.24]: https://github.com/tddworks/asc-cli/compare/v0.1.23...v0.1.24 +[0.1.23]: https://github.com/tddworks/asc-cli/compare/v0.1.22...v0.1.23 +[0.1.22]: https://github.com/tddworks/asc-cli/compare/v0.1.21...v0.1.22 +[0.1.21]: https://github.com/tddworks/asc-cli/compare/v0.1.20...v0.1.21 +[0.1.20]: https://github.com/tddworks/asc-cli/compare/v0.1.9...v0.1.20 +[0.1.9]: https://github.com/tddworks/asc-cli/compare/v0.1.8...v0.1.9 +[0.1.8]: https://github.com/tddworks/asc-cli/compare/v0.1.7...v0.1.8 +[0.1.7]: https://github.com/tddworks/asc-cli/compare/v0.1.6...v0.1.7 +[0.1.6]: https://github.com/tddworks/asc-cli/compare/v0.1.5...v0.1.6 +[0.1.5]: https://github.com/tddworks/asc-cli/compare/v0.1.4...v0.1.5 +[0.1.4]: https://github.com/tddworks/asc-cli/compare/v0.1.3...v0.1.4 +[0.1.3]: https://github.com/tddworks/asc-cli/compare/v0.1.1...v0.1.3 +[0.1.1]: https://github.com/tddworks/asc-cli/compare/v0.1.0...v0.1.1 +[0.1.0]: https://github.com/tddworks/asc-cli/releases/tag/v0.1.0 diff --git a/docs/changelog/0.16.md b/docs/changelog/0.16.md new file mode 100644 index 00000000..5bac2c59 --- /dev/null +++ b/docs/changelog/0.16.md @@ -0,0 +1,18 @@ +# Changelog — 0.16.x + +Archived unchanged from [CHANGELOG.md](../../CHANGELOG.md), which holds the current releases. + +## [0.16.9] - 2026-04-20 + +### Added +- **`asc review-submissions list`** — list App Store review submissions for an app. Required `--app-id`; optional `--state ` (e.g. `WAITING_FOR_REVIEW,IN_REVIEW,READY_FOR_REVIEW` or `UNRESOLVED_ISSUES`) and `--limit`. Backed by `SubmissionRepository.listSubmissions(appId:states:limit:)` +- **`asc certificates list` filtering flags** — `--limit` (server-side), `--expired-only` (client-side, drops unexpired certs), `--before ` (client-side, keeps certs with expirationDate strictly before the cutoff) +- **REST endpoint `GET /api/v1/apps/{appId}/review-submissions`** — new `ReviewSubmissionsController` in the `asc web-server`. Supports `?state=…&limit=…` query params; mirrors the CLI. **No fleet route** — Apple's OpenAPI spec marks `filter[app]` as required, so review submissions can only be listed per-app +- **REST endpoint `GET /api/v1/certificates` query params** — now honours `?type=&limit=&expired-only=&before=` (previously ignored). `before` accepts both full ISO8601 (`2026-11-01T00:00:00Z`) and date-only (`2026-11-01`, interpreted as midnight UTC). Same filter semantics as the CLI +- **REST endpoint `GET /api/v1/builds`** — fleet listing with optional `?app-id=&platform=&version=&limit=` query params. `/api/v1/apps/{appId}/builds` still works for the nested form + +### Changed +- **`CertificateRepository.listCertificates`** signature now takes `(certificateType:limit:)` — forwards `limit` to the SDK +- **`ReviewSubmission` REST links** — affordance migrated from raw `affordances` dictionary to `structuredAffordances`, so REST responses now render `_links` correctly (e.g. `/api/v1/apps/{appId}/versions`) + +[0.16.9]: https://github.com/tddworks/asc-cli/compare/v0.1.68...v0.16.9 diff --git a/docs/changelog/0.17.md b/docs/changelog/0.17.md new file mode 100644 index 00000000..d0468a62 --- /dev/null +++ b/docs/changelog/0.17.md @@ -0,0 +1,247 @@ +# Changelog — 0.17.x + +Archived unchanged from [CHANGELOG.md](../../CHANGELOG.md), which holds the current releases. + +## [0.17.9] - 2026-05-09 + +### Added +- **`asc builds set-encryption-compliance --build-id --uses-non-exempt-encryption ` — set Apple's export-compliance answer on a build** — closes the second TestFlight-blocking gap: when an IPA is uploaded without `ITSAppUsesNonExemptEncryption` in Info.plist, ASC marks the build "Missing Compliance" and external testing is blocked. The new command PATCHes `/v1/builds/{id}` with `usesNonExemptEncryption` so the answer can be supplied post-upload from CI scripts that don't control the Info.plist. `Build` domain model now carries `usesNonExemptEncryption: Bool?` and a `isMissingEncryptionCompliance` semantic boolean (`true` when nil); usable builds in the missing state advertise a `setEncryptionCompliance` affordance with the ready-to-run command. Build's `tableRow` gains an "Encryption" column showing `uses` / `exempt` / `missing`. REST equivalent: `PATCH /api/v1/builds/:buildId/encryption-compliance` body `{"usesNonExemptEncryption": true|false}`. New `BuildRepository.updateBuildEncryptionCompliance(buildId:usesNonExemptEncryption:)`. See `docs/features/builds-upload.md`. +- **`asc beta-app-localizations {list,get,create,update,delete}` — TestFlight Beta App Description per locale** — closes the parity gap that blocked TestFlight submissions: Apple's `BetaAppLocalizations` resource holds the per-locale beta description, tester feedback email, marketing URL, and privacy policy URL shown in TestFlight before external testing can be enabled. Distinct from the existing `asc builds update-beta-notes` (which writes per-build "What to Test" notes) and `asc beta-review` (which writes review contact info). New `BetaAppLocalization` domain model + `@Mockable BetaAppLocalizationRepository` (Domain/Apps/TestFlight/), `SDKBetaAppLocalizationRepository` (Infrastructure) backed by `/v1/apps/{id}/betaAppLocalizations` (list), `/v1/betaAppLocalizations/{id}` (get/update/delete), and `/v1/betaAppLocalizations` (create). Affordances: `delete`, `get`, `listSiblings`, `update`. REST equivalents: `GET /api/v1/apps/:appId/beta-app-localizations` and `GET /api/v1/beta-app-localizations/:id` via `BetaAppLocalizationsController`. New REST path resolver entry for `beta-app-localizations` parented under `apps`. See `docs/features/beta-app-localizations.md`. + +--- + +## [0.17.8] - 2026-04-29 + +### Added +- **`asc iris iap-submissions delete --submission-id `** — iris-side dequeue of an IAP submission (`DELETE /iris/v1/inAppPurchaseSubmissions/{id}`). Iris-queued submissions don't round-trip through the public-SDK delete; they can only be removed via this iris-cookie-authed path. New `IrisClient.delete`, new `IrisInAppPurchaseSubmissionRepository.deleteSubmission(session:submissionId:)`. REST equivalent: `DELETE /api/v1/iris/iap-submissions/:submissionId` on `IrisIAPSubmissionsController`. **`removeFromNextVersion` affordance now resolves end-to-end through iris** (CLI: `asc iris iap-submissions delete --submission-id `; REST: `DELETE /api/v1/iris/iap-submissions/`). `RESTPathResolver.resourcePath` now converts spaces in segment names to slashes so multi-word commands like `iris iap-submissions` produce clean URL segments. Removed the short-lived public-SDK alias `POST /api/v1/iap/:iapId/unsubmit` (incorrect — public DELETE doesn't accept iris-queued submissions). +- **`removeFromNextVersion` affordance on queued IAPs** — when an IAP is in `READY_TO_SUBMIT` and Apple's iris listing reports `submitWithNextAppStoreVersion: true` (queued to ride along with the next app version), `InAppPurchase.structuredAffordances` surfaces `removeFromNextVersion` → `asc iap unsubmit --submission-id ` and **suppresses** the `submit` affordance (mutually exclusive — Apple rejects re-submits on already-queued IAPs). The dequeue uses the existing public-SDK `DELETE /v1/inAppPurchaseSubmissions/{id}` (Apple's iris flow keys the submission resource by parent IAP id, so `--submission-id ` works). Underpinned by a new best-effort iris enrichment: `IrisSDKInAppPurchaseStateRepository` calls `GET /iris/v1/apps/:appId/inAppPurchasesV2?fields[inAppPurchases]=submitWithNextAppStoreVersion&filter[state]=READY_TO_SUBMIT`, mapping each IAP to its queued bit. `SDKInAppPurchaseRepository.listInAppPurchases` accepts an optional `irisFlagsProvider` closure; the factory wires it from `IrisCookieProvider` + `IrisSDKInAppPurchaseStateRepository`. CI scripts using API-key auth keep their existing JSON output (flag stays `false`, no `removeFromNextVersion` affordance). New `InAppPurchase.submitWithNextAppStoreVersion: Bool` field, encoded only when `true` to avoid snapshot churn. New `IrisInAppPurchaseStateRepository` `@Mockable` Domain protocol. + +### Changed +- **`InAppPurchase` submission affordances reorganized into iris-queue family vs. public-SDK direct submit.** Two inverse pairs, all gated on `state == .readyToSubmit`: + - `addToNextVersion` (NEW) → `asc iris iap-submissions create --iap-id ` — queues the IAP to ride along with the next App Store version submission. Always available for ready, not-yet-queued IAPs. + - `removeFromNextVersion` → `asc iap unsubmit --submission-id ` — dequeues. Mutually exclusive with the others; emitted only when iris listing reports `submitWithNextAppStoreVersion: true`. + - `submit` → `asc iap submit --iap-id ` — public-SDK direct submit (standalone review). Now emitted **only** when `isFirstTimeSubmission == false` (Apple rejects the direct path for the first IAP for an app). + - Established-app ready IAPs see **both** `submit` and `addToNextVersion`; agent picks based on release strategy. + - Replaces the previous behavior where `submit` quietly auto-dispatched between iris and public SDK paths — keys are now explicit about what each call does. New `InAppPurchaseState.hasBeenApproved` semantic boolean. New `iris iap-submissions` REST route registered with the path resolver so `addToNextVersion` `_links` resolve to `/api/v1/iris/iap//submissions`. + +### Added +- **`asc iris iap-submissions create --iap-id ` — IAP submission via the iris private API** — closes the parity gap with the public SDK's `POST /v1/inAppPurchaseSubmissions`, which has no `attributes` field and therefore can't carry `submitWithNextAppStoreVersion`. The new command posts to `POST /iris/v1/inAppPurchaseSubmissions` with that attribute set, which is required for first-time IAP submissions (Apple binds the first IAP to the next App Store version). Defaults to `--with-next-version`; pass `--no-with-next-version` to opt out. Lives under `asc iris …` because it needs iris cookies (either browser cookies or a session from `asc iris auth login`). Surfaced via `IrisStatus.affordances.submitIAP` so `asc iris status` advertises the path when authenticated. Existing `asc iap submit` stays untouched — CI scripts using key-auth keep working unchanged. See `docs/features/iap-subscriptions/submission-iris-parity.md`. +- **REST: `POST /api/v1/iris/iap/:iapId/submissions`** — body `{"submitWithNextAppStoreVersion": true}` (default true); returns the submission resource with `_links.viewIAP` pointing back at the public IAP endpoint. + +### Fixed +- **iris 2FA `verify-code` no longer 401s with a valid code** — `IdmsaAPIClient.applyHeaders` was sending `X-Apple-OAuth-Client-Id`, `X-Apple-OAuth-Redirect-URI`, `X-Apple-OAuth-Response-Mode`, `X-Apple-OAuth-Response-Type`, `Origin`, and `Referer` on `signin/init` and `signin/complete`. None of `XcodesOrg/XcodesApp`, `XcodesOrg/XcodesLoginKit`, or the `rorkai/App-Store-Connect-CLI` Go reference send those — they taint the SRP session so the post-409 `GET /appleauth/auth` returns 401, rotates `scnt`, and the subsequent `POST /verify/trusteddevice/securitycode` is rejected as out-of-sequence. Header set is now narrowed to `Accept`, `Content-Type`, `X-Requested-With`, `X-Apple-Widget-Key`, `scnt`, `X-Apple-ID-Session-Id` (+ `X-Apple-HC` on `signin/complete`). `signinComplete` also flips to `?isRememberMeEnabled=false` with body `rememberMe: false` to match the same references — `true` puts Apple in the "trust this browser" path, which mismatches a CLI session. + +### Added +- **`asc iris auth login` / `verify-code` / `logout` — Apple ID SRP login for the iris private API** — until now iris features (`asc iris apps list`, `asc iris status`) only worked when the user had logged into App Store Connect in their browser; we'd borrow cookies from there. Headless CI had no path. The new command performs a full Apple SRP-6a handshake against `idmsa.apple.com`, handles 2FA (trusted-device push or phone code), then persists the resulting session to `~/.asc/iris/session.json` (0600 perms). New `CompositeIrisCookieProvider` chains SRP-stored cookies before browser cookies, so any existing iris command immediately picks up the SRP session without code changes elsewhere. See `docs/features/iris/iris-srp-login.md`. + - `asc iris auth login --apple-id user@example.com` — prompts for password on stdin, runs SRP, persists session on success or pending-2FA state on 409. + - `asc iris auth verify-code 123456` — submits the 2FA code, calls `2sv/trust`, fetches `olympus/v1/session` for team metadata, persists the full session. + - `asc iris auth logout` — deletes the persisted session. + - `ASC_IRIS_DEBUG=1` — dumps every idmsa request/response to stderr with `a`/`m1`/`m2` redacted, so failures self-capture safely-shareable test data. + - Built on `adam-fowler/swift-srp` (Apache 2.0) for RFC 5054 group-2048 constants, BigNum, and key generation. Apple's PBKDF2-derived `x` is layered on top in `AppleSRPClient`. PBKDF2-HMAC-SHA256 implementation cross-checked against RFC 7914 §11 vectors. + - **Caveat**: while every layer is unit-tested with mocked HTTP and synthesized vectors, real-world correctness against Apple's specific `M1` shape gets validated only on the first real login attempt. If Apple's protocol shifts, expect churn here. + +--- + +## [0.17.7] - 2026-04-29 + +### Added +- **REST `POST` / `PATCH` / `DELETE` for subscription group localizations** — `SubscriptionGroupLocalizationsController` previously exposed only `GET`, so `POST /api/v1/subscription-groups/:groupId/subscription-group-localizations` (the call the AppNexus web UI emits when creating a new locale) returned 404. Now wired: + - `POST /api/v1/subscription-groups/:groupId/subscription-group-localizations` — body `{locale, name, customAppName?}`. + - `PATCH /api/v1/subscription-group-localizations/:localizationId` — body `{name?, customAppName?}`. + - `DELETE /api/v1/subscription-group-localizations/:localizationId` — returns `{"deleted": true}`. +- **`uploadReviewScreenshot` and `uploadImage` affordances on `InAppPurchase` and `Subscription`** — until now an agent looking at an IAP or subscription with no review screenshot had no `_links` path to discover the upload command. The four new affordances close that gap (CLI surface, REST `_links`, REST endpoints). +- **REST upload endpoints (raw body)** — `POST /api/v1/iap/:iapId/review-screenshot`, `POST /api/v1/iap/:iapId/images`, `POST /api/v1/subscriptions/:subscriptionId/review-screenshot`, `POST /api/v1/subscriptions/:subscriptionId/images`. Send the image bytes as the request body (Content-Type `image/png`, `image/jpeg`, etc.); body is spooled to a temp file and uploaded through the existing `uploadReviewScreenshot` / `uploadImage` repository methods. 20MB body ceiling. Same path as the `GET` collection — verb selects read vs. upload. + +### Fixed +- **`uploadImage` / `uploadReviewScreenshot` now wait for ASC processing before returning** — ASC's PATCH-commit response carries an empty `imageAsset` because processing is async; clients were getting `{templateURL: "", width: 0, height: 0}` and had no signal of when the asset URL would appear. Both `SDKInAppPurchaseReviewRepository` and `SDKSubscriptionReviewRepository` now mirror the iOS SDK pattern (`AppStoreSdk` `verifyImageUpload` / `verifyReviewScreenshotUpload`): after the commit PATCH, poll `GET /v1/inAppPurchaseImages/{id}` (or the screenshot equivalent) until `state ∈ {PREPARE_FOR_SUBMISSION, WAITING_FOR_REVIEW, APPROVED}` for images / `assetDeliveryState == COMPLETE` for screenshots. Defaults: 15 attempts × 2s = 30s ceiling; failure states (`FAILED`, `REJECTED`) throw immediately. The mapper additionally treats empty `templateURL` / zero dimensions as nil so any leftover not-ready shape is omitted from JSON via `encodeIfPresent` rather than leaking. Result: `POST /api/v1/iap/:iapId/images` and the three sibling upload endpoints return a record whose `imageAsset` is fully populated and ready to render — no client-side polling needed. + +### Changed +- **`upload` action resolves to bare collection path in REST** — `RESTPathResolver` now treats `upload` like `get/update/delete` (no extra suffix) so an upload `_links` href is `/api/v1/iap/:iapId/review-screenshot` (POST), not `/...review-screenshot/upload`. Standard REST: the verb carries the intent, the path names the resource. +- **`reviewNote` now read-side on `InAppPurchase` and `Subscription`** — the field was previously write-only via `asc iap update --review-note` / `asc subscriptions update --review-note`. List/get responses now expose `reviewNote: String?` so an agent (or web UI like AppNexus's IAP/Subscription review tabs) can read the existing value without a separate fetch. SDK mappers thread `attributes.reviewNote` from `InAppPurchaseV2` and `Subscription` SDK responses; nil values are omitted from JSON output via `encodeIfPresent`. Existing JSON snapshots stay byte-identical when no review note is set. + +--- + +## [0.17.6] - 2026-04-28 + +### Fixed +- **`PATCH /api/v1/iap/{iapId}/availability` returned 404** — the route was never registered, so the web frontend's "save territories" call failed. `IAPAvailabilityController` now serves `PATCH` with body `{territoryIds, availableInNewTerritories}` and routes through `repo.createAvailability` (ASC's `POST /v1/inAppPurchaseAvailabilities` upserts — there is no separate update endpoint). + +--- + +## [0.17.5] - 2026-04-28 + +### Added +- **Production / sandbox split for offer codes** — `InAppPurchaseOfferCode` and `SubscriptionOfferCode` now expose `productionCodeCount` and `sandboxCodeCount` (Apple returns these alongside the existing total). Visible in `asc iap-offer-codes list`, `asc subscription-offer-codes list`, and on the existing `GET /api/v1/iap/:iapId/offer-codes` and `GET /api/v1/subscriptions/:subscriptionId/offer-codes` endpoints (flow through Codable). +- **`environment` on one-time-use code batches** — `InAppPurchaseOfferCodeOneTimeUseCode` and `SubscriptionOfferCodeOneTimeUseCode` now carry `environment: OfferCodeEnvironment?` (PRODUCTION or SANDBOX). Apple separates redemption by environment; sandbox batches redeem against sandbox tester accounts and have a smaller per-quarter ceiling. +- **`--environment` flag on one-time-codes create** — `asc iap-offer-code-one-time-codes create --environment sandbox` and `asc subscription-offer-code-one-time-codes create --environment sandbox`. Default is `production`, matching prior behaviour. Maps to the SDK's `Attributes(environment:)` field on the create-request body so sandbox batches can be generated from CLI. +- **REST endpoints for one-time-codes** — `GET/POST /api/v1/iap-offer-codes/:offerCodeId/one-time-codes`, `PATCH /api/v1/iap-offer-code-one-time-codes/:oneTimeCodeId`, and the subscription mirrors. POST body accepts `{numberOfCodes, expirationDate, environment?}` so REST clients can generate sandbox redemption batches without dropping to CLI. Each batch row's `_links` resolves `listOneTimeCodes` to the nested parent path. +- **`createOfferCode` affordance + POST endpoints** — `InAppPurchase` and `Subscription` now advertise `createOfferCode` in `_links`, mirroring the iOS app's "+ New offer code" affordance. `POST /api/v1/iap/:iapId/offer-codes` accepts `{name, customerEligibilities[]}`; `POST /api/v1/subscriptions/:subscriptionId/offer-codes` accepts `{name, duration, mode, periods, customerEligibilities[], offerEligibility}`. Previously these were CLI-only. +- **Per-territory `prices` on offer-code create (IAP + Subscription)** — both CLI and REST now accept the full territory price list at creation time, matching ASC's actual contract. CLI: `--price =` (repeatable, paid) and `--free-territory ` (repeatable). REST: `prices: [{territory, pricePointId?}]` in POST body — omit `pricePointId` for free. New shared `OfferCodePriceInput` domain type. SDK adapters compose the `included[InAppPurchaseOfferPriceInlineCreate]` / `included[SubscriptionOfferCodePriceInlineCreate]` payload with `${local-price-N}` placeholder ids referenced from `relationships.prices.data` — same pattern as the existing IAP price-schedule create. **Fixes a long-standing bug**: previous `createOfferCode` calls sent an empty `prices` relationship, so every offer code created via asc-cli was missing per-territory pricing (and ASC's `prices` is read-only post-create — there was no recovery path). +- **`--auto-renew` on subscription offer-code create** — CLI flag (default `true`) and REST body field `isAutoRenewEnabled` (also accepts `autoRenew`). Setting it `false` creates a non-renewing/one-time offer; ASC only accepts `--mode FREE_TRIAL` in that case. Previously not exposed. + +--- + +## [0.1.74] - 2026-04-27 + +### Added +- **HATEOAS `_links` for IAPs and Subscriptions** — `GET /api/v1/apps/{id}/iap` and `GET /api/v1/subscription-groups/{id}/subscriptions` now embed `_links` per item so an agent can navigate to localizations, availability, offer codes, price points, review screenshots, promotional offers, win-back offers (subscriptions only), and intro offers without knowing the URL conventions. +- **REST controllers for IAP details** — `GET /api/v1/iap/:id/localizations`, `/availability`, `/offer-codes`, `/price-points`. Each returns the agent-first `data: [...]` envelope with `_links` already populated. +- **REST controllers for Subscription details** — `GET /api/v1/subscriptions/:id/localizations`, `/availability`, `/offer-codes`, `/introductory-offers`. +- **`InAppPurchasePriceSchedule` enriched with `baseTerritory` + `territoryPrices`** — `GET /api/v1/iap/:id/price-schedule` now returns the developer-set base territory plus all auto-equalized per-territory prices (currency + customerPrice + proceeds) so the iOS Pricing tab has data to render. Composes three ASC API calls under the hood: schedule (with base territory), manual prices, and equalizations. +- **IAP equalizations primitive** — `asc iap-equalizations list --price-point-id [--limit N]` and `GET /api/v1/iap-price-points/:id/equalizations`. Exposes `GET /v1/inAppPurchasePricePoints/{id}/equalizations` (~175 territory prices auto-derived from a manual base price). +- **Subscription price schedule** — new `SubscriptionPriceSchedule` domain type with `territoryPrices` and `price(for:)` lookup (no base territory — subscriptions are per-territory). `asc subscription-price-schedule get --subscription-id ` and `GET /api/v1/subscriptions/:id/price-schedule`. Composes `GET /v1/subscriptions/{id}/prices` + equalizations. +- **Subscription equalizations primitive** — `asc subscription-equalizations list --price-point-id ` and `GET /api/v1/subscription-price-points/:id/equalizations`. +- **Multi-territory subscription `setPrices` (batch)** — `asc subscriptions prices set-batch --subscription-id --price USA=spp-1 --price JPN=spp-2 ...`. Mirrors iOS `setPrices(prices:)`. Returns the post-write schedule. +- **Subscription promotional images** — `asc subscription-images list|upload|delete` and `GET /api/v1/subscriptions/:id/images`. New `SubscriptionPromotionalImage` domain type with `delete` suppression while `state.isPendingReview` (mirrors IAP images). +- **`asc subscriptions update --period `** — change billing period (ONE_WEEK, ONE_MONTH, …, ONE_YEAR) on an existing subscription. Mirrors the iOS app's `subscription.update(subscriptionPeriod:)`. +- **`setPrice` affordance + `POST /api/v1/iap/{id}/prices/set`** — IAPs now advertise how to set or change their base price/territory. Calling with a new `baseTerritory` is how the iOS app implements "Change Base Territory" — ASC replaces the schedule and re-equalizes other territories. +- **`setPrices` affordance + `POST /api/v1/subscriptions/{id}/prices/set-batch`** — subscriptions now advertise the batch multi-territory price-set operation. Body accepts `{ prices: [{territory, pricePointId, startDate?, preserveCurrentPrice?}] }` (preferred) or a flat `{ "USA": "spp-1", "JPN": "spp-2" }` shorthand. +- **Cursor pagination for IAP/Subscription price points** — `listPricePoints` now accepts `limit` + `cursor` and returns `PaginatedResponse { data, nextCursor, totalCount }`. CLI: `asc iap price-points list --iap-id X --limit 200 --cursor `. REST: `GET /api/v1/iap/{id}/price-points?territory=CHN&limit=200&cursor=` returns `{ data, nextCursor, totalCount }`. Frontends loop until `nextCursor` is absent. Mirrors iOS's `loadPricePoints(territory:limit:cursor:) → PaginatedResult`. + +### Fixed +- **`POST/PATCH /api/v1/iap/{id}/localizations` returned bare 500 on ASC validation failures + dropped over-limit content** — when the description exceeded ASC's 45-character cap (or name exceeded 30), Hummingbird's default error path returned 500 with no body, browsers showed "Failed to fetch", and the user lost the typed text. The controller now (1) trims `name` to 30 grapheme clusters and `description` to 45 before sending, matching the iOS app's `LocaleCard` `limit:` constants, and (2) catches any remaining ASC error (e.g., invalid locale) and surfaces it as `400 Bad Request` with the underlying message so the UI can render a meaningful inline error. +- **IAP/Subscription availability returned only ~10 territories instead of all ~175** — the parent endpoint's `include=availableTerritories` truncates the relationship to a single page. `getAvailability` now issues two parallel calls (attributes + dedicated `/availableTerritories?limit=200`) so the full territory list reaches the frontend's Availability tab. Matches the iOS SDK's `fetchAvailability` composition. +- **`SubscriptionGroup._links` was empty** — `GET /api/v1/apps/{id}/subscription-groups` items now embed populated `_links` for `listSubscriptions`, `listLocalizations`, `createSubscription`, `createLocalization`, `update`, `delete`. Migrated `SubscriptionGroup` from raw `affordances` to `structuredAffordances` so `apiLinks` auto-derives, and registered `_subscriptionGroupLocalizationRoutes` in `RESTPathResolver.ensureInitialized` so the nested localizations path resolves. +- **`/api/v1/subscription-groups/{id}/subscriptions` returned 404** — the `_links.listSubscriptions` URL had no controller. Added `SubscriptionsController` and wired it in `RESTRoutes.swift`. Each subscription's `_links` already advertise the full child surface (localizations, availability, price-schedule, offers, etc.). +- **Review screenshots and promotional images returned no image URL** — the responses included `id`/`fileName`/`fileSize`/`assetState` but nothing renderable. Added `imageAsset: ImageAsset?` to `SubscriptionReviewScreenshot`, `InAppPurchaseReviewScreenshot`, `SubscriptionPromotionalImage`, `InAppPurchasePromotionalImage` — populated from the SDK's `attributes.imageAsset` (templateUrl + width + height). Frontend calls `imageAsset.url(maxSize:format:)` to substitute the `{w}/{h}/{f}` placeholders and produce a real CDN URL. +- **`SubscriptionPrice._links` was empty** — migrated `SubscriptionPrice` from raw `affordances` to `structuredAffordances` so `apiLinks` auto-derives. `listPricePoints` now resolves to `/api/v1/subscriptions/{id}/price-points` over REST. +- **`POST /iap/{id}/prices/set` returned 500** — the inline manual-price payload omitted the `inAppPurchaseV2` back-reference and used a plain `"p1"` placeholder, both of which ASC rejects. The request now mirrors the iOS app shape: `${local-manual-price-1}` placeholder + `inAppPurchaseV2` relationship pointing back at the IAP. Also wrapped `IAPPricesController` in try/catch so future failures log the underlying ASC error to stderr and return a JSON error body instead of an empty 500. +- **`POST /api/v1/apps/{id}/iap` returned 404** — the route wasn't registered. `IAPController` now serves `POST` (create — accepts `referenceName`, `productId`, and `inAppPurchaseType` in either CLI-style `non-consumable` or raw enum `NON_CONSUMABLE`), `PATCH /iap/{id}` (update), and `DELETE /iap/{id}`. Mirrors `VersionsController`'s POST/PATCH conventions. +- **`POST /api/v1/apps/{id}/subscription-groups` returned 404** — same fix for subscription groups. `SubscriptionGroupsController` now serves `POST` (create — `referenceName`), `PATCH /subscription-groups/{id}` (rename), and `DELETE /subscription-groups/{id}`. +- **`GET /api/v1/iap/{id}/availability` returned 500 for newly-created IAPs** — ASC returns 404 until the developer creates the availability resource. `getAvailability(iapId:)` and `getAvailability(subscriptionId:)` now return `Optional` and tolerate the 404 by returning nil; the SDK adapter mirrors iOS's `refreshTerritoryStatuses` 404 tolerance. +- **Availability tab showed empty list on fresh IAP/subscription** — iOS shows all 175 territories preselected as the default UX; the web frontend got `data: []` and rendered nothing. `IAPAvailabilityController` and `SubscriptionAvailabilityController` now synthesize a default-all-territories availability (with `isAvailableInNewTerritories: true`) by fetching `/v1/territories` whenever no resource exists. Frontend renders the same 175-territory grid iOS does. + +### Changed +- **`RESTPathResolver` resolves singleton-under-parent `get` to nested path** — when an action is not `list`/`create`, the singularized own-id is missing, and a registered route's parent param matches one in `params`, the resolver now returns the nested `/parent/{id}/segment` path. This corrects `_links.getReviewScreenshot` for IAP and Subscription (was `/api/v1/iap-review-screenshot/{id}` → now `/api/v1/iap/{id}/review-screenshot`) and similar singletons (availability, age-rating). +- **`AgeRatingController`** now serves the canonical nested path `/api/v1/app-infos/{appInfoId}/age-rating` matching the resolver's `_links`. The flat `/api/v1/age-rating/{id}` remains for back-compat. + +--- + +## [0.17.3] - 2026-04-26 + +### Added +- **State-aware affordances on the new aggregates** — affordances no longer suggest illegal next actions: + - `PromotedPurchase` exposes `state.isLocked` (true while `WAITING_FOR_REVIEW` / `IN_REVIEW`); `update` and `delete` are suppressed while locked, so an agent following affordances won't issue a 409 against App Review. + - `InAppPurchasePromotionalImage` suppresses `delete` while `state.isPendingReview` is true. + - `InAppPurchaseReviewScreenshot.AssetState` and `SubscriptionReviewScreenshot.AssetState` gain `isComplete` / `hasFailed` semantic booleans. `delete` is offered once the asset is reachable (`uploadComplete` / `complete` / `failed`); while `awaitingUpload`, only `upload` is offered as the recovery path. +- **IAP & Subscription review screenshots + IAP promotional images** — closes the upload-heavy feature parity gap. New CLI commands implement the standard ASC reserve → upload chunks → commit-with-MD5 protocol on top of `URLSession`: + - `asc iap-review-screenshot get|upload|delete` — single review screenshot per IAP. `get` returns an empty `data: []` array when no screenshot is present, mirroring CAEOAS conventions. + - `asc iap-images list|upload|delete` — 1024×1024 promotional images for an IAP, with state semantic booleans (`isApproved`, `isPendingReview`). + - `asc subscription-review-screenshot get|upload|delete` — single review screenshot per subscription. + - New domain types: `InAppPurchaseReviewScreenshot`, `InAppPurchasePromotionalImage` (with `ImageState` enum), `SubscriptionReviewScreenshot`. New repository protocols `InAppPurchaseReviewRepository` + `SubscriptionReviewRepository`, both `@Mockable`. + - `InAppPurchase` now advertises `getReviewScreenshot` + `listImages`; `Subscription` advertises `getReviewScreenshot`. +- **Promoted purchases** — App Store product page promoted slot CRUD. New CLI commands under `asc promoted-purchases`: + - `list --app-id `, `create --app-id (--iap-id | --subscription-id ) [--visible|--hidden] [--enabled|--disabled]`, `update --promoted-id ...`, `delete --promoted-id `. + - New `PromotedPurchase` domain type carrying `appId` + either `inAppPurchaseId` or `subscriptionId` (mutually exclusive at create time, validated in the command). State enum `PromotedPurchaseState` with raw values matching ASC plus `isLocked` / `isApproved` semantic booleans. + - Backed by `GET/POST /v1/apps/{id}/promotedPurchases` and `PATCH/DELETE /v1/promotedPurchases/{id}`. +- **Win-back offers** — full CRUD with eligibility rules, priority, promotion intent, and per-territory pricing. New CLI command tree under `asc win-back-offers`: + - `list --subscription-id `, `delete --offer-id `, `update --offer-id [--priority HIGH|NORMAL] [--start-date ...] [--end-date ...] [--paid-months n] [--since-min n] [--since-max n] [--wait-months n] [--promotion-intent ...]`, `prices list --offer-id `. + - `create --subscription-id --reference-name --offer-id --duration --mode --periods --paid-months --since-min --since-max --start-date --priority HIGH|NORMAL [--end-date ...] [--wait-months n] [--promotion-intent ...] [--price USA=spp-1 --price GBR=spp-2 ...]` — bypasses the generated SDK's incomplete `WinBackOfferPriceInlineCreate` (missing relationships) by encoding the body manually with type-erased `AnyCodable`. + - Domain types: `WinBackOffer` (with `customerEligibilityTimeSinceLastSubscribed` min/max, `WinBackOfferPriority`, `WinBackOfferPromotionIntent`), `WinBackOfferPrice`, `WinBackOfferPriceInput`. `Subscription` advertises `listWinBackOffers`. +- **Subscription promotional offers** — CRUD plus per-territory inline price creation. New CLI commands under `asc subscription-promotional-offers`: + - `list --subscription-id `, `delete --offer-id `, `prices list --offer-id `. + - `create --subscription-id --name --offer-code --duration --mode --periods [--price USA=spp-1 ...]` — uses `${newPromoOfferPrice-N}` 1-based local IDs in the `included` array, matching the ASC web UI request shape. + - Domain types: `SubscriptionPromotionalOffer`, `SubscriptionPromotionalOfferPrice`, shared `PromotionalOfferPriceInput`. `Subscription` advertises `createPromotionalOffer` + `listPromotionalOffers`. +- **Subscription group localizations** — per-locale display name and Custom App Name for a subscription group. New CLI command tree under `asc subscription-group-localizations`: + - `list --group-id `, `create --group-id --locale --name [--custom-app-name ]`, `update --localization-id [--name ] [--custom-app-name ]`, and `delete --localization-id `. + - New `SubscriptionGroupLocalization` domain type ({id, groupId, locale, name?, customAppName?, state?}) with `listSiblings`/`update`/`delete` affordances. New `SubscriptionGroupLocalizationRepository` `@Mockable` protocol, SDK adapter on `/v1/subscriptionGroupLocalizations`. + - `SubscriptionGroup` now advertises `createLocalization`/`listLocalizations` so an agent navigating from a group can discover the localization tree. +- **Offer code prices + one-time code values** — completes the offer-code feature so an agent can read back per-territory pricing and download distributable redemption codes: + - `asc iap-offer-codes prices list --offer-code-id ` and `asc subscription-offer-codes prices list --offer-code-id ` — returns each price with `territory` and `pricePointId` (IAP) or `subscriptionPricePointId` (Subscription). Backed by `GET /v1/inAppPurchaseOfferCodes/{id}/prices` and `GET /v1/subscriptionOfferCodes/{id}/prices`. + - `asc iap-offer-code-one-time-codes values --one-time-code-id ` and `asc subscription-offer-code-one-time-codes values --one-time-code-id ` — fetches the raw CSV body of one-time-use redemption codes for distribution. Backed by `GET /v1/.../oneTimeUseCodes/{id}/values` (`Request`). + - Two new domain types: `InAppPurchaseOfferCodePrice { id, offerCodeId, territory?, pricePointId? }` and `SubscriptionOfferCodePrice { id, offerCodeId, territory?, subscriptionPricePointId? }`. Each advertises a `listPrices` affordance that points back at its parent offer code. + - Repository protocols extended with `listPrices(offerCodeId:)` and `fetchOneTimeUseCodeValues(oneTimeCodeId:) -> String`; SDK adapters implement them on top of the appstoreconnect-swift-sdk. +- **Subscription pricing parity** — subscriptions now match IAP for browsing tiers and committing per-territory prices. Subscriptions don't have a base territory (Apple auto-equalizes), so the model has `proceedsYear2` and `prices set` is per-territory rather than per-base. New CLI commands (mirrored under `asc subscriptions`): + - `asc subscriptions price-points list --subscription-id [--territory ]` — list `SubscriptionPricePoint`s with optional territory filter. Each result advertises `setPrice` only when a territory is attached. + - `asc subscriptions prices set --subscription-id --territory --price-point-id [--start-date YYYY-MM-DD] [--preserve-current-price]` — POST `/v1/subscriptionPrices` to commit a price. + - New domain models: `SubscriptionPricePoint` (id, subscriptionId, territory, customerPrice, proceeds, **proceedsYear2**) and `SubscriptionPrice` (id, subscriptionId). + - New `SubscriptionPriceRepository` `@Mockable` protocol with `listPricePoints` + `setPrice`; SDK adapter wires `GET /v1/subscriptions/{id}/pricePoints` and `POST /v1/subscriptionPrices`. +- **IAP & Subscription lifecycle parity** — symmetric update/delete/unsubmit across the in-app-purchase and subscription aggregates so an agent can drive the full lifecycle, not just create-then-submit. New CLI commands: + - `asc iap-localizations update --localization-id [--name ] [--description ]` and `asc iap-localizations delete --localization-id `. + - `asc subscription-localizations update --localization-id [--name ] [--description ]` and `asc subscription-localizations delete --localization-id `. + - `asc iap update --iap-id [--reference-name ] [--review-note ] [--family-sharable | --not-family-sharable]`, `asc iap delete --iap-id `, and `asc iap unsubmit --submission-id ` (the SDK lacks a generated DELETE, so it goes through a manual `Request`). + - `asc subscriptions update --subscription-id [--name ] [--family-sharable | --not-family-sharable] [--group-level ] [--review-note ]`, `asc subscriptions delete --subscription-id `, and `asc subscriptions unsubmit --submission-id ` (manual DELETE same as IAP). + - `asc subscription-groups update --group-id --reference-name ` and `asc subscription-groups delete --group-id `. + - `asc subscription-offers delete --offer-id ` to drop an introductory offer. +- Affordance updates so the new commands surface in the JSON output of every list/create/submit response: `InAppPurchase` now advertises `update`/`delete`, `Subscription` advertises `update`/`delete`, `SubscriptionGroup` advertises `update`/`delete`, both localization models advertise `update`/`delete`, both submission models advertise `unsubmit`, and `SubscriptionIntroductoryOffer` advertises `delete`. + +--- + +## [0.17.2] - 2026-04-25 + +### Added +- **Plugin update workflow (Sparkle-style)** — check for and apply plugin updates via CLI or REST: + - `asc plugins updates` (CLI) and `GET /api/v1/plugins/updates` (REST) — list every installed plugin where the marketplace has a newer version. Each entry is a `PluginUpdate { name, installedVersion, latestVersion, repositoryURL?, downloadURL? }` with affordances pointing at `asc plugins update --name X` (CLI) and `POST /api/v1/plugins/:name/update` (REST). + - `asc plugins update --name X` (CLI) and `POST /api/v1/plugins/:name/update` (REST) — uninstall the named plugin and reinstall the latest marketplace version. Returns the freshly installed `Plugin`. + - `Plugin.affordances` adds `checkUpdate → asc plugins updates` for installed plugins so frontends can wire a "Check for updates" entry without hard-coding the path. + - New `PluginRepository.listOutdated()` and `update(name:)` methods on the repository protocol; implemented in `PluginMarketRepository` by zipping `listInstalled()` with `listAvailable()` on `name`. +- **Plugins REST install/uninstall/search** — `PluginsController` now mirrors the full `asc plugins` CLI: + - `POST /api/v1/plugins` — install a plugin from the marketplace. Body: `{ "name": "Hello.plugin" }`. Returns the installed `Plugin` with `isInstalled: true`. + - `DELETE /api/v1/plugins/:name` — uninstall by name (or slug). Returns `204 No Content`. + - `GET /api/v1/plugins/market?q=` — search the marketplace. Without `q`, behaves like the existing list. With `q`, calls `PluginRepository.searchAvailable`. Same response shape as `GET /api/v1/plugins/market`. + - These endpoints back the install/uninstall/search affordances already advertised on `Plugin.affordances` — frontends following affordances no longer hit a 404. +- **Auth REST endpoints** — `asc auth` is now drivable from a web client. New `AuthController` exposes: + - `POST /api/v1/auth/accounts` (login) — body `{ keyId, issuerId, privateKeyPEM, name?, vendorNumber? }`. Saves to `~/.asc/credentials.json` and marks active. Returns the new `AuthStatus`. + - `GET /api/v1/auth/accounts` (list) — returns all saved `ConnectAccount` records. + - `GET /api/v1/auth/accounts/active` (check) — returns the active account's `AuthStatus`. + - `PATCH /api/v1/auth/accounts/active` (use) — body `{ name }` switches the active account, returns updated `AuthStatus`. + - `PATCH /api/v1/auth/accounts/:name` (update) — body `{ vendorNumber }` updates a stored account. + - `DELETE /api/v1/auth/accounts/active` and `DELETE /api/v1/auth/accounts/:name` (logout) — return `204 No Content`. + - **Security:** the controller writes API key PEMs to disk via `AuthStorage`. Run `asc web-server` bound to loopback only when this controller is enabled; the request body is sensitive. + - `AuthStatus` and `ConnectAccount` now conform to `Presentable` so REST responses share the `{"data":[…]}` shape used elsewhere. + +--- + +## [0.17.1] - 2026-04-24 + +### Added +- **`createVersion` affordance on `App`** — every app response now advertises `asc versions create --app-id ` (REST: `POST /api/v1/apps/{appId}/versions`). Frontends driven by affordances (e.g. the command center UI) can use the presence of this key to enable a "Create Version" action without hard-coding capabilities. +- **`updateVersion` affordance on editable `AppStoreVersion`** — versions in `prepareForSubmission` state expose `asc versions update --version-id ` (REST: `PATCH /api/v1/versions/{id}`). Live and pending versions omit it, giving the UI a state-aware signal for the edit dialog. +- **`asc versions update --version-id --version `** — new CLI command to update an existing App Store version's version string. Backed by new `VersionRepository.updateVersion(id:versionString:)` which maps to the ASC SDK `AppStoreVersionUpdateRequest`. +- **`POST /api/v1/apps/{appId}/versions`** and **`PATCH /api/v1/versions/{versionId}`** — REST endpoints for version create/update on `VersionsController`. Request body: `{ "versionString": "...", "platform": "IOS" }` for create, `{ "versionString": "..." }` for update. +- **`createLocalization` affordance on `AppInfo`** — every app-info response now advertises the locale-add endpoint. Unblocks the frontend's "+ Add Locale" button, which previously had no link to POST against. +- **`POST /api/v1/app-infos/{appInfoId}/localizations`** — create an `AppInfoLocalization` via REST. Body: `{ "locale": "fr-FR", "name": "..." }`. Returns the new row with its `_links` (`updateLocalization`, `delete`, `listLocalizations`). +- **`PATCH /api/v1/app-info-localizations/{localizationId}`** — update `name`, `subtitle`, `privacyPolicyUrl`, `privacyChoicesUrl`, `privacyPolicyText`. Missing keys mean "don't change"; sending an empty string clears the field. Fixes the `404` the frontend was seeing when saving per-locale name / subtitle / privacy URLs. +- **`DELETE /api/v1/app-info-localizations/{localizationId}`** — delete a locale, returns `204 No Content`. Backs the "trash" button on the localization row. +- **`App.contentRightsDeclaration` + `ContentRightsDeclaration` enum** — apps now carry the third-party-content declaration (`USES_THIRD_PARTY_CONTENT` / `DOES_NOT_USE_THIRD_PARTY_CONTENT`). Field is optional and omitted from JSON when unset. This is the ASC-accurate mapping — the declaration lives on `App`, not on `AppInfo`. +- **`asc apps update --app-id --content-rights-declaration `** + **`PATCH /api/v1/apps/{appId}`** — update content rights declaration via CLI or REST. Backed by new `AppRepository.updateContentRights(appId:declaration:)` which maps to the ASC SDK `AppUpdateRequest`. +- **`updateContentRights` affordance on `App`** — every app response advertises the new PATCH endpoint so frontends can wire the declaration switch without hard-coding the URL. +- **`PATCH /api/v1/age-rating/{declarationId}`** — update an age rating declaration via REST. Body accepts any subset of the `AgeRatingDeclarationUpdate` fields (boolean flags, `ContentIntensity` values, `kidsAgeBand`, `ageRatingOverride`, `koreaAgeRatingOverride`). Fixes the `404` the frontend was seeing when following the `update` `_link` on an `AgeRatingDeclaration` response. Matches the affordance key already advertised by `AgeRatingDeclaration.structuredAffordances`. + +--- + +## [0.17.0] - 2026-04-21 + +### Added +- **`GET /api/v1/apps?include=icon`** — enriches each app in the response with its primary build's `iconAsset` (`templateUrl`, `width`, `height`). Icon fetch is opt-in to keep the default list-apps path fast. Without `?include=icon`, behaviour is unchanged. Template URL placeholders (`{w}`, `{h}`, `{f}`) can be substituted client-side to render at any size (e.g. `120x120bb.png`). +- **Domain types `ImageAsset`, `App.iconAsset`** — new optional `iconAsset` on `App` (omitted from JSON when nil), new `ImageAsset` value type under `Domain/Shared/` with `url(maxSize:format:)` helper. Populated from SDK `Build.iconAssetToken` via `/v1/apps/{id}/appStoreVersions?include=build` +- **`AppRepository.fetchAppIcon(appId:)`** — returns `ImageAsset?` by joining the latest app version to its build. Returns `nil` when no version has an attached build +- **`GET /api/v1/apps/{appId}/app-infos`** — new REST route backed by `AppInfoRepository`. Previously the affordance on `App` advertised this path but the controller returned 404. `AppInfo` is now `Presentable` and uses `structuredAffordances`, so responses include `_links` to app-info localizations, age rating, and the enclosing app-infos list +- **`AppInfo.appStoreState` / `AppInfo.state`** — app-infos responses now include lifecycle fields (`appStoreState` uses legacy ASC version states like `READY_FOR_SALE`; `state` uses the newer `AppInfo.State` enum like `READY_FOR_DISTRIBUTION`, `PREPARE_FOR_SUBMISSION`). Agents can use these to pick the live-version app-info vs the version-being-prepared. Computed booleans `isLive` and `isEditable` expose the common decisions +- **`AppInfo.appStoreAgeRating`** — computed App Store age rating (e.g. `FOUR_PLUS`, `NINE_PLUS`, `TWELVE_PLUS`, `SEVENTEEN_PLUS`) now surfaced on app-infos responses. This is what the App Store listing page displays; cheaper than fetching the full `ageRatingDeclaration` when only the label is needed +- **`asc app-categories get --category-id `** + **`GET /api/v1/app-categories/{id}`** — fetch a single App Store category by ID. `AppInfo` responses now include `getPrimaryCategory`, `getSecondaryCategory`, and four subcategory affordances (conditional on the respective ID being set), each pointing at the new endpoint. Backed by new `AppCategoryRepository.getCategory(id:)` +- **`GET /api/v1/app-categories`** — list categories exposed at the REST layer (previously CLI-only). Optional `?platform=` query param mirrors the CLI flag +- **`GET /api/v1/app-infos/{appInfoId}/localizations`** — new REST route backed by `AppInfoRepository.listLocalizations`. `AppInfoLocalization` is now `Presentable` with `structuredAffordances` including update/delete links +- **Unified REST path resolver** — removed the global `{param → segment}` alias table from `RESTPathResolver`. New single rule: for resource actions (get/update/delete/submit/…), the command name IS the REST segment. CLI flag aliases (e.g. `--localization-id`, `--product-id`) are presentation concerns and no longer drive REST routing. Fixes ambiguity where the same CLI flag belonged to multiple resources +- **`GET /api/v1/age-rating/{appInfoId}`** — fetch the age rating declaration for an app-info. Previously only `asc age-rating get` worked; the REST route was advertised via `_links.getAgeRating` but returned 404. `AgeRatingDeclaration` is now `Presentable` with `structuredAffordances` +- **`PATCH /api/v1/app-infos/{appInfoId}`** — update app-info categories (primary, secondary, up to two subcategories each) via REST. JSON body accepts `primaryCategoryId`, `primarySubcategoryOneId`, `primarySubcategoryTwoId`, `secondaryCategoryId`, `secondarySubcategoryOneId`, `secondarySubcategoryTwoId` — any subset. Returns the updated `AppInfo` with `_links` that now include `getPrimaryCategory` / `getSecondaryCategory` pointing at the new values +- **`GET /api/v1/version-localizations/{localizationId}/screenshot-sets`** and **`GET /api/v1/screenshot-sets/{setId}/screenshots`** — screenshot-related `_links` on `AppStoreVersionLocalization` and `AppScreenshotSet` responses now resolve to working REST endpoints. `AppStoreVersionLocalization`, `AppScreenshotSet`, and `AppScreenshot` now use `structuredAffordances` (replacing raw `affordances`); the latter two also gained `Presentable` conformance + +### Changed +- **REST controllers split by resource (SRP/OCP refactor)** — the former `AppsController` was a god controller accumulating 15+ routes across 8 unrelated resources (apps, versions, localizations, screenshot-sets, screenshots, builds, testflight, reviews, iap, subscription-groups, app-infos, app-info-localizations, app-categories, age-rating). Now split into 13 focused controllers, one per REST resource type — `AppsController`, `VersionsController`, `VersionLocalizationsController`, `ScreenshotSetsController`, `ScreenshotsController`, `BuildsController`, `TestFlightController`, `CustomerReviewsController`, `IAPController`, `SubscriptionGroupsController`, `AppInfosController`, `AppCategoriesController`, `AgeRatingController`. Each takes only the 1–2 repositories it actually needs. `RESTRoutes.configure` composes them with one line per resource. Adding a new resource no longer requires editing an existing controller + +### Fixed +- **`RESTPathResolver.ensureInitialized` race** — the `initialized` flag was being set *before* the domain route registrations ran, so concurrent callers could observe `initialized == true` while the `routes` dictionary was still empty. Now uses a separate `initLock` and sets the flag only after all `_*Routes` registrations complete. Fixes intermittent test failures where screenshot/localization `_links` resolved to top-level paths under parallel test execution +- **`include=primaryCategory,…` on app-infos request** — added to `SDKAppInfoRepository.listAppInfos` alongside `fields[appInfos]`. The ASC API returns only relationship `links` with `fields[]` alone; `include=` is required to populate `relationships.primaryCategory.data.id` so the mapper can surface `primaryCategoryId` etc. + +### Fixed +- **`AppInfo.primaryCategoryId` / secondary categories missing from `/v1/apps/{id}/appInfos`** — the ASC API returns relationship `data` sparsely unless the client requests the explicit sparse fieldset. `SDKAppInfoRepository.listAppInfos` now passes `fields[appInfos]=primaryCategory,primarySubcategoryOne,primarySubcategoryTwo,secondaryCategory,secondarySubcategoryOne,secondarySubcategoryTwo`, so the mapped `AppInfo` carries the category IDs + +[0.17.9]: https://github.com/tddworks/asc-cli/compare/v0.17.8...v0.17.9 +[0.17.8]: https://github.com/tddworks/asc-cli/compare/v0.17.7...v0.17.8 +[0.17.7]: https://github.com/tddworks/asc-cli/compare/v0.1.74...v0.17.7 +[0.1.74]: https://github.com/tddworks/asc-cli/compare/v0.17.3...v0.1.74 +[0.17.3]: https://github.com/tddworks/asc-cli/compare/v0.17.2...v0.17.3 +[0.17.2]: https://github.com/tddworks/asc-cli/compare/v0.17.1...v0.17.2 +[0.17.1]: https://github.com/tddworks/asc-cli/compare/v0.17.0...v0.17.1 +[0.17.0]: https://github.com/tddworks/asc-cli/compare/v0.16.9...v0.17.0 diff --git a/docs/features/simulators.md b/docs/features/simulators.md index f1e1f780..88421019 100644 --- a/docs/features/simulators.md +++ b/docs/features/simulators.md @@ -2,7 +2,7 @@ Manage local iOS simulators from the CLI — list, boot, and shutdown. -Streaming and interaction features are available via the [ASC Pro plugin](plugin-ui-architecture.md). +Streaming and interaction features are available via the [ASC Pro plugin](plugins.md). ## CLI Usage @@ -123,7 +123,7 @@ asc simulators shutdown --udid CF65871E-B600-40CB-8B18-B6B7101D38E1 ``` Streaming, interaction, and device bezels are provided by the ASC Pro plugin. -See [Plugin Architecture](plugin-ui-architecture.md) for details. +See [Plugin Architecture](plugins.md) for details. --- diff --git a/docs/features/web-server-architecture.md b/docs/features/web-server-architecture.md index 30061aa7..babc49b7 100644 --- a/docs/features/web-server-architecture.md +++ b/docs/features/web-server-architecture.md @@ -4,7 +4,7 @@ The ASC web server (`asc web-server`) is a Swift HTTP/WebSocket server built on [Hummingbird 2.21.1](https://github.com/hummingbird-project/hummingbird). It serves as the API bridge between the browser UI (hosted on `asccli.app`) and the CLI. Single binary, zero external dependencies. -Plugins extend the server with additional routes, commands, and affordances. See [Plugin Architecture](plugin-ui-architecture.md). +Plugins extend the server with additional routes, commands, and affordances. See [Plugin Architecture](plugins.md). ## Architecture diff --git a/scripts/changelog-rollover.py b/scripts/changelog-rollover.py new file mode 100755 index 00000000..d3b73a4b --- /dev/null +++ b/scripts/changelog-rollover.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Keep CHANGELOG.md to [Unreleased] + the current minor version. + +Every released section whose minor is older than the newest release is moved, +unchanged, into docs/changelog/.md, together with its reference +link. CHANGELOG.md ends with an "Older releases" line linking every archive. +Safe to run repeatedly; a no-op when there is nothing to move. + +Usage: scripts/changelog-rollover.py [CHANGELOG.md] +""" +import re +import sys +from pathlib import Path + +# Headings that were released with a typo. Archived under the minor their +# release date puts them in; the heading itself is left unchanged. +MISNUMBERED = {"0.1.74": "0.17"} + +SECTION = re.compile(r"^## \[([^\]]+)\]") +LINK = re.compile(r"^\[([^\]]+)\]: \S+") +OLDER = "## Older releases" + + +def minor(version): + if version in MISNUMBERED: + return MISNUMBERED[version] + return ".".join(version.split(".")[:2]) + + +def sort_key(m): + return tuple(int(p) for p in m.split(".")) + + +def split(text): + """Return (header, [(version, lines)], links) for a changelog file.""" + header, sections, links = [], [], [] + current = None + for line in text.splitlines(): + if LINK.match(line): + links.append(line) + elif line.startswith(OLDER): + current = None # regenerated below + elif (m := SECTION.match(line)): + current = (m.group(1), [line]) + sections.append(current) + elif current is not None: + current[1].append(line) + elif not sections: + header.append(line) + return header, sections, links + + +def strip(lines): + while lines and lines[-1].strip() in ("", "---"): + lines = lines[:-1] + return lines + + +def main(): + path = Path(sys.argv[1] if len(sys.argv) > 1 else "CHANGELOG.md") + archive_dir = path.parent / "docs" / "changelog" + header, sections, links = split(path.read_text()) + + released = [v for v, _ in sections if v != "Unreleased"] + if not released: + return + keep_minor = minor(released[0]) + + kept = [(v, l) for v, l in sections if v == "Unreleased" or minor(v) == keep_minor] + moved = {} + for v, l in sections: + if v != "Unreleased" and minor(v) != keep_minor: + moved.setdefault(minor(v), []).append((v, l)) + + kept_versions = {v for v, _ in kept} + kept_links = [l for l in links if LINK.match(l).group(1) in kept_versions] + moved_links = {} + for l in links: + v = LINK.match(l).group(1) + if v not in kept_versions: + moved_links.setdefault(minor(v), []).append(l) + + archive_dir.mkdir(parents=True, exist_ok=True) + for m, secs in moved.items(): + target = archive_dir / f"{m}.md" + old_secs, old_links = [], [] + if target.exists(): + _, old_secs, old_links = split(target.read_text()) + body = [ + f"# Changelog — {m}.x", + "", + "Archived unchanged from [CHANGELOG.md](../../CHANGELOG.md), which holds the current releases.", + "", + ] + for _, lines in secs + old_secs: + body += strip(lines) + ["", "---", ""] + body = strip(body) + [""] + all_links = moved_links.get(m, []) + old_links + if all_links: + body += all_links + [""] + target.write_text("\n".join(body)) + + archives = sorted((p.stem for p in archive_dir.glob("*.md")), key=sort_key, reverse=True) + out = strip(header) + [""] + for _, lines in kept: + out += strip(lines) + ["", "---", ""] + out = strip(out) + [""] + if archives: + out += [OLDER, "", " · ".join(f"[{a}](docs/changelog/{a}.md)" for a in archives), ""] + if kept_links: + out += kept_links + [""] + path.write_text("\n".join(out)) + + +if __name__ == "__main__": + main() diff --git a/scripts/check-docs.py b/scripts/check-docs.py new file mode 100755 index 00000000..12d72ee8 --- /dev/null +++ b/scripts/check-docs.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Check the docs against docs/documentation-design/README.md. + +Reports every problem; exits non-zero only with --strict. +Usage: scripts/check-docs.py [--strict] +""" +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +BUDGETS = {"README.md": 150, "CLAUDE.md": 100} +FEATURE_BUDGET = 200 +BULLET_CHARS = 300 +SCANNED = ["*.md", "docs/**/*.md", ".claude/skills/**/*.md"] + +LINK = re.compile(r"\]\(([^)\s]+)\)") +URL = re.compile(r"\(https?://[^)]*\)") +FENCE = re.compile(r"^\s*(```|~~~)") + +problems = [] + + +def report(path, msg): + problems.append(f"{path.relative_to(ROOT)}: {msg}") + + +def lines_outside_fences(text): + fenced = False + for n, line in enumerate(text.splitlines(), 1): + if FENCE.match(line): + fenced = not fenced + elif not fenced: + yield n, line + + +def feature_docs(): + features = ROOT / "docs" / "features" + return sorted(features.glob("*/README.md")) + sorted(features.glob("*.md")) + + +def description(path): + m = re.match(r"^---\n(.*?)\n---\n", path.read_text(), re.S) + if not m: + return None + d = re.search(r"^description:\s*(.+)$", m.group(1), re.M) + return d.group(1).strip() if d else None + + +def check_budgets(): + for name, limit in BUDGETS.items(): + path = ROOT / name + n = len(path.read_text().splitlines()) + if n > limit: + report(path, f"{n} lines, budget {limit}") + for path in feature_docs(): + n = len(path.read_text().splitlines()) + if n > FEATURE_BUDGET: + report(path, f"{n} lines, budget {FEATURE_BUDGET}") + + +def check_descriptions(): + for path in feature_docs(): + d = description(path) + if not d: + report(path, "missing frontmatter `description`") + elif len(d) > 250: + report(path, f"description is {len(d)} chars, limit 250") + + +def check_changelog(): + path = ROOT / "CHANGELOG.md" + text = path.read_text() + unreleased = re.search(r"^## \[Unreleased\]\n(.*?)(?=^## |\Z)", text, re.S | re.M) + if unreleased: + for line in unreleased.group(1).splitlines(): + if line.startswith("- "): + n = len(URL.sub("()", line)) + if n > BULLET_CHARS: + report(path, f"[Unreleased] bullet is {n} chars, limit {BULLET_CHARS}: {line[:60]}…") + minors = [] + for v in re.findall(r"^## \[(\d+\.\d+)\.\d+\]", text, re.M): + if v not in minors: + minors.append(v) + if len(minors) > 1: + report(path, f"holds minors {', '.join(minors)}; run scripts/changelog-rollover.py") + + +def check_links(): + files = {p for pattern in SCANNED for p in ROOT.glob(pattern)} + for path in sorted(files): + for n, line in lines_outside_fences(path.read_text()): + line = re.sub(r"`[^`]*`", "", line) # links inside inline code are examples + for target in LINK.findall(line): + if re.match(r"^(https?:|mailto:|#)", target): + continue + target = target.split("#")[0] + if not (path.parent / target).exists(): + report(path, f"line {n}: broken link {target}") + + +def main(): + check_budgets() + check_descriptions() + check_changelog() + check_links() + for p in problems: + print(p) + print(f"{len(problems)} problem(s)") + if problems and "--strict" in sys.argv: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/promote-changelog.sh b/scripts/promote-changelog.sh index 96736c35..07e42c24 100755 --- a/scripts/promote-changelog.sh +++ b/scripts/promote-changelog.sh @@ -65,3 +65,6 @@ awk \ mv "$TEMP_FILE" "$CHANGELOG_FILE" echo "Promoted [Unreleased] → [$VERSION] - $RELEASE_DATE in $CHANGELOG_FILE" + +# Keep CHANGELOG.md to [Unreleased] + the current minor; move older minors to docs/changelog/ +python3 "$(dirname "$0")/changelog-rollover.py" "$CHANGELOG_FILE" From d71713655f0df67b14aa61106f3242ae2a822ab5 Mon Sep 17 00:00:00 2001 From: slamhan Date: Wed, 23 Sep 2026 18:32:48 +0800 Subject: [PATCH 03/14] docs: generate command reference and docs index (make docs) docs/commands.md is generated from `asc --experimental-dump-help` with plugins disabled; docs/README.md is generated from feature doc frontmatter. Neither is edited by hand. Co-Authored-By: Claude Opus 5.5 (1M context) --- Makefile | 9 +- docs/README.md | 54 + docs/commands.md | 4020 +++++++++++++++++++++++++++++++++++++++++++ scripts/gen-docs.py | 158 ++ 4 files changed, 4240 insertions(+), 1 deletion(-) create mode 100644 docs/README.md create mode 100644 docs/commands.md create mode 100644 scripts/gen-docs.py diff --git a/Makefile b/Makefile index dbe7ee9d..68350bca 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build test lint format clean dev run +.PHONY: build test lint format clean dev run docs check-docs build: swift build @@ -22,3 +22,10 @@ dev: build run: swift run asc $(ARGS) + +# Regenerate docs/commands.md and docs/README.md (never edit those by hand) +docs: build + python3 scripts/gen-docs.py .build/debug/asc + +check-docs: + python3 scripts/check-docs.py diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..09f4c0b6 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,54 @@ + +# asc-cli docs + +New here? Start with the [README](../README.md). For every flag, see the [command reference](commands.md). + +| Feature | What it's for | +|---|---| +| [age-rating](features/age-rating.md) | | +| [app-clips](features/app-clips.md) | | +| [app-icons](features/app-icons.md) | | +| [app-info-localizations](features/app-info-localizations.md) | | +| [app-infos](features/app-infos.md) | | +| [app-previews](features/app-previews.md) | | +| [app-pricing](features/app-pricing.md) | | +| [app-shots](features/app-shots.md) | | +| [app-shots-themes](features/app-shots-themes.md) | | +| [app-wall](features/app-wall.md) | | +| [asc-auth](features/asc-auth.md) | | +| [asc-users](features/asc-users.md) | | +| [asc-web-management](features/asc-web-management.md) | | +| [beta-app-localizations](features/beta-app-localizations.md) | | +| [beta-review](features/beta-review.md) | | +| [builds-archive](features/builds-archive.md) | | +| [builds-upload](features/builds-upload.md) | | +| [code-signing](features/code-signing.md) | | +| [command-center-react](features/command-center-react.md) | | +| [customer-reviews](features/customer-reviews.md) | | +| [game-center](features/game-center.md) | | +| [iap-subscription-availability](features/iap-subscription-availability.md) | | +| [iap-subscriptions](features/iap-subscriptions.md) | | +| [init](features/init.md) | | +| [market](features/market.md) | | +| [performance](features/performance.md) | | +| [plugins](features/plugins.md) | | +| [product-page-optimization](features/product-page-optimization.md) | | +| [promoted-purchases](features/promoted-purchases.md) | | +| [reports](features/reports.md) | | +| [resolution-center](features/resolution-center.md) | | +| [rest-api](features/rest-api.md) | | +| [review-submissions](features/review-submissions.md) | | +| [screenshot-editor](features/screenshot-editor.md) | | +| [screenshots](features/screenshots.md) | | +| [simulators](features/simulators.md) | | +| [skills](features/skills.md) | | +| [submit-with-products](features/submit-with-products.md) | | +| [testflight](features/testflight.md) | | +| [version-check-readiness](features/version-check-readiness.md) | | +| [version-localizations](features/version-localizations.md) | | +| [version-review-detail](features/version-review-detail.md) | | +| [web-apps](features/web-apps.md) | | +| [web-server-architecture](features/web-server-architecture.md) | | +| [xcode-cloud](features/xcode-cloud.md) | | + +**Guides:** [Release workflow](release.md) · [Use as a Swift package](library.md) · [Design: CAEOAS and architecture](design.md) · [How these docs are organised](documentation-design/README.md) diff --git a/docs/commands.md b/docs/commands.md new file mode 100644 index 00000000..718bf8ad --- /dev/null +++ b/docs/commands.md @@ -0,0 +1,4020 @@ + +# Command reference + +Every `asc` command and flag, generated from the binary. For how to use a feature, start at the [docs index](README.md). + +**Global flags** (accepted by every command, not repeated below): +`--output json|table|markdown` (default `json`) · `--pretty` · `--timeout <30s|2m>` + +**Commands:** [apps](#asc-apps) · [init](#asc-init) · [versions](#asc-versions) · [version-localizations](#asc-version-localizations) · [screenshot-sets](#asc-screenshot-sets) · [screenshots](#asc-screenshots) · [app-infos](#asc-app-infos) · [app-info-localizations](#asc-app-info-localizations) · [builds](#asc-builds) · [testflight](#asc-testflight) · [auth](#asc-auth) · [tui](#asc-tui) · [bundle-ids](#asc-bundle-ids) · [certificates](#asc-certificates) · [devices](#asc-devices) · [profiles](#asc-profiles) · [app-preview-sets](#asc-app-preview-sets) · [app-previews](#asc-app-previews) · [iap](#asc-iap) · [iap-equalizations](#asc-iap-equalizations) · [iap-localizations](#asc-iap-localizations) · [iap-price-schedule](#asc-iap-price-schedule) · [subscription-groups](#asc-subscription-groups) · [subscriptions](#asc-subscriptions) · [subscription-localizations](#asc-subscription-localizations) · [subscription-group-localizations](#asc-subscription-group-localizations) · [subscription-price-schedule](#asc-subscription-price-schedule) · [subscription-equalizations](#asc-subscription-equalizations) · [subscription-offers](#asc-subscription-offers) · [subscription-promotional-offers](#asc-subscription-promotional-offers) · [win-back-offers](#asc-win-back-offers) · [promoted-purchases](#asc-promoted-purchases) · [experiments](#asc-experiments) · [experiment-treatments](#asc-experiment-treatments) · [experiment-treatment-localizations](#asc-experiment-treatment-localizations) · [iap-review-screenshot](#asc-iap-review-screenshot) · [iap-images](#asc-iap-images) · [subscription-review-screenshot](#asc-subscription-review-screenshot) · [subscription-images](#asc-subscription-images) · [subscription-offer-codes](#asc-subscription-offer-codes) · [subscription-offer-code-custom-codes](#asc-subscription-offer-code-custom-codes) · [subscription-offer-code-one-time-codes](#asc-subscription-offer-code-one-time-codes) · [iap-offer-codes](#asc-iap-offer-codes) · [iap-offer-code-custom-codes](#asc-iap-offer-code-custom-codes) · [iap-offer-code-one-time-codes](#asc-iap-offer-code-one-time-codes) · [app-shots](#asc-app-shots) · [age-rating](#asc-age-rating) · [app-categories](#asc-app-categories) · [version-review-detail](#asc-version-review-detail) · [plugins](#asc-plugins) · [skills](#asc-skills) · [app-wall](#asc-app-wall) · [users](#asc-users) · [user-invitations](#asc-user-invitations) · [xcode-cloud](#asc-xcode-cloud) · [game-center](#asc-game-center) · [app-clips](#asc-app-clips) · [app-clip-experiences](#asc-app-clip-experiences) · [app-clip-experience-localizations](#asc-app-clip-experience-localizations) · [sales-reports](#asc-sales-reports) · [finance-reports](#asc-finance-reports) · [analytics-reports](#asc-analytics-reports) · [reviews](#asc-reviews) · [review-responses](#asc-review-responses) · [perf-metrics](#asc-perf-metrics) · [diagnostics](#asc-diagnostics) · [diagnostic-logs](#asc-diagnostic-logs) · [beta-review](#asc-beta-review) · [beta-app-localizations](#asc-beta-app-localizations) · [review-submissions](#asc-review-submissions) · [app-availability](#asc-app-availability) · [iap-availability](#asc-iap-availability) · [subscription-availability](#asc-subscription-availability) · [territories](#asc-territories) · [web-server](#asc-web-server) · [iris](#asc-iris) · [simulators](#asc-simulators) + +## asc apps + +Manage apps + +### asc apps list + +List all apps + +``` +asc apps list [--limit ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--limit` | | | Maximum number of apps to return | + +### asc apps update + +Update app-level metadata (currently: content rights declaration) + +``` +asc apps update --app-id [--content-rights-declaration ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID | +| `--content-rights-declaration` | | | Content rights declaration: USES_THIRD_PARTY_CONTENT or DOES_NOT_USE_THIRD_PARTY_CONTENT | + +### asc apps price-points list + +List the prices an app can be sold at in a territory (the 0 price makes it free) + +``` +asc apps price-points list --app-id [--territory ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID | +| `--territory` | | `USA` | Territory code (default: USA) | + +### asc apps prices set + +Set an app's price from a base-territory price point; Apple equalizes the other territories + +``` +asc apps prices set --app-id --base-territory --price-point-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID | +| `--base-territory` | yes | | Base territory code, e.g. USA | +| `--price-point-id` | yes | | Price point ID in the base territory (from `asc apps price-points list`) | + +## asc init + +Initialise project context — saves app ID to .asc/project.json + +``` +asc init [--app-id ] [--name ] [--contact-first-name ] [--contact-last-name ] [--contact-phone ] [--contact-email ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | | | App Store Connect app ID | +| `--name` | | | App name to search for | +| `--contact-first-name` | | | Review contact first name | +| `--contact-last-name` | | | Review contact last name | +| `--contact-phone` | | | Review contact phone number | +| `--contact-email` | | | Review contact email address | + +## asc versions + +Manage App Store versions + +### asc versions list + +List App Store versions for an app (one per platform: iOS, macOS, tvOS, …) + +``` +asc versions list --app-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID | + +### asc versions create + +Create a new App Store version + +``` +asc versions create --app-id --platform +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID | +| `--platform` | yes | | Platform: ios, macos, tvos, watchos, visionos | + +### asc versions update + +Update an App Store version's version string, copyright, or release schedule + +``` +asc versions update --version-id [--copyright ] [--release-type ] [--earliest-release-date ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--version-id` | yes | | App Store version ID | +| `--copyright` | | | Copyright line shown on the App Store page (e.g. "© 2026 Acme") | +| `--release-type` | | | Release type: MANUAL, AFTER_APPROVAL, or SCHEDULED | +| `--earliest-release-date` | | | ISO-8601 timestamp for SCHEDULED releases (e.g. 2026-06-01T00:00:00Z) | + +### asc versions submit + +Submit an App Store version for review + +``` +asc versions submit --version-id [--with-products] [--dry-run] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--version-id` | yes | | App Store Version ID | +| `--with-products` | | | Also submit every ready in-app purchase and subscription version with this app version | +| `--dry-run` | | | List what would be submitted without submitting (implies --with-products) | + +### asc versions set-build + +Link a build to an App Store version before submission + +``` +asc versions set-build --version-id --build-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--version-id` | yes | | App Store version ID | +| `--build-id` | yes | | Build ID | + +### asc versions check-readiness + +Pre-flight check for App Store submission readiness + +``` +asc versions check-readiness --version-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--version-id` | yes | | App Store Version ID | + +## asc version-localizations + +Manage App Store version localizations + +### asc version-localizations list + +List localizations for an App Store version + +``` +asc version-localizations list --version-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--version-id` | yes | | App Store version ID | + +### asc version-localizations create + +Create a new App Store version localization + +``` +asc version-localizations create --version-id --locale +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--version-id` | yes | | App Store version ID | +| `--locale` | yes | | Locale identifier (e.g. en-US, zh-Hans) | + +### asc version-localizations update + +Update text content for an App Store version localization + +``` +asc version-localizations update --localization-id [--whats-new ] [--description ] [--keywords ] [--marketing-url ] [--support-url ] [--promotional-text ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--localization-id` | yes | | App Store version localization ID | +| `--whats-new` | | | What's New text (release notes) | +| `--description` | | | App description | +| `--keywords` | | | Keywords (comma-separated) | +| `--marketing-url` | | | Marketing URL | +| `--support-url` | | | Support URL | +| `--promotional-text` | | | Promotional text | + +## asc screenshot-sets + +Manage App Store screenshot sets + +### asc screenshot-sets list + +List screenshot sets for an App Store version localization + +``` +asc screenshot-sets list --localization-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--localization-id` | yes | | App Store version localization ID | + +### asc screenshot-sets create + +Create a new screenshot set for a localization + +``` +asc screenshot-sets create --localization-id --display-type +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--localization-id` | yes | | Localization ID | +| `--display-type` | yes | | Screenshot display type (e.g. APP_IPHONE_67) | + +## asc screenshots + +Manage App Store screenshots + +### asc screenshots list + +List screenshots in a screenshot set + +``` +asc screenshots list --set-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--set-id` | yes | | Screenshot set ID | + +### asc screenshots upload + +Upload a screenshot to a screenshot set + +``` +asc screenshots upload --set-id --file +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--set-id` | yes | | Screenshot set ID | +| `--file` | yes | | Path to the image file | + +### asc screenshots import + +Import screenshots from an exported ZIP file + +``` +asc screenshots import --version-id --from +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--version-id` | yes | | App Store version ID | +| `--from` | yes | | Path to export.zip from the screenshot editor | + +## asc app-infos + +Manage App Store app info + +### asc app-infos list + +List app infos for an app + +``` +asc app-infos list --app-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID | + +### asc app-infos update + +Update app info categories + +``` +asc app-infos update --app-info-id [--primary-category ] [--primary-subcategory-one ] [--primary-subcategory-two ] [--secondary-category ] [--secondary-subcategory-one ] [--secondary-subcategory-two ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-info-id` | yes | | App info ID | +| `--primary-category` | | | Primary category ID (e.g. 6014 for Games) | +| `--primary-subcategory-one` | | | Primary subcategory 1 ID | +| `--primary-subcategory-two` | | | Primary subcategory 2 ID | +| `--secondary-category` | | | Secondary category ID | +| `--secondary-subcategory-one` | | | Secondary subcategory 1 ID | +| `--secondary-subcategory-two` | | | Secondary subcategory 2 ID | + +## asc app-info-localizations + +Manage App Store app info localizations + +### asc app-info-localizations list + +List localizations for an app info + +``` +asc app-info-localizations list --app-info-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-info-id` | yes | | App info ID | + +### asc app-info-localizations create + +Create a new app info localization + +``` +asc app-info-localizations create --app-info-id --locale --name +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-info-id` | yes | | App info ID | +| `--locale` | yes | | Locale identifier (e.g. en-US, zh-Hans) | +| `--name` | yes | | App name (up to 30 characters) | + +### asc app-info-localizations update + +Update an app info localization + +``` +asc app-info-localizations update --localization-id [--name ] [--subtitle ] [--privacy-policy-url ] [--privacy-choices-url ] [--privacy-policy-text ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--localization-id` | yes | | Localization ID | +| `--name` | | | App name (up to 30 characters) | +| `--subtitle` | | | Subtitle (up to 30 characters) | +| `--privacy-policy-url` | | | Privacy policy URL | +| `--privacy-choices-url` | | | Privacy choices URL | +| `--privacy-policy-text` | | | Privacy policy text | + +### asc app-info-localizations delete + +Delete an app info localization + +``` +asc app-info-localizations delete --localization-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--localization-id` | yes | | Localization ID | + +## asc builds + +Manage builds + +### asc builds list + +List builds + +``` +asc builds list [--app-id ] [--platform ] [--limit ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | | | Filter by app ID | +| `--platform` | | | Filter by platform (ios, macos, tvos, visionos) | +| `--limit` | | | Maximum number of builds to return | + +### asc builds next-number + +Get the next build number for a version + +``` +asc builds next-number --app-id --platform +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID (required) | +| `--platform` | yes | | Platform (ios, macos, tvos, visionos) | + +### asc builds upload + +Upload an IPA or PKG to App Store Connect + +``` +asc builds upload --app-id --file --build-number [--platform ] [--wait] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID | +| `--file` | yes | | Path to .ipa (iOS/tvOS/visionOS) or .pkg (macOS) file | +| `--build-number` | yes | | CFBundleVersion build number (e.g. 42) | +| `--platform` | | | Platform: ios, macos, tvos, visionos (auto-detected from file extension) | +| `--wait` | | | Poll until build processing completes | + +### asc builds archive + +Archive and export an Xcode project for App Store / TestFlight + +``` +asc builds archive --scheme [--workspace ] [--project ] [--platform ] [--configuration ] [--export-method ] [--signing-style ] [--team-id ] [--output-dir ] [--upload] [--app-id ] [--build-number ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--scheme` | yes | | Xcode scheme to archive | +| `--workspace` | | | Path to .xcworkspace (auto-detected if omitted) | +| `--project` | | | Path to .xcodeproj (auto-detected if omitted) | +| `--platform` | | | Platform: ios, macos, tvos, visionos (default: ios) | +| `--configuration` | | `Release` | Build configuration (default: Release) | +| `--export-method` | | | Export method: app-store-connect, ad-hoc, development, enterprise (default: app-store-connect) | +| `--signing-style` | | | Signing style: automatic, manual (default: automatic) | +| `--team-id` | | | Team ID for signing | +| `--output-dir` | | `.build` | Output directory for archive and export (default: .build) | +| `--upload` | | | Upload the exported IPA/PKG to App Store Connect after archiving | +| `--app-id` | | | App ID (required if --upload) | +| `--build-number` | | | Build number (required if --upload, e.g. 42) | + +### asc builds uploads list + +List build upload records for an app + +``` +asc builds uploads list --app-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID | + +### asc builds uploads get + +Get a build upload record by ID + +``` +asc builds uploads get --upload-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--upload-id` | yes | | Build upload ID | + +### asc builds uploads delete + +Delete a build upload record + +``` +asc builds uploads delete --upload-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--upload-id` | yes | | Build upload ID | + +### asc builds add-beta-group + +Add a beta group to a build for TestFlight distribution + +``` +asc builds add-beta-group --build-id --beta-group-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--build-id` | yes | | Build ID | +| `--beta-group-id` | yes | | Beta group ID | + +### asc builds remove-beta-group + +Remove a beta group from a build + +``` +asc builds remove-beta-group --build-id --beta-group-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--build-id` | yes | | Build ID | +| `--beta-group-id` | yes | | Beta group ID | + +### asc builds update-beta-notes + +Set TestFlight 'What's New' notes for a build locale + +``` +asc builds update-beta-notes --build-id --locale --notes +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--build-id` | yes | | Build ID | +| `--locale` | yes | | Locale (e.g. en-US) | +| `--notes` | yes | | What's new notes text | + +### asc builds set-encryption-compliance + +Set Apple's export-compliance answer (Info.plist ITSAppUsesNonExemptEncryption) on a build + +``` +asc builds set-encryption-compliance --build-id --uses-non-exempt-encryption +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--build-id` | yes | | Build ID | +| `--uses-non-exempt-encryption` | yes | | Whether the build uses non-exempt encryption (true\|false) | + +## asc testflight + +Manage TestFlight beta testing + +### asc testflight groups list + +List beta groups + +``` +asc testflight groups list [--app-id ] [--limit ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | | | Filter by app ID | +| `--limit` | | | Maximum number of groups to return | + +### asc testflight groups create + +Create a beta group (external by default; use --internal for an internal group) + +``` +asc testflight groups create --app-id --name [--internal] [--public-link-enabled] [--no-public-link-enabled] [--feedback-enabled] [--no-feedback-enabled] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID that owns the group | +| `--name` | yes | | Group name | +| `--internal` | | | Create an internal beta group (testers must be members of your App Store Connect team). Default: external. | +| `--public-link-enabled` | | `--no-public-link-enabled` | Enable the public TestFlight link (external groups only) | +| `--no-public-link-enabled` | | | Enable the public TestFlight link (external groups only) | +| `--feedback-enabled` | | `--no-feedback-enabled` | Enable tester feedback (external groups only) | +| `--no-feedback-enabled` | | | Enable tester feedback (external groups only) | + +### asc testflight testers list + +List beta testers in a group + +``` +asc testflight testers list --beta-group-id [--limit ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--beta-group-id` | yes | | Beta group ID | +| `--limit` | | | Maximum number of testers to return | + +### asc testflight testers add + +Add a beta tester to a group + +``` +asc testflight testers add --beta-group-id --email [--first-name ] [--last-name ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--beta-group-id` | yes | | Beta group ID | +| `--email` | yes | | Tester email address | +| `--first-name` | | | Tester first name | +| `--last-name` | | | Tester last name | + +### asc testflight testers remove + +Remove a beta tester from a group + +``` +asc testflight testers remove --beta-group-id --tester-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--beta-group-id` | yes | | Beta group ID | +| `--tester-id` | yes | | Tester ID to remove | + +### asc testflight testers import + +Import beta testers from a CSV file (columns: email,firstName,lastName) + +``` +asc testflight testers import --beta-group-id --file +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--beta-group-id` | yes | | Beta group ID | +| `--file` | yes | | Path to CSV file with columns: email,firstName,lastName | + +### asc testflight testers export + +Export beta testers from a group as CSV + +``` +asc testflight testers export --beta-group-id [--limit ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--beta-group-id` | yes | | Beta group ID | +| `--limit` | | | Maximum number of testers to export | + +## asc auth + +Manage App Store Connect authentication + +### asc auth check + +Verify authentication credentials and show status + +``` +asc auth check +``` + +### asc auth login + +Save API key credentials for authentication + +``` +asc auth login --key-id --issuer-id [--name ] [--private-key-path ] [--private-key ] [--vendor-number ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--key-id` | yes | | App Store Connect API Key ID | +| `--issuer-id` | yes | | App Store Connect Issuer ID | +| `--name` | | | Account name (defaults to "default"); no spaces allowed | +| `--private-key-path` | | | Path to .p8 private key file | +| `--private-key` | | | Private key PEM content (alternative to --private-key-path) | +| `--vendor-number` | | | Vendor number for financial/sales reports (found in App Store Connect → Payments and Financial Reports) | + +### asc auth logout + +Remove saved authentication credentials + +``` +asc auth logout [--name ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--name` | | | Account name to remove (defaults to active account) | + +### asc auth list + +List all saved App Store Connect accounts + +``` +asc auth list +``` + +### asc auth use + +Switch the active App Store Connect account + +``` +asc auth use +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `` | yes | | Account name to activate | + +### asc auth update + +Update an existing account (e.g. add vendor number) + +``` +asc auth update [--name ] [--vendor-number ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--name` | | | Account name (defaults to active account) | +| `--vendor-number` | | | Vendor number for financial/sales reports | + +## asc tui + +Interactive terminal UI + +``` +asc tui +``` + +## asc bundle-ids + +Manage bundle identifiers + +### asc bundle-ids list + +List bundle identifiers + +``` +asc bundle-ids list [--platform ] [--identifier ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--platform` | | | Filter by platform (ios, macos, universal) | +| `--identifier` | | | Filter by bundle identifier string (e.g. com.example.app) | + +### asc bundle-ids create + +Register a new bundle identifier + +``` +asc bundle-ids create --name --identifier --platform +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--name` | yes | | Display name for the bundle ID | +| `--identifier` | yes | | Bundle identifier string (e.g. com.example.app) | +| `--platform` | yes | | Platform: ios, macos, or universal | + +### asc bundle-ids delete + +Delete a bundle identifier + +``` +asc bundle-ids delete --bundle-id-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--bundle-id-id` | yes | | Bundle ID resource ID | + +## asc certificates + +Manage signing certificates + +### asc certificates list + +List signing certificates + +``` +asc certificates list [--type ] [--limit ] [--expired-only] [--before ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--type` | | | Filter by certificate type (e.g. IOS_DISTRIBUTION, MAC_APP_STORE) | +| `--limit` | | | Maximum number of certificates to return (server-side) | +| `--expired-only` | | | Only return certificates whose expirationDate has passed | +| `--before` | | | Only return certificates with expirationDate strictly before this date. Accepts `YYYY-MM-DD` (midnight UTC) or full ISO8601 (`2026-11-01T00:00:00Z`) | + +### asc certificates create + +Create a signing certificate from a CSR + +``` +asc certificates create --type [--csr-path ] [--csr-content ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--type` | yes | | Certificate type (e.g. IOS_DISTRIBUTION, MAC_APP_DISTRIBUTION) | +| `--csr-path` | | | Path to the .csr file — use this for PEM files (avoids shell quoting issues with dashes) | +| `--csr-content` | | | CSR content as a string — NOTE: PEM files start with '-----' which breaks shell argument parsing; use --csr-path instead | + +### asc certificates revoke + +Revoke a signing certificate + +``` +asc certificates revoke --certificate-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--certificate-id` | yes | | Certificate resource ID | + +## asc devices + +Manage registered test devices + +### asc devices list + +List registered test devices + +``` +asc devices list [--platform ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--platform` | | | Filter by platform (ios, macos) | + +### asc devices register + +Register a test device + +``` +asc devices register --name --udid --platform +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--name` | yes | | Device name | +| `--udid` | yes | | Device UDID | +| `--platform` | yes | | Platform: ios or macos | + +## asc profiles + +Manage provisioning profiles + +### asc profiles list + +List provisioning profiles + +``` +asc profiles list [--bundle-id-id ] [--type ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--bundle-id-id` | | | Filter by bundle ID resource ID | +| `--type` | | | Filter by profile type (e.g. IOS_APP_STORE, MAC_APP_STORE) | + +### asc profiles create + +Create a provisioning profile + +``` +asc profiles create --name --type --bundle-id-id --certificate-ids [--device-ids ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--name` | yes | | Profile name | +| `--type` | yes | | Profile type (e.g. IOS_APP_STORE, MAC_APP_STORE) | +| `--bundle-id-id` | yes | | Bundle ID resource ID | +| `--certificate-ids` | yes | | Comma-separated certificate resource IDs | +| `--device-ids` | | | Comma-separated device resource IDs (optional, for development profiles) | + +### asc profiles delete + +Delete a provisioning profile + +``` +asc profiles delete --profile-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--profile-id` | yes | | Profile resource ID | + +## asc app-preview-sets + +Manage App Store app preview sets + +### asc app-preview-sets list + +List app preview sets for an App Store version localization + +``` +asc app-preview-sets list --localization-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--localization-id` | yes | | App Store version localization ID | + +### asc app-preview-sets create + +Create an app preview set for a localization + +``` +asc app-preview-sets create --localization-id --preview-type +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--localization-id` | yes | | App Store version localization ID | +| `--preview-type` | yes | | Preview type raw value (e.g. IPHONE_67, IPAD_PRO_3GEN_129, APPLE_TV) | + +## asc app-previews + +Manage App Store app preview videos + +### asc app-previews list + +List app previews in a preview set + +``` +asc app-previews list --set-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--set-id` | yes | | App preview set ID | + +### asc app-previews upload + +Upload a video file to an app preview set + +``` +asc app-previews upload --set-id --file [--preview-frame-time-code ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--set-id` | yes | | App preview set ID | +| `--file` | yes | | Path to video file (.mp4, .mov, .m4v) | +| `--preview-frame-time-code` | | | Preview frame timecode (e.g. 00:00:05) | + +## asc iap + +Manage in-app purchases + +### asc iap list + +List in-app purchases for an app + +``` +asc iap list --app-id [--limit ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID to list IAPs for | +| `--limit` | | | Maximum number of results | + +### asc iap create + +Create a new in-app purchase + +``` +asc iap create --app-id --reference-name --product-id --type +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID to create the IAP for | +| `--reference-name` | yes | | Internal reference name (not displayed to users) | +| `--product-id` | yes | | Product ID (e.g. com.app.goldcoins) | +| `--type` | yes | | IAP type: consumable, non-consumable, or non-renewing-subscription | + +### asc iap update + +Update an in-app purchase (reference name, review note, family sharable) + +``` +asc iap update --iap-id [--reference-name ] [--review-note ] [--family-sharable] [--not-family-sharable] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | IAP ID | +| `--reference-name` | | | New reference name | +| `--review-note` | | | App Review note | +| `--family-sharable` | | | Mark as Family Sharable | +| `--not-family-sharable` | | | Mark as not Family Sharable | + +### asc iap delete + +Delete an in-app purchase + +``` +asc iap delete --iap-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | IAP ID | + +### asc iap submit + +Submit an in-app purchase for App Store review + +``` +asc iap submit --iap-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | IAP ID to submit for review | + +### asc iap unsubmit + +Withdraw an in-app purchase from review by deleting its submission + +``` +asc iap unsubmit --submission-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--submission-id` | yes | | Submission ID | + +### asc iap price-points list + +List available price points for an in-app purchase + +``` +asc iap price-points list --iap-id [--territory ] [--limit ] [--cursor ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | IAP ID | +| `--territory` | | | Filter by territory code (e.g. USA) | +| `--limit` | | | Page size (default: ASC's ~50) | +| `--cursor` | | | Cursor from a previous response's `nextCursor` to fetch the next page | + +### asc iap prices set + +Set the price schedule for an in-app purchase + +``` +asc iap prices set --iap-id --base-territory --price-point-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | IAP ID | +| `--base-territory` | yes | | Base territory for pricing (e.g. USA) | +| `--price-point-id` | yes | | Price point ID from asc iap price-points list | + +### asc iap versions list + +List review versions of an in-app purchase (add a submittable one to a review submission) + +``` +asc iap versions list --iap-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | IAP ID | + +## asc iap-equalizations + +List Apple's auto-equalized territory prices for a given IAP price point + +### asc iap-equalizations list + +List equalized territory prices for an IAP price point + +``` +asc iap-equalizations list --price-point-id [--limit ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--price-point-id` | yes | | IAP price point ID (from `asc iap price-points list`) | +| `--limit` | | | Maximum number of entries to return (default 200) | + +## asc iap-localizations + +Manage in-app purchase localizations + +### asc iap-localizations list + +List localizations for an in-app purchase + +``` +asc iap-localizations list --iap-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | IAP ID to list localizations for | + +### asc iap-localizations create + +Create a localization for an in-app purchase + +``` +asc iap-localizations create --iap-id --locale --name [--description ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | IAP ID to create localization for | +| `--locale` | yes | | Locale code (e.g. en-US, zh-Hans) | +| `--name` | yes | | Display name shown to users | +| `--description` | | | Optional description shown to users | + +### asc iap-localizations update + +Update a localization for an in-app purchase + +``` +asc iap-localizations update --localization-id [--name ] [--description ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--localization-id` | yes | | Localization ID | +| `--name` | | | New display name | +| `--description` | | | New description | + +### asc iap-localizations delete + +Delete a localization for an in-app purchase + +``` +asc iap-localizations delete --localization-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--localization-id` | yes | | Localization ID | + +## asc iap-price-schedule + +Manage the price schedule (currently set price) for an in-app purchase + +### asc iap-price-schedule get + +Get the current price schedule for an in-app purchase + +``` +asc iap-price-schedule get --iap-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | IAP ID | + +## asc subscription-groups + +Manage subscription groups + +### asc subscription-groups list + +List subscription groups for an app + +``` +asc subscription-groups list --app-id [--limit ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID to list subscription groups for | +| `--limit` | | | Maximum number of results | + +### asc subscription-groups create + +Create a new subscription group + +``` +asc subscription-groups create --app-id --reference-name +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID to create the subscription group for | +| `--reference-name` | yes | | Internal reference name for the group | + +### asc subscription-groups update + +Update a subscription group reference name + +``` +asc subscription-groups update --group-id --reference-name +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--group-id` | yes | | Subscription group ID | +| `--reference-name` | yes | | New reference name | + +### asc subscription-groups delete + +Delete a subscription group + +``` +asc subscription-groups delete --group-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--group-id` | yes | | Subscription group ID | + +### asc subscription-groups versions list + +List review versions of a subscription group (add a submittable one to a review submission) + +``` +asc subscription-groups versions list --group-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--group-id` | yes | | Subscription group ID | + +## asc subscriptions + +Manage subscriptions + +### asc subscriptions list + +List subscriptions in a subscription group + +``` +asc subscriptions list --group-id [--limit ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--group-id` | yes | | Subscription group ID | +| `--limit` | | | Maximum number of results | + +### asc subscriptions create + +Create a new subscription in a group + +``` +asc subscriptions create --group-id --name --product-id --period [--family-sharable] [--group-level ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--group-id` | yes | | Subscription group ID | +| `--name` | yes | | Display name for the subscription | +| `--product-id` | yes | | Product ID (e.g. com.app.monthly) | +| `--period` | yes | | Billing period: ONE_WEEK, ONE_MONTH, TWO_MONTHS, THREE_MONTHS, SIX_MONTHS, ONE_YEAR | +| `--family-sharable` | | | Enable Family Sharing for this subscription | +| `--group-level` | | | Level within the group (used for upgrade/downgrade) | + +### asc subscriptions update + +Update a subscription (name, family sharable, group level, review note) + +``` +asc subscriptions update --subscription-id [--name ] [--family-sharable] [--not-family-sharable] [--group-level ] [--period ] [--review-note ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | +| `--name` | | | New display name | +| `--family-sharable` | | | Mark as Family Sharable | +| `--not-family-sharable` | | | Mark as not Family Sharable | +| `--group-level` | | | Group level for upgrade/downgrade ordering | +| `--period` | | | Billing period (ONE_WEEK, ONE_MONTH, TWO_MONTHS, THREE_MONTHS, SIX_MONTHS, ONE_YEAR) | +| `--review-note` | | | App Review note | + +### asc subscriptions delete + +Delete a subscription + +``` +asc subscriptions delete --subscription-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | + +### asc subscriptions submit + +Submit a subscription for App Store review + +``` +asc subscriptions submit --subscription-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID to submit for review | + +### asc subscriptions unsubmit + +Withdraw a subscription from review by deleting its submission + +``` +asc subscriptions unsubmit --submission-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--submission-id` | yes | | Submission ID | + +### asc subscriptions price-points list + +List available price points for a subscription + +``` +asc subscriptions price-points list --subscription-id [--territory ] [--limit ] [--cursor ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | +| `--territory` | | | Filter by territory code (e.g. USA) | +| `--limit` | | | Page size (default: ASC's ~50) | +| `--cursor` | | | Cursor from a previous response's `nextCursor` to fetch the next page | + +### asc subscriptions prices set + +Set the per-territory price for a subscription + +``` +asc subscriptions prices set --subscription-id --territory --price-point-id [--start-date ] [--preserve-current-price] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | +| `--territory` | yes | | Territory code (e.g. USA) | +| `--price-point-id` | yes | | Price point ID from `asc subscriptions price-points list` | +| `--start-date` | | | Optional start date YYYY-MM-DD | +| `--preserve-current-price` | | | Preserve current price during the transition | + +### asc subscriptions prices set-batch + +Set multiple per-territory prices in a single call (mirrors iOS `setPrices(prices:)`) + +``` +asc subscriptions prices set-batch --subscription-id [--price …] [--start-date ] [--preserve-current-price] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | +| `--price` | | | Repeatable territory=pricePointId pair, e.g. --price USA=spp-1 --price JPN=spp-2 | +| `--start-date` | | | Optional shared start date YYYY-MM-DD applied to every entry | +| `--preserve-current-price` | | | Preserve current price during the transition for every entry | + +### asc subscriptions versions list + +List review versions of a subscription (add a submittable one to a review submission) + +``` +asc subscriptions versions list --subscription-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | + +## asc subscription-localizations + +Manage subscription localizations + +### asc subscription-localizations list + +List localizations for a subscription + +``` +asc subscription-localizations list --subscription-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID to list localizations for | + +### asc subscription-localizations create + +Create a localization for a subscription + +``` +asc subscription-localizations create --subscription-id --locale --name [--description ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID to create localization for | +| `--locale` | yes | | Locale code (e.g. en-US, zh-Hans) | +| `--name` | yes | | Display name shown to users in the App Store | +| `--description` | | | Optional description shown to users | + +### asc subscription-localizations update + +Update a localization for a subscription + +``` +asc subscription-localizations update --localization-id [--name ] [--description ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--localization-id` | yes | | Localization ID | +| `--name` | | | New display name | +| `--description` | | | New description | + +### asc subscription-localizations delete + +Delete a localization for a subscription + +``` +asc subscription-localizations delete --localization-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--localization-id` | yes | | Localization ID | + +## asc subscription-group-localizations + +Manage subscription group localizations (display name and custom app name per locale) + +### asc subscription-group-localizations list + +List localizations for a subscription group + +``` +asc subscription-group-localizations list --group-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--group-id` | yes | | Subscription group ID | + +### asc subscription-group-localizations create + +Create a localization for a subscription group + +``` +asc subscription-group-localizations create --group-id --locale --name [--custom-app-name ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--group-id` | yes | | Subscription group ID | +| `--locale` | yes | | Locale code (e.g. en-US, zh-Hans) | +| `--name` | yes | | Display name shown to users | +| `--custom-app-name` | | | Optional custom app name | + +### asc subscription-group-localizations update + +Update a localization for a subscription group + +``` +asc subscription-group-localizations update --localization-id [--name ] [--custom-app-name ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--localization-id` | yes | | Localization ID | +| `--name` | | | New display name | +| `--custom-app-name` | | | New custom app name | + +### asc subscription-group-localizations delete + +Delete a subscription group localization + +``` +asc subscription-group-localizations delete --localization-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--localization-id` | yes | | Localization ID | + +## asc subscription-price-schedule + +Manage the per-territory price schedule for a subscription + +### asc subscription-price-schedule get + +Get the per-territory price schedule for a subscription + +``` +asc subscription-price-schedule get --subscription-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | + +## asc subscription-equalizations + +List Apple's auto-equalized territory prices for a given subscription price point + +### asc subscription-equalizations list + +List equalized territory prices for a subscription price point + +``` +asc subscription-equalizations list --price-point-id [--limit ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--price-point-id` | yes | | Subscription price point ID (from `asc subscriptions price-points list`) | +| `--limit` | | | Maximum number of entries to return (default 200) | + +## asc subscription-offers + +Manage subscription introductory offers + +### asc subscription-offers list + +List introductory offers for a subscription + +``` +asc subscription-offers list --subscription-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | + +### asc subscription-offers create + +Create an introductory offer for a subscription + +``` +asc subscription-offers create --subscription-id --duration --mode --periods [--territory ] [--price-point-id ] [--start-date ] [--end-date ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | +| `--duration` | yes | | Offer duration: THREE_DAYS, ONE_WEEK, TWO_WEEKS, ONE_MONTH, TWO_MONTHS, THREE_MONTHS, SIX_MONTHS, ONE_YEAR | +| `--mode` | yes | | Offer mode: PAY_AS_YOU_GO, PAY_UP_FRONT, FREE_TRIAL | +| `--periods` | yes | | Number of periods | +| `--territory` | | | Territory (e.g. USA) — optional | +| `--price-point-id` | | | Subscription price point ID — required for PAY_AS_YOU_GO and PAY_UP_FRONT | +| `--start-date` | | | Start date in YYYY-MM-DD format — optional | +| `--end-date` | | | End date in YYYY-MM-DD format — optional | + +### asc subscription-offers delete + +Delete an introductory offer + +``` +asc subscription-offers delete --offer-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-id` | yes | | Introductory offer ID | + +## asc subscription-promotional-offers + +Manage subscription promotional offers (with per-territory pricing) + +### asc subscription-promotional-offers list + +List promotional offers for a subscription + +``` +asc subscription-promotional-offers list --subscription-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | + +### asc subscription-promotional-offers create + +Create a promotional offer with per-territory pricing + +``` +asc subscription-promotional-offers create --subscription-id --name --offer-code --duration --mode --periods [--price …] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | +| `--name` | yes | | Internal name for the offer | +| `--offer-code` | yes | | Offer code (consumer-facing identifier passed to StoreKit) | +| `--duration` | yes | | THREE_DAYS, ONE_WEEK, TWO_WEEKS, ONE_MONTH, TWO_MONTHS, THREE_MONTHS, SIX_MONTHS, ONE_YEAR | +| `--mode` | yes | | FREE_TRIAL, PAY_AS_YOU_GO, PAY_UP_FRONT | +| `--periods` | yes | | Number of periods | +| `--price` | | | Per-territory price specs in TERRITORY=PRICE_POINT_ID form, repeatable (e.g. --price USA=pp-1 --price GBR=pp-2) | + +### asc subscription-promotional-offers delete + +Delete a promotional offer + +``` +asc subscription-promotional-offers delete --offer-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-id` | yes | | Promotional offer ID | + +### asc subscription-promotional-offers prices list + +List per-territory prices for a promotional offer + +``` +asc subscription-promotional-offers prices list --offer-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-id` | yes | | Promotional offer ID | + +## asc win-back-offers + +Manage win-back offers for lapsed subscribers + +### asc win-back-offers list + +List win-back offers for a subscription + +``` +asc win-back-offers list --subscription-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | + +### asc win-back-offers create + +Create a win-back offer with eligibility rules and per-territory pricing + +``` +asc win-back-offers create --subscription-id --reference-name --offer-id --duration --mode --periods --paid-months --since-min --since-max --start-date --priority [--wait-months ] [--end-date ] [--promotion-intent ] [--price …] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | +| `--reference-name` | yes | | Internal name | +| `--offer-id` | yes | | Offer ID (consumer-facing identifier) | +| `--duration` | yes | | Duration: THREE_DAYS, ONE_WEEK, TWO_WEEKS, ONE_MONTH, TWO_MONTHS, THREE_MONTHS, SIX_MONTHS, ONE_YEAR | +| `--mode` | yes | | Mode: FREE_TRIAL, PAY_AS_YOU_GO, PAY_UP_FRONT | +| `--periods` | yes | | Number of periods | +| `--paid-months` | yes | | Months the customer must have paid before they qualify | +| `--since-min` | yes | | Minimum months since last subscribed | +| `--since-max` | yes | | Maximum months since last subscribed | +| `--wait-months` | | | Optional minimum wait between offers in months | +| `--start-date` | yes | | Start date YYYY-MM-DD | +| `--end-date` | | | Optional end date YYYY-MM-DD | +| `--priority` | yes | | Priority: HIGH or NORMAL | +| `--promotion-intent` | | | Optional promotion intent: NOT_PROMOTED or USE_AUTO_GENERATED_ASSETS | +| `--price` | | | Per-territory price specs in TERRITORY=PRICE_POINT_ID form, repeatable | + +### asc win-back-offers update + +Update a win-back offer (priority, dates, eligibility) + +``` +asc win-back-offers update --offer-id [--start-date ] [--end-date ] [--priority ] [--promotion-intent ] [--paid-months ] [--since-min ] [--since-max ] [--wait-months ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-id` | yes | | Win-back offer ID | +| `--start-date` | | | New start date YYYY-MM-DD | +| `--end-date` | | | New end date YYYY-MM-DD | +| `--priority` | | | New priority: HIGH or NORMAL | +| `--promotion-intent` | | | New promotion intent: NOT_PROMOTED or USE_AUTO_GENERATED_ASSETS | +| `--paid-months` | | | Months the customer must have paid | +| `--since-min` | | | Minimum months since last subscribed | +| `--since-max` | | | Maximum months since last subscribed | +| `--wait-months` | | | Wait between offers in months | + +### asc win-back-offers delete + +Delete a win-back offer + +``` +asc win-back-offers delete --offer-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-id` | yes | | Win-back offer ID | + +### asc win-back-offers prices list + +List per-territory prices for a win-back offer + +``` +asc win-back-offers prices list --offer-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-id` | yes | | Win-back offer ID | + +## asc promoted-purchases + +Manage promoted in-app purchases (App Store product page slots) + +### asc promoted-purchases list + +List promoted purchase slots for an app + +``` +asc promoted-purchases list --app-id [--limit ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID | +| `--limit` | | | Max results | + +### asc promoted-purchases create + +Create a promoted purchase slot referencing an IAP or subscription + +``` +asc promoted-purchases create --app-id [--iap-id ] [--subscription-id ] [--visible] [--hidden] [--enabled] [--disabled] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID | +| `--iap-id` | | | IAP ID to promote (mutually exclusive with --subscription-id) | +| `--subscription-id` | | | Subscription ID to promote (mutually exclusive with --iap-id) | +| `--visible` | | | Make visible for all users | +| `--hidden` | | | Hide from all users | +| `--enabled` | | | Enable the slot | +| `--disabled` | | | Disable the slot | + +### asc promoted-purchases update + +Update a promoted purchase slot's visibility or enabled state + +``` +asc promoted-purchases update --promoted-id [--visible] [--hidden] [--enabled] [--disabled] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--promoted-id` | yes | | Promoted purchase ID | +| `--visible` | | | Make visible for all users | +| `--hidden` | | | Hide from all users | +| `--enabled` | | | Enable the slot | +| `--disabled` | | | Disable the slot | + +### asc promoted-purchases delete + +Delete a promoted purchase slot + +``` +asc promoted-purchases delete --promoted-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--promoted-id` | yes | | Promoted purchase ID | + +## asc experiments + +Manage product page optimization tests (App Store version experiments) + +### asc experiments list + +List product page optimization tests for an app + +``` +asc experiments list --app-id [--state ] [--limit ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID | +| `--state` | | | Filter by state (e.g. PREPARE_FOR_SUBMISSION, APPROVED, COMPLETED) | +| `--limit` | | | Max results | + +### asc experiments get + +Get a product page optimization test + +``` +asc experiments get --experiment-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--experiment-id` | yes | | Experiment ID | + +### asc experiments create + +Create a product page optimization test + +``` +asc experiments create --app-id --name --traffic-proportion [--platform ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--app-id` | yes | | App ID | +| `--name` | yes | | Reference name shown in App Analytics | +| `--platform` | | `ios` | Platform: ios, macos, tvos, visionos | +| `--traffic-proportion` | yes | | Percentage of users (1-100) shown a treatment instead of the original page | + +### asc experiments update + +Rename a test or change its traffic proportion + +``` +asc experiments update --experiment-id [--name ] [--traffic-proportion ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--experiment-id` | yes | | Experiment ID | +| `--name` | | | New reference name | +| `--traffic-proportion` | | | New traffic proportion (1-100) | + +### asc experiments start + +Start an approved test so it begins serving traffic + +``` +asc experiments start --experiment-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--experiment-id` | yes | | Experiment ID | + +### asc experiments stop + +Stop a running test + +``` +asc experiments stop --experiment-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--experiment-id` | yes | | Experiment ID | + +### asc experiments delete + +Delete a product page optimization test + +``` +asc experiments delete --experiment-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--experiment-id` | yes | | Experiment ID | + +## asc experiment-treatments + +Manage treatments (product page variants) of a product page optimization test + +### asc experiment-treatments list + +List treatments of a test + +``` +asc experiment-treatments list --experiment-id [--limit ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--experiment-id` | yes | | Experiment ID | +| `--limit` | | | Max results | + +### asc experiment-treatments create + +Add a treatment (up to three per test) + +``` +asc experiment-treatments create --experiment-id --name [--app-icon-name ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--experiment-id` | yes | | Experiment ID | +| `--name` | yes | | Treatment name | +| `--app-icon-name` | | | Alternate app icon asset name to test | + +### asc experiment-treatments update + +Rename a treatment or change its app icon + +``` +asc experiment-treatments update --treatment-id [--name ] [--app-icon-name ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--treatment-id` | yes | | Treatment ID | +| `--name` | | | New treatment name | +| `--app-icon-name` | | | Alternate app icon asset name | + +### asc experiment-treatments delete + +Delete a treatment + +``` +asc experiment-treatments delete --treatment-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--treatment-id` | yes | | Treatment ID | + +## asc experiment-treatment-localizations + +Manage the locales included in a treatment + +### asc experiment-treatment-localizations list + +List the locales included in a treatment + +``` +asc experiment-treatment-localizations list --treatment-id [--limit ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--treatment-id` | yes | | Treatment ID | +| `--limit` | | | Max results | + +### asc experiment-treatment-localizations create + +Include a locale in a treatment + +``` +asc experiment-treatment-localizations create --treatment-id --locale +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--treatment-id` | yes | | Treatment ID | +| `--locale` | yes | | Locale code (e.g. en-US) | + +### asc experiment-treatment-localizations delete + +Remove a locale from a treatment + +``` +asc experiment-treatment-localizations delete --localization-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--localization-id` | yes | | Treatment localization ID | + +## asc iap-review-screenshot + +Manage the App Store review screenshot for an in-app purchase + +### asc iap-review-screenshot get + +Fetch the review screenshot for an in-app purchase + +``` +asc iap-review-screenshot get --iap-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | IAP ID | + +### asc iap-review-screenshot upload + +Upload (or replace) the App Store review screenshot for an in-app purchase + +``` +asc iap-review-screenshot upload --iap-id --file +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | IAP ID | +| `--file` | yes | | Path to image file | + +### asc iap-review-screenshot delete + +Delete the review screenshot + +``` +asc iap-review-screenshot delete --screenshot-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--screenshot-id` | yes | | Review screenshot ID | + +## asc iap-images + +Manage 1024x1024 promotional images for an in-app purchase + +### asc iap-images list + +List promotional images for an in-app purchase + +``` +asc iap-images list --iap-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | IAP ID | + +### asc iap-images upload + +Upload a 1024x1024 promotional image for an in-app purchase + +``` +asc iap-images upload --iap-id --file +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | IAP ID | +| `--file` | yes | | Path to image file | + +### asc iap-images delete + +Delete a promotional image + +``` +asc iap-images delete --image-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--image-id` | yes | | Image ID | + +## asc subscription-review-screenshot + +Manage the App Store review screenshot for a subscription + +### asc subscription-review-screenshot get + +Fetch the review screenshot for a subscription + +``` +asc subscription-review-screenshot get --subscription-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | + +### asc subscription-review-screenshot upload + +Upload (or replace) the App Store review screenshot for a subscription + +``` +asc subscription-review-screenshot upload --subscription-id --file +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | +| `--file` | yes | | Path to image file | + +### asc subscription-review-screenshot delete + +Delete the subscription review screenshot + +``` +asc subscription-review-screenshot delete --screenshot-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--screenshot-id` | yes | | Review screenshot ID | + +## asc subscription-images + +Manage promotional images for a subscription + +### asc subscription-images list + +List promotional images for a subscription + +``` +asc subscription-images list --subscription-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | + +### asc subscription-images upload + +Upload a promotional image for a subscription + +``` +asc subscription-images upload --subscription-id --file +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | +| `--file` | yes | | Path to image file | + +### asc subscription-images delete + +Delete a subscription promotional image + +``` +asc subscription-images delete --image-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--image-id` | yes | | Image ID | + +## asc subscription-offer-codes + +Manage subscription offer codes + +### asc subscription-offer-codes list + +List offer codes for a subscription + +``` +asc subscription-offer-codes list --subscription-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | + +### asc subscription-offer-codes create + +Create an offer code for a subscription + +``` +asc subscription-offer-codes create --subscription-id --name --duration --mode --periods --eligibility … --offer-eligibility [--auto-renew ] [--price …] [--free-territory …] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--subscription-id` | yes | | Subscription ID | +| `--name` | yes | | Offer code name | +| `--duration` | yes | | Offer duration: THREE_DAYS, ONE_WEEK, TWO_WEEKS, ONE_MONTH, TWO_MONTHS, THREE_MONTHS, SIX_MONTHS, ONE_YEAR | +| `--mode` | yes | | Offer mode: PAY_AS_YOU_GO, PAY_UP_FRONT, FREE_TRIAL | +| `--periods` | yes | | Number of periods | +| `--eligibility` | yes | | Customer eligibility: NEW, LAPSED, WIN_BACK, PAID_SUBSCRIBER (repeatable) | +| `--offer-eligibility` | yes | | Offer eligibility: STACKABLE, INTRODUCTORY, SUBSCRIPTION_OFFER | +| `--auto-renew` | | `true` | Whether the offer auto-renews after the offer period (default: true). Set --auto-renew false for non-renewing (one-time) offers. | +| `--price` | | | Paid price for a territory in `=` form (repeatable) | +| `--free-territory` | | | Territory that should redeem the offer for free (repeatable) | + +### asc subscription-offer-codes update + +Update an offer code (activate/deactivate) + +``` +asc subscription-offer-codes update --offer-code-id --active +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-code-id` | yes | | Offer code ID | +| `--active` | yes | | Active status (true/false) | + +### asc subscription-offer-codes prices list + +List per-territory prices for a subscription offer code + +``` +asc subscription-offer-codes prices list --offer-code-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-code-id` | yes | | Offer code ID | + +## asc subscription-offer-code-custom-codes + +Manage subscription offer code custom codes + +### asc subscription-offer-code-custom-codes list + +List custom codes for a subscription offer code + +``` +asc subscription-offer-code-custom-codes list --offer-code-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-code-id` | yes | | Offer code ID | + +### asc subscription-offer-code-custom-codes create + +Create a custom code for a subscription offer code + +``` +asc subscription-offer-code-custom-codes create --offer-code-id --custom-code --number-of-codes [--expiration-date ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-code-id` | yes | | Offer code ID | +| `--custom-code` | yes | | Custom code string | +| `--number-of-codes` | yes | | Number of codes to generate | +| `--expiration-date` | | | Expiration date in YYYY-MM-DD format — optional | + +### asc subscription-offer-code-custom-codes update + +Update a custom code (activate/deactivate) + +``` +asc subscription-offer-code-custom-codes update --custom-code-id --active +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--custom-code-id` | yes | | Custom code ID | +| `--active` | yes | | Active status (true/false) | + +## asc subscription-offer-code-one-time-codes + +Manage subscription offer code one-time use codes + +### asc subscription-offer-code-one-time-codes list + +List one-time use codes for a subscription offer code + +``` +asc subscription-offer-code-one-time-codes list --offer-code-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-code-id` | yes | | Offer code ID | + +### asc subscription-offer-code-one-time-codes create + +Create one-time use codes for a subscription offer code + +``` +asc subscription-offer-code-one-time-codes create --offer-code-id --number-of-codes --expiration-date [--environment ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-code-id` | yes | | Offer code ID | +| `--number-of-codes` | yes | | Number of codes to generate | +| `--expiration-date` | yes | | Expiration date in YYYY-MM-DD format | +| `--environment` | | `PRODUCTION` | Redemption environment (production or sandbox) | + +### asc subscription-offer-code-one-time-codes update + +Update a one-time use code (activate/deactivate) + +``` +asc subscription-offer-code-one-time-codes update --one-time-code-id --active +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--one-time-code-id` | yes | | One-time code ID | +| `--active` | yes | | Active status (true/false) | + +### asc subscription-offer-code-one-time-codes values + +Fetch the redemption values (CSV) for a subscription one-time code batch + +``` +asc subscription-offer-code-one-time-codes values --one-time-code-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--one-time-code-id` | yes | | One-time code batch ID | + +## asc iap-offer-codes + +Manage in-app purchase offer codes + +### asc iap-offer-codes list + +List offer codes for an in-app purchase + +``` +asc iap-offer-codes list --iap-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | In-app purchase ID | + +### asc iap-offer-codes create + +Create an offer code for an in-app purchase + +``` +asc iap-offer-codes create --iap-id --name --eligibility … [--price …] [--free-territory …] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--iap-id` | yes | | In-app purchase ID | +| `--name` | yes | | Offer code name | +| `--eligibility` | yes | | Customer eligibility: NON_SPENDER, ACTIVE_SPENDER, CHURNED_SPENDER (repeatable) | +| `--price` | | | Paid price for a territory in `=` form (repeatable) | +| `--free-territory` | | | Territory that should redeem the offer for free (repeatable) | + +### asc iap-offer-codes update + +Update an IAP offer code (activate/deactivate) + +``` +asc iap-offer-codes update --offer-code-id --active +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-code-id` | yes | | Offer code ID | +| `--active` | yes | | Active status (true/false) | + +### asc iap-offer-codes prices list + +List per-territory prices for an IAP offer code + +``` +asc iap-offer-codes prices list --offer-code-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-code-id` | yes | | Offer code ID | + +## asc iap-offer-code-custom-codes + +Manage in-app purchase offer code custom codes + +### asc iap-offer-code-custom-codes list + +List custom codes for an IAP offer code + +``` +asc iap-offer-code-custom-codes list --offer-code-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-code-id` | yes | | Offer code ID | + +### asc iap-offer-code-custom-codes create + +Create a custom code for an IAP offer code + +``` +asc iap-offer-code-custom-codes create --offer-code-id --custom-code --number-of-codes [--expiration-date ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-code-id` | yes | | Offer code ID | +| `--custom-code` | yes | | Custom code string | +| `--number-of-codes` | yes | | Number of codes to generate | +| `--expiration-date` | | | Expiration date in YYYY-MM-DD format — optional | + +### asc iap-offer-code-custom-codes update + +Update an IAP custom code (activate/deactivate) + +``` +asc iap-offer-code-custom-codes update --custom-code-id --active +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--custom-code-id` | yes | | Custom code ID | +| `--active` | yes | | Active status (true/false) | + +## asc iap-offer-code-one-time-codes + +Manage in-app purchase offer code one-time use codes + +### asc iap-offer-code-one-time-codes list + +List one-time use codes for an IAP offer code + +``` +asc iap-offer-code-one-time-codes list --offer-code-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-code-id` | yes | | Offer code ID | + +### asc iap-offer-code-one-time-codes create + +Create one-time use codes for an IAP offer code + +``` +asc iap-offer-code-one-time-codes create --offer-code-id --number-of-codes --expiration-date [--environment ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--offer-code-id` | yes | | Offer code ID | +| `--number-of-codes` | yes | | Number of codes to generate | +| `--expiration-date` | yes | | Expiration date in YYYY-MM-DD format | +| `--environment` | | `PRODUCTION` | Redemption environment (production or sandbox) | + +### asc iap-offer-code-one-time-codes update + +Update an IAP one-time use code (activate/deactivate) + +``` +asc iap-offer-code-one-time-codes update --one-time-code-id --active +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--one-time-code-id` | yes | | One-time code ID | +| `--active` | yes | | Active status (true/false) | + +### asc iap-offer-code-one-time-codes values + +Fetch the redemption values (CSV) for an IAP one-time code batch + +``` +asc iap-offer-code-one-time-codes values --one-time-code-id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--one-time-code-id` | yes | | One-time code batch ID | + +## asc app-shots + +AI-powered App Store screenshot planning and generation + +### asc app-shots templates list + +List available screenshot templates + +``` +asc app-shots templates list [--size ] [--preview] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--size` | | | Filter by size: portrait, landscape, portrait43, square One of: portrait, portrait43, landscape, square. | +| `--preview` | | | Include self-contained HTML preview for each template | + +### asc app-shots templates get + +Get details of a specific template + +``` +asc app-shots templates get --id [--preview] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--id` | yes | | Template ID | +| `--preview` | | | Output self-contained HTML preview page | + +### asc app-shots templates apply + +Apply a template to a screenshot — returns the composed design with preview + +``` +asc app-shots templates apply --id --screenshot --headline [--subtitle ] [--tagline ] [--app-name ] [--preview ] [--image-output ] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--id` | yes | | Template ID | +| `--screenshot` | yes | | Path to screenshot file | +| `--headline` | yes | | Headline text | +| `--subtitle` | | | Subtitle text | +| `--tagline` | | | Tagline text (overrides template default) | +| `--app-name` | | `My App` | App name | +| `--preview` | | | Preview format: html (default) or image (renders to PNG) One of: html, image. | +| `--image-output` | | | Output PNG path (for --preview image) | + +### asc app-shots gallery-templates list + +List available gallery templates + +``` +asc app-shots gallery-templates list +``` + +### asc app-shots gallery-templates get + +Get details of a specific gallery template + +``` +asc app-shots gallery-templates get --id [--preview] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--id` | yes | | Gallery template ID | +| `--preview` | | | Output self-contained HTML gallery preview page | + +### asc app-shots themes list + +List available visual themes + +``` +asc app-shots themes list +``` + +### asc app-shots themes get + +Get details of a specific theme + +``` +asc app-shots themes get --id [--context] +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--id` | yes | | Theme ID | +| `--context` | | | Output the buildContext() prompt string instead of JSON | + +### asc app-shots themes design + +Generate a ThemeDesign (palette + decorations) from AI — one call, reusable + +``` +asc app-shots themes design --id +``` + +| Flag | Required | Default | Description | +|---|---|---|---| +| `--id` | yes | | Theme ID | + +### asc app-shots themes apply-design + +Apply a cached ThemeDesign to a template — deterministic, no AI + +``` +asc app-shots themes apply-design --design --template