apple: macOS (tvOS, visionOS) versions are no longer invisible: every version and review submission lookup is per platform; status, listing, listing pull, resubmit, submit, release and screenshots take --platform and otherwise pick the platform that has the version they need (refuse naming both when several do); status, validate, release status, the report and the desktop label versions with their platform; MCP and owner actions get platform; pitfall 77

This commit is contained in:
CRTkafa 2026-10-20 03:00:05 +07:00
parent 1736300784
commit 47dfd372fe
30 changed files with 423 additions and 127 deletions

61
cli/apple/platform.ts Normal file
View file

@ -0,0 +1,61 @@
// App Store platforms of one app record. An app can have an iOS and a macOS (tvOS, visionOS) version at the same time,
// each with its own state and its own review submission (pitfall 77): every version lookup filters by platform, and a
// command that needs one version picks the platform that has it, or asks for --platform when several do.
import type { Asc, J } from './asc';
export type AscPlatform = 'IOS' | 'MAC_OS' | 'TV_OS' | 'VISION_OS';
export const ASC_PLATFORMS: AscPlatform[] = ['IOS', 'MAC_OS', 'TV_OS', 'VISION_OS'];
const FLAG: Record<string, AscPlatform> = { ios: 'IOS', macos: 'MAC_OS', tvos: 'TV_OS', visionos: 'VISION_OS' };
const LABEL: Record<AscPlatform, string> = { IOS: 'iOS', MAC_OS: 'macOS', TV_OS: 'tvOS', VISION_OS: 'visionOS' };
/** The console's URL segment of a platform's version page. */
export const CONSOLE_SEGMENT: Record<AscPlatform, string> = { IOS: 'ios', MAC_OS: 'macos', TV_OS: 'appletvos', VISION_OS: 'visionos' };
/** --platform ios|macos|tvos|visionos (also the API names IOS, MAC_OS, ...) -> the API's platform; undefined when not
* given; throws on anything else. Pure. */
export function parsePlatform(v: string | undefined): AscPlatform | undefined {
if (v == null || v === '') return undefined;
const k = v.toLowerCase().replace(/[_\s-]/g, '');
const p = FLAG[k] ?? FLAG[k.replace(/^appletv/, 'tv')];
if (!p) throw new Error(`--platform: ios | macos | tvos | visionos (got "${v}")`);
return p;
}
/** "iOS", "macOS", ... for any platform string (unknown ones as they are). Pure. */
export const platformLabel = (p: string | undefined | null): string => LABEL[(p ?? 'IOS') as AscPlatform] ?? String(p);
export const platformOf = (v: J): AscPlatform => (v?.attributes?.platform ?? 'IOS') as AscPlatform;
export const stateOf = (v: J): string => v?.attributes?.appVersionState ?? v?.attributes?.appStoreState ?? '';
/** "0.8.7 (macOS)": a version as people read it. */
export const versionLabel = (v: J): string => `${v?.attributes?.versionString ?? '?'} (${platformLabel(platformOf(v))})`;
/** The appStoreVersions list path of one platform. */
export const versionsPath = (appId: string, platform: AscPlatform, limit = 50): string =>
`/v1/apps/${appId}/appStoreVersions?filter[platform]=${platform}&limit=${limit}`;
/**
* The platform a command works on. `flag` (--platform) wins. Else the platforms that have a version `needs` accepts:
* one -> that one; several -> an error naming them all (ask for --platform); none -> the only platform the app has
* versions on, else iOS. Pure.
*/
export function pickPlatform(versions: J[], needs: ((v: J) => boolean) | ((v: J) => boolean)[], flag: AscPlatform | undefined, what: string): AscPlatform {
if (flag) return flag;
// several tests: the first one any version passes decides (e.g. "this version string, editable", then "editable")
if (Array.isArray(needs)) {
for (const n of needs) if (versions.some(n)) return pickPlatform(versions, n, undefined, what);
return pickPlatform(versions, () => false, undefined, what);
}
const hits = [...new Set(versions.filter(needs).map(platformOf))];
if (hits.length === 1) return hits[0]!;
if (hits.length > 1) {
const which = versions.filter(needs).map(v => `${versionLabel(v)} ${stateOf(v)}`).join(', ');
throw new Error(`${what} on more than one platform (${which}): run it again with --platform ${hits.map(p => Object.keys(FLAG).find(k => FLAG[k] === p)).join(' or --platform ')}`);
}
const all = [...new Set(versions.map(platformOf))];
return all.length === 1 ? all[0]! : 'IOS';
}
/** Every App Store version of the app on every platform (only to pick the platform; the command then reads its
* platform's list with filter[platform]). */
export async function allVersions(c: Asc, appId: string): Promise<J[]> {
return c.all(`/v1/apps/${appId}/appStoreVersions?filter[platform]=${ASC_PLATFORMS.join(',')}&limit=200`);
}