@forvibe/cli 1.0.4 → 1.0.6

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/dist/index.js CHANGED
@@ -985,11 +985,13 @@ function extractBranding(rootDir, techStack) {
985
985
  primary_color: null,
986
986
  secondary_color: null,
987
987
  app_icon_base64: null,
988
- app_icon_path: null
988
+ app_icon_path: null,
989
+ brand_colors: []
989
990
  };
990
991
  const colors = extractColors(rootDir, techStack);
991
992
  result.primary_color = colors.primary;
992
993
  result.secondary_color = colors.secondary;
994
+ result.brand_colors = extractBrandColors(colors.allColors);
993
995
  const icon = findAppIcon(rootDir, techStack);
994
996
  result.app_icon_base64 = icon.base64;
995
997
  result.app_icon_path = icon.path;
@@ -1008,9 +1010,29 @@ function extractColors(rootDir, techStack) {
1008
1010
  case "java":
1009
1011
  return extractAndroidColors(rootDir);
1010
1012
  default:
1011
- return { primary: null, secondary: null };
1013
+ return { primary: null, secondary: null, allColors: [] };
1012
1014
  }
1013
1015
  }
1016
+ function isIrrelevantColor(hex) {
1017
+ const r = parseInt(hex.slice(1, 3), 16);
1018
+ const g = parseInt(hex.slice(3, 5), 16);
1019
+ const b = parseInt(hex.slice(5, 7), 16);
1020
+ const brightness = (r * 299 + g * 587 + b * 114) / 1e3;
1021
+ if (brightness > 242 || brightness < 13) return true;
1022
+ const maxDiff = Math.max(Math.abs(r - g), Math.abs(g - b), Math.abs(r - b));
1023
+ if (maxDiff < 15) return true;
1024
+ return false;
1025
+ }
1026
+ function extractBrandColors(allColors) {
1027
+ const freq = /* @__PURE__ */ new Map();
1028
+ for (const raw of allColors) {
1029
+ const hex = raw.toLowerCase().slice(0, 7);
1030
+ if (hex.length !== 7 || !hex.startsWith("#")) continue;
1031
+ freq.set(hex, (freq.get(hex) || 0) + 1);
1032
+ }
1033
+ const filtered = [...freq.entries()].filter(([hex]) => !isIrrelevantColor(hex)).sort((a, b) => b[1] - a[1]);
1034
+ return filtered.slice(0, 3).map(([hex]) => hex);
1035
+ }
1014
1036
  function extractFlutterColors(rootDir) {
1015
1037
  const dartFiles = findFiles(rootDir, [".dart"], 6);
1016
1038
  const hexColors = [];
@@ -1038,14 +1060,16 @@ function extractFlutterColors(rootDir) {
1038
1060
  if (primaryMatch[2]) {
1039
1061
  return {
1040
1062
  primary: `#${primaryMatch[2].substring(2)}`,
1041
- secondary: hexColors.length > 1 ? hexColors[1] : null
1063
+ secondary: hexColors.length > 1 ? hexColors[1] : null,
1064
+ allColors: hexColors
1042
1065
  };
1043
1066
  }
1044
1067
  }
1045
1068
  }
1046
1069
  return {
1047
1070
  primary: hexColors.length > 0 ? hexColors[0] : null,
1048
- secondary: hexColors.length > 1 ? hexColors[1] : null
1071
+ secondary: hexColors.length > 1 ? hexColors[1] : null,
1072
+ allColors: hexColors
1049
1073
  };
1050
1074
  }
1051
1075
  function extractJSColors(rootDir) {
@@ -1082,14 +1106,16 @@ function extractJSColors(rootDir) {
1082
1106
  );
1083
1107
  return {
1084
1108
  primary: `#${primaryMatch[1]}`,
1085
- secondary: secondaryMatch ? `#${secondaryMatch[1]}` : null
1109
+ secondary: secondaryMatch ? `#${secondaryMatch[1]}` : null,
1110
+ allColors: hexColors
1086
1111
  };
1087
1112
  }
1088
1113
  }
1089
1114
  }
1090
1115
  return {
1091
1116
  primary: hexColors.length > 0 ? hexColors[0] : null,
1092
- secondary: hexColors.length > 1 ? hexColors[1] : null
1117
+ secondary: hexColors.length > 1 ? hexColors[1] : null,
1118
+ allColors: hexColors
1093
1119
  };
1094
1120
  }
1095
1121
  function extractSwiftColors(rootDir) {
@@ -1147,7 +1173,8 @@ function extractSwiftColors(rootDir) {
1147
1173
  }
1148
1174
  return {
1149
1175
  primary: hexColors.length > 0 ? hexColors[0] : null,
1150
- secondary: hexColors.length > 1 ? hexColors[1] : null
1176
+ secondary: hexColors.length > 1 ? hexColors[1] : null,
1177
+ allColors: hexColors
1151
1178
  };
1152
1179
  }
1153
1180
  function extractAndroidColors(rootDir) {
@@ -1196,7 +1223,8 @@ function extractAndroidColors(rootDir) {
1196
1223
  }
1197
1224
  return {
1198
1225
  primary: hexColors.length > 0 ? hexColors[0] : null,
1199
- secondary: hexColors.length > 1 ? hexColors[1] : null
1226
+ secondary: hexColors.length > 1 ? hexColors[1] : null,
1227
+ allColors: hexColors
1200
1228
  };
1201
1229
  }
