@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/aso-generator-AZXT6ZCL.js +144 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2215 -0
- package/dist/report-generator-NMGCGKPS.js +168 -0
- package/package.json +57 -0
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();
|