Show HN: Storeshot/Google Play store screenshots built for agents Storeshot, an open-source tool by Thiago Peres, generates Apple and Google Play store screenshots from a single command for native, web, or hybrid apps, enforcing store rules and working in CI. The MIT-licensed tool runs on Node 20+ with no browser dependency for native apps, using sharp for composition and supporting agent-driven config files. Apple and Google Play store screenshots, built by agents, for agents. - ⚡ One command — npx storeshot captures, frames and stages a whole set, in CI or locally. - 📱 Native, web, or hybrid — one pipeline for the iOS Simulator, an Android emulator, a headless browser, or PNGs something else produced. The rest of the field picks a side. - 🌍 Locales are JSON — captions wrap and rebalance to fit; nothing overflows. - 🎯 Store rules, enforced — exact sizes, no alpha, under 8 MB, Apple bezels whole, Android free to bleed. A bad asset fails the run, not the upload. - 🤖 Agent-first — the whole job is one config file, so a coding agent can add a screen or rewrite the copy and the diff lands as images in a pull request. - 🆓 Open source, no account — MIT, runs on your machine, frames cached locally. | | Describe the screens you want in a config file and get back store-ready assets: real captures of your app, clipped into real device bezels, laid out under marketing copy, at the exact pixel sizes Apple and Google accept, staged for fastlane deliver and supply . Works with native and web apps alike. Captures come from the iOS Simulator, an Android emulator, a headless browser, or a directory of screenshots something else produced — everything after that point is identical. No design tool in the loop. The whole set is one command, so it can run in CI, and an agent can change the copy or add a screen by editing one file. The other ways of doing this all make you give something up: | Tool | The catch | |---|---| 🐌 fastlane frameit | Fragile ImageMagick and JSON layout, device frames that trail Apple's hardware, native-only | 🖱️ Mockup makers | Free, browser-based, one frame at a time by hand — nothing to run in CI | 🎨 Design templates | Drift the moment the UI ships; every device and locale is re-exported by hand | 💸 Hosted generators | A subscription, an account, and your app's screens on someone else's servers | storeshot gives up none of them. npm install --save-dev github:thiagoperes/storeshot Node 20 or newer. Nothing else, and no browser: the marketing canvas is composed with sharp https://sharp.pixelplumbing.com , so a native app never downloads a browser binary. Device bezels are fetched on first use and cached. Capturing from a browser is the one thing that needs more. Playwright is an optional peer dependency, so add it only for that: npm install --save-dev playwright npx playwright install webkit chromium Composition shapes text with Pango, so at least one font has to be installed. A desktop or a normal CI runner already has plenty; a slim Linux image may need a package such as fonts-dejavu-core . Name real families in your theme — -apple-system and other CSS keywords mean nothing outside a browser and are skipped over in the stack. The examples below build the real App Store and Play sets for Rally https://getrally.com , a fleet expense platform that ships on both stores. storeshot.ios.config.ts — the App Store set, captured from the real build in the Simulator. Requires macOS with Xcode. js import { defineConfig, findTarget } from 'storeshot'; export default defineConfig { // Rally's iOS app is iPhone-only, so the iPad target comes off. Apple scales // the 6.9" set down for every smaller iPhone, so one target covers them all. targets: findTarget 'ios-iphone-6.9' , capture: { kind: 'ios-simulator', // First name that exists wins, and an already-booted device is preferred. device: 'iPhone 17 Pro Max', 'iPhone 16 Pro Max' , bundleId: 'com.getrally', appPath: 'ios/App/build/Debug-iphonesimulator/App.app', }, // A launch and a route change need longer to settle than a browser does. settleDelay: 2500, screens: { id: 'platform', theme: 'dark' }, { id: 'transactions', theme: 'dark' }, { id: 'cards', theme: 'dark' }, , captions: { en: { platform: { kicker: 'Payments', title: 'Every fleet expense, one app' }, transactions: { kicker: 'Transactions', title: 'See every purchase as it happens', }, cards: { kicker: 'Cards', title: 'Issue cards in seconds, not days' }, }, }, } ; npx storeshot --config storeshot.ios.config.ts Captures come out full-screen, with the device's own status bar, pinned to 9:41 on a full battery. Rally's screens are ordinary in-app routes with no URL scheme behind them, so this run stops and asks you to navigate before each capture. Give a screen a deepLink , or the config a navigate hook, to make it unattended — see Navigating a native app navigating-a-native-app . storeshot.android.config.ts — the Play set, from the release APK on an emulator. Requires the Android SDK platform tools on your PATH . js import { defineConfig, findTarget } from 'storeshot'; export default defineConfig { // Play asks for a phone and a tablet asset separately. targets: findTarget 'android-phone' , findTarget 'android-tablet' , capture: { kind: 'android-emulator', appId: 'com.getrally.app', apkPath: 'android/app/build/outputs/apk/release/app-release.apk', // Booted when nothing is already attached. Omit to use adb 's only device. avd: 'Pixel 5 API 35', }, screens: { id: 'platform', deepLink: 'https://app.getrally.com/home', theme: 'dark', }, { id: 'cards', deepLink: 'https://app.getrally.com/home/cards', theme: 'dark', }, , captions: { en: { platform: { kicker: 'Payments', title: 'Every fleet expense, one app' }, cards: { kicker: 'Cards', title: 'Issue cards in seconds, not days' }, }, }, } ; npx storeshot --config storeshot.android.config.ts A deepLink here is anything adb shell am start can open, so an app link like the one above works without registering a custom scheme, as long as the app declares the intent filter. Play allows the device to bleed off the bottom edge, and these targets do, where the App Store ones above keep the bezel whole. Rally is a Capacitor app, so the same screens can be captured headless in seconds without building either binary — WebKit for iOS targets, Chromium for Android ones, matching the web view each platform actually runs. This is Rally's default config, with the two native ones above kept for checking the native shell before a listing refresh. js import { defineConfig } from 'storeshot'; export default defineConfig { baseUrl: process.env.SCREENSHOT BASE URL ?? 'http://localhost:3000', screens: { id: 'platform', path: '/home/acme', theme: 'dark', // Nothing is captured until these are on screen, so no skeletons. waitFor: ' data-slot="card" ' , }, , captions: { // as above }, // Signs in once; the session is reused across targets. auth: { page } = signIn page , } ; npx storeshot Whichever route you take, assets land in screenshots/framed/