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,36 @@
1
+ /**
2
+ * src/server/session.js
3
+ * ---------------------
4
+ * Shared session state between Express server and MCP server.
5
+ * Single-user tool — one session at a time.
6
+ */
7
+
8
+ const session = {
9
+ projectPath: null,
10
+ outputDir: null,
11
+ framework: null,
12
+ brand: null,
13
+ screenshots: [],
14
+ suggestions: [],
15
+ approvedTexts: [],
16
+ selectedTemplate: null,
17
+ renderStatus: { phase: 'idle', progress: 0, message: '' },
18
+ outputs: [],
19
+ _simDevice: null,
20
+ };
21
+
22
+ function resetSession() {
23
+ session.projectPath = null;
24
+ session.outputDir = null;
25
+ session.framework = null;
26
+ session.brand = null;
27
+ session.screenshots = [];
28
+ session.suggestions = [];
29
+ session.approvedTexts = [];
30
+ session.selectedTemplate = null;
31
+ session.renderStatus = { phase: 'idle', progress: 0, message: '' };
32
+ session.outputs = [];
33
+ session._simDevice = null;
34
+ }
35
+
36
+ module.exports = { session, resetSession };
@@ -0,0 +1,284 @@
1
+ /**
2
+ * templates/ae_swap.jsx
3
+ * ---------------------
4
+ * ExtendScript for After Effects.
5
+ *
6
+ * Reads a render_manifest.json from the same directory as the AEP,
7
+ * then walks all comps/layers looking for [SWAP_*] tags and replaces:
8
+ *
9
+ * [SWAP_COLOR_*] — updates Color Control effect value
10
+ * [SWAP_TEXT_*] — updates source text
11
+ * [SWAP_SCREENSHOT_*] — replaces footage item with new PNG
12
+ * [SWAP_IMAGE_*] — replaces footage item with new image
13
+ *
14
+ * Usage:
15
+ * 1. Open the AE template project
16
+ * 2. File → Scripts → Run Script File → ae_swap.jsx
17
+ * -- or --
18
+ * aerender -project "template.aep" -s ae_swap.jsx
19
+ *
20
+ * The script expects render_manifest.json in the project's root folder:
21
+ * {
22
+ * "brand": {
23
+ * "primary": "#7068B1",
24
+ * "secondary": "#435B98",
25
+ * "accent": "#7068b1",
26
+ * "background": "#1a1a2e",
27
+ * "appName": "My App"
28
+ * },
29
+ * "texts": [
30
+ * { "white": "Transform your", "accent": "inner voice" },
31
+ * ...
32
+ * ],
33
+ * "screenshots": [
34
+ * { "file": "screenshots/screen_01_raw.png" },
35
+ * ...
36
+ * ]
37
+ * }
38
+ */
39
+
40
+ (function () {
41
+ // ── JSON polyfill (ExtendScript has no native JSON) ─────────────
42
+ if (typeof JSON === "undefined") {
43
+ JSON = {};
44
+ }
45
+ if (typeof JSON.parse !== "function") {
46
+ JSON.parse = function (str) {
47
+ return eval("(" + str + ")");
48
+ };
49
+ }
50
+ if (typeof JSON.stringify !== "function") {
51
+ JSON.stringify = function (obj) {
52
+ if (obj === null) return "null";
53
+ if (typeof obj === "string") return '"' + obj.replace(/"/g, '\\"') + '"';
54
+ if (typeof obj === "number" || typeof obj === "boolean") return String(obj);
55
+ if (obj instanceof Array) {
56
+ var arr = [];
57
+ for (var i = 0; i < obj.length; i++) arr.push(JSON.stringify(obj[i]));
58
+ return "[" + arr.join(",") + "]";
59
+ }
60
+ if (typeof obj === "object") {
61
+ var pairs = [];
62
+ for (var k in obj) {
63
+ if (obj.hasOwnProperty(k)) {
64
+ pairs.push('"' + k + '":' + JSON.stringify(obj[k]));
65
+ }
66
+ }
67
+ return "{" + pairs.join(",") + "}";
68
+ }
69
+ return "undefined";
70
+ };
71
+ }
72
+
73
+ // ── Locate manifest ────────────────────────────────────────────
74
+ var projectFolder = app.project.file.parent;
75
+ var manifestFile = new File(projectFolder.fsName + "/render_manifest.json");
76
+
77
+ if (!manifestFile.exists) {
78
+ alert("ScreenCraft: render_manifest.json not found in:\n" + projectFolder.fsName);
79
+ return;
80
+ }
81
+
82
+ manifestFile.open("r");
83
+ var raw = manifestFile.read();
84
+ manifestFile.close();
85
+
86
+ var manifest = JSON.parse(raw);
87
+ var brand = manifest.brand || {};
88
+ var texts = manifest.texts || [];
89
+ var screenshots = manifest.screenshots || [];
90
+
91
+ // ── Build color map ────────────────────────────────────────────
92
+ var colorMap = {
93
+ "primary": brand.primary || null,
94
+ "secondary": brand.secondary || null,
95
+ "accent": brand.accent || null,
96
+ "background": brand.background || null
97
+ };
98
+
99
+ // ── Build text map ─────────────────────────────────────────────
100
+ // Per-scene numbered text (for AE with multiple scenes in one comp):
101
+ // [SWAP_TEXT_1_headline] → texts[0].white
102
+ // [SWAP_TEXT_1_headline_accent]→ texts[0].accent
103
+ // [SWAP_TEXT_2_headline] → texts[1].white
104
+ // ...
105
+ // Unnumbered fallback (for single-scene or PSD):
106
+ // [SWAP_TEXT_headline] → texts[0].white
107
+ // [SWAP_TEXT_headline_accent] → texts[0].accent
108
+ // Global:
109
+ // [SWAP_TEXT_app_name] → brand.appName
110
+ // [SWAP_TEXT_tagline] → brand.tagline
111
+ var textMap = {
112
+ "headline": texts[0] ? texts[0].white : null,
113
+ "headline_accent": texts[0] ? texts[0].accent : null,
114
+ "app_name": brand.appName || null,
115
+ "tagline": brand.tagline || null
116
+ };
117
+
118
+ // Add per-scene numbered text entries
119
+ for (var i = 0; i < texts.length; i++) {
120
+ var n = String(i + 1);
121
+ textMap[n + "_headline"] = texts[i].white;
122
+ textMap[n + "_headline_accent"] = texts[i].accent;
123
+ textMap["screenshot_" + n + "_label"] = texts[i].white + " " + texts[i].accent;
124
+ }
125
+
126
+ // ── Build screenshot map ───────────────────────────────────────
127
+ // Maps [SWAP_SCREENSHOT_1] → absolute path to PNG
128
+ var screenshotMap = {};
129
+ for (var i = 0; i < screenshots.length; i++) {
130
+ var ssPath = screenshots[i].file || screenshots[i].png || "";
131
+ // Resolve relative paths against project folder
132
+ if (ssPath.indexOf("/") !== 0 && ssPath.indexOf("\\") !== 0) {
133
+ ssPath = projectFolder.fsName + "/" + ssPath;
134
+ }
135
+ screenshotMap[String(i + 1)] = ssPath;
136
+ }
137
+
138
+ // ── Walk all comps and layers ──────────────────────────────────
139
+ var swapCount = 0;
140
+
141
+ for (var c = 1; c <= app.project.numItems; c++) {
142
+ var item = app.project.item(c);
143
+ if (!(item instanceof CompItem)) continue;
144
+
145
+ for (var l = 1; l <= item.numLayers; l++) {
146
+ var layer = item.layer(l);
147
+ var name = layer.name;
148
+
149
+ // Skip layers without [SWAP_ tag
150
+ if (name.indexOf("[SWAP_") === -1) continue;
151
+
152
+ // Extract the tag: [SWAP_TYPE_identifier]
153
+ var match = name.match(/\[SWAP_(\w+?)_(\w+)\]/);
154
+ if (!match) continue;
155
+
156
+ var swapType = match[1]; // COLOR, TEXT, SCREENSHOT, IMAGE
157
+ var swapId = match[2]; // primary, headline, 1, etc.
158
+
159
+ // Also extract full identifier after type (for multi-word ids like headline_accent)
160
+ var fullMatch = name.match(/\[SWAP_\w+?_([\w_]+)\]/);
161
+ var fullId = fullMatch ? fullMatch[1] : swapId;
162
+
163
+ // ── COLOR SWAP ─────────────────────────────────────────
164
+ if (swapType === "COLOR") {
165
+ var hex = colorMap[fullId];
166
+ if (hex) {
167
+ var rgb = hexToRgb(hex);
168
+ // Look for Color Control effect
169
+ var colorCtrl = findEffect(layer, "Color Control");
170
+ if (colorCtrl) {
171
+ colorCtrl.property("Color").setValue(rgb);
172
+ swapCount++;
173
+ } else {
174
+ // No Color Control — try to set the layer's solid color
175
+ // (works if it's a solid layer)
176
+ try {
177
+ if (layer.source instanceof FootageItem && layer.source.mainSource instanceof SolidSource) {
178
+ layer.source.mainSource.color = rgb;
179
+ swapCount++;
180
+ }
181
+ } catch (e) {}
182
+ }
183
+ }
184
+ }
185
+
186
+ // ── TEXT SWAP ──────────────────────────────────────────
187
+ if (swapType === "TEXT") {
188
+ var newText = textMap[fullId];
189
+ if (newText && layer.property("Source Text")) {
190
+ var textDoc = layer.property("Source Text").value;
191
+ textDoc.text = newText;
192
+
193
+ // If this is an accent text, set the fill color to brand accent
194
+ if (fullId.indexOf("accent") !== -1 && colorMap["accent"]) {
195
+ var accentRgb = hexToRgb(colorMap["accent"]);
196
+ textDoc.fillColor = accentRgb;
197
+ }
198
+
199
+ layer.property("Source Text").setValue(textDoc);
200
+ swapCount++;
201
+ }
202
+ }
203
+
204
+ // ── SCREENSHOT SWAP ────────────────────────────────────
205
+ if (swapType === "SCREENSHOT") {
206
+ var ssFile = screenshotMap[fullId];
207
+ if (ssFile) {
208
+ var newFile = new File(ssFile);
209
+ if (newFile.exists) {
210
+ try {
211
+ layer.source.replace(newFile);
212
+ swapCount++;
213
+ } catch (e) {
214
+ // Layer might not have replaceable source
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ // ── IMAGE SWAP (app icon, logo, etc.) ──────────────────
221
+ if (swapType === "IMAGE") {
222
+ var imgPath = null;
223
+ if (fullId === "app_icon" && brand.icon) imgPath = brand.icon;
224
+ if (fullId === "store_badge") imgPath = null; // keep default
225
+ if (fullId.indexOf("logo") !== -1 && brand.logo) imgPath = brand.logo;
226
+
227
+ if (imgPath) {
228
+ var imgFile = new File(imgPath);
229
+ if (imgFile.exists) {
230
+ try {
231
+ layer.source.replace(imgFile);
232
+ swapCount++;
233
+ } catch (e) {}
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ // ── Done ───────────────────────────────────────────────────────
241
+ if (swapCount > 0) {
242
+ app.project.save();
243
+ }
244
+
245
+ // Write a status file so the Node pipeline knows we're done
246
+ var statusFile = new File(projectFolder.fsName + "/swap_complete.json");
247
+ statusFile.open("w");
248
+ statusFile.write(JSON.stringify({
249
+ success: true,
250
+ swapsApplied: swapCount,
251
+ timestamp: new Date().toString()
252
+ }));
253
+ statusFile.close();
254
+
255
+
256
+ // ── Helpers ────────────────────────────────────────────────────
257
+
258
+ /**
259
+ * Convert hex color to AE [r, g, b] array (0-1 range).
260
+ */
261
+ function hexToRgb(hex) {
262
+ hex = hex.replace("#", "");
263
+ var r = parseInt(hex.substring(0, 2), 16) / 255;
264
+ var g = parseInt(hex.substring(2, 4), 16) / 255;
265
+ var b = parseInt(hex.substring(4, 6), 16) / 255;
266
+ return [r, g, b];
267
+ }
268
+
269
+ /**
270
+ * Find an effect by match name or display name on a layer.
271
+ */
272
+ function findEffect(layer, effectName) {
273
+ if (!layer.property("Effects")) return null;
274
+ var effects = layer.property("Effects");
275
+ for (var i = 1; i <= effects.numProperties; i++) {
276
+ var eff = effects.property(i);
277
+ if (eff.name === effectName || eff.matchName === "ADBE Color Control") {
278
+ return eff;
279
+ }
280
+ }
281
+ return null;
282
+ }
283
+
284
+ })();
Binary file
@@ -0,0 +1,165 @@
1
+ /**
2
+ * screencraft.config.js
3
+ * ---------------------
4
+ * Place this file in your project root.
5
+ * All fields are optional except `screenshots[].route`.
6
+ * Everything else is auto-detected from your codebase.
7
+ *
8
+ * Run `npx screencraft init` to generate this file automatically
9
+ * with detected values pre-filled.
10
+ */
11
+
12
+ module.exports = {
13
+
14
+ // ─────────────────────────────────────────────────────────────
15
+ // APP INFO
16
+ // Auto-detected from: Info.plist, app.json, pubspec.yaml,
17
+ // AndroidManifest.xml, package.json
18
+ // Override here if detection is wrong or you want different copy.
19
+ // ─────────────────────────────────────────────────────────────
20
+ app: {
21
+ name: "Navi", // App display name
22
+ tagline: "Navigate anything.", // One-line tagline (used in video title card)
23
+ cta: "Download Free", // CTA button text in video
24
+ category: "productivity", // App Store category (used for icon style selection)
25
+ platforms: ["ios", "android"], // Which store badges to include in video
26
+ storeUrl: {
27
+ ios: "apps.apple.com/app/navi/id123456789",
28
+ android: "play.google.com/store/apps/details?id=com.navi.app"
29
+ }
30
+ },
31
+
32
+ // ─────────────────────────────────────────────────────────────
33
+ // SCREENSHOTS
34
+ // The only required section. Define up to 6 screens.
35
+ // `route` is the deep link / navigation path to reach each screen.
36
+ // `label` is the short callout text that appears beside the screen
37
+ // in the video. Max 5 words. Auto-generated by AI if omitted.
38
+ // `state` lets you set up specific UI state before screenshotting.
39
+ // ─────────────────────────────────────────────────────────────
40
+ screenshots: [
41
+ {
42
+ route: "/onboarding",
43
+ label: "Get started in seconds",
44
+ state: null // No special state needed
45
+ },
46
+ {
47
+ route: "/home",
48
+ label: "Everything at a glance",
49
+ state: {
50
+ user: "demo", // Log in as demo user so home screen is populated
51
+ mock: "home_populated" // Use mock data preset (see Mocks section below)
52
+ }
53
+ },
54
+ {
55
+ route: "/search",
56
+ label: "Find anything instantly",
57
+ state: {
58
+ mock: "search_results"
59
+ }
60
+ },
61
+ {
62
+ route: "/profile",
63
+ label: "Your space, your way",
64
+ state: {
65
+ user: "demo"
66
+ }
67
+ },
68
+ {
69
+ route: "/settings",
70
+ label: "Built for how you work",
71
+ state: null
72
+ },
73
+ {
74
+ route: "/home",
75
+ label: "Easy on the eyes",
76
+ state: {
77
+ theme: "dark", // Force dark mode for this screenshot
78
+ mock: "home_populated"
79
+ }
80
+ }
81
+ ],
82
+
83
+ // ─────────────────────────────────────────────────────────────
84
+ // BRAND
85
+ // Auto-detected from: theme.js, colors.js, colors.swift,
86
+ // Color.xcassets, theme.dart, styles/colors.css, tokens.json
87
+ // Override here if you want to use different colors in the
88
+ // marketing materials vs the app itself.
89
+ // ─────────────────────────────────────────────────────────────
90
+ brand: {
91
+ primary: null, // e.g. "#1B3A6B" — null = auto-detect
92
+ secondary: null, // e.g. "#F5F7FA" — null = auto-detect
93
+ accent: null, // e.g. "#C9A84C" — null = auto-detect
94
+ font: null, // e.g. "Inter" — null = auto-detect, falls back to system
95
+ icon: null, // Path to app icon PNG (1024x1024). null = auto-detect
96
+ // Searched: ios/AppIcon.appiconset, assets/icon.png,
97
+ // android/app/src/main/res/mipmap-xxxhdpi
98
+ },
99
+
100
+ // ─────────────────────────────────────────────────────────────
101
+ // DEVICE
102
+ // Which device frame to use for screenshots and video.
103
+ // Auto-selected based on platform if not specified.
104
+ // ─────────────────────────────────────────────────────────────
105
+ device: {
106
+ model: "iphone-15-pro", // iphone-15-pro | iphone-15 | pixel-8 | pixel-8-pro
107
+ color: "black-titanium", // black-titanium | white-titanium | natural | blue
108
+ showNotch: true, // Show status bar / dynamic island area
109
+ },
110
+
111
+ // ─────────────────────────────────────────────────────────────
112
+ // VIDEO
113
+ // Controls the generated preview video.
114
+ // ─────────────────────────────────────────────────────────────
115
+ video: {
116
+ duration: 30, // 30 or 45 seconds
117
+ music: "upbeat", // upbeat | calm | cinematic | none
118
+ showRating: false, // Include App Store rating scene (requires rating + reviewCount)
119
+ rating: null, // e.g. "4.8" — shown as star rating in social proof scene
120
+ reviewCount: null, // e.g. "12,400+" — shown beneath stars
121
+ reviewQuote: null, // Short user review quote, max 10 words
122
+ },
123
+
124
+ // ─────────────────────────────────────────────────────────────
125
+ // OUTPUT
126
+ // Controls what gets generated and where.
127
+ // ─────────────────────────────────────────────────────────────
128
+ output: {
129
+ dir: "./launch-kit", // Output directory (relative to project root)
130
+ screenshots: true, // Always true — free tier
131
+ video: true, // Watermarked always; clean requires license
132
+ aepFile: true, // Requires Launch ($49) or Pro ($99) license
133
+ psds: true, // Requires Pro ($99) license
134
+ socialCut: true, // Square 1:1 MP4 — requires Pro ($99) license
135
+ storeCopy: true, // App Store description + keywords — requires Pro
136
+ },
137
+
138
+ // ─────────────────────────────────────────────────────────────
139
+ // MOCKS (optional)
140
+ // Define named data states the simulator can be put into
141
+ // before a screenshot. Reference these in screenshots[].state.mock
142
+ // ScreenCraft will look for a matching setup function in:
143
+ // screencraft.mocks.js (you create this — see docs)
144
+ // ─────────────────────────────────────────────────────────────
145
+ mocks: {
146
+ enabled: false, // Set to true if you have screencraft.mocks.js
147
+ },
148
+
149
+ // ─────────────────────────────────────────────────────────────
150
+ // SIMULATOR (rarely needed — auto-configured per framework)
151
+ // ─────────────────────────────────────────────────────────────
152
+ simulator: {
153
+ ios: {
154
+ device: "iPhone 15 Pro", // Simulator device name
155
+ os: "17.0", // iOS version
156
+ timeout: 8000, // ms to wait after navigation before screenshot
157
+ },
158
+ android: {
159
+ device: "Pixel 8 Pro",
160
+ api: 34,
161
+ timeout: 8000,
162
+ }
163
+ }
164
+
165
+ };
Binary file
@@ -0,0 +1,64 @@
1
+ {
2
+ "layout": {
3
+ "type": "multi-phone",
4
+ "phoneCount": 3,
5
+ "arrangement": "stacked-cascade",
6
+ "tiltAngles": [-14, -7, 0],
7
+ "phoneScales": [0.65, 0.8, 1.0],
8
+ "phonePositions": [
9
+ { "anchor": "left", "xOffset": -28, "yOffset": 8 },
10
+ { "anchor": "center", "xOffset": -10, "yOffset": 4 },
11
+ { "anchor": "right", "xOffset": 12, "yOffset": 0 }
12
+ ],
13
+ "textPosition": "top-left"
14
+ },
15
+
16
+ "background": {
17
+ "type": "solid",
18
+ "color": "#5C3317",
19
+ "gradientEndColor": null,
20
+ "gradientAngle": null,
21
+ "noiseAmount": 0.08,
22
+ "adaptToBrand": true
23
+ },
24
+
25
+ "phoneFrame": {
26
+ "style": "white-minimal",
27
+ "borderColor": null,
28
+ "borderWidth": 0,
29
+ "shadowStyle": "soft",
30
+ "shadowColor": "#00000055",
31
+ "cornerRadius": 44,
32
+ "adaptBorderToBrand": false
33
+ },
34
+
35
+ "typography": {
36
+ "headlineStyle": "large-bold-sans",
37
+ "headlineSize": "xxl",
38
+ "headlineCase": "title",
39
+ "headlinePosition": "top-left",
40
+ "subheadPresent": true,
41
+ "subheadSize": "sm",
42
+ "usesEmoji": false,
43
+ "textColor": "#FFFFFF",
44
+ "accentTextColor": null,
45
+ "lineStyle": "tight",
46
+ "fontPersonality": "bold-sans"
47
+ },
48
+
49
+ "spacing": {
50
+ "density": "airy",
51
+ "phonePaddingPercent": 6,
52
+ "textToPhoneGap": "generous"
53
+ },
54
+
55
+ "colorExtraction": {
56
+ "dominantBgColor": "#5C3317",
57
+ "phoneFrameColor": "#FFFFFF",
58
+ "headlineTextColor": "#FFFFFF",
59
+ "accentColor": null,
60
+ "overallMood": "warm"
61
+ },
62
+
63
+ "layoutDescription": "Three white-framed phones in a stacked left-to-right cascade at increasing scale, small-to-large, with bold left-aligned headline text occupying the upper-left quadrant above the phone stack."
64
+ }
Binary file
@@ -0,0 +1,69 @@
1
+ /**
2
+ * test/test_brand.js
3
+ * ------------------
4
+ * Tests brand detection against a real project directory.
5
+ *
6
+ * Run: node test/test_brand.js /path/to/your/app
7
+ * or: node test/test_brand.js (uses current directory)
8
+ */
9
+
10
+ const path = require('path');
11
+ const { detectBrand, detectFramework } = require('../src/detectors/detectBrand');
12
+
13
+ const targetDir = process.argv[2] || process.cwd();
14
+
15
+ async function main() {
16
+ console.log('');
17
+ console.log(' Testing brand detection on:', targetDir);
18
+ console.log('');
19
+
20
+ const framework = await detectFramework(targetDir);
21
+ console.log(' Framework:', framework.name);
22
+ console.log(' Platform:', framework.platform.join(', ') || 'none detected');
23
+ console.log(' Config file:', framework.configFile || 'none');
24
+ console.log('');
25
+
26
+ const brand = await detectBrand(targetDir, {});
27
+ console.log(' Brand results:');
28
+ console.log(' ├─ App name: ', brand.appName || '(not detected)');
29
+ console.log(' ├─ Primary: ', brand.primary);
30
+ console.log(' ├─ Secondary: ', brand.secondary);
31
+ console.log(' ├─ Accent: ', brand.accent);
32
+ console.log(' ├─ Background:', brand.background);
33
+ console.log(' ├─ Icon: ', brand.icon || '(not detected)');
34
+ console.log(' ├─ Font: ', brand.font?.family ? `${brand.font.family}${brand.font.weight ? ` (${brand.font.weight})` : ''}` : '(not detected)');
35
+ console.log(' │ File: ', brand.font?.file || '(none)');
36
+ console.log(' └─ Logo: ', brand.logo || '(not detected)');
37
+ console.log('');
38
+
39
+ // Visual color swatches in terminal (if supported)
40
+ const swatches = [
41
+ ['Primary', brand.primary],
42
+ ['Secondary', brand.secondary],
43
+ ['Accent', brand.accent],
44
+ ];
45
+
46
+ console.log(' Color swatches:');
47
+ for (const [name, hex] of swatches) {
48
+ const { r, g, b } = hexToRgb(hex);
49
+ // ANSI background color block
50
+ const swatch = `\x1b[48;2;${r};${g};${b}m \x1b[0m`;
51
+ console.log(` ${swatch} ${name.padEnd(10)} ${hex}`);
52
+ }
53
+
54
+ console.log('');
55
+ }
56
+
57
+ function hexToRgb(hex) {
58
+ const h = hex.replace('#', '');
59
+ return {
60
+ r: parseInt(h.substring(0, 2), 16),
61
+ g: parseInt(h.substring(2, 4), 16),
62
+ b: parseInt(h.substring(4, 6), 16),
63
+ };
64
+ }
65
+
66
+ main().catch(err => {
67
+ console.error(' ✗ Error:', err.message);
68
+ process.exit(1);
69
+ });