appshot-cli 0.1.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 +381 -0
- package/bin/appshot.js +5 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +31 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/build.d.ts +3 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +200 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/caption.d.ts +3 -0
- package/dist/commands/caption.d.ts.map +1 -0
- package/dist/commands/caption.js +118 -0
- package/dist/commands/caption.js.map +1 -0
- package/dist/commands/check.d.ts +3 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +121 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/clean.d.ts +3 -0
- package/dist/commands/clean.d.ts.map +1 -0
- package/dist/commands/clean.js +133 -0
- package/dist/commands/clean.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +84 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/localize.d.ts +3 -0
- package/dist/commands/localize.d.ts.map +1 -0
- package/dist/commands/localize.js +31 -0
- package/dist/commands/localize.js.map +1 -0
- package/dist/commands/presets.d.ts +3 -0
- package/dist/commands/presets.d.ts.map +1 -0
- package/dist/commands/presets.js +135 -0
- package/dist/commands/presets.js.map +1 -0
- package/dist/commands/specs.d.ts +3 -0
- package/dist/commands/specs.d.ts.map +1 -0
- package/dist/commands/specs.js +69 -0
- package/dist/commands/specs.js.map +1 -0
- package/dist/commands/validate.d.ts +3 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +183 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/core/app-store-specs.d.ts +55 -0
- package/dist/core/app-store-specs.d.ts.map +1 -0
- package/dist/core/app-store-specs.js +457 -0
- package/dist/core/app-store-specs.js.map +1 -0
- package/dist/core/compose.d.ts +30 -0
- package/dist/core/compose.d.ts.map +1 -0
- package/dist/core/compose.js +420 -0
- package/dist/core/compose.js.map +1 -0
- package/dist/core/devices.d.ts +50 -0
- package/dist/core/devices.d.ts.map +1 -0
- package/dist/core/devices.js +404 -0
- package/dist/core/devices.js.map +1 -0
- package/dist/core/files.d.ts +5 -0
- package/dist/core/files.d.ts.map +1 -0
- package/dist/core/files.js +35 -0
- package/dist/core/files.js.map +1 -0
- package/dist/core/frames-analyzer.d.ts +24 -0
- package/dist/core/frames-analyzer.d.ts.map +1 -0
- package/dist/core/frames-analyzer.js +97 -0
- package/dist/core/frames-analyzer.js.map +1 -0
- package/dist/core/frames-loader.d.ts +113 -0
- package/dist/core/frames-loader.d.ts.map +1 -0
- package/dist/core/frames-loader.js +440 -0
- package/dist/core/frames-loader.js.map +1 -0
- package/dist/core/mask-generator.d.ts +10 -0
- package/dist/core/mask-generator.d.ts.map +1 -0
- package/dist/core/mask-generator.js +99 -0
- package/dist/core/mask-generator.js.map +1 -0
- package/dist/core/render.d.ts +10 -0
- package/dist/core/render.d.ts.map +1 -0
- package/dist/core/render.js +92 -0
- package/dist/core/render.js.map +1 -0
- package/dist/core/text-renderer.d.ts +18 -0
- package/dist/core/text-renderer.d.ts.map +1 -0
- package/dist/core/text-renderer.js +148 -0
- package/dist/core/text-renderer.js.map +1 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/caption-history.d.ts +35 -0
- package/dist/utils/caption-history.d.ts.map +1 -0
- package/dist/utils/caption-history.js +233 -0
- package/dist/utils/caption-history.js.map +1 -0
- package/frames/Frames.json +308 -0
- package/frames/MacBook Air 2020.png +0 -0
- package/frames/MacBook Air 2022.png +0 -0
- package/frames/MacBook Pro 13.png +0 -0
- package/frames/MacBook Pro 2021 14.png +0 -0
- package/frames/MacBook Pro 2021 16.png +0 -0
- package/frames/Watch Series 10 42.png +0 -0
- package/frames/Watch Series 10 42_mask.png +0 -0
- package/frames/Watch Series 10 46.png +0 -0
- package/frames/Watch Series 10 46_mask.png +0 -0
- package/frames/Watch Series 4 40.png +0 -0
- package/frames/Watch Series 4 44.png +0 -0
- package/frames/Watch Series 7 41.png +0 -0
- package/frames/Watch Series 7 45.png +0 -0
- package/frames/Watch Ultra 2024.png +0 -0
- package/frames/iMac 2021.png +0 -0
- package/frames/iPad 2021 Landscape.png +0 -0
- package/frames/iPad 2021 Portrait.png +0 -0
- package/frames/iPad Air 2020 Landscape.png +0 -0
- package/frames/iPad Air 2020 Portrait.png +0 -0
- package/frames/iPad Pro 2018-2021 11 Landscape.png +0 -0
- package/frames/iPad Pro 2018-2021 11 Portrait.png +0 -0
- package/frames/iPad Pro 2018-2021 Landscape.png +0 -0
- package/frames/iPad Pro 2018-2021 Portrait.png +0 -0
- package/frames/iPad Pro 2024 11 Landscape.png +0 -0
- package/frames/iPad Pro 2024 11 Portrait.png +0 -0
- package/frames/iPad Pro 2024 13 Landscape.png +0 -0
- package/frames/iPad Pro 2024 13 Portrait.png +0 -0
- package/frames/iPad mini 2021 Landscape.png +0 -0
- package/frames/iPad mini 2021 Portrait.png +0 -0
- package/frames/iPhone 11 Landscape.png +0 -0
- package/frames/iPhone 11 Portrait.png +0 -0
- package/frames/iPhone 11 Pro Max Landscape.png +0 -0
- package/frames/iPhone 11 Pro Max Portrait.png +0 -0
- package/frames/iPhone 11 Pro Portrait.png +0 -0
- package/frames/iPhone 12-13 Pro Landscape.png +0 -0
- package/frames/iPhone 12-13 Pro Max Landscape.png +0 -0
- package/frames/iPhone 12-13 Pro Max Landscape_mask.png +0 -0
- package/frames/iPhone 12-13 Pro Max Portrait.png +0 -0
- package/frames/iPhone 12-13 Pro Max Portrait_mask.png +0 -0
- package/frames/iPhone 12-13 Pro Portrait.png +0 -0
- package/frames/iPhone 12-13 mini Landscape.png +0 -0
- package/frames/iPhone 12-13 mini Portrait.png +0 -0
- package/frames/iPhone 16 Landscape.png +0 -0
- package/frames/iPhone 16 Landscape_frame_no_island.png +0 -0
- package/frames/iPhone 16 Landscape_mask.png +0 -0
- package/frames/iPhone 16 Plus Landscape.png +0 -0
- package/frames/iPhone 16 Plus Landscape_frame_no_island.png +0 -0
- package/frames/iPhone 16 Plus Landscape_mask.png +0 -0
- package/frames/iPhone 16 Plus Portrait.png +0 -0
- package/frames/iPhone 16 Plus Portrait_frame_no_island.png +0 -0
- package/frames/iPhone 16 Plus Portrait_mask.png +0 -0
- package/frames/iPhone 16 Portrait.png +0 -0
- package/frames/iPhone 16 Portrait_frame_no_island.png +0 -0
- package/frames/iPhone 16 Portrait_mask.png +0 -0
- package/frames/iPhone 16 Pro Landscape.png +0 -0
- package/frames/iPhone 16 Pro Landscape_frame_no_island.png +0 -0
- package/frames/iPhone 16 Pro Landscape_mask.png +0 -0
- package/frames/iPhone 16 Pro Max Landscape.png +0 -0
- package/frames/iPhone 16 Pro Max Landscape_frame_no_island.png +0 -0
- package/frames/iPhone 16 Pro Max Landscape_mask.png +0 -0
- package/frames/iPhone 16 Pro Max Portrait.png +0 -0
- package/frames/iPhone 16 Pro Max Portrait_frame_no_island.png +0 -0
- package/frames/iPhone 16 Pro Max Portrait_mask.png +0 -0
- package/frames/iPhone 16 Pro Portrait.png +0 -0
- package/frames/iPhone 16 Pro Portrait_frame_no_island.png +0 -0
- package/frames/iPhone 16 Pro Portrait_mask.png +0 -0
- package/frames/iPhone 8 Plus Landscape.png +0 -0
- package/frames/iPhone 8 Plus Portrait.png +0 -0
- 2020 SE.png +0 -0
- package/frames/version.txt +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Chris Van Buskirk
|
|
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,381 @@
|
|
|
1
|
+
# Appshot 📸
|
|
2
|
+
|
|
3
|
+
> 🎉 **Now available on NPM!** Install with `npm install -g appshot-cli`
|
|
4
|
+
|
|
5
|
+
Generate beautiful, App Store-ready screenshots with device frames, gradients, and captions.
|
|
6
|
+
|
|
7
|
+
[](https://github.com/chrisvanbuskirk/appshot/actions/workflows/ci.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/appshot-cli)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- 🖼️ **Smart Frames** - Automatically detects portrait/landscape and selects appropriate device frame
|
|
14
|
+
- 🎨 **Gradients** - Beautiful gradient backgrounds with customizable colors
|
|
15
|
+
- ✏️ **Captions** - Add marketing text with full typography control (above or overlay)
|
|
16
|
+
- 🌍 **Localization** - Multi-language support for international app stores
|
|
17
|
+
- 📱 **Multi-Device** - Support for iPhone, iPad, Mac, Apple TV, Vision Pro, and Apple Watch
|
|
18
|
+
- 📏 **App Store Specs** - Built-in support for all official App Store screenshot resolutions
|
|
19
|
+
- ✅ **Validation** - Verify screenshots meet App Store requirements
|
|
20
|
+
- 🔄 **Orientation Detection** - Intelligently handles both portrait and landscape screenshots
|
|
21
|
+
- ⚡ **Fast** - Parallel processing with configurable concurrency
|
|
22
|
+
- 🛠️ **CLI** - Simple command-line interface
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g appshot-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> **Note**: The package is called `appshot-cli` on NPM, but the command is still `appshot`
|
|
33
|
+
|
|
34
|
+
### Initialize a new project
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
appshot init
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This creates:
|
|
41
|
+
- `appshot.json` - Configuration file
|
|
42
|
+
- `screenshots/` - Directory structure for your screenshots
|
|
43
|
+
- Device-specific folders with `captions.json` files
|
|
44
|
+
|
|
45
|
+
### Add your screenshots
|
|
46
|
+
|
|
47
|
+
Place your app screenshots in the appropriate device folders:
|
|
48
|
+
```
|
|
49
|
+
screenshots/
|
|
50
|
+
├── iphone/
|
|
51
|
+
├── ipad/
|
|
52
|
+
├── mac/
|
|
53
|
+
└── watch/
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Add captions
|
|
57
|
+
|
|
58
|
+
Use the interactive caption editor with autocomplete:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
appshot caption --device iphone
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Features:
|
|
65
|
+
- 🔍 **Autocomplete** - Smart suggestions as you type
|
|
66
|
+
- 📊 **Frequency tracking** - Most-used captions appear first
|
|
67
|
+
- 🎯 **Device-specific** - Suggestions tailored to device type
|
|
68
|
+
- ⌨️ **Keyboard shortcuts** - Tab to complete, arrows to navigate
|
|
69
|
+
|
|
70
|
+
### Build final screenshots
|
|
71
|
+
|
|
72
|
+
Generate your App Store-ready screenshots:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
appshot build
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Output appears in the `final/` directory, ready for upload!
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
Edit `appshot.json` to customize your screenshots:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"output": "./final",
|
|
87
|
+
"frames": "./frames",
|
|
88
|
+
"gradient": {
|
|
89
|
+
"colors": ["#FF5733", "#FFC300"],
|
|
90
|
+
"direction": "top-bottom"
|
|
91
|
+
},
|
|
92
|
+
"caption": {
|
|
93
|
+
"font": "SF Pro",
|
|
94
|
+
"fontsize": 64,
|
|
95
|
+
"color": "#FFFFFF",
|
|
96
|
+
"align": "center",
|
|
97
|
+
"paddingTop": 100
|
|
98
|
+
},
|
|
99
|
+
"devices": {
|
|
100
|
+
"iphone": {
|
|
101
|
+
"input": "./screenshots/iphone",
|
|
102
|
+
"resolution": "1284x2778",
|
|
103
|
+
"autoFrame": true,
|
|
104
|
+
"preferredFrame": "iphone-15-pro-max"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Gradient Options
|
|
111
|
+
|
|
112
|
+
- **colors**: Array of hex color codes
|
|
113
|
+
- **direction**: `top-bottom`, `bottom-top`, `left-right`, `right-left`, `diagonal`
|
|
114
|
+
|
|
115
|
+
### Caption Options
|
|
116
|
+
|
|
117
|
+
- **font**: Font family name
|
|
118
|
+
- **fontsize**: Size in pixels
|
|
119
|
+
- **color**: Hex color code
|
|
120
|
+
- **align**: `left`, `center`, `right`
|
|
121
|
+
- **paddingTop**: Space from top in pixels
|
|
122
|
+
- **paddingBottom**: Space from bottom in pixels
|
|
123
|
+
- **position**: `above` (above device frame) or `overlay` (on gradient)
|
|
124
|
+
|
|
125
|
+
### Device Options
|
|
126
|
+
|
|
127
|
+
- **input**: Directory containing screenshots
|
|
128
|
+
- **resolution**: Output resolution (use App Store specs or custom)
|
|
129
|
+
- **autoFrame**: Enable automatic frame selection based on screenshot orientation (default: true)
|
|
130
|
+
- **preferredFrame**: Preferred frame name from the registry (optional)
|
|
131
|
+
- **partialFrame**: Cut off bottom portion of frame for dynamic look (default: false)
|
|
132
|
+
- **frameOffset**: Percentage to cut off when partialFrame is true (default: 25)
|
|
133
|
+
|
|
134
|
+
## Commands
|
|
135
|
+
|
|
136
|
+
### `appshot init`
|
|
137
|
+
Initialize a new appshot project with scaffolding.
|
|
138
|
+
|
|
139
|
+
Options:
|
|
140
|
+
- `--force` - Overwrite existing files
|
|
141
|
+
|
|
142
|
+
### `appshot caption`
|
|
143
|
+
Interactively add or edit captions for screenshots with intelligent autocomplete.
|
|
144
|
+
|
|
145
|
+
Features:
|
|
146
|
+
- **Autocomplete suggestions** - Shows previous captions as you type
|
|
147
|
+
- **Fuzzy search** - Finds captions even with typos
|
|
148
|
+
- **Usage tracking** - Frequently used captions appear first
|
|
149
|
+
- **Learning system** - Improves suggestions over time
|
|
150
|
+
- **Device-specific** - Prioritizes captions used for the same device
|
|
151
|
+
|
|
152
|
+
Options:
|
|
153
|
+
- `--device <name>` - Device name (required)
|
|
154
|
+
- `--lang <code>` - Language code (default: en)
|
|
155
|
+
|
|
156
|
+
Keyboard shortcuts:
|
|
157
|
+
- **Tab** - Autocomplete the top suggestion
|
|
158
|
+
- **↑↓** - Navigate through suggestions
|
|
159
|
+
- **Enter** - Select current suggestion
|
|
160
|
+
- **Esc** - Dismiss suggestions
|
|
161
|
+
|
|
162
|
+
### `appshot build`
|
|
163
|
+
Generate final screenshots with frames, gradients, and captions.
|
|
164
|
+
|
|
165
|
+
Options:
|
|
166
|
+
- `--devices <list>` - Comma-separated device list
|
|
167
|
+
- `--preset <ids>` - Use specific App Store presets (e.g., iphone-6-9,ipad-13)
|
|
168
|
+
- `--config <file>` - Use specific config file (default: appshot.json)
|
|
169
|
+
- `--langs <list>` - Comma-separated language codes
|
|
170
|
+
- `--preview` - Generate low-res previews
|
|
171
|
+
- `--concurrency <n>` - Parallel processing limit
|
|
172
|
+
- `--no-frame` - Skip device frames
|
|
173
|
+
- `--no-gradient` - Skip gradient backgrounds
|
|
174
|
+
- `--no-caption` - Skip captions
|
|
175
|
+
|
|
176
|
+
### `appshot specs`
|
|
177
|
+
Display device specifications and resolutions.
|
|
178
|
+
|
|
179
|
+
### `appshot clean`
|
|
180
|
+
Remove generated screenshots and temporary files.
|
|
181
|
+
|
|
182
|
+
Options:
|
|
183
|
+
- `--all` - Remove all generated files including .appshot/ directory
|
|
184
|
+
- `--history` - Clear caption autocomplete history
|
|
185
|
+
- `--keep-history` - Preserve caption history when using --all
|
|
186
|
+
- `--yes` - Skip confirmation prompt
|
|
187
|
+
|
|
188
|
+
Options:
|
|
189
|
+
- `--device <name>` - Filter by device type
|
|
190
|
+
- `--json` - Output as JSON
|
|
191
|
+
|
|
192
|
+
### `appshot check`
|
|
193
|
+
Validate project configuration and assets.
|
|
194
|
+
|
|
195
|
+
Options:
|
|
196
|
+
- `--fix` - Attempt to fix issues automatically
|
|
197
|
+
|
|
198
|
+
### `appshot presets`
|
|
199
|
+
Manage App Store screenshot presets for all official resolutions.
|
|
200
|
+
|
|
201
|
+
Options:
|
|
202
|
+
- `--list` - List all available presets
|
|
203
|
+
- `--required` - Show only required presets for App Store submission
|
|
204
|
+
- `--generate <ids>` - Generate config for specific preset IDs (comma-separated)
|
|
205
|
+
- `--category <type>` - Filter by category (iphone, ipad, mac, appletv, visionpro, watch)
|
|
206
|
+
- `--output <file>` - Output file for generated config (default: appshot-presets.json)
|
|
207
|
+
|
|
208
|
+
### `appshot validate`
|
|
209
|
+
Validate screenshots against App Store requirements.
|
|
210
|
+
|
|
211
|
+
Options:
|
|
212
|
+
- `--strict` - Validate against required presets only
|
|
213
|
+
- `--fix` - Suggest fixes for invalid screenshots
|
|
214
|
+
|
|
215
|
+
### `appshot localize` (Coming Soon)
|
|
216
|
+
Generate translations for captions using AI providers.
|
|
217
|
+
|
|
218
|
+
Options:
|
|
219
|
+
- `--langs <codes>` - Target languages
|
|
220
|
+
- `--device <name>` - Specific device or all
|
|
221
|
+
- `--provider <name>` - Translation provider
|
|
222
|
+
|
|
223
|
+
## Multi-Language Support
|
|
224
|
+
|
|
225
|
+
Captions support multiple languages:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"home.png": {
|
|
230
|
+
"en": "Organize your life",
|
|
231
|
+
"fr": "Organisez votre vie",
|
|
232
|
+
"es": "Organiza tu vida"
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Build for specific languages:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
appshot build --langs en,fr,es
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## App Store Specifications
|
|
244
|
+
|
|
245
|
+
Appshot includes all official App Store screenshot resolutions. View them with:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
# Show all presets
|
|
249
|
+
appshot presets
|
|
250
|
+
|
|
251
|
+
# Show only required presets
|
|
252
|
+
appshot presets --required
|
|
253
|
+
|
|
254
|
+
# Generate config for specific devices
|
|
255
|
+
appshot presets --generate iphone-6-9,ipad-13,mac-2880,watch-ultra
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Required Resolutions
|
|
259
|
+
|
|
260
|
+
**iPhone** (choose one):
|
|
261
|
+
- **6.9" Display**: 1290×2796 (iPhone 16 Pro Max, 15 Pro Max, etc.)
|
|
262
|
+
- **6.5" Display**: 1284×2778 (iPhone 14 Plus, 13 Pro Max, etc.)
|
|
263
|
+
|
|
264
|
+
**iPad** (required):
|
|
265
|
+
- **13" Display**: 2064×2752 or 2048×2732
|
|
266
|
+
|
|
267
|
+
**Mac** (for Mac apps):
|
|
268
|
+
- **16:10 aspect ratio**: 2880×1800, 2560×1600, 1440×900, or 1280×800
|
|
269
|
+
|
|
270
|
+
**Apple Watch** (for Watch apps):
|
|
271
|
+
- **Ultra**: 410×502
|
|
272
|
+
- **Series 10**: 416×496
|
|
273
|
+
- **Series 9/8/7**: 396×484
|
|
274
|
+
|
|
275
|
+
### Quick Preset Usage
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
# Build with iPhone 6.9" and iPad 13" presets
|
|
279
|
+
appshot build --preset iphone-6-9,ipad-13
|
|
280
|
+
|
|
281
|
+
# Validate existing screenshots
|
|
282
|
+
appshot validate --strict
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Examples
|
|
286
|
+
|
|
287
|
+
See the `examples/minimal-project` directory for a complete example setup.
|
|
288
|
+
|
|
289
|
+
## Development
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
# Clone the repository
|
|
293
|
+
git clone https://github.com/chrisvanbuskirk/appshot.git
|
|
294
|
+
cd appshot
|
|
295
|
+
|
|
296
|
+
# Install dependencies
|
|
297
|
+
npm install
|
|
298
|
+
|
|
299
|
+
# Build
|
|
300
|
+
npm run build
|
|
301
|
+
|
|
302
|
+
# Run tests
|
|
303
|
+
npm test
|
|
304
|
+
|
|
305
|
+
# Link for local development
|
|
306
|
+
npm link
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Contributing
|
|
310
|
+
|
|
311
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
312
|
+
|
|
313
|
+
## Security
|
|
314
|
+
|
|
315
|
+
For security issues, please see [SECURITY.md](SECURITY.md).
|
|
316
|
+
|
|
317
|
+
## License
|
|
318
|
+
|
|
319
|
+
MIT © Chris Van Buskirk
|
|
320
|
+
|
|
321
|
+
## Advanced Features
|
|
322
|
+
|
|
323
|
+
### Smart Frame Selection
|
|
324
|
+
|
|
325
|
+
Appshot automatically detects whether your screenshots are portrait or landscape and selects the appropriate device frame:
|
|
326
|
+
|
|
327
|
+
- **iPhone screenshots** automatically use portrait frames for vertical shots and landscape frames for horizontal ones
|
|
328
|
+
- **iPad screenshots** work seamlessly in both orientations
|
|
329
|
+
- **Mac screenshots** always use landscape frames
|
|
330
|
+
- **Watch screenshots** always use portrait frames
|
|
331
|
+
|
|
332
|
+
Override automatic selection with `preferredFrame` in your device configuration.
|
|
333
|
+
|
|
334
|
+
### Partial Frames
|
|
335
|
+
|
|
336
|
+
Create dynamic App Store screenshots with partial device frames:
|
|
337
|
+
|
|
338
|
+
```json
|
|
339
|
+
{
|
|
340
|
+
"devices": {
|
|
341
|
+
"iphone": {
|
|
342
|
+
"partialFrame": true,
|
|
343
|
+
"frameOffset": 25 // Cut off bottom 25%
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Caption Positioning
|
|
350
|
+
|
|
351
|
+
Place captions above the device frame (recommended) or as an overlay:
|
|
352
|
+
|
|
353
|
+
```json
|
|
354
|
+
{
|
|
355
|
+
"caption": {
|
|
356
|
+
"position": "above", // or "overlay"
|
|
357
|
+
"paddingTop": 120,
|
|
358
|
+
"paddingBottom": 80
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Roadmap
|
|
364
|
+
|
|
365
|
+
- [x] Official App Store specifications support
|
|
366
|
+
- [x] Caption positioning (above/overlay)
|
|
367
|
+
- [x] Partial frame support
|
|
368
|
+
- [x] Intelligent caption autocomplete
|
|
369
|
+
- [x] Apple Watch optimizations
|
|
370
|
+
- [ ] AI-powered translations
|
|
371
|
+
- [ ] Cloud rendering service
|
|
372
|
+
- [ ] Android device support
|
|
373
|
+
- [ ] Custom frame designer
|
|
374
|
+
- [ ] Figma plugin
|
|
375
|
+
- [ ] Web dashboard
|
|
376
|
+
|
|
377
|
+
## Support
|
|
378
|
+
|
|
379
|
+
- [Report issues](https://github.com/chrisvanbuskirk/appshot/issues)
|
|
380
|
+
- [Request features](https://github.com/chrisvanbuskirk/appshot/issues/new?labels=enhancement)
|
|
381
|
+
- [Documentation](https://github.com/chrisvanbuskirk/appshot/wiki)
|
package/bin/appshot.js
ADDED
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import initCmd from './commands/init.js';
|
|
4
|
+
import captionCmd from './commands/caption.js';
|
|
5
|
+
import localizeCmd from './commands/localize.js';
|
|
6
|
+
import buildCmd from './commands/build.js';
|
|
7
|
+
import specsCmd from './commands/specs.js';
|
|
8
|
+
import checkCmd from './commands/check.js';
|
|
9
|
+
import presetsCmd from './commands/presets.js';
|
|
10
|
+
import validateCmd from './commands/validate.js';
|
|
11
|
+
import { createCleanCommand } from './commands/clean.js';
|
|
12
|
+
const program = new Command();
|
|
13
|
+
program
|
|
14
|
+
.name('appshot')
|
|
15
|
+
.description('Generate App Store–ready screenshots with frames, gradients, and captions.')
|
|
16
|
+
.version('0.1.0');
|
|
17
|
+
program.addCommand(initCmd());
|
|
18
|
+
program.addCommand(captionCmd());
|
|
19
|
+
program.addCommand(localizeCmd());
|
|
20
|
+
program.addCommand(buildCmd());
|
|
21
|
+
program.addCommand(specsCmd());
|
|
22
|
+
program.addCommand(checkCmd());
|
|
23
|
+
program.addCommand(presetsCmd());
|
|
24
|
+
program.addCommand(validateCmd());
|
|
25
|
+
program.addCommand(createCleanCommand());
|
|
26
|
+
program.showHelpAfterError(pc.dim('\nUse --help for usage.'));
|
|
27
|
+
program.parseAsync().catch((err) => {
|
|
28
|
+
console.error(pc.red('Error:'), err.message);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
31
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,OAAO,MAAM,oBAAoB,CAAC;AACzC,OAAO,UAAU,MAAM,uBAAuB,CAAC;AAC/C,OAAO,WAAW,MAAM,wBAAwB,CAAC;AACjD,OAAO,QAAQ,MAAM,qBAAqB,CAAC;AAC3C,OAAO,QAAQ,MAAM,qBAAqB,CAAC;AAC3C,OAAO,QAAQ,MAAM,qBAAqB,CAAC;AAC3C,OAAO,UAAU,MAAM,uBAAuB,CAAC;AAC/C,OAAO,WAAW,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,SAAS,CAAC;KACf,WAAW,CAAC,4EAA4E,CAAC;KACzF,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC;AAC9B,OAAO,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC,CAAC;AACjC,OAAO,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC;AAClC,OAAO,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAC;AAC/B,OAAO,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAC;AAC/B,OAAO,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAC;AAC/B,OAAO,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC,CAAC;AACjC,OAAO,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC;AAClC,OAAO,CAAC,UAAU,CAAC,kBAAkB,EAAE,CAAC,CAAC;AAEzC,OAAO,CAAC,kBAAkB,CAAC,EAAE,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC,CAAC;AAE9D,OAAO,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACjC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IAC7C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,MAAM,CAAC,OAAO,UAAU,QAAQ,YA6N/B"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import sharp from 'sharp';
|
|
6
|
+
import { loadConfig, loadCaptions } from '../core/files.js';
|
|
7
|
+
import { autoSelectFrame, getImageDimensions, initializeFrameRegistry } from '../core/devices.js';
|
|
8
|
+
import { composeAppStoreScreenshot } from '../core/compose.js';
|
|
9
|
+
export default function buildCmd() {
|
|
10
|
+
const cmd = new Command('build')
|
|
11
|
+
.description('Render screenshots using frames, gradients, and captions')
|
|
12
|
+
.option('--devices <list>', 'comma-separated device list (e.g., iphone,ipad)', 'iphone,ipad,mac,watch')
|
|
13
|
+
.option('--preset <ids>', 'use specific App Store presets (e.g., iphone-6-9,ipad-13)')
|
|
14
|
+
.option('--config <file>', 'use specific config file', 'appshot.json')
|
|
15
|
+
.option('--langs <list>', 'comma-separated language codes (e.g., en,fr,de)', 'en')
|
|
16
|
+
.option('--preview', 'generate low-res preview images')
|
|
17
|
+
.option('--concurrency <n>', 'number of parallel renders', '4')
|
|
18
|
+
.option('--no-frame', 'skip device frames')
|
|
19
|
+
.option('--no-gradient', 'skip gradient backgrounds')
|
|
20
|
+
.option('--no-caption', 'skip captions')
|
|
21
|
+
.action(async (opts) => {
|
|
22
|
+
try {
|
|
23
|
+
console.log(pc.bold('Building screenshots...'));
|
|
24
|
+
// Load configuration
|
|
25
|
+
const config = await loadConfig();
|
|
26
|
+
const devices = opts.devices.split(',').map((d) => d.trim());
|
|
27
|
+
const langs = opts.langs.split(',').map((l) => l.trim());
|
|
28
|
+
const concurrency = parseInt(opts.concurrency, 10);
|
|
29
|
+
// Initialize frame registry from Frames.json if available
|
|
30
|
+
await initializeFrameRegistry(path.resolve(config.frames));
|
|
31
|
+
// Ensure output directory exists
|
|
32
|
+
await fs.mkdir(config.output, { recursive: true });
|
|
33
|
+
let totalProcessed = 0;
|
|
34
|
+
let totalErrors = 0;
|
|
35
|
+
// Process each device
|
|
36
|
+
for (const device of devices) {
|
|
37
|
+
if (!config.devices[device]) {
|
|
38
|
+
console.log(pc.yellow('⚠'), `Device '${device}' not configured in appshot.json`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const deviceConfig = config.devices[device];
|
|
42
|
+
const inputDir = path.resolve(deviceConfig.input);
|
|
43
|
+
const outputDir = path.join(config.output, device);
|
|
44
|
+
// Check if input directory exists
|
|
45
|
+
try {
|
|
46
|
+
await fs.access(inputDir);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
console.log(pc.yellow('⚠'), `Input directory not found: ${inputDir}`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// Create device output directory
|
|
53
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
54
|
+
// Get screenshots
|
|
55
|
+
const screenshots = (await fs.readdir(inputDir))
|
|
56
|
+
.filter(f => f.match(/\.(png|jpg|jpeg)$/i))
|
|
57
|
+
.sort();
|
|
58
|
+
if (screenshots.length === 0) {
|
|
59
|
+
console.log(pc.yellow('⚠'), `No screenshots found in ${inputDir}`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// Load captions from .appshot/captions/
|
|
63
|
+
const captionsPath = path.join(process.cwd(), '.appshot', 'captions', `${device}.json`);
|
|
64
|
+
const captions = await loadCaptions(captionsPath);
|
|
65
|
+
console.log(pc.cyan(`\n${device}:`), `Processing ${screenshots.length} screenshots`);
|
|
66
|
+
// Process each language
|
|
67
|
+
for (const lang of langs) {
|
|
68
|
+
const langDir = langs.length > 1 ? path.join(outputDir, lang) : outputDir;
|
|
69
|
+
await fs.mkdir(langDir, { recursive: true });
|
|
70
|
+
// Process screenshots in batches
|
|
71
|
+
for (let i = 0; i < screenshots.length; i += concurrency) {
|
|
72
|
+
const batch = screenshots.slice(i, i + concurrency);
|
|
73
|
+
const promises = batch.map(async (screenshot) => {
|
|
74
|
+
try {
|
|
75
|
+
const inputPath = path.join(inputDir, screenshot);
|
|
76
|
+
const outputPath = path.join(langDir, screenshot);
|
|
77
|
+
// Get caption for this screenshot and language
|
|
78
|
+
const captionData = captions[screenshot];
|
|
79
|
+
let captionText = '';
|
|
80
|
+
if (typeof captionData === 'string') {
|
|
81
|
+
captionText = captionData;
|
|
82
|
+
}
|
|
83
|
+
else if (captionData && typeof captionData === 'object') {
|
|
84
|
+
captionText = captionData[lang] || '';
|
|
85
|
+
}
|
|
86
|
+
// Get screenshot dimensions and orientation
|
|
87
|
+
const { orientation } = await getImageDimensions(inputPath);
|
|
88
|
+
// Load screenshot
|
|
89
|
+
let screenshotBuffer;
|
|
90
|
+
try {
|
|
91
|
+
// First verify the file exists and is readable
|
|
92
|
+
await fs.access(inputPath, fs.constants.R_OK);
|
|
93
|
+
// Load the screenshot into a buffer
|
|
94
|
+
screenshotBuffer = await sharp(inputPath)
|
|
95
|
+
.png() // Ensure output is PNG
|
|
96
|
+
.toBuffer();
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.error(pc.red(` ✗ ${path.basename(inputPath)}`), `Failed to load screenshot: ${error instanceof Error ? error.message : String(error)}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Parse resolution for output dimensions
|
|
103
|
+
const [configWidth, configHeight] = deviceConfig.resolution.split('x').map(Number);
|
|
104
|
+
// Ensure output dimensions match screenshot orientation
|
|
105
|
+
let outWidth;
|
|
106
|
+
let outHeight;
|
|
107
|
+
if (orientation === 'portrait') {
|
|
108
|
+
// For portrait, ensure height > width
|
|
109
|
+
outWidth = Math.min(configWidth, configHeight);
|
|
110
|
+
outHeight = Math.max(configWidth, configHeight);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// For landscape, ensure width > height
|
|
114
|
+
outWidth = Math.max(configWidth, configHeight);
|
|
115
|
+
outHeight = Math.min(configWidth, configHeight);
|
|
116
|
+
}
|
|
117
|
+
// Warn if orientation mismatch detected
|
|
118
|
+
const configOrientation = configWidth > configHeight ? 'landscape' : 'portrait';
|
|
119
|
+
if (configOrientation !== orientation) {
|
|
120
|
+
console.log(pc.yellow(' ⚠'), pc.dim(`Config specifies ${configOrientation} (${deviceConfig.resolution}) but screenshot is ${orientation} - auto-adjusting`));
|
|
121
|
+
}
|
|
122
|
+
// Auto-select frame if enabled
|
|
123
|
+
let frame = null;
|
|
124
|
+
let frameMetadata = null;
|
|
125
|
+
let frameUsed = false;
|
|
126
|
+
if (opts.frame !== false && (deviceConfig.autoFrame !== false)) {
|
|
127
|
+
const result = await autoSelectFrame(inputPath, path.resolve(config.frames), device, deviceConfig.preferredFrame);
|
|
128
|
+
frame = result.frame;
|
|
129
|
+
frameMetadata = result.metadata;
|
|
130
|
+
if (frame && frameMetadata) {
|
|
131
|
+
frameUsed = true;
|
|
132
|
+
console.log(pc.dim(` Using ${frameMetadata.displayName} ${orientation} frame`));
|
|
133
|
+
}
|
|
134
|
+
else if (frameMetadata && !frame) {
|
|
135
|
+
console.error(pc.red(' ERROR: Frame metadata found but image failed to load!'));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Use the new compose function
|
|
139
|
+
let image;
|
|
140
|
+
try {
|
|
141
|
+
image = await composeAppStoreScreenshot({
|
|
142
|
+
screenshot: screenshotBuffer,
|
|
143
|
+
frame: frame,
|
|
144
|
+
frameMetadata: frameMetadata ? {
|
|
145
|
+
frameWidth: frameMetadata.frameWidth,
|
|
146
|
+
frameHeight: frameMetadata.frameHeight,
|
|
147
|
+
screenRect: frameMetadata.screenRect,
|
|
148
|
+
maskPath: frameMetadata.maskPath,
|
|
149
|
+
deviceType: frameMetadata.deviceType,
|
|
150
|
+
displayName: frameMetadata.displayName,
|
|
151
|
+
name: frameMetadata.name
|
|
152
|
+
} : undefined,
|
|
153
|
+
caption: opts.caption !== false ? captionText : undefined,
|
|
154
|
+
captionConfig: config.caption,
|
|
155
|
+
gradientConfig: config.gradient,
|
|
156
|
+
deviceConfig: deviceConfig,
|
|
157
|
+
outputWidth: outWidth,
|
|
158
|
+
outputHeight: outHeight
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
console.error(pc.red(` ✗ ${path.basename(inputPath)}`), error instanceof Error ? error.message : String(error));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Save final image
|
|
166
|
+
await sharp(image)
|
|
167
|
+
.resize(opts.preview ? 800 : undefined, undefined, {
|
|
168
|
+
fit: 'inside',
|
|
169
|
+
withoutEnlargement: true
|
|
170
|
+
})
|
|
171
|
+
.png() // Ensure output is PNG
|
|
172
|
+
.toFile(outputPath);
|
|
173
|
+
console.log(pc.green(' ✓'), path.basename(outputPath), pc.dim(`[${orientation}${frameUsed ? ', framed' : ''}${captionText ? ', captioned' : ''}]`));
|
|
174
|
+
totalProcessed++;
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
console.log(pc.red(' ✗'), screenshot, pc.dim(error instanceof Error ? error.message : String(error)));
|
|
178
|
+
totalErrors++;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
await Promise.all(promises);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Summary
|
|
186
|
+
console.log('\n' + pc.bold('Build complete!'));
|
|
187
|
+
console.log(pc.green(`✓ ${totalProcessed} screenshots processed`));
|
|
188
|
+
if (totalErrors > 0) {
|
|
189
|
+
console.log(pc.red(`✗ ${totalErrors} errors`));
|
|
190
|
+
}
|
|
191
|
+
console.log(pc.dim(`Output directory: ${config.output}`));
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : String(error));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return cmd;
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=build.js.map
|