app-deploy-kit
v0.1.1
Published
CLI toolkit for automating app store deployment — screenshots, listings, graphics, and upload
Maintainers
Readme
deploy-kit
CLI toolkit for automating app-store deployment to Google Play and the Apple App Store. Captures screenshots from a running web app, frames them with device bezels, generates store listings, and creates feature graphics — all from the command line.
Published on npm as
app-deploy-kit; the CLI command isdeploy-kit.
Scope:
captureuses headless Chromium (Puppeteer), so it works for any web-renderable UI — plain web apps, Expo web, Flutter web, Capacitor, static sites. For native-only iOS/Android builds you'll need a simulator-based tool (e.g. fastlane snapshot, Maestro) to produce raw PNGs; the rest of deploy-kit (frame,listing,graphics) still works if you drop those PNGs indeploy-kit/screenshots/raw/.
Install
npm install -g app-deploy-kitRequires:
- Node 20+
- Puppeteer (bundled). On Linux, deploy-kit auto-detects a system Chromium at
/usr/bin/chromium,/usr/bin/chromium-browser, or/usr/bin/google-chrome(-stable)— useful on ARM (Raspberry Pi, ARM cloud) where Puppeteer's bundled Chromium doesn't run. Override withPUPPETEER_EXECUTABLE_PATH=/path/to/chromeif needed. - An Anthropic API key — only needed for
listing generate.
Quick start
# 1. Initialize config in your app project
deploy-kit init --name "My App" --bundle com.example.myapp --tagline "Fast, focused, fair." --color "#6B5BFF" --locales en-US,fr-FR
# 2. Start your dev server, then edit deploy-kit/config.json:
# - set screenshots.capture.baseUrl to your running app
# - list the routes you want in screenshots.capture.routes
# - (optional) add prerequisites for backend services that must be up
# Then capture raw screenshots:
deploy-kit capture
# 3. Generate store listing copy (requires ANTHROPIC_API_KEY)
deploy-kit listing generate --from README.md --locale all
# 4. Frame screenshots with device bezels
deploy-kit frame
# 5. Generate feature graphic and social card
deploy-kit graphics generateCommands
deploy-kit init
Initialize config for an app. Creates deploy-kit/ directory with config and folder structure.
deploy-kit init --name "My App" --bundle com.example.app \
[--tagline "Your tagline"] [--color "#007AFF"] [--locales en-US,fr-FR]Generated config includes 3 device defaults (iphone-15-pro, pixel-8, ipad-pro-11) with bezel references and a framing.caption block you can edit.
deploy-kit listing generate
Generate ASO-optimized store listing text using Claude. Outputs metadata in fastlane-compatible format for both iOS (metadata/) and Google Play (metadata-google/).
deploy-kit listing generate --from README.md \
[--keywords "todo,productivity,minimal"] [--locale en-US|all]Pass --locale all to generate listings for every locale in your config.
deploy-kit capture
Capture raw screenshots from a running web app and write them to deploy-kit/screenshots/raw/ — exactly where frame expects them.
deploy-kit capture [--base-url http://localhost:3000] \
[--viewport phone] [--route home] [--output deploy-kit/screenshots/raw]Configure under screenshots.capture in deploy-kit/config.json:
{
"screenshots": {
"capture": {
"baseUrl": "http://localhost:3000",
"viewports": [
{ "name": "phone", "width": 390, "height": 844, "deviceScaleFactor": 3 },
{ "name": "tablet", "width": 834, "height": 1194, "deviceScaleFactor": 2 }
],
"routes": [
{ "name": "home", "path": "/" },
{ "name": "editor", "path": "/editor", "waitFor": "#canvas-ready" },
{ "name": "settings", "path": "/settings", "hook": "deploy-kit/hooks/settings.mjs" }
],
"prerequisites": [
{ "name": "API server", "url": "http://localhost:3001/health" }
],
"waitForSelector": "body",
"navigationTimeoutMs": 30000,
"selectorTimeoutMs": 10000
}
}
}viewports[]— capture viewport in CSS pixels. Output PNG resolution =width × deviceScaleFactor×height × deviceScaleFactor.routes[].waitFor— CSS selector to wait for before snapping (per-route override ofwaitForSelector).prerequisites[]— services that must be reachable before capture starts. Each is fetched once; any HTTP response counts as up. If any fail, capture exits with a clear error before launching the browser. Use this for backend APIs the SPA proxies to.navigationTimeoutMs— max time forpage.goto(default 30000). Bump for slow first-route compiles.selectorTimeoutMs— max time waiting forwaitForSelector(default 10000).routes[].hook— path to a JS file that runs after navigation and before screenshot. Use it to dismiss onboarding splashes, set auth, scroll, fill mock data, or anything else that puts the page in a screenshot-ready state.
A common SPA pattern: dismiss a first-launch onboarding overlay that's gated on a localStorage flag.
// deploy-kit/hooks/dismiss-onboarding.mjs
export default async function dismissOnboarding(page, { route }) {
await page.evaluate(() => {
localStorage.setItem("onboarding-seen", "1");
});
await page.reload({ waitUntil: "domcontentloaded" });
await page.waitForSelector(`app-${route.name}`, { timeout: 30000 });
}Or a login flow that gates protected routes:
// deploy-kit/hooks/login.mjs
export default async function login(page) {
await page.click('[data-test="login"]');
await page.type("#email", "[email protected]");
await page.type("#password", "hunter2");
await page.click('button[type="submit"]');
await page.waitForSelector('[data-test="dashboard-loaded"]');
}Output filenames are <viewport>--<route>.png (e.g. phone--home.png). CLI flags (--base-url, --viewport, --route) override config values for ad-hoc runs.
Viewport ↔ device matching: each device entry can specify a
"viewport": "phone" | "tablet" | "<custom>".frameonly pairs a raw screenshot with a device when the screenshot's filename prefix (<viewport>--<route>.png, written bycapture) matches the device'sviewportfield. Devices without aviewportfield accept all screenshots (back-compat). Default devices ship with sensible mappings: phone-shape devices (iPhone, Pixel) get phone-viewport captures; iPad gets tablet-viewport captures.
deploy-kit frame
Frame raw screenshots with device bezels and marketing captions.
deploy-kit frame [--device iphone-15-pro] [--locale en-US]By default, frames every device in your config and writes one PNG per (raw file × device) pair into deploy-kit/screenshots/framed/<device>/<basename>.png. Use --device to target a single device.
deploy-kit graphics generate
Generate a Google Play feature graphic (1024×500) and social OG card (1200×630).
deploy-kit graphics generate [--output deploy-kit/graphics]Framing screenshots
deploy-kit frame takes raw app screenshots and outputs upload-ready framed PNGs — device bezel scaled to fit, caption, background gradient. Output dimensions match the device's native screen resolution (e.g. 1320×2868 for iPhone 16 Pro Max) so you can upload directly to App Store Connect and Google Play with no manual resize step.
Bundled device templates: iPhone 16 Pro Max (6.9", 1320×2868 — the only iPhone size App Store Connect requires; auto-scales to smaller iPhones), iPhone 15 Pro (6.1", 1179×2556), Pixel 8 (1080×2400), iPad Pro 11" (1668×2388). Framework-agnostic — drop in raw PNGs from any Expo / Capacitor / Flutter / native build.
Caption configuration
Edit framing.caption in deploy-kit/config.json:
{
"framing": {
"caption": {
"text": "Fast, focused, fair.",
"font": "DejaVu Sans",
"color": "#ffffff",
"position": "top", // "top" | "bottom"
"sizeRatio": 0.045 // fraction of canvas width
}
}
}If caption.text is empty, frame falls back to deploy-kit/metadata/<locale>/subtitle.txt if it exists (written by listing generate).
Background
By default, the area outside the device bezel is a 135° linear gradient auto-derived from primaryColor (darkened by ~20%). Override with any CSS background value via framing.background:
{
"framing": {
"background": "linear-gradient(135deg, #6B5BFF, #FF5BB5)",
// or radial: "radial-gradient(circle at top, #1a1a2e, #0f0f1a)"
// or solid: "#0f0f1a"
// or image: "url(data:image/png;base64,...) center/cover"
"caption": { "text": "Fast, focused, fair." }
}
}If unset, the default auto-gradient is used. Existing configs need no change.
Landscape devices
For landscape-locked apps (games, media players), set rotate: 90 on the device and use landscape canvas dimensions. The portrait bezel rotates 90° clockwise at render time and the screenshot fills the rotated screen rect — no separate landscape bezel assets required. Works with any bundled bezel (Android or iOS).
{
"screenshots": {
"devices": [
{
"name": "pixel-8-landscape",
"width": 2400, "height": 1080,
"bezel": "pixel-8",
"rotate": 90,
"viewport": "landscape"
},
{
"name": "iphone-16-pro-max-landscape",
"width": 2868, "height": 1320,
"bezel": "iphone-16-pro-max",
"rotate": 90,
"viewport": "landscape"
},
{
"name": "ipad-pro-11-landscape",
"width": 2388, "height": 1668,
"bezel": "ipad-pro-11",
"rotate": 90,
"viewport": "landscape"
}
],
"capture": {
"viewports": [
{ "name": "landscape", "width": 844, "height": 390, "deviceScaleFactor": 3 }
]
}
}
}The capture viewport just needs to be landscape (width > height) for @media (orientation: landscape) queries to fire. Output PNG dimensions match device.width × device.height and align with both stores' specs:
- Play Console — landscape phone slot accepts any 320–3840px PNG within 16:9 to 9:16 aspect, so a landscape Pixel 8 (2400×1080) drops straight in.
- App Store Connect — landscape iPhone 6.9" requires exactly 2868×1320; landscape iPad Pro 11" requires exactly 2388×1668. The bundled bezels match these specs at
width × heightswapped.
Custom device bezels
Each device with a bezel field references a template directory under src/templates/screenshots/<bezel>/:
bezel.svg— chrome overlay (hollow — screen area transparent)meta.json—{ device: {width,height}, screen: {x,y,width,height,radius} }
Add your own by dropping a new pair under src/templates/screenshots/<name>/ and setting "bezel": "<name>" on a device entry in config.json.
Custom templates (bezel fallback)
For devices without a bezel asset, frame falls back to src/templates/frame-default.html. You can override the fallback by copying it to deploy-kit/templates/frame-default.html and editing — user templates take priority over built-in ones. Same goes for graphic-feature.html used by graphics generate.
Output structure
deploy-kit/
├── config.json
├── screenshots/
│ ├── raw/ # Your raw screenshots
│ └── framed/ # Output, organized by device
│ ├── iphone-15-pro/
│ ├── pixel-8/
│ └── ipad-pro-11/
├── graphics/
│ ├── feature-graphic.png
│ └── social-card.png
├── metadata/ # iOS (fastlane-compatible)
│ └── en-US/
│ ├── name.txt
│ ├── subtitle.txt
│ ├── description.txt
│ ├── keywords.txt
│ ├── promotional_text.txt
│ └── release_notes.txt
└── metadata-google/ # Google Play
└── en-US/
├── title.txt
├── short_description.txt
└── full_description.txtUploading to stores
deploy-kit generates assets — use existing tools to upload them.
iOS (App Store Connect)
Install asc CLI (brew install asc) or use fastlane deliver:
# fastlane
fastlane deliver --metadata_path deploy-kit/metadata/en-US \
--screenshots_path deploy-kit/screenshots/framed
# asc CLI
asc apps screenshots upload --app-id <YOUR_APP_ID> --locale en-US \
--display-type APP_IPHONE_67 deploy-kit/screenshots/framed/iphone-15-pro/*.pngAndroid (Google Play)
Use fastlane supply:
fastlane supply --metadata_path deploy-kit/metadata-google/en-US \
--images_path deploy-kit/graphicsDevelopment
npm install
npm run typecheck # tsc strict + tests
npm run lint # eslint
npm run test # vitest + pixelmatch golden-file framing tests (tolerance 0.5%)
npm run build # compiles to dist/, copies templates to dist/templates/npm test compares framed output against committed PNG goldens in src/fixtures/frame/golden/. If a change intentionally alters rendering, regenerate goldens with:
UPDATE_GOLDENS=1 npm testThen inspect and commit the new goldens.
License
MIT.
