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

@ -1,5 +1,6 @@
// Per-project config (storebound.config.ts at the project root). Everything app-specific lives here; credentials never
// do (see credentials.ts). The file default-exports a plain object; `import type { ShipConfig }` is optional.
import { consoleIdsFromConfig, consoleLink, setConsoleIds } from './console-links';
import { existsSync } from 'node:fs';
import { dirname, isAbsolute, join, resolve } from 'node:path';
import * as rt from './rt';
@ -1074,7 +1075,7 @@ export function validateConfig(c: unknown): string[] {
e.push('play.video: a plain YouTube video URL (https://www.youtube.com/watch?v=... or https://youtu.be/...), no playlist, channel or extra parameters');
e.push(...validatePlayBuild(cfg.play?.build), ...validateOriginality(cfg.originality));
if (cfg.play?.reportsBucket && !/^(gs:\/\/)?pubsite_prod(_rev)?_\d+\/?$/.test(cfg.play.reportsBucket))
e.push('play.reportsBucket: the Play reports bucket, e.g. "pubsite_prod_rev_<number>" (Play Console > Download reports > Copy Cloud Storage URI)');
e.push(`play.reportsBucket: the Play reports bucket, e.g. "pubsite_prod_rev_<number>" (${consoleLink('play.reports-statistics', '> Copy Cloud Storage URI')})`);
if (cfg.play?.targetAges !== undefined && !(Array.isArray(cfg.play.targetAges) && cfg.play.targetAges.length && cfg.play.targetAges.every(a => PLAY_AGE_GROUPS.includes(a))))
e.push(`play.targetAges: a list of ${PLAY_AGE_GROUPS.join(' | ')}`);
if (cfg.play?.category !== undefined) {
@ -1096,7 +1097,7 @@ export function validateConfig(c: unknown): string[] {
const an = cfg.apple.name;
if (an !== undefined && (typeof an !== 'string' || !an.trim())) e.push('apple.name: the App Store name (text), or leave it out to use app.name');
else if (an !== undefined && [...an].length > APPLE_NAME_MAX) e.push(`apple.name: ${[...an].length} characters > ${APPLE_NAME_MAX} (App Store limit)`);
if (cfg.apple.appId && !/^\d+$/.test(cfg.apple.appId)) e.push('apple.appId: numeric Apple ID of the app (App Store Connect > App Information)');
if (cfg.apple.appId && !/^\d+$/.test(cfg.apple.appId)) e.push(`apple.appId: numeric Apple ID of the app (${consoleLink('asc.app-info')})`);
const enc = cfg.apple.usesNonExemptEncryption;
if (enc !== undefined && typeof enc !== 'boolean') e.push('apple.usesNonExemptEncryption: true | false');
if (cfg.apple.screenshotTypes && !cfg.apple.screenshotTypes.every(t => /^APP_[A-Z0-9_]+$/.test(t))) e.push('apple.screenshotTypes: display types like APP_IPHONE_67');
@ -1230,6 +1231,7 @@ export async function loadConfig(explicit?: string, start = process.cwd()): Prom
if (errors.length) throw new Error(`${path}:\n ` + errors.join('\n '));
const root = dirname(path);
setConfigAccount(root, config.account);
setConsoleIds(consoleIdsFromConfig(config));
return { config, root, path, resolve: (p: string) => (isAbsolute(p) ? p : join(root, p)) };
}