vydanne 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -16
- package/SKILL.md +50 -13
- package/bin/vydanne.mjs +46 -13
- package/package.json +2 -2
- package/src/client.mjs +36 -1
- package/src/commands/accessibility.mjs +129 -19
- package/src/commands/ageRating.mjs +1 -1
- package/src/commands/fill.mjs +11 -0
- package/src/commands/preflight.mjs +6 -0
- package/src/commands/prerelease.mjs +267 -0
- package/src/commands/previews.mjs +10 -3
- package/src/commands/reviewContact.mjs +3 -2
- package/src/config.mjs +3 -1
- package/src/crossStore.mjs +151 -0
- package/src/play/client.mjs +6 -3
- package/src/play/commands/fill.mjs +12 -5
- package/src/play/commands/preflight.mjs +5 -0
- package/src/play/commands/prerelease.mjs +2 -2
- package/src/registry.mjs +19 -9
- package/src/upload.mjs +3 -0
- package/types/index.d.ts +57 -3
package/README.md
CHANGED
|
@@ -7,7 +7,9 @@ description, keywords, screenshots… once per language, twice per store, again
|
|
|
7
7
|
box and the store quietly shows a blank page to half the world. Get a folder name wrong and the whole
|
|
8
8
|
upload fails.
|
|
9
9
|
|
|
10
|
-
vydanne does that typing for you, then **checks your work before Apple or Google does
|
|
10
|
+
vydanne does that typing for you, then **checks your work before Apple or Google does** — and puts
|
|
11
|
+
your build in front of testers on both stores. It stops at the one step that should stay a human's:
|
|
12
|
+
it never submits for review, and never ships to the public.
|
|
11
13
|
|
|
12
14
|
> *выданне* (Belarusian) — "publishing".
|
|
13
15
|
|
|
@@ -155,26 +157,58 @@ fastlane/metadata/en-US/keywords.txt
|
|
|
155
157
|
```sh
|
|
156
158
|
npx vydanne preflight # anything missing or too long?
|
|
157
159
|
npx vydanne diff # what exactly would change?
|
|
158
|
-
npx vydanne fill #
|
|
160
|
+
npx vydanne fill # DRY RUN — prints every write it would make
|
|
161
|
+
npx vydanne fill --apply # do it
|
|
159
162
|
```
|
|
160
163
|
|
|
161
164
|
Run vydanne **from your project folder** — it finds everything relative to where you are.
|
|
162
165
|
|
|
163
166
|
<br>
|
|
164
167
|
|
|
168
|
+
## `--apply`, or nothing happens
|
|
169
|
+
|
|
170
|
+
**Every command that can change a store is a dry run unless you pass `--apply`.** It reads the store,
|
|
171
|
+
prints each write it would make, and sends nothing:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
DRY RUN — 'fill' will not change App Store Connect. Add --apply to write.
|
|
175
|
+
would PATCH /v1/appStoreVersionLocalizations/ad2f… — description, keywords, whatsNew
|
|
176
|
+
…
|
|
177
|
+
DRY RUN — 40 store write(s) withheld. Re-run with --apply to perform them.
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The commands this applies to are marked `✎` in `vydanne help`: `fill`, `previews`, `age-rating`,
|
|
181
|
+
`review-contact`, `accessibility`, `prerelease`. Everything else only reads, and ignores the flag.
|
|
182
|
+
|
|
183
|
+
Two details worth knowing:
|
|
184
|
+
|
|
185
|
+
- **A dry run walks the whole plan.** It does not stop at the first locale — the count at the end is the
|
|
186
|
+
number to compare against `diff`. "Nothing to write" and "nothing happened" are different sentences, and
|
|
187
|
+
only the first one means your local files already match the store.
|
|
188
|
+
- **The two stores enforce it differently, deliberately.** Play builds the Edit and *validates it against
|
|
189
|
+
Google* for real, then discards it — so a dry run catches everything a commit would have caught. Apple
|
|
190
|
+
has no transaction to roll back, so there the block is at the HTTP layer: no `POST`/`PATCH`/`PUT`/
|
|
191
|
+
`DELETE` leaves the process at all. `prerelease` also refuses the `altool` upload, after validating the
|
|
192
|
+
archive.
|
|
193
|
+
|
|
194
|
+
> Upgrading from ≤ 0.5? The Apple half used to write immediately — `vydanne fill` now needs `--apply`.
|
|
195
|
+
> `VYDANNE_COMMIT=1` still works as an alias so existing Play scripts keep running, but prefer the flag.
|
|
196
|
+
|
|
197
|
+
<br>
|
|
198
|
+
|
|
165
199
|
## What each command does
|
|
166
200
|
|
|
167
201
|
| Command | In plain English |
|
|
168
202
|
|---|---|
|
|
169
|
-
| `preflight` | **Run this first.** Checks the listing is complete
|
|
203
|
+
| `preflight` | **Run this first.** Checks the listing is complete, nothing is over a character limit, and no locale mentions the other app store. Green means submittable. |
|
|
170
204
|
| `diff` | Shows exactly what's different between your files and what's live. Nothing is changed — a safe preview. |
|
|
171
|
-
| `fill` | Uploads your listing text and screenshots. Handles iPhone, iPad and Mac. |
|
|
205
|
+
| `fill` | Uploads your listing text and screenshots. Handles iPhone, iPad and Mac. Refuses to upload text that names the other mobile platform. |
|
|
172
206
|
| `previews` | Uploads App Preview videos. |
|
|
173
207
|
| `inspect` | Shows the app's current state in the store. Read-only. |
|
|
174
208
|
| `locales` | Lists your languages and Apple's code for each — and warns about any language the App Store doesn't offer. |
|
|
175
209
|
| `age-rating` | Sets the age rating. |
|
|
176
210
|
| `review-contact` | Fills in the App Review contact details (who Apple calls if there's a problem). |
|
|
177
|
-
| `accessibility` | Saves Accessibility Nutrition Labels. Stays a draft until your app is live. |
|
|
211
|
+
| `accessibility` | Saves Accessibility Nutrition Labels from the `accessibility` block in your config. Stays a draft until your app is live. Refuses to run if you have not declared one — see below. |
|
|
178
212
|
| `privacy` | Prints the privacy answers to paste into Apple's website (Apple's privacy section has no API). |
|
|
179
213
|
| `iap` | Checks your in-app purchase text fits, and can strip transparency from an image. |
|
|
180
214
|
| `compliance` | Generates the US encryption self-classification PDF that Apple asks for. |
|
|
@@ -182,6 +216,9 @@ Run vydanne **from your project folder** — it finds everything relative to whe
|
|
|
182
216
|
|
|
183
217
|
For **Google Play**, add `--store google` to `inspect`, `diff`, `preflight`, `fill`, or `prerelease`.
|
|
184
218
|
|
|
219
|
+
`fill`, `previews`, `age-rating`, `review-contact`, `accessibility` and `prerelease` change the store, so
|
|
220
|
+
they need [`--apply`](#--apply-or-nothing-happens); without it they report and exit.
|
|
221
|
+
|
|
185
222
|
<br>
|
|
186
223
|
|
|
187
224
|
## Google Play
|
|
@@ -191,19 +228,60 @@ Add a `google` block to your config and point `PLAY_JSON_KEY_FILE` at a service-
|
|
|
191
228
|
|
|
192
229
|
```sh
|
|
193
230
|
npx vydanne preflight --store google
|
|
194
|
-
npx vydanne fill --store google
|
|
195
|
-
|
|
196
|
-
|
|
231
|
+
npx vydanne fill --store google # dry run — shows what would change
|
|
232
|
+
npx vydanne fill --store google --apply # actually do it
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### `prerelease` — the build, to testers
|
|
236
|
+
|
|
237
|
+
`fill` writes the *listing*; `prerelease` uploads the **binary** — to TestFlight on Apple, or to a
|
|
238
|
+
closed testing track on Play. Neither submits anything for review.
|
|
239
|
+
|
|
240
|
+
**Apple — TestFlight**
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
npx vydanne prerelease # validate, upload, wait for processing
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
ios: {
|
|
248
|
+
ipa: "./dist", // a file, or a directory whose NEWEST .ipa is taken
|
|
249
|
+
testFlightGroup: "Internal", // optional; INTERNAL groups only
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The App Store Connect REST API has never accepted a binary, so this is the one command that shells
|
|
254
|
+
out — to `xcrun altool`, which ships with Xcode and authenticates from the same
|
|
255
|
+
`~/.appstoreconnect/private_keys` key `vydanne auth` reports. That makes it **macOS-only**, which it
|
|
256
|
+
checks up front. The archive is validated before it is uploaded, so the common refusals (bad
|
|
257
|
+
entitlements, missing icons, a version Apple already holds) cost seconds rather than a full transfer.
|
|
258
|
+
|
|
259
|
+
Build numbers come from the archive's own `CFBundleVersion` — re-uploading one Apple already holds
|
|
260
|
+
fails loudly instead of quietly replacing a binary.
|
|
261
|
+
|
|
262
|
+
It also **points the version you are preparing at the build it just uploaded**, which is the loop
|
|
263
|
+
this command exists for:
|
|
264
|
+
|
|
265
|
+
```sh
|
|
266
|
+
npx vydanne prerelease # version 1.1 -> build 66, off to testers
|
|
267
|
+
# …find something, fix it, re-archive with a new build number…
|
|
268
|
+
npx vydanne prerelease # version 1.1: build 66 -> build 67
|
|
197
269
|
```
|
|
198
270
|
|
|
199
|
-
|
|
271
|
+
The version→build relationship holds exactly one build, so re-running re-points it and there is
|
|
272
|
+
nothing to clean up. A version that is no longer editable — `IN_REVIEW`, `READY_FOR_SALE` — is left
|
|
273
|
+
alone and said so, because re-pointing it would mean withdrawing that submission, and that is a
|
|
274
|
+
decision with reviewer-facing consequences.
|
|
275
|
+
|
|
276
|
+
**External TestFlight groups are refused.** Distributing to them requires Beta App Review, which is a
|
|
277
|
+
submission by another name; internal groups are the exact parallel of Play's `internal` track, and on
|
|
278
|
+
a paid app they are the testers who install without buying it.
|
|
200
279
|
|
|
201
|
-
|
|
202
|
-
release notes, all inside one edit transaction:
|
|
280
|
+
**Google Play — a closed track**
|
|
203
281
|
|
|
204
282
|
```sh
|
|
205
|
-
npx vydanne prerelease --store google
|
|
206
|
-
|
|
283
|
+
npx vydanne prerelease --store google # dry run
|
|
284
|
+
npx vydanne prerelease --store google --apply # publish to the track
|
|
207
285
|
```
|
|
208
286
|
|
|
209
287
|
```js
|
|
@@ -227,7 +305,7 @@ Play's 500-char cap with a warning. The versionCode comes from the bundle's own
|
|
|
227
305
|
numbering stays with the build and re-uploading a used code fails loudly instead of silently replacing a
|
|
228
306
|
binary. Overrides: `VYDANNE_AAB`, `VYDANNE_TRACK`, `VYDANNE_RELEASE_NAME`.
|
|
229
307
|
|
|
230
|
-
**Play is dry by default on purpose.** Nothing goes live until you add
|
|
308
|
+
**Play is dry by default on purpose.** Nothing goes live until you add `--apply`, so a
|
|
231
309
|
half-finished folder can never overwrite a good listing. Play also uses its **own** language codes
|
|
232
310
|
(`zh-CN`, `iw-IL`) which are *not* Apple's — `vydanne locales` and the
|
|
233
311
|
[layout guide](GETTING_STARTED.md#6-put-your-text-and-images-where-vydanne-looks) keep them straight.
|
|
@@ -261,6 +339,7 @@ don't have to learn them the hard way.
|
|
|
261
339
|
| Screenshots with transparency get rejected | Converts them to RGB |
|
|
262
340
|
| Once a version is *Ready for Review*, most tools can no longer edit it | Uses a method that still works |
|
|
263
341
|
| Character limits (30 / 30 / 100 / 170; purchases 30 / 45) | Checked before upload, not after rejection |
|
|
342
|
+
| One translation says "also on Google Play" → rejected under guideline 2.3.10 | Every locale is scanned before upload; `preflight` and `fill` both refuse |
|
|
264
343
|
| Apple's privacy section can't be reached by any API key | Prints the exact answers to paste in |
|
|
265
344
|
| Accessibility labels can't publish before launch | Saved as a draft automatically |
|
|
266
345
|
| In-app purchases need **two** different images, easily confused | Labels both slots |
|
|
@@ -271,8 +350,12 @@ don't have to learn them the hard way.
|
|
|
271
350
|
|
|
272
351
|
## What vydanne will never do
|
|
273
352
|
|
|
274
|
-
- **It never submits
|
|
275
|
-
|
|
353
|
+
- **It never submits for review, and never ships to the public.** It *will* put a build in front of
|
|
354
|
+
your testers — TestFlight internal, or a Play closed track — and point the version you are
|
|
355
|
+
preparing at it. Pressing Submit, promoting to Play production, and distributing to external
|
|
356
|
+
TestFlight (which needs Beta App Review) all stay yours. Those are refusals, not flags: there is
|
|
357
|
+
no argument combination that reaches the public.
|
|
358
|
+
- It doesn't build or sign your binary. It uploads the `.ipa` / `.aab` you already produced.
|
|
276
359
|
- It doesn't create the app record — make that in App Store Connect / Play Console first.
|
|
277
360
|
|
|
278
361
|
<br>
|
|
@@ -293,3 +376,56 @@ goes through the Google Play Developer **Edits** API.
|
|
|
293
376
|
Releasing a new version: [RELEASING.md](RELEASING.md).
|
|
294
377
|
|
|
295
378
|
MIT.
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
### One store never mentions the other
|
|
382
|
+
|
|
383
|
+
Both stores reject a listing that advertises the competing platform — Apple under App Review
|
|
384
|
+
guideline **2.3.10** ("no names, icons, or imagery of other mobile platforms"), Google under its
|
|
385
|
+
Store Listing and Promotion policy. It is an easy mistake to make and an expensive one to find: the
|
|
386
|
+
two listings come from the same source copy, so a single translator writing "auch für Android"
|
|
387
|
+
costs a review cycle, in one locale out of twenty, days later.
|
|
388
|
+
|
|
389
|
+
`preflight` and `fill` both scan the local metadata before anything is uploaded, per locale, per
|
|
390
|
+
field — store names, store URLs, and the other platform's device names, in Latin script and in the
|
|
391
|
+
localized forms (安卓, Андроид, アンドロイド, …). A store's OWN platform is never flagged: "Android"
|
|
392
|
+
belongs in a Play listing. Neither does the bare word "Play", which every game listing uses.
|
|
393
|
+
|
|
394
|
+
Ambiguous words ("apple" in a game about fruit) are reported as warnings and never block. If one
|
|
395
|
+
genuinely belongs in your copy:
|
|
396
|
+
|
|
397
|
+
```js
|
|
398
|
+
allowCrossStoreTerms: ["Apple"],
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`VYDANNE_ALLOW_CROSS_STORE=1` overrides the whole check for one run.
|
|
402
|
+
|
|
403
|
+
### Accessibility Nutrition Labels
|
|
404
|
+
|
|
405
|
+
Every other thing vydanne writes is a *fact* about your app. This one is a **claim about its
|
|
406
|
+
behaviour**, made to Apple — so the tool will not guess it for you.
|
|
407
|
+
|
|
408
|
+
```js
|
|
409
|
+
accessibility: {
|
|
410
|
+
voiceover: true,
|
|
411
|
+
voiceControl: true,
|
|
412
|
+
largerText: true,
|
|
413
|
+
sufficientContrast: true,
|
|
414
|
+
darkInterface: true,
|
|
415
|
+
differentiateWithoutColorAlone: true,
|
|
416
|
+
reducedMotion: true,
|
|
417
|
+
captions: false,
|
|
418
|
+
audioDescriptions: false,
|
|
419
|
+
},
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Every feature is stated explicitly. An omission would read as a quiet "no", which is just as
|
|
423
|
+
unverified as a quiet "yes", so a partial block is rejected along with a missing one.
|
|
424
|
+
|
|
425
|
+
Earlier versions applied one hardcoded matrix to every app, which meant an app inherited claims
|
|
426
|
+
nobody had checked against it. At least one shipped app declared Larger Text support while its
|
|
427
|
+
board glyphs scaled twice and grew off the high-contrast disc behind them. If you are upgrading,
|
|
428
|
+
audit before you declare.
|
|
429
|
+
|
|
430
|
+
Apple's platform caveats are still applied automatically: Larger Text does not exist on macOS and
|
|
431
|
+
Voice Control does not exist on watchOS, so those are sent as false whatever you declare.
|
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vydanne
|
|
3
|
-
description: Prepare an App Store Connect / Google Play submission — write AND push the localized store listing. Crafts the ASO copy (app-store name, subtitle, the 100-char keyword field, description, promo text), then fills the listing + screenshots + previews, age rating, review contact, accessibility & App Privacy labels, IAP fields, and export-compliance docs; verifies with a preflight gate and diffs local-vs-live. Native Node (ES256 JWT + fetch, no fastlane/Ruby). One vydanne.config.mjs per app. Use when writing or shipping any App Store / Play listing.
|
|
3
|
+
description: Prepare an App Store Connect / Google Play submission — write AND push the localized store listing. Crafts the ASO copy (app-store name, subtitle, the 100-char keyword field, description, promo text), then fills the listing + screenshots + previews, age rating, review contact, accessibility & App Privacy labels, IAP fields, and export-compliance docs; verifies with a preflight gate and diffs local-vs-live. Native Node (ES256 JWT + fetch, no fastlane/Ruby). One vydanne.config.mjs per app. Use when writing or shipping any App Store / Play listing. Uploads builds to testers (TestFlight internal / Play closed track) and points the prepared version at them — but never submits for review and never ships to the public.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# vydanne
|
|
@@ -14,7 +14,11 @@ user needs screenshots or a preview video produced, that's zdymak's job, not vyd
|
|
|
14
14
|
uploads files that already exist. zdymak's default output paths are exactly the paths vydanne reads for
|
|
15
15
|
Play images (below), so the two line up with no glue.
|
|
16
16
|
|
|
17
|
-
**Never submits
|
|
17
|
+
**Never submits for review; never ships to the public.** It *does* upload builds — `prerelease` sends
|
|
18
|
+
the `.ipa` to TestFlight (internal groups) or the `.aab` to a Play closed track, and points the version
|
|
19
|
+
being prepared at it. What stays human: pressing Submit, Play `production`, and external TestFlight
|
|
20
|
+
(which needs Beta App Review). Those are refusals rather than flags, so there is no argument
|
|
21
|
+
combination that reaches the public. Don't try to work around that.
|
|
18
22
|
|
|
19
23
|
## Setup
|
|
20
24
|
|
|
@@ -162,32 +166,65 @@ who it's for → honest close. Keep it scannable; lead each bullet with the payo
|
|
|
162
166
|
`previews` (App Preview videos) · `age-rating` · `review-contact` · `accessibility` (draft; publish once
|
|
163
167
|
live) · `privacy` (prints answers for the UI — the API can't reach Apple's iris host) · `iap` (validate +
|
|
164
168
|
RGB flatten) · `compliance` (US self-classification PDF) · `diff` (what differs vs live) · `preflight`
|
|
165
|
-
(completeness gate) · `inspect` · `auth` (what credentials resolved, and from where) · `locales` · `version`.
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
(completeness gate + cross-store lint) · `inspect` · `auth` (what credentials resolved, and from where) · `locales` · `version`.
|
|
170
|
+
|
|
171
|
+
**Cross-store lint.** `preflight` and `fill` refuse listing text that names the other mobile platform
|
|
172
|
+
— App Review 2.3.10 for Apple, the Store Listing and Promotion policy for Google — scanning every
|
|
173
|
+
locale and field of the LOCAL metadata before upload, including localized spellings of Android/Apple.
|
|
174
|
+
A store's own platform is never flagged, nor the bare word "Play". Ambiguous words warn instead of
|
|
175
|
+
blocking; `allowCrossStoreTerms: [...]` in the config silences a specific one, and
|
|
176
|
+
`VYDANNE_ALLOW_CROSS_STORE=1` overrides a single run. This is a rejection that surfaces days later in
|
|
177
|
+
one locale out of twenty, so it is checked where it is free to fix.
|
|
178
|
+
|
|
179
|
+
`prerelease` uploads the BUILD. On Apple it validates and uploads the `.ipa` to **TestFlight** via
|
|
180
|
+
`xcrun altool` — the one command that shells out, because the ASC REST API has never carried a binary,
|
|
181
|
+
which also makes it macOS-only. `.ipa` comes from `ios.ipa` / `VYDANNE_IPA` (a directory takes its
|
|
182
|
+
newest), the build number from the archive's own `CFBundleVersion`, and `ios.testFlightGroup` may add
|
|
183
|
+
it to an **internal** group. External groups are REFUSED — they need Beta App Review, a submission by
|
|
184
|
+
another name — and App Store review is never submitted, mirroring the Play side refusing `production`.
|
|
185
|
+
It then points the version being prepared AT that build, so the fix-and-re-upload loop is one command:
|
|
186
|
+
re-running re-points (the relationship holds one build), while a version that is `IN_REVIEW` or
|
|
187
|
+
`READY_FOR_SALE` is left alone, since re-pointing it would mean withdrawing a submission.
|
|
188
|
+
|
|
189
|
+
With `--store google` it uploads an `.aab` to a **closed testing track** with release notes, inside one
|
|
168
190
|
edit transaction. `production` is REFUSED — not flag-gated — so no argument combination ships to the
|
|
169
191
|
public; promoting the tested build stays a human's job, mirroring the Apple side never submitting. Track
|
|
170
192
|
comes from `google.track` / `VYDANNE_TRACK`, default `internal`; the bundle from `google.aab` /
|
|
171
193
|
`VYDANNE_AAB` (a directory takes its newest `.aab`). **For a PAID app use `internal`** — it's the only
|
|
172
194
|
track where testers install without buying. Notes follow supply's layout:
|
|
173
195
|
`<google.metadataDir>/<play-locale>/changelogs/<versionCode>.txt`, falling back to `default.txt`, capped
|
|
174
|
-
at Play's 500 chars. DRY by default
|
|
175
|
-
|
|
196
|
+
at Play's 500 chars. DRY by default; `--apply` publishes. The versionCode comes from the bundle itself,
|
|
197
|
+
so re-uploading one fails loudly instead of silently replacing.
|
|
176
198
|
|
|
177
199
|
`--store google` routes `inspect` · `diff` · `preflight` · `fill` · `prerelease` to the Play Developer **Edits** API
|
|
178
200
|
(OAuth2 service account; **scoped to the config's `packageName`** — a shared key can't touch another app).
|
|
179
|
-
|
|
180
|
-
|
|
201
|
+
The AAB binary and the (YouTube-URL) promo video stay outside vydanne.
|
|
202
|
+
|
|
203
|
+
## `--apply` — writes are opt-in
|
|
204
|
+
|
|
205
|
+
**Every store-mutating command is a DRY RUN without `--apply`**: `fill` · `previews` · `age-rating` ·
|
|
206
|
+
`review-contact` · `accessibility` · `prerelease` (marked `✎` in `vydanne help`). They read the store,
|
|
207
|
+
print each write they would make, and send nothing. Read-only commands ignore the flag.
|
|
208
|
+
|
|
209
|
+
Never reach for `--apply` to "check whether it works" — the dry run IS the check, and it walks the whole
|
|
210
|
+
plan rather than stopping at the first locale. Its closing count is what you compare against `diff`.
|
|
211
|
+
|
|
212
|
+
Enforcement differs per store, on purpose: **Play** builds the Edit and validates it against Google for
|
|
213
|
+
real, then discards it (nothing is live until commit). **Apple** has no transaction, so the gate is at the
|
|
214
|
+
HTTP layer in `src/client.mjs` — no `POST`/`PATCH`/`PUT`/`DELETE` leaves the process, and each is recorded
|
|
215
|
+
in `client.planned`. `prerelease` needs its own guard because the `altool` binary upload does not go
|
|
216
|
+
through that client. **A new command that touches the store must be marked `writes: true` in
|
|
217
|
+
`src/registry.mjs`** — that flag is the whole opt-in, not a label.
|
|
181
218
|
|
|
182
219
|
**Env toggles:** `VYDANNE_CONFIG` · `VYDANNE_SKIP_METADATA` / `VYDANNE_SKIP_SCREENSHOTS` (fill) ·
|
|
183
|
-
`
|
|
184
|
-
`
|
|
220
|
+
`VYDANNE_REPLACE` (previews) · `VYDANNE_FLATTEN=<png>` (iap) · `VYDANNE_A11Y_PUBLISH` (accessibility) ·
|
|
221
|
+
`VYDANNE_COMMIT=1` (legacy alias for `--apply`; prefer the flag).
|
|
185
222
|
|
|
186
223
|
## Flow
|
|
187
224
|
|
|
188
225
|
config → **write the English master listing (ASO, research-grounded)** → `preflight` (char limits) → fan
|
|
189
|
-
out one copywriter agent per locale → media from zdymak → `fill` + `previews` + declarations
|
|
190
|
-
|
|
226
|
+
out one copywriter agent per locale → media from zdymak → `fill` + `previews` + declarations (read the
|
|
227
|
+
dry run, then re-run with `--apply`) → `diff` → `preflight` (must be green) → **a human submits**.
|
|
191
228
|
|
|
192
229
|
## Gotchas it encodes (don't re-derive)
|
|
193
230
|
|
package/bin/vydanne.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { loadConfig } from "../src/config.mjs";
|
|
6
6
|
import { Client } from "../src/client.mjs";
|
|
7
7
|
import { COMMANDS, PLAY_COMMANDS } from "../src/registry.mjs";
|
|
8
|
+
import { yellow } from "../src/util.mjs";
|
|
8
9
|
|
|
9
10
|
const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url))).version;
|
|
10
11
|
|
|
@@ -15,6 +16,15 @@ const cfgPath = i >= 0 ? argv[i + 1] : undefined;
|
|
|
15
16
|
const si = argv.indexOf("--store");
|
|
16
17
|
const store = si >= 0 ? argv[si + 1] : "apple";
|
|
17
18
|
|
|
19
|
+
// SAFE BY DEFAULT. Every store-mutating command is a dry run unless `--apply` is passed. The Play half
|
|
20
|
+
// always worked this way (VYDANNE_COMMIT=1, enforceable because an Edit can be discarded); the Apple half
|
|
21
|
+
// did not, and wrote the moment it was invoked — including from a mistyped `vydanne fill --help`, which
|
|
22
|
+
// is not a hypothetical. One flag now means the same thing on both stores.
|
|
23
|
+
//
|
|
24
|
+
// VYDANNE_COMMIT=1 stays as an alias so the existing `play:internal` / `play:closed` scripts in the games
|
|
25
|
+
// keep working unchanged; `--apply` is what the docs teach.
|
|
26
|
+
const apply = argv.includes("--apply") || process.env.VYDANNE_COMMIT === "1";
|
|
27
|
+
|
|
18
28
|
try {
|
|
19
29
|
if (["version", "-v", "--version"].includes(cmd)) {
|
|
20
30
|
console.log(`vydanne ${VERSION}`);
|
|
@@ -53,16 +63,32 @@ try {
|
|
|
53
63
|
if (!PLAY_COMMANDS[cmd]) throw new Error(`vydanne: '${cmd}' isn't available for --store google (try: ${Object.keys(PLAY_COMMANDS).join(", ")})`);
|
|
54
64
|
if (!cfg.google.serviceAccountKey) throw new Error("vydanne: set PLAY_JSON_KEY_FILE (or google.serviceAccountKey) to the Play service-account JSON");
|
|
55
65
|
const { PlayClient } = await import("../src/play/client.mjs");
|
|
56
|
-
const
|
|
57
|
-
const
|
|
66
|
+
const spec = PLAY_COMMANDS[cmd];
|
|
67
|
+
const dryRun = Boolean(spec.writes) && !apply;
|
|
68
|
+
// Play needs no request-level gate: every mutation happens inside an Edit, and an Edit that is never
|
|
69
|
+
// committed changes nothing. So a dry run here VALIDATES for real against Google, then discards.
|
|
70
|
+
const client = await PlayClient.create({ keyPath: cfg.google.serviceAccountKey, packageName: cfg.google.packageName, dryRun });
|
|
71
|
+
if (dryRun) console.log(yellow(`DRY RUN — '${cmd} --store google' validates against Play and discards the edit. Add --apply to commit.`));
|
|
72
|
+
const { run } = await import(`../src/play/commands/${spec.mod}.mjs`);
|
|
58
73
|
const ok = await run(cfg, client);
|
|
59
74
|
if (ok === false) process.exit(1);
|
|
60
75
|
} else if (COMMANDS[cmd]) {
|
|
61
76
|
const cfg = await loadConfig(cfgPath);
|
|
62
77
|
const spec = COMMANDS[cmd];
|
|
63
78
|
const { run } = await import(`../src/commands/${spec.mod}.mjs`);
|
|
64
|
-
const
|
|
65
|
-
const
|
|
79
|
+
const dryRun = Boolean(spec.writes) && !apply;
|
|
80
|
+
const client = spec.client ? new Client({ keyId: cfg.keyId, issuerId: cfg.issuerId, dryRun }) : null;
|
|
81
|
+
if (dryRun) console.log(yellow(`DRY RUN — '${cmd}' will not change App Store Connect. Add --apply to write.`));
|
|
82
|
+
// altool authenticates on its own rather than through our JWT, so it needs the raw ids.
|
|
83
|
+
const ok = await run(cfg, client, spec.credentials ? { keyId: cfg.keyId, issuerId: cfg.issuerId } : undefined);
|
|
84
|
+
// The count is the point: "nothing happened" is not the same as "nothing would happen", and only the
|
|
85
|
+
// second one means the local state already matches the store.
|
|
86
|
+
if (dryRun && client) {
|
|
87
|
+
const n = client.planned.length;
|
|
88
|
+
console.log(yellow(n
|
|
89
|
+
? `DRY RUN — ${n} store write(s) withheld. Re-run with --apply to perform them.`
|
|
90
|
+
: "DRY RUN — nothing to write; the store already matches local."));
|
|
91
|
+
}
|
|
66
92
|
if (ok === false) process.exit(1);
|
|
67
93
|
} else {
|
|
68
94
|
console.error(usage());
|
|
@@ -74,25 +100,32 @@ try {
|
|
|
74
100
|
}
|
|
75
101
|
|
|
76
102
|
function usage() {
|
|
77
|
-
return `vydanne ${VERSION} — App Store Connect
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
103
|
+
return `vydanne ${VERSION} — App Store Connect + Play prep (companion to zdymak). Ships builds to
|
|
104
|
+
testers; never submits for review.
|
|
105
|
+
usage: vydanne <command> [--apply] [--config vydanne.config.mjs]
|
|
106
|
+
|
|
107
|
+
--apply PERFORM the writes. Without it every store-mutating command below (marked ✎) runs
|
|
108
|
+
as a DRY RUN: it reads the store, reports exactly what it would change, and sends
|
|
109
|
+
nothing. Read-only commands ignore the flag.
|
|
110
|
+
✎ fill metadata + screenshots + previews (native; iOS & macOS separate)
|
|
111
|
+
✎ age-rating set the age rating (AppInfo declaration)
|
|
112
|
+
✎ review-contact App Review contact from the gitignored files
|
|
113
|
+
✎ accessibility Accessibility Nutrition Labels (draft; VYDANNE_A11Y_PUBLISH=1 to publish once live)
|
|
83
114
|
privacy write the record + print the ASC-UI answers (API can't reach iris)
|
|
84
|
-
|
|
115
|
+
✎ previews upload App Preview videos (native chunked upload)
|
|
85
116
|
iap validate IAP fields; VYDANNE_FLATTEN=<png> flattens a screenshot to RGB
|
|
86
117
|
compliance generate the US encryption self-classification PDF
|
|
87
118
|
inspect read-only ASC state
|
|
88
119
|
diff show what differs between local (metadata/screenshots/previews) and ASC
|
|
89
120
|
preflight verify submission-completeness (the gotcha checker)
|
|
90
|
-
|
|
121
|
+
✎ prerelease upload the build for testers — .ipa to TestFlight (internal groups only),
|
|
122
|
+
or --store google: the .aab to a closed track. Refuses production/review.
|
|
91
123
|
locales UI -> ASC locale mapping + unsupported
|
|
92
124
|
auth which credentials resolved, and from where (masked) — run this on a 401
|
|
93
125
|
credentials: env > .env cascade (.env, .env.<mode>, .env.local, .env.<mode>.local) > user config
|
|
94
126
|
(\$VYDANNE_CONFIG_HOME, %APPDATA%\\vydanne or \$XDG_CONFIG_HOME/vydanne, ~/.appstoreconnect).
|
|
95
127
|
NEVER the committed vydanne.config.mjs — run \`vydanne auth\` to see what resolved.
|
|
96
128
|
toggles: VYDANNE_SKIP_METADATA / VYDANNE_SKIP_SCREENSHOTS (fill), VYDANNE_A11Y_PUBLISH (accessibility),
|
|
97
|
-
VYDANNE_PROFILE (named profile), VYDANNE_ENV (.env mode)
|
|
129
|
+
VYDANNE_PROFILE (named profile), VYDANNE_ENV (.env mode),
|
|
130
|
+
VYDANNE_COMMIT=1 (legacy alias for --apply — prefer the flag)`;
|
|
98
131
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vydanne",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "App Store Connect + Google Play submission prep — the companion to zdymak (media). Native Node (no fastlane/Ruby/Python): localized listings, screenshot/preview/icon upload, ratings, review contact, accessibility & privacy labels, IAP, export docs, a diff of local-vs-store, and a preflight verifier that encodes the store gotchas. iOS/macOS via the ASC REST API; Android via the Play Developer Edits API (--store google).",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "App Store Connect + Google Play submission prep — the companion to zdymak (media). Native Node (no fastlane/Ruby/Python): localized listings, screenshot/preview/icon upload, ratings, review contact, accessibility & privacy labels, IAP, export docs, build upload to TestFlight / a Play closed track, a diff of local-vs-store, and a preflight verifier that encodes the store gotchas. Never submits for review. iOS/macOS via the ASC REST API; Android via the Play Developer Edits API (--store google).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app-store-connect",
|
|
7
7
|
"google-play",
|
package/src/client.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { makeToken } from "./jwt.mjs";
|
|
2
|
+
import { yellow } from "./util.mjs";
|
|
2
3
|
|
|
3
4
|
const API = "https://api.appstoreconnect.apple.com";
|
|
4
5
|
const IRIS = "https://appstoreconnect.apple.com/iris";
|
|
@@ -10,15 +11,24 @@ const DEAD_VERSION = ["READY_FOR_SALE", "REMOVED_FROM_SALE", "REPLACED_WITH_NEW_
|
|
|
10
11
|
// release notes included, on any app that already has a version on sale.
|
|
11
12
|
const DEAD_INFO = ["READY_FOR_SALE", "READY_FOR_DISTRIBUTION", "REPLACED_WITH_NEW_VERSION", "REMOVED_FROM_SALE"];
|
|
12
13
|
|
|
14
|
+
// Anything that is not a read. ASC has no transaction to roll back — unlike Play, where an edit can be
|
|
15
|
+
// discarded — so for Apple the only safe place to stand between a command and a live listing is here.
|
|
16
|
+
const MUTATING = new Set(["POST", "PATCH", "PUT", "DELETE"]);
|
|
17
|
+
|
|
13
18
|
// Thin ASC REST client. Encodes the gotchas: `iris` host (App Privacy 401s the JWT), version + app-info
|
|
14
19
|
// fetched from the FULL list (get_edit filters out READY_FOR_REVIEW), and individual localization reads
|
|
15
20
|
// (list endpoints return sparse/empty text).
|
|
16
21
|
export class Client {
|
|
17
|
-
constructor({ keyId, issuerId }) {
|
|
22
|
+
constructor({ keyId, issuerId, dryRun = false }) {
|
|
18
23
|
this.token = makeToken({ keyId, issuerId });
|
|
24
|
+
/** No mutating request leaves this process. Set by bin/ for a write command without `--apply`. */
|
|
25
|
+
this.dryRun = dryRun;
|
|
26
|
+
/** What a real run WOULD have sent, in order — the dry-run report, and the count bin/ prints. */
|
|
27
|
+
this.planned = [];
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
async req(method, urlPath, { iris = false, body, rawHeaders, rawBody } = {}) {
|
|
31
|
+
if (this.dryRun && MUTATING.has(method)) return this.#plan(method, urlPath, body);
|
|
22
32
|
const headers = { Authorization: `Bearer ${this.token}` };
|
|
23
33
|
if (body) headers["Content-Type"] = "application/json";
|
|
24
34
|
Object.assign(headers, rawHeaders || {});
|
|
@@ -31,6 +41,31 @@ export class Client {
|
|
|
31
41
|
return { status: res.status, json, text };
|
|
32
42
|
}
|
|
33
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Record a mutation instead of sending it, and hand back a response shaped like the one Apple would
|
|
46
|
+
* have returned.
|
|
47
|
+
*
|
|
48
|
+
* The shape matters as much as the refusal. A dry run that returned `null` here would crash the first
|
|
49
|
+
* caller that reads `.json.data.id` — and the operator would see one locale out of twenty, which is
|
|
50
|
+
* exactly the report they cannot act on. So a synthesised `data` carries the id the caller needs to
|
|
51
|
+
* keep going, and the run walks the WHOLE plan: every locale, every screenshot set, every field.
|
|
52
|
+
*
|
|
53
|
+
* The id is deliberately `dry-run-<n>` rather than a plausible-looking one — if it ever escapes into a
|
|
54
|
+
* URL, the request 404s loudly instead of touching some real record.
|
|
55
|
+
*/
|
|
56
|
+
#plan(method, urlPath, body) {
|
|
57
|
+
const attributes = body?.data?.attributes || {};
|
|
58
|
+
const fields = Object.keys(attributes);
|
|
59
|
+
this.planned.push({ method, path: urlPath, attributes });
|
|
60
|
+
console.log(yellow(` would ${method} ${urlPath}${fields.length ? ` — ${fields.join(", ")}` : ""}`));
|
|
61
|
+
return {
|
|
62
|
+
status: method === "DELETE" ? 204 : 200,
|
|
63
|
+
json: { data: { id: body?.data?.id || `dry-run-${this.planned.length}`, type: body?.data?.type, attributes } },
|
|
64
|
+
text: "",
|
|
65
|
+
dryRun: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
34
69
|
get(p, opts) { return this.req("GET", p, opts); }
|
|
35
70
|
post(p, body) { return this.req("POST", p, { body }); }
|
|
36
71
|
patch(p, body) { return this.req("PATCH", p, { body }); }
|