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,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/commands/init.js
|
|
3
|
+
* --------------------
|
|
4
|
+
* Generates screencraft.config.js and .mcp.json in the project root.
|
|
5
|
+
* Auto-fills detected values where possible.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
|
|
12
|
+
const { detectFramework, detectBrand } = require('../detectors/detectBrand');
|
|
13
|
+
|
|
14
|
+
module.exports = async function init() {
|
|
15
|
+
const ROOT = process.cwd();
|
|
16
|
+
|
|
17
|
+
// ── Detect what we can ────────────────────────────────────────
|
|
18
|
+
let framework = { name: 'unknown', platform: 'ios' };
|
|
19
|
+
let brand = {};
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
console.log(chalk.dim(' Detecting project...'));
|
|
23
|
+
framework = await detectFramework(ROOT);
|
|
24
|
+
brand = await detectBrand(ROOT, {});
|
|
25
|
+
console.log(chalk.hex('#6BC46A')(` ✓ Detected: ${framework.name}`));
|
|
26
|
+
} catch {
|
|
27
|
+
console.log(chalk.dim(' Could not auto-detect framework — using defaults'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── screencraft.config.js ─────────────────────────────────────
|
|
31
|
+
const configPath = path.join(ROOT, 'screencraft.config.js');
|
|
32
|
+
|
|
33
|
+
if (fs.existsSync(configPath)) {
|
|
34
|
+
console.log(chalk.dim(' screencraft.config.js already exists — skipping'));
|
|
35
|
+
} else {
|
|
36
|
+
const configContent = `/**
|
|
37
|
+
* screencraft.config.js
|
|
38
|
+
* ---------------------
|
|
39
|
+
* All fields are optional. ScreenCraft auto-detects everything it can.
|
|
40
|
+
* Override here if detection is wrong or you want different values.
|
|
41
|
+
*
|
|
42
|
+
* Docs: https://screencraft.dev/docs
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
app: {
|
|
47
|
+
name: ${brand.appName ? `"${brand.appName}"` : 'null'}, // App display name
|
|
48
|
+
tagline: null, // One-line tagline (used in video title card)
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
brand: {
|
|
52
|
+
primary: ${brand.primary ? `"${brand.primary}"` : 'null'}, // e.g. "#1B3A6B" — null = auto-detect
|
|
53
|
+
secondary: ${brand.secondary ? `"${brand.secondary}"` : 'null'},
|
|
54
|
+
accent: ${brand.accent ? `"${brand.accent}"` : 'null'},
|
|
55
|
+
background: ${brand.background ? `"${brand.background}"` : 'null'},
|
|
56
|
+
icon: null, // Path to app icon PNG (1024x1024). null = auto-detect
|
|
57
|
+
font: null, // e.g. "Inter" — null = auto-detect
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
screenshots: [
|
|
61
|
+
// Define screens for auto-capture (optional).
|
|
62
|
+
// If omitted, use the web UI to capture interactively.
|
|
63
|
+
// { route: "/home", label: "Home screen" },
|
|
64
|
+
// { route: "/settings", label: "Settings" },
|
|
65
|
+
],
|
|
66
|
+
|
|
67
|
+
output: {
|
|
68
|
+
dir: "./launch-kit", // Output directory
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
`;
|
|
72
|
+
fs.writeFileSync(configPath, configContent);
|
|
73
|
+
console.log(chalk.hex('#6BC46A')(' ✓ Created screencraft.config.js'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── .mcp.json ─────────────────────────────────────────────────
|
|
77
|
+
const mcpPath = path.join(ROOT, '.mcp.json');
|
|
78
|
+
|
|
79
|
+
if (fs.existsSync(mcpPath)) {
|
|
80
|
+
// Check if screencraft is already in it
|
|
81
|
+
try {
|
|
82
|
+
const existing = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
|
|
83
|
+
if (existing.mcpServers?.screencraft) {
|
|
84
|
+
console.log(chalk.dim(' .mcp.json already has screencraft — skipping'));
|
|
85
|
+
} else {
|
|
86
|
+
// Add screencraft to existing config
|
|
87
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
88
|
+
existing.mcpServers.screencraft = {
|
|
89
|
+
type: 'stdio',
|
|
90
|
+
command: 'npx',
|
|
91
|
+
args: ['screencraft', '--mcp'],
|
|
92
|
+
};
|
|
93
|
+
fs.writeFileSync(mcpPath, JSON.stringify(existing, null, 2) + '\n');
|
|
94
|
+
console.log(chalk.hex('#6BC46A')(' ✓ Added screencraft to .mcp.json'));
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
console.log(chalk.dim(' .mcp.json exists but could not parse — skipping'));
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
const mcpContent = {
|
|
101
|
+
mcpServers: {
|
|
102
|
+
screencraft: {
|
|
103
|
+
type: 'stdio',
|
|
104
|
+
command: 'npx',
|
|
105
|
+
args: ['screencraft', '--mcp'],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
fs.writeFileSync(mcpPath, JSON.stringify(mcpContent, null, 2) + '\n');
|
|
110
|
+
console.log(chalk.hex('#6BC46A')(' ✓ Created .mcp.json (MCP integration for Claude Code/Desktop)'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Done ──────────────────────────────────────────────────────
|
|
114
|
+
console.log('');
|
|
115
|
+
console.log(chalk.dim(' Next steps:'));
|
|
116
|
+
console.log(' ' + chalk.white('screencraft serve') + chalk.dim(' — open the web UI'));
|
|
117
|
+
console.log(' ' + chalk.white('screencraft launch') + chalk.dim(' — run the full pipeline'));
|
|
118
|
+
console.log('');
|
|
119
|
+
};
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/commands/launch.js
|
|
3
|
+
* ----------------------
|
|
4
|
+
* Main pipeline orchestrator. Runs in sequence:
|
|
5
|
+
* 1. Detect framework + brand
|
|
6
|
+
* 2. Capture or load screenshots
|
|
7
|
+
* 3. Suggest headline text via Claude API
|
|
8
|
+
* 4. User approves/edits text interactively
|
|
9
|
+
* 5. Gate: check license key
|
|
10
|
+
* 6. Composite PSDs
|
|
11
|
+
* 7. Submit render job (AE video)
|
|
12
|
+
* 8. Download + unpack bundle
|
|
13
|
+
* 9. Open output folder
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const chalk = require('chalk');
|
|
19
|
+
const ora = require('ora');
|
|
20
|
+
const inquirer = require('inquirer');
|
|
21
|
+
|
|
22
|
+
const { detectFramework, detectBrand } = require('../detectors/detectBrand');
|
|
23
|
+
const { captureScreenshots } = require('../detectors/simulator');
|
|
24
|
+
const { suggestHeadlines } = require('../generators/copy');
|
|
25
|
+
const { compositePSD } = require('../generators/compositePSD');
|
|
26
|
+
const { validateKey, getStoredKey } = require('../auth/keystore');
|
|
27
|
+
const { submitRenderJob, pollJob } = require('../pipeline/queue');
|
|
28
|
+
const { downloadBundle } = require('../pipeline/download');
|
|
29
|
+
const { prepareAEProject, renderLocally } = require('../pipeline/aeSwap');
|
|
30
|
+
|
|
31
|
+
const ROOT = process.cwd();
|
|
32
|
+
|
|
33
|
+
module.exports = async function launch(args) {
|
|
34
|
+
let screenshotsOnly = args.includes('--screenshots-only');
|
|
35
|
+
|
|
36
|
+
// ── Load config ───────────────────────────────────────────────
|
|
37
|
+
const configPath = path.join(ROOT, 'screencraft.config.js');
|
|
38
|
+
const config = fs.existsSync(configPath)
|
|
39
|
+
? require(configPath)
|
|
40
|
+
: {};
|
|
41
|
+
|
|
42
|
+
const outputDir = path.join(ROOT, config.output?.dir || './launch-kit');
|
|
43
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
44
|
+
fs.mkdirSync(path.join(outputDir, 'screenshots'), { recursive: true });
|
|
45
|
+
fs.mkdirSync(path.join(outputDir, 'video'), { recursive: true });
|
|
46
|
+
fs.mkdirSync(path.join(outputDir, 'source'), { recursive: true });
|
|
47
|
+
fs.mkdirSync(path.join(outputDir, 'brand'), { recursive: true });
|
|
48
|
+
|
|
49
|
+
// ── PHASE 1: Detect ───────────────────────────────────────────
|
|
50
|
+
log.step('1/5', 'Detecting project...');
|
|
51
|
+
const framework = await detectFramework(ROOT);
|
|
52
|
+
const brand = await detectBrand(ROOT, config);
|
|
53
|
+
|
|
54
|
+
log.success(`Framework: ${framework.name}`);
|
|
55
|
+
log.success(`App name: ${brand.appName || '(not found — set app.name in config)'}`);
|
|
56
|
+
log.success(`Primary: ${brand.primary}`);
|
|
57
|
+
log.success(`Accent: ${brand.accent}`);
|
|
58
|
+
log.success(`Icon: ${brand.icon ? 'found' : chalk.yellow('not found')}`);
|
|
59
|
+
log.success(`Font: ${brand.font?.family || chalk.yellow('not found (using system font)')}`);
|
|
60
|
+
log.success(`Logo: ${brand.logo ? 'found' : chalk.yellow('not found')}`);
|
|
61
|
+
console.log('');
|
|
62
|
+
|
|
63
|
+
// ── PHASE 2: Screenshots ──────────────────────────────────────
|
|
64
|
+
log.step('2/5', 'Capturing screenshots...');
|
|
65
|
+
|
|
66
|
+
let screenshots;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
screenshots = await captureScreenshots(ROOT, framework, {
|
|
70
|
+
screenConfigs: config.screenshots || null,
|
|
71
|
+
outputDir: outputDir,
|
|
72
|
+
log,
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
log.warn(err.message);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
console.log('');
|
|
79
|
+
|
|
80
|
+
// ── PHASE 3: AI Headline Suggestions ─────────────────────────
|
|
81
|
+
log.step('3/5', 'Generating headline suggestions with AI...');
|
|
82
|
+
log.info('Sending screenshots to Claude...');
|
|
83
|
+
|
|
84
|
+
const suggestions = await suggestHeadlines(screenshots, brand, config);
|
|
85
|
+
console.log('');
|
|
86
|
+
|
|
87
|
+
// ── PHASE 4: Interactive approval ────────────────────────────
|
|
88
|
+
log.step('4/5', 'Approve headline text for each screen');
|
|
89
|
+
console.log(chalk.dim(' Format: white text + accent color word'));
|
|
90
|
+
console.log(chalk.dim(' Press enter to accept suggestion, or type a replacement'));
|
|
91
|
+
console.log('');
|
|
92
|
+
|
|
93
|
+
const approvedTexts = [];
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < screenshots.length; i++) {
|
|
96
|
+
const screen = screenshots[i];
|
|
97
|
+
const opts = suggestions[i];
|
|
98
|
+
|
|
99
|
+
console.log(chalk.hex('#A78BFA')(` Screen ${i + 1}:`), chalk.dim(path.basename(screen.file)));
|
|
100
|
+
console.log(chalk.dim(` ↳ ${opts.description || ''}`));
|
|
101
|
+
console.log('');
|
|
102
|
+
|
|
103
|
+
// Show the 3 options
|
|
104
|
+
opts.options.forEach((o, idx) => {
|
|
105
|
+
console.log(` ${chalk.dim(`[${idx + 1}]`)} ${chalk.white(o.white)} ${chalk.hex('#A78BFA')(o.accent)}`);
|
|
106
|
+
});
|
|
107
|
+
console.log(` ${chalk.dim('[c]')} custom`);
|
|
108
|
+
console.log('');
|
|
109
|
+
|
|
110
|
+
const { choice } = await inquirer.prompt([{
|
|
111
|
+
type: 'input',
|
|
112
|
+
name: 'choice',
|
|
113
|
+
message: chalk.dim(' Select (1/2/3/c) or press enter for [1]:'),
|
|
114
|
+
default: '1',
|
|
115
|
+
}]);
|
|
116
|
+
|
|
117
|
+
let approved;
|
|
118
|
+
if (choice === 'c' || choice === 'C') {
|
|
119
|
+
const { custom } = await inquirer.prompt([{
|
|
120
|
+
type: 'input',
|
|
121
|
+
name: 'custom',
|
|
122
|
+
message: chalk.dim(' Enter text (use | to split white|accent):'),
|
|
123
|
+
}]);
|
|
124
|
+
const [w, a] = custom.includes('|') ? custom.split('|') : [custom, ''];
|
|
125
|
+
approved = { white: w.trim(), accent: a.trim() };
|
|
126
|
+
} else {
|
|
127
|
+
const idx = Math.max(0, Math.min(2, parseInt(choice || '1') - 1));
|
|
128
|
+
approved = opts.options[idx] || opts.options[0];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
approvedTexts.push(approved);
|
|
132
|
+
log.success(`Approved: "${approved.white} ${approved.accent}"`);
|
|
133
|
+
console.log('');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── GATE: License check ───────────────────────────────────────
|
|
137
|
+
if (!screenshotsOnly) {
|
|
138
|
+
console.log(chalk.dim('─────────────────────────────────────'));
|
|
139
|
+
const key = getStoredKey();
|
|
140
|
+
let tier = null;
|
|
141
|
+
|
|
142
|
+
if (key) {
|
|
143
|
+
log.info('Validating license key...');
|
|
144
|
+
tier = await validateKey(key);
|
|
145
|
+
if (tier) {
|
|
146
|
+
log.success(`License: ${tier} tier active`);
|
|
147
|
+
} else {
|
|
148
|
+
log.warn('License key invalid or expired.');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!tier) {
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log(chalk.hex('#C9A84C')(' A license key is required to generate the video + AE file.'));
|
|
155
|
+
console.log(chalk.dim(' Get one at: ') + chalk.white('screencraft.dev/activate'));
|
|
156
|
+
console.log('');
|
|
157
|
+
|
|
158
|
+
const { enteredKey } = await inquirer.prompt([{
|
|
159
|
+
type: 'input',
|
|
160
|
+
name: 'enteredKey',
|
|
161
|
+
message: chalk.dim(' Enter license key (or press enter to generate screenshots only):'),
|
|
162
|
+
}]);
|
|
163
|
+
|
|
164
|
+
if (enteredKey.trim()) {
|
|
165
|
+
tier = await validateKey(enteredKey.trim());
|
|
166
|
+
if (!tier) {
|
|
167
|
+
log.warn('Invalid key. Continuing with screenshots only.');
|
|
168
|
+
screenshotsOnly = true;
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
screenshotsOnly = true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
console.log('');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── PHASE 5: Generate ─────────────────────────────────────────
|
|
178
|
+
log.step('5/5', 'Generating launch kit...');
|
|
179
|
+
console.log('');
|
|
180
|
+
|
|
181
|
+
// Always: composite App Store PNGs
|
|
182
|
+
const spinner = ora({ text: 'Compositing device mockups...', color: 'magenta' }).start();
|
|
183
|
+
|
|
184
|
+
const psdOutputs = [];
|
|
185
|
+
for (let i = 0; i < screenshots.length; i++) {
|
|
186
|
+
const screen = screenshots[i];
|
|
187
|
+
const text = approvedTexts[i];
|
|
188
|
+
const outPng = path.join(outputDir, 'screenshots', `screen_${String(i + 1).padStart(2, '0')}.png`);
|
|
189
|
+
const outPsd = path.join(outputDir, 'source', `screen_${String(i + 1).padStart(2, '0')}.psd`);
|
|
190
|
+
|
|
191
|
+
spinner.text = `Compositing screen ${i + 1} of ${screenshots.length}...`;
|
|
192
|
+
|
|
193
|
+
await compositePSD({
|
|
194
|
+
screenshotPath: screen.file,
|
|
195
|
+
headlineWhite: text.white,
|
|
196
|
+
headlineAccent: text.accent,
|
|
197
|
+
brand,
|
|
198
|
+
font: brand.font || {},
|
|
199
|
+
outputPng: outPng,
|
|
200
|
+
outputPsd: outPsd,
|
|
201
|
+
writePsd: !screenshotsOnly,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
psdOutputs.push({ png: outPng, psd: outPsd });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
spinner.succeed('Screenshots composited');
|
|
208
|
+
|
|
209
|
+
// Copy brand assets into output folder
|
|
210
|
+
const assetsDir = path.join(outputDir, 'brand');
|
|
211
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
212
|
+
|
|
213
|
+
const copyAsset = (src, destName) => {
|
|
214
|
+
try {
|
|
215
|
+
if (src && fs.existsSync(src)) {
|
|
216
|
+
fs.copyFileSync(src, path.join(assetsDir, destName));
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
} catch {}
|
|
220
|
+
return false;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
copyAsset(brand.icon, `app-icon${path.extname(brand.icon || '.png')}`);
|
|
224
|
+
copyAsset(brand.logo, `logo${path.extname(brand.logo || '.png')}`);
|
|
225
|
+
if (brand.font?.file) copyAsset(brand.font.file, `font${path.extname(brand.font.file)}`);
|
|
226
|
+
|
|
227
|
+
// Generate brand kit README
|
|
228
|
+
generateReadme(outputDir, brand, approvedTexts, screenshots, screenshotsOnly);
|
|
229
|
+
|
|
230
|
+
if (!screenshotsOnly) {
|
|
231
|
+
// Prepare AE project with swapped footage + manifest
|
|
232
|
+
const aeSpinner = ora({ text: 'Preparing After Effects project...', color: 'magenta' }).start();
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const ae = await prepareAEProject({
|
|
236
|
+
brand,
|
|
237
|
+
texts: approvedTexts,
|
|
238
|
+
screenshots,
|
|
239
|
+
outputDir,
|
|
240
|
+
log,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (ae.aepPath) {
|
|
244
|
+
aeSpinner.succeed('AE project prepared');
|
|
245
|
+
|
|
246
|
+
// Try local render if aerender is available
|
|
247
|
+
if (ae.canRender) {
|
|
248
|
+
const renderSpinner = ora({ text: 'Rendering video...', color: 'magenta' }).start();
|
|
249
|
+
try {
|
|
250
|
+
const videoPath = await renderLocally({
|
|
251
|
+
aepPath: ae.aepPath,
|
|
252
|
+
outputDir,
|
|
253
|
+
onProgress: (step) => { renderSpinner.text = step; },
|
|
254
|
+
});
|
|
255
|
+
renderSpinner.succeed('Video rendered');
|
|
256
|
+
} catch (err) {
|
|
257
|
+
renderSpinner.warn('Local render failed — open source/app-launch.aep in After Effects');
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
log.info('Open source/app-launch.aep in After Effects to render the video');
|
|
261
|
+
log.info('Or run: File → Scripts → Run Script File → ae_swap.jsx');
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
aeSpinner.warn('No AE template found — skipping video');
|
|
265
|
+
}
|
|
266
|
+
} catch (err) {
|
|
267
|
+
aeSpinner.warn('AE project preparation failed: ' + err.message);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Done ──────────────────────────────────────────────────────
|
|
272
|
+
console.log('');
|
|
273
|
+
console.log(chalk.dim('─────────────────────────────────────'));
|
|
274
|
+
console.log(chalk.hex('#6BC46A')(' ✓ Launch kit ready → ') + chalk.white(outputDir));
|
|
275
|
+
console.log('');
|
|
276
|
+
console.log(chalk.dim(' screenshots/') + chalk.hex('#6BC46A')(` ← ${screenshots.length} composited + ${screenshots.length} raw PNGs`));
|
|
277
|
+
console.log(chalk.dim(' brand/ ') + chalk.hex('#6BC46A')(' ← icon, logo, font'));
|
|
278
|
+
|
|
279
|
+
if (!screenshotsOnly) {
|
|
280
|
+
console.log(chalk.dim(' source/ ') + chalk.hex('#6BC46A')(' ← app-launch.aep + PSDs'));
|
|
281
|
+
console.log(chalk.dim(' video/ ') + chalk.hex('#6BC46A')(' ← preview.mp4'));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.log(chalk.dim(' README.md ') + chalk.hex('#6BC46A')(' ← brand kit + checklist'));
|
|
285
|
+
console.log('');
|
|
286
|
+
|
|
287
|
+
if (screenshotsOnly) {
|
|
288
|
+
console.log(chalk.hex('#C9A84C')(' Video + AE file → screencraft.dev/activate'));
|
|
289
|
+
console.log('');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Open the output folder
|
|
293
|
+
const { exec } = require('child_process');
|
|
294
|
+
exec(`open "${outputDir}"`);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
298
|
+
const log = {
|
|
299
|
+
step: (n, msg) => console.log(chalk.dim(`[${n}]`) + ' ' + chalk.white(msg)),
|
|
300
|
+
success: (msg) => console.log(' ' + chalk.hex('#6BC46A')('✓') + ' ' + chalk.dim(msg)),
|
|
301
|
+
info: (msg) => console.log(' ' + chalk.hex('#6AABDD')('↳') + ' ' + chalk.dim(msg)),
|
|
302
|
+
warn: (msg) => console.log(' ' + chalk.hex('#C9A84C')('⚠') + ' ' + chalk.hex('#C9A84C')(msg)),
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
function generateReadme(outputDir, brand, texts, screenshots, screenshotsOnly) {
|
|
306
|
+
const appName = brand.appName || 'App';
|
|
307
|
+
const date = new Date().toLocaleDateString();
|
|
308
|
+
|
|
309
|
+
// ── Brand Colors section
|
|
310
|
+
const colors = [
|
|
311
|
+
['Primary', brand.primary],
|
|
312
|
+
['Secondary', brand.secondary],
|
|
313
|
+
['Accent', brand.accent],
|
|
314
|
+
['Background', brand.background],
|
|
315
|
+
].filter(([, hex]) => hex);
|
|
316
|
+
|
|
317
|
+
const colorRows = colors
|
|
318
|
+
.map(([name, hex]) => `| ${name} | \`${hex}\` | }/${hex.replace('#', '')}.png) |`)
|
|
319
|
+
.join('\n');
|
|
320
|
+
|
|
321
|
+
// ── Brand Assets section
|
|
322
|
+
const assetRows = [];
|
|
323
|
+
if (brand.icon) assetRows.push('| App Icon | `brand/app-icon' + path.extname(brand.icon) + '` | 1024×1024 PNG |');
|
|
324
|
+
if (brand.logo) assetRows.push('| Logo | `brand/logo' + path.extname(brand.logo) + '` | Vector/PNG |');
|
|
325
|
+
if (brand.font?.file) assetRows.push(`| Font | \`brand/font${path.extname(brand.font.file)}\` | ${brand.font.family || 'Custom'}${brand.font.weight ? ' ' + brand.font.weight : ''} |`);
|
|
326
|
+
|
|
327
|
+
// ── Screenshots section
|
|
328
|
+
const screenRows = screenshots.map((s, i) => {
|
|
329
|
+
const t = texts[i];
|
|
330
|
+
const headline = t ? `"${t.white} **${t.accent}**"` : '—';
|
|
331
|
+
return `| ${i + 1} | \`screenshots/screen_${String(i + 1).padStart(2, '0')}.png\` | ${headline} | \`screenshots/${path.basename(s.file)}\` |`;
|
|
332
|
+
}).join('\n');
|
|
333
|
+
|
|
334
|
+
const readme = `# Launch Kit — ${appName}
|
|
335
|
+
|
|
336
|
+
> Generated by [ScreenCraft](https://screencraft.dev) on ${date}
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Brand Colors
|
|
341
|
+
|
|
342
|
+
| Name | Hex | Swatch |
|
|
343
|
+
|------|-----|--------|
|
|
344
|
+
${colorRows}
|
|
345
|
+
|
|
346
|
+
## Brand Assets
|
|
347
|
+
|
|
348
|
+
| Asset | File | Details |
|
|
349
|
+
|-------|------|---------|
|
|
350
|
+
${assetRows.length > 0 ? assetRows.join('\n') : '| — | No assets detected | Set in screencraft.config.js |'}
|
|
351
|
+
|
|
352
|
+
${brand.font?.family ? `**Font:** ${brand.font.family}${brand.font.weight ? ' (' + brand.font.weight + ')' : ''}` : '**Font:** System default (no custom font detected)'}
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Screenshots
|
|
357
|
+
|
|
358
|
+
| # | Composited | Headline | Raw |
|
|
359
|
+
|---|------------|----------|-----|
|
|
360
|
+
${screenRows}
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Files
|
|
365
|
+
|
|
366
|
+
| Path | Description |
|
|
367
|
+
|------|-------------|
|
|
368
|
+
| \`screenshots/\` | App Store-ready composited PNGs (1290×2796) |
|
|
369
|
+
| \`screenshots/*_raw.png\` | Original simulator captures |
|
|
370
|
+
| \`brand/\` | Extracted brand assets (icon, logo, font) |
|
|
371
|
+
${screenshotsOnly ? '' : `| \`source/app-launch.aep\` | Editable After Effects project |
|
|
372
|
+
| \`source/*.psd\` | Layered Photoshop files |
|
|
373
|
+
| \`video/preview.mp4\` | App Store preview video |
|
|
374
|
+
`}| \`README.md\` | This file — brand kit reference |
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## App Store Submission Checklist
|
|
379
|
+
|
|
380
|
+
### iOS App Store
|
|
381
|
+
- [ ] Screenshots: 6.7" (1290×2796) — required
|
|
382
|
+
- [ ] Screenshots: 6.1" (1179×2556) — recommended
|
|
383
|
+
- [ ] App preview video: up to 30s, 9:16 vertical
|
|
384
|
+
- [ ] Icon: 1024×1024 PNG, no alpha, no rounded corners
|
|
385
|
+
|
|
386
|
+
### Google Play
|
|
387
|
+
- [ ] Feature graphic: 1024×500
|
|
388
|
+
- [ ] Screenshots: min 2, up to 8, 16:9 or 9:16
|
|
389
|
+
- [ ] Hi-res icon: 512×512
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Regenerate
|
|
394
|
+
|
|
395
|
+
\`\`\`bash
|
|
396
|
+
cd your-app-project
|
|
397
|
+
npx screencraft launch
|
|
398
|
+
\`\`\`
|
|
399
|
+
|
|
400
|
+
${screenshotsOnly ? `> Video + AE project file available with a license key — [screencraft.dev/activate](https://screencraft.dev/activate)\n` : ''}---
|
|
401
|
+
[screencraft.dev](https://screencraft.dev)
|
|
402
|
+
`;
|
|
403
|
+
|
|
404
|
+
fs.writeFileSync(path.join(outputDir, 'README.md'), readme);
|
|
405
|
+
}
|