games status: both stores' game services in one read-only report (CLI, MCP games_status, desktop read method); --json status output stays pure JSON

This commit is contained in:
CRTkafa 2026-10-21 05:07:58 +07:00
parent a4c31e096b
commit 26f0723641
7 changed files with 23 additions and 4 deletions

View file

@ -65,7 +65,7 @@ export async function run(ctx: Ctx): Promise<void> {
const plan = planGameCenter(g, config.languages, s);
const project = gcFindings(config, readProjectEntitlements(root));
if (a.has('json')) { console.log(JSON.stringify({ ...stateSummary(s, plan), project, plan }, null, 2)); if (sub === 'status') return; }
if (a.has('json')) { console.log(JSON.stringify({ ...stateSummary(s, plan), project, plan }, null, 2)); return; }
if (s.capability === false) warn(TAG, `the App ID ${bundleId} has no GAME_CENTER capability: storebound apple signing ensure --dry, then without --dry (games.gameCenter adds it)`);
if (s.capability === null) warn(TAG, `bundle id ${bundleId} not found in Certificates, Identifiers & Profiles: storebound apple bundle`);
for (const f of project) (f.level === 'ok' ? log : warn)(TAG, `${f.msg}${f.fix ? ` (${f.fix})` : ''}`);

View file

@ -61,6 +61,7 @@ export const COMMANDS: Record<string, [() => Promise<Mod>, string]> = {
'play rollout': [() => import('./play/rollout'), 'staged rollout: --fraction x | --halt | --resume | --complete (asks first)'],
'play share': [() => import('./play/share'), 'internal app sharing: upload an AAB / APK, print the share link (asks first; --dry)'],
'play app-id': [() => import('./play/app-id'), '[--from-text <file|->] [--write]: the Play Console app id (play.appId) by package name: known ids, else the agent steps (app list search / pages, capture) and the dry diff; --write saves it; read-only on Play'],
'games status': [() => import('./games-status'), 'Game Center + Play Games Services on both stores, read-only: App ID capability, entitlement, games_APP_ID resource, detail / version switch, leaderboard and achievement counts vs the config, console links; --json'],
'play games': [() => import('./play/games'), '[status] Play Games Services leaderboards + achievements from games (Games Configuration API upsert, matched by playId / name; drafts only, the owner publishes); --dry, asks first'],
'play next-version-code': [() => import('./play/next-version-code'), 'next free versionCode (prints the number)'],
'apple bundle': [() => import('./apple/bundle'), 'register the bundle id; finds the app record'],

View file

@ -460,6 +460,12 @@
"from",
"resume"
],
"games status": [
"achievements",
"keychain",
"leaderboards",
"no-install"
],
"health": [
"apple",
"days",

View file

@ -86,3 +86,12 @@ export function gameServicesFindings(s: GameServicesStatus): VFinding[] {
}
return out;
}
/** `storebound games status [--json]`: both stores' game services in one read-only report (desktop card, MCP). */
export async function run(ctx: import('./lib/ctx').Ctx): Promise<void> {
const s = await gameServicesStatus(ctx.loaded);
if (ctx.args.has('json')) { console.log(JSON.stringify(s, null, 2)); return; }
const { formatFindings } = await import('./lib/validate-rules');
console.log(formatFindings(gameServicesFindings(s)));
for (const l of s.links) console.log(`${l.label}: ${l.url}`);
}

View file

@ -217,6 +217,9 @@ export const TOOLS: ToolDef[] = [
{ name: 'apple_encryption', title: 'Export compliance answer', command: ['apple', 'encryption'], write: true, network: true, idempotent: true,
description: 'Answer export compliance for builds from apple.usesNonExemptEncryption in the config (never guesses).',
props: { build: { type: 'string', description: 'Build number (default: every build without an answer)' } }, args: i => opt('build', i.build) },
{ name: 'games_status', title: 'Game services status (both stores)', command: ['games', 'status'], write: false, network: true,
description: 'Read-only Game Center and Play Games Services status: App ID GAME_CENTER capability, the entitlement, Game Center detail and version switch, the games_APP_ID resource vs games.playGames.appId, leaderboard / achievement counts on each store vs the config with pending changes, findings and absolute console links.',
args: () => ['--json'] },
{ name: 'apple_game_center', title: 'Game Center sync', command: ['apple', 'game-center'], write: true, network: true, idempotent: true,
description: 'Game Center from the config\'s games section: switch it on for the app (gameCenterDetails) and the editable version (gameCenterAppVersions), upsert leaderboards and achievements with their localizations by vendor id (create / PATCH only what differs; never deletes or archives), then read back. Nothing is submitted: they go to review with the next version. The App ID capability is signing ensure\'s (terminal).',
args: () => [] },

View file

@ -72,13 +72,13 @@ export async function run(ctx: Ctx): Promise<void> {
const pgs = hasAndroidProject(root) ? readPgsProject(root) : null;
const project = pgs ? pgsFindings(config, pgs) : [];
const appId = config.games!.playGames!.appId ?? pgs?.appId?.value;
for (const f of project) (f.level === 'ok' ? log : warn)(TAG, `${f.msg}${f.fix ? ` (${f.fix})` : ''}`);
if (!a.has('json')) for (const f of project) (f.level === 'ok' ? log : warn)(TAG, `${f.msg}${f.fix ? ` (${f.fix})` : ''}`);
if (!appId) throw new Error('games.playGames.appId is not set and the Android project has no games_APP_ID: create the games project first (storebound console plan play-games-services), then set the id');
const c = gamesConfigClient(root);
const have = await readPlayGames(c, appId);
const locale = playDefaultLocale(config);
const plan = planPlayGames(config.games!, config.languages, locale, have);
if (a.has('json')) console.log(JSON.stringify({ appId, leaderboards: have.leaderboards.length, achievements: have.achievements.length, project, plan }, null, 2));
if (a.has('json')) { console.log(JSON.stringify({ appId, leaderboards: have.leaderboards.length, achievements: have.achievements.length, project, plan }, null, 2)); return; }
if (sub === 'status' || a.has('dry')) {
if (!a.has('json')) console.log(dryPlan(`play games (project ${appId})`, planRows(plan)));
return;

View file

@ -54,7 +54,7 @@ export const DESKTOP_EXCLUDED = new Set(['play_release_testing', 'apple_phased_r
export const FORBIDDEN_METHOD_WORDS = /submit|release|publish|rollout|resume|complete|send|start|stop|cancel|delete|remove|invite/i;
/** Read tools whose command prints a structured result with --json. */
export const JSON_TOOLS = new Set(['check', 'report', 'check_aso', 'aso_audit', 'aso_suggest', 'aso_competitors', 'scan', 'validate', 'health', 'health_issue', 'reviews_list', 'prices_diff', 'analytics_compare', 'apple_status',
export const JSON_TOOLS = new Set(['games_status', 'apple_game_center_status', 'play_games_status', 'check', 'report', 'check_aso', 'aso_audit', 'aso_suggest', 'aso_competitors', 'scan', 'validate', 'health', 'health_issue', 'reviews_list', 'prices_diff', 'analytics_compare', 'apple_status',
'apple_next_build', 'play_status', 'play_next_version_code', 'shots_check', 'apple_testflight_status', 'apple_ppo_status', 'catalog_diff',
'project_icon', 'project_listings', 'reviews_drafts', 'project_declarations', 'project_listing_text', 'project_renders', 'project_render', 'prices_current', 'project_editable', 'listing_live', 'art_live', 'art_thumb']);