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.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +381 -0
  3. package/bin/appshot.js +5 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +31 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/commands/build.d.ts +3 -0
  9. package/dist/commands/build.d.ts.map +1 -0
  10. package/dist/commands/build.js +200 -0
  11. package/dist/commands/build.js.map +1 -0
  12. package/dist/commands/caption.d.ts +3 -0
  13. package/dist/commands/caption.d.ts.map +1 -0
  14. package/dist/commands/caption.js +118 -0
  15. package/dist/commands/caption.js.map +1 -0
  16. package/dist/commands/check.d.ts +3 -0
  17. package/dist/commands/check.d.ts.map +1 -0
  18. package/dist/commands/check.js +121 -0
  19. package/dist/commands/check.js.map +1 -0
  20. package/dist/commands/clean.d.ts +3 -0
  21. package/dist/commands/clean.d.ts.map +1 -0
  22. package/dist/commands/clean.js +133 -0
  23. package/dist/commands/clean.js.map +1 -0
  24. package/dist/commands/init.d.ts +3 -0
  25. package/dist/commands/init.d.ts.map +1 -0
  26. package/dist/commands/init.js +84 -0
  27. package/dist/commands/init.js.map +1 -0
  28. package/dist/commands/localize.d.ts +3 -0
  29. package/dist/commands/localize.d.ts.map +1 -0
  30. package/dist/commands/localize.js +31 -0
  31. package/dist/commands/localize.js.map +1 -0
  32. package/dist/commands/presets.d.ts +3 -0
  33. package/dist/commands/presets.d.ts.map +1 -0
  34. package/dist/commands/presets.js +135 -0
  35. package/dist/commands/presets.js.map +1 -0
  36. package/dist/commands/specs.d.ts +3 -0
  37. package/dist/commands/specs.d.ts.map +1 -0
  38. package/dist/commands/specs.js +69 -0
  39. package/dist/commands/specs.js.map +1 -0
  40. package/dist/commands/validate.d.ts +3 -0
  41. package/dist/commands/validate.d.ts.map +1 -0
  42. package/dist/commands/validate.js +183 -0
  43. package/dist/commands/validate.js.map +1 -0
  44. package/dist/core/app-store-specs.d.ts +55 -0
  45. package/dist/core/app-store-specs.d.ts.map +1 -0
  46. package/dist/core/app-store-specs.js +457 -0
  47. package/dist/core/app-store-specs.js.map +1 -0
  48. package/dist/core/compose.d.ts +30 -0
  49. package/dist/core/compose.d.ts.map +1 -0
  50. package/dist/core/compose.js +420 -0
  51. package/dist/core/compose.js.map +1 -0
  52. package/dist/core/devices.d.ts +50 -0
  53. package/dist/core/devices.d.ts.map +1 -0
  54. package/dist/core/devices.js +404 -0
  55. package/dist/core/devices.js.map +1 -0
  56. package/dist/core/files.d.ts +5 -0
  57. package/dist/core/files.d.ts.map +1 -0
  58. package/dist/core/files.js +35 -0
  59. package/dist/core/files.js.map +1 -0
  60. package/dist/core/frames-analyzer.d.ts +24 -0
  61. package/dist/core/frames-analyzer.d.ts.map +1 -0
  62. package/dist/core/frames-analyzer.js +97 -0
  63. package/dist/core/frames-analyzer.js.map +1 -0
  64. package/dist/core/frames-loader.d.ts +113 -0
  65. package/dist/core/frames-loader.d.ts.map +1 -0
  66. package/dist/core/frames-loader.js +440 -0
  67. package/dist/core/frames-loader.js.map +1 -0
  68. package/dist/core/mask-generator.d.ts +10 -0
  69. package/dist/core/mask-generator.d.ts.map +1 -0
  70. package/dist/core/mask-generator.js +99 -0
  71. package/dist/core/mask-generator.js.map +1 -0
  72. package/dist/core/render.d.ts +10 -0
  73. package/dist/core/render.d.ts.map +1 -0
  74. package/dist/core/render.js +92 -0
  75. package/dist/core/render.js.map +1 -0
  76. package/dist/core/text-renderer.d.ts +18 -0
  77. package/dist/core/text-renderer.d.ts.map +1 -0
  78. package/dist/core/text-renderer.js +148 -0
  79. package/dist/core/text-renderer.js.map +1 -0
  80. package/dist/types.d.ts +57 -0
  81. package/dist/types.d.ts.map +1 -0
  82. package/dist/types.js +2 -0
  83. package/dist/types.js.map +1 -0
  84. package/dist/utils/caption-history.d.ts +35 -0
  85. package/dist/utils/caption-history.d.ts.map +1 -0
  86. package/dist/utils/caption-history.js +233 -0
  87. package/dist/utils/caption-history.js.map +1 -0
  88. package/frames/Frames.json +308 -0
  89. package/frames/MacBook Air 2020.png +0 -0
  90. package/frames/MacBook Air 2022.png +0 -0
  91. package/frames/MacBook Pro 13.png +0 -0
  92. package/frames/MacBook Pro 2021 14.png +0 -0
  93. package/frames/MacBook Pro 2021 16.png +0 -0
  94. package/frames/Watch Series 10 42.png +0 -0
  95. package/frames/Watch Series 10 42_mask.png +0 -0
  96. package/frames/Watch Series 10 46.png +0 -0
  97. package/frames/Watch Series 10 46_mask.png +0 -0
  98. package/frames/Watch Series 4 40.png +0 -0
  99. package/frames/Watch Series 4 44.png +0 -0
  100. package/frames/Watch Series 7 41.png +0 -0
  101. package/frames/Watch Series 7 45.png +0 -0
  102. package/frames/Watch Ultra 2024.png +0 -0
  103. package/frames/iMac 2021.png +0 -0
  104. package/frames/iPad 2021 Landscape.png +0 -0
  105. package/frames/iPad 2021 Portrait.png +0 -0
  106. package/frames/iPad Air 2020 Landscape.png +0 -0
  107. package/frames/iPad Air 2020 Portrait.png +0 -0
  108. package/frames/iPad Pro 2018-2021 11 Landscape.png +0 -0
  109. package/frames/iPad Pro 2018-2021 11 Portrait.png +0 -0
  110. package/frames/iPad Pro 2018-2021 Landscape.png +0 -0
  111. package/frames/iPad Pro 2018-2021 Portrait.png +0 -0
  112. package/frames/iPad Pro 2024 11 Landscape.png +0 -0
  113. package/frames/iPad Pro 2024 11 Portrait.png +0 -0
  114. package/frames/iPad Pro 2024 13 Landscape.png +0 -0
  115. package/frames/iPad Pro 2024 13 Portrait.png +0 -0
  116. package/frames/iPad mini 2021 Landscape.png +0 -0
  117. package/frames/iPad mini 2021 Portrait.png +0 -0
  118. package/frames/iPhone 11 Landscape.png +0 -0
  119. package/frames/iPhone 11 Portrait.png +0 -0
  120. package/frames/iPhone 11 Pro Max Landscape.png +0 -0
  121. package/frames/iPhone 11 Pro Max Portrait.png +0 -0
  122. package/frames/iPhone 11 Pro Portrait.png +0 -0
  123. package/frames/iPhone 12-13 Pro Landscape.png +0 -0
  124. package/frames/iPhone 12-13 Pro Max Landscape.png +0 -0
  125. package/frames/iPhone 12-13 Pro Max Landscape_mask.png +0 -0
  126. package/frames/iPhone 12-13 Pro Max Portrait.png +0 -0
  127. package/frames/iPhone 12-13 Pro Max Portrait_mask.png +0 -0
  128. package/frames/iPhone 12-13 Pro Portrait.png +0 -0
  129. package/frames/iPhone 12-13 mini Landscape.png +0 -0
  130. package/frames/iPhone 12-13 mini Portrait.png +0 -0
  131. package/frames/iPhone 16 Landscape.png +0 -0
  132. package/frames/iPhone 16 Landscape_frame_no_island.png +0 -0
  133. package/frames/iPhone 16 Landscape_mask.png +0 -0
  134. package/frames/iPhone 16 Plus Landscape.png +0 -0
  135. package/frames/iPhone 16 Plus Landscape_frame_no_island.png +0 -0
  136. package/frames/iPhone 16 Plus Landscape_mask.png +0 -0
  137. package/frames/iPhone 16 Plus Portrait.png +0 -0
  138. package/frames/iPhone 16 Plus Portrait_frame_no_island.png +0 -0
  139. package/frames/iPhone 16 Plus Portrait_mask.png +0 -0
  140. package/frames/iPhone 16 Portrait.png +0 -0
  141. package/frames/iPhone 16 Portrait_frame_no_island.png +0 -0
  142. package/frames/iPhone 16 Portrait_mask.png +0 -0
  143. package/frames/iPhone 16 Pro Landscape.png +0 -0
  144. package/frames/iPhone 16 Pro Landscape_frame_no_island.png +0 -0
  145. package/frames/iPhone 16 Pro Landscape_mask.png +0 -0
  146. package/frames/iPhone 16 Pro Max Landscape.png +0 -0
  147. package/frames/iPhone 16 Pro Max Landscape_frame_no_island.png +0 -0
  148. package/frames/iPhone 16 Pro Max Landscape_mask.png +0 -0
  149. package/frames/iPhone 16 Pro Max Portrait.png +0 -0
  150. package/frames/iPhone 16 Pro Max Portrait_frame_no_island.png +0 -0
  151. package/frames/iPhone 16 Pro Max Portrait_mask.png +0 -0
  152. package/frames/iPhone 16 Pro Portrait.png +0 -0
  153. package/frames/iPhone 16 Pro Portrait_frame_no_island.png +0 -0
  154. package/frames/iPhone 16 Pro Portrait_mask.png +0 -0
  155. package/frames/iPhone 8 Plus Landscape.png +0 -0
  156. package/frames/iPhone 8 Plus Portrait.png +0 -0
  157. 2020 SE.png +0 -0
  158. package/frames/version.txt +1 -0
  159. 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
+ [![CI](https://github.com/chrisvanbuskirk/appshot/actions/workflows/ci.yml/badge.svg)](https://github.com/chrisvanbuskirk/appshot/actions/workflows/ci.yml)
8
+ [![npm version](https://badge.fury.io/js/appshot-cli.svg)](https://www.npmjs.com/package/appshot-cli)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli.js').catch(err => {
3
+ console.error(err);
4
+ process.exit(1);
5
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli.d.ts.map
@@ -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
@@ -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,3 @@
1
+ import { Command } from 'commander';
2
+ export default function buildCmd(): Command;
3
+ //# sourceMappingURL=build.d.ts.map
@@ -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