storebound/cli/commands.ts

100 lines
17 KiB
TypeScript

// The command tables: name -> (lazy module, one-line description). index.ts dispatches from here; tools/gen-flags.ts
// reads this file to find each command's module and writes the flags it accepts to cli/data/flags.json.
import type { parseArgs } from './lib/args';
import type { Ctx } from './lib/ctx';
export type Mod = { run(ctx: Ctx): Promise<void> };
export type Plain = { run(args: ReturnType<typeof parseArgs>): Promise<void> };
/** Commands that work without a storebound.config.ts. */
export const PLAIN: Record<string, [() => Promise<Plain>, string]> = {
'init': [() => import('./init'), 'copy the templates (config, listing, copy, declarations, privacy policy) into this project; --profile app|game|subscription-app (default app)'],
'import': [() => import('./import'), 'fastlane [--dir fastlane] [--profile app|game|subscription-app] [--dry]: Appfile, deliver + supply metadata and screenshots -> config, store/*.md, shots frames (read-only on fastlane, never overwrites)'],
'install': [() => import('./install'), 'skills + MCP server for --agent claude|codex|cursor|copilot|windsurf|gemini|opencode'],
'mcp': [async () => ({ run: async () => (await import('./mcp')).serve() }), 'stdio MCP server wrapping these commands (for any MCP-capable AI tool)'],
'serve': [() => import('./serve'), '--stdio: JSON-RPC session for the storebound desktop app (dry runs; writes need a host confirm token)'],
'apps': [() => import('./apps'), 'list (every app the store keys can see: App Store Connect team, Play developer accounts; --icons) | check (key works / permission missing / revoked, with the fix); --store apple|play, --json; read-only | folders | add [<folder>] | remove <folder>: the app folders of this machine, the same list as the desktop app'],
'desktop': [() => import('./desktop'), 'open the storebound desktop app on this folder; not installed or another version: download it for this system (size, SHA-256, signature checked), install it for this user (asks once; --yes) and open it; --download only saves the installer, --dry / --json change nothing'],
'accounts': [() => import('./accounts'), 'the store accounts of this machine (credential profiles in ~/.storebound/credentials, shared with the desktop app): list | import (a project\'s store keys -> a profile; --dry first) | use <profile> | default <profile> (the machine default; --clear)'],
'doctor': [() => import('./doctor'), 'local tools for screenshots, previews, icons and captures (ffmpeg, Chrome, playwright, Xcode, adb, git) and how to install what is missing; --json'],
'scan': [() => import('./scan'), 'offline, read-only rejection-risk scan of the project itself (stack, SDKs, Info.plist, manifest, listings) with guideline, evidence file:line, fix and skill, plus the Data safety / App Privacy / ATT / privacy manifest proposal; --write puts the detected answers into the config; --json; exit 1 on blockers'],
'api': [() => import('./api'), 'every App Store Connect / Google Play API operation: list | coverage | apple|play <operationId|METHOD path> [--param k=v] [--body f.json] [--all-pages] (GET runs; writes dry unless --yes in a terminal; owner-only ones need the id typed)'],
'privacy-manifest': [() => import('./privacy-manifest'), 'iOS PrivacyInfo.xcprivacy + required-reason API + SDK manifest scan (--ios <path>)'],
'ws': [() => import('./workspace'), 'many apps and studios (storebound.workspace.json): init | list | status (apps x stores, parallel) | check | validate | run <command> --apps a,b|--all (writes dry unless --apply) | apps import --account <profile> | desktop export|import'],
'release': [() => import('./release'), 'status [--app a,b] [--versions]: both stores on one train per version (build > testing > review > live > rollout %) | plan <version>: what is left on each store, in fix order; read-only, --json'],
'flow': [() => import('./flow'), 'repo-local workflows (store/workflows/*.yml, storebound.workflows.*): list | validate | run <name> [NAME=value] [--dry plan only | --apply] [--from <step> | --resume]; writes run as dry runs unless --apply, owner steps (submit, release, rollout) are never run'],
'ads': [() => import('./ads'), 'scan --ios <path>: AdAttributionKit / SKAdNetwork checks of Info.plist (network ids, postback copy endpoints, attribution windows, cooldown) and sources; --json'],
};
export const COMMANDS: Record<string, [() => Promise<Mod>, string]> = {
'edit': [() => import('./edit'), 'edit the app folder\'s sources of truth: --ops \'<json>\' (config values by path, listing fields per store and language, the App Review contact), dry run with the diff and the checks first; --show --json lists what the desktop editors show'],
'inspect': [() => import('./inspect'), 'offline summary of the config (name, store ids, languages, products, subscriptions); --json'],
'check': [() => import('./check'), 'validate config, copy files, data safety answers, products, subscriptions and the app profile (offline); --aso keyword audit'],
'validate': [() => import('./validate'), 'read-only store readiness report with ordered blockers (--apple / --play), plus local renders checks (--no-local skips); exit 1 on blockers'],
'analytics': [() => import('./analytics'), 'setup (App Store report request) | snapshot (impressions, page views, installs, conversion -> .storebound/analytics) | compare (vs listing changes in git)'],
'health': [() => import('./health'), 'crash / ANR vitals (Play) and diagnostics (App Store), read-only; --weekly writes a markdown report; exit 1 over a threshold'],
'onboard': [() => import('./onboard'), 'an app that is already live: the live products + subscriptions with their USD / base prices, contact e-mail, website, privacy and support URLs, the App Review contact and demo account, sign-in for review and the account deletion page, from the stores, the code and the app\'s site -> config and .storebound/credentials.json (--write; read-only on the stores, never changes store prices); then the owner questions nothing proves'],
'catalog': [() => import('./catalog'), 'diff: cross-store parity of products + subscriptions (missing / extra / mismatched, prices per region, offers); read-only, exit 1 on mismatches | pull [--write]: store texts of IAPs + subscriptions as a config proposal'],
'reports': [() => import('./reports'), 'apple-sales | apple-finance --month | play-earnings --month | play-sales --month: download to .storebound/reports/, summary table + CSV; read-only'],
'triage': [() => import('./triage'), 'TestFlight crash / screenshot feedback and Play error reports grouped by signature, device, OS version, build; read-only, --json'],
'aso': [() => import('./aso'), 'rank | competitors | suggest | audit: App Store ranks, top apps per keyword, keyword field proposals (Apple Ads popularity with credentials) and one ASO report, from the public iTunes Search API -> .storebound/aso/'],
'prices': [() => import('./prices'), 'export | import a CSV of product x store x region prices (diff first, asks before writing; Apple future-dated changes) | ppp --base <USD> (purchasing power price sheet, snapped to store price points)'],
'ads apple': [() => import('./ads/apple'), 'Apple Ads (Search Ads): status | campaigns | report (spend, taps, installs, CPA per campaign / keyword, --csv); bid | budget | pause (dry run, asks first; never via MCP)'],
'web': [() => import('./web'), 'init (apple-app-site-association, assetlinks.json, Smart App Banner tag, optional landing page) | check <domain> (live files: content type, no redirects, ids match)'],
'revenuecat': [() => import('./revenuecat'), 'diff (catalog vs RevenueCat products, entitlements, default offering) | sync (dry run, asks first; never via MCP)'],
'console plan': [() => import('./console/plan'), '<form> [--json] [--all]: agent-first console mode: the owner batch first, start URL, steps, answer sheet (certain / ask owner, console labels), never-press list, UI facts and how to verify, for an AI agent that drives the browser itself; read-only'],
'console verify': [() => import('./console/verify'), '<form> [--from-text <file|->] [--json]: check a filled console form against the config (page text, input states or the App Store Connect API); read-only, exit 1 on a mismatch'],
'console fill': [() => import('./console/fill'), '<form> [--store apple|play]: optional scripted fast path: autofill in a visible Chromium you sign in to yourself; stops before Save / Submit'],
'reviews': [() => import('./reviews'), 'list | draft (local reply drafts in .storebound/replies) | send (approved drafts only, asks first)'],
'originality': [() => import('./originality'), 'Guideline 4.3 check vs your other apps (--against <folders>): listing text, name, icon and screenshot perceptual hashes, project structure; scores, flags, suggestions; offline, exit 1 on a near-duplicate'],
'listing': [() => import('./listing'), 'translate --to de-DE,ja [--from en-US] [--translate "<command>"]: listing translation drafts per language (App Store name, subtitle, promo text, keywords, description; Play title, short, full) with store limits and keyword rules, marked until reviewed | check | reviewed --lang <ids>; local files only'],
'whats-new': [() => import('./whats-new'), 'draft user-facing release notes from git log into the listing files (--since <tag>); --languages a,b: one file per language to translate | check | apply'],
'play products': [() => import('./play/products'), 'one-time products: create / update, regional prices, activate'],
'play subscriptions': [() => import('./play/subscriptions'), 'auto-renewable subscriptions: listings, base plan, regional prices, intro offer, activate (--csv prices sheet; asks first)'],
'play listing': [() => import('./play/listing'), 'app details + title / short / full description per language; pull [--write]: live listings back into the listing file (diff first)'],
'play images': [() => import('./play/images'), 'icon, feature graphic, phone screenshots per language, optional tablet / TV / Wear sets when rendered (play.tablets); --variant <id>, --prune'],
'play data-safety': [() => import('./play/data-safety'), '[--force] Data safety form (CSV import through the API; skipped when unchanged since the last upload)'],
'play release': [() => import('./play/release'), 'upload an AAB and put it on tracks with release notes'],
'play build': [() => import('./play/build'), 'gradle bundleRelease (Flutter: flutter build appbundle) with the next free versionCode and the upload key from env / .storebound; --dry'],
'play upload': [() => import('./play/upload'), 'upload an AAB to a testing track (--track internal|alpha|beta), versionCode checked first; --dry read-only, asks first'],
'play status': [() => import('./play/status'), 'tracks, listings, products (read-only)'],
'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'],
'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'],
'apple iap': [() => import('./apple/iap'), 'in-app purchases, localizations, price schedules, availability'],
'apple subscriptions': [() => import('./apple/subscriptions'), 'subscription groups, subscriptions, localizations, prices (equalized + overrides + --csv), intro offers, availability, review screenshot (asks first)'],
'apple iap-review': [() => import('./apple/iap-review'), 'IAP review screenshots + product states'],
'apple iap-submit': [() => import('./apple/iap-submit'), 'IAP + subscription versions into a review submission (first of a type: --with-version), then submit (asks first; --no-submit keeps a draft); reuses the open submission, never a second one'],
'apple submit': [() => import('./apple/submit'), 'a version to App Review: --build <n> attached (by number, VALID), the open submission reused, the ready IAPs / subscriptions with it, submitted with retry (dry run unless --yes)'],
'apple resubmit': [() => import('./apple/resubmit'), 'after a rejection: --build <n> attached to the rejected version, the UNRESOLVED_ISSUES submission reused (missing items added), resubmitted with retry (dry run unless --yes)'],
'apple offer-codes': [() => import('./apple/offer-codes'), 'offer codes for one-time purchases (campaigns; custom and one-time-use codes with --codes, asks first)'],
'apple win-back': [() => import('./apple/win-back'), 'win-back offers for lapsed subscribers (asks first: they go live on their start date)'],
'apple promoted': [() => import('./apple/promoted'), 'promoted in-app purchases on the product page: create, order, --enable (asks first)'],
'apple preorder': [() => import('./apple/preorder'), 'status | enable --release-date | end: App Store pre-orders per territory (asks first)'],
'apple cpp': [() => import('./apple/cpp'), 'custom product pages: list | create | upload --name | submit --name | visible --name (submit / visible ask first)'],
'apple events': [() => import('./apple/events'), 'in-app events: list | create | upload --name | submit --name (submit asks first)'],
'apple listing': [() => import('./apple/listing'), 'version, build, name / subtitle / description / keywords per locale; pull [--write]: live texts back into the listing file (diff first)'],
'apple screenshots': [() => import('./apple/screenshots'), '6.9" + 6.5" (+ 13" iPad) screenshots per locale; --variant <id>'],
'apple appinfo': [() => import('./apple/appinfo'), 'categories, age rating, content rights, Free price, territories'],
'apple review-details': [() => import('./apple/review-details'), 'App Review contact, demo account (from .storebound/, masked) + notes'],
'apple status': [() => import('./apple/status'), 'versions, builds, IAPs, review submissions (--wait-review, --cancel-submission <id>)'],
'apple previews': [() => import('./apple/previews'), 'app preview videos per locale (MD5-idempotent, poster frame time code); --dry'],
'apple release': [() => import('./apple/release'), 'phased release on|pause|resume|complete, --manual-release (asks first)'],
'apple signing': [() => import('./apple/signing'), 'status | ensure: bundle id capabilities, distribution certificate (key kept in .storebound/signing), App Store profiles installed for Xcode; --dry, asks first'],
'apple build': [() => import('./apple/build'), 'xcodebuild archive + exportArchive (ExportOptions.plist generated, next build number, API key signing); Capacitor, React Native, Flutter, Unity; --dry'],
'apple upload': [() => import('./apple/upload'), 'upload an IPA (App Store Connect API build upload, or --via altool), build number checked, waits for processing; --dry, asks first'],
'apple adhoc': [() => import('./apple/adhoc'), 'ad-hoc / release-testing: devices (list) | register (dry run, asks first) | options (ExportOptions.plist) | export --archive <x.xcarchive>'],
'apple next-build': [() => import('./apple/next-build'), 'next safe build number (prints the number)'],
'apple encryption': [() => import('./apple/encryption'), 'export compliance answer for builds, from the config'],
'apple ppo': [() => import('./apple/ppo'), 'Product Page Optimization: status | create --variant b | upload --variant b | start | stop (asks first)'],
'apple testflight': [() => import('./apple/testflight'), 'TestFlight: status | groups | testers (CSV in .storebound/) | what-to-test --build N | info | distribute --build N --group g [--submit-review] | wait --build N | expire --keep N|--older-than D (asks)'],
'icons': [() => import('./icons'), 'App Store / Play / web / adaptive / legacy launcher icons from one artwork'],
'shots capture': [() => import('./shots/capture'), 'raw frames: --source web (playwright) | ios-sim (simctl) | android-emu (adb) | folder; --dry'],
'shots fonts': [() => import('./shots/fonts'), 'download font subsets for the copy (CJK / Arabic / Cyrillic ...)'],
'shots render': [() => import('./shots/render'), 'store screenshots + feature graphic in every size and language'],
'shots jpeg': [() => import('./shots/jpeg'), 'JPEG q92 copies of the Play renders (4x faster uploads)'],
'shots check': [() => import('./shots/check'), 'local store checks of the renders (size, alpha, counts, overflow) and folder frames; --variants, --json'],
'shots record': [() => import('./shots/record'), 'app or gameplay clips for app previews: --source web (playwright video) | ios-sim (simctl recordVideo) | android-emu (screenrecord) | folder; --dry'],
'shots video': [() => import('./shots/video'), 'App Store app previews (886x1920, 1200x1600) + YouTube promo video from the clips, captions, poster, store checks; --dry'],
'shots gallery': [() => import('./shots/gallery'), 'review page <renders>/index.html: every language, size, scene and variant, issues highlighted'],
};