1202
1230
  function findAssetsCatalogs(rootDir) {
@@ -1369,9 +1397,290 @@ function findIconsInAppIconSet(searchDir) {
1369
1397
  return icons;
1370
1398
  }
1371
1399
 
1400
+ // src/analyzers/font-detector.ts
1401
+ import { existsSync as existsSync4, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
1402
+ import { join as join6, extname as extname2, basename } from "path";
1403
+ import yaml from "yaml";
1404
+ import plist2 from "plist";
1405
+ var SYSTEM_FONTS = /* @__PURE__ */ new Set([
1406
+ "system",
1407
+ "sans-serif",
1408
+ "serif",
1409
+ "monospace",
1410
+ "cursive",
1411
+ "fantasy",
1412
+ "sans-serif-medium",
1413
+ "sans-serif-light",
1414
+ "sans-serif-thin",
1415
+ "sans-serif-black",
1416
+ "sans-serif-condensed",
1417
+ "sans-serif-smallcaps",
1418
+ "sf pro",
1419
+ "sf pro text",
1420
+ "sf pro display",
1421
+ "sf pro rounded",
1422
+ "sf compact",
1423
+ "sf mono",
1424
+ "new york",
1425
+ "helvetica",
1426
+ "helvetica neue",
1427
+ "arial",
1428
+ "times new roman",
1429
+ "courier",
1430
+ "roboto"
1431
+ // Android system font — usually not a brand choice
1432
+ ]);
1433
+ var WEIGHT_SUFFIXES = /[-_ ]?(Regular|Bold|Italic|Light|Medium|SemiBold|ExtraBold|Thin|Black|Heavy|Book|Condensed|Expanded|ExtraLight|UltraLight|UltraBold|DemiBold|Oblique|Roman|Normal|Variable|Display|Text|Mono)\b/gi;
1434
+ function cleanFontName(raw) {
1435
+ let name = raw.replace(/\.(ttf|otf|woff2?|eot)$/i, "");
1436
+ name = name.replace(WEIGHT_SUFFIXES, "");
1437
+ name = name.replace(/[-_]/g, " ").trim();
1438
+ name = name.replace(/\b\w/g, (c) => c.toUpperCase());
1439
+ name = name.replace(/\s+/g, " ").trim();
1440
+ return name;
1441
+ }
1442
+ function camelToTitleCase(camel) {
1443
+ return camel.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (s) => s.toUpperCase());
1444
+ }
1445
+ function fontNameFromFile(fileName) {
1446
+ return cleanFontName(basename(fileName));
1447
+ }
1448
+ function scanForFontFiles(dir, maxDepth = 3) {
1449
+ const fonts = [];
1450
+ function walk(d, depth) {
1451
+ if (depth > maxDepth) return;
1452
+ try {
1453
+ const entries = readdirSync5(d);
1454
+ for (const entry of entries) {
1455
+ if (entry.startsWith(".")) continue;
1456
+ const fullPath = join6(d, entry);
1457
+ try {
1458
+ const stat = statSync3(fullPath);
1459
+ if (stat.isDirectory()) {
1460
+ walk(fullPath, depth + 1);
1461
+ } else {
1462
+ const ext = extname2(entry).toLowerCase();
1463
+ if (ext === ".ttf" || ext === ".otf") {
1464
+ fonts.push(entry);
1465
+ }
1466
+ }
1467
+ } catch {
1468
+ }
1469
+ }
1470
+ } catch {
1471
+ }
1472
+ }
1473
+ if (existsSync4(dir)) walk(dir, 0);
1474
+ return fonts;
1475
+ }
1476
+ function detectFlutterFonts(rootDir) {
1477
+ const fonts = [];
1478
+ const pubspecPath = join6(rootDir, "pubspec.yaml");
1479
+ const pubspecContent = readFileSafe(pubspecPath);
1480
+ if (pubspecContent) {
1481
+ try {
1482
+ const doc = yaml.parse(pubspecContent);
1483
+ const flutterFonts = doc?.flutter?.fonts;
1484
+ if (Array.isArray(flutterFonts)) {
1485
+ for (const entry of flutterFonts) {
1486
+ if (entry.family && typeof entry.family === "string") {
1487
+ fonts.push(entry.family);
1488
+ }
1489
+ }
1490
+ }
1491
+ } catch {
1492
+ }
1493
+ }
1494
+ const dartFiles = findFiles(rootDir, [".dart"], 6);
1495
+ for (const file of dartFiles.slice(0, 80)) {
1496
+ const content = readFileSafe(file);
1497
+ if (!content) continue;
1498
+ const fontFamilyMatches = content.matchAll(/fontFamily\s*:\s*['"]([^'"]+)['"]/g);
1499
+ for (const match of fontFamilyMatches) {
1500
+ fonts.push(match[1]);
1501
+ }
1502
+ const googleFontsMatches = content.matchAll(/GoogleFonts\.(\w+)\s*\(/g);
1503
+ for (const match of googleFontsMatches) {
1504
+ if (match[1] !== "getFont" && match[1] !== "getTextTheme") {
1505
+ fonts.push(camelToTitleCase(match[1]));
1506
+ }
1507
+ }
1508
+ const getFontMatches = content.matchAll(/GoogleFonts\.getFont\s*\(\s*['"]([^'"]+)['"]/g);
1509
+ for (const match of getFontMatches) {
1510
+ fonts.push(match[1]);
1511
+ }
1512
+ }
1513
+ return fonts;
1514
+ }
1515
+ function detectJSFonts(rootDir) {
1516
+ const fonts = [];
1517
+ const jsFiles = findFiles(rootDir, [".ts", ".tsx", ".js", ".jsx"], 5);
1518
+ for (const file of jsFiles.slice(0, 100)) {
1519
+ const content = readFileSafe(file);
1520
+ if (!content) continue;
1521
+ const matches = content.matchAll(/fontFamily\s*:\s*['"]([^'"]+)['"]/g);
1522
+ for (const match of matches) {
1523
+ fonts.push(match[1]);
1524
+ }
1525
+ }
1526
+ const fontDirs = [
1527
+ join6(rootDir, "assets/fonts"),
1528
+ join6(rootDir, "src/assets/fonts"),
1529
+ join6(rootDir, "app/assets/fonts")
1530
+ ];
1531
+ for (const dir of fontDirs) {
1532
+ const fontFiles = scanForFontFiles(dir, 1);
1533
+ for (const f of fontFiles) {
1534
+ fonts.push(fontNameFromFile(f));
1535
+ }
1536
+ }
1537
+ return fonts;
1538
+ }
1539
+ function detectSwiftFonts(rootDir) {
1540
+ const fonts = [];
1541
+ const swiftFiles = findFiles(rootDir, [".swift"], 6);
1542
+ for (const file of swiftFiles.slice(0, 80)) {
1543
+ const content = readFileSafe(file);
1544
+ if (!content) continue;
1545
+ const uiFontMatches = content.matchAll(/UIFont\s*\(\s*name:\s*"([^"]+)"/g);
1546
+ for (const match of uiFontMatches) {
1547
+ fonts.push(match[1]);
1548
+ }
1549
+ const customFontMatches = content.matchAll(/\.?custom\s*\(\s*"([^"]+)"/g);
1550
+ for (const match of customFontMatches) {
1551
+ fonts.push(match[1]);
1552
+ }
1553
+ }
1554
+ const plistPaths = [
1555
+ join6(rootDir, "Info.plist"),
1556
+ ...findPlistFiles(rootDir)
1557
+ ];
1558
+ for (const plistPath of plistPaths) {
1559
+ const content = readFileSafe(plistPath);
1560
+ if (!content) continue;
1561
+ try {
1562
+ const data = plist2.parse(content);
1563
+ const appFonts = data.UIAppFonts;
1564
+ if (Array.isArray(appFonts)) {
1565
+ for (const fontFile of appFonts) {
1566
+ if (typeof fontFile === "string") {
1567
+ fonts.push(fontNameFromFile(fontFile));
1568
+ }
1569
+ }
1570
+ }
1571
+ } catch {
1572
+ }
1573
+ }
1574
+ const fontDirs = [
1575
+ join6(rootDir, "Fonts"),
1576
+ join6(rootDir, "Resources/Fonts")
1577
+ ];
1578
+ for (const dir of fontDirs) {
1579
+ const fontFiles = scanForFontFiles(dir, 2);
1580
+ for (const f of fontFiles) {
1581
+ fonts.push(fontNameFromFile(f));
1582
+ }
1583
+ }
1584
+ return fonts;
1585
+ }
1586
+ function findPlistFiles(rootDir) {
1587
+ const results = [];
1588
+ function search(dir, depth) {
1589
+ if (depth > 4) return;
1590
+ try {
1591
+ const entries = readdirSync5(dir);
1592
+ for (const entry of entries) {
1593
+ if (entry.startsWith(".") || entry === "Pods" || entry === "DerivedData" || entry === "build") continue;
1594
+ const fullPath = join6(dir, entry);
1595
+ if (entry === "Info.plist") {
1596
+ results.push(fullPath);
1597
+ continue;
1598
+ }
1599
+ try {
1600
+ if (statSync3(fullPath).isDirectory()) {
1601
+ search(fullPath, depth + 1);
1602
+ }
1603
+ } catch {
1604
+ }
1605
+ }
1606
+ } catch {
1607
+ }
1608
+ }
1609
+ search(rootDir, 0);
1610
+ return results;
1611
+ }
1612
+ function detectAndroidFonts(rootDir) {
1613
+ const fonts = [];
1614
+ const resFontDirs = [
1615
+ join6(rootDir, "app/src/main/res/font"),
1616
+ join6(rootDir, "src/main/res/font")
1617
+ ];
1618
+ for (const dir of resFontDirs) {
1619
+ const fontFiles = scanForFontFiles(dir, 1);
1620
+ for (const f of fontFiles) {
1621
+ fonts.push(fontNameFromFile(f));
1622
+ }
1623
+ }
1624
+ const xmlFiles = findFiles(rootDir, [".xml"], 5);
1625
+ for (const file of xmlFiles.slice(0, 50)) {
1626
+ const content = readFileSafe(file);
1627
+ if (!content) continue;
1628
+ const matches = content.matchAll(/android:fontFamily\s*=\s*"(?:@font\/)?([^"]+)"/g);
1629
+ for (const match of matches) {
1630
+ const name = match[1];
1631
+ fonts.push(name.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()));
1632
+ }
1633
+ }
1634
+ const ktFiles = findFiles(rootDir, [".kt"], 6);
1635
+ for (const file of ktFiles.slice(0, 80)) {
1636
+ const content = readFileSafe(file);
1637
+ if (!content) continue;
1638
+ const fontResMatches = content.matchAll(/Font\s*\(\s*R\.font\.(\w+)/g);
1639
+ for (const match of fontResMatches) {
1640
+ fonts.push(match[1].replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()));
1641
+ }
1642
+ const fontFamilyMatches = content.matchAll(/fontFamily\s*=\s*FontFamily\s*\(/g);
1643
+ void fontFamilyMatches;
1644
+ }
1645
+ return fonts;
1646
+ }
1647
+ function detectFonts(rootDir, techStack) {
1648
+ let rawFonts;
1649
+ switch (techStack) {
1650
+ case "flutter":
1651
+ rawFonts = detectFlutterFonts(rootDir);
1652
+ break;
1653
+ case "react-native":
1654
+ case "capacitor":
1655
+ rawFonts = detectJSFonts(rootDir);
1656
+ break;
1657
+ case "swift":
1658
+ rawFonts = detectSwiftFonts(rootDir);
1659
+ break;
1660
+ case "kotlin":
1661
+ case "java":
1662
+ rawFonts = detectAndroidFonts(rootDir);
1663
+ break;
1664
+ default:
1665
+ rawFonts = [];
1666
+ }
1667
+ const seen = /* @__PURE__ */ new Set();
1668
+ const result = [];
1669
+ for (const raw of rawFonts) {
1670
+ const cleaned = cleanFontName(raw);
1671
+ if (!cleaned || cleaned.length < 2) continue;
1672
+ const key = cleaned.toLowerCase();
1673
+ if (seen.has(key)) continue;
1674
+ if (SYSTEM_FONTS.has(key)) continue;
1675
+ seen.add(key);
1676
+ result.push(cleaned);
1677
+ }
1678
+ return result.sort().slice(0, 5);
1679
+ }
1680
+
1372
1681
  // src/analyzers/source-reader.ts
1373
- import { readdirSync as readdirSync5, statSync as statSync3 } from "fs";
1374
- import { join as join6, relative as relative2 } from "path";
1682
+ import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
1683
+ import { join as join7, relative as relative2 } from "path";
1375
1684
  var README_NAMES = [
1376
1685
  "README.md",
1377
1686
  "readme.md",
@@ -1410,7 +1719,7 @@ var TREE_IGNORE = /* @__PURE__ */ new Set([
1410
1719
  ]);
1411
1720
  function readReadme(rootDir) {
1412
1721
  for (const name of README_NAMES) {
1413
- const content = readFileSafe(join6(rootDir, name));
1722
+ const content = readFileSafe(join7(rootDir, name));
1414
1723
  if (content && content.trim().length > 0) {
1415
1724
  return content.substring(0, 1e4);
1416
1725
  }
@@ -1430,7 +1739,7 @@ function readSourceCode(rootDir, techStack, maxTotalChars = 5e4) {
1430
1739
  let totalChars = 0;
1431
1740
  for (const file of sorted) {
1432
1741
  if (totalChars >= maxTotalChars) break;
1433
- const content = readFileSafe(join6(rootDir, file));
1742
+ const content = readFileSafe(join7(rootDir, file));
1434
1743
  if (!content || content.trim().length < 20) continue;
1435
1744
  if (isTestFile(file)) continue;
1436
1745
  if (isGeneratedFile(file, content)) continue;
@@ -1525,9 +1834,9 @@ function generateProjectTree(rootDir, maxDepth = 5, maxEntries = 300) {
1525
1834
  if (depth > maxDepth || entryCount >= maxEntries) return;
1526
1835
  let entries;
1527
1836
  try {
1528
- entries = readdirSync5(dir).sort((a, b) => {
1529
- const aIsDir = isDir(join6(dir, a));
1530
- const bIsDir = isDir(join6(dir, b));
1837
+ entries = readdirSync6(dir).sort((a, b) => {
1838
+ const aIsDir = isDir(join7(dir, a));
1839
+ const bIsDir = isDir(join7(dir, b));
1531
1840
  if (aIsDir && !bIsDir) return -1;
1532
1841
  if (!aIsDir && bIsDir) return 1;
1533
1842
  return a.localeCompare(b);
@@ -1546,7 +1855,7 @@ function generateProjectTree(rootDir, maxDepth = 5, maxEntries = 300) {
1546
1855
  return;
1547
1856
  }
1548
1857
  const entry = entries[i];
1549
- const fullPath = join6(dir, entry);
1858
+ const fullPath = join7(dir, entry);
1550
1859
  const isLast = i === entries.length - 1;
1551
1860
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
1552
1861
  const childPrefix = isLast ? " " : "\u2502 ";
@@ -1562,20 +1871,20 @@ function generateProjectTree(rootDir, maxDepth = 5, maxEntries = 300) {
1562
1871
  }
1563
1872
  function isDir(path) {
1564
1873
  try {
1565
- return statSync3(path).isDirectory();
1874
+ return statSync4(path).isDirectory();
1566
1875
  } catch {
1567
1876
  return false;
1568
1877
  }
1569
1878
  }
1570
- const rootName = relative2(join6(rootDir, ".."), rootDir) || "project";
1879
+ const rootName = relative2(join7(rootDir, ".."), rootDir) || "project";
1571
1880
  lines.push(`${rootName}/`);
1572
1881
  walk(rootDir, "", 0);
1573
1882
  return lines.join("\n");
1574
1883
  }
1575
1884
 
1576
1885
  // src/analyzers/asset-scanner.ts
1577
- import { readFileSync as readFileSync3, existsSync as existsSync4, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
1578
- import { join as join7, extname as extname2, relative as relative3, basename } from "path";
1886
+ import { readFileSync as readFileSync4, existsSync as existsSync5, readdirSync as readdirSync7, statSync as statSync5 } from "fs";
1887
+ import { join as join8, extname as extname3, relative as relative3, basename as basename2 } from "path";
1579
1888
  var MAX_ASSET_SIZE = 1 * 1024 * 1024;
1580
1889
  var MAX_TOTAL_BASE64_SIZE = 3 * 1024 * 1024;
1581
1890
  var MAX_TOTAL_ASSETS = 10;
@@ -1584,25 +1893,25 @@ var MIN_ASSET_SIZE = 500;
1584
1893
  var MIN_SCREENSHOT_SIZE = 1e4;
1585
1894
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp"]);
1586
1895
  function getMimeType(filePath) {
1587
- const ext = extname2(filePath).toLowerCase();
1896
+ const ext = extname3(filePath).toLowerCase();
1588
1897
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
1589
1898
  if (ext === ".webp") return "image/webp";
1590
1899
  return "image/png";
1591
1900
  }
1592
1901
  function isImageFile(filePath) {
1593
- return IMAGE_EXTENSIONS.has(extname2(filePath).toLowerCase());
1902
+ return IMAGE_EXTENSIONS.has(extname3(filePath).toLowerCase());
1594
1903
  }
1595
1904
  function tryReadImage(rootDir, filePath, assetType, minSize = MIN_ASSET_SIZE) {
1596
1905
  try {
1597
- if (!existsSync4(filePath)) return null;
1598
- const stat = statSync4(filePath);
1906
+ if (!existsSync5(filePath)) return null;
1907
+ const stat = statSync5(filePath);
1599
1908
  if (!stat.isFile()) return null;
1600
1909
  if (stat.size < minSize) return null;
1601
1910
  if (stat.size > MAX_ASSET_SIZE) return null;
1602
- const buffer = readFileSync3(filePath);
1911
+ const buffer = readFileSync4(filePath);
1603
1912
  return {
1604
1913
  asset_type: assetType,
1605
- file_name: basename(filePath),
1914
+ file_name: basename2(filePath),
1606
1915
  mime_type: getMimeType(filePath),
1607
1916
  base64_data: buffer.toString("base64"),
1608
1917
  width: null,
@@ -1615,13 +1924,13 @@ function tryReadImage(rootDir, filePath, assetType, minSize = MIN_ASSET_SIZE) {
1615
1924
  }
1616
1925
  function collectImagesRecursive(rootDir, dirPath, assetType, assets, maxCount, maxDepth = 3, currentDepth = 0, minSize = MIN_ASSET_SIZE) {
1617
1926
  if (currentDepth > maxDepth) return;
1618
- if (!existsSync4(dirPath)) return;
1927
+ if (!existsSync5(dirPath)) return;
1619
1928
  try {
1620
- const entries = readdirSync6(dirPath);
1929
+ const entries = readdirSync7(dirPath);
1621
1930
  for (const entry of entries) {
1622
1931
  if (assets.filter((a) => a.asset_type === assetType).length >= maxCount) return;
1623
1932
  if (assets.length >= MAX_TOTAL_ASSETS) return;
1624
- const fullPath = join7(dirPath, entry);
1933
+ const fullPath = join8(dirPath, entry);
1625
1934
  if (isImageFile(entry)) {
1626
1935
  const asset = tryReadImage(rootDir, fullPath, assetType, minSize);
1627
1936
  if (asset) assets.push(asset);
@@ -1629,9 +1938,9 @@ function collectImagesRecursive(rootDir, dirPath, assetType, assets, maxCount, m
1629
1938
  }
1630
1939
  for (const entry of entries) {
1631
1940
  if (assets.filter((a) => a.asset_type === assetType).length >= maxCount) return;
1632
- const fullPath = join7(dirPath, entry);
1941
+ const fullPath = join8(dirPath, entry);
1633
1942
  try {
1634
- if (statSync4(fullPath).isDirectory() && !entry.startsWith(".")) {
1943
+ if (statSync5(fullPath).isDirectory() && !entry.startsWith(".")) {
1635
1944
  collectImagesRecursive(rootDir, fullPath, assetType, assets, maxCount, maxDepth, currentDepth + 1, minSize);
1636
1945
  }
1637
1946
  } catch {
@@ -1645,53 +1954,53 @@ function getScreenshotDirs(rootDir, techStack) {
1645
1954
  const ss = (p) => ({ path: p, minSize: MIN_ASSET_SIZE });
1646
1955
  const gen = (p) => ({ path: p, minSize: MIN_SCREENSHOT_SIZE });
1647
1956
  dirs.push(
1648
- ss(join7(rootDir, "screenshots")),
1649
- ss(join7(rootDir, "Screenshots")),
1650
- ss(join7(rootDir, "assets/screenshots")),
1651
- ss(join7(rootDir, "assets/images/screenshots"))
1957
+ ss(join8(rootDir, "screenshots")),
1958
+ ss(join8(rootDir, "Screenshots")),
1959
+ ss(join8(rootDir, "assets/screenshots")),
1960
+ ss(join8(rootDir, "assets/images/screenshots"))
1652
1961
  );
1653
1962
  dirs.push(
1654
- ss(join7(rootDir, "fastlane/screenshots")),
1655
- ss(join7(rootDir, "fastlane/metadata/en-US/images/phoneScreenshots")),
1656
- ss(join7(rootDir, "fastlane/metadata/en-US/images/tabletScreenshots"))
1963
+ ss(join8(rootDir, "fastlane/screenshots")),
1964
+ ss(join8(rootDir, "fastlane/metadata/en-US/images/phoneScreenshots")),
1965
+ ss(join8(rootDir, "fastlane/metadata/en-US/images/tabletScreenshots"))
1657
1966
  );
1658
1967
  switch (techStack) {
1659
1968
  case "flutter":
1660
1969
  dirs.push(
1661
- ss(join7(rootDir, "assets/screenshots")),
1662
- ss(join7(rootDir, "metadata/screenshots")),
1970
+ ss(join8(rootDir, "assets/screenshots")),
1971
+ ss(join8(rootDir, "metadata/screenshots")),
1663
1972
  // General Flutter asset directories — use higher threshold
1664
- gen(join7(rootDir, "assets/images")),
1665
- gen(join7(rootDir, "assets/img")),
1666
- gen(join7(rootDir, "assets"))
1973
+ gen(join8(rootDir, "assets/images")),
1974
+ gen(join8(rootDir, "assets/img")),
1975
+ gen(join8(rootDir, "assets"))
1667
1976
  );
1668
1977
  break;
1669
1978
  case "react-native":
1670
1979
  case "capacitor":
1671
1980
  dirs.push(
1672
- ss(join7(rootDir, "src/assets/screenshots")),
1673
- gen(join7(rootDir, "src/assets/images")),
1674
- gen(join7(rootDir, "src/assets")),
1675
- ss(join7(rootDir, "docs/screenshots")),
1676
- gen(join7(rootDir, "assets/images")),
1677
- gen(join7(rootDir, "assets"))
1981
+ ss(join8(rootDir, "src/assets/screenshots")),
1982
+ gen(join8(rootDir, "src/assets/images")),
1983
+ gen(join8(rootDir, "src/assets")),
1984
+ ss(join8(rootDir, "docs/screenshots")),
1985
+ gen(join8(rootDir, "assets/images")),
1986
+ gen(join8(rootDir, "assets"))
1678
1987
  );
1679
1988
  break;
1680
1989
  case "swift":
1681
1990
  dirs.push(
1682
- ss(join7(rootDir, "fastlane/screenshots/en-US")),
1683
- ss(join7(rootDir, "fastlane/metadata/en-US/images/phoneScreenshots")),
1684
- gen(join7(rootDir, "marketing")),
1685
- gen(join7(rootDir, "Marketing"))
1991
+ ss(join8(rootDir, "fastlane/screenshots/en-US")),
1992
+ ss(join8(rootDir, "fastlane/metadata/en-US/images/phoneScreenshots")),
1993
+ gen(join8(rootDir, "marketing")),
1994
+ gen(join8(rootDir, "Marketing"))
1686
1995
  );
1687
1996
  break;
1688
1997
  case "kotlin":
1689
1998
  case "java":
1690
1999
  dirs.push(
1691
- ss(join7(rootDir, "fastlane/metadata/android/en-US/images/phoneScreenshots")),
1692
- gen(join7(rootDir, "metadata/android/en-US/images")),
1693
- gen(join7(rootDir, "marketing")),
1694
- gen(join7(rootDir, "Marketing"))
2000
+ ss(join8(rootDir, "fastlane/metadata/android/en-US/images/phoneScreenshots")),
2001
+ gen(join8(rootDir, "metadata/android/en-US/images")),
2002
+ gen(join8(rootDir, "marketing")),
2003
+ gen(join8(rootDir, "Marketing"))
1695
2004
  );
1696
2005
  break;
1697
2006
  }
@@ -1702,39 +2011,39 @@ function getSplashPaths(rootDir, techStack) {
1702
2011
  switch (techStack) {
1703
2012
  case "flutter":
1704
2013
  paths.push(
1705
- join7(rootDir, "assets/splash.png"),
1706
- join7(rootDir, "assets/images/splash.png"),
1707
- join7(rootDir, "assets/splash/splash.png"),
1708
- join7(rootDir, "assets/images/splash_screen.png"),
2014
+ join8(rootDir, "assets/splash.png"),
2015
+ join8(rootDir, "assets/images/splash.png"),
2016
+ join8(rootDir, "assets/splash/splash.png"),
2017
+ join8(rootDir, "assets/images/splash_screen.png"),
1709
2018
  // flutter_native_splash output paths
1710
- join7(rootDir, "android/app/src/main/res/drawable-xxhdpi/android12splash.png"),
1711
- join7(rootDir, "android/app/src/main/res/drawable-xxxhdpi/android12splash.png"),
1712
- join7(rootDir, "ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png"),
1713
- join7(rootDir, "ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png"),
1714
- join7(rootDir, "ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png")
2019
+ join8(rootDir, "android/app/src/main/res/drawable-xxhdpi/android12splash.png"),
2020
+ join8(rootDir, "android/app/src/main/res/drawable-xxxhdpi/android12splash.png"),
2021
+ join8(rootDir, "ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png"),
2022
+ join8(rootDir, "ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png"),
2023
+ join8(rootDir, "ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png")
1715
2024
  );
1716
2025
  for (const density of ["xxxhdpi", "xxhdpi", "xhdpi"]) {
1717
2026
  paths.push(
1718
- join7(rootDir, `android/app/src/main/res/drawable-${density}/splash.png`),
1719
- join7(rootDir, `android/app/src/main/res/drawable-${density}/launch_screen.png`)
2027
+ join8(rootDir, `android/app/src/main/res/drawable-${density}/splash.png`),
2028
+ join8(rootDir, `android/app/src/main/res/drawable-${density}/launch_screen.png`)
1720
2029
  );
1721
2030
  }
1722
2031
  break;
1723
2032
  case "react-native":
1724
2033
  case "capacitor":
1725
2034
  paths.push(
1726
- join7(rootDir, "assets/splash.png"),
1727
- join7(rootDir, "src/assets/splash.png"),
1728
- join7(rootDir, "assets/images/splash.png"),
1729
- join7(rootDir, "resources/splash.png")
2035
+ join8(rootDir, "assets/splash.png"),
2036
+ join8(rootDir, "src/assets/splash.png"),
2037
+ join8(rootDir, "assets/images/splash.png"),
2038
+ join8(rootDir, "resources/splash.png")
1730
2039
  );
1731
2040
  break;
1732
2041
  case "kotlin":
1733
2042
  case "java":
1734
2043
  for (const density of ["xxxhdpi", "xxhdpi", "xhdpi"]) {
1735
2044
  paths.push(
1736
- join7(rootDir, `app/src/main/res/drawable-${density}/splash.png`),
1737
- join7(rootDir, `app/src/main/res/drawable-${density}/launch_screen.png`)
2045
+ join8(rootDir, `app/src/main/res/drawable-${density}/splash.png`),
2046
+ join8(rootDir, `app/src/main/res/drawable-${density}/launch_screen.png`)
1738
2047
  );
1739
2048
  }
1740
2049
  break;
@@ -1744,23 +2053,23 @@ function getSplashPaths(rootDir, techStack) {
1744
2053
  function getFeatureGraphicPaths(rootDir, techStack) {
1745
2054
  const paths = [];
1746
2055
  paths.push(
1747
- join7(rootDir, "fastlane/metadata/android/en-US/images/featureGraphic.png"),
1748
- join7(rootDir, "fastlane/metadata/android/en-US/images/featureGraphic.jpg"),
1749
- join7(rootDir, "metadata/android/en-US/images/featureGraphic.png")
2056
+ join8(rootDir, "fastlane/metadata/android/en-US/images/featureGraphic.png"),
2057
+ join8(rootDir, "fastlane/metadata/android/en-US/images/featureGraphic.jpg"),
2058
+ join8(rootDir, "metadata/android/en-US/images/featureGraphic.png")
1750
2059
  );
1751
2060
  switch (techStack) {
1752
2061
  case "flutter":
1753
2062
  case "react-native":
1754
2063
  case "capacitor":
1755
2064
  paths.push(
1756
- join7(rootDir, "assets/feature_graphic.png"),
1757
- join7(rootDir, "assets/feature-graphic.png")
2065
+ join8(rootDir, "assets/feature_graphic.png"),
2066
+ join8(rootDir, "assets/feature-graphic.png")
1758
2067
  );
1759
2068
  break;
1760
2069
  case "kotlin":
1761
2070
  case "java":
1762
2071
  paths.push(
1763
- join7(rootDir, "app/src/main/feature_graphic.png")
2072
+ join8(rootDir, "app/src/main/feature_graphic.png")
1764
2073
  );
1765
2074
  break;
1766
2075
  }
@@ -1769,14 +2078,14 @@ function getFeatureGraphicPaths(rootDir, techStack) {
1769
2078
  function getPromotionalPaths(rootDir, techStack) {
1770
2079
  const paths = [];
1771
2080
  paths.push(
1772
- join7(rootDir, "fastlane/metadata/android/en-US/images/promoGraphic.png"),
1773
- join7(rootDir, "fastlane/metadata/android/en-US/images/promoGraphic.jpg"),
1774
- join7(rootDir, "assets/promo.png"),
1775
- join7(rootDir, "assets/promotional.png")
2081
+ join8(rootDir, "fastlane/metadata/android/en-US/images/promoGraphic.png"),
2082
+ join8(rootDir, "fastlane/metadata/android/en-US/images/promoGraphic.jpg"),
2083
+ join8(rootDir, "assets/promo.png"),
2084
+ join8(rootDir, "assets/promotional.png")
1776
2085
  );
1777
2086
  if (techStack === "swift") {
1778
2087
  paths.push(
1779
- join7(rootDir, "fastlane/metadata/en-US/promotional.png")
2088
+ join8(rootDir, "fastlane/metadata/en-US/promotional.png")
1780
2089
  );
1781
2090
  }
1782
2091
  return paths;
@@ -2027,14 +2336,36 @@ function createClaudeProvider(apiKey) {
2027
2336
  }
2028
2337
  };
2029
2338
  }
2030
- function detectProvider() {
2031
- const geminiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_AI_API_KEY;
2032
- if (geminiKey) return createGeminiProvider(geminiKey);
2033
- const openaiKey = process.env.OPENAI_API_KEY;
2034
- if (openaiKey) return createOpenAIProvider(openaiKey);
2339
+ function getAvailableProviders() {
2340
+ const providers = [];
2035
2341
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
2036
- if (anthropicKey) return createClaudeProvider(anthropicKey);
2037
- throw new Error("NO_API_KEY");
2342
+ if (anthropicKey) {
2343
+ providers.push({
2344
+ id: "claude",
2345
+ name: "Claude",
2346
+ recommended: true,
2347
+ create: () => createClaudeProvider(anthropicKey)
2348
+ });
2349
+ }
2350
+ const openaiKey = process.env.OPENAI_API_KEY;
2351
+ if (openaiKey) {
2352
+ providers.push({
2353
+ id: "openai",
2354
+ name: "OpenAI",
2355
+ recommended: false,
2356
+ create: () => createOpenAIProvider(openaiKey)
2357
+ });
2358
+ }
2359
+ const geminiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_AI_API_KEY;
2360
+ if (geminiKey) {
2361
+ providers.push({
2362
+ id: "gemini",
2363
+ name: "Gemini",
2364
+ recommended: false,
2365
+ create: () => createGeminiProvider(geminiKey)
2366
+ });
2367
+ }
2368
+ return providers;
2038
2369
  }
2039
2370
 
2040
2371
  // src/commands/analyze.ts
@@ -2057,18 +2388,37 @@ async function analyzeCommand(options) {
2057
2388
  chalk.bold(" Forvibe CLI") + chalk.gray(" \u2014 AI-powered App Store automation")
2058
2389
  );
2059
2390
  console.log();
2060
- let provider;
2061
- try {
2062
- provider = detectProvider();
2063
- } catch {
2391
+ const availableProviders = getAvailableProviders();
2392
+ if (availableProviders.length === 0) {
2064
2393
  console.log(chalk.red(" \u2717 No AI API key found. Set one of the following:\n"));
2065
- console.log(chalk.cyan(" export GEMINI_API_KEY=your-key") + chalk.gray(" https://aistudio.google.com/apikey"));
2394
+ console.log(chalk.cyan(" export ANTHROPIC_API_KEY=your-key") + chalk.gray(" https://console.anthropic.com/settings/keys") + chalk.green(" (recommended)"));
2066
2395
  console.log(chalk.cyan(" export OPENAI_API_KEY=your-key") + chalk.gray(" https://platform.openai.com/api-keys"));
2067
- console.log(chalk.cyan(" export ANTHROPIC_API_KEY=your-key") + chalk.gray(" https://console.anthropic.com/settings/keys"));
2396
+ console.log(chalk.cyan(" export GEMINI_API_KEY=your-key") + chalk.gray(" https://aistudio.google.com/apikey"));
2068
2397
  console.log();
2069
2398
  console.log(chalk.gray(" Your source code is analyzed locally \u2014 it never leaves your machine.\n"));
2070
2399
  process.exit(1);
2071
2400
  }
2401
+ let provider;
2402
+ if (availableProviders.length === 1) {
2403
+ provider = availableProviders[0].create();
2404
+ } else {
2405
+ console.log(chalk.white(" Multiple AI API keys detected. Choose a provider:\n"));
2406
+ availableProviders.forEach((p, i) => {
2407
+ const label = `${i + 1}. ${p.name}${p.recommended ? chalk.green(" (recommended)") : ""}`;
2408
+ console.log(` ${label}`);
2409
+ });
2410
+ console.log();
2411
+ const answer = await askQuestion(chalk.cyan(` Enter choice (1-${availableProviders.length}): `));
2412
+ const index = parseInt(answer, 10) - 1;
2413
+ if (isNaN(index) || index < 0 || index >= availableProviders.length) {
2414
+ const recommended = availableProviders.find((p) => p.recommended) || availableProviders[0];
2415
+ provider = recommended.create();
2416
+ console.log(chalk.gray(` Using ${recommended.name} (default)
2417
+ `));
2418
+ } else {
2419
+ provider = availableProviders[index].create();
2420
+ }
2421
+ }
2072
2422
  console.log(chalk.gray(` AI Provider: ${provider.name} \u2713`));
2073
2423
  const otcCode = await askQuestion(
2074
2424
  chalk.cyan(" \u{1F517} Enter your Forvibe connection code: ")
@@ -2143,6 +2493,14 @@ async function analyzeCommand(options) {
2143
2493
  brandingSpinner.succeed(
2144
2494
  `Branding: ${chalk.gray(brandingDetails || "no colors/icon detected")}`
2145
2495
  );
2496
+ const fontSpinner = ora({
2497
+ text: "Detecting fonts...",
2498
+ prefixText: " "
2499
+ }).start();
2500
+ const detectedFonts = detectFonts(rootDir, techStack.stack);
2501
+ fontSpinner.succeed(
2502
+ `Fonts: ${detectedFonts.length > 0 ? chalk.white(detectedFonts.join(", ")) : chalk.gray("none detected")}`
2503
+ );
2146
2504
  const assetSpinner = ora({
2147
2505
  text: "Scanning app assets...",
2148
2506
  prefixText: " "
@@ -2175,7 +2533,7 @@ async function analyzeCommand(options) {
2175
2533
  prefixText: " "
2176
2534
  }).start();
2177
2535
  try {
2178
- const { generateReport } = await import("./report-generator-QV2BWI2J.js");
2536
+ const { generateReport } = await import("./report-generator-7E72G6KB.js");
2179
2537
  report = await generateReport(
2180
2538
  { techStack, config, sdkScan, branding, readmeContent, sourceCode, projectTree },
2181
2539
  provider
@@ -2183,6 +2541,12 @@ async function analyzeCommand(options) {
2183
2541
  if (appAssets.length > 0) {
2184
2542
  report.app_assets = appAssets;
2185
2543
  }
2544
+ if (branding.brand_colors.length > 0) {
2545
+ report.brand_colors = branding.brand_colors;
2546
+ }
2547
+ if (detectedFonts.length > 0) {
2548
+ report.detected_fonts = detectedFonts;
2549
+ }
2186
2550
  aiSpinner.succeed(chalk.green("Analysis complete!"));
2187
2551
  } catch (error) {
2188
2552
  aiSpinner.fail(
@@ -2219,7 +2583,8 @@ async function analyzeCommand(options) {
2219
2583
  console.log(` Audience: ${chalk.white(report.target_audience.length > 100 ? report.target_audience.substring(0, 100) + "..." : report.target_audience)}`);
2220
2584
  console.log(` SDKs: ${chalk.white(String(report.detected_sdks.length))} detected`);
2221
2585
  console.log(` Data: ${chalk.white(report.data_collected.join(", ") || "none")}`);
2222
- console.log(` Colors: ${chalk.hex(report.primary_color)("\u25A0")} ${report.primary_color} ${report.secondary_color ? `${chalk.hex(report.secondary_color)("\u25A0")} ${report.secondary_color}` : ""}`);
2586
+ console.log(` Colors: ${chalk.hex(report.primary_color)("\u25A0")} ${report.primary_color} ${report.secondary_color ? `${chalk.hex(report.secondary_color)("\u25A0")} ${report.secondary_color}` : ""}${report.brand_colors?.length ? chalk.gray(` +${report.brand_colors.length} brand colors`) : ""}`);
2587
+ console.log(` Fonts: ${report.detected_fonts?.length ? chalk.white(report.detected_fonts.join(", ")) : chalk.gray("none detected")}`);
2223
2588
  console.log(` Icon: ${report.app_icon_base64 ? chalk.green("\u2713 found") : chalk.gray("not found")}`);
2224
2589
  console.log(` Assets: ${report.app_assets?.length ? chalk.green(`${report.app_assets.length} found`) : chalk.gray("none")}`);
2225
2590
  console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
@@ -142,6 +142,7 @@ async function generateReport(input, provider) {
142
142
  // Branding
143
143
  primary_color: input.branding.primary_color || "#007AFF",
144
144
  secondary_color: input.branding.secondary_color || "#5856D6",
145
+ brand_colors: input.branding.brand_colors.length > 0 ? input.branding.brand_colors : void 0,
145
146
  app_icon_base64: input.branding.app_icon_base64,
146
147
  // Raw data
147
148
  detected_sdks: input.sdkScan.detected_sdks,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forvibe/cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Forvibe CLI - AI-powered project analyzer for App Store automation",
5
5
  "type": "module",
6
6
  "bin": {