@forvibe/cli 1.1.1 → 2.0.1

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 (2) hide show
  1. package/dist/index.js +933 -152
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -129,7 +129,7 @@ function findFiles(rootDir, extensions, maxDepth = 5) {
129
129
  (f) => join(rootDir, f)
130
130
  );
131
131
  }
132
- function readFileSafe(filePath) {
132
+ function readFileSafe2(filePath) {
133
133
  try {
134
134
  if (!existsSync(filePath)) return null;
135
135
  const stat = statSync(filePath);
@@ -155,6 +155,45 @@ var DETECTION_RULES = [
155
155
  return files.length >= 2 ? files : [];
156
156
  }
157
157
  },
158
+ {
159
+ stack: "expo",
160
+ label: "Expo (React Native)",
161
+ platforms: ["ios", "android"],
162
+ detect: (dir) => {
163
+ const files = [];
164
+ const pkgPath = join2(dir, "package.json");
165
+ const pkgContent = existsSync2(pkgPath) ? readFileSafe2(pkgPath) : null;
166
+ let hasExpoDep = false;
167
+ if (pkgContent) {
168
+ try {
169
+ const pkg = JSON.parse(pkgContent);
170
+ const allDeps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
171
+ if ("expo" in allDeps) {
172
+ hasExpoDep = true;
173
+ }
174
+ } catch {
175
+ }
176
+ }
177
+ let hasExpoBlock = false;
178
+ const appJsonPath = join2(dir, "app.json");
179
+ if (existsSync2(appJsonPath)) {
180
+ const appJson = readFileSafe2(appJsonPath);
181
+ if (appJson && /"expo"\s*:\s*\{/.test(appJson)) {
182
+ hasExpoBlock = true;
183
+ }
184
+ }
185
+ const hasAppConfig = existsSync2(join2(dir, "app.config.js")) || existsSync2(join2(dir, "app.config.ts"));
186
+ const hasEas = existsSync2(join2(dir, "eas.json"));
187
+ if (hasExpoDep || hasExpoBlock || hasEas) {
188
+ if (pkgContent) files.push("package.json");
189
+ if (hasExpoBlock) files.push("app.json");
190
+ if (hasAppConfig) files.push(existsSync2(join2(dir, "app.config.ts")) ? "app.config.ts" : "app.config.js");
191
+ if (hasEas) files.push("eas.json");
192
+ if (existsSync2(join2(dir, "app"))) files.push("app/");
193
+ }
194
+ return files;
195
+ }
196
+ },
158
197
  {
159
198
  stack: "react-native",
160
199
  label: "React Native",
@@ -163,7 +202,7 @@ var DETECTION_RULES = [
163
202
  const files = [];
164
203
  const pkgPath = join2(dir, "package.json");
165
204
  if (existsSync2(pkgPath)) {
166
- const content = readFileSafe(pkgPath);
205
+ const content = readFileSafe2(pkgPath);
167
206
  if (content && content.includes('"react-native"')) {
168
207
  files.push("package.json");
169
208
  if (existsSync2(join2(dir, "app.json"))) files.push("app.json");
@@ -255,8 +294,10 @@ var DETECTION_RULES = [
255
294
  const entries = readdirSync2(dir);
256
295
  for (const entry of entries) {
257
296
  if (entry.endsWith(".csproj")) {
258
- const content = readFileSafe(join2(dir, entry));
259
- if (content && (content.includes("Maui") || content.includes("MAUI"))) {
297
+ const content = readFileSafe2(join2(dir, entry));
298
+ if (!content) continue;
299
+ const isMaui = /<UseMaui>\s*true\s*<\/UseMaui>/i.test(content) || /Microsoft\.Maui\.Controls/.test(content) || /Microsoft\.Maui\.Sdk/.test(content) || /-android|-ios|-maccatalyst/.test(content);
300
+ if (isMaui) {
260
301
  files.push(entry);
261
302
  }
262
303
  }
@@ -264,6 +305,7 @@ var DETECTION_RULES = [
264
305
  } catch {
265
306
  }
266
307
  if (existsSync2(join2(dir, "Platforms"))) files.push("Platforms/");
308
+ if (existsSync2(join2(dir, "Resources"))) files.push("Resources/");
267
309
  return files.length >= 1 ? files : [];
268
310
  }
269
311
  }
@@ -297,12 +339,13 @@ function parseConfig(rootDir, techStack) {
297
339
  switch (techStack) {
298
340
  case "flutter":
299
341
  return parseFlutterConfig(rootDir);
342
+ case "expo":
343
+ return parseExpoConfig(rootDir);
300
344
  case "react-native":
301
345
  return parseReactNativeConfig(rootDir);
302
346
  case "swift":
303
347
  return parseSwiftConfig(rootDir);
304
348
  case "kotlin":
305
- case "java":
306
349
  return parseAndroidConfig(rootDir);
307
350
  case "capacitor":
308
351
  return parseCapacitorConfig(rootDir);
@@ -324,7 +367,7 @@ function emptyConfig() {
324
367
  }
325
368
  function parseFlutterConfig(rootDir) {
326
369
  const config = emptyConfig();
327
- const pubspecContent = readFileSafe(join3(rootDir, "pubspec.yaml"));
370
+ const pubspecContent = readFileSafe2(join3(rootDir, "pubspec.yaml"));
328
371
  if (pubspecContent) {
329
372
  try {
330
373
  const pubspec = YAML.parse(pubspecContent);
@@ -349,7 +392,7 @@ function parseFlutterConfig(rootDir) {
349
392
  if (!config.bundle_id || config.bundle_id.includes("$(")) {
350
393
  const pbxprojPath = findFile(join3(rootDir, "ios"), "project.pbxproj", 4);
351
394
  if (pbxprojPath) {
352
- const content = readFileSafe(pbxprojPath);
395
+ const content = readFileSafe2(pbxprojPath);
353
396
  if (content) {
354
397
  const bundleMatch = content.match(
355
398
  /PRODUCT_BUNDLE_IDENTIFIER\s*=\s*"?([^";]+)"?/
@@ -370,9 +413,85 @@ function parseFlutterConfig(rootDir) {
370
413
  }
371
414
  return config;
372
415
  }
416
+ function parseExpoConfig(rootDir) {
417
+ const config = emptyConfig();
418
+ const appJsonContent = readFileSafe2(join3(rootDir, "app.json"));
419
+ if (appJsonContent) {
420
+ try {
421
+ const appJson = JSON.parse(appJsonContent);
422
+ const expo = appJson.expo || appJson;
423
+ config.app_name = expo.name || appJson.displayName || appJson.name || null;
424
+ config.version = expo.version || null;
425
+ config.bundle_id = expo.ios?.bundleIdentifier || expo.android?.package || null;
426
+ config.description = expo.description || null;
427
+ config.min_ios_version = expo.ios?.deploymentTarget || null;
428
+ } catch {
429
+ }
430
+ }
431
+ if (!config.app_name || !config.bundle_id || !config.version) {
432
+ for (const name of ["app.config.ts", "app.config.js"]) {
433
+ const content = readFileSafe2(join3(rootDir, name));
434
+ if (!content) continue;
435
+ if (!config.app_name) {
436
+ const m = content.match(/name\s*:\s*['"]([^'"]+)['"]/);
437
+ if (m) config.app_name = m[1];
438
+ }
439
+ if (!config.version) {
440
+ const m = content.match(/version\s*:\s*['"]([^'"]+)['"]/);
441
+ if (m) config.version = m[1];
442
+ }
443
+ if (!config.bundle_id) {
444
+ const m = content.match(/bundleIdentifier\s*:\s*['"]([^'"]+)['"]/) || content.match(/package\s*:\s*['"]([^'"]+)['"]/);
445
+ if (m) config.bundle_id = m[1];
446
+ }
447
+ if (!config.description) {
448
+ const m = content.match(/description\s*:\s*['"]([^'"]+)['"]/);
449
+ if (m) config.description = m[1];
450
+ }
451
+ }
452
+ }
453
+ const pkgContent = readFileSafe2(join3(rootDir, "package.json"));
454
+ if (pkgContent) {
455
+ try {
456
+ const pkg = JSON.parse(pkgContent);
457
+ if (!config.version) config.version = pkg.version || null;
458
+ if (!config.description) config.description = pkg.description || null;
459
+ if (!config.app_name) config.app_name = pkg.name || null;
460
+ } catch {
461
+ }
462
+ }
463
+ if (!config.bundle_id || !config.app_name) {
464
+ const infoPlistPath = findFile(join3(rootDir, "ios"), "Info.plist", 4);
465
+ if (infoPlistPath) {
466
+ const plistData = parsePlist(infoPlistPath);
467
+ if (plistData) {
468
+ if (!config.bundle_id) {
469
+ const id = plistData.CFBundleIdentifier;
470
+ if (id && !isXcodeVariable(id)) config.bundle_id = id;
471
+ }
472
+ if (!config.app_name) {
473
+ const display = plistData.CFBundleDisplayName || plistData.CFBundleName;
474
+ if (display && !isXcodeVariable(display)) config.app_name = display;
475
+ }
476
+ }
477
+ }
478
+ }
479
+ if (!config.bundle_id) {
480
+ const buildGradlePath = findFile(join3(rootDir, "android/app"), "build.gradle", 2) || findFile(join3(rootDir, "android/app"), "build.gradle.kts", 2);
481
+ if (buildGradlePath) {
482
+ const gradleConfig = parseGradle(buildGradlePath);
483
+ if (gradleConfig.applicationId) config.bundle_id = gradleConfig.applicationId;
484
+ if (!config.min_android_sdk) config.min_android_sdk = gradleConfig.minSdk || null;
485
+ }
486
+ }
487
+ if (!config.app_name) {
488
+ config.app_name = readAndroidAppName(join3(rootDir, "android"));
489
+ }
490
+ return config;
491
+ }
373
492
  function parseReactNativeConfig(rootDir) {
374
493
  const config = emptyConfig();
375
- const appJsonContent = readFileSafe(join3(rootDir, "app.json"));
494
+ const appJsonContent = readFileSafe2(join3(rootDir, "app.json"));
376
495
  if (appJsonContent) {
377
496
  try {
378
497
  const appJson = JSON.parse(appJsonContent);
@@ -380,7 +499,18 @@ function parseReactNativeConfig(rootDir) {
380
499
  } catch {
381
500
  }
382
501
  }
383
- const pkgContent = readFileSafe(join3(rootDir, "package.json"));
502
+ if (!config.app_name) {
503
+ for (const name of ["app.config.ts", "app.config.js"]) {
504
+ const content = readFileSafe2(join3(rootDir, name));
505
+ if (!content) continue;
506
+ const m = content.match(/name\s*:\s*['"]([^'"]+)['"]/);
507
+ if (m) {
508
+ config.app_name = m[1];
509
+ break;
510
+ }
511
+ }
512
+ }
513
+ const pkgContent = readFileSafe2(join3(rootDir, "package.json"));
384
514
  if (pkgContent) {
385
515
  try {
386
516
  const pkg = JSON.parse(pkgContent);
@@ -396,12 +526,18 @@ function parseReactNativeConfig(rootDir) {
396
526
  if (plistData) {
397
527
  config.bundle_id = plistData.CFBundleIdentifier || null;
398
528
  config.min_ios_version = plistData.MinimumOSVersion || null;
529
+ if (!config.app_name) {
530
+ const display = plistData.CFBundleDisplayName || plistData.CFBundleName;
531
+ if (display && !isXcodeVariable(display)) {
532
+ config.app_name = display;
533
+ }
534
+ }
399
535
  }
400
536
  }
401
537
  if (!config.bundle_id || config.bundle_id.includes("$(")) {
402
538
  const pbxprojPath = findFile(join3(rootDir, "ios"), "project.pbxproj", 4);
403
539
  if (pbxprojPath) {
404
- const content = readFileSafe(pbxprojPath);
540
+ const content = readFileSafe2(pbxprojPath);
405
541
  if (content) {
406
542
  const bundleMatch = content.match(
407
543
  /PRODUCT_BUNDLE_IDENTIFIER\s*=\s*"?([^";]+)"?/
@@ -420,6 +556,9 @@ function parseReactNativeConfig(rootDir) {
420
556
  }
421
557
  config.min_android_sdk = gradleConfig.minSdk || null;
422
558
  }
559
+ if (!config.app_name) {
560
+ config.app_name = readAndroidAppName(join3(rootDir, "android"));
561
+ }
423
562
  return config;
424
563
  }
425
564
  var EXTENSION_SUFFIXES = [
@@ -510,7 +649,7 @@ function parseSwiftConfig(rootDir) {
510
649
  const config = emptyConfig();
511
650
  const pbxprojPath = findFile(rootDir, "project.pbxproj", 5);
512
651
  if (pbxprojPath) {
513
- const content = readFileSafe(pbxprojPath);
652
+ const content = readFileSafe2(pbxprojPath);
514
653
  if (content) {
515
654
  const pbx = parsePbxproj(content);
516
655
  config.bundle_id = pbx.bundleId;
@@ -562,34 +701,26 @@ function parseSwiftConfig(rootDir) {
562
701
  }
563
702
  function parseAndroidConfig(rootDir) {
564
703
  const config = emptyConfig();
565
- const buildGradlePath = findFile(join3(rootDir, "app"), "build.gradle", 2) || findFile(join3(rootDir, "app"), "build.gradle.kts", 2) || findFile(rootDir, "build.gradle", 1) || findFile(rootDir, "build.gradle.kts", 1);
566
- if (buildGradlePath) {
567
- const gradleConfig = parseGradle(buildGradlePath);
568
- config.bundle_id = gradleConfig.applicationId || null;
569
- config.version = gradleConfig.versionName || null;
570
- config.min_android_sdk = gradleConfig.minSdk || null;
571
- }
572
- const manifestPath = findFile(rootDir, "AndroidManifest.xml", 6);
573
- if (manifestPath) {
574
- const content = readFileSafe(manifestPath);
575
- if (content) {
576
- const labelMatch = content.match(/android:label="([^"]+)"/);
577
- if (labelMatch && !labelMatch[1].startsWith("@")) {
578
- config.app_name = labelMatch[1];
579
- }
580
- }
704
+ const candidates = [];
705
+ const addCandidate = (p) => {
706
+ if (p) candidates.push(p);
707
+ };
708
+ addCandidate(findFile(join3(rootDir, "app"), "build.gradle", 2));
709
+ addCandidate(findFile(join3(rootDir, "app"), "build.gradle.kts", 2));
710
+ addCandidate(findFile(rootDir, "build.gradle", 1));
711
+ addCandidate(findFile(rootDir, "build.gradle.kts", 1));
712
+ for (const f of findFiles(rootDir, ["build.gradle", "build.gradle.kts"], 4)) {
713
+ if (!candidates.includes(f)) candidates.push(f);
714
+ }
715
+ for (const path2 of candidates) {
716
+ const g = parseGradle(path2);
717
+ if (!config.bundle_id && g.applicationId) config.bundle_id = g.applicationId;
718
+ if (!config.version && g.versionName) config.version = g.versionName;
719
+ if (!config.min_android_sdk && g.minSdk) config.min_android_sdk = g.minSdk;
720
+ if (config.bundle_id && config.version && config.min_android_sdk) break;
581
721
  }
582
722
  if (!config.app_name) {
583
- const stringsPath = findFile(rootDir, "strings.xml", 8);
584
- if (stringsPath) {
585
- const content = readFileSafe(stringsPath);
586
- if (content) {
587
- const nameMatch = content.match(
588
- /<string name="app_name">(.*?)<\/string>/
589
- );
590
- if (nameMatch) config.app_name = nameMatch[1];
591
- }
592
- }
723
+ config.app_name = readAndroidAppName(rootDir);
593
724
  }
594
725
  return config;
595
726
  }
@@ -600,7 +731,7 @@ function parseCapacitorConfig(rootDir) {
600
731
  "capacitor.config.ts",
601
732
  "capacitor.config.js"
602
733
  ]) {
603
- const content = readFileSafe(join3(rootDir, name));
734
+ const content = readFileSafe2(join3(rootDir, name));
604
735
  if (content) {
605
736
  if (name.endsWith(".json")) {
606
737
  try {
@@ -618,7 +749,7 @@ function parseCapacitorConfig(rootDir) {
618
749
  break;
619
750
  }
620
751
  }
621
- const pkgContent = readFileSafe(join3(rootDir, "package.json"));
752
+ const pkgContent = readFileSafe2(join3(rootDir, "package.json"));
622
753
  if (pkgContent) {
623
754
  try {
624
755
  const pkg = JSON.parse(pkgContent);
@@ -635,7 +766,7 @@ function parseMauiConfig(rootDir) {
635
766
  const entries = readdirSync3(rootDir);
636
767
  for (const entry of entries) {
637
768
  if (entry.endsWith(".csproj")) {
638
- const content = readFileSafe(join3(rootDir, entry));
769
+ const content = readFileSafe2(join3(rootDir, entry));
639
770
  if (content) {
640
771
  const appIdMatch = content.match(
641
772
  /<ApplicationId>(.*?)<\/ApplicationId>/
@@ -658,7 +789,7 @@ function parseMauiConfig(rootDir) {
658
789
  return config;
659
790
  }
660
791
  function parsePlist(filePath) {
661
- const content = readFileSafe(filePath);
792
+ const content = readFileSafe2(filePath);
662
793
  if (!content) return null;
663
794
  try {
664
795
  return plist.parse(content);
@@ -666,8 +797,40 @@ function parsePlist(filePath) {
666
797
  return null;
667
798
  }
668
799
  }
800
+ function lookupAndroidString(searchDir, stringName) {
801
+ const stringsPath = findFile(searchDir, "strings.xml", 8);
802
+ if (!stringsPath) return null;
803
+ const content = readFileSafe2(stringsPath);
804
+ if (!content) return null;
805
+ const escaped = stringName.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
806
+ const pattern = new RegExp(
807
+ `<string\\s+name="${escaped}"[^>]*>([\\s\\S]*?)<\\/string>`
808
+ );
809
+ const m = content.match(pattern);
810
+ return m ? m[1].trim() : null;
811
+ }
812
+ function readAndroidAppName(searchDir) {
813
+ const manifestPath = findFile(searchDir, "AndroidManifest.xml", 6);
814
+ if (manifestPath) {
815
+ const content = readFileSafe2(manifestPath);
816
+ if (content) {
817
+ const labelMatch = content.match(/android:label\s*=\s*"([^"]+)"/);
818
+ if (labelMatch) {
819
+ const label = labelMatch[1];
820
+ const stringRef = label.match(/^@string\/(.+)$/);
821
+ if (stringRef) {
822
+ const resolved = lookupAndroidString(searchDir, stringRef[1]);
823
+ if (resolved) return resolved;
824
+ } else if (!label.startsWith("@")) {
825
+ return label;
826
+ }
827
+ }
828
+ }
829
+ }
830
+ return lookupAndroidString(searchDir, "app_name");
831
+ }
669
832
  function parseGradle(filePath) {
670
- const content = readFileSafe(filePath);
833
+ const content = readFileSafe2(filePath);
671
834
  if (!content) return { applicationId: null, versionName: null, minSdk: null };
672
835
  const appIdMatch = content.match(
673
836
  /applicationId\s*[=:]\s*['"]([^'"]+)['"]/
@@ -687,6 +850,7 @@ function parseGradle(filePath) {
687
850
 
688
851
  // src/analyzers/sdk-scanner.ts
689
852
  import { join as join4 } from "path";
853
+ import { readdirSync as readdirSync4 } from "fs";
690
854
  import YAML2 from "yaml";
691
855
  var SDK_MAP = {
692
856
  // Analytics
@@ -791,6 +955,45 @@ var SDK_MAP = {
791
955
  ARKit: { category: "other", collects: [] },
792
956
  RealityKit: { category: "other", collects: [] },
793
957
  EventKit: { category: "other", collects: [] },
958
+ // --- Expo SDK packages ---
959
+ "expo-notifications": { category: "other", collects: ["device_info"] },
960
+ "expo-tracking-transparency": { category: "ads", collects: ["device_info"], advertising: "personalized" },
961
+ "expo-location": { category: "other", collects: ["location"] },
962
+ "expo-contacts": { category: "other", collects: ["contacts"] },
963
+ "expo-camera": { category: "other", collects: ["photos_media"] },
964
+ "expo-image-picker": { category: "other", collects: ["photos_media"] },
965
+ "expo-media-library": { category: "other", collects: ["photos_media"] },
966
+ "expo-auth-session": { category: "auth", collects: ["name_email"] },
967
+ "expo-apple-authentication": { category: "auth", collects: ["name_email"] },
968
+ "expo-google-sign-in": { category: "auth", collects: ["name_email", "profile_photo"] },
969
+ "expo-in-app-purchases": { category: "payment", collects: ["financial_info"] },
970
+ "expo-ads-admob": { category: "ads", collects: ["device_info"], advertising: "personalized" },
971
+ "expo-firebase-analytics": { category: "analytics", collects: ["usage_analytics", "device_info"] },
972
+ "expo-local-authentication": { category: "auth", collects: ["biometric_data"] },
973
+ "expo-health-connect": { category: "other", collects: ["health_data"] },
974
+ "expo-sensors": { category: "other", collects: ["device_info"] },
975
+ "expo-device": { category: "other", collects: ["device_info"] },
976
+ // --- .NET MAUI packages ---
977
+ "Microsoft.AppCenter.Analytics": { category: "analytics", collects: ["usage_analytics", "device_info"] },
978
+ "Microsoft.AppCenter.Crashes": { category: "analytics", collects: ["device_info"] },
979
+ "Sentry.Maui": { category: "analytics", collects: ["device_info"] },
980
+ "Plugin.Firebase": { category: "analytics", collects: ["usage_analytics", "device_info"] },
981
+ "Plugin.Firebase.Analytics": { category: "analytics", collects: ["usage_analytics", "device_info"] },
982
+ "Plugin.Firebase.Auth": { category: "auth", collects: ["name_email"] },
983
+ "Plugin.Firebase.CloudMessaging": { category: "other", collects: ["device_info"] },
984
+ "Plugin.GoogleClient": { category: "auth", collects: ["name_email", "profile_photo"] },
985
+ "Plugin.SignInWithApple": { category: "auth", collects: ["name_email"] },
986
+ "Plugin.InAppBilling": { category: "payment", collects: ["financial_info"] },
987
+ "CommunityToolkit.Maui": { category: "other", collects: [] },
988
+ "CommunityToolkit.Maui.MediaElement": { category: "other", collects: ["photos_media"] },
989
+ "Plugin.Maui.Audio": { category: "other", collects: ["photos_media"] },
990
+ "Xamarin.Google.Android.Play.Review": { category: "other", collects: [] },
991
+ "Plugin.LocalNotification": { category: "other", collects: [] },
992
+ "Plugin.Maui.Biometric": { category: "auth", collects: ["biometric_data"] },
993
+ "Plugin.Permissions": { category: "other", collects: [] },
994
+ "Mapsui.Maui": { category: "other", collects: ["location"] },
995
+ "Plugin.Maui.AppRating": { category: "other", collects: [] },
996
+ "Plugin.Geolocator": { category: "other", collects: ["location"] },
794
997
  // --- Native Android frameworks (detected via `import` in Kotlin/Java source) ---
795
998
  "com.google.android.gms.ads": { category: "ads", collects: ["device_info"], advertising: "personalized" },
796
999
  "com.google.android.gms.maps": { category: "other", collects: ["location"] },
@@ -799,8 +1002,123 @@ var SDK_MAP = {
799
1002
  "com.android.billingclient": { category: "payment", collects: ["financial_info"] },
800
1003
  "androidx.health": { category: "other", collects: ["health_data"] },
801
1004
  "androidx.biometric": { category: "auth", collects: ["biometric_data"] },
802
- "androidx.camera": { category: "other", collects: ["photos_media"] }
1005
+ "androidx.camera": { category: "other", collects: ["photos_media"] },
1006
+ "androidx.compose": { category: "other", collects: [] },
1007
+ "androidx.navigation.compose": { category: "other", collects: [] },
1008
+ // --- Android artifact-name keys (unique enough that bare artifact name is safe) ---
1009
+ "play-services-ads": { category: "ads", collects: ["device_info"], advertising: "personalized" },
1010
+ "play-services-ads-lite": { category: "ads", collects: ["device_info"], advertising: "personalized" },
1011
+ "play-services-maps": { category: "other", collects: ["location"] },
1012
+ "play-services-location": { category: "other", collects: ["location"] },
1013
+ "play-services-auth": { category: "auth", collects: ["name_email", "profile_photo"] },
1014
+ "play-services-auth-api-phone": { category: "auth", collects: ["name_email"] },
1015
+ billing: { category: "payment", collects: ["financial_info"] },
1016
+ "billing-ktx": { category: "payment", collects: ["financial_info"] },
1017
+ biometric: { category: "auth", collects: ["biometric_data"] },
1018
+ "biometric-ktx": { category: "auth", collects: ["biometric_data"] },
1019
+ "health-connect-client": { category: "other", collects: ["health_data"] },
1020
+ "camera-core": { category: "other", collects: ["photos_media"] },
1021
+ "camera-camera2": { category: "other", collects: ["photos_media"] },
1022
+ "camera-lifecycle": { category: "other", collects: ["photos_media"] },
1023
+ "camera-view": { category: "other", collects: ["photos_media"] },
1024
+ "camera-extensions": { category: "other", collects: ["photos_media"] },
1025
+ // firebase-analytics / firebase-auth already declared above (Flutter dep names)
1026
+ "firebase-analytics-ktx": { category: "analytics", collects: ["usage_analytics", "device_info"] },
1027
+ "firebase-auth-ktx": { category: "auth", collects: ["name_email"] },
1028
+ "firebase-crashlytics": { category: "analytics", collects: ["device_info"] },
1029
+ "firebase-crashlytics-ktx": { category: "analytics", collects: ["device_info"] },
1030
+ "firebase-messaging": { category: "other", collects: ["device_info"] },
1031
+ "firebase-messaging-ktx": { category: "other", collects: ["device_info"] },
1032
+ "firebase-firestore": { category: "cloud", collects: [] },
1033
+ "firebase-firestore-ktx": { category: "cloud", collects: [] },
1034
+ "firebase-storage": { category: "cloud", collects: [] },
1035
+ "firebase-storage-ktx": { category: "cloud", collects: [] },
1036
+ "firebase-database": { category: "cloud", collects: [] },
1037
+ "firebase-database-ktx": { category: "cloud", collects: [] },
1038
+ "firebase-config": { category: "cloud", collects: [] },
1039
+ "firebase-config-ktx": { category: "cloud", collects: [] },
1040
+ "firebase-functions": { category: "cloud", collects: [] },
1041
+ "firebase-functions-ktx": { category: "cloud", collects: [] },
1042
+ "hilt-android": { category: "other", collects: [] },
1043
+ "hilt-navigation-compose": { category: "other", collects: [] },
1044
+ "koin-android": { category: "other", collects: [] },
1045
+ "koin-androidx-compose": { category: "other", collects: [] },
1046
+ "room-runtime": { category: "other", collects: [] },
1047
+ "room-ktx": { category: "other", collects: [] },
1048
+ "room-compiler": { category: "other", collects: [] },
1049
+ retrofit: { category: "other", collects: [] },
1050
+ "retrofit-converter-gson": { category: "other", collects: [] },
1051
+ "retrofit-converter-moshi": { category: "other", collects: [] },
1052
+ okhttp: { category: "other", collects: [] },
1053
+ "okhttp-logging-interceptor": { category: "other", collects: [] },
1054
+ "coil-compose": { category: "other", collects: [] },
1055
+ coil: { category: "other", collects: [] },
1056
+ glide: { category: "other", collects: [] },
1057
+ "navigation-compose": { category: "other", collects: [] },
1058
+ "activity-compose": { category: "other", collects: [] },
1059
+ "lifecycle-viewmodel-compose": { category: "other", collects: [] },
1060
+ "lifecycle-runtime-compose": { category: "other", collects: [] },
1061
+ "work-runtime": { category: "other", collects: [] },
1062
+ "work-runtime-ktx": { category: "other", collects: [] },
1063
+ "datastore-preferences": { category: "other", collects: [] },
1064
+ "kotlinx-coroutines-android": { category: "other", collects: [] },
1065
+ // --- group:artifact keys for Compose (artifact names alone are ambiguous: "ui", "material3") ---
1066
+ "androidx.compose.ui:ui": { category: "other", collects: [] },
1067
+ "androidx.compose.ui:ui-tooling": { category: "other", collects: [] },
1068
+ "androidx.compose.ui:ui-tooling-preview": { category: "other", collects: [] },
1069
+ "androidx.compose.material3:material3": { category: "other", collects: [] },
1070
+ "androidx.compose.material:material": { category: "other", collects: [] },
1071
+ "androidx.compose.material:material-icons-extended": { category: "other", collects: [] },
1072
+ "androidx.compose.foundation:foundation": { category: "other", collects: [] },
1073
+ "androidx.compose.runtime:runtime": { category: "other", collects: [] },
1074
+ "androidx.compose.animation:animation": { category: "other", collects: [] },
1075
+ // --- Popular third-party Android SDKs (group:artifact keys) ---
1076
+ "com.revenuecat.purchases:purchases": { category: "payment", collects: ["financial_info"] },
1077
+ "com.adjust.sdk:adjust-android": { category: "analytics", collects: ["usage_analytics", "device_info"] },
1078
+ "com.appsflyer:af-android-sdk": { category: "analytics", collects: ["usage_analytics", "device_info"] },
1079
+ "io.sentry:sentry-android": { category: "analytics", collects: ["device_info"] },
1080
+ "com.amplitude:android-sdk": { category: "analytics", collects: ["usage_analytics"] },
1081
+ "com.mixpanel.android:mixpanel-android": { category: "analytics", collects: ["usage_analytics"] },
1082
+ "com.posthog:posthog-android": { category: "analytics", collects: ["usage_analytics"] },
1083
+ "com.stripe:stripe-android": { category: "payment", collects: ["financial_info"] }
803
1084
  };
1085
+ function parseVersionCatalog(content, catalog) {
1086
+ const idx = content.indexOf("[libraries]");
1087
+ if (idx < 0) return;
1088
+ const after = content.slice(idx + "[libraries]".length);
1089
+ const nextSection = after.search(/\n\s*\[[^\]]+\]/);
1090
+ const block = nextSection >= 0 ? after.slice(0, nextSection) : after;
1091
+ for (const rawLine of block.split("\n")) {
1092
+ const line = rawLine.replace(/#.*$/, "").trim();
1093
+ if (!line || line.startsWith("[")) continue;
1094
+ const eq = line.indexOf("=");
1095
+ if (eq < 0) continue;
1096
+ const alias = line.slice(0, eq).trim();
1097
+ const value = line.slice(eq + 1).trim();
1098
+ if (!alias) continue;
1099
+ const shorthand = value.match(/^"([^"]+)"$/);
1100
+ if (shorthand) {
1101
+ const parts = shorthand[1].split(":");
1102
+ if (parts.length >= 2 && parts[0] && parts[1]) {
1103
+ catalog.set(alias, `${parts[0]}:${parts[1]}`);
1104
+ }
1105
+ continue;
1106
+ }
1107
+ const moduleMatch = value.match(/module\s*=\s*"([^"]+)"/);
1108
+ if (moduleMatch) {
1109
+ const parts = moduleMatch[1].split(":");
1110
+ if (parts.length >= 2 && parts[0] && parts[1]) {
1111
+ catalog.set(alias, `${parts[0]}:${parts[1]}`);
1112
+ }
1113
+ continue;
1114
+ }
1115
+ const groupMatch = value.match(/group\s*=\s*"([^"]+)"/);
1116
+ const nameMatch = value.match(/name\s*=\s*"([^"]+)"/);
1117
+ if (groupMatch && nameMatch) {
1118
+ catalog.set(alias, `${groupMatch[1]}:${nameMatch[1]}`);
1119
+ }
1120
+ }
1121
+ }
804
1122
  function scanSDKs(rootDir, techStack) {
805
1123
  const detectedSDKs = [];
806
1124
  const dataCollected = /* @__PURE__ */ new Set();
@@ -852,7 +1170,7 @@ function getDependencies(rootDir, techStack) {
852
1170
  const deps = [];
853
1171
  switch (techStack) {
854
1172
  case "flutter": {
855
- const pubspecContent = readFileSafe(join4(rootDir, "pubspec.yaml"));
1173
+ const pubspecContent = readFileSafe2(join4(rootDir, "pubspec.yaml"));
856
1174
  if (pubspecContent) {
857
1175
  try {
858
1176
  const pubspec = YAML2.parse(pubspecContent);
@@ -867,9 +1185,10 @@ function getDependencies(rootDir, techStack) {
867
1185
  }
868
1186
  break;
869
1187
  }
1188
+ case "expo":
870
1189
  case "react-native":
871
1190
  case "capacitor": {
872
- const pkgContent = readFileSafe(join4(rootDir, "package.json"));
1191
+ const pkgContent = readFileSafe2(join4(rootDir, "package.json"));
873
1192
  if (pkgContent) {
874
1193
  try {
875
1194
  const pkg = JSON.parse(pkgContent);
@@ -880,15 +1199,39 @@ function getDependencies(rootDir, techStack) {
880
1199
  }
881
1200
  break;
882
1201
  }
1202
+ case "dotnet-maui": {
1203
+ const csprojFiles = [];
1204
+ try {
1205
+ for (const entry of readdirSync4(rootDir)) {
1206
+ if (entry.endsWith(".csproj")) csprojFiles.push(join4(rootDir, entry));
1207
+ }
1208
+ } catch {
1209
+ }
1210
+ const nested = findFiles(rootDir, [".csproj"], 3);
1211
+ for (const f of nested) {
1212
+ if (!csprojFiles.includes(f)) csprojFiles.push(f);
1213
+ }
1214
+ for (const path2 of csprojFiles) {
1215
+ const content = readFileSafe2(path2);
1216
+ if (!content) continue;
1217
+ const pkgMatches = content.matchAll(
1218
+ /<PackageReference\s+[^>]*Include\s*=\s*"([^"]+)"/g
1219
+ );
1220
+ for (const match of pkgMatches) {
1221
+ if (!deps.includes(match[1])) deps.push(match[1]);
1222
+ }
1223
+ }
1224
+ break;
1225
+ }
883
1226
  case "swift": {
884
- const podfileContent = readFileSafe(join4(rootDir, "Podfile"));
1227
+ const podfileContent = readFileSafe2(join4(rootDir, "Podfile"));
885
1228
  if (podfileContent) {
886
1229
  const podMatches = podfileContent.matchAll(/pod\s+['"]([^'"]+)['"]/g);
887
1230
  for (const match of podMatches) {
888
1231
  deps.push(match[1]);
889
1232
  }
890
1233
  }
891
- const packageSwift = readFileSafe(join4(rootDir, "Package.swift"));
1234
+ const packageSwift = readFileSafe2(join4(rootDir, "Package.swift"));
892
1235
  if (packageSwift) {
893
1236
  const urlMatches = packageSwift.matchAll(
894
1237
  /\.package\([^)]*url:\s*"[^"]*\/([^/"]+?)(?:\.git)?"/g
@@ -899,7 +1242,7 @@ function getDependencies(rootDir, techStack) {
899
1242
  }
900
1243
  const pbxprojPath = findFile(rootDir, "project.pbxproj", 5);
901
1244
  if (pbxprojPath) {
902
- const pbxContent = readFileSafe(pbxprojPath);
1245
+ const pbxContent = readFileSafe2(pbxprojPath);
903
1246
  if (pbxContent) {
904
1247
  const spmMatches = pbxContent.matchAll(
905
1248
  /repositoryURL\s*=\s*"[^"]*\/([^/"]+?)(?:\.git)?"/g
@@ -914,7 +1257,7 @@ function getDependencies(rootDir, techStack) {
914
1257
  const swiftFiles = findFiles(rootDir, [".swift"], 6);
915
1258
  const nativeImports = /* @__PURE__ */ new Set();
916
1259
  for (const file of swiftFiles.slice(0, 100)) {
917
- const content = readFileSafe(file);
1260
+ const content = readFileSafe2(file);
918
1261
  if (!content) continue;
919
1262
  const importMatches = content.matchAll(/^import\s+(\w+)/gm);
920
1263
  for (const match of importMatches) {
@@ -927,29 +1270,51 @@ function getDependencies(rootDir, techStack) {
927
1270
  deps.push(...nativeImports);
928
1271
  break;
929
1272
  }
930
- case "kotlin":
931
- case "java": {
932
- const gradlePaths = [
1273
+ case "kotlin": {
1274
+ const catalog = /* @__PURE__ */ new Map();
1275
+ for (const p of [
1276
+ join4(rootDir, "gradle/libs.versions.toml"),
1277
+ join4(rootDir, "libs.versions.toml")
1278
+ ]) {
1279
+ const content = readFileSafe2(p);
1280
+ if (content) parseVersionCatalog(content, catalog);
1281
+ }
1282
+ const gradleFiles = /* @__PURE__ */ new Set([
933
1283
  join4(rootDir, "app/build.gradle"),
934
1284
  join4(rootDir, "app/build.gradle.kts"),
935
1285
  join4(rootDir, "build.gradle"),
936
1286
  join4(rootDir, "build.gradle.kts")
937
- ];
938
- for (const path2 of gradlePaths) {
939
- const content = readFileSafe(path2);
940
- if (content) {
941
- const implMatches = content.matchAll(
942
- /implementation\s*\(?['"]([^'"]+)['"]\)?/g
943
- );
944
- for (const match of implMatches) {
945
- const parts = match[1].split(":");
946
- if (parts.length >= 2) {
947
- deps.push(parts[1]);
948
- }
949
- }
1287
+ ]);
1288
+ for (const f of findFiles(rootDir, ["build.gradle", "build.gradle.kts"], 5)) {
1289
+ gradleFiles.add(f);
1290
+ }
1291
+ const addCoord = (coord) => {
1292
+ const parts = coord.split(":");
1293
+ if (parts.length < 2) return;
1294
+ const [group, artifact] = parts;
1295
+ if (!group || !artifact) return;
1296
+ if (!deps.includes(artifact)) deps.push(artifact);
1297
+ const gav = `${group}:${artifact}`;
1298
+ if (!deps.includes(gav)) deps.push(gav);
1299
+ };
1300
+ const coordRegex = /['"]([a-zA-Z][\w.-]*\.[a-zA-Z][\w.-]*:[a-zA-Z][\w.-]*(?::[^'"\s]+)?)['"]/g;
1301
+ const libsRefRegex = /\blibs\.([a-zA-Z][\w.]*)/g;
1302
+ for (const path2 of gradleFiles) {
1303
+ const content = readFileSafe2(path2);
1304
+ if (!content) continue;
1305
+ for (const m of content.matchAll(coordRegex)) {
1306
+ addCoord(m[1]);
1307
+ }
1308
+ for (const m of content.matchAll(libsRefRegex)) {
1309
+ const ref = m[1];
1310
+ const resolved = catalog.get(ref) || catalog.get(ref.replace(/\./g, "-"));
1311
+ if (resolved) addCoord(resolved);
950
1312
  }
951
1313
  }
952
- const androidFiles = findFiles(join4(rootDir, "app/src"), [".kt", ".java"], 8);
1314
+ const androidFiles = [
1315
+ ...findFiles(join4(rootDir, "app/src"), [".kt", ".java"], 8),
1316
+ ...findFiles(rootDir, [".kt", ".java"], 6)
1317
+ ];
953
1318
  const androidNativeImports = /* @__PURE__ */ new Set();
954
1319
  const androidFrameworkPrefixes = [
955
1320
  "com.google.android.gms.ads",
@@ -959,16 +1324,20 @@ function getDependencies(rootDir, techStack) {
959
1324
  "com.android.billingclient",
960
1325
  "androidx.health",
961
1326
  "androidx.biometric",
962
- "androidx.camera"
1327
+ "androidx.camera",
1328
+ "androidx.compose",
1329
+ "androidx.navigation.compose"
963
1330
  ];
964
- for (const file of androidFiles.slice(0, 100)) {
965
- const content = readFileSafe(file);
1331
+ const seen = /* @__PURE__ */ new Set();
1332
+ for (const file of androidFiles) {
1333
+ if (seen.has(file)) continue;
1334
+ seen.add(file);
1335
+ if (seen.size > 200) break;
1336
+ const content = readFileSafe2(file);
966
1337
  if (!content) continue;
967
1338
  for (const prefix of androidFrameworkPrefixes) {
968
- if (content.includes(`import ${prefix}`)) {
969
- if (!androidNativeImports.has(prefix)) {
970
- androidNativeImports.add(prefix);
971
- }
1339
+ if (content.includes(`import ${prefix}`) && !androidNativeImports.has(prefix)) {
1340
+ androidNativeImports.add(prefix);
972
1341
  }
973
1342
  }
974
1343
  }
@@ -980,7 +1349,7 @@ function getDependencies(rootDir, techStack) {
980
1349
  }
981
1350
 
982
1351
  // src/analyzers/branding.ts
983
- import { readFileSync as readFileSync2, existsSync as existsSync3, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
1352
+ import { readFileSync as readFileSync2, existsSync as existsSync3, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
984
1353
  import { join as join5, extname } from "path";
985
1354
  function extractBranding(rootDir, techStack) {
986
1355
  const result = {
@@ -1003,18 +1372,127 @@ function extractColors(rootDir, techStack) {
1003
1372
  switch (techStack) {
1004
1373
  case "flutter":
1005
1374
  return extractFlutterColors(rootDir);
1375
+ case "expo":
1376
+ return extractExpoColors(rootDir);
1006
1377
  case "react-native":
1007
1378
  case "capacitor":
1008
1379
  return extractJSColors(rootDir);
1009
1380
  case "swift":
1010
1381
  return extractSwiftColors(rootDir);
1011
1382
  case "kotlin":
1012
- case "java":
1013
1383
  return extractAndroidColors(rootDir);
1384
+ case "dotnet-maui":
1385
+ return extractMauiColors(rootDir);
1014
1386
  default:
1015
1387
  return { primary: null, secondary: null, allColors: [] };
1016
1388
  }
1017
1389
  }
1390
+ function extractExpoColors(rootDir) {
1391
+ const hexColors = [];
1392
+ let primary = null;
1393
+ let secondary = null;
1394
+ const HEX = /^#[0-9a-fA-F]{6}$/;
1395
+ const normalize = (raw) => {
1396
+ if (!raw) return null;
1397
+ const v = raw.trim();
1398
+ if (HEX.test(v)) return v.toLowerCase();
1399
+ if (/^#[0-9a-fA-F]{8}$/.test(v)) return `#${v.slice(3).toLowerCase()}`;
1400
+ return null;
1401
+ };
1402
+ const appJsonContent = readFileSafe2(join5(rootDir, "app.json"));
1403
+ if (appJsonContent) {
1404
+ try {
1405
+ const data = JSON.parse(appJsonContent);
1406
+ const expo = data.expo || data;
1407
+ const candidates = [
1408
+ expo.primaryColor,
1409
+ expo.splash?.backgroundColor,
1410
+ expo.android?.adaptiveIcon?.backgroundColor,
1411
+ expo.android?.splash?.backgroundColor,
1412
+ expo.ios?.splash?.backgroundColor
1413
+ ];
1414
+ for (const c of candidates) {
1415
+ const n = normalize(c || "");
1416
+ if (n) hexColors.push(n);
1417
+ }
1418
+ primary = normalize(expo.primaryColor || "") || null;
1419
+ } catch {
1420
+ }
1421
+ }
1422
+ const jsResult = extractJSColors(rootDir);
1423
+ if (!primary) primary = jsResult.primary;
1424
+ if (!secondary) secondary = jsResult.secondary;
1425
+ hexColors.push(...jsResult.allColors);
1426
+ return {
1427
+ primary: primary || (hexColors[0] ?? null),
1428
+ secondary: secondary || (hexColors[1] ?? null),
1429
+ allColors: hexColors
1430
+ };
1431
+ }
1432
+ function extractMauiColors(rootDir) {
1433
+ const hexColors = [];
1434
+ const xamlPaths = [
1435
+ join5(rootDir, "Resources/Styles/Colors.xaml"),
1436
+ join5(rootDir, "Resources/Styles/Styles.xaml")
1437
+ ];
1438
+ const allXaml = findFiles(rootDir, [".xaml"], 5);
1439
+ for (const p of allXaml) {
1440
+ if (/Colors\.xaml$/i.test(p) && !xamlPaths.includes(p)) xamlPaths.push(p);
1441
+ }
1442
+ for (const path2 of xamlPaths) {
1443
+ const content = readFileSafe2(path2);
1444
+ if (!content) continue;
1445
+ const colorMatches = content.matchAll(
1446
+ /<Color\s+x:Key="([^"]+)"\s*>\s*#([0-9a-fA-F]{6,8})\s*<\/Color>/g
1447
+ );
1448
+ for (const match of colorMatches) {
1449
+ const name = match[1].toLowerCase();
1450
+ let hex = match[2];
1451
+ if (hex.length === 8) hex = hex.substring(2);
1452
+ const color = `#${hex.toLowerCase()}`;
1453
+ if (name.includes("primary") && !name.includes("dark") && !name.includes("light")) {
1454
+ hexColors.unshift(color);
1455
+ } else {
1456
+ hexColors.push(color);
1457
+ }
1458
+ }
1459
+ const brushMatches = content.matchAll(
1460
+ /<SolidColorBrush\s+[^>]*Color\s*=\s*"#([0-9a-fA-F]{6,8})"/g
1461
+ );
1462
+ for (const match of brushMatches) {
1463
+ let hex = match[1];
1464
+ if (hex.length === 8) hex = hex.substring(2);
1465
+ hexColors.push(`#${hex.toLowerCase()}`);
1466
+ }
1467
+ }
1468
+ const csFiles = findFiles(rootDir, [".cs"], 5);
1469
+ for (const file of csFiles.slice(0, 50)) {
1470
+ const content = readFileSafe2(file);
1471
+ if (!content) continue;
1472
+ const argbMatches = content.matchAll(
1473
+ /Color\.FromArgb\s*\(\s*"#?([0-9a-fA-F]{6,8})"/g
1474
+ );
1475
+ for (const match of argbMatches) {
1476
+ let hex = match[1];
1477
+ if (hex.length === 8) hex = hex.substring(2);
1478
+ hexColors.push(`#${hex.toLowerCase()}`);
1479
+ }
1480
+ const rgbMatches = content.matchAll(
1481
+ /Color\.FromRgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/g
1482
+ );
1483
+ for (const match of rgbMatches) {
1484
+ const r = parseInt(match[1]).toString(16).padStart(2, "0");
1485
+ const g = parseInt(match[2]).toString(16).padStart(2, "0");
1486
+ const b = parseInt(match[3]).toString(16).padStart(2, "0");
1487
+ hexColors.push(`#${r}${g}${b}`);
1488
+ }
1489
+ }
1490
+ return {
1491
+ primary: hexColors[0] ?? null,
1492
+ secondary: hexColors[1] ?? null,
1493
+ allColors: hexColors
1494
+ };
1495
+ }
1018
1496
  function isIrrelevantColor(hex) {
1019
1497
  const r = parseInt(hex.slice(1, 3), 16);
1020
1498
  const g = parseInt(hex.slice(3, 5), 16);
@@ -1039,7 +1517,7 @@ function extractFlutterColors(rootDir) {
1039
1517
  const dartFiles = findFiles(rootDir, [".dart"], 6);
1040
1518
  const hexColors = [];
1041
1519
  for (const file of dartFiles.slice(0, 50)) {
1042
- const content = readFileSafe(file);
1520
+ const content = readFileSafe2(file);
1043
1521
  if (!content) continue;
1044
1522
  const colorMatches = content.matchAll(/Color\(0[xX]([0-9a-fA-F]{8})\)/g);
1045
1523
  for (const match of colorMatches) {
@@ -1091,7 +1569,7 @@ function extractJSColors(rootDir) {
1091
1569
  (f) => f.endsWith(fileName)
1092
1570
  );
1093
1571
  for (const file of files) {
1094
- const content = readFileSafe(file);
1572
+ const content = readFileSafe2(file);
1095
1573
  if (!content) continue;
1096
1574
  const matches = content.matchAll(
1097
1575
  /['"]#([0-9a-fA-F]{6})['"]/g
@@ -1132,7 +1610,7 @@ function extractSwiftColors(rootDir) {
1132
1610
  }
1133
1611
  for (const catalog of assetsCatalogs) {
1134
1612
  try {
1135
- const entries = readdirSync4(catalog);
1613
+ const entries = readdirSync5(catalog);
1136
1614
  for (const entry of entries) {
1137
1615
  if (entry.endsWith(".colorset") && entry !== "AccentColor.colorset") {
1138
1616
  const color = parseColorSet(join5(catalog, entry));
@@ -1145,7 +1623,7 @@ function extractSwiftColors(rootDir) {
1145
1623
  }
1146
1624
  const swiftFiles = findFiles(rootDir, [".swift"], 6);
1147
1625
  for (const file of swiftFiles.slice(0, 50)) {
1148
- const content = readFileSafe(file);
1626
+ const content = readFileSafe2(file);
1149
1627
  if (!content) continue;
1150
1628
  const hexMatches = content.matchAll(
1151
1629
  /(?:UIColor|Color)\s*\(\s*(?:hex|hexString)\s*:\s*(?:0[xX]|"#?)([0-9a-fA-F]{6})/g
@@ -1181,51 +1659,103 @@ function extractSwiftColors(rootDir) {
1181
1659
  }
1182
1660
  function extractAndroidColors(rootDir) {
1183
1661
  const hexColors = [];
1184
- const colorFiles = ["colors.xml", "themes.xml", "styles.xml"];
1185
- for (const fileName of colorFiles) {
1186
- for (const resDir of ["app/src/main/res/values", "src/main/res/values"]) {
1187
- const filePath = join5(rootDir, resDir, fileName);
1188
- const content = readFileSafe(filePath);
1189
- if (!content) continue;
1190
- const colorMatches = content.matchAll(
1191
- /<color\s+name="([^"]*)">\s*#([0-9a-fA-F]{6,8})\s*<\/color>/g
1192
- );
1193
- for (const match of colorMatches) {
1194
- const name = match[1].toLowerCase();
1195
- let hex = match[2];
1196
- if (hex.length === 8) hex = hex.substring(2);
1197
- const color = `#${hex}`;
1198
- if (name.includes("primary") && !name.includes("variant") && !name.includes("dark")) {
1199
- hexColors.unshift(color);
1200
- } else if (name.includes("secondary") || name.includes("accent")) {
1201
- hexColors.push(color);
1202
- } else {
1203
- hexColors.push(color);
1204
- }
1662
+ let primary = null;
1663
+ let secondary = null;
1664
+ const normalize = (hexDigits) => {
1665
+ const h = hexDigits.length === 8 ? hexDigits.slice(2) : hexDigits;
1666
+ return `#${h.toLowerCase()}`;
1667
+ };
1668
+ const colorMap = /* @__PURE__ */ new Map();
1669
+ const colorsFiles = findAllFiles(rootDir, "colors.xml", 8);
1670
+ for (const path2 of colorsFiles) {
1671
+ const content = readFileSafe2(path2);
1672
+ if (!content) continue;
1673
+ const matches = content.matchAll(
1674
+ /<color\s+name="([^"]+)"[^>]*>\s*#([0-9a-fA-F]{6,8})\s*<\/color>/g
1675
+ );
1676
+ for (const m of matches) {
1677
+ const hex = normalize(m[2]);
1678
+ colorMap.set(m[1], hex);
1679
+ hexColors.push(hex);
1680
+ const name = m[1].toLowerCase();
1681
+ if (name.includes("primary") && !name.includes("variant") && !name.includes("dark") && !name.includes("container") && !primary) {
1682
+ primary = hex;
1683
+ } else if ((name.includes("secondary") || name.includes("accent")) && !secondary) {
1684
+ secondary = hex;
1205
1685
  }
1206
- const themeColorMatches = content.matchAll(
1207
- /<item\s+name="(?:color|android:color)([^"]*)">\s*@color\/([^<]+)\s*<\/item>/g
1208
- );
1209
- for (const match of themeColorMatches) {
1210
- if (match[1].toLowerCase().includes("primary")) {
1211
- }
1686
+ }
1687
+ }
1688
+ const themeFiles = [
1689
+ ...findAllFiles(rootDir, "themes.xml", 8),
1690
+ ...findAllFiles(rootDir, "styles.xml", 8)
1691
+ ];
1692
+ for (const path2 of themeFiles) {
1693
+ const content = readFileSafe2(path2);
1694
+ if (!content) continue;
1695
+ const itemMatches = content.matchAll(
1696
+ /<item\s+name="(?:android:)?([A-Za-z]+)"[^>]*>\s*(?:@color\/([A-Za-z0-9_]+)|#([0-9a-fA-F]{6,8}))\s*<\/item>/g
1697
+ );
1698
+ for (const m of itemMatches) {
1699
+ const itemName = m[1].toLowerCase();
1700
+ if (!itemName.startsWith("color")) continue;
1701
+ let hex = null;
1702
+ if (m[2]) hex = colorMap.get(m[2]) || null;
1703
+ else if (m[3]) hex = normalize(m[3]);
1704
+ if (!hex) continue;
1705
+ if (itemName === "colorprimary" || itemName === "colorprimarydark") {
1706
+ if (!primary) primary = hex;
1707
+ } else if (itemName === "colorsecondary" || itemName === "colortertiary" || itemName === "coloraccent") {
1708
+ if (!secondary) secondary = hex;
1212
1709
  }
1710
+ hexColors.push(hex);
1213
1711
  }
1214
1712
  }
1215
1713
  const ktFiles = findFiles(rootDir, [".kt"], 6);
1216
- for (const file of ktFiles.slice(0, 50)) {
1714
+ for (const file of ktFiles.slice(0, 80)) {
1217
1715
  const fileName = file.toLowerCase();
1218
1716
  if (!fileName.includes("color") && !fileName.includes("theme")) continue;
1219
- const content = readFileSafe(file);
1717
+ const content = readFileSafe2(file);
1220
1718
  if (!content) continue;
1221
- const colorMatches = content.matchAll(/Color\s*\(\s*0[xX]([0-9a-fA-F]{8})\s*\)/g);
1222
- for (const match of colorMatches) {
1223
- hexColors.push(`#${match[1].substring(2)}`);
1719
+ const valMap = /* @__PURE__ */ new Map();
1720
+ const valMatches = content.matchAll(
1721
+ /val\s+(\w+)\s*(?::\s*Color\s*)?=\s*Color\s*\(\s*0[xX]([0-9a-fA-F]{8})\s*\)/g
1722
+ );
1723
+ for (const m of valMatches) {
1724
+ const hex = normalize(m[2]);
1725
+ valMap.set(m[1], hex);
1726
+ hexColors.push(hex);
1727
+ }
1728
+ const literalMatches = content.matchAll(/Color\s*\(\s*0[xX]([0-9a-fA-F]{8})\s*\)/g);
1729
+ for (const m of literalMatches) {
1730
+ hexColors.push(normalize(m[1]));
1731
+ }
1732
+ const schemeMatch = content.match(
1733
+ /(?:light|dark)ColorScheme\s*\(\s*([\s\S]*?)\n\s*\)/
1734
+ );
1735
+ if (schemeMatch) {
1736
+ const body = schemeMatch[1];
1737
+ const resolve = (key) => {
1738
+ const re = new RegExp(
1739
+ `\\b${key}\\s*=\\s*(?:Color\\s*\\(\\s*0[xX]([0-9a-fA-F]{8})\\s*\\)|(\\w+))`
1740
+ );
1741
+ const m = body.match(re);
1742
+ if (!m) return null;
1743
+ if (m[1]) return normalize(m[1]);
1744
+ return valMap.get(m[2]) || null;
1745
+ };
1746
+ if (!primary) {
1747
+ const p = resolve("primary");
1748
+ if (p) primary = p;
1749
+ }
1750
+ if (!secondary) {
1751
+ const s = resolve("secondary") || resolve("tertiary");
1752
+ if (s) secondary = s;
1753
+ }
1224
1754
  }
1225
1755
  }
1226
1756
  return {
1227
- primary: hexColors.length > 0 ? hexColors[0] : null,
1228
- secondary: hexColors.length > 1 ? hexColors[1] : null,
1757
+ primary: primary || hexColors[0] || null,
1758
+ secondary: secondary || hexColors[1] || null,
1229
1759
  allColors: hexColors
1230
1760
  };
1231
1761
  }
@@ -1234,7 +1764,7 @@ function findAssetsCatalogs(rootDir) {
1234
1764
  function search(dir, depth) {
1235
1765
  if (depth > 5) return;
1236
1766
  try {
1237
- const entries = readdirSync4(dir);
1767
+ const entries = readdirSync5(dir);
1238
1768
  for (const entry of entries) {
1239
1769
  if (entry.startsWith(".") || entry === "Pods" || entry === "DerivedData") continue;
1240
1770
  const fullPath = join5(dir, entry);
@@ -1260,7 +1790,7 @@ function parseColorSetInCatalog(catalogPath, colorName) {
1260
1790
  }
1261
1791
  function parseColorSet(colorSetPath) {
1262
1792
  const contentsPath = join5(colorSetPath, "Contents.json");
1263
- const content = readFileSafe(contentsPath);
1793
+ const content = readFileSafe2(contentsPath);
1264
1794
  if (!content) return null;
1265
1795
  try {
1266
1796
  const data = JSON.parse(content);
@@ -1300,6 +1830,18 @@ function findAppIcon(rootDir, techStack) {
1300
1830
  join5(rootDir, "android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png")
1301
1831
  );
1302
1832
  break;
1833
+ case "expo":
1834
+ iconPaths.push(
1835
+ ...resolveExpoIconPaths(rootDir),
1836
+ // Common Expo defaults
1837
+ join5(rootDir, "assets/icon.png"),
1838
+ join5(rootDir, "assets/adaptive-icon.png"),
1839
+ join5(rootDir, "assets/images/icon.png"),
1840
+ join5(rootDir, "assets/images/adaptive-icon.png"),
1841
+ ...findIconsInAppIconSet(join5(rootDir, "ios")),
1842
+ join5(rootDir, "android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png")
1843
+ );
1844
+ break;
1303
1845
  case "react-native":
1304
1846
  case "capacitor":
1305
1847
  iconPaths.push(
@@ -1315,13 +1857,47 @@ function findAppIcon(rootDir, techStack) {
1315
1857
  ...findIconsInAppIconSet(rootDir)
1316
1858
  );
1317
1859
  break;
1318
- case "kotlin":
1319
- case "java":
1860
+ case "kotlin": {
1861
+ const modules = ["app", "mobile", "androidApp", "presentation", ""];
1862
+ const densities = ["xxxhdpi", "xxhdpi", "xhdpi"];
1863
+ for (const mod of modules) {
1864
+ const prefix = mod ? `${mod}/` : "";
1865
+ iconPaths.push(
1866
+ join5(rootDir, `${prefix}src/main/ic_launcher-playstore.png`),
1867
+ join5(rootDir, `${prefix}ic_launcher-playstore.png`)
1868
+ );
1869
+ }
1870
+ for (const mod of modules) {
1871
+ const prefix = mod ? `${mod}/` : "";
1872
+ for (const d of densities) {
1873
+ iconPaths.push(
1874
+ join5(rootDir, `${prefix}src/main/res/mipmap-${d}/ic_launcher_foreground.png`),
1875
+ join5(rootDir, `${prefix}src/main/res/mipmap-${d}/ic_launcher_foreground.webp`)
1876
+ );
1877
+ }
1878
+ }
1879
+ for (const mod of modules) {
1880
+ const prefix = mod ? `${mod}/` : "";
1881
+ for (const d of densities) {
1882
+ iconPaths.push(
1883
+ join5(rootDir, `${prefix}src/main/res/mipmap-${d}/ic_launcher.png`),
1884
+ join5(rootDir, `${prefix}src/main/res/mipmap-${d}/ic_launcher.webp`),
1885
+ join5(rootDir, `${prefix}src/main/res/mipmap-${d}/ic_launcher_round.png`),
1886
+ join5(rootDir, `${prefix}src/main/res/mipmap-${d}/ic_launcher_round.webp`)
1887
+ );
1888
+ }
1889
+ }
1890
+ break;
1891
+ }
1892
+ case "dotnet-maui":
1320
1893
  iconPaths.push(
1321
- join5(rootDir, "app/src/main/res/mipmap-xxxhdpi/ic_launcher.png"),
1322
- join5(rootDir, "app/src/main/res/mipmap-xxhdpi/ic_launcher.png"),
1323
- join5(rootDir, "app/src/main/res/mipmap-xhdpi/ic_launcher.png"),
1324
- join5(rootDir, "app/src/main/ic_launcher-playstore.png")
1894
+ join5(rootDir, "Resources/AppIcon/appicon.png"),
1895
+ join5(rootDir, "Resources/AppIcon/appiconfg.png"),
1896
+ join5(rootDir, "Resources/Images/icon.png"),
1897
+ join5(rootDir, "Resources/Images/appicon.png"),
1898
+ // SVG fallback (MAUI default is SVG appicon.svg)
1899
+ join5(rootDir, "Resources/AppIcon/appicon.svg"),
1900
+ join5(rootDir, "Resources/AppIcon/appiconfg.svg")
1325
1901
  );
1326
1902
  break;
1327
1903
  }
@@ -1333,7 +1909,7 @@ function findAppIcon(rootDir, techStack) {
1333
1909
  if (stat.size < 100) continue;
1334
1910
  const buffer = readFileSync2(iconPath);
1335
1911
  const ext = extname(iconPath).toLowerCase();
1336
- const mime = ext === ".png" ? "image/png" : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
1912
+ const mime = ext === ".png" ? "image/png" : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : ext === ".svg" ? "image/svg+xml" : ext === ".webp" ? "image/webp" : "image/png";
1337
1913
  const base64 = `data:${mime};base64,${buffer.toString("base64")}`;
1338
1914
  return { base64, path: iconPath };
1339
1915
  } catch {
@@ -1343,17 +1919,38 @@ function findAppIcon(rootDir, techStack) {
1343
1919
  }
1344
1920
  return { base64: null, path: null };
1345
1921
  }
1922
+ function resolveExpoIconPaths(rootDir) {
1923
+ const paths = [];
1924
+ const appJsonContent = readFileSafe2(join5(rootDir, "app.json"));
1925
+ if (!appJsonContent) return paths;
1926
+ try {
1927
+ const data = JSON.parse(appJsonContent);
1928
+ const expo = data.expo || data;
1929
+ const candidates = [
1930
+ expo.icon,
1931
+ expo.android?.icon,
1932
+ expo.android?.adaptiveIcon?.foregroundImage,
1933
+ expo.ios?.icon
1934
+ ].filter((v) => typeof v === "string");
1935
+ for (const rel of candidates) {
1936
+ const cleaned = rel.replace(/^\.?\//, "");
1937
+ paths.push(join5(rootDir, cleaned));
1938
+ }
1939
+ } catch {
1940
+ }
1941
+ return paths;
1942
+ }
1346
1943
  function findIconsInAppIconSet(searchDir) {
1347
1944
  const icons = [];
1348
1945
  function searchRecursive(dir, depth) {
1349
1946
  if (depth > 6) return;
1350
1947
  try {
1351
- const entries = readdirSync4(dir);
1948
+ const entries = readdirSync5(dir);
1352
1949
  for (const entry of entries) {
1353
1950
  const fullPath = join5(dir, entry);
1354
1951
  if (entry === "AppIcon.appiconset") {
1355
1952
  const contentsPath = join5(fullPath, "Contents.json");
1356
- const content = readFileSafe(contentsPath);
1953
+ const content = readFileSafe2(contentsPath);
1357
1954
  if (content) {
1358
1955
  try {
1359
1956
  const data = JSON.parse(content);
@@ -1372,7 +1969,7 @@ function findIconsInAppIconSet(searchDir) {
1372
1969
  }
1373
1970
  }
1374
1971
  try {
1375
- const iconEntries = readdirSync4(fullPath);
1972
+ const iconEntries = readdirSync5(fullPath);
1376
1973
  for (const ie of iconEntries) {
1377
1974
  if (ie.endsWith(".png")) {
1378
1975
  icons.push(join5(fullPath, ie));
@@ -1400,7 +1997,7 @@ function findIconsInAppIconSet(searchDir) {
1400
1997
  }
1401
1998
 
1402
1999
  // src/analyzers/font-detector.ts
1403
- import { existsSync as existsSync4, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
2000
+ import { existsSync as existsSync4, readdirSync as readdirSync6, statSync as statSync3 } from "fs";
1404
2001
  import { join as join6, extname as extname2, basename } from "path";
1405
2002
  import yaml from "yaml";
1406
2003
  import plist2 from "plist";
@@ -1452,7 +2049,7 @@ function scanForFontFiles(dir, maxDepth = 3) {
1452
2049
  function walk(d, depth) {
1453
2050
  if (depth > maxDepth) return;
1454
2051
  try {
1455
- const entries = readdirSync5(d);
2052
+ const entries = readdirSync6(d);
1456
2053
  for (const entry of entries) {
1457
2054
  if (entry.startsWith(".")) continue;
1458
2055
  const fullPath = join6(d, entry);
@@ -1478,7 +2075,7 @@ function scanForFontFiles(dir, maxDepth = 3) {
1478
2075
  function detectFlutterFonts(rootDir) {
1479
2076
  const fonts = [];
1480
2077
  const pubspecPath = join6(rootDir, "pubspec.yaml");
1481
- const pubspecContent = readFileSafe(pubspecPath);
2078
+ const pubspecContent = readFileSafe2(pubspecPath);
1482
2079
  if (pubspecContent) {
1483
2080
  try {
1484
2081
  const doc = yaml.parse(pubspecContent);
@@ -1495,7 +2092,7 @@ function detectFlutterFonts(rootDir) {
1495
2092
  }
1496
2093
  const dartFiles = findFiles(rootDir, [".dart"], 6);
1497
2094
  for (const file of dartFiles.slice(0, 80)) {
1498
- const content = readFileSafe(file);
2095
+ const content = readFileSafe2(file);
1499
2096
  if (!content) continue;
1500
2097
  const fontFamilyMatches = content.matchAll(/fontFamily\s*:\s*['"]([^'"]+)['"]/g);
1501
2098
  for (const match of fontFamilyMatches) {
@@ -1518,7 +2115,7 @@ function detectJSFonts(rootDir) {
1518
2115
  const fonts = [];
1519
2116
  const jsFiles = findFiles(rootDir, [".ts", ".tsx", ".js", ".jsx"], 5);
1520
2117
  for (const file of jsFiles.slice(0, 100)) {
1521
- const content = readFileSafe(file);
2118
+ const content = readFileSafe2(file);
1522
2119
  if (!content) continue;
1523
2120
  const matches = content.matchAll(/fontFamily\s*:\s*['"]([^'"]+)['"]/g);
1524
2121
  for (const match of matches) {
@@ -1542,7 +2139,7 @@ function detectSwiftFonts(rootDir) {
1542
2139
  const fonts = [];
1543
2140
  const swiftFiles = findFiles(rootDir, [".swift"], 6);
1544
2141
  for (const file of swiftFiles.slice(0, 80)) {
1545
- const content = readFileSafe(file);
2142
+ const content = readFileSafe2(file);
1546
2143
  if (!content) continue;
1547
2144
  const uiFontMatches = content.matchAll(/UIFont\s*\(\s*name:\s*"([^"]+)"/g);
1548
2145
  for (const match of uiFontMatches) {
@@ -1558,7 +2155,7 @@ function detectSwiftFonts(rootDir) {
1558
2155
  ...findPlistFiles(rootDir)
1559
2156
  ];
1560
2157
  for (const plistPath of plistPaths) {
1561
- const content = readFileSafe(plistPath);
2158
+ const content = readFileSafe2(plistPath);
1562
2159
  if (!content) continue;
1563
2160
  try {
1564
2161
  const data = plist2.parse(content);
@@ -1590,7 +2187,7 @@ function findPlistFiles(rootDir) {
1590
2187
  function search(dir, depth) {
1591
2188
  if (depth > 4) return;
1592
2189
  try {
1593
- const entries = readdirSync5(dir);
2190
+ const entries = readdirSync6(dir);
1594
2191
  for (const entry of entries) {
1595
2192
  if (entry.startsWith(".") || entry === "Pods" || entry === "DerivedData" || entry === "build") continue;
1596
2193
  const fullPath = join6(dir, entry);
@@ -1625,7 +2222,7 @@ function detectAndroidFonts(rootDir) {
1625
2222
  }
1626
2223
  const xmlFiles = findFiles(rootDir, [".xml"], 5);
1627
2224
  for (const file of xmlFiles.slice(0, 50)) {
1628
- const content = readFileSafe(file);
2225
+ const content = readFileSafe2(file);
1629
2226
  if (!content) continue;
1630
2227
  const matches = content.matchAll(/android:fontFamily\s*=\s*"(?:@font\/)?([^"]+)"/g);
1631
2228
  for (const match of matches) {
@@ -1635,7 +2232,7 @@ function detectAndroidFonts(rootDir) {
1635
2232
  }
1636
2233
  const ktFiles = findFiles(rootDir, [".kt"], 6);
1637
2234
  for (const file of ktFiles.slice(0, 80)) {
1638
- const content = readFileSafe(file);
2235
+ const content = readFileSafe2(file);
1639
2236
  if (!content) continue;
1640
2237
  const fontResMatches = content.matchAll(/Font\s*\(\s*R\.font\.(\w+)/g);
1641
2238
  for (const match of fontResMatches) {
@@ -1646,12 +2243,111 @@ function detectAndroidFonts(rootDir) {
1646
2243
  }
1647
2244
  return fonts;
1648
2245
  }
2246
+ function detectExpoFonts(rootDir) {
2247
+ const fonts = [];
2248
+ const fontDirs = [
2249
+ join6(rootDir, "assets/fonts"),
2250
+ join6(rootDir, "src/assets/fonts"),
2251
+ join6(rootDir, "assets")
2252
+ ];
2253
+ for (const dir of fontDirs) {
2254
+ const fontFiles = scanForFontFiles(dir, 2);
2255
+ for (const f of fontFiles) {
2256
+ fonts.push(fontNameFromFile(f));
2257
+ }
2258
+ }
2259
+ const jsFiles = findFiles(rootDir, [".ts", ".tsx", ".js", ".jsx"], 6);
2260
+ for (const file of jsFiles.slice(0, 100)) {
2261
+ const content = readFileSafe2(file);
2262
+ if (!content) continue;
2263
+ const useFontsMatch = content.match(/useFonts\s*\(\s*\{([\s\S]*?)\}\s*\)/);
2264
+ if (useFontsMatch) {
2265
+ const block = useFontsMatch[1];
2266
+ const keyMatches = block.matchAll(/['"`]([^'"`]+)['"`]\s*:/g);
2267
+ for (const m of keyMatches) {
2268
+ fonts.push(m[1]);
2269
+ }
2270
+ }
2271
+ const loadAsyncMatch = content.match(/Font\.loadAsync\s*\(\s*\{([\s\S]*?)\}\s*\)/);
2272
+ if (loadAsyncMatch) {
2273
+ const block = loadAsyncMatch[1];
2274
+ const keyMatches = block.matchAll(/['"`]([^'"`]+)['"`]\s*:/g);
2275
+ for (const m of keyMatches) {
2276
+ fonts.push(m[1]);
2277
+ }
2278
+ }
2279
+ const familyMatches = content.matchAll(/fontFamily\s*:\s*['"]([^'"]+)['"]/g);
2280
+ for (const m of familyMatches) {
2281
+ fonts.push(m[1]);
2282
+ }
2283
+ }
2284
+ const appJsonContent = readFileSafe2(join6(rootDir, "app.json"));
2285
+ if (appJsonContent) {
2286
+ try {
2287
+ const data = JSON.parse(appJsonContent);
2288
+ const expoFonts = data.expo?.fonts || data.fonts;
2289
+ if (Array.isArray(expoFonts)) {
2290
+ for (const entry of expoFonts) {
2291
+ if (typeof entry === "string") {
2292
+ fonts.push(fontNameFromFile(entry));
2293
+ } else if (entry?.name) {
2294
+ fonts.push(entry.name);
2295
+ } else if (entry?.path) {
2296
+ fonts.push(fontNameFromFile(entry.path));
2297
+ }
2298
+ }
2299
+ }
2300
+ } catch {
2301
+ }
2302
+ }
2303
+ return fonts;
2304
+ }
2305
+ function detectMauiFonts(rootDir) {
2306
+ const fonts = [];
2307
+ const fontDirs = [
2308
+ join6(rootDir, "Resources/Fonts"),
2309
+ join6(rootDir, "Resources/Font")
2310
+ ];
2311
+ for (const dir of fontDirs) {
2312
+ const fontFiles = scanForFontFiles(dir, 2);
2313
+ for (const f of fontFiles) {
2314
+ fonts.push(fontNameFromFile(f));
2315
+ }
2316
+ }
2317
+ const csFiles = findFiles(rootDir, [".cs"], 4);
2318
+ for (const file of csFiles.slice(0, 30)) {
2319
+ const content = readFileSafe2(file);
2320
+ if (!content) continue;
2321
+ if (!content.includes("AddFont")) continue;
2322
+ const matches = content.matchAll(
2323
+ /\.AddFont\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g
2324
+ );
2325
+ for (const match of matches) {
2326
+ fonts.push(match[2]);
2327
+ }
2328
+ }
2329
+ const xamlFiles = findFiles(rootDir, [".xaml"], 5);
2330
+ for (const file of xamlFiles.slice(0, 50)) {
2331
+ const content = readFileSafe2(file);
2332
+ if (!content) continue;
2333
+ const matches = content.matchAll(/FontFamily\s*=\s*"([^"]+)"/g);
2334
+ for (const match of matches) {
2335
+ const v = match[1];
2336
+ if (v.startsWith("{") || v.includes(",")) continue;
2337
+ fonts.push(v);
2338
+ }
2339
+ }
2340
+ return fonts;
2341
+ }
1649
2342
  function detectFonts(rootDir, techStack) {
1650
2343
  let rawFonts;
1651
2344
  switch (techStack) {
1652
2345
  case "flutter":
1653
2346
  rawFonts = detectFlutterFonts(rootDir);
1654
2347
  break;
2348
+ case "expo":
2349
+ rawFonts = detectExpoFonts(rootDir);
2350
+ break;
1655
2351
  case "react-native":
1656
2352
  case "capacitor":
1657
2353
  rawFonts = detectJSFonts(rootDir);
@@ -1660,9 +2356,11 @@ function detectFonts(rootDir, techStack) {
1660
2356
  rawFonts = detectSwiftFonts(rootDir);
1661
2357
  break;
1662
2358
  case "kotlin":
1663
- case "java":
1664
2359
  rawFonts = detectAndroidFonts(rootDir);
1665
2360
  break;
2361
+ case "dotnet-maui":
2362
+ rawFonts = detectMauiFonts(rootDir);
2363
+ break;
1666
2364
  default:
1667
2365
  rawFonts = [];
1668
2366
  }
@@ -1681,7 +2379,7 @@ function detectFonts(rootDir, techStack) {
1681
2379
  }
1682
2380
 
1683
2381
  // src/analyzers/source-reader.ts
1684
- import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
2382
+ import { readdirSync as readdirSync7, statSync as statSync4 } from "fs";
1685
2383
  import { join as join7, relative as relative2 } from "path";
1686
2384
  var README_NAMES = [
1687
2385
  "README.md",
@@ -1721,7 +2419,7 @@ var TREE_IGNORE = /* @__PURE__ */ new Set([
1721
2419
  ]);
1722
2420
  function readReadme(rootDir) {
1723
2421
  for (const name of README_NAMES) {
1724
- const content = readFileSafe(join7(rootDir, name));
2422
+ const content = readFileSafe2(join7(rootDir, name));
1725
2423
  if (content && content.trim().length > 0) {
1726
2424
  return content.substring(0, 1e4);
1727
2425
  }
@@ -1741,7 +2439,7 @@ function readSourceCode(rootDir, techStack, maxTotalChars = 5e4) {
1741
2439
  let totalChars = 0;
1742
2440
  for (const file of sorted) {
1743
2441
  if (totalChars >= maxTotalChars) break;
1744
- const content = readFileSafe(join7(rootDir, file));
2442
+ const content = readFileSafe2(join7(rootDir, file));
1745
2443
  if (!content || content.trim().length < 20) continue;
1746
2444
  if (isTestFile(file)) continue;
1747
2445
  if (isGeneratedFile(file, content)) continue;
@@ -1760,8 +2458,7 @@ function getExtensionsForStack(techStack) {
1760
2458
  return [".swift"];
1761
2459
  case "kotlin":
1762
2460
  return [".kt", ".kts"];
1763
- case "java":
1764
- return [".java"];
2461
+ case "expo":
1765
2462
  case "react-native":
1766
2463
  case "capacitor":
1767
2464
  return [".ts", ".tsx", ".js", ".jsx"];
@@ -1792,11 +2489,30 @@ function getPriorityPatternsForStack(techStack) {
1792
2489
  case "swift":
1793
2490
  return [...common, "view", "controller", "manager", "delegate", "contentview"];
1794
2491
  case "kotlin":
1795
- case "java":
1796
- return [...common, "activity", "fragment", "viewmodel", "repository"];
2492
+ return [
2493
+ ...common,
2494
+ "activity",
2495
+ "fragment",
2496
+ "viewmodel",
2497
+ "repository",
2498
+ // Jetpack Compose deyimleri — Compose-first projelerde UI kodları
2499
+ // bunlar olmadan priority listesinde aşağı sıralanıyordu
2500
+ "composable",
2501
+ "screen",
2502
+ "theme",
2503
+ "navigation",
2504
+ "navgraph",
2505
+ "scaffold",
2506
+ "compose",
2507
+ "material"
2508
+ ];
2509
+ case "expo":
2510
+ return [...common, "screen", "component", "hook", "context", "store", "slice", "_layout", "tabs"];
1797
2511
  case "react-native":
1798
2512
  case "capacitor":
1799
2513
  return [...common, "screen", "component", "hook", "context", "store", "slice"];
2514
+ case "dotnet-maui":
2515
+ return [...common, "page", "view", "viewmodel", "service", "mauiprogram", "appshell"];
1800
2516
  default:
1801
2517
  return common;
1802
2518
  }
@@ -1819,7 +2535,14 @@ function sortByPriority(files, patterns) {
1819
2535
  }
1820
2536
  function isTestFile(file) {
1821
2537
  const lower = file.toLowerCase();
1822
- return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__");
2538
+ if (/(?:^|[/\\])(?:test|tests|__tests__|spec|specs|androidtest|mocks?|fixtures?)[/\\]/.test(
2539
+ lower
2540
+ )) {
2541
+ return true;
2542
+ }
2543
+ if (/\.(?:test|spec|mock|fixture)\.[a-z]+$/.test(lower)) return true;
2544
+ if (/(?:test|tests|spec|specs)\.[a-z]+$/.test(lower)) return true;
2545
+ return false;
1823
2546
  }
1824
2547
  function isGeneratedFile(file, content) {
1825
2548
  const lower = file.toLowerCase();
@@ -1836,7 +2559,7 @@ function generateProjectTree(rootDir, maxDepth = 5, maxEntries = 300) {
1836
2559
  if (depth > maxDepth || entryCount >= maxEntries) return;
1837
2560
  let entries;
1838
2561
  try {
1839
- entries = readdirSync6(dir).sort((a, b) => {
2562
+ entries = readdirSync7(dir).sort((a, b) => {
1840
2563
  const aIsDir = isDir(join7(dir, a));
1841
2564
  const bIsDir = isDir(join7(dir, b));
1842
2565
  if (aIsDir && !bIsDir) return -1;
@@ -1885,7 +2608,7 @@ function generateProjectTree(rootDir, maxDepth = 5, maxEntries = 300) {
1885
2608
  }
1886
2609
 
1887
2610
  // src/analyzers/asset-scanner.ts
1888
- import { readFileSync as readFileSync4, existsSync as existsSync5, readdirSync as readdirSync7, statSync as statSync5 } from "fs";
2611
+ import { readFileSync as readFileSync4, existsSync as existsSync5, readdirSync as readdirSync8, statSync as statSync5 } from "fs";
1889
2612
  import { join as join8, extname as extname3, relative as relative3, basename as basename2 } from "path";
1890
2613
  var MAX_ASSET_SIZE = 5 * 1024 * 1024;
1891
2614
  var MAX_TOTAL_ASSETS = 10;
@@ -1927,7 +2650,7 @@ function collectImagesRecursive(rootDir, dirPath, assetType, assets, maxCount, m
1927
2650
  if (currentDepth > maxDepth) return;
1928
2651
  if (!existsSync5(dirPath)) return;
1929
2652
  try {
1930
- const entries = readdirSync7(dirPath);
2653
+ const entries = readdirSync8(dirPath);
1931
2654
  for (const entry of entries) {
1932
2655
  if (assets.filter((a) => a.asset_type === assetType).length >= maxCount) return;
1933
2656
  if (assets.length >= MAX_TOTAL_ASSETS) return;
@@ -1976,6 +2699,17 @@ function getScreenshotDirs(rootDir, techStack) {
1976
2699
  gen(join8(rootDir, "assets"))
1977
2700
  );
1978
2701
  break;
2702
+ case "expo":
2703
+ dirs.push(
2704
+ ss(join8(rootDir, "assets/screenshots")),
2705
+ ss(join8(rootDir, "src/assets/screenshots")),
2706
+ ss(join8(rootDir, "docs/screenshots")),
2707
+ gen(join8(rootDir, "assets/images")),
2708
+ gen(join8(rootDir, "assets")),
2709
+ gen(join8(rootDir, "src/assets/images")),
2710
+ gen(join8(rootDir, "src/assets"))
2711
+ );
2712
+ break;
1979
2713
  case "react-native":
1980
2714
  case "capacitor":
1981
2715
  dirs.push(
@@ -1996,7 +2730,6 @@ function getScreenshotDirs(rootDir, techStack) {
1996
2730
  );
1997
2731
  break;
1998
2732
  case "kotlin":
1999
- case "java":
2000
2733
  dirs.push(
2001
2734
  ss(join8(rootDir, "fastlane/metadata/android/en-US/images/phoneScreenshots")),
2002
2735
  gen(join8(rootDir, "metadata/android/en-US/images")),
@@ -2004,6 +2737,15 @@ function getScreenshotDirs(rootDir, techStack) {
2004
2737
  gen(join8(rootDir, "Marketing"))
2005
2738
  );
2006
2739
  break;
2740
+ case "dotnet-maui":
2741
+ dirs.push(
2742
+ ss(join8(rootDir, "Resources/Images/screenshots")),
2743
+ ss(join8(rootDir, "Resources/Screenshots")),
2744
+ gen(join8(rootDir, "Resources/Images")),
2745
+ gen(join8(rootDir, "marketing")),
2746
+ gen(join8(rootDir, "Marketing"))
2747
+ );
2748
+ break;
2007
2749
  }
2008
2750
  return dirs;
2009
2751
  }
@@ -2030,6 +2772,31 @@ function getSplashPaths(rootDir, techStack) {
2030
2772
  );
2031
2773
  }
2032
2774
  break;
2775
+ case "expo":
2776
+ paths.push(
2777
+ join8(rootDir, "assets/splash.png"),
2778
+ join8(rootDir, "assets/splash-icon.png"),
2779
+ join8(rootDir, "assets/images/splash.png"),
2780
+ join8(rootDir, "assets/images/splash-icon.png"),
2781
+ join8(rootDir, "src/assets/splash.png")
2782
+ );
2783
+ const expoSplashContent = readFileSafe(join8(rootDir, "app.json"));
2784
+ if (expoSplashContent) {
2785
+ try {
2786
+ const data = JSON.parse(expoSplashContent);
2787
+ const expo = data.expo || data;
2788
+ const candidates = [
2789
+ expo.splash?.image,
2790
+ expo.android?.splash?.image,
2791
+ expo.ios?.splash?.image
2792
+ ].filter((v) => typeof v === "string");
2793
+ for (const rel of candidates) {
2794
+ paths.push(join8(rootDir, rel.replace(/^\.?\//, "")));
2795
+ }
2796
+ } catch {
2797
+ }
2798
+ }
2799
+ break;
2033
2800
  case "react-native":
2034
2801
  case "capacitor":
2035
2802
  paths.push(
@@ -2040,7 +2807,6 @@ function getSplashPaths(rootDir, techStack) {
2040
2807
  );
2041
2808
  break;
2042
2809
  case "kotlin":
2043
- case "java":
2044
2810
  for (const density of ["xxxhdpi", "xxhdpi", "xhdpi"]) {
2045
2811
  paths.push(
2046
2812
  join8(rootDir, `app/src/main/res/drawable-${density}/splash.png`),
@@ -2048,6 +2814,14 @@ function getSplashPaths(rootDir, techStack) {
2048
2814
  );
2049
2815
  }
2050
2816
  break;
2817
+ case "dotnet-maui":
2818
+ paths.push(
2819
+ join8(rootDir, "Resources/Splash/splash.png"),
2820
+ join8(rootDir, "Resources/Splash/splashscreen.png"),
2821
+ join8(rootDir, "Resources/Splash/splash.svg"),
2822
+ join8(rootDir, "Resources/Images/splash.png")
2823
+ );
2824
+ break;
2051
2825
  }
2052
2826
  return paths;
2053
2827
  }
@@ -2060,19 +2834,26 @@ function getFeatureGraphicPaths(rootDir, techStack) {
2060
2834
  );
2061
2835
  switch (techStack) {
2062
2836
  case "flutter":
2837
+ case "expo":
2063
2838
  case "react-native":
2064
2839
  case "capacitor":
2065
2840
  paths.push(
2066
2841
  join8(rootDir, "assets/feature_graphic.png"),
2067
- join8(rootDir, "assets/feature-graphic.png")
2842
+ join8(rootDir, "assets/feature-graphic.png"),
2843
+ join8(rootDir, "assets/images/feature_graphic.png")
2068
2844
  );
2069
2845
  break;
2070
2846
  case "kotlin":
2071
- case "java":
2072
2847
  paths.push(
2073
2848
  join8(rootDir, "app/src/main/feature_graphic.png")
2074
2849
  );
2075
2850
  break;
2851
+ case "dotnet-maui":
2852
+ paths.push(
2853
+ join8(rootDir, "Resources/Images/feature_graphic.png"),
2854
+ join8(rootDir, "Resources/Images/feature-graphic.png")
2855
+ );
2856
+ break;
2076
2857
  }
2077
2858
  return paths;
2078
2859
  }
@@ -2607,7 +3388,7 @@ async function analyzeCommand(options) {
2607
3388
  if (techStack.stack === "unknown") {
2608
3389
  techSpinner.fail(
2609
3390
  chalk.red(
2610
- "No supported project found. Supported: Swift, Flutter, React Native, Kotlin, Capacitor, .NET MAUI"
3391
+ "No supported project found. Supported: Swift, Flutter, React Native, Expo, Kotlin, Capacitor, .NET MAUI"
2611
3392
  )
2612
3393
  );
2613
3394
  process.exit(1);