@forvibe/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2215 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/analyze.ts
7
+ import { createInterface } from "readline";
8
+ import chalk from "chalk";
9
+ import ora from "ora";
10
+
11
+ // src/analyzers/tech-detector.ts
12
+ import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
13
+ import { join as join2 } from "path";
14
+
15
+ // src/utils/file-scanner.ts
16
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
17
+ import { join, relative } from "path";
18
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
19
+ "node_modules",
20
+ ".git",
21
+ ".svn",
22
+ ".hg",
23
+ "build",
24
+ "dist",
25
+ "Pods",
26
+ ".dart_tool",
27
+ ".gradle",
28
+ ".idea",
29
+ ".vscode",
30
+ "__pycache__",
31
+ ".next",
32
+ ".nuxt",
33
+ "DerivedData",
34
+ ".build",
35
+ ".swiftpm",
36
+ "vendor",
37
+ ".pub-cache",
38
+ "coverage",
39
+ ".cache"
40
+ ]);
41
+ var IGNORE_EXTENSIONS = /* @__PURE__ */ new Set([
42
+ ".lock",
43
+ ".png",
44
+ ".jpg",
45
+ ".jpeg",
46
+ ".gif",
47
+ ".svg",
48
+ ".ico",
49
+ ".woff",
50
+ ".woff2",
51
+ ".ttf",
52
+ ".eot",
53
+ ".mp3",
54
+ ".mp4",
55
+ ".wav",
56
+ ".avi",
57
+ ".mov",
58
+ ".zip",
59
+ ".tar",
60
+ ".gz",
61
+ ".rar",
62
+ ".7z",
63
+ ".pdf",
64
+ ".db",
65
+ ".sqlite",
66
+ ".sqlite3"
67
+ ]);
68
+ function shouldIgnore(name) {
69
+ if (name.startsWith(".") && name !== ".env" && name !== ".gitignore") {
70
+ return true;
71
+ }
72
+ return IGNORE_DIRS.has(name);
73
+ }
74
+ function scanDirectory(rootDir, options = {}) {
75
+ const { maxDepth = 10, extensions, fileNames, maxFiles = 5e3 } = options;
76
+ const files = [];
77
+ function walk(dir, depth) {
78
+ if (depth > maxDepth || files.length >= maxFiles) return;
79
+ let entries;
80
+ try {
81
+ entries = readdirSync(dir);
82
+ } catch {
83
+ return;
84
+ }
85
+ for (const entry of entries) {
86
+ if (files.length >= maxFiles) return;
87
+ if (shouldIgnore(entry)) continue;
88
+ const fullPath = join(dir, entry);
89
+ let stat;
90
+ try {
91
+ stat = statSync(fullPath);
92
+ } catch {
93
+ continue;
94
+ }
95
+ if (stat.isDirectory()) {
96
+ walk(fullPath, depth + 1);
97
+ } else if (stat.isFile()) {
98
+ const ext = entry.substring(entry.lastIndexOf(".")).toLowerCase();
99
+ if (IGNORE_EXTENSIONS.has(ext)) continue;
100
+ if (extensions && !extensions.some((e) => entry.endsWith(e))) continue;
101
+ if (fileNames && !fileNames.includes(entry)) continue;
102
+ files.push(relative(rootDir, fullPath));
103
+ }
104
+ }
105
+ }
106
+ walk(rootDir, 0);
107
+ return files;
108
+ }
109
+ function findFile(rootDir, fileName, maxDepth = 5) {
110
+ const results = scanDirectory(rootDir, {
111
+ fileNames: [fileName],
112
+ maxDepth,
113
+ maxFiles: 1
114
+ });
115
+ return results.length > 0 ? join(rootDir, results[0]) : null;
116
+ }
117
+ function findAllFiles(rootDir, fileName, maxDepth = 5) {
118
+ const results = scanDirectory(rootDir, {
119
+ fileNames: [fileName],
120
+ maxDepth,
121
+ maxFiles: 50
122
+ });
123
+ return results.map((f) => join(rootDir, f));
124
+ }
125
+ function findFiles(rootDir, extensions, maxDepth = 5) {
126
+ return scanDirectory(rootDir, { extensions, maxDepth }).map(
127
+ (f) => join(rootDir, f)
128
+ );
129
+ }
130
+ function readFileSafe(filePath) {
131
+ try {
132
+ if (!existsSync(filePath)) return null;
133
+ const stat = statSync(filePath);
134
+ if (stat.size > 1024 * 1024) return null;
135
+ return readFileSync(filePath, "utf-8");
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ // src/analyzers/tech-detector.ts
142
+ var DETECTION_RULES = [
143
+ {
144
+ stack: "flutter",
145
+ label: "Flutter (Dart)",
146
+ platforms: ["ios", "android"],
147
+ detect: (dir) => {
148
+ const files = [];
149
+ if (existsSync2(join2(dir, "pubspec.yaml"))) files.push("pubspec.yaml");
150
+ if (existsSync2(join2(dir, "lib"))) files.push("lib/");
151
+ if (existsSync2(join2(dir, "android"))) files.push("android/");
152
+ if (existsSync2(join2(dir, "ios"))) files.push("ios/");
153
+ return files.length >= 2 ? files : [];
154
+ }
155
+ },
156
+ {
157
+ stack: "react-native",
158
+ label: "React Native",
159
+ platforms: ["ios", "android"],
160
+ detect: (dir) => {
161
+ const files = [];
162
+ const pkgPath = join2(dir, "package.json");
163
+ if (existsSync2(pkgPath)) {
164
+ const content = readFileSafe(pkgPath);
165
+ if (content && content.includes('"react-native"')) {
166
+ files.push("package.json");
167
+ if (existsSync2(join2(dir, "app.json"))) files.push("app.json");
168
+ if (existsSync2(join2(dir, "ios"))) files.push("ios/");
169
+ if (existsSync2(join2(dir, "android"))) files.push("android/");
170
+ }
171
+ }
172
+ return files;
173
+ }
174
+ },
175
+ {
176
+ stack: "capacitor",
177
+ label: "Capacitor (Ionic)",
178
+ platforms: ["ios", "android"],
179
+ detect: (dir) => {
180
+ const files = [];
181
+ const configNames = [
182
+ "capacitor.config.ts",
183
+ "capacitor.config.js",
184
+ "capacitor.config.json"
185
+ ];
186
+ for (const name of configNames) {
187
+ if (existsSync2(join2(dir, name))) {
188
+ files.push(name);
189
+ break;
190
+ }
191
+ }
192
+ if (existsSync2(join2(dir, "ios"))) files.push("ios/");
193
+ if (existsSync2(join2(dir, "android"))) files.push("android/");
194
+ return files.length >= 2 ? files : [];
195
+ }
196
+ },
197
+ {
198
+ stack: "swift",
199
+ label: "Swift / SwiftUI",
200
+ platforms: ["ios"],
201
+ detect: (dir) => {
202
+ const files = [];
203
+ try {
204
+ const entries = readdirSync2(dir);
205
+ for (const entry of entries) {
206
+ if (entry.endsWith(".xcodeproj")) files.push(entry);
207
+ if (entry.endsWith(".xcworkspace")) files.push(entry);
208
+ }
209
+ } catch {
210
+ }
211
+ if (existsSync2(join2(dir, "Package.swift"))) files.push("Package.swift");
212
+ if (existsSync2(join2(dir, "Podfile"))) files.push("Podfile");
213
+ try {
214
+ const entries = readdirSync2(dir);
215
+ for (const entry of entries) {
216
+ if (entry.endsWith(".swift")) {
217
+ files.push(entry);
218
+ break;
219
+ }
220
+ }
221
+ } catch {
222
+ }
223
+ return files;
224
+ }
225
+ },
226
+ {
227
+ stack: "kotlin",
228
+ label: "Android (Kotlin)",
229
+ platforms: ["android"],
230
+ detect: (dir) => {
231
+ const files = [];
232
+ const hasGradle = existsSync2(join2(dir, "build.gradle")) || existsSync2(join2(dir, "build.gradle.kts"));
233
+ const hasApp = existsSync2(join2(dir, "app/build.gradle")) || existsSync2(join2(dir, "app/build.gradle.kts"));
234
+ const hasAndroidManifest = existsSync2(
235
+ join2(dir, "app/src/main/AndroidManifest.xml")
236
+ );
237
+ if (hasGradle) files.push("build.gradle");
238
+ if (hasApp) files.push("app/build.gradle");
239
+ if (hasAndroidManifest) files.push("AndroidManifest.xml");
240
+ if (existsSync2(join2(dir, "pubspec.yaml")) || existsSync2(join2(dir, "package.json"))) {
241
+ return [];
242
+ }
243
+ return files.length >= 2 ? files : [];
244
+ }
245
+ },
246
+ {
247
+ stack: "dotnet-maui",
248
+ label: ".NET MAUI",
249
+ platforms: ["ios", "android"],
250
+ detect: (dir) => {
251
+ const files = [];
252
+ try {
253
+ const entries = readdirSync2(dir);
254
+ for (const entry of entries) {
255
+ if (entry.endsWith(".csproj")) {
256
+ const content = readFileSafe(join2(dir, entry));
257
+ if (content && (content.includes("Maui") || content.includes("MAUI"))) {
258
+ files.push(entry);
259
+ }
260
+ }
261
+ }
262
+ } catch {
263
+ }
264
+ if (existsSync2(join2(dir, "Platforms"))) files.push("Platforms/");
265
+ return files.length >= 1 ? files : [];
266
+ }
267
+ }
268
+ ];
269
+ function detectTechStack(rootDir) {
270
+ for (const rule of DETECTION_RULES) {
271
+ const configFiles = rule.detect(rootDir);
272
+ if (configFiles.length > 0) {
273
+ return {
274
+ stack: rule.stack,
275
+ label: rule.label,
276
+ platforms: rule.platforms,
277
+ configFiles
278
+ };
279
+ }
280
+ }
281
+ return {
282
+ stack: "unknown",
283
+ label: "Unknown",
284
+ platforms: [],
285
+ configFiles: []
286
+ };
287
+ }
288
+
289
+ // src/analyzers/config-parser.ts
290
+ import { readdirSync as readdirSync3 } from "fs";
291
+ import { join as join3 } from "path";
292
+ import plist from "plist";
293
+ import YAML from "yaml";
294
+ function parseConfig(rootDir, techStack) {
295
+ switch (techStack) {
296
+ case "flutter":
297
+ return parseFlutterConfig(rootDir);
298
+ case "react-native":
299
+ return parseReactNativeConfig(rootDir);
300
+ case "swift":
301
+ return parseSwiftConfig(rootDir);
302
+ case "kotlin":
303
+ case "java":
304
+ return parseAndroidConfig(rootDir);
305
+ case "capacitor":
306
+ return parseCapacitorConfig(rootDir);
307
+ case "dotnet-maui":
308
+ return parseMauiConfig(rootDir);
309
+ default:
310
+ return emptyConfig();
311
+ }
312
+ }
313
+ function emptyConfig() {
314
+ return {
315
+ app_name: null,
316
+ bundle_id: null,
317
+ version: null,
318
+ min_ios_version: null,
319
+ min_android_sdk: null,
320
+ description: null
321
+ };
322
+ }
323
+ function parseFlutterConfig(rootDir) {
324
+ const config = emptyConfig();
325
+ const pubspecContent = readFileSafe(join3(rootDir, "pubspec.yaml"));
326
+ if (pubspecContent) {
327
+ try {
328
+ const pubspec = YAML.parse(pubspecContent);
329
+ config.app_name = pubspec.name || null;
330
+ config.description = pubspec.description || null;
331
+ config.version = pubspec.version?.split("+")[0] || null;
332
+ } catch {
333
+ }
334
+ }
335
+ const infoPlistPath = findFile(
336
+ join3(rootDir, "ios"),
337
+ "Info.plist",
338
+ 4
339
+ );
340
+ if (infoPlistPath) {
341
+ const plistData = parsePlist(infoPlistPath);
342
+ if (plistData) {
343
+ config.bundle_id = plistData.CFBundleIdentifier || config.bundle_id;
344
+ config.min_ios_version = plistData.MinimumOSVersion || null;
345
+ }
346
+ }
347
+ if (!config.bundle_id || config.bundle_id.includes("$(")) {
348
+ const pbxprojPath = findFile(join3(rootDir, "ios"), "project.pbxproj", 4);
349
+ if (pbxprojPath) {
350
+ const content = readFileSafe(pbxprojPath);
351
+ if (content) {
352
+ const bundleMatch = content.match(
353
+ /PRODUCT_BUNDLE_IDENTIFIER\s*=\s*"?([^";]+)"?/
354
+ );
355
+ if (bundleMatch && !bundleMatch[1].includes("$(")) {
356
+ config.bundle_id = bundleMatch[1];
357
+ }
358
+ }
359
+ }
360
+ }
361
+ const buildGradlePath = findFile(join3(rootDir, "android/app"), "build.gradle", 2) || findFile(join3(rootDir, "android/app"), "build.gradle.kts", 2);
362
+ if (buildGradlePath) {
363
+ const gradleConfig = parseGradle(buildGradlePath);
364
+ if (!config.bundle_id && gradleConfig.applicationId) {
365
+ config.bundle_id = gradleConfig.applicationId;
366
+ }
367
+ config.min_android_sdk = gradleConfig.minSdk || null;
368
+ }
369
+ return config;
370
+ }
371
+ function parseReactNativeConfig(rootDir) {
372
+ const config = emptyConfig();
373
+ const appJsonContent = readFileSafe(join3(rootDir, "app.json"));
374
+ if (appJsonContent) {
375
+ try {
376
+ const appJson = JSON.parse(appJsonContent);
377
+ config.app_name = appJson.displayName || appJson.name || null;
378
+ } catch {
379
+ }
380
+ }
381
+ const pkgContent = readFileSafe(join3(rootDir, "package.json"));
382
+ if (pkgContent) {
383
+ try {
384
+ const pkg = JSON.parse(pkgContent);
385
+ config.version = pkg.version || null;
386
+ config.description = pkg.description || null;
387
+ if (!config.app_name) config.app_name = pkg.name || null;
388
+ } catch {
389
+ }
390
+ }
391
+ const infoPlistPath = findFile(join3(rootDir, "ios"), "Info.plist", 4);
392
+ if (infoPlistPath) {
393
+ const plistData = parsePlist(infoPlistPath);
394
+ if (plistData) {
395
+ config.bundle_id = plistData.CFBundleIdentifier || null;
396
+ config.min_ios_version = plistData.MinimumOSVersion || null;
397
+ }
398
+ }
399
+ if (!config.bundle_id || config.bundle_id.includes("$(")) {
400
+ const pbxprojPath = findFile(join3(rootDir, "ios"), "project.pbxproj", 4);
401
+ if (pbxprojPath) {
402
+ const content = readFileSafe(pbxprojPath);
403
+ if (content) {
404
+ const bundleMatch = content.match(
405
+ /PRODUCT_BUNDLE_IDENTIFIER\s*=\s*"?([^";]+)"?/
406
+ );
407
+ if (bundleMatch && !bundleMatch[1].includes("$(")) {
408
+ config.bundle_id = bundleMatch[1];
409
+ }
410
+ }
411
+ }
412
+ }
413
+ const buildGradlePath = findFile(join3(rootDir, "android/app"), "build.gradle", 2) || findFile(join3(rootDir, "android/app"), "build.gradle.kts", 2);
414
+ if (buildGradlePath) {
415
+ const gradleConfig = parseGradle(buildGradlePath);
416
+ if (!config.bundle_id && gradleConfig.applicationId) {
417
+ config.bundle_id = gradleConfig.applicationId;
418
+ }
419
+ config.min_android_sdk = gradleConfig.minSdk || null;
420
+ }
421
+ return config;
422
+ }
423
+ var EXTENSION_SUFFIXES = [
424
+ "Extension",
425
+ "Widget",
426
+ "WidgetExtension",
427
+ "Intent",
428
+ "IntentExtension",
429
+ "NotificationService",
430
+ "NotificationContent",
431
+ "ShieldConfiguration",
432
+ "ShieldConfigurationExtension",
433
+ "ShieldAction",
434
+ "ShieldActionExtension",
435
+ "WatchKit",
436
+ "Watch",
437
+ "Clip",
438
+ "Tests",
439
+ "UITests",
440
+ "StickerPack",
441
+ "ShareExtension",
442
+ "TodayExtension",
443
+ "KeyboardExtension"
444
+ ];
445
+ function isExtensionBundleId(bundleId) {
446
+ return EXTENSION_SUFFIXES.some(
447
+ (suffix) => bundleId.endsWith(`.${suffix}`) || bundleId.toLowerCase().endsWith(`.${suffix.toLowerCase()}`)
448
+ );
449
+ }
450
+ function isXcodeVariable(value) {
451
+ return value.includes("$(") || value.includes("${");
452
+ }
453
+ function parsePbxproj(content) {
454
+ const bundleIdMatches = content.matchAll(
455
+ /PRODUCT_BUNDLE_IDENTIFIER\s*=\s*"?([^";]+)"?/g
456
+ );
457
+ const allBundleIds = [];
458
+ for (const match of bundleIdMatches) {
459
+ const id = match[1].trim();
460
+ if (!isXcodeVariable(id)) {
461
+ allBundleIds.push(id);
462
+ }
463
+ }
464
+ let bundleId = null;
465
+ const nonExtensionIds = allBundleIds.filter((id) => !isExtensionBundleId(id));
466
+ if (nonExtensionIds.length > 0) {
467
+ bundleId = nonExtensionIds.sort((a, b) => a.length - b.length)[0];
468
+ } else if (allBundleIds.length > 0) {
469
+ bundleId = allBundleIds.sort((a, b) => a.length - b.length)[0];
470
+ }
471
+ let appName = null;
472
+ const productNames = content.matchAll(
473
+ /PRODUCT_NAME\s*=\s*"?([^";]+)"?/g
474
+ );
475
+ for (const match of productNames) {
476
+ const name = match[1].trim();
477
+ if (!isXcodeVariable(name) && name !== "$(TARGET_NAME)") {
478
+ appName = name;
479
+ break;
480
+ }
481
+ }
482
+ if (!appName && bundleId) {
483
+ const parts = bundleId.split(".");
484
+ appName = parts[parts.length - 1].replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2");
485
+ }
486
+ if (!appName || isXcodeVariable(appName)) {
487
+ const displayNameMatch = content.match(
488
+ /INFOPLIST_KEY_CFBundleDisplayName\s*=\s*"?([^";]+)"?/
489
+ );
490
+ if (displayNameMatch && !isXcodeVariable(displayNameMatch[1])) {
491
+ appName = displayNameMatch[1].trim();
492
+ }
493
+ }
494
+ const versionMatch = content.match(
495
+ /MARKETING_VERSION\s*=\s*"?([^";]+)"?/
496
+ );
497
+ const iosMatch = content.match(
498
+ /IPHONEOS_DEPLOYMENT_TARGET\s*=\s*"?([^";]+)"?/
499
+ );
500
+ return {
501
+ bundleId,
502
+ appName,
503
+ version: versionMatch ? versionMatch[1].trim() : null,
504
+ minIos: iosMatch ? iosMatch[1].trim() : null
505
+ };
506
+ }
507
+ function parseSwiftConfig(rootDir) {
508
+ const config = emptyConfig();
509
+ const pbxprojPath = findFile(rootDir, "project.pbxproj", 5);
510
+ if (pbxprojPath) {
511
+ const content = readFileSafe(pbxprojPath);
512
+ if (content) {
513
+ const pbx = parsePbxproj(content);
514
+ config.bundle_id = pbx.bundleId;
515
+ config.app_name = pbx.appName;
516
+ config.version = pbx.version;
517
+ config.min_ios_version = pbx.minIos;
518
+ }
519
+ }
520
+ const allPlists = findAllFiles(rootDir, "Info.plist", 5);
521
+ const mainPlist = allPlists.find(
522
+ (p) => !EXTENSION_SUFFIXES.some((ext) => p.includes(ext))
523
+ );
524
+ if (mainPlist) {
525
+ const plistData = parsePlist(mainPlist);
526
+ if (plistData) {
527
+ if (!config.app_name || isXcodeVariable(config.app_name)) {
528
+ const plistName = plistData.CFBundleDisplayName || plistData.CFBundleName;
529
+ if (plistName && !isXcodeVariable(plistName)) {
530
+ config.app_name = plistName;
531
+ }
532
+ }
533
+ if (!config.bundle_id || isXcodeVariable(config.bundle_id)) {
534
+ const plistBundleId = plistData.CFBundleIdentifier;
535
+ if (plistBundleId && !isXcodeVariable(plistBundleId)) {
536
+ config.bundle_id = plistBundleId;
537
+ }
538
+ }
539
+ if (!config.version) {
540
+ config.version = plistData.CFBundleShortVersionString || null;
541
+ }
542
+ if (!config.min_ios_version) {
543
+ config.min_ios_version = plistData.MinimumOSVersion || null;
544
+ }
545
+ }
546
+ }
547
+ if (!config.app_name || isXcodeVariable(config.app_name)) {
548
+ try {
549
+ const entries = readdirSync3(rootDir);
550
+ for (const entry of entries) {
551
+ if (entry.endsWith(".xcodeproj")) {
552
+ config.app_name = entry.replace(".xcodeproj", "");
553
+ break;
554
+ }
555
+ }
556
+ } catch {
557
+ }
558
+ }
559
+ return config;
560
+ }
561
+ function parseAndroidConfig(rootDir) {
562
+ const config = emptyConfig();
563
+ 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);
564
+ if (buildGradlePath) {
565
+ const gradleConfig = parseGradle(buildGradlePath);
566
+ config.bundle_id = gradleConfig.applicationId || null;
567
+ config.version = gradleConfig.versionName || null;
568
+ config.min_android_sdk = gradleConfig.minSdk || null;
569
+ }
570
+ const manifestPath = findFile(rootDir, "AndroidManifest.xml", 6);
571
+ if (manifestPath) {
572
+ const content = readFileSafe(manifestPath);
573
+ if (content) {
574
+ const labelMatch = content.match(/android:label="([^"]+)"/);
575
+ if (labelMatch && !labelMatch[1].startsWith("@")) {
576
+ config.app_name = labelMatch[1];
577
+ }
578
+ }
579
+ }
580
+ if (!config.app_name) {
581
+ const stringsPath = findFile(rootDir, "strings.xml", 8);
582
+ if (stringsPath) {
583
+ const content = readFileSafe(stringsPath);
584
+ if (content) {
585
+ const nameMatch = content.match(
586
+ /<string name="app_name">(.*?)<\/string>/
587
+ );
588
+ if (nameMatch) config.app_name = nameMatch[1];
589
+ }
590
+ }
591
+ }
592
+ return config;
593
+ }
594
+ function parseCapacitorConfig(rootDir) {
595
+ const config = emptyConfig();
596
+ for (const name of [
597
+ "capacitor.config.json",
598
+ "capacitor.config.ts",
599
+ "capacitor.config.js"
600
+ ]) {
601
+ const content = readFileSafe(join3(rootDir, name));
602
+ if (content) {
603
+ if (name.endsWith(".json")) {
604
+ try {
605
+ const capConfig = JSON.parse(content);
606
+ config.app_name = capConfig.appName || null;
607
+ config.bundle_id = capConfig.appId || null;
608
+ } catch {
609
+ }
610
+ } else {
611
+ const appIdMatch = content.match(/appId:\s*['"]([^'"]+)['"]/);
612
+ const appNameMatch = content.match(/appName:\s*['"]([^'"]+)['"]/);
613
+ if (appIdMatch) config.bundle_id = appIdMatch[1];
614
+ if (appNameMatch) config.app_name = appNameMatch[1];
615
+ }
616
+ break;
617
+ }
618
+ }
619
+ const pkgContent = readFileSafe(join3(rootDir, "package.json"));
620
+ if (pkgContent) {
621
+ try {
622
+ const pkg = JSON.parse(pkgContent);
623
+ config.version = pkg.version || null;
624
+ config.description = pkg.description || null;
625
+ } catch {
626
+ }
627
+ }
628
+ return config;
629
+ }
630
+ function parseMauiConfig(rootDir) {
631
+ const config = emptyConfig();
632
+ try {
633
+ const entries = readdirSync3(rootDir);
634
+ for (const entry of entries) {
635
+ if (entry.endsWith(".csproj")) {
636
+ const content = readFileSafe(join3(rootDir, entry));
637
+ if (content) {
638
+ const appIdMatch = content.match(
639
+ /<ApplicationId>(.*?)<\/ApplicationId>/
640
+ );
641
+ const titleMatch = content.match(
642
+ /<ApplicationTitle>(.*?)<\/ApplicationTitle>/
643
+ );
644
+ const versionMatch = content.match(
645
+ /<ApplicationDisplayVersion>(.*?)<\/ApplicationDisplayVersion>/
646
+ );
647
+ if (appIdMatch) config.bundle_id = appIdMatch[1];
648
+ if (titleMatch) config.app_name = titleMatch[1];
649
+ if (versionMatch) config.version = versionMatch[1];
650
+ }
651
+ break;
652
+ }
653
+ }
654
+ } catch {
655
+ }
656
+ return config;
657
+ }
658
+ function parsePlist(filePath) {
659
+ const content = readFileSafe(filePath);
660
+ if (!content) return null;
661
+ try {
662
+ return plist.parse(content);
663
+ } catch {
664
+ return null;
665
+ }
666
+ }
667
+ function parseGradle(filePath) {
668
+ const content = readFileSafe(filePath);
669
+ if (!content) return { applicationId: null, versionName: null, minSdk: null };
670
+ const appIdMatch = content.match(
671
+ /applicationId\s*[=:]\s*['"]([^'"]+)['"]/
672
+ );
673
+ const versionMatch = content.match(
674
+ /versionName\s*[=:]\s*['"]([^'"]+)['"]/
675
+ );
676
+ const minSdkMatch = content.match(
677
+ /minSdk(?:Version)?\s*[=:]\s*(\d+)/
678
+ );
679
+ return {
680
+ applicationId: appIdMatch?.[1] || null,
681
+ versionName: versionMatch?.[1] || null,
682
+ minSdk: minSdkMatch?.[1] || null
683
+ };
684
+ }
685
+
686
+ // src/analyzers/sdk-scanner.ts
687
+ import { join as join4 } from "path";
688
+ import YAML2 from "yaml";
689
+ var SDK_MAP = {
690
+ // Analytics
691
+ firebase_analytics: { category: "analytics", collects: ["usage_analytics", "device_info"] },
692
+ "firebase-analytics": { category: "analytics", collects: ["usage_analytics", "device_info"] },
693
+ firebase_core: { category: "analytics", collects: ["device_info"] },
694
+ "firebase-core": { category: "analytics", collects: ["device_info"] },
695
+ amplitude_flutter: { category: "analytics", collects: ["usage_analytics"] },
696
+ amplitude: { category: "analytics", collects: ["usage_analytics"] },
697
+ "@amplitude/analytics-react-native": { category: "analytics", collects: ["usage_analytics"] },
698
+ mixpanel_flutter: { category: "analytics", collects: ["usage_analytics"] },
699
+ mixpanel: { category: "analytics", collects: ["usage_analytics"] },
700
+ "@mixpanel/mixpanel-react-native": { category: "analytics", collects: ["usage_analytics"] },
701
+ posthog_flutter: { category: "analytics", collects: ["usage_analytics"] },
702
+ "posthog-react-native": { category: "analytics", collects: ["usage_analytics"] },
703
+ sentry_flutter: { category: "analytics", collects: ["device_info"] },
704
+ "@sentry/react-native": { category: "analytics", collects: ["device_info"] },
705
+ appsflyer_sdk: { category: "analytics", collects: ["usage_analytics", "device_info"] },
706
+ "react-native-appsflyer": { category: "analytics", collects: ["usage_analytics", "device_info"] },
707
+ adjust_sdk: { category: "analytics", collects: ["usage_analytics", "device_info"] },
708
+ // Auth
709
+ firebase_auth: { category: "auth", collects: ["name_email"] },
710
+ "firebase-auth": { category: "auth", collects: ["name_email"] },
711
+ google_sign_in: { category: "auth", collects: ["name_email", "profile_photo"] },
712
+ "@react-native-google-signin/google-signin": { category: "auth", collects: ["name_email", "profile_photo"] },
713
+ sign_in_with_apple: { category: "auth", collects: ["name_email"] },
714
+ "@invertase/react-native-apple-authentication": { category: "auth", collects: ["name_email"] },
715
+ flutter_facebook_auth: { category: "auth", collects: ["name_email", "profile_photo"] },
716
+ supabase_flutter: { category: "auth", collects: ["name_email"] },
717
+ "@supabase/supabase-js": { category: "auth", collects: ["name_email"] },
718
+ appwrite: { category: "auth", collects: ["name_email"] },
719
+ // Ads
720
+ google_mobile_ads: { category: "ads", collects: ["device_info"], advertising: "personalized" },
721
+ "react-native-google-mobile-ads": { category: "ads", collects: ["device_info"], advertising: "personalized" },
722
+ facebook_audience_network: { category: "ads", collects: ["device_info"], advertising: "personalized" },
723
+ unity_ads: { category: "ads", collects: ["device_info"], advertising: "personalized" },
724
+ applovin_max: { category: "ads", collects: ["device_info"], advertising: "personalized" },
725
+ ironsource: { category: "ads", collects: ["device_info"], advertising: "personalized" },
726
+ // Payment
727
+ in_app_purchase: { category: "payment", collects: ["financial_info"] },
728
+ in_app_purchase_storekit: { category: "payment", collects: ["financial_info"] },
729
+ "react-native-iap": { category: "payment", collects: ["financial_info"] },
730
+ purchases_flutter: { category: "payment", collects: ["financial_info"] },
731
+ "react-native-purchases": { category: "payment", collects: ["financial_info"] },
732
+ stripe_flutter: { category: "payment", collects: ["financial_info"] },
733
+ "@stripe/stripe-react-native": { category: "payment", collects: ["financial_info"] },
734
+ // Cloud
735
+ cloud_firestore: { category: "cloud", collects: [] },
736
+ firebase_storage: { category: "cloud", collects: [] },
737
+ firebase_database: { category: "cloud", collects: [] },
738
+ aws_amplify: { category: "cloud", collects: [] },
739
+ // Location
740
+ geolocator: { category: "other", collects: ["location"] },
741
+ location: { category: "other", collects: ["location"] },
742
+ "react-native-geolocation-service": { category: "other", collects: ["location"] },
743
+ "@react-native-community/geolocation": { category: "other", collects: ["location"] },
744
+ google_maps_flutter: { category: "other", collects: ["location"] },
745
+ "react-native-maps": { category: "other", collects: ["location"] },
746
+ // Health
747
+ health: { category: "other", collects: ["health_data"] },
748
+ "react-native-health": { category: "other", collects: ["health_data"] },
749
+ // Camera / Photos
750
+ image_picker: { category: "other", collects: ["photos_media"] },
751
+ camera: { category: "other", collects: ["photos_media"] },
752
+ "react-native-image-picker": { category: "other", collects: ["photos_media"] },
753
+ "react-native-camera": { category: "other", collects: ["photos_media"] },
754
+ // Contacts
755
+ contacts_service: { category: "other", collects: ["contacts"] },
756
+ "react-native-contacts": { category: "other", collects: ["contacts"] },
757
+ // AI
758
+ google_generative_ai: { category: "ai", collects: [] },
759
+ openai: { category: "ai", collects: [] },
760
+ langchain: { category: "ai", collects: [] },
761
+ // Social
762
+ share_plus: { category: "social", collects: [] },
763
+ "react-native-share": { category: "social", collects: [] },
764
+ // --- Native iOS frameworks (detected via `import` in Swift source) ---
765
+ HealthKit: { category: "other", collects: ["health_data"] },
766
+ StoreKit: { category: "payment", collects: ["financial_info"] },
767
+ MapKit: { category: "other", collects: ["location"] },
768
+ CoreLocation: { category: "other", collects: ["location"] },
769
+ PhotosUI: { category: "other", collects: ["photos_media"] },
770
+ AVFoundation: { category: "other", collects: ["photos_media"] },
771
+ Contacts: { category: "other", collects: ["contacts"] },
772
+ ContactsUI: { category: "other", collects: ["contacts"] },
773
+ AuthenticationServices: { category: "auth", collects: ["name_email"] },
774
+ AdSupport: { category: "ads", collects: ["device_info"], advertising: "personalized" },
775
+ AppTrackingTransparency: { category: "ads", collects: ["device_info"], advertising: "personalized" },
776
+ GameKit: { category: "other", collects: [] },
777
+ WebKit: { category: "other", collects: [] },
778
+ CoreBluetooth: { category: "other", collects: [] },
779
+ CoreMotion: { category: "other", collects: [] },
780
+ LocalAuthentication: { category: "auth", collects: ["biometric_data"] },
781
+ UserNotifications: { category: "other", collects: [] },
782
+ CloudKit: { category: "cloud", collects: [] },
783
+ CoreData: { category: "cloud", collects: [] },
784
+ CoreML: { category: "ai", collects: [] },
785
+ Vision: { category: "ai", collects: [] },
786
+ NaturalLanguage: { category: "ai", collects: [] },
787
+ SpriteKit: { category: "other", collects: [] },
788
+ SceneKit: { category: "other", collects: [] },
789
+ ARKit: { category: "other", collects: [] },
790
+ RealityKit: { category: "other", collects: [] },
791
+ EventKit: { category: "other", collects: [] },
792
+ // --- Native Android frameworks (detected via `import` in Kotlin/Java source) ---
793
+ "com.google.android.gms.ads": { category: "ads", collects: ["device_info"], advertising: "personalized" },
794
+ "com.google.android.gms.maps": { category: "other", collects: ["location"] },
795
+ "com.google.android.gms.location": { category: "other", collects: ["location"] },
796
+ "com.google.android.gms.auth": { category: "auth", collects: ["name_email", "profile_photo"] },
797
+ "com.android.billingclient": { category: "payment", collects: ["financial_info"] },
798
+ "androidx.health": { category: "other", collects: ["health_data"] },
799
+ "androidx.biometric": { category: "auth", collects: ["biometric_data"] },
800
+ "androidx.camera": { category: "other", collects: ["photos_media"] }
801
+ };
802
+ function scanSDKs(rootDir, techStack) {
803
+ const detectedSDKs = [];
804
+ const dataCollected = /* @__PURE__ */ new Set();
805
+ let advertisingType = "none";
806
+ const thirdPartyServices = {
807
+ analytics: [],
808
+ payment: [],
809
+ auth: [],
810
+ ads: [],
811
+ cloud: [],
812
+ ai: [],
813
+ social: [],
814
+ other: []
815
+ };
816
+ let hasIAP = false;
817
+ const dependencies = getDependencies(rootDir, techStack);
818
+ for (const dep of dependencies) {
819
+ const normalizedDep = dep.toLowerCase().replace(/-/g, "_");
820
+ const mapping = SDK_MAP[dep] || SDK_MAP[normalizedDep];
821
+ if (mapping) {
822
+ detectedSDKs.push({
823
+ name: dep,
824
+ category: mapping.category,
825
+ collects: mapping.collects
826
+ });
827
+ for (const collected of mapping.collects) {
828
+ dataCollected.add(collected);
829
+ }
830
+ if (mapping.advertising && mapping.advertising !== "none") {
831
+ advertisingType = mapping.advertising;
832
+ }
833
+ if (!thirdPartyServices[mapping.category].includes(dep)) {
834
+ thirdPartyServices[mapping.category].push(dep);
835
+ }
836
+ if (mapping.category === "payment") {
837
+ hasIAP = true;
838
+ }
839
+ }
840
+ }
841
+ return {
842
+ detected_sdks: detectedSDKs,
843
+ data_collected: Array.from(dataCollected),
844
+ advertising_type: advertisingType,
845
+ third_party_services: thirdPartyServices,
846
+ has_iap: hasIAP
847
+ };
848
+ }
849
+ function getDependencies(rootDir, techStack) {
850
+ const deps = [];
851
+ switch (techStack) {
852
+ case "flutter": {
853
+ const pubspecContent = readFileSafe(join4(rootDir, "pubspec.yaml"));
854
+ if (pubspecContent) {
855
+ try {
856
+ const pubspec = YAML2.parse(pubspecContent);
857
+ if (pubspec.dependencies) {
858
+ deps.push(...Object.keys(pubspec.dependencies));
859
+ }
860
+ if (pubspec.dev_dependencies) {
861
+ deps.push(...Object.keys(pubspec.dev_dependencies));
862
+ }
863
+ } catch {
864
+ }
865
+ }
866
+ break;
867
+ }
868
+ case "react-native":
869
+ case "capacitor": {
870
+ const pkgContent = readFileSafe(join4(rootDir, "package.json"));
871
+ if (pkgContent) {
872
+ try {
873
+ const pkg = JSON.parse(pkgContent);
874
+ if (pkg.dependencies) deps.push(...Object.keys(pkg.dependencies));
875
+ if (pkg.devDependencies) deps.push(...Object.keys(pkg.devDependencies));
876
+ } catch {
877
+ }
878
+ }
879
+ break;
880
+ }
881
+ case "swift": {
882
+ const podfileContent = readFileSafe(join4(rootDir, "Podfile"));
883
+ if (podfileContent) {
884
+ const podMatches = podfileContent.matchAll(/pod\s+['"]([^'"]+)['"]/g);
885
+ for (const match of podMatches) {
886
+ deps.push(match[1]);
887
+ }
888
+ }
889
+ const packageSwift = readFileSafe(join4(rootDir, "Package.swift"));
890
+ if (packageSwift) {
891
+ const urlMatches = packageSwift.matchAll(
892
+ /\.package\([^)]*url:\s*"[^"]*\/([^/"]+?)(?:\.git)?"/g
893
+ );
894
+ for (const match of urlMatches) {
895
+ deps.push(match[1]);
896
+ }
897
+ }
898
+ const pbxprojPath = findFile(rootDir, "project.pbxproj", 5);
899
+ if (pbxprojPath) {
900
+ const pbxContent = readFileSafe(pbxprojPath);
901
+ if (pbxContent) {
902
+ const spmMatches = pbxContent.matchAll(
903
+ /repositoryURL\s*=\s*"[^"]*\/([^/"]+?)(?:\.git)?"/g
904
+ );
905
+ for (const match of spmMatches) {
906
+ if (!deps.includes(match[1])) {
907
+ deps.push(match[1]);
908
+ }
909
+ }
910
+ }
911
+ }
912
+ const swiftFiles = findFiles(rootDir, [".swift"], 6);
913
+ const nativeImports = /* @__PURE__ */ new Set();
914
+ for (const file of swiftFiles.slice(0, 100)) {
915
+ const content = readFileSafe(file);
916
+ if (!content) continue;
917
+ const importMatches = content.matchAll(/^import\s+(\w+)/gm);
918
+ for (const match of importMatches) {
919
+ const framework = match[1];
920
+ if (SDK_MAP[framework] && !nativeImports.has(framework)) {
921
+ nativeImports.add(framework);
922
+ }
923
+ }
924
+ }
925
+ deps.push(...nativeImports);
926
+ break;
927
+ }
928
+ case "kotlin":
929
+ case "java": {
930
+ const gradlePaths = [
931
+ join4(rootDir, "app/build.gradle"),
932
+ join4(rootDir, "app/build.gradle.kts"),
933
+ join4(rootDir, "build.gradle"),
934
+ join4(rootDir, "build.gradle.kts")
935
+ ];
936
+ for (const path of gradlePaths) {
937
+ const content = readFileSafe(path);
938
+ if (content) {
939
+ const implMatches = content.matchAll(
940
+ /implementation\s*\(?['"]([^'"]+)['"]\)?/g
941
+ );
942
+ for (const match of implMatches) {
943
+ const parts = match[1].split(":");
944
+ if (parts.length >= 2) {
945
+ deps.push(parts[1]);
946
+ }
947
+ }
948
+ }
949
+ }
950
+ const androidFiles = findFiles(join4(rootDir, "app/src"), [".kt", ".java"], 8);
951
+ const androidNativeImports = /* @__PURE__ */ new Set();
952
+ const androidFrameworkPrefixes = [
953
+ "com.google.android.gms.ads",
954
+ "com.google.android.gms.maps",
955
+ "com.google.android.gms.location",
956
+ "com.google.android.gms.auth",
957
+ "com.android.billingclient",
958
+ "androidx.health",
959
+ "androidx.biometric",
960
+ "androidx.camera"
961
+ ];
962
+ for (const file of androidFiles.slice(0, 100)) {
963
+ const content = readFileSafe(file);
964
+ if (!content) continue;
965
+ for (const prefix of androidFrameworkPrefixes) {
966
+ if (content.includes(`import ${prefix}`)) {
967
+ if (!androidNativeImports.has(prefix)) {
968
+ androidNativeImports.add(prefix);
969
+ }
970
+ }
971
+ }
972
+ }
973
+ deps.push(...androidNativeImports);
974
+ break;
975
+ }
976
+ }
977
+ return deps;
978
+ }
979
+
980
+ // src/analyzers/branding.ts
981
+ import { readFileSync as readFileSync2, existsSync as existsSync3, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
982
+ import { join as join5, extname } from "path";
983
+ function extractBranding(rootDir, techStack) {
984
+ const result = {
985
+ primary_color: null,
986
+ secondary_color: null,
987
+ app_icon_base64: null,
988
+ app_icon_path: null
989
+ };
990
+ const colors = extractColors(rootDir, techStack);
991
+ result.primary_color = colors.primary;
992
+ result.secondary_color = colors.secondary;
993
+ const icon = findAppIcon(rootDir, techStack);
994
+ result.app_icon_base64 = icon.base64;
995
+ result.app_icon_path = icon.path;
996
+ return result;
997
+ }
998
+ function extractColors(rootDir, techStack) {
999
+ switch (techStack) {
1000
+ case "flutter":
1001
+ return extractFlutterColors(rootDir);
1002
+ case "react-native":
1003
+ case "capacitor":
1004
+ return extractJSColors(rootDir);
1005
+ case "swift":
1006
+ return extractSwiftColors(rootDir);
1007
+ case "kotlin":
1008
+ case "java":
1009
+ return extractAndroidColors(rootDir);
1010
+ default:
1011
+ return { primary: null, secondary: null };
1012
+ }
1013
+ }
1014
+ function extractFlutterColors(rootDir) {
1015
+ const dartFiles = findFiles(rootDir, [".dart"], 6);
1016
+ const hexColors = [];
1017
+ for (const file of dartFiles.slice(0, 50)) {
1018
+ const content = readFileSafe(file);
1019
+ if (!content) continue;
1020
+ const colorMatches = content.matchAll(/Color\(0[xX]([0-9a-fA-F]{8})\)/g);
1021
+ for (const match of colorMatches) {
1022
+ const hex = match[1].substring(2);
1023
+ hexColors.push(`#${hex}`);
1024
+ }
1025
+ const argbMatches = content.matchAll(
1026
+ /Color\.fromARGB\(\s*\d+\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/g
1027
+ );
1028
+ for (const match of argbMatches) {
1029
+ const r = parseInt(match[1]).toString(16).padStart(2, "0");
1030
+ const g = parseInt(match[2]).toString(16).padStart(2, "0");
1031
+ const b = parseInt(match[3]).toString(16).padStart(2, "0");
1032
+ hexColors.push(`#${r}${g}${b}`);
1033
+ }
1034
+ const primaryMatch = content.match(
1035
+ /primary(?:Swatch|Color)\s*:\s*(?:Colors\.(\w+)|Color\(0[xX]([0-9a-fA-F]{8})\))/
1036
+ );
1037
+ if (primaryMatch) {
1038
+ if (primaryMatch[2]) {
1039
+ return {
1040
+ primary: `#${primaryMatch[2].substring(2)}`,
1041
+ secondary: hexColors.length > 1 ? hexColors[1] : null
1042
+ };
1043
+ }
1044
+ }
1045
+ }
1046
+ return {
1047
+ primary: hexColors.length > 0 ? hexColors[0] : null,
1048
+ secondary: hexColors.length > 1 ? hexColors[1] : null
1049
+ };
1050
+ }
1051
+ function extractJSColors(rootDir) {
1052
+ const themeFileNames = [
1053
+ "theme.ts",
1054
+ "theme.js",
1055
+ "colors.ts",
1056
+ "colors.js",
1057
+ "theme.tsx",
1058
+ "constants.ts",
1059
+ "Colors.ts",
1060
+ "Colors.js"
1061
+ ];
1062
+ const hexColors = [];
1063
+ for (const fileName of themeFileNames) {
1064
+ const files = findFiles(rootDir, [], 5).filter(
1065
+ (f) => f.endsWith(fileName)
1066
+ );
1067
+ for (const file of files) {
1068
+ const content = readFileSafe(file);
1069
+ if (!content) continue;
1070
+ const matches = content.matchAll(
1071
+ /['"]#([0-9a-fA-F]{6})['"]/g
1072
+ );
1073
+ for (const match of matches) {
1074
+ hexColors.push(`#${match[1]}`);
1075
+ }
1076
+ const primaryMatch = content.match(
1077
+ /primary\s*[:=]\s*['"]#([0-9a-fA-F]{6})['"]/
1078
+ );
1079
+ if (primaryMatch) {
1080
+ const secondaryMatch = content.match(
1081
+ /secondary\s*[:=]\s*['"]#([0-9a-fA-F]{6})['"]/
1082
+ );
1083
+ return {
1084
+ primary: `#${primaryMatch[1]}`,
1085
+ secondary: secondaryMatch ? `#${secondaryMatch[1]}` : null
1086
+ };
1087
+ }
1088
+ }
1089
+ }
1090
+ return {
1091
+ primary: hexColors.length > 0 ? hexColors[0] : null,
1092
+ secondary: hexColors.length > 1 ? hexColors[1] : null
1093
+ };
1094
+ }
1095
+ function extractSwiftColors(rootDir) {
1096
+ const hexColors = [];
1097
+ const assetsCatalogs = findAssetsCatalogs(rootDir);
1098
+ for (const catalog of assetsCatalogs) {
1099
+ const accentColor = parseColorSetInCatalog(catalog, "AccentColor");
1100
+ if (accentColor) {
1101
+ hexColors.push(accentColor);
1102
+ break;
1103
+ }
1104
+ }
1105
+ for (const catalog of assetsCatalogs) {
1106
+ try {
1107
+ const entries = readdirSync4(catalog);
1108
+ for (const entry of entries) {
1109
+ if (entry.endsWith(".colorset") && entry !== "AccentColor.colorset") {
1110
+ const color = parseColorSet(join5(catalog, entry));
1111
+ if (color) hexColors.push(color);
1112
+ }
1113
+ }
1114
+ } catch {
1115
+ }
1116
+ if (hexColors.length >= 2) break;
1117
+ }
1118
+ const swiftFiles = findFiles(rootDir, [".swift"], 6);
1119
+ for (const file of swiftFiles.slice(0, 50)) {
1120
+ const content = readFileSafe(file);
1121
+ if (!content) continue;
1122
+ const hexMatches = content.matchAll(
1123
+ /(?:UIColor|Color)\s*\(\s*(?:hex|hexString)\s*:\s*(?:0[xX]|"#?)([0-9a-fA-F]{6})/g
1124
+ );
1125
+ for (const match of hexMatches) {
1126
+ hexColors.push(`#${match[1]}`);
1127
+ }
1128
+ const colorLiteral = content.matchAll(
1129
+ /#colorLiteral\s*\(\s*red:\s*([0-9.]+)\s*,\s*green:\s*([0-9.]+)\s*,\s*blue:\s*([0-9.]+)/g
1130
+ );
1131
+ for (const match of colorLiteral) {
1132
+ const r = Math.round(parseFloat(match[1]) * 255).toString(16).padStart(2, "0");
1133
+ const g = Math.round(parseFloat(match[2]) * 255).toString(16).padStart(2, "0");
1134
+ const b = Math.round(parseFloat(match[3]) * 255).toString(16).padStart(2, "0");
1135
+ hexColors.push(`#${r}${g}${b}`);
1136
+ }
1137
+ const namedColorMatches = content.matchAll(/Color\s*\(\s*"([^"]+)"\s*\)/g);
1138
+ for (const match of namedColorMatches) {
1139
+ for (const catalog of assetsCatalogs) {
1140
+ const color = parseColorSetInCatalog(catalog, match[1]);
1141
+ if (color) {
1142
+ hexColors.push(color);
1143
+ break;
1144
+ }
1145
+ }
1146
+ }
1147
+ }
1148
+ return {
1149
+ primary: hexColors.length > 0 ? hexColors[0] : null,
1150
+ secondary: hexColors.length > 1 ? hexColors[1] : null
1151
+ };
1152
+ }
1153
+ function extractAndroidColors(rootDir) {
1154
+ const hexColors = [];
1155
+ const colorFiles = ["colors.xml", "themes.xml", "styles.xml"];
1156
+ for (const fileName of colorFiles) {
1157
+ for (const resDir of ["app/src/main/res/values", "src/main/res/values"]) {
1158
+ const filePath = join5(rootDir, resDir, fileName);
1159
+ const content = readFileSafe(filePath);
1160
+ if (!content) continue;
1161
+ const colorMatches = content.matchAll(
1162
+ /<color\s+name="([^"]*)">\s*#([0-9a-fA-F]{6,8})\s*<\/color>/g
1163
+ );
1164
+ for (const match of colorMatches) {
1165
+ const name = match[1].toLowerCase();
1166
+ let hex = match[2];
1167
+ if (hex.length === 8) hex = hex.substring(2);
1168
+ const color = `#${hex}`;
1169
+ if (name.includes("primary") && !name.includes("variant") && !name.includes("dark")) {
1170
+ hexColors.unshift(color);
1171
+ } else if (name.includes("secondary") || name.includes("accent")) {
1172
+ hexColors.push(color);
1173
+ } else {
1174
+ hexColors.push(color);
1175
+ }
1176
+ }
1177
+ const themeColorMatches = content.matchAll(
1178
+ /<item\s+name="(?:color|android:color)([^"]*)">\s*@color\/([^<]+)\s*<\/item>/g
1179
+ );
1180
+ for (const match of themeColorMatches) {
1181
+ if (match[1].toLowerCase().includes("primary")) {
1182
+ }
1183
+ }
1184
+ }
1185
+ }
1186
+ const ktFiles = findFiles(rootDir, [".kt"], 6);
1187
+ for (const file of ktFiles.slice(0, 50)) {
1188
+ const fileName = file.toLowerCase();
1189
+ if (!fileName.includes("color") && !fileName.includes("theme")) continue;
1190
+ const content = readFileSafe(file);
1191
+ if (!content) continue;
1192
+ const colorMatches = content.matchAll(/Color\s*\(\s*0[xX]([0-9a-fA-F]{8})\s*\)/g);
1193
+ for (const match of colorMatches) {
1194
+ hexColors.push(`#${match[1].substring(2)}`);
1195
+ }
1196
+ }
1197
+ return {
1198
+ primary: hexColors.length > 0 ? hexColors[0] : null,
1199
+ secondary: hexColors.length > 1 ? hexColors[1] : null
1200
+ };
1201
+ }
1202
+ function findAssetsCatalogs(rootDir) {
1203
+ const catalogs = [];
1204
+ function search(dir, depth) {
1205
+ if (depth > 5) return;
1206
+ try {
1207
+ const entries = readdirSync4(dir);
1208
+ for (const entry of entries) {
1209
+ if (entry.startsWith(".") || entry === "Pods" || entry === "DerivedData") continue;
1210
+ const fullPath = join5(dir, entry);
1211
+ if (entry === "Assets.xcassets") {
1212
+ catalogs.push(fullPath);
1213
+ continue;
1214
+ }
1215
+ try {
1216
+ if (statSync2(fullPath).isDirectory()) {
1217
+ search(fullPath, depth + 1);
1218
+ }
1219
+ } catch {
1220
+ }
1221
+ }
1222
+ } catch {
1223
+ }
1224
+ }
1225
+ search(rootDir, 0);
1226
+ return catalogs;
1227
+ }
1228
+ function parseColorSetInCatalog(catalogPath, colorName) {
1229
+ return parseColorSet(join5(catalogPath, `${colorName}.colorset`));
1230
+ }
1231
+ function parseColorSet(colorSetPath) {
1232
+ const contentsPath = join5(colorSetPath, "Contents.json");
1233
+ const content = readFileSafe(contentsPath);
1234
+ if (!content) return null;
1235
+ try {
1236
+ const data = JSON.parse(content);
1237
+ const colors = data.colors;
1238
+ if (!colors?.length) return null;
1239
+ const universalEntry = colors.find((c) => !c.appearances) || colors[0];
1240
+ const components = universalEntry?.color?.components;
1241
+ if (!components) return null;
1242
+ const parseComponent = (val) => {
1243
+ if (val.startsWith("0x") || val.startsWith("0X")) {
1244
+ return parseInt(val, 16);
1245
+ }
1246
+ const num = parseFloat(val);
1247
+ return num > 1 ? Math.round(num) : Math.round(num * 255);
1248
+ };
1249
+ const r = parseComponent(components.red).toString(16).padStart(2, "0");
1250
+ const g = parseComponent(components.green).toString(16).padStart(2, "0");
1251
+ const b = parseComponent(components.blue).toString(16).padStart(2, "0");
1252
+ return `#${r}${g}${b}`;
1253
+ } catch {
1254
+ }
1255
+ return null;
1256
+ }
1257
+ function findAppIcon(rootDir, techStack) {
1258
+ const iconPaths = [];
1259
+ switch (techStack) {
1260
+ case "flutter":
1261
+ iconPaths.push(
1262
+ // Flutter launcher icons plugin
1263
+ join5(rootDir, "assets/icon/icon.png"),
1264
+ join5(rootDir, "assets/icon.png"),
1265
+ join5(rootDir, "assets/images/icon.png"),
1266
+ join5(rootDir, "assets/launcher_icon.png"),
1267
+ ...findIconsInAppIconSet(join5(rootDir, "ios/Runner/Assets.xcassets/AppIcon.appiconset")),
1268
+ // Android
1269
+ join5(rootDir, "android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png"),
1270
+ join5(rootDir, "android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png")
1271
+ );
1272
+ break;
1273
+ case "react-native":
1274
+ case "capacitor":
1275
+ iconPaths.push(
1276
+ join5(rootDir, "assets/icon.png"),
1277
+ join5(rootDir, "src/assets/icon.png"),
1278
+ join5(rootDir, "assets/images/icon.png"),
1279
+ ...findIconsInAppIconSet(join5(rootDir, "ios")),
1280
+ join5(rootDir, "android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png")
1281
+ );
1282
+ break;
1283
+ case "swift":
1284
+ iconPaths.push(
1285
+ ...findIconsInAppIconSet(rootDir)
1286
+ );
1287
+ break;
1288
+ case "kotlin":
1289
+ case "java":
1290
+ iconPaths.push(
1291
+ join5(rootDir, "app/src/main/res/mipmap-xxxhdpi/ic_launcher.png"),
1292
+ join5(rootDir, "app/src/main/res/mipmap-xxhdpi/ic_launcher.png"),
1293
+ join5(rootDir, "app/src/main/res/mipmap-xhdpi/ic_launcher.png"),
1294
+ join5(rootDir, "app/src/main/ic_launcher-playstore.png")
1295
+ );
1296
+ break;
1297
+ }
1298
+ for (const iconPath of iconPaths) {
1299
+ if (existsSync3(iconPath)) {
1300
+ try {
1301
+ const stat = statSync2(iconPath);
1302
+ if (stat.size > 5 * 1024 * 1024) continue;
1303
+ if (stat.size < 100) continue;
1304
+ const buffer = readFileSync2(iconPath);
1305
+ const ext = extname(iconPath).toLowerCase();
1306
+ const mime = ext === ".png" ? "image/png" : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
1307
+ const base64 = `data:${mime};base64,${buffer.toString("base64")}`;
1308
+ return { base64, path: iconPath };
1309
+ } catch {
1310
+ continue;
1311
+ }
1312
+ }
1313
+ }
1314
+ return { base64: null, path: null };
1315
+ }
1316
+ function findIconsInAppIconSet(searchDir) {
1317
+ const icons = [];
1318
+ function searchRecursive(dir, depth) {
1319
+ if (depth > 6) return;
1320
+ try {
1321
+ const entries = readdirSync4(dir);
1322
+ for (const entry of entries) {
1323
+ const fullPath = join5(dir, entry);
1324
+ if (entry === "AppIcon.appiconset") {
1325
+ const contentsPath = join5(fullPath, "Contents.json");
1326
+ const content = readFileSafe(contentsPath);
1327
+ if (content) {
1328
+ try {
1329
+ const data = JSON.parse(content);
1330
+ const images = data.images;
1331
+ const sorted = images.filter((img) => img.filename).sort((a, b) => {
1332
+ const sizeA = parseInt(a.size?.split("x")[0] || "0") * parseInt(a.scale?.replace("x", "") || "1");
1333
+ const sizeB = parseInt(b.size?.split("x")[0] || "0") * parseInt(b.scale?.replace("x", "") || "1");
1334
+ return sizeB - sizeA;
1335
+ });
1336
+ for (const img of sorted) {
1337
+ if (img.filename) {
1338
+ icons.push(join5(fullPath, img.filename));
1339
+ }
1340
+ }
1341
+ } catch {
1342
+ }
1343
+ }
1344
+ try {
1345
+ const iconEntries = readdirSync4(fullPath);
1346
+ for (const ie of iconEntries) {
1347
+ if (ie.endsWith(".png")) {
1348
+ icons.push(join5(fullPath, ie));
1349
+ }
1350
+ }
1351
+ } catch {
1352
+ }
1353
+ return;
1354
+ }
1355
+ try {
1356
+ const stat = statSync2(fullPath);
1357
+ if (stat.isDirectory()) {
1358
+ searchRecursive(fullPath, depth + 1);
1359
+ }
1360
+ } catch {
1361
+ }
1362
+ }
1363
+ } catch {
1364
+ }
1365
+ }
1366
+ if (existsSync3(searchDir)) {
1367
+ searchRecursive(searchDir, 0);
1368
+ }
1369
+ return icons;
1370
+ }
1371
+
1372
+ // 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";
1375
+ var README_NAMES = [
1376
+ "README.md",
1377
+ "readme.md",
1378
+ "Readme.md",
1379
+ "README.txt",
1380
+ "README",
1381
+ "README.rst"
1382
+ ];
1383
+ var TREE_IGNORE = /* @__PURE__ */ new Set([
1384
+ "node_modules",
1385
+ ".git",
1386
+ ".svn",
1387
+ ".hg",
1388
+ "build",
1389
+ "dist",
1390
+ "Pods",
1391
+ ".dart_tool",
1392
+ ".gradle",
1393
+ ".idea",
1394
+ ".vscode",
1395
+ "__pycache__",
1396
+ ".next",
1397
+ ".nuxt",
1398
+ "DerivedData",
1399
+ ".build",
1400
+ ".swiftpm",
1401
+ "vendor",
1402
+ ".pub-cache",
1403
+ "coverage",
1404
+ ".cache",
1405
+ ".pub",
1406
+ "windows",
1407
+ "linux",
1408
+ "web",
1409
+ "macos"
1410
+ ]);
1411
+ function readReadme(rootDir) {
1412
+ for (const name of README_NAMES) {
1413
+ const content = readFileSafe(join6(rootDir, name));
1414
+ if (content && content.trim().length > 0) {
1415
+ return content.substring(0, 1e4);
1416
+ }
1417
+ }
1418
+ return null;
1419
+ }
1420
+ function readSourceCode(rootDir, techStack, maxTotalChars = 5e4) {
1421
+ const extensions = getExtensionsForStack(techStack);
1422
+ const priorityPatterns = getPriorityPatternsForStack(techStack);
1423
+ const allFiles = scanDirectory(rootDir, {
1424
+ extensions,
1425
+ maxDepth: 6,
1426
+ maxFiles: 200
1427
+ });
1428
+ const sorted = sortByPriority(allFiles, priorityPatterns);
1429
+ const parts = [];
1430
+ let totalChars = 0;
1431
+ for (const file of sorted) {
1432
+ if (totalChars >= maxTotalChars) break;
1433
+ const content = readFileSafe(join6(rootDir, file));
1434
+ if (!content || content.trim().length < 20) continue;
1435
+ if (isTestFile(file)) continue;
1436
+ if (isGeneratedFile(file, content)) continue;
1437
+ const truncated = content.substring(0, 8e3);
1438
+ parts.push(`--- ${file} ---
1439
+ ${truncated}`);
1440
+ totalChars += truncated.length;
1441
+ }
1442
+ return parts.join("\n\n");
1443
+ }
1444
+ function getExtensionsForStack(techStack) {
1445
+ switch (techStack) {
1446
+ case "flutter":
1447
+ return [".dart"];
1448
+ case "swift":
1449
+ return [".swift"];
1450
+ case "kotlin":
1451
+ return [".kt", ".kts"];
1452
+ case "java":
1453
+ return [".java"];
1454
+ case "react-native":
1455
+ case "capacitor":
1456
+ return [".ts", ".tsx", ".js", ".jsx"];
1457
+ case "dotnet-maui":
1458
+ return [".cs", ".xaml"];
1459
+ default:
1460
+ return [".ts", ".js", ".swift", ".dart", ".kt"];
1461
+ }
1462
+ }
1463
+ function getPriorityPatternsForStack(techStack) {
1464
+ const common = [
1465
+ "main",
1466
+ "app",
1467
+ "index",
1468
+ "home",
1469
+ "root",
1470
+ "navigation",
1471
+ "router",
1472
+ "config",
1473
+ "theme",
1474
+ "constants",
1475
+ "model",
1476
+ "service"
1477
+ ];
1478
+ switch (techStack) {
1479
+ case "flutter":
1480
+ return [...common, "widget", "screen", "page", "bloc", "provider", "controller"];
1481
+ case "swift":
1482
+ return [...common, "view", "controller", "manager", "delegate", "contentview"];
1483
+ case "kotlin":
1484
+ case "java":
1485
+ return [...common, "activity", "fragment", "viewmodel", "repository"];
1486
+ case "react-native":
1487
+ case "capacitor":
1488
+ return [...common, "screen", "component", "hook", "context", "store", "slice"];
1489
+ default:
1490
+ return common;
1491
+ }
1492
+ }
1493
+ function sortByPriority(files, patterns) {
1494
+ return [...files].sort((a, b) => {
1495
+ const aLower = a.toLowerCase();
1496
+ const bLower = b.toLowerCase();
1497
+ const aScore = patterns.reduce(
1498
+ (score, pattern) => aLower.includes(pattern) ? score + 1 : score,
1499
+ 0
1500
+ );
1501
+ const bScore = patterns.reduce(
1502
+ (score, pattern) => bLower.includes(pattern) ? score + 1 : score,
1503
+ 0
1504
+ );
1505
+ if (aScore !== bScore) return bScore - aScore;
1506
+ return a.length - b.length;
1507
+ });
1508
+ }
1509
+ function isTestFile(file) {
1510
+ const lower = file.toLowerCase();
1511
+ return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__");
1512
+ }
1513
+ function isGeneratedFile(file, content) {
1514
+ const lower = file.toLowerCase();
1515
+ if (lower.includes(".g.dart") || lower.includes(".freezed.dart") || lower.includes(".gen.") || lower.includes("generated")) {
1516
+ return true;
1517
+ }
1518
+ const firstLine = content.split("\n")[0] || "";
1519
+ return firstLine.includes("GENERATED") || firstLine.includes("DO NOT EDIT") || firstLine.includes("AUTO-GENERATED");
1520
+ }
1521
+ function generateProjectTree(rootDir, maxDepth = 5, maxEntries = 300) {
1522
+ const lines = [];
1523
+ let entryCount = 0;
1524
+ function walk(dir, prefix, depth) {
1525
+ if (depth > maxDepth || entryCount >= maxEntries) return;
1526
+ let entries;
1527
+ try {
1528
+ entries = readdirSync5(dir).sort((a, b) => {
1529
+ const aIsDir = isDir(join6(dir, a));
1530
+ const bIsDir = isDir(join6(dir, b));
1531
+ if (aIsDir && !bIsDir) return -1;
1532
+ if (!aIsDir && bIsDir) return 1;
1533
+ return a.localeCompare(b);
1534
+ });
1535
+ } catch {
1536
+ return;
1537
+ }
1538
+ entries = entries.filter((e) => {
1539
+ if (e.startsWith(".")) return false;
1540
+ if (TREE_IGNORE.has(e)) return false;
1541
+ return true;
1542
+ });
1543
+ for (let i = 0; i < entries.length; i++) {
1544
+ if (entryCount >= maxEntries) {
1545
+ lines.push(`${prefix}... (truncated)`);
1546
+ return;
1547
+ }
1548
+ const entry = entries[i];
1549
+ const fullPath = join6(dir, entry);
1550
+ const isLast = i === entries.length - 1;
1551
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
1552
+ const childPrefix = isLast ? " " : "\u2502 ";
1553
+ if (isDir(fullPath)) {
1554
+ lines.push(`${prefix}${connector}${entry}/`);
1555
+ entryCount++;
1556
+ walk(fullPath, prefix + childPrefix, depth + 1);
1557
+ } else {
1558
+ lines.push(`${prefix}${connector}${entry}`);
1559
+ entryCount++;
1560
+ }
1561
+ }
1562
+ }
1563
+ function isDir(path) {
1564
+ try {
1565
+ return statSync3(path).isDirectory();
1566
+ } catch {
1567
+ return false;
1568
+ }
1569
+ }
1570
+ const rootName = relative2(join6(rootDir, ".."), rootDir) || "project";
1571
+ lines.push(`${rootName}/`);
1572
+ walk(rootDir, "", 0);
1573
+ return lines.join("\n");
1574
+ }
1575
+
1576
+ // 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";
1579
+ var MAX_ASSET_SIZE = 5 * 1024 * 1024;
1580
+ var MAX_TOTAL_ASSETS = 10;
1581
+ var MAX_SCREENSHOTS = 5;
1582
+ var MIN_ASSET_SIZE = 500;
1583
+ var MIN_SCREENSHOT_SIZE = 1e4;
1584
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp"]);
1585
+ function getMimeType(filePath) {
1586
+ const ext = extname2(filePath).toLowerCase();
1587
+ if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
1588
+ if (ext === ".webp") return "image/webp";
1589
+ return "image/png";
1590
+ }
1591
+ function isImageFile(filePath) {
1592
+ return IMAGE_EXTENSIONS.has(extname2(filePath).toLowerCase());
1593
+ }
1594
+ function tryReadImage(rootDir, filePath, assetType, minSize = MIN_ASSET_SIZE) {
1595
+ try {
1596
+ if (!existsSync4(filePath)) return null;
1597
+ const stat = statSync4(filePath);
1598
+ if (!stat.isFile()) return null;
1599
+ if (stat.size < minSize) return null;
1600
+ if (stat.size > MAX_ASSET_SIZE) return null;
1601
+ const buffer = readFileSync3(filePath);
1602
+ return {
1603
+ asset_type: assetType,
1604
+ file_name: basename(filePath),
1605
+ mime_type: getMimeType(filePath),
1606
+ base64_data: buffer.toString("base64"),
1607
+ width: null,
1608
+ height: null,
1609
+ source_path: relative3(rootDir, filePath)
1610
+ };
1611
+ } catch {
1612
+ return null;
1613
+ }
1614
+ }
1615
+ function collectImagesRecursive(rootDir, dirPath, assetType, assets, maxCount, maxDepth = 3, currentDepth = 0, minSize = MIN_ASSET_SIZE) {
1616
+ if (currentDepth > maxDepth) return;
1617
+ if (!existsSync4(dirPath)) return;
1618
+ try {
1619
+ const entries = readdirSync6(dirPath);
1620
+ for (const entry of entries) {
1621
+ if (assets.filter((a) => a.asset_type === assetType).length >= maxCount) return;
1622
+ if (assets.length >= MAX_TOTAL_ASSETS) return;
1623
+ const fullPath = join7(dirPath, entry);
1624
+ if (isImageFile(entry)) {
1625
+ const asset = tryReadImage(rootDir, fullPath, assetType, minSize);
1626
+ if (asset) assets.push(asset);
1627
+ }
1628
+ }
1629
+ for (const entry of entries) {
1630
+ if (assets.filter((a) => a.asset_type === assetType).length >= maxCount) return;
1631
+ const fullPath = join7(dirPath, entry);
1632
+ try {
1633
+ if (statSync4(fullPath).isDirectory() && !entry.startsWith(".")) {
1634
+ collectImagesRecursive(rootDir, fullPath, assetType, assets, maxCount, maxDepth, currentDepth + 1, minSize);
1635
+ }
1636
+ } catch {
1637
+ }
1638
+ }
1639
+ } catch {
1640
+ }
1641
+ }
1642
+ function getScreenshotDirs(rootDir, techStack) {
1643
+ const dirs = [];
1644
+ const ss = (p) => ({ path: p, minSize: MIN_ASSET_SIZE });
1645
+ const gen = (p) => ({ path: p, minSize: MIN_SCREENSHOT_SIZE });
1646
+ dirs.push(
1647
+ ss(join7(rootDir, "screenshots")),
1648
+ ss(join7(rootDir, "Screenshots")),
1649
+ ss(join7(rootDir, "assets/screenshots")),
1650
+ ss(join7(rootDir, "assets/images/screenshots"))
1651
+ );
1652
+ dirs.push(
1653
+ ss(join7(rootDir, "fastlane/screenshots")),
1654
+ ss(join7(rootDir, "fastlane/metadata/en-US/images/phoneScreenshots")),
1655
+ ss(join7(rootDir, "fastlane/metadata/en-US/images/tabletScreenshots"))
1656
+ );
1657
+ switch (techStack) {
1658
+ case "flutter":
1659
+ dirs.push(
1660
+ ss(join7(rootDir, "assets/screenshots")),
1661
+ ss(join7(rootDir, "metadata/screenshots")),
1662
+ // General Flutter asset directories — use higher threshold
1663
+ gen(join7(rootDir, "assets/images")),
1664
+ gen(join7(rootDir, "assets/img")),
1665
+ gen(join7(rootDir, "assets"))
1666
+ );
1667
+ break;
1668
+ case "react-native":
1669
+ case "capacitor":
1670
+ dirs.push(
1671
+ ss(join7(rootDir, "src/assets/screenshots")),
1672
+ gen(join7(rootDir, "src/assets/images")),
1673
+ gen(join7(rootDir, "src/assets")),
1674
+ ss(join7(rootDir, "docs/screenshots")),
1675
+ gen(join7(rootDir, "assets/images")),
1676
+ gen(join7(rootDir, "assets"))
1677
+ );
1678
+ break;
1679
+ case "swift":
1680
+ dirs.push(
1681
+ ss(join7(rootDir, "fastlane/screenshots/en-US")),
1682
+ ss(join7(rootDir, "fastlane/metadata/en-US/images/phoneScreenshots")),
1683
+ gen(join7(rootDir, "marketing")),
1684
+ gen(join7(rootDir, "Marketing"))
1685
+ );
1686
+ break;
1687
+ case "kotlin":
1688
+ case "java":
1689
+ dirs.push(
1690
+ ss(join7(rootDir, "fastlane/metadata/android/en-US/images/phoneScreenshots")),
1691
+ gen(join7(rootDir, "metadata/android/en-US/images")),
1692
+ gen(join7(rootDir, "marketing")),
1693
+ gen(join7(rootDir, "Marketing"))
1694
+ );
1695
+ break;
1696
+ }
1697
+ return dirs;
1698
+ }
1699
+ function getSplashPaths(rootDir, techStack) {
1700
+ const paths = [];
1701
+ switch (techStack) {
1702
+ case "flutter":
1703
+ paths.push(
1704
+ join7(rootDir, "assets/splash.png"),
1705
+ join7(rootDir, "assets/images/splash.png"),
1706
+ join7(rootDir, "assets/splash/splash.png"),
1707
+ join7(rootDir, "assets/images/splash_screen.png"),
1708
+ // flutter_native_splash output paths
1709
+ join7(rootDir, "android/app/src/main/res/drawable-xxhdpi/android12splash.png"),
1710
+ join7(rootDir, "android/app/src/main/res/drawable-xxxhdpi/android12splash.png"),
1711
+ join7(rootDir, "ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png"),
1712
+ join7(rootDir, "ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png"),
1713
+ join7(rootDir, "ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png")
1714
+ );
1715
+ for (const density of ["xxxhdpi", "xxhdpi", "xhdpi"]) {
1716
+ paths.push(
1717
+ join7(rootDir, `android/app/src/main/res/drawable-${density}/splash.png`),
1718
+ join7(rootDir, `android/app/src/main/res/drawable-${density}/launch_screen.png`)
1719
+ );
1720
+ }
1721
+ break;
1722
+ case "react-native":
1723
+ case "capacitor":
1724
+ paths.push(
1725
+ join7(rootDir, "assets/splash.png"),
1726
+ join7(rootDir, "src/assets/splash.png"),
1727
+ join7(rootDir, "assets/images/splash.png"),
1728
+ join7(rootDir, "resources/splash.png")
1729
+ );
1730
+ break;
1731
+ case "kotlin":
1732
+ case "java":
1733
+ for (const density of ["xxxhdpi", "xxhdpi", "xhdpi"]) {
1734
+ paths.push(
1735
+ join7(rootDir, `app/src/main/res/drawable-${density}/splash.png`),
1736
+ join7(rootDir, `app/src/main/res/drawable-${density}/launch_screen.png`)
1737
+ );
1738
+ }
1739
+ break;
1740
+ }
1741
+ return paths;
1742
+ }
1743
+ function getFeatureGraphicPaths(rootDir, techStack) {
1744
+ const paths = [];
1745
+ paths.push(
1746
+ join7(rootDir, "fastlane/metadata/android/en-US/images/featureGraphic.png"),
1747
+ join7(rootDir, "fastlane/metadata/android/en-US/images/featureGraphic.jpg"),
1748
+ join7(rootDir, "metadata/android/en-US/images/featureGraphic.png")
1749
+ );
1750
+ switch (techStack) {
1751
+ case "flutter":
1752
+ case "react-native":
1753
+ case "capacitor":
1754
+ paths.push(
1755
+ join7(rootDir, "assets/feature_graphic.png"),
1756
+ join7(rootDir, "assets/feature-graphic.png")
1757
+ );
1758
+ break;
1759
+ case "kotlin":
1760
+ case "java":
1761
+ paths.push(
1762
+ join7(rootDir, "app/src/main/feature_graphic.png")
1763
+ );
1764
+ break;
1765
+ }
1766
+ return paths;
1767
+ }
1768
+ function getPromotionalPaths(rootDir, techStack) {
1769
+ const paths = [];
1770
+ paths.push(
1771
+ join7(rootDir, "fastlane/metadata/android/en-US/images/promoGraphic.png"),
1772
+ join7(rootDir, "fastlane/metadata/android/en-US/images/promoGraphic.jpg"),
1773
+ join7(rootDir, "assets/promo.png"),
1774
+ join7(rootDir, "assets/promotional.png")
1775
+ );
1776
+ if (techStack === "swift") {
1777
+ paths.push(
1778
+ join7(rootDir, "fastlane/metadata/en-US/promotional.png")
1779
+ );
1780
+ }
1781
+ return paths;
1782
+ }
1783
+ function scanAppAssets(rootDir, techStack) {
1784
+ const assets = [];
1785
+ const screenshotDirs = getScreenshotDirs(rootDir, techStack);
1786
+ for (const { path: dir, minSize } of screenshotDirs) {
1787
+ if (assets.filter((a) => a.asset_type === "screenshot").length >= MAX_SCREENSHOTS) break;
1788
+ if (assets.length >= MAX_TOTAL_ASSETS) break;
1789
+ collectImagesRecursive(rootDir, dir, "screenshot", assets, MAX_SCREENSHOTS, 3, 0, minSize);
1790
+ }
1791
+ const splashPaths = getSplashPaths(rootDir, techStack);
1792
+ for (const path of splashPaths) {
1793
+ if (assets.length >= MAX_TOTAL_ASSETS) break;
1794
+ if (assets.some((a) => a.asset_type === "splash_screen")) break;
1795
+ const asset = tryReadImage(rootDir, path, "splash_screen");
1796
+ if (asset) {
1797
+ assets.push(asset);
1798
+ break;
1799
+ }
1800
+ }
1801
+ const featurePaths = getFeatureGraphicPaths(rootDir, techStack);
1802
+ for (const path of featurePaths) {
1803
+ if (assets.length >= MAX_TOTAL_ASSETS) break;
1804
+ if (assets.some((a) => a.asset_type === "feature_graphic")) break;
1805
+ const asset = tryReadImage(rootDir, path, "feature_graphic");
1806
+ if (asset) {
1807
+ assets.push(asset);
1808
+ break;
1809
+ }
1810
+ }
1811
+ const promoPaths = getPromotionalPaths(rootDir, techStack);
1812
+ for (const path of promoPaths) {
1813
+ if (assets.length >= MAX_TOTAL_ASSETS) break;
1814
+ if (assets.some((a) => a.asset_type === "promotional_image")) break;
1815
+ const asset = tryReadImage(rootDir, path, "promotional_image");
1816
+ if (asset) {
1817
+ assets.push(asset);
1818
+ break;
1819
+ }
1820
+ }
1821
+ return assets.slice(0, MAX_TOTAL_ASSETS);
1822
+ }
1823
+
1824
+ // src/api/forvibe-client.ts
1825
+ var DEFAULT_API_URL = "https://forvibe.app";
1826
+ var ForvibeClient = class {
1827
+ baseUrl;
1828
+ sessionToken = null;
1829
+ sessionId = null;
1830
+ constructor(baseUrl) {
1831
+ this.baseUrl = (baseUrl || process.env.FORVIBE_API_URL || DEFAULT_API_URL).replace(/\/$/, "");
1832
+ }
1833
+ /**
1834
+ * Validate OTC code and get session token
1835
+ */
1836
+ async validateOTC(code) {
1837
+ const normalizedCode = code.toUpperCase().replace(/[^A-Z0-9]/g, "");
1838
+ const response = await fetch(`${this.baseUrl}/api/agent/validate-otc`, {
1839
+ method: "POST",
1840
+ headers: { "Content-Type": "application/json" },
1841
+ body: JSON.stringify({ code: normalizedCode })
1842
+ });
1843
+ if (!response.ok) {
1844
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
1845
+ if (response.status === 429) {
1846
+ throw new Error("Too many attempts. Please wait a moment and try again.");
1847
+ }
1848
+ if (response.status === 410) {
1849
+ throw new Error("Connection code has expired. Please generate a new one on forvibe.app.");
1850
+ }
1851
+ if (response.status === 404) {
1852
+ throw new Error("Invalid connection code. Please check and try again.");
1853
+ }
1854
+ throw new Error(error.error || `Validation failed (${response.status})`);
1855
+ }
1856
+ const data = await response.json();
1857
+ this.sessionToken = data.session_token;
1858
+ this.sessionId = data.session_id;
1859
+ return data;
1860
+ }
1861
+ /**
1862
+ * Submit the CLI project report
1863
+ */
1864
+ async submitReport(report) {
1865
+ if (!this.sessionToken) {
1866
+ throw new Error("Not connected. Please validate OTC code first.");
1867
+ }
1868
+ const response = await fetch(`${this.baseUrl}/api/agent/report`, {
1869
+ method: "POST",
1870
+ headers: {
1871
+ "Content-Type": "application/json",
1872
+ Authorization: `Bearer ${this.sessionToken}`
1873
+ },
1874
+ body: JSON.stringify({ report })
1875
+ });
1876
+ if (!response.ok) {
1877
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
1878
+ if (response.status === 401) {
1879
+ throw new Error("Session expired. Please generate a new connection code.");
1880
+ }
1881
+ if (response.status === 409) {
1882
+ throw new Error("Report has already been submitted for this session.");
1883
+ }
1884
+ throw new Error(error.error || `Report submission failed (${response.status})`);
1885
+ }
1886
+ return await response.json();
1887
+ }
1888
+ /**
1889
+ * Send raw project data to backend for AI analysis (Gemini proxy)
1890
+ */
1891
+ async analyzeProject(input) {
1892
+ if (!this.sessionToken) {
1893
+ throw new Error("Not connected. Please validate OTC code first.");
1894
+ }
1895
+ const response = await fetch(`${this.baseUrl}/api/agent/analyze`, {
1896
+ method: "POST",
1897
+ headers: {
1898
+ "Content-Type": "application/json",
1899
+ Authorization: `Bearer ${this.sessionToken}`
1900
+ },
1901
+ body: JSON.stringify({
1902
+ tech_stack: {
1903
+ stack: input.techStack.stack,
1904
+ label: input.techStack.label,
1905
+ platforms: input.techStack.platforms,
1906
+ configFiles: input.techStack.configFiles
1907
+ },
1908
+ config: {
1909
+ app_name: input.config.app_name,
1910
+ bundle_id: input.config.bundle_id,
1911
+ version: input.config.version,
1912
+ min_ios_version: input.config.min_ios_version,
1913
+ min_android_sdk: input.config.min_android_sdk,
1914
+ description: input.config.description
1915
+ },
1916
+ sdk_scan: {
1917
+ detected_sdks: input.sdkScan.detected_sdks,
1918
+ data_collected: input.sdkScan.data_collected,
1919
+ advertising_type: input.sdkScan.advertising_type,
1920
+ third_party_services: input.sdkScan.third_party_services,
1921
+ has_iap: input.sdkScan.has_iap
1922
+ },
1923
+ branding: {
1924
+ primary_color: input.branding.primary_color,
1925
+ secondary_color: input.branding.secondary_color,
1926
+ app_icon_base64: input.branding.app_icon_base64
1927
+ },
1928
+ readme_content: input.readmeContent,
1929
+ source_code: input.sourceCode,
1930
+ project_tree: input.projectTree
1931
+ }),
1932
+ signal: AbortSignal.timeout(12e4)
1933
+ });
1934
+ if (!response.ok) {
1935
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
1936
+ if (response.status === 401) {
1937
+ throw new Error("Session expired. Please generate a new connection code.");
1938
+ }
1939
+ throw new Error(error.error || `Analysis failed (${response.status})`);
1940
+ }
1941
+ return await response.json();
1942
+ }
1943
+ };
1944
+
1945
+ // src/commands/analyze.ts
1946
+ function askQuestion(question) {
1947
+ const rl = createInterface({
1948
+ input: process.stdin,
1949
+ output: process.stdout
1950
+ });
1951
+ return new Promise((resolve) => {
1952
+ rl.question(question, (answer) => {
1953
+ rl.close();
1954
+ resolve(answer.trim());
1955
+ });
1956
+ });
1957
+ }
1958
+ async function analyzeCommand(options) {
1959
+ const rootDir = options.dir || process.cwd();
1960
+ console.log();
1961
+ console.log(
1962
+ chalk.bold(" Forvibe CLI") + chalk.gray(" \u2014 AI-powered App Store automation")
1963
+ );
1964
+ console.log();
1965
+ const otcCode = await askQuestion(
1966
+ chalk.cyan(" \u{1F517} Enter your Forvibe connection code: ")
1967
+ );
1968
+ if (!otcCode || otcCode.length < 6) {
1969
+ console.log(
1970
+ chalk.red("\n \u2717 Invalid code. Please enter the 6-character code from forvibe.app\n")
1971
+ );
1972
+ process.exit(1);
1973
+ }
1974
+ const connectSpinner = ora({
1975
+ text: "Connecting to Forvibe...",
1976
+ prefixText: " "
1977
+ }).start();
1978
+ const client = new ForvibeClient(options.apiUrl);
1979
+ try {
1980
+ await client.validateOTC(otcCode);
1981
+ connectSpinner.succeed(chalk.green("Connected to Forvibe!"));
1982
+ } catch (error) {
1983
+ connectSpinner.fail(
1984
+ chalk.red(error instanceof Error ? error.message : "Connection failed")
1985
+ );
1986
+ process.exit(1);
1987
+ }
1988
+ console.log();
1989
+ const techSpinner = ora({
1990
+ text: "Detecting tech stack...",
1991
+ prefixText: " "
1992
+ }).start();
1993
+ const techStack = detectTechStack(rootDir);
1994
+ if (techStack.stack === "unknown") {
1995
+ techSpinner.fail(
1996
+ chalk.red(
1997
+ "No supported project found. Supported: Swift, Flutter, React Native, Kotlin, Capacitor, .NET MAUI"
1998
+ )
1999
+ );
2000
+ process.exit(1);
2001
+ }
2002
+ techSpinner.succeed(
2003
+ `Tech stack: ${chalk.bold(techStack.label)} ${chalk.gray(`(${techStack.platforms.join(", ")})`)}`
2004
+ );
2005
+ const configSpinner = ora({
2006
+ text: "Reading project configuration...",
2007
+ prefixText: " "
2008
+ }).start();
2009
+ const config = parseConfig(rootDir, techStack.stack);
2010
+ const configDetails = [
2011
+ config.bundle_id && `Bundle ID: ${config.bundle_id}`,
2012
+ config.app_name && `Name: ${config.app_name}`,
2013
+ config.version && `v${config.version}`
2014
+ ].filter(Boolean).join(" \xB7 ");
2015
+ configSpinner.succeed(
2016
+ `Config parsed: ${chalk.gray(configDetails || "partial data")}`
2017
+ );
2018
+ const sdkSpinner = ora({
2019
+ text: "Scanning dependencies...",
2020
+ prefixText: " "
2021
+ }).start();
2022
+ const sdkScan = scanSDKs(rootDir, techStack.stack);
2023
+ sdkSpinner.succeed(
2024
+ `Dependencies: ${chalk.bold(String(sdkScan.detected_sdks.length))} known SDKs detected`
2025
+ );
2026
+ const brandingSpinner = ora({
2027
+ text: "Extracting branding...",
2028
+ prefixText: " "
2029
+ }).start();
2030
+ const branding = extractBranding(rootDir, techStack.stack);
2031
+ const brandingDetails = [
2032
+ branding.primary_color && `Primary: ${branding.primary_color}`,
2033
+ branding.app_icon_path && "Icon found"
2034
+ ].filter(Boolean).join(" \xB7 ");
2035
+ brandingSpinner.succeed(
2036
+ `Branding: ${chalk.gray(brandingDetails || "no colors/icon detected")}`
2037
+ );
2038
+ const assetSpinner = ora({
2039
+ text: "Scanning app assets...",
2040
+ prefixText: " "
2041
+ }).start();
2042
+ const appAssets = scanAppAssets(rootDir, techStack.stack);
2043
+ const assetParts = [
2044
+ appAssets.filter((a) => a.asset_type === "screenshot").length > 0 && `${appAssets.filter((a) => a.asset_type === "screenshot").length} screenshots`,
2045
+ appAssets.some((a) => a.asset_type === "splash_screen") && "splash screen",
2046
+ appAssets.some((a) => a.asset_type === "feature_graphic") && "feature graphic",
2047
+ appAssets.some((a) => a.asset_type === "promotional_image") && "promo image"
2048
+ ].filter(Boolean);
2049
+ assetSpinner.succeed(
2050
+ `Assets: ${assetParts.length > 0 ? assetParts.join(", ") : "none found"} ${chalk.gray(`(${appAssets.length} total)`)}`
2051
+ );
2052
+ const sourceSpinner = ora({
2053
+ text: "Reading source code & project structure...",
2054
+ prefixText: " "
2055
+ }).start();
2056
+ const readmeContent = readReadme(rootDir);
2057
+ const sourceCode = readSourceCode(rootDir, techStack.stack);
2058
+ const projectTree = generateProjectTree(rootDir);
2059
+ const sourceLines = sourceCode.split("\n").length;
2060
+ sourceSpinner.succeed(
2061
+ `Source code: ${chalk.bold(String(sourceLines))} lines analyzed ${readmeContent ? chalk.gray("(README found)") : ""}`
2062
+ );
2063
+ console.log();
2064
+ const useLocalAI = options.local && (process.env.GEMINI_API_KEY || process.env.GOOGLE_AI_API_KEY);
2065
+ let report;
2066
+ if (useLocalAI) {
2067
+ console.log(chalk.yellow(" \u26A0 Using local Gemini API (deprecated \u2014 will be removed in a future version)"));
2068
+ const geminiApiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_AI_API_KEY;
2069
+ const aiSpinner = ora({
2070
+ text: "AI is analyzing your project...",
2071
+ prefixText: " "
2072
+ }).start();
2073
+ try {
2074
+ const { generateReport } = await import("./report-generator-NMGCGKPS.js");
2075
+ report = await generateReport(
2076
+ { techStack, config, sdkScan, branding, readmeContent, sourceCode, projectTree },
2077
+ geminiApiKey
2078
+ );
2079
+ if (appAssets.length > 0) {
2080
+ report.app_assets = appAssets;
2081
+ }
2082
+ aiSpinner.succeed(chalk.green("Analysis complete!"));
2083
+ } catch (error) {
2084
+ aiSpinner.fail(
2085
+ chalk.red(`AI analysis failed: ${error instanceof Error ? error.message : "Unknown error"}`)
2086
+ );
2087
+ process.exit(1);
2088
+ }
2089
+ const asoSpinner = ora({
2090
+ text: "Generating ASO-optimized store listing...",
2091
+ prefixText: " "
2092
+ }).start();
2093
+ try {
2094
+ const { generateASOContent } = await import("./aso-generator-AZXT6ZCL.js");
2095
+ const asoContent = await generateASOContent(report, geminiApiKey);
2096
+ report.aso_content = asoContent;
2097
+ asoSpinner.succeed(chalk.green("Store listing content generated!"));
2098
+ } catch (error) {
2099
+ asoSpinner.warn(
2100
+ chalk.yellow(`ASO generation skipped: ${error instanceof Error ? error.message : "Unknown error"}`)
2101
+ );
2102
+ }
2103
+ } else {
2104
+ const aiSpinner = ora({
2105
+ text: "Analyzing your project...",
2106
+ prefixText: " "
2107
+ }).start();
2108
+ try {
2109
+ const result = await client.analyzeProject({
2110
+ techStack,
2111
+ config,
2112
+ sdkScan,
2113
+ branding,
2114
+ readmeContent,
2115
+ sourceCode,
2116
+ projectTree
2117
+ });
2118
+ report = result.report;
2119
+ if (appAssets.length > 0) {
2120
+ report.app_assets = appAssets;
2121
+ }
2122
+ if (result.warnings && result.warnings.length > 0) {
2123
+ aiSpinner.succeed(chalk.green("Analysis complete!"));
2124
+ for (const warning of result.warnings) {
2125
+ console.log(chalk.yellow(` \u26A0 ${warning}`));
2126
+ }
2127
+ } else {
2128
+ aiSpinner.succeed(chalk.green("Analysis complete!"));
2129
+ }
2130
+ } catch (error) {
2131
+ aiSpinner.fail(
2132
+ chalk.red(`Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}`)
2133
+ );
2134
+ process.exit(1);
2135
+ }
2136
+ }
2137
+ console.log();
2138
+ console.log(chalk.bold(" \u{1F4CB} Report Summary"));
2139
+ 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"));
2140
+ console.log(` App Name: ${chalk.white(report.app_name)}`);
2141
+ console.log(` Bundle ID: ${chalk.white(report.bundle_id)}`);
2142
+ console.log(` Type: ${chalk.white(report.app_type)}`);
2143
+ console.log(` Category: ${chalk.white(report.app_category_suggestion)}`);
2144
+ if (report.description) {
2145
+ const descPreview = report.description.length > 200 ? report.description.substring(0, 200).replace(/\n/g, " ") + "..." : report.description.replace(/\n/g, " ");
2146
+ console.log(` Description: ${chalk.white(descPreview)}`);
2147
+ }
2148
+ console.log(` Features: ${chalk.white(report.key_features.slice(0, 3).join(", "))}${report.key_features.length > 3 ? chalk.gray(` +${report.key_features.length - 3} more`) : ""}`);
2149
+ console.log(` Audience: ${chalk.white(report.target_audience.length > 100 ? report.target_audience.substring(0, 100) + "..." : report.target_audience)}`);
2150
+ console.log(` SDKs: ${chalk.white(String(report.detected_sdks.length))} detected`);
2151
+ console.log(` Data: ${chalk.white(report.data_collected.join(", ") || "none")}`);
2152
+ console.log(` Colors: ${chalk.hex(report.primary_color)("\u25A0")} ${report.primary_color} ${report.secondary_color ? `${chalk.hex(report.secondary_color)("\u25A0")} ${report.secondary_color}` : ""}`);
2153
+ console.log(` Icon: ${report.app_icon_base64 ? chalk.green("\u2713 found") : chalk.gray("not found")}`);
2154
+ console.log(` Assets: ${report.app_assets?.length ? chalk.green(`${report.app_assets.length} found`) : chalk.gray("none")}`);
2155
+ 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"));
2156
+ if (report.aso_content) {
2157
+ console.log();
2158
+ console.log(chalk.bold(" \u{1F4F1} App Store Listing Preview"));
2159
+ 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"));
2160
+ const aso = report.aso_content.appstore;
2161
+ console.log(` Title: ${chalk.white(aso.app_name)} ${chalk.gray(`(${aso.app_name.length}/30)`)}`);
2162
+ console.log(` Subtitle: ${chalk.white(aso.subtitle)} ${chalk.gray(`(${aso.subtitle.length}/30)`)}`);
2163
+ console.log(` Keywords: ${chalk.white(aso.keywords)} ${chalk.gray(`(${aso.keywords.length}/100)`)}`);
2164
+ console.log(` Promo: ${chalk.white(aso.promotional_text)} ${chalk.gray(`(${aso.promotional_text.length}/170)`)}`);
2165
+ const descPreview = aso.description.substring(0, 150).replace(/\n/g, " ");
2166
+ console.log(` Description: ${chalk.white(descPreview)}${aso.description.length > 150 ? chalk.gray("...") : ""} ${chalk.gray(`(${aso.description.length}/4000)`)}`);
2167
+ if (report.aso_content.playstore) {
2168
+ console.log();
2169
+ console.log(chalk.bold(" \u{1F916} Play Store Listing Preview"));
2170
+ 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"));
2171
+ const ps = report.aso_content.playstore;
2172
+ console.log(` Title: ${chalk.white(ps.title)} ${chalk.gray(`(${ps.title.length}/30)`)}`);
2173
+ console.log(` Short Desc: ${chalk.white(ps.short_description)} ${chalk.gray(`(${ps.short_description.length}/80)`)}`);
2174
+ const psDescPreview = ps.description.substring(0, 150).replace(/\n/g, " ");
2175
+ console.log(` Description: ${chalk.white(psDescPreview)}${ps.description.length > 150 ? chalk.gray("...") : ""} ${chalk.gray(`(${ps.description.length}/4000)`)}`);
2176
+ }
2177
+ 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"));
2178
+ }
2179
+ console.log();
2180
+ const sendSpinner = ora({
2181
+ text: "Sending report to Forvibe...",
2182
+ prefixText: " "
2183
+ }).start();
2184
+ try {
2185
+ const result = await client.submitReport(report);
2186
+ sendSpinner.succeed(chalk.green("Report sent successfully!"));
2187
+ console.log();
2188
+ console.log(
2189
+ chalk.bold(" \u{1F310} Continue setup at: ") + chalk.cyan.underline(result.web_url)
2190
+ );
2191
+ console.log();
2192
+ } catch (error) {
2193
+ sendSpinner.fail(
2194
+ chalk.red(
2195
+ `Failed to send report: ${error instanceof Error ? error.message : "Unknown error"}`
2196
+ )
2197
+ );
2198
+ process.exit(1);
2199
+ }
2200
+ }
2201
+
2202
+ // src/index.ts
2203
+ var program = new Command();
2204
+ program.name("forvibe").description("Forvibe CLI \u2014 AI-powered App Store automation").version("0.1.0");
2205
+ program.command("analyze", { isDefault: true }).description(
2206
+ "Analyze your project and send the report to Forvibe for automated App Store setup"
2207
+ ).option("-d, --dir <path>", "Project directory to analyze", process.cwd()).option(
2208
+ "--api-url <url>",
2209
+ "Forvibe API URL (for development)",
2210
+ void 0
2211
+ ).option(
2212
+ "--local",
2213
+ "Use local Gemini API key instead of Forvibe backend (requires GEMINI_API_KEY env var)"
2214
+ ).action(analyzeCommand);
2215
+ program.parse();