113 lines
22 KiB
TypeScript
113 lines
22 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)'],
|
|
'watch': [() => import('./watch'), 'store events for every app on this machine: rejected, approved, live, rollout halted, crash threshold, low reviews; loops every --every 15m or --once; sinks --notify os|ntfy|telegram|webhook|ingest; test sends a test event; --dry, --dir <folder>, --json (read-only on the stores)'],
|
|
'status': [() => import('./status'), '[--app <name|store id|folder>] [--json]: per platform the live and next version, build, track, review state and rollout %, without uploading (read-only; read by ship pipelines, docs/ship-step.md)'],
|
|
'events': [() => import('./events'), '[--since 24h] [--app <name>] [--kind rejected] [--json]: the events watch recorded in ~/.storebound/events.jsonl (read-only)'],
|
|
'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 | ignore|unignore <name|id> [--store apple|play]: leave a dormant app (or one store side) out of watch, portfolio reads and the desktop lists; storebound only, the store is untouched'],
|
|
'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> | play-developer <profile> <id> (Play Console deep links) | default <profile> (the machine default; --clear)'],
|
|
'settings': [() => import('./settings'), 'per-machine settings: bypass on | off | status (owner opt-in: the agent also presses the owner-only buttons; default off)'],
|
|
'aab': [() => import('./aab'), 'offline: an AAB file: packageName, versionCode, versionName, min / target SDK, sha256 (no config, no upload); --json'],
|
|
'doctor': [() => import('./doctor'), 'build and capture dependencies (JDK with the version the project needs, Android SDK, Xcode, CocoaPods, Node, Bun, ffmpeg, Chrome, adb, git, bundletool) and how to install what is missing; --install <tool> --yes; --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'],
|
|
'report': [() => import('./report'), 'shareable store readiness report: scan + check + validate (read-only; only the stores with credentials, else it says why) in one self-contained HTML page .storebound/reports/readiness-<date>.html, opened in the browser; --out <file>, --md, --json, --no-open, --offline'],
|
|
'analytics': [() => import('./analytics'), 'setup (App Store report request) | snapshot (impressions, page views, installs, App Usage, conversion -> .storebound/analytics; --json prints it) | 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'],
|
|
'art': [() => import('./art'), 'pull [--store apple|play] [--write] [--force]: the live store art (App Store screenshot sets, previews, icon; Play icon, feature graphic, screenshots per language) into store/renders + pulled.json, so checks and uploads treat it like rendered art (upsert; differing local files kept without --force; read-only on the stores) | live [--json]: the live art with thumbnails | replace --from <file> --to <path>: one image of the app folder'],
|
|
'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; apple-sales and play-sales also --days N | --from/--to (per-day JSON); 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 <file|->] [--from-text <file|->] [--json]: check a filled console form against the config (page text, the input-state JSON of the plan\'s verify.capture, 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; --production completed|inProgress --yes ships an UPDATE of a published app without an owner step (a first-ever production release is the owner\'s: exit 3 OWNER_STEP, --first-release after the owner\'s yes)'],
|
|
'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 track (--track internal|alpha|beta|production), versionCode checked first; production with --yes is an update step for a published app (a first-ever release is the owner\'s: exit 3 OWNER_STEP); --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'],
|
|
'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'],
|
|
'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; an UPDATE of an app on sale needs no owner step, the first-ever submission is exit 3 OWNER_STEP unless --first-release); --platform ios|macos|tvos|visionos (default: the platform that has the version this needs; asks when several do)'],
|
|
'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); --platform ios|macos|tvos|visionos (default: the platform that has the version this needs; asks when several do)'],
|
|
'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); --platform ios|macos|tvos|visionos (default: the platform that has the version this needs; asks when several do)'],
|
|
'apple screenshots': [() => import('./apple/screenshots'), '6.9" + 6.5" (+ 13" iPad) screenshots per locale; --variant <id>; --repair-stuck [--dry] (replace only screenshots not COMPLETE, at their positions); --platform ios|macos|tvos|visionos (default: the platform that has the version this needs; asks when several do)'],
|
|
'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>); --platform ios|macos|tvos|visionos (default: the platform that has the version this needs; asks when several do)'],
|
|
'apple creative-assets': [() => import('./apple/creative-assets'), 'Header and Search Results: product page header (21:9) and search results asset (3:2) per locale, or one universal 5244x2950 PNG; uploads to the Asset Library and places it once Apple approved it, reporting waiting ones per locale (MD5 upsert, safe to re-run); --add-for-review, --verify (read-only), --dry'],
|
|
'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; --yes for an update of an app on sale, exit 7 when already released; the first-ever release is exit 3 OWNER_STEP unless --first-release); --platform ios|macos|tvos|visionos (default: the platform that has the version this needs; asks when several do)'],
|
|
'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; --platform macos --tauri <src-tauri> [--path-free] builds a Tauri app for the Mac App Store (signed .app, sandbox entitlements, profile, productbuild .pkg); --dry'],
|
|
'apple upload': [() => import('./apple/upload'), 'upload an IPA, or with --platform macos a signed .pkg (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 game-center': [() => import('./apple/game-center'), '[status] Game Center from games: the app\'s Game Center detail, the version switch, leaderboards + achievements with localizations (upsert by vendor id); App ID capability via signing ensure; --dry, asks first'],
|
|
'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; signed-in apps: shots.capture.auth) | ios-sim (simctl) | android-emu (adb) | folder; --dry'],
|
|
'shots login': [() => import('./shots/login'), 'opens a visible browser (fresh profile) at shots.capture.baseUrl or --url; you sign in to a demo account by hand, Enter saves .storebound/auth.json (0600) for shots.capture.auth.storageState; --timeout <s>'],
|
|
'shots fonts': [() => import('./shots/fonts'), 'download font subsets for the copy (CJK / Arabic / Cyrillic ...)'],
|
|
'shots copy': [() => import('./shots/copy'), 'translate --to de,es [--from en] [--translate "<command>"]: fill store/shots/copy.json per language (machine-translated until reviewed, caption limits); check | reviewed --lang de'],
|
|
'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'],
|
|
};
|