screencraft 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 (40) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/.env.example +3 -0
  3. package/MCP_README.md +200 -0
  4. package/README.md +148 -0
  5. package/bin/screencraft.js +61 -0
  6. package/package.json +31 -0
  7. package/src/auth/keystore.js +148 -0
  8. package/src/commands/init.js +119 -0
  9. package/src/commands/launch.js +405 -0
  10. package/src/detectors/detectBrand.js +1222 -0
  11. package/src/detectors/simulator.js +317 -0
  12. package/src/generators/analyzeStyleReference.js +471 -0
  13. package/src/generators/compositePSD.js +682 -0
  14. package/src/generators/copy.js +147 -0
  15. package/src/mcp/index.js +394 -0
  16. package/src/pipeline/aeSwap.js +369 -0
  17. package/src/pipeline/download.js +32 -0
  18. package/src/pipeline/queue.js +101 -0
  19. package/src/server/index.js +627 -0
  20. package/src/server/public/app.js +738 -0
  21. package/src/server/public/index.html +255 -0
  22. package/src/server/public/style.css +751 -0
  23. package/src/server/session.js +36 -0
  24. package/templates/ae/(Footage)/Assets/This Hip-Hop Upbeat (Short version).wav +0 -0
  25. package/templates/ae/(Footage)/Assets/screen_01_raw.png +0 -0
  26. package/templates/ae/(Footage)/Assets/screen_02_raw.png +0 -0
  27. package/templates/ae/(Footage)/Assets/screen_03_raw.png +0 -0
  28. package/templates/ae/(Footage)/Assets/screen_04_raw.png +0 -0
  29. package/templates/ae/(Footage)/Assets/screen_05_raw.png +0 -0
  30. package/templates/ae/(Footage)/Assets/screen_06_raw.png +0 -0
  31. package/templates/ae/Motion Forge Test 1.0 (converted).aep +0 -0
  32. package/templates/ae_swap.jsx +284 -0
  33. package/templates/layouts/minimal.psd +0 -0
  34. package/templates/screencraft.config.example.js +165 -0
  35. package/test/output/layout_test.png +0 -0
  36. package/test/output/style_profile.json +64 -0
  37. package/test/reference.png +0 -0
  38. package/test/test_brand.js +69 -0
  39. package/test/test_psd.js +83 -0
  40. package/test/test_style_analysis.js +114 -0
