From ce1b1edcb8f0b1794bd35eaf849ba6523457ca89 Mon Sep 17 00:00:00 2001 From: CRT Date: Wed, 21 Oct 2026 04:55:50 +0700 Subject: [PATCH] serve: desktop methods for play build / upload / share / app-id, apple build / upload / signing ensure / adhoc register + export, shots fonts / jpeg, install (dry run first; the host token answers their question) --- cli/serve.ts | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/cli/serve.ts b/cli/serve.ts index b0df36e..e08378c 100644 --- a/cli/serve.ts +++ b/cli/serve.ts @@ -63,7 +63,7 @@ export const CONSOLE_FORMS = ['apple-app-record', 'apple-age-rating', 'apple-app /** Write tools that print a structured result with --json in the dry run and the real run (console.fill: the answers). */ export const JSON_WRITES = new Set(['console_fill', 'owner_apple_submit', 'owner_apple_resubmit', 'project_edit', 'listing_set', 'art_replace']); /** Write tools whose command may ask its own question (confirm()): a verified host token answers it, as for owner actions. */ -const TOKEN_ANSWERS = new Set(['console_fill']); +const TOKEN_ANSWERS = new Set(['console_fill', 'play_upload', 'play_share', 'apple_upload', 'apple_signing_ensure', 'apple_adhoc_register']); /** The one desktop exception to FORBIDDEN_ARGS: `scan --write-only` (scan.write, the Fix button of a privacy-labels * finding) writes the detected declarations into the config, only after the native dialog showed its diff and the @@ -143,6 +143,52 @@ export const DESKTOP_ONLY: ToolDef[] = [ description: 'Read-only: the current price of every product on both stores per region (Apple manual prices, with with_automatic also the equalized ones). Writes nothing.', props: { store: { type: 'string', enum: ['apple', 'play'] }, with_automatic: { type: 'boolean', default: false } }, args: i => [...opt('store', i.store), ...(i.with_automatic === true ? ['--with-automatic'] : [])] }, + // Build, sign and test-distribution commands (docs/desktop-coverage.md: the More tools tab). Their own question is + // answered by the host token (TOKEN_ANSWERS) after the native dialog showed the dry run. + { name: 'play_build', title: 'Build the Android app bundle', command: ['play', 'build'], write: true, network: false, + description: 'gradle bundleRelease (Flutter: flutter build appbundle) with the next free versionCode and the upload key from env / .storebound. The dry run shows the plan; nothing goes to a store.', + props: { flavor: { type: 'string', pattern: '^[A-Za-z0-9_]{1,60}$', description: 'Gradle product flavor' }, module: { type: 'string', pattern: '^[A-Za-z0-9_:-]{1,60}$', description: 'Gradle module (default app)' }, + version_name: { type: 'string', pattern: '^[0-9A-Za-z.+-]{1,40}$', description: 'versionName (default: from the project)' } }, + args: i => [...opt('flavor', i.flavor), ...opt('module', i.module), ...opt('version-name', i.version_name)] }, + { name: 'play_upload', title: 'Upload an AAB to a testing track', command: ['play', 'upload'], write: true, network: true, + description: 'Uploads the newest AAB of the build output to a Google Play testing track (internal, closed or open testing), versionCode checked first. Testers of that track get it; production stays an owner action.', + props: { track: { type: 'string', enum: ['internal', 'alpha', 'beta'], default: 'internal' }, status: { type: 'string', enum: ['completed', 'draft'], default: 'completed' }, + name: { type: 'string', pattern: '^[^\\x00-\\x1f]{1,50}$', description: 'Release name (default: the version)' } }, + args: i => [...opt('track', i.track), ...opt('status', i.status), ...opt('name', i.name)] }, + { name: 'play_share', title: 'Internal app sharing link', command: ['play', 'share'], write: true, network: true, + description: 'Uploads one AAB or APK to Google Play internal app sharing and prints the share link (anyone allowed to use the link can install it for 60 days).', + props: { aab: { type: 'string', pattern: '^(?![/-])(?!.*\\.\\.)[A-Za-z0-9 _./-]{1,500}\\.aab$', description: 'AAB path relative to the app folder' }, + apk: { type: 'string', pattern: '^(?![/-])(?!.*\\.\\.)[A-Za-z0-9 _./-]{1,500}\\.apk$', description: 'APK path relative to the app folder' } }, + args: i => [...opt('aab', i.aab), ...opt('apk', i.apk)] }, + { name: 'play_app_id', title: 'Play Console app id', command: ['play', 'app-id'], write: false, network: true, + description: 'Read-only: the Play Console app id (play.appId) for the package name, from the known ids, else the agent steps to find it.' }, + { name: 'apple_build', title: 'Build the iOS app', command: ['apple', 'build'], write: true, network: true, + description: 'xcodebuild archive + exportArchive with a generated ExportOptions.plist, the next build number and API key signing (Capacitor, React Native, Flutter, Unity). The dry run shows the plan; nothing goes to a store.', + props: { scheme: { type: 'string', pattern: '^[^\\x00-\\x1f]{1,100}$', description: 'Xcode scheme (default: from the config or the project)' }, configuration: { type: 'string', pattern: '^[A-Za-z0-9_ -]{1,40}$', description: 'Build configuration (default Release)' } }, + args: i => [...opt('scheme', i.scheme), ...opt('configuration', i.configuration)] }, + { name: 'apple_upload', title: 'Upload an IPA', command: ['apple', 'upload'], write: true, network: true, + description: 'Uploads the newest exported IPA to App Store Connect (API build upload, or altool), build number checked first, and waits for processing. It shows up in TestFlight; submitting stays an owner action.', + props: { via: { type: 'string', enum: ['api', 'altool'], default: 'api' }, no_wait: { type: 'boolean', default: false, description: 'Do not wait for processing' } }, + args: i => [...opt('via', i.via), ...(i.no_wait === true ? ['--no-wait'] : [])] }, + { name: 'apple_signing_ensure', title: 'Set up signing', command: ['apple', 'signing', 'ensure'], write: true, network: true, idempotent: true, + description: 'Creates what signing is missing on the Apple Developer team: bundle id capabilities, a distribution certificate (key kept in .storebound/signing) and the App Store profiles, installed for Xcode. The dry run lists the items.' }, + { name: 'apple_adhoc_register', title: 'Register test devices', command: ['apple', 'adhoc', 'register'], write: true, network: true, idempotent: true, + description: 'Registers the devices of apple.adhoc.devices (or the given UDID) on the Apple Developer team. Each device uses one of the yearly slots of its product family.', + props: { udid: { type: 'string', pattern: '^[A-Fa-f0-9-]{24,40}$', description: 'One device UDID' }, name: { type: 'string', pattern: '^[^\\x00-\\x1f]{1,50}$', description: 'Its device name' } }, + args: i => [...opt('udid', i.udid), ...opt('name', i.name)] }, + { name: 'apple_adhoc_export', title: 'Export an ad-hoc IPA', command: ['apple', 'adhoc', 'export'], write: true, network: false, + description: 'xcodebuild -exportArchive of an archive with the ad-hoc / release-testing ExportOptions.plist; writes the IPA locally.', + props: { archive: { type: 'string', pattern: '^(?!-)(?!.*\\.\\.)[^\\x00-\\x1f]{1,1000}\\.xcarchive$', description: 'The .xcarchive path' } }, required: ['archive'], args: i => opt('archive', i.archive) }, + { name: 'shots_fonts', title: 'Download font subsets', command: ['shots', 'fonts'], write: true, network: true, idempotent: true, + description: 'Downloads the font subsets the screenshot copy needs (CJK, Arabic, Cyrillic ...) into the app folder.' }, + { name: 'shots_jpeg', title: 'JPEG copies of the Play renders', command: ['shots', 'jpeg'], write: true, network: false, idempotent: true, + description: 'Writes JPEG copies (quality 92) of the Play renders next to them, for faster Play uploads.', + props: { variant: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*$', description: 'A/B variant id from shots.variants (default: the base renders)' }, all_variants: { type: 'boolean', default: false, description: 'The base renders and every variant' } }, + args: i => [...opt('variant', i.variant), ...(i.all_variants === true ? ['--variants'] : [])] }, + { name: 'agent_install', title: 'Install for an AI tool', command: ['install'], write: true, network: false, idempotent: true, + description: 'Installs the storebound skills and the MCP server for one AI tool (Claude, Codex, Cursor, Copilot, Windsurf, Gemini, OpenCode) for this app folder.', + props: { agent: { type: 'string', enum: ['claude', 'codex', 'cursor', 'copilot', 'windsurf', 'gemini', 'opencode'] }, no_mcp: { type: 'boolean', default: false }, no_skills: { type: 'boolean', default: false } }, + required: ['agent'], args: i => [...opt('agent', i.agent), ...(i.no_mcp === true ? ['--no-mcp'] : []), ...(i.no_skills === true ? ['--no-skills'] : [])] }, ]; const BUILD_NO = { type: 'string', pattern: '^\\d{1,9}(\\.\\d{1,9}){0,3}$', description: 'The build number (CFBundleVersion) the owner picked from the uploaded builds; never "the latest"' }; const PLATFORM = { type: 'string', enum: ['ios', 'macos', 'tvos', 'visionos'], description: 'App Store platform of the version (default: the one that has it; when several do, the command asks)' };