@agustin-perticaro/store-pilot 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/bin/store-pilot.js +66 -0
- package/commands/init.js +466 -0
- package/commands/keywords.js +168 -0
- package/commands/match-setup.js +61 -0
- package/commands/screenshots.js +206 -0
- package/commands/ship.js +154 -0
- package/lib/config.js +24 -0
- package/package.json +55 -0
- package/templates/scripts/update-keywords.mjs +125 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Agustin Perticaro
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# store-pilot
|
|
2
|
+
|
|
3
|
+
CLI to automate ASO, builds, and store uploads for Expo/React Native projects **without EAS**.
|
|
4
|
+
|
|
5
|
+
One command to do it all: keywords → screenshots → build → upload.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g store-pilot
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Node.js ≥ 18, Ruby + Bundler, and fastlane installed.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
gem install fastlane
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Setup (once per project)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd your-expo-project
|
|
27
|
+
store-pilot init
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`init` auto-detects your `app.json`, asks questions, and generates:
|
|
31
|
+
|
|
32
|
+
- `fastlane/Appfile` — identifies your app
|
|
33
|
+
- `fastlane/Fastfile` — all automated lanes
|
|
34
|
+
- `fastlane/Matchfile` — iOS certificate management
|
|
35
|
+
- `fastlane/metadata/` — complete structure per language
|
|
36
|
+
- `.env.store-pilot` — your credentials (DO NOT commit)
|
|
37
|
+
- `.env.store-pilot.example` — safe template for the repo
|
|
38
|
+
- `store-pilot-scripts/` — standalone scripts
|
|
39
|
+
|
|
40
|
+
### Configure iOS certificates (once)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
store-pilot match-setup
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Uses fastlane match: creates and stores your certificates in a private git repo. Any team Mac can sync them with `bundle exec fastlane match appstore --readonly`.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Required Credentials
|
|
51
|
+
|
|
52
|
+
Fill in `.env.store-pilot` with:
|
|
53
|
+
|
|
54
|
+
| Variable | Where to get it |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `ASC_KEY_ID` | App Store Connect → Users → API Keys |
|
|
57
|
+
| `ASC_ISSUER_ID` | Same page |
|
|
58
|
+
| `ASC_KEY_PATH` | The downloaded `.p8` file |
|
|
59
|
+
| `MATCH_PASSWORD` | Password you chose in match-setup |
|
|
60
|
+
| `GOOGLE_PLAY_JSON_KEY_PATH` | Play Console → Settings → API Access → Service Account |
|
|
61
|
+
| `ANDROID_KEYSTORE_*` | Your release keystore |
|
|
62
|
+
| `APPSCREENS_API_KEY` | appscreens.io → Account → API |
|
|
63
|
+
| `APPSCREENS_PROJECT_ID` | Your AppScreens project ID |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
### Full pipeline (recommended)
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
store-pilot ship
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Runs in order: expo prebuild → keywords → screenshots → build iOS + Android → upload.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# iOS only, submit for review automatically
|
|
79
|
+
store-pilot ship --platform ios --submit
|
|
80
|
+
|
|
81
|
+
# Android only, upload to beta
|
|
82
|
+
store-pilot ship --platform android --track beta
|
|
83
|
+
|
|
84
|
+
# Skip keywords and screenshots (if unchanged)
|
|
85
|
+
store-pilot ship --skip-keywords --skip-screenshots
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Individual commands
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Update keywords from Astro MCP
|
|
92
|
+
store-pilot keywords
|
|
93
|
+
|
|
94
|
+
# New app not published yet → use a competitor as reference
|
|
95
|
+
store-pilot keywords --competitor "https://apps.apple.com/app/notion/id1232780281"
|
|
96
|
+
|
|
97
|
+
# Generate and download screenshots from AppScreens
|
|
98
|
+
store-pilot screenshots
|
|
99
|
+
|
|
100
|
+
# Generate AND upload directly to stores
|
|
101
|
+
store-pilot screenshots --upload
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Workflow for new apps (pre-publication)
|
|
107
|
+
|
|
108
|
+
Astro works before publishing by using competitors as proxy:
|
|
109
|
+
|
|
110
|
+
1. Find 2-3 competitor apps in the App Store
|
|
111
|
+
2. Copy their URLs
|
|
112
|
+
3. Run for each one:
|
|
113
|
+
```bash
|
|
114
|
+
store-pilot keywords --competitor "https://apps.apple.com/app/.../id123"
|
|
115
|
+
```
|
|
116
|
+
4. store-pilot combines and ranks keywords by popularity/difficulty
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Metadata
|
|
121
|
+
|
|
122
|
+
Fill in the files at `fastlane/metadata/` — plain text files versioned in git:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
fastlane/metadata/
|
|
126
|
+
├── ios/
|
|
127
|
+
│ ├── en-US/
|
|
128
|
+
│ │ ├── name.txt ← app name
|
|
129
|
+
│ │ ├── subtitle.txt ← subtitle (30 chars)
|
|
130
|
+
│ │ ├── description.txt ← long description
|
|
131
|
+
│ │ ├── keywords.txt ← generated by store-pilot keywords
|
|
132
|
+
│ │ ├── promotional_text.txt ← updatable without new build
|
|
133
|
+
│ │ └── release_notes.txt ← what's new in this version
|
|
134
|
+
│ └── review_information/
|
|
135
|
+
│ ├── first_name.txt
|
|
136
|
+
│ └── ...
|
|
137
|
+
└── android/
|
|
138
|
+
└── en-US/
|
|
139
|
+
├── title.txt
|
|
140
|
+
├── short_description.txt
|
|
141
|
+
├── full_description.txt
|
|
142
|
+
└── changelogs/
|
|
143
|
+
└── default.txt
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## CI/CD (GitHub Actions)
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
# .github/workflows/ship.yml
|
|
152
|
+
name: Ship to stores
|
|
153
|
+
|
|
154
|
+
on:
|
|
155
|
+
push:
|
|
156
|
+
tags: ['v*']
|
|
157
|
+
|
|
158
|
+
jobs:
|
|
159
|
+
ship:
|
|
160
|
+
runs-on: macos-latest
|
|
161
|
+
steps:
|
|
162
|
+
- uses: actions/checkout@v4
|
|
163
|
+
- uses: actions/setup-node@v4
|
|
164
|
+
with:
|
|
165
|
+
node-version: '20'
|
|
166
|
+
- run: npm install -g store-pilot
|
|
167
|
+
- run: gem install fastlane
|
|
168
|
+
- run: store-pilot ship --skip-keywords
|
|
169
|
+
env:
|
|
170
|
+
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
|
171
|
+
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
|
172
|
+
ASC_KEY_PATH: ${{ secrets.ASC_KEY_PATH }}
|
|
173
|
+
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
|
174
|
+
GOOGLE_PLAY_JSON_KEY_PATH: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
|
|
175
|
+
ANDROID_KEYSTORE_PATH: ${{ secrets.ANDROID_KEYSTORE }}
|
|
176
|
+
ANDROID_KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASS }}
|
|
177
|
+
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
|
178
|
+
ANDROID_KEY_PASS: ${{ secrets.ANDROID_KEY_PASS }}
|
|
179
|
+
APPSCREENS_API_KEY: ${{ secrets.APPSCREENS_API_KEY }}
|
|
180
|
+
APPSCREENS_PROJECT_ID: ${{ secrets.APPSCREENS_PROJECT_ID }}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
> **Note:** `store-pilot keywords` uses Astro MCP which runs locally on your Mac, so that step is done on your machine before pushing. In CI it's skipped with `--skip-keywords`.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Project structure
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
store-pilot/
|
|
191
|
+
├── bin/store-pilot.js CLI entrypoint
|
|
192
|
+
├── commands/
|
|
193
|
+
│ ├── init.js scaffolding
|
|
194
|
+
│ ├── keywords.js Astro MCP → keywords.txt
|
|
195
|
+
│ ├── screenshots.js AppScreens API → assets
|
|
196
|
+
│ ├── ship.js full pipeline
|
|
197
|
+
│ └── match-setup.js certificate setup
|
|
198
|
+
├── lib/
|
|
199
|
+
│ └── config.js config + env loader
|
|
200
|
+
└── templates/
|
|
201
|
+
├── scripts/ copied to project as store-pilot-scripts/
|
|
202
|
+
└── fastlane/metadata/ base metadata structure
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('store-pilot')
|
|
12
|
+
.description('Automate ASO, builds & store uploads for Expo/React Native projects')
|
|
13
|
+
.version(pkg.version);
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.command('init')
|
|
17
|
+
.description('Configure store-pilot in an existing Expo project')
|
|
18
|
+
.option('--yes', 'Saltar confirmaciones interactivas')
|
|
19
|
+
.action(async (opts) => {
|
|
20
|
+
const { runInit } = await import('../commands/init.js');
|
|
21
|
+
await runInit(opts);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.command('keywords')
|
|
26
|
+
.description('Generate keywords.txt from Astro MCP (must be running)')
|
|
27
|
+
.option('--lang <lang>', 'Target language (e.g. en-US)', 'en-US')
|
|
28
|
+
.option('--min-popularity <n>', 'Minimum popularity', '20')
|
|
29
|
+
.option('--max-difficulty <n>', 'Maximum difficulty', '50')
|
|
30
|
+
.option('--competitor <url>', 'App Store URL of a competitor to extract keywords from')
|
|
31
|
+
.action(async (opts) => {
|
|
32
|
+
const { runKeywords } = await import('../commands/keywords.js');
|
|
33
|
+
await runKeywords(opts);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('screenshots')
|
|
38
|
+
.description('Generate and download screenshots from AppScreens API')
|
|
39
|
+
.option('--upload', 'Upload directly to App Store Connect and Google Play')
|
|
40
|
+
.action(async (opts) => {
|
|
41
|
+
const { runScreenshots } = await import('../commands/screenshots.js');
|
|
42
|
+
await runScreenshots(opts);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command('ship')
|
|
47
|
+
.description('Full pipeline: keywords → screenshots → build → upload')
|
|
48
|
+
.option('--platform <p>', 'ios | android | all', 'all')
|
|
49
|
+
.option('--track <t>', 'Android track: internal | alpha | beta | production', 'internal')
|
|
50
|
+
.option('--submit', 'Submit for review automatically (iOS)')
|
|
51
|
+
.option('--skip-keywords', 'Skip keyword update')
|
|
52
|
+
.option('--skip-screenshots', 'Skip screenshot regeneration')
|
|
53
|
+
.action(async (opts) => {
|
|
54
|
+
const { runShip } = await import('../commands/ship.js');
|
|
55
|
+
await runShip(opts);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command('match-setup')
|
|
60
|
+
.description('Configure fastlane match for automatic iOS certificate management')
|
|
61
|
+
.action(async () => {
|
|
62
|
+
const { runMatchSetup } = await import('../commands/match-setup.js');
|
|
63
|
+
await runMatchSetup();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
program.parse();
|