@@ -0,0 +1,317 @@
1
+ /**
2
+ * src/detectors/simulator.js
3
+ * --------------------------
4
+ * Captures screenshots from the iOS or Android simulator.
5
+ *
6
+ * Three modes (checked in order):
7
+ * 1. Manual folder — ./screenshots/ already has PNGs → use those
8
+ * 2. Interactive — simulator is running, user navigates + presses Enter
9
+ * 3. Deep-link auto — config defines routes, tool navigates automatically
10
+ *
11
+ * iOS: uses xcrun simctl
12
+ * Android: uses adb
13
+ */
14
+
15
+ const { execSync } = require('child_process');
16
+ const path = require('path');
17
+ const fs = require('fs');
18
+ const readline = require('readline');
19
+
20
+ const MAX_SCREENSHOTS = 6;
21
+
22
+ // ─────────────────────────────────────────────────────────────────
23
+ // MAIN EXPORT
24
+ // ─────────────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * captureScreenshots(root, framework, opts)
28
+ *
29
+ * opts.screenConfigs — array of { route, label, timeout } for auto-nav mode
30
+ * opts.outputDir — where to save captured PNGs
31
+ * opts.log — { info, success, warn } log helpers
32
+ *
33
+ * Returns array of { index, file, label }
34
+ */
35
+ async function captureScreenshots(root, framework, opts = {}) {
36
+ const { screenConfigs, outputDir, log } = opts;
37
+
38
+ const screenshotDir = outputDir
39
+ ? path.join(outputDir, 'screenshots')
40
+ : path.join(root, 'launch-kit', 'screenshots');
41
+ fs.mkdirSync(screenshotDir, { recursive: true });
42
+
43
+ // Mode 1: manual screenshots folder
44
+ const manualDir = path.join(root, 'screenshots');
45
+ if (fs.existsSync(manualDir)) {
46
+ const files = fs.readdirSync(manualDir)
47
+ .filter(f => /\.(png|jpg|jpeg)$/i.test(f))
48
+ .sort()
49
+ .slice(0, MAX_SCREENSHOTS);
50
+
51
+ if (files.length > 0) {
52
+ if (log) log.success(`Found ${files.length} screenshots in ./screenshots/`);
53
+ return files.map((f, i) => ({
54
+ index: i + 1,
55
+ file: path.join(manualDir, f),
56
+ label: null,
57
+ }));
58
+ }
59
+ }
60
+
61
+ // Mode 2 & 3: simulator capture
62
+ if (framework.platform.includes('ios')) {
63
+ return captureIOS(root, screenshotDir, screenConfigs, log);
64
+ } else if (framework.platform.includes('android')) {
65
+ return captureAndroid(root, screenshotDir, screenConfigs, log);
66
+ }
67
+
68
+ throw new Error(
69
+ 'No screenshots found and no simulator available.\n' +
70
+ 'Place PNGs in a ./screenshots/ folder, or run the iOS/Android simulator first.'
71
+ );
72
+ }
73
+
74
+ module.exports = { captureScreenshots };
75
+
76
+
77
+ // ─────────────────────────────────────────────────────────────────
78
+ // iOS CAPTURE
79
+ // ─────────────────────────────────────────────────────────────────
80
+
81
+ async function captureIOS(root, outputDir, screenConfigs, log) {
82
+ // Find a booted simulator, or boot one
83
+ const device = ensureIOSSimulator(log);
84
+
85
+ // If we have route configs, use auto-nav mode
86
+ if (screenConfigs?.length) {
87
+ return captureIOSAutoNav(device, outputDir, screenConfigs, log);
88
+ }
89
+
90
+ // Otherwise: interactive capture
91
+ return captureIOSInteractive(device, outputDir, log);
92
+ }
93
+
94
+ /**
95
+ * Ensure an iOS simulator is booted. Returns the device UDID.
96
+ */
97
+ function ensureIOSSimulator(log) {
98
+ // Check if one is already booted
99
+ try {
100
+ const list = execSync('xcrun simctl list devices booted -j', { encoding: 'utf8' });
101
+ const json = JSON.parse(list);
102
+ for (const runtime of Object.values(json.devices)) {
103
+ for (const device of runtime) {
104
+ if (device.state === 'Booted') {
105
+ if (log) log.success(`Simulator running: ${device.name}`);
106
+ return device.udid;
107
+ }
108
+ }
109
+ }
110
+ } catch {}
111
+
112
+ // None booted — try to boot an iPhone
113
+ if (log) log.info('No simulator running. Booting one...');
114
+
115
+ const preferred = [
116
+ 'iPhone 16 Pro', 'iPhone 16', 'iPhone 15 Pro', 'iPhone 15',
117
+ 'iPhone 16 Pro Max', 'iPhone 15 Pro Max',
118
+ ];
119
+
120
+ for (const name of preferred) {
121
+ try {
122
+ execSync(`xcrun simctl boot "${name}" 2>/dev/null`);
123
+ execSync('open -a Simulator 2>/dev/null');
124
+ if (log) log.success(`Booted: ${name}`);
125
+ // Wait for it to finish booting
126
+ sleepSync(4000);
127
+ // Get the UDID
128
+ const list = execSync('xcrun simctl list devices booted -j', { encoding: 'utf8' });
129
+ const json = JSON.parse(list);
130
+ for (const runtime of Object.values(json.devices)) {
131
+ for (const device of runtime) {
132
+ if (device.state === 'Booted') return device.udid;
133
+ }
134
+ }
135
+ } catch {}
136
+ }
137
+
138
+ throw new Error(
139
+ 'Could not boot a simulator. Open Xcode → run your app in the Simulator first,\n' +
140
+ 'then re-run screencraft launch.'
141
+ );
142
+ }
143
+
144
+ /**
145
+ * Interactive capture: user navigates the app, presses Enter to capture each screen.
146
+ */
147
+ async function captureIOSInteractive(device, outputDir, log) {
148
+ const results = [];
149
+
150
+ console.log('');
151
+ console.log(' ┌─────────────────────────────────────────────┐');
152
+ console.log(' │ Navigate your app in the Simulator. │');
153
+ console.log(' │ Press Enter here to capture each screen. │');
154
+ console.log(' │ Press D then Enter when done. │');
155
+ console.log(' │ (Up to 6 screenshots) │');
156
+ console.log(' └─────────────────────────────────────────────┘');
157
+ console.log('');
158
+
159
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
160
+
161
+ const ask = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
162
+
163
+ // Capture first screen automatically (whatever's showing)
164
+ const firstFile = path.join(outputDir, 'screen_01_raw.png');
165
+ execSync(`xcrun simctl io "${device}" screenshot "${firstFile}"`);
166
+ results.push({ index: 1, file: firstFile, label: null });
167
+ if (log) log.success('Screen 1 captured (current view)');
168
+
169
+ // Interactive loop for remaining screens
170
+ for (let i = 2; i <= MAX_SCREENSHOTS; i++) {
171
+ const input = await ask(` Navigate to screen ${i}, then press Enter (or D to finish): `);
172
+
173
+ if (input.trim().toLowerCase() === 'd') break;
174
+
175
+ // Small delay to let any animation finish
176
+ await sleep(300);
177
+
178
+ const outFile = path.join(outputDir, `screen_${String(i).padStart(2, '0')}_raw.png`);
179
+ execSync(`xcrun simctl io "${device}" screenshot "${outFile}"`);
180
+ results.push({ index: i, file: outFile, label: null });
181
+ if (log) log.success(`Screen ${i} captured`);
182
+ }
183
+
184
+ rl.close();
185
+ console.log('');
186
+ if (log) log.success(`${results.length} screenshot${results.length > 1 ? 's' : ''} captured`);
187
+
188
+ return results;
189
+ }
190
+
191
+ /**
192
+ * Auto-nav capture: config defines routes, tool deep-links to each one.
193
+ */
194
+ async function captureIOSAutoNav(device, outputDir, screenConfigs, log) {
195
+ const results = [];
196
+
197
+ for (let i = 0; i < screenConfigs.length; i++) {
198
+ const cfg = screenConfigs[i];
199
+ const outFile = path.join(outputDir, `screen_${String(i + 1).padStart(2, '0')}_raw.png`);
200
+
201
+ if (cfg.route) {
202
+ try {
203
+ execSync(`xcrun simctl openurl "${device}" "${cfg.route}" 2>/dev/null || true`);
204
+ await sleep(cfg.timeout || 2500);
205
+ } catch {}
206
+ }
207
+
208
+ execSync(`xcrun simctl io "${device}" screenshot "${outFile}"`);
209
+ results.push({ index: i + 1, file: outFile, label: cfg.label || null });
210
+
211
+ if (log) log.success(`Screen ${i + 1} captured${cfg.label ? ` (${cfg.label})` : ''}`);
212
+ }
213
+
214
+ return results;
215
+ }
216
+
217
+
218
+ // ─────────────────────────────────────────────────────────────────
219
+ // ANDROID CAPTURE
220
+ // ─────────────────────────────────────────────────────────────────
221
+
222
+ async function captureAndroid(root, outputDir, screenConfigs, log) {
223
+ // Check ADB is available and a device is connected
224
+ try {
225
+ const devices = execSync('adb devices', { encoding: 'utf8' });
226
+ if (!devices.includes('\tdevice')) {
227
+ throw new Error('No Android device/emulator found. Run an emulator first.');
228
+ }
229
+ } catch (err) {
230
+ throw new Error('adb not found. Install Android SDK platform-tools.');
231
+ }
232
+
233
+ if (log) log.success('Android device connected');
234
+
235
+ // If we have route configs, use auto-nav
236
+ if (screenConfigs?.length) {
237
+ return captureAndroidAutoNav(outputDir, screenConfigs, log);
238
+ }
239
+
240
+ // Otherwise: interactive
241
+ return captureAndroidInteractive(outputDir, log);
242
+ }
243
+
244
+ async function captureAndroidInteractive(outputDir, log) {
245
+ const results = [];
246
+
247
+ console.log('');
248
+ console.log(' ┌─────────────────────────────────────────────┐');
249
+ console.log(' │ Navigate your app on the emulator. │');
250
+ console.log(' │ Press Enter here to capture each screen. │');
251
+ console.log(' │ Press D then Enter when done. │');
252
+ console.log(' │ (Up to 6 screenshots) │');
253
+ console.log(' └─────────────────────────────────────────────┘');
254
+ console.log('');
255
+
256
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
257
+ const ask = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
258
+
259
+ // Capture first screen
260
+ const firstFile = path.join(outputDir, 'screen_01_raw.png');
261
+ execSync(`adb exec-out screencap -p > "${firstFile}"`);
262
+ results.push({ index: 1, file: firstFile, label: null });
263
+ if (log) log.success('Screen 1 captured (current view)');
264
+
265
+ for (let i = 2; i <= MAX_SCREENSHOTS; i++) {
266
+ const input = await ask(` Navigate to screen ${i}, then press Enter (or D to finish): `);
267
+ if (input.trim().toLowerCase() === 'd') break;
268
+
269
+ await sleep(300);
270
+
271
+ const outFile = path.join(outputDir, `screen_${String(i).padStart(2, '0')}_raw.png`);
272
+ execSync(`adb exec-out screencap -p > "${outFile}"`);
273
+ results.push({ index: i, file: outFile, label: null });
274
+ if (log) log.success(`Screen ${i} captured`);
275
+ }
276
+
277
+ rl.close();
278
+ console.log('');
279
+ if (log) log.success(`${results.length} screenshot${results.length > 1 ? 's' : ''} captured`);
280
+
281
+ return results;
282
+ }
283
+
284
+ async function captureAndroidAutoNav(outputDir, screenConfigs, log) {
285
+ const results = [];
286
+
287
+ for (let i = 0; i < screenConfigs.length; i++) {
288
+ const cfg = screenConfigs[i];
289
+ const outFile = path.join(outputDir, `screen_${String(i + 1).padStart(2, '0')}_raw.png`);
290
+
291
+ if (cfg.route) {
292
+ try {
293
+ execSync(`adb shell am start -a android.intent.action.VIEW -d "${cfg.route}" 2>/dev/null`);
294
+ await sleep(cfg.timeout || 3000);
295
+ } catch {}
296
+ }
297
+
298
+ execSync(`adb exec-out screencap -p > "${outFile}"`);
299
+ results.push({ index: i + 1, file: outFile, label: cfg.label || null });
300
+ if (log) log.success(`Screen ${i + 1} captured${cfg.label ? ` (${cfg.label})` : ''}`);
301
+ }
302
+
303
+ return results;
304
+ }
305
+
306
+
307
+ // ─────────────────────────────────────────────────────────────────
308
+ // HELPERS
309
+ // ─────────────────────────────────────────────────────────────────
310
+
311
+ function sleep(ms) {
312
+ return new Promise(resolve => setTimeout(resolve, ms));
313
+ }
314
+
315
+ function sleepSync(ms) {
316
+ execSync(`sleep ${ms / 1000}`);
317
+ }