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.
- package/.claude/settings.local.json +30 -0
- package/.env.example +3 -0
- package/MCP_README.md +200 -0
- package/README.md +148 -0
- package/bin/screencraft.js +61 -0
- package/package.json +31 -0
- package/src/auth/keystore.js +148 -0
- package/src/commands/init.js +119 -0
- package/src/commands/launch.js +405 -0
- package/src/detectors/detectBrand.js +1222 -0
- package/src/detectors/simulator.js +317 -0
- package/src/generators/analyzeStyleReference.js +471 -0
- package/src/generators/compositePSD.js +682 -0
- package/src/generators/copy.js +147 -0
- package/src/mcp/index.js +394 -0
- package/src/pipeline/aeSwap.js +369 -0
- package/src/pipeline/download.js +32 -0
- package/src/pipeline/queue.js +101 -0
- package/src/server/index.js +627 -0
- package/src/server/public/app.js +738 -0
- package/src/server/public/index.html +255 -0
- package/src/server/public/style.css +751 -0
- package/src/server/session.js +36 -0
- package/templates/ae/(Footage)/Assets/This Hip-Hop Upbeat (Short version).wav +0 -0
- package/templates/ae/(Footage)/Assets/screen_01_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_02_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_03_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_04_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_05_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_06_raw.png +0 -0
- package/templates/ae/Motion Forge Test 1.0 (converted).aep +0 -0
- package/templates/ae_swap.jsx +284 -0
- package/templates/layouts/minimal.psd +0 -0
- package/templates/screencraft.config.example.js +165 -0
- package/test/output/layout_test.png +0 -0
- package/test/output/style_profile.json +64 -0
- package/test/reference.png +0 -0
- package/test/test_brand.js +69 -0
- package/test/test_psd.js +83 -0
- 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
|
+
}
|