@armcompany/armfly
v0.1.4
Published
ARM FLY — zero-touch deploy CLI for App Store & Google Play
Maintainers
Readme
armfly — zero-touch deploy CLI (ARM FLY)
Full publishing pipeline with minimal human action: 3 commands on the first deploy, 1 command on every deploy after that.
armfly init # once: generates armfly.yaml + .gitignore
armfly analyze # detects features from an artifact (IPA/APK/AAB) → marketing copy
armfly translate # re-localizes metadata for every locale (ASO-aware)
armfly review # diagnoses a rejection and drafts a reply to the reviewer
armfly assets # generates store screenshots from raw screens (frame + headline)
armfly video # creates promo video (App Store preview / Reel / TikTok) — needs ffmpeg
armfly doctor # validates config, credentials and local tooling
armfly deploy # build → metadata (AI) → upload → submit (Apple + Google)
armfly watch # polls review until approved/rejected, with diagnosis on rejection
armfly ui # local web dashboard (edit metadata, run deploys, live logs)
armfly batch ... # runs any command in batch over many projects (white label)Mass publishing / white label (armfly batch)
Runs any ARM FLY command over several projects at once — the "100 apps → update → publish in batch" case:
# armfly.workspace.yaml
name: My Agency
env: # shared value, injected into every project
RELEASE_NOTES: "Bug fixes and improvements"
projects:
- clients/barberpro
- clients/petshoparmfly batch translate # localize every app
armfly batch deploy --stagger 5 # publish in batch, 5s between apps (rate-limit)
armfly batch assets --concurrency 4 # generate screenshots in parallelEach project runs in its own directory with its own armfly.yaml; the workspace
env: becomes ${VAR} inside every config (the white-label lever). Sequential
by default (safe for the stores); use --concurrency for read-only commands.
Mockup Studio (armfly assets)
Turns a raw app screenshot into a store-ready screenshot — device frame + marketing headline — at the exact size each store requires:
assets:
output: assets/generated
style: modern # clean | dark | modern | startup | luxury | enterprise
devices: [iphone_6_7, pixel] # + iphone_6_5, ipad_13, galaxy, android_phone
screens:
- source: raw/home.png
headline: "Book services in seconds"
subheadline: "More convenience for your customers"
locales: [en-US] # optional; default = metadata.localesarmfly assets renders output/{locale}/{device}/NN.png and prints a
ready-to-paste screenshots: block — which armfly deploy then uploads to the
stores. Deterministic rendering via resvg; no device farm.
Web dashboard (armfly ui)
Serves a panel at http://localhost:4321 (loopback only — credentials never
leave your machine):
- Pipeline: live status of every step (metadata, builds, publish) read from the journal
- Metadata per locale: tabs per locale, fields with a character counter against store limits; edits are saved and become the source of truth for the next deploy
- Actions: Generate metadata, Dry-run, Deploy, Deploy --fresh and Doctor with real-time logs
Flags: -p/--port (default 4321), -c for an alternative config. On macOS it
opens the browser automatically.
Installation
For use in an app project:
npm install --global @armcompany/armfly
armfly --helpOr run it without a global install:
npx @armcompany/armfly --helpTo develop ARM FLY from the monorepo:
npm install # installs all workspaces
npm run build # builds @armcompany/armfly-core, then the CLI
npm link -w armfly # exposes the `armfly` binary globallyThe minimal flow
cd my-app
armfly init # edit the armfly.yaml placeholders
armfly doctor # all green?
armfly deploy --dry-run # review the plan without touching the stores
armfly deploy # runs end to end
armfly watch # optional: poll review, with hints on rejectionFailed midway? Run armfly deploy again — the journal in .armfly/state.json
skips every step already completed (same fingerprint ⇒ skip). --fresh resets
the journal.
Credentials (the only unavoidable manual step)
| Store | What to get | Where to put it |
|---|---|---|
| Apple | App Store Connect API Key (.p8 + keyId + issuerId) — Users and Access → Integrations | stores.apple (use ${ENV_VAR} in the yaml) |
| Google | Service account JSON with Play Console access ("Release manager" role) | stores.google.serviceAccountPath |
| AI (optional) | OPENCODE_ZEN_API_KEY (or ANTHROPIC_API_KEY) for AI-generated metadata | environment; without it, falls back to deterministic template copy |
Useful flags
| Flag | Effect |
|---|---|
| --dry-run | prints the full plan with no side effects |
| --no-submit | uploads build+metadata but does not submit for review |
| --platform ios\|android | limits to one side |
| --fresh | ignores the journal and re-runs everything |
| -c other.yaml | alternative config (multi-app in the same repo) |
What is real vs. known limitations
Real: App Store Connect ES256 JWT; version + localizations + title/subtitle
(appInfoLocalizations) + promotional text / marketing copy + screenshot
upload (reservation → PUT → md5 confirmation) + build attach (filtered by the
correct version) + submission via ASC API; full transactional Play flow (edit →
upload AAB → store listing per locale → screenshots/graphics per locale →
track with staged rollout → commit); metadata with the stores' hard limits;
idempotent resume; retries with backoff; AI-generated metadata, localization and
rejection diagnosis; mockup screenshot generation; promo video generation; local
web dashboard (armfly ui).
Limitations: IPA upload delegates to xcrun altool (macOS only); binaries and
images are read into memory (fine up to ~hundreds of MB); the app must already
exist in the stores (creating the app record is done manually in the consoles —
Google requires this for the first AAB); not yet validated against real store
accounts end to end.
Declaring screenshots in armfly.yaml
screenshots:
ios: # Apple types: APP_IPHONE_67, APP_IPHONE_65, APP_IPAD_PRO_3GEN_129 ...
en-US:
APP_IPHONE_67: [assets/ios/en/6.7/01.png, assets/ios/en/6.7/02.png]
android: # Play types: phoneScreenshots, sevenInchScreenshots, featureGraphic ...
en-US:
phoneScreenshots: [assets/android/en/01.png]Each deploy replaces the set for that type/locale (idempotent).
