Console pages as absolute deep links: cli/lib/console-links.ts builds the URL of every App Store Connect / Play Console page from apple.appId, play.developerId and play.appId (app list / developer chooser when unknown); every plain 'App Store Connect > ...' / 'Play Console > ...' direction in cli/ and templates/ now carries it, validate / scan / declarations / release plan JSON carry url, the desktop shows an Open button; tests/console-links.test.ts guards it; AGENTS rule 11, pitfall 71

This commit is contained in:
CRTkafa 2026-10-15 19:55:37 +08:00
parent f898d642d0
commit e500e88e40
50 changed files with 381 additions and 92 deletions

View file

@ -2,6 +2,7 @@
// storebound apple bundle [--name "My App"] [--dry]
// The App Store Connect app record itself can NOT be created through the API: do that in the web UI afterwards
// (Apps > + > New App, pick this bundle id), see the appstore-first-release skill.
import { consoleLink } from '../lib/console-links';
import type { Ctx } from '../lib/ctx';
import { need } from '../lib/ctx';
import { done, log } from '../lib/log';
@ -25,6 +26,6 @@ export async function run(ctx: Ctx): Promise<void> {
}
const apps = (await c.asc('GET', `/v1/apps?filter[bundleId]=${encodeURIComponent(bundleId)}`)).data;
if (apps.length) log('apple', `app record: ${apps[0].attributes.name} (Apple ID ${apps[0].id}); put it in apple.appId`);
else log('apple', 'no app record yet: App Store Connect > Apps > + > New App (UI only), then re-run `apple status`');
else log('apple', `no app record yet: ${consoleLink('asc.new-app')} (UI only), then re-run \`apple status\``);
done('apple', 'bundle done');
}

View file

@ -7,6 +7,7 @@
// with documents where required). This command lists the declarations and links the approved one to the builds; it
// never creates a declaration or claims an exemption for you (export-compliance skill).
// Not set: refuses. storebound never guesses this answer.
import { consoleLink } from '../lib/console-links';
import type { Ctx } from '../lib/ctx';
import { need } from '../lib/ctx';
import { done, log, warn } from '../lib/log';
@ -39,7 +40,7 @@ export async function run(ctx: Ctx): Promise<void> {
for (const d of decls) log('apple', `declaration ${d.id}: ${d.attributes.appEncryptionDeclarationState} exempt=${d.attributes.exempt} code=${d.attributes.codeValue ?? '-'}`);
const approved = decls.find(d => d.attributes.appEncryptionDeclarationState === 'APPROVED');
if (!approved) {
warn('apple', 'no APPROVED encryption declaration: create it in App Store Connect > App Information > App Encryption Documentation (export-compliance skill), then re-run');
warn('apple', `no APPROVED encryption declaration: create it in ${consoleLink('asc.encryption')} (export-compliance skill), then re-run`);
process.exitCode = 1;
return;
}

View file

@ -9,6 +9,7 @@
// screenshot sets, MD5-idempotent like `apple screenshots`. start / stop change what users see (stop is final, a test
// cannot be restarted) and ask first; they are not MCP tools. Google Play store listing experiments have no API: they
// stay in Play Console (store-screenshots skill). Pure planners: cli/lib/ppo.ts.
import { consoleLink } from '../lib/console-links';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import type { Ctx } from '../lib/ctx';
@ -66,7 +67,7 @@ export async function run(ctx: Ctx): Promise<void> {
log(TAG, `"${x.name}" treatment "${t.attributes.name}": ${locs.length} locale(s), ${shots} screenshot(s)`);
}
}
log(TAG, 'results (conversion per treatment) are in App Store Connect > Product Page Optimization; no API returns them');
log(TAG, `results (conversion per treatment) are in ${consoleLink('asc.ppo')}; no API returns them`);
return;
}

View file

@ -11,6 +11,7 @@
// exit codes: 0 approved, 1 rejected, 7 timeout (2 = nothing in review)
// Also compares the App ID's capabilities, apple.signing.capabilities and the iOS project's entitlements and warns,
// naming what is missing where.
import { consoleLink } from '../lib/console-links';
import type { Ctx } from '../lib/ctx';
import { confirm, need } from '../lib/ctx';
import { log, printJson, warn } from '../lib/log';
@ -37,7 +38,7 @@ async function waitReview(ctx: Ctx, c: Asc, appId: string): Promise<never> {
const outcome = classifyReviewState(st);
if (st !== last) { log('apple', `${v.attributes.versionString}: ${st}`); last = st; }
if (outcome === 'approved') { log('apple', 'approved (release it yourself, or it goes out automatically if the release type says so)'); process.exit(WAIT_EXIT.approved); }
if (outcome === 'rejected') { log('apple', 'rejected: read the message in App Store Connect > App Review, then the review-rejections skill'); process.exit(WAIT_EXIT.rejected); }
if (outcome === 'rejected') { log('apple', `rejected: read the message in ${consoleLink('asc.app-review')}, then the review-rejections skill`); process.exit(WAIT_EXIT.rejected); }
if (outcome === 'not-submitted') { log('apple', 'nothing is waiting for review'); process.exit(2); }
delay = nextPollDelay(delay);
if (Date.now() + delay > deadline) { log('apple', 'timeout, still in review'); process.exit(WAIT_EXIT.timeout); }