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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { red, yellow, green } from "./util.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Refuse to send one store a listing that talks about the other one.
|
|
7
|
+
*
|
|
8
|
+
* WHY THIS IS IN THE TOOL AND NOT IN A CHECKLIST. Both stores forbid it, both enforce it by
|
|
9
|
+
* REJECTING a submission, and it is the single easiest thing to do by accident — because the two
|
|
10
|
+
* listings are written from the same source copy, by the same person, often by a translator who was
|
|
11
|
+
* handed an English master that happened to say "also on Google Play". Apple's App Review guideline
|
|
12
|
+
* 2.3.10 is explicit ("does not include names, icons, or imagery of other mobile platforms"), and
|
|
13
|
+
* Google Play's Store Listing and Promotion policy is the mirror of it. Nobody discovers this while
|
|
14
|
+
* writing the copy; they discover it days later, from a rejection, in one locale out of twenty.
|
|
15
|
+
*
|
|
16
|
+
* So it is checked HERE, against the local files that are about to be uploaded, before anything is
|
|
17
|
+
* sent. That is the only place it can be caught for free.
|
|
18
|
+
*
|
|
19
|
+
* WHAT IT DELIBERATELY DOES NOT DO. It does not flag a store's own platform — "Android" belongs in
|
|
20
|
+
* a Play listing and "iPhone" belongs in an App Store one. And it does not flag the bare word
|
|
21
|
+
* "Play", which is a verb every game listing on earth uses; only the store's actual name.
|
|
22
|
+
*
|
|
23
|
+
* Findings come in two tiers, because the certainty differs:
|
|
24
|
+
* block — unambiguous: a store name, a store URL, a competing platform's device name.
|
|
25
|
+
* warn — a word that is usually a reference but sometimes just a word ("apple" in a game about
|
|
26
|
+
* fruit). Reported, never fatal, and silenceable per app.
|
|
27
|
+
*
|
|
28
|
+
* An app with a genuine exception sets `allowCrossStoreTerms: ["..."]` in its vydanne config.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** Terms that must not appear in a listing for [store]. Ordered longest-first so the report names
|
|
32
|
+
* the most specific match rather than a fragment of it. */
|
|
33
|
+
const FOREIGN = {
|
|
34
|
+
apple: [
|
|
35
|
+
{ term: "play.google.com", re: /play\.google\.com/i, level: "block" },
|
|
36
|
+
{ term: "Google Play", re: /google\s*play/i, level: "block" },
|
|
37
|
+
{ term: "Play Store", re: /\bplay[- ]?store\b/i, level: "block" },
|
|
38
|
+
{ term: "Play Market", re: /\bplay[- ]?market\b/i, level: "block" },
|
|
39
|
+
{ term: "Android", re: /\bandroid\b/i, level: "block" },
|
|
40
|
+
{ term: "安卓", re: /安卓/, level: "block" },
|
|
41
|
+
{ term: "アンドロイド", re: /アンドロイド/, level: "block" },
|
|
42
|
+
{ term: "안드로이드", re: /안드로이드/, level: "block" },
|
|
43
|
+
{ term: "Андроид/Андроїд", re: /андро[иї]д/i, level: "block" },
|
|
44
|
+
{ term: "أندرويد", re: /أندرويد/, level: "block" },
|
|
45
|
+
{ term: "אנדרואיד", re: /אנדרואיד/, level: "block" },
|
|
46
|
+
{ term: "एंड्रॉइड", re: /एंड्रॉ?इड/, level: "block" },
|
|
47
|
+
{ term: "APK", re: /\bapk\b/i, level: "warn" },
|
|
48
|
+
{ term: "Chromebook", re: /\bchromebook\b/i, level: "warn" },
|
|
49
|
+
],
|
|
50
|
+
google: [
|
|
51
|
+
{ term: "apps.apple.com", re: /apps\.apple\.com/i, level: "block" },
|
|
52
|
+
{ term: "App Store", re: /\bapp[- ]?store\b/i, level: "block" },
|
|
53
|
+
{ term: "TestFlight", re: /\btestflight\b/i, level: "block" },
|
|
54
|
+
{ term: "iPhone", re: /\biphone\b/i, level: "block" },
|
|
55
|
+
{ term: "iPad", re: /\bipad(os)?\b/i, level: "block" },
|
|
56
|
+
{ term: "iOS", re: /\bios\b/i, level: "block" },
|
|
57
|
+
{ term: "Apple Arcade", re: /\bapple\s+arcade\b/i, level: "block" },
|
|
58
|
+
{ term: "苹果", re: /苹果/, level: "block" },
|
|
59
|
+
{ term: "애플", re: /애플/, level: "block" },
|
|
60
|
+
{ term: "アップル", re: /アップル/, level: "block" },
|
|
61
|
+
// Sometimes a fruit, so it is reported rather than fatal.
|
|
62
|
+
{ term: "Apple", re: /\bapple\b/i, level: "warn" },
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Local text a store actually uploads. Keys are the file basenames each fill() reads. */
|
|
67
|
+
const APPLE_FILES = [
|
|
68
|
+
"name", "subtitle", "description", "keywords", "promotional_text",
|
|
69
|
+
"release_notes", "marketing_url", "support_url",
|
|
70
|
+
];
|
|
71
|
+
const PLAY_FILES = ["title", "short_description", "full_description"];
|
|
72
|
+
|
|
73
|
+
/** One short line of context so a finding can be found and fixed without opening the file blind. */
|
|
74
|
+
function excerpt(text, match) {
|
|
75
|
+
const at = text.toLowerCase().indexOf(match.toLowerCase());
|
|
76
|
+
if (at < 0) return "";
|
|
77
|
+
const from = Math.max(0, at - 28);
|
|
78
|
+
const to = Math.min(text.length, at + match.length + 28);
|
|
79
|
+
return `${from ? "…" : ""}${text.slice(from, to).replace(/\s+/g, " ")}${to < text.length ? "…" : ""}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Scan the local metadata tree for [store].
|
|
84
|
+
*
|
|
85
|
+
* @returns {{findings: Array, scanned: number}} findings carry {level, locale, field, term, excerpt}
|
|
86
|
+
*/
|
|
87
|
+
export function scanCrossStore(store, metadataDir, allow = []) {
|
|
88
|
+
const rules = FOREIGN[store].filter((r) => !allow.some((a) => a.toLowerCase() === r.term.toLowerCase()));
|
|
89
|
+
const files = store === "apple" ? APPLE_FILES : PLAY_FILES;
|
|
90
|
+
const findings = [];
|
|
91
|
+
let scanned = 0;
|
|
92
|
+
if (!metadataDir || !fs.existsSync(metadataDir)) return { findings, scanned };
|
|
93
|
+
|
|
94
|
+
for (const entry of fs.readdirSync(metadataDir, { withFileTypes: true })) {
|
|
95
|
+
if (!entry.isDirectory()) continue;
|
|
96
|
+
for (const field of files) {
|
|
97
|
+
const p = path.join(metadataDir, entry.name, `${field}.txt`);
|
|
98
|
+
if (!fs.existsSync(p)) continue;
|
|
99
|
+
const text = fs.readFileSync(p, "utf8");
|
|
100
|
+
scanned++;
|
|
101
|
+
for (const rule of rules) {
|
|
102
|
+
const m = text.match(rule.re);
|
|
103
|
+
if (!m) continue;
|
|
104
|
+
findings.push({
|
|
105
|
+
level: rule.level,
|
|
106
|
+
locale: entry.name,
|
|
107
|
+
field,
|
|
108
|
+
term: rule.term,
|
|
109
|
+
excerpt: excerpt(text, m[0]),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { findings, scanned };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The other store's human name, for the message. */
|
|
118
|
+
const OTHER = { apple: "Google Play / Android", google: "the App Store / iOS" };
|
|
119
|
+
const GUIDELINE = {
|
|
120
|
+
apple: "App Review guideline 2.3.10 — metadata must not name other mobile platforms.",
|
|
121
|
+
google: "Google Play Store Listing and Promotion policy — no references to other app stores.",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Report findings. Returns false when something BLOCKING was found, so callers can refuse to upload.
|
|
126
|
+
*
|
|
127
|
+
* Printing every locale rather than the first is the point: this fails one translation at a time,
|
|
128
|
+
* and knowing it is 3 locales and not 20 is the difference between a fix and a re-translation.
|
|
129
|
+
*/
|
|
130
|
+
export function reportCrossStore(store, metadataDir, allow = []) {
|
|
131
|
+
const { findings, scanned } = scanCrossStore(store, metadataDir, allow);
|
|
132
|
+
if (!scanned) return true;
|
|
133
|
+
|
|
134
|
+
const blocking = findings.filter((f) => f.level === "block");
|
|
135
|
+
const warnings = findings.filter((f) => f.level === "warn");
|
|
136
|
+
|
|
137
|
+
if (blocking.length) {
|
|
138
|
+
console.log(red(` cross-store: ${blocking.length} reference(s) to ${OTHER[store]} in listing text`));
|
|
139
|
+
console.log(` ${GUIDELINE[store]}`);
|
|
140
|
+
for (const f of blocking) {
|
|
141
|
+
console.log(` ${red("x")} ${f.locale}/${f.field}: "${f.term}" ${f.excerpt}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const f of warnings) {
|
|
145
|
+
console.log(` ${yellow("!")} ${f.locale}/${f.field}: "${f.term}" — check this is not a platform reference ${f.excerpt}`);
|
|
146
|
+
}
|
|
147
|
+
if (!blocking.length && !warnings.length) {
|
|
148
|
+
console.log(green(` cross-store: clean (${scanned} files)`));
|
|
149
|
+
}
|
|
150
|
+
return blocking.length === 0;
|
|
151
|
+
}
|
package/src/play/client.mjs
CHANGED
|
@@ -9,13 +9,16 @@ const UPLOAD = "https://androidpublisher.googleapis.com/upload/androidpublisher/
|
|
|
9
9
|
// transaction — insert an edit, mutate listings/images/details against it, then commit (all-or-nothing).
|
|
10
10
|
// Nothing is live until commit; a dropped edit changes nothing. Image bytes go to the /upload endpoint.
|
|
11
11
|
export class PlayClient {
|
|
12
|
-
static async create({ keyPath, packageName }) {
|
|
12
|
+
static async create({ keyPath, packageName, dryRun = false }) {
|
|
13
13
|
const token = await getAccessToken(keyPath);
|
|
14
|
-
return new PlayClient(token, packageName);
|
|
14
|
+
return new PlayClient(token, packageName, dryRun);
|
|
15
15
|
}
|
|
16
|
-
constructor(token, packageName) {
|
|
16
|
+
constructor(token, packageName, dryRun = false) {
|
|
17
17
|
this.token = token;
|
|
18
18
|
this.pkg = packageName;
|
|
19
|
+
// Gates the COMMIT, not the requests: the edit is still built and validated against Google for real,
|
|
20
|
+
// which is the whole advantage of Play's transaction over Apple's fire-and-forget PATCHes.
|
|
21
|
+
this.dryRun = dryRun;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
async req(method, subpath, { body, base = BASE } = {}) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { green, yellow, red } from "../../util.mjs";
|
|
4
|
+
import { reportCrossStore } from "../../crossStore.mjs";
|
|
4
5
|
|
|
5
6
|
const FIELDS = [["title", "title"], ["shortDescription", "short_description"], ["fullDescription", "full_description"]];
|
|
6
7
|
// Play image type -> local source (a dir of PNGs = screenshots; a single file = graphic). From zdymak.
|
|
@@ -16,12 +17,12 @@ const IMAGES = [
|
|
|
16
17
|
|
|
17
18
|
// Push the Play listing (text + images) inside one Edit, then validate and commit. iOS/Android are separate
|
|
18
19
|
// stores — this is the Google half. Images only touch a type whose local asset EXISTS (so a missing local
|
|
19
|
-
// set never deletes the live one).
|
|
20
|
+
// set never deletes the live one). Without `--apply` it validates and discards without committing.
|
|
20
21
|
export async function run(config, client) {
|
|
21
22
|
const g = config.google;
|
|
22
|
-
// SAFE BY DEFAULT: validate + discard the edit unless
|
|
23
|
-
//
|
|
24
|
-
const commit =
|
|
23
|
+
// SAFE BY DEFAULT: validate + discard the edit unless `--apply`. A store-mutating commit must be an
|
|
24
|
+
// explicit opt-in — never the default (a stale/partial local set could otherwise clobber a live one).
|
|
25
|
+
const commit = !client.dryRun;
|
|
25
26
|
const localLangs = fs.existsSync(g.metadataDir)
|
|
26
27
|
? fs.readdirSync(g.metadataDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
|
|
27
28
|
: [];
|
|
@@ -31,6 +32,12 @@ export async function run(config, client) {
|
|
|
31
32
|
return true;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
if (process.env.VYDANNE_ALLOW_CROSS_STORE !== "1"
|
|
36
|
+
&& !reportCrossStore("google", g.metadataDir, config.allowCrossStoreTerms)) {
|
|
37
|
+
console.error(red("fill(play): refusing to upload — fix the listing text, or set VYDANNE_ALLOW_CROSS_STORE=1."));
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
const editId = await client.newEdit();
|
|
35
42
|
try {
|
|
36
43
|
// Listing text
|
|
@@ -57,7 +64,7 @@ export async function run(config, client) {
|
|
|
57
64
|
|
|
58
65
|
const v = await client.validate(editId);
|
|
59
66
|
if (v.status >= 300) throw new Error(`validate ${v.status}: ${JSON.stringify(v.json).slice(0, 200)}`);
|
|
60
|
-
if (!commit) { await client.deleteEdit(editId); console.log(yellow("fill(play): validated — DRY (nothing changed). Review the above, then
|
|
67
|
+
if (!commit) { await client.deleteEdit(editId); console.log(yellow("fill(play): validated — DRY (nothing changed). Review the above, then re-run with --apply to commit.")); return true; }
|
|
61
68
|
const co = await client.commit(editId);
|
|
62
69
|
if (co.status >= 300) throw new Error(`commit ${co.status}: ${JSON.stringify(co.json).slice(0, 200)}`);
|
|
63
70
|
console.log(green("fill(play): committed."));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { green, red, yellow } from "../../util.mjs";
|
|
2
|
+
import { reportCrossStore } from "../../crossStore.mjs";
|
|
2
3
|
|
|
3
4
|
// Play listing limits.
|
|
4
5
|
const LIMITS = { title: 30, shortDescription: 80, fullDescription: 4000 };
|
|
@@ -30,6 +31,10 @@ export async function run(config, client) {
|
|
|
30
31
|
} finally {
|
|
31
32
|
await client.deleteEdit(editId);
|
|
32
33
|
}
|
|
34
|
+
if (!reportCrossStore("google", g.metadataDir, config.allowCrossStoreTerms)) {
|
|
35
|
+
problems.push("listing text references another app store (see above)");
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
console.log();
|
|
34
39
|
if (!problems.length) console.log(green("preflight: no blockers"));
|
|
35
40
|
else { console.log(red(`preflight: ${problems.length} blocker(s)`)); problems.forEach((p) => console.log(` ${red("x")} ${p}`)); }
|
|
@@ -79,9 +79,9 @@ export async function run(config, client) {
|
|
|
79
79
|
const put = await client.putTrack(editId, track, [release]);
|
|
80
80
|
if (put.status >= 300) throw new Error(`tracks.update ${put.status}: ${JSON.stringify(put.json).slice(0, 300)}`);
|
|
81
81
|
|
|
82
|
-
if (
|
|
82
|
+
if (client.dryRun) {
|
|
83
83
|
await client.deleteEdit(editId);
|
|
84
|
-
console.log(yellow(`\n DRY RUN — edit discarded, nothing changed. Re-run with
|
|
84
|
+
console.log(yellow(`\n DRY RUN — edit discarded, nothing changed. Re-run with --apply to publish to "${track}".`));
|
|
85
85
|
return true;
|
|
86
86
|
}
|
|
87
87
|
const res = await client.commit(editId);
|
package/src/registry.mjs
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
// The canonical command registry — the single source of vydanne's public commands. bin/ dispatches from
|
|
2
2
|
// this, and the drift guards (scripts/check-docs.mjs, scripts/check-types.mjs) assert every command is
|
|
3
3
|
// documented in README/SKILL and typed in types/index.d.ts. Add a command here → the guards force it into
|
|
4
|
-
// the docs + types before publish.
|
|
4
|
+
// the docs + types before publish.
|
|
5
|
+
//
|
|
6
|
+
// name -> { mod: <file in src/commands>, client: needs an ASC client, writes: mutates the STORE }
|
|
7
|
+
//
|
|
8
|
+
// `writes` is what makes a command dry-run unless `--apply` is passed, so it is a safety declaration, not
|
|
9
|
+
// a label: mark a new command `writes: true` the moment it can change anything on the store side. It means
|
|
10
|
+
// the STORE specifically — `privacy` and `compliance` write local files (a record, a PDF) and are not
|
|
11
|
+
// marked, because a dry run that refused to produce a local artefact would just be broken.
|
|
5
12
|
export const COMMANDS = {
|
|
6
|
-
fill: { mod: "fill", client: true },
|
|
7
|
-
"age-rating": { mod: "ageRating", client: true },
|
|
8
|
-
"review-contact": { mod: "reviewContact", client: true },
|
|
9
|
-
accessibility: { mod: "accessibility", client: true },
|
|
13
|
+
fill: { mod: "fill", client: true, writes: true },
|
|
14
|
+
"age-rating": { mod: "ageRating", client: true, writes: true },
|
|
15
|
+
"review-contact": { mod: "reviewContact", client: true, writes: true },
|
|
16
|
+
accessibility: { mod: "accessibility", client: true, writes: true },
|
|
10
17
|
privacy: { mod: "privacy", client: false },
|
|
11
|
-
previews: { mod: "previews", client: true },
|
|
18
|
+
previews: { mod: "previews", client: true, writes: true },
|
|
12
19
|
iap: { mod: "iap", client: false },
|
|
13
20
|
compliance: { mod: "compliance", client: false },
|
|
14
21
|
inspect: { mod: "inspect", client: true },
|
|
15
22
|
diff: { mod: "diff", client: true },
|
|
16
23
|
preflight: { mod: "preflight", client: true },
|
|
24
|
+
// Uploads the .ipa to TestFlight. Needs the credentials as well as the client: the REST API
|
|
25
|
+
// cannot carry a binary, so this one shells out to `xcrun altool`, which authenticates itself.
|
|
26
|
+
prerelease: { mod: "prerelease", client: true, credentials: true, writes: true },
|
|
17
27
|
};
|
|
18
28
|
|
|
19
29
|
// Commands available for `--store google` (Google Play). Same names as the Apple ones, different backend
|
|
@@ -22,9 +32,9 @@ export const PLAY_COMMANDS = {
|
|
|
22
32
|
inspect: { mod: "inspect" },
|
|
23
33
|
preflight: { mod: "preflight" },
|
|
24
34
|
diff: { mod: "diff" },
|
|
25
|
-
fill: { mod: "fill" },
|
|
26
|
-
prerelease: { mod: "prerelease" },
|
|
35
|
+
fill: { mod: "fill", writes: true },
|
|
36
|
+
prerelease: { mod: "prerelease", writes: true },
|
|
27
37
|
};
|
|
28
38
|
|
|
29
39
|
// Full public command surface (the module-dispatched ones above + the three handled inline in bin/).
|
|
30
|
-
export const COMMAND_NAMES = [...Object.keys(COMMANDS), "
|
|
40
|
+
export const COMMAND_NAMES = [...Object.keys(COMMANDS), "auth", "locales", "version"];
|
package/src/upload.mjs
CHANGED
|
@@ -33,6 +33,9 @@ export async function uploadAsset(client, { type, setType, setId, filePath }) {
|
|
|
33
33
|
|
|
34
34
|
// Previews process asynchronously — poll until Apple exposes videoUrl, then set the poster frame.
|
|
35
35
|
export async function setPreviewPoster(client, previewId, frameTimeCode, { tries = 30, delayMs = 15000 } = {}) {
|
|
36
|
+
// In a dry run the preview was never created, so `previewId` is a synthetic `dry-run-<n>` and this would
|
|
37
|
+
// poll a 404 for seven and a half minutes before giving up.
|
|
38
|
+
if (client.dryRun) return true;
|
|
36
39
|
for (let i = 0; i < tries; i++) {
|
|
37
40
|
const { json } = await client.get(`/v1/appPreviews/${previewId}`);
|
|
38
41
|
if (json.data?.attributes?.videoUrl) {
|
package/types/index.d.ts
CHANGED
|
@@ -16,7 +16,10 @@ export type CommandName =
|
|
|
16
16
|
| 'inspect'
|
|
17
17
|
| 'diff'
|
|
18
18
|
| 'preflight'
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Upload the build for testers: an .ipa to TestFlight (internal groups only), or with
|
|
21
|
+
* `--store google` an .aab to a closed track. Refuses App Store review and `production`.
|
|
22
|
+
*/
|
|
20
23
|
| 'prerelease'
|
|
21
24
|
| 'auth'
|
|
22
25
|
| 'locales'
|
|
@@ -52,6 +55,38 @@ export interface PrivacyConfig {
|
|
|
52
55
|
tracking: boolean;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Accessibility Nutrition Labels — CLAIMS about your app's behaviour, published to Apple.
|
|
60
|
+
*
|
|
61
|
+
* Every feature is stated explicitly and none is optional: an omission would read as a quiet
|
|
62
|
+
* "no", which is exactly as unverified as a quiet "yes". A missing block is an error rather than
|
|
63
|
+
* a default, because "nobody wrote this down" must never become "supports everything".
|
|
64
|
+
*
|
|
65
|
+
* Apple's platform caveats are applied for you — Larger Text does not exist on macOS, Voice
|
|
66
|
+
* Control does not exist on watchOS — so those are sent as false whatever you declare.
|
|
67
|
+
*/
|
|
68
|
+
export interface AccessibilityConfig {
|
|
69
|
+
voiceover: boolean;
|
|
70
|
+
voiceControl: boolean;
|
|
71
|
+
largerText: boolean;
|
|
72
|
+
sufficientContrast: boolean;
|
|
73
|
+
darkInterface: boolean;
|
|
74
|
+
differentiateWithoutColorAlone: boolean;
|
|
75
|
+
reducedMotion: boolean;
|
|
76
|
+
captions: boolean;
|
|
77
|
+
audioDescriptions: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface IosConfig {
|
|
81
|
+
/** `.ipa` for `prerelease` — a file, or a directory whose NEWEST .ipa is taken. Override: VYDANNE_IPA. */
|
|
82
|
+
ipa?: string;
|
|
83
|
+
/**
|
|
84
|
+
* INTERNAL TestFlight group to add the uploaded build to. External groups are refused:
|
|
85
|
+
* distributing to them requires Beta App Review, which is a submission by another name.
|
|
86
|
+
*/
|
|
87
|
+
testFlightGroup?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
55
90
|
export interface GoogleConfig {
|
|
56
91
|
/** Play package name (usually the same as the iOS bundle id). */
|
|
57
92
|
packageName: string;
|
|
@@ -98,20 +133,38 @@ export interface VydanneConfig {
|
|
|
98
133
|
iaps?: IapConfig[];
|
|
99
134
|
previews?: PreviewSpec[];
|
|
100
135
|
export?: ExportConfig;
|
|
136
|
+
/** What the app actually supports, for the Accessibility Nutrition Labels. Required by that command. */
|
|
137
|
+
accessibility?: AccessibilityConfig;
|
|
138
|
+
/** iOS build upload (`prerelease`): where the .ipa is, and which internal TestFlight group. */
|
|
139
|
+
ios?: IosConfig;
|
|
101
140
|
/** Google Play (`--store google`): listings, screenshots, feature graphic via the Edits API. */
|
|
102
141
|
google?: GoogleConfig;
|
|
142
|
+
/**
|
|
143
|
+
* Terms the cross-store check must not flag for this app.
|
|
144
|
+
*
|
|
145
|
+
* `preflight` and `fill` refuse listing text that names the OTHER mobile platform — App Review
|
|
146
|
+
* guideline 2.3.10 and Google Play's Store Listing and Promotion policy both reject it. Use this
|
|
147
|
+
* only for a word that genuinely belongs in your copy (a game about fruit really does say
|
|
148
|
+
* "apple"); it is not a way to ship a store name.
|
|
149
|
+
*/
|
|
150
|
+
allowCrossStoreTerms?: string[];
|
|
103
151
|
}
|
|
104
152
|
|
|
105
153
|
/** Thin ASC REST client (native fetch + ES256 JWT). */
|
|
106
154
|
export declare class Client {
|
|
107
|
-
constructor(opts: { keyId: string; issuerId: string });
|
|
155
|
+
constructor(opts: { keyId: string; issuerId: string; dryRun?: boolean });
|
|
108
156
|
token: string;
|
|
109
157
|
appId?: string;
|
|
110
158
|
app?: unknown;
|
|
159
|
+
/** When true, no POST/PATCH/PUT/DELETE leaves the process — each is recorded in `planned` instead. */
|
|
160
|
+
dryRun: boolean;
|
|
161
|
+
/** The mutations a real run would have sent, in order. Populated only while `dryRun`. */
|
|
162
|
+
planned: Array<{ method: string; path: string; attributes: Record<string, unknown> }>;
|
|
111
163
|
findApp(bundleId: string): Promise<unknown>;
|
|
112
164
|
get(path: string, opts?: { iris?: boolean }): Promise<{ status: number; json: any }>;
|
|
113
165
|
post(path: string, body: unknown): Promise<{ status: number; json: any }>;
|
|
114
166
|
patch(path: string, body: unknown): Promise<{ status: number; json: any }>;
|
|
167
|
+
del(path: string): Promise<{ status: number; json: any }>;
|
|
115
168
|
editVersion(platform: Platform): Promise<any>;
|
|
116
169
|
appInfo(): Promise<any>;
|
|
117
170
|
versionLocalizations(versionId: string): Promise<any[]>;
|
|
@@ -125,6 +178,7 @@ export declare function toAsc(code: string): string | null;
|
|
|
125
178
|
export declare const VALID: Set<string>;
|
|
126
179
|
export declare const CONFIG_KEYS: readonly string[];
|
|
127
180
|
export declare const COMMAND_NAMES: readonly CommandName[];
|
|
128
|
-
|
|
181
|
+
/** `writes` marks a command that mutates the STORE — those are dry-run unless the CLI gets `--apply`. */
|
|
182
|
+
export declare const COMMANDS: Record<string, { mod: string; client: boolean; writes?: boolean }>;
|
|
129
183
|
|
|
130
184
|
export default VydanneConfig;
|