ai-first-cli 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.es.md +137 -1
  3. package/README.md +136 -4
  4. package/ai/ai_context.md +2 -2
  5. package/ai/architecture.md +3 -3
  6. package/ai/cache.json +85 -57
  7. package/ai/ccp/jira-123/context.json +7 -0
  8. package/ai/context/repo.json +56 -0
  9. package/ai/context/utils.json +7 -0
  10. package/ai/dependencies.json +51 -1026
  11. package/ai/files.json +195 -3
  12. package/ai/git/commit-activity.json +8646 -0
  13. package/ai/git/recent-features.json +1 -0
  14. package/ai/git/recent-files.json +52 -0
  15. package/ai/git/recent-flows.json +1 -0
  16. package/ai/graph/knowledge-graph.json +43643 -0
  17. package/ai/graph/module-graph.json +4 -0
  18. package/ai/graph/symbol-graph.json +3307 -879
  19. package/ai/graph/symbol-references.json +119 -32
  20. package/ai/index-state.json +843 -188
  21. package/ai/index.db +0 -0
  22. package/ai/modules.json +4 -0
  23. package/ai/repo-map.json +81 -17
  24. package/ai/repo_map.json +81 -17
  25. package/ai/repo_map.md +21 -7
  26. package/ai/summary.md +5 -5
  27. package/ai/symbols.json +1 -20287
  28. package/dist/analyzers/androidResources.d.ts +23 -0
  29. package/dist/analyzers/androidResources.d.ts.map +1 -0
  30. package/dist/analyzers/androidResources.js +93 -0
  31. package/dist/analyzers/androidResources.js.map +1 -0
  32. package/dist/analyzers/dependencies.d.ts.map +1 -1
  33. package/dist/analyzers/dependencies.js +37 -0
  34. package/dist/analyzers/dependencies.js.map +1 -1
  35. package/dist/analyzers/entrypoints.d.ts.map +1 -1
  36. package/dist/analyzers/entrypoints.js +71 -1
  37. package/dist/analyzers/entrypoints.js.map +1 -1
  38. package/dist/analyzers/gradleModules.d.ts +22 -0
  39. package/dist/analyzers/gradleModules.d.ts.map +1 -0
  40. package/dist/analyzers/gradleModules.js +75 -0
  41. package/dist/analyzers/gradleModules.js.map +1 -0
  42. package/dist/analyzers/techStack.d.ts +7 -0
  43. package/dist/analyzers/techStack.d.ts.map +1 -1
  44. package/dist/analyzers/techStack.js +44 -1
  45. package/dist/analyzers/techStack.js.map +1 -1
  46. package/dist/commands/ai-first.d.ts.map +1 -1
  47. package/dist/commands/ai-first.js +311 -1
  48. package/dist/commands/ai-first.js.map +1 -1
  49. package/dist/core/adapters/adapterRegistry.d.ts +39 -0
  50. package/dist/core/adapters/adapterRegistry.d.ts.map +1 -0
  51. package/dist/core/adapters/adapterRegistry.js +155 -0
  52. package/dist/core/adapters/adapterRegistry.js.map +1 -0
  53. package/dist/core/adapters/baseAdapter.d.ts +49 -0
  54. package/dist/core/adapters/baseAdapter.d.ts.map +1 -0
  55. package/dist/core/adapters/baseAdapter.js +28 -0
  56. package/dist/core/adapters/baseAdapter.js.map +1 -0
  57. package/dist/core/adapters/community/fastapiAdapter.d.ts +7 -0
  58. package/dist/core/adapters/community/fastapiAdapter.d.ts.map +1 -0
  59. package/dist/core/adapters/community/fastapiAdapter.js +40 -0
  60. package/dist/core/adapters/community/fastapiAdapter.js.map +1 -0
  61. package/dist/core/adapters/community/index.d.ts +11 -0
  62. package/dist/core/adapters/community/index.d.ts.map +1 -0
  63. package/dist/core/adapters/community/index.js +11 -0
  64. package/dist/core/adapters/community/index.js.map +1 -0
  65. package/dist/core/adapters/community/laravelAdapter.d.ts +7 -0
  66. package/dist/core/adapters/community/laravelAdapter.d.ts.map +1 -0
  67. package/dist/core/adapters/community/laravelAdapter.js +47 -0
  68. package/dist/core/adapters/community/laravelAdapter.js.map +1 -0
  69. package/dist/core/adapters/community/nestjsAdapter.d.ts +7 -0
  70. package/dist/core/adapters/community/nestjsAdapter.d.ts.map +1 -0
  71. package/dist/core/adapters/community/nestjsAdapter.js +48 -0
  72. package/dist/core/adapters/community/nestjsAdapter.js.map +1 -0
  73. package/dist/core/adapters/community/phoenixAdapter.d.ts +7 -0
  74. package/dist/core/adapters/community/phoenixAdapter.d.ts.map +1 -0
  75. package/dist/core/adapters/community/phoenixAdapter.js +45 -0
  76. package/dist/core/adapters/community/phoenixAdapter.js.map +1 -0
  77. package/dist/core/adapters/community/springBootAdapter.d.ts +7 -0
  78. package/dist/core/adapters/community/springBootAdapter.d.ts.map +1 -0
  79. package/dist/core/adapters/community/springBootAdapter.js +44 -0
  80. package/dist/core/adapters/community/springBootAdapter.js.map +1 -0
  81. package/dist/core/adapters/dotnetAdapter.d.ts +20 -0
  82. package/dist/core/adapters/dotnetAdapter.d.ts.map +1 -0
  83. package/dist/core/adapters/dotnetAdapter.js +86 -0
  84. package/dist/core/adapters/dotnetAdapter.js.map +1 -0
  85. package/dist/core/adapters/index.d.ts +18 -0
  86. package/dist/core/adapters/index.d.ts.map +1 -0
  87. package/dist/core/adapters/index.js +19 -0
  88. package/dist/core/adapters/index.js.map +1 -0
  89. package/dist/core/adapters/javascriptAdapter.d.ts +11 -0
  90. package/dist/core/adapters/javascriptAdapter.d.ts.map +1 -0
  91. package/dist/core/adapters/javascriptAdapter.js +47 -0
  92. package/dist/core/adapters/javascriptAdapter.js.map +1 -0
  93. package/dist/core/adapters/pythonAdapter.d.ts +20 -0
  94. package/dist/core/adapters/pythonAdapter.d.ts.map +1 -0
  95. package/dist/core/adapters/pythonAdapter.js +99 -0
  96. package/dist/core/adapters/pythonAdapter.js.map +1 -0
  97. package/dist/core/adapters/railsAdapter.d.ts +10 -0
  98. package/dist/core/adapters/railsAdapter.d.ts.map +1 -0
  99. package/dist/core/adapters/railsAdapter.js +52 -0
  100. package/dist/core/adapters/railsAdapter.js.map +1 -0
  101. package/dist/core/adapters/salesforceAdapter.d.ts +16 -0
  102. package/dist/core/adapters/salesforceAdapter.d.ts.map +1 -0
  103. package/dist/core/adapters/salesforceAdapter.js +64 -0
  104. package/dist/core/adapters/salesforceAdapter.js.map +1 -0
  105. package/dist/core/adapters/sdk.d.ts +83 -0
  106. package/dist/core/adapters/sdk.d.ts.map +1 -0
  107. package/dist/core/adapters/sdk.js +114 -0
  108. package/dist/core/adapters/sdk.js.map +1 -0
  109. package/dist/core/ccp.d.ts +37 -0
  110. package/dist/core/ccp.d.ts.map +1 -0
  111. package/dist/core/ccp.js +184 -0
  112. package/dist/core/ccp.js.map +1 -0
  113. package/dist/core/gitAnalyzer.d.ts +74 -0
  114. package/dist/core/gitAnalyzer.d.ts.map +1 -0
  115. package/dist/core/gitAnalyzer.js +298 -0
  116. package/dist/core/gitAnalyzer.js.map +1 -0
  117. package/dist/core/incrementalAnalyzer.d.ts +28 -0
  118. package/dist/core/incrementalAnalyzer.d.ts.map +1 -0
  119. package/dist/core/incrementalAnalyzer.js +343 -0
  120. package/dist/core/incrementalAnalyzer.js.map +1 -0
  121. package/dist/core/knowledgeGraphBuilder.d.ts +31 -0
  122. package/dist/core/knowledgeGraphBuilder.d.ts.map +1 -0
  123. package/dist/core/knowledgeGraphBuilder.js +197 -0
  124. package/dist/core/knowledgeGraphBuilder.js.map +1 -0
  125. package/dist/core/lazyAnalyzer.d.ts +57 -0
  126. package/dist/core/lazyAnalyzer.d.ts.map +1 -0
  127. package/dist/core/lazyAnalyzer.js +204 -0
  128. package/dist/core/lazyAnalyzer.js.map +1 -0
  129. package/dist/core/schema.d.ts +57 -0
  130. package/dist/core/schema.d.ts.map +1 -0
  131. package/dist/core/schema.js +131 -0
  132. package/dist/core/schema.js.map +1 -0
  133. package/dist/core/semanticContexts.d.ts +40 -0
  134. package/dist/core/semanticContexts.d.ts.map +1 -0
  135. package/dist/core/semanticContexts.js +454 -0
  136. package/dist/core/semanticContexts.js.map +1 -0
  137. package/docs/es/guide/adapters.md +143 -0
  138. package/docs/es/guide/ai-repository-schema.md +119 -0
  139. package/docs/es/guide/features.md +67 -0
  140. package/docs/es/guide/flows.md +134 -0
  141. package/docs/es/guide/git-intelligence.md +170 -0
  142. package/docs/es/guide/incremental-analysis.md +131 -0
  143. package/docs/es/guide/knowledge-graph.md +135 -0
  144. package/docs/es/guide/lazy-indexing.md +144 -0
  145. package/docs/es/guide/performance.md +125 -0
  146. package/docs/guide/adapters.md +225 -0
  147. package/docs/guide/ai-repository-schema.md +119 -0
  148. package/docs/guide/architecture.md +69 -1
  149. package/docs/guide/flows.md +134 -0
  150. package/docs/guide/git-intelligence.md +170 -0
  151. package/docs/guide/incremental-analysis.md +131 -0
  152. package/docs/guide/knowledge-graph.md +135 -0
  153. package/docs/guide/lazy-indexing.md +144 -0
  154. package/docs/guide/performance.md +125 -0
  155. package/package.json +5 -2
  156. package/src/analyzers/androidResources.ts +113 -0
  157. package/src/analyzers/dependencies.ts +41 -0
  158. package/src/analyzers/entrypoints.ts +80 -1
  159. package/src/analyzers/gradleModules.ts +100 -0
  160. package/src/analyzers/techStack.ts +56 -0
  161. package/src/commands/ai-first.ts +342 -1
  162. package/src/core/adapters/adapterRegistry.ts +187 -0
  163. package/src/core/adapters/baseAdapter.ts +82 -0
  164. package/src/core/adapters/community/fastapiAdapter.ts +50 -0
  165. package/src/core/adapters/community/index.ts +11 -0
  166. package/src/core/adapters/community/laravelAdapter.ts +56 -0
  167. package/src/core/adapters/community/nestjsAdapter.ts +57 -0
  168. package/src/core/adapters/community/phoenixAdapter.ts +54 -0
  169. package/src/core/adapters/community/springBootAdapter.ts +53 -0
  170. package/src/core/adapters/dotnetAdapter.ts +104 -0
  171. package/src/core/adapters/index.ts +24 -0
  172. package/src/core/adapters/javascriptAdapter.ts +56 -0
  173. package/src/core/adapters/pythonAdapter.ts +118 -0
  174. package/src/core/adapters/railsAdapter.ts +65 -0
  175. package/src/core/adapters/salesforceAdapter.ts +76 -0
  176. package/src/core/adapters/sdk.ts +172 -0
  177. package/src/core/ccp.ts +240 -0
  178. package/src/core/gitAnalyzer.ts +391 -0
  179. package/src/core/incrementalAnalyzer.ts +382 -0
  180. package/src/core/knowledgeGraphBuilder.ts +181 -0
  181. package/src/core/lazyAnalyzer.ts +261 -0
  182. package/src/core/schema.ts +157 -0
  183. package/src/core/semanticContexts.ts +575 -0
  184. package/tests/adapters.test.ts +159 -0
  185. package/tests/gitAnalyzer.test.ts +133 -0
  186. package/tests/incrementalAnalyzer.test.ts +83 -0
  187. package/tests/knowledgeGraph.test.ts +146 -0
  188. package/tests/lazyAnalyzer.test.ts +230 -0
  189. package/tests/schema.test.ts +203 -0
  190. package/tests/semanticContexts.test.ts +435 -0
  191. package/ai/context/analyzers.Symbol.json +0 -19
  192. package/ai/context/analyzers.extractSymbols.json +0 -19
@@ -66,6 +66,8 @@ function parseFileForDependencies(file: FileInfo): Dependency[] {
66
66
  deps.push(...parseRustImports(content, file.relativePath));
67
67
  } else if (file.extension === "java" || file.extension === "cs") {
68
68
  deps.push(...parseJavaImports(content, file.relativePath));
69
+ } else if (file.extension === "gradle" || file.extension === "gradle.kts") {
70
+ deps.push(...parseGradleDependencies(content, file.relativePath));
69
71
  }
70
72
  } catch {}
71
73
 
@@ -305,3 +307,42 @@ export function generateDependenciesJson(analysis: DependencyAnalysis): string {
305
307
  };
306
308
  return JSON.stringify(output, null, 2);
307
309
  }
310
+
311
+ /**
312
+ * Parse Gradle dependencies from build.gradle files
313
+ */
314
+ function parseGradleDependencies(content: string, sourceFile: string): Dependency[] {
315
+ const deps: Dependency[] = [];
316
+
317
+ // implementation "com.example:library:1.0"
318
+ const implMatches = content.matchAll(/(?:implementation|api|compile|testImplementation|androidTestImplementation)\s+["\']([@\w.\-]+):([@\w.\-]+):([@\w.\-]+)["\']/g);
319
+ for (const match of implMatches) {
320
+ deps.push({
321
+ source: sourceFile,
322
+ target: `${match[1]}:${match[2]}:${match[3]}`,
323
+ type: "import",
324
+ });
325
+ }
326
+
327
+ // implementation project(":module")
328
+ const projMatches = content.matchAll(/implementation\s+project\s*\(\s*["\']([@\w.\-]+)["\']\s*\)/g);
329
+ for (const match of projMatches) {
330
+ deps.push({
331
+ source: sourceFile,
332
+ target: `project:${match[1]}`,
333
+ type: "import",
334
+ });
335
+ }
336
+
337
+ // include ":module"
338
+ const includeMatches = content.matchAll(/include\s*\(\s*["\']([@\w.\-]+)["\']\s*\)/g);
339
+ for (const match of includeMatches) {
340
+ deps.push({
341
+ source: sourceFile,
342
+ target: `module:${match[1]}`,
343
+ type: "include",
344
+ });
345
+ }
346
+
347
+ return deps;
348
+ }
@@ -1,5 +1,5 @@
1
1
  import { FileInfo } from "../core/repoScanner.js";
2
- import { readJsonFile } from "../utils/fileUtils.js";
2
+ import { readJsonFile, readFile } from "../utils/fileUtils.js";
3
3
  import path from "path";
4
4
 
5
5
  export interface Entrypoint {
@@ -69,6 +69,85 @@ export function discoverEntrypoints(files: FileInfo[], rootDir: string): Entrypo
69
69
  }
70
70
  }
71
71
 
72
+ // Detect Android entrypoints from AndroidManifest.xml
73
+ const androidManifests = files.filter(f => f.name === "AndroidManifest.xml");
74
+ for (const manifest of androidManifests) {
75
+ try {
76
+ const manifestContent = readFile(path.join(rootDir, manifest.relativePath));
77
+ const androidEntrypoints = parseAndroidManifest(manifestContent, manifest.relativePath);
78
+ entrypoints.push(...androidEntrypoints);
79
+ } catch {}
80
+ }
81
+
82
+ return entrypoints;
83
+ }
84
+
85
+ /**
86
+ * Parse AndroidManifest.xml to extract entrypoints
87
+ */
88
+ function parseAndroidManifest(content: string, manifestPath: string): Entrypoint[] {
89
+ const entrypoints: Entrypoint[] = [];
90
+
91
+ // Extract package name
92
+ const packageMatch = content.match(/package="([^"]+)"/);
93
+ const packageName = packageMatch ? packageMatch[1] : "unknown";
94
+
95
+ // Extract activities
96
+ const activityRegex = /<activity[^>]+android:name="([^"]+)"[^>]*>/g;
97
+ let match;
98
+ while ((match = activityRegex.exec(content)) !== null) {
99
+ const activityName = match[1];
100
+ const isMain = content.includes(`android:name="${activityName}"`) &&
101
+ content.includes("android.intent.action.MAIN") &&
102
+ content.includes("android.intent.category.LAUNCHER");
103
+
104
+ entrypoints.push({
105
+ name: activityName.split('.').pop() || activityName,
106
+ path: `${manifestPath}#${activityName}`,
107
+ type: isMain ? "client" : "other",
108
+ description: isMain ? `Main Activity (${packageName})` : `Activity: ${activityName}`,
109
+ command: isMain ? `adb shell am start -n ${packageName}/${activityName}` : undefined,
110
+ });
111
+ }
112
+
113
+ // Extract services
114
+ const serviceRegex = /<service[^>]+android:name="([^"]+)"[^>]*>/g;
115
+ while ((match = serviceRegex.exec(content)) !== null) {
116
+ entrypoints.push({
117
+ name: match[1].split('.').pop() || match[1],
118
+ path: `${manifestPath}#service.${match[1]}`,
119
+ type: "other",
120
+ description: `Service: ${match[1]}`,
121
+ });
122
+ }
123
+
124
+ // Extract receivers
125
+ const receiverRegex = /<receiver[^>]+android:name="([^"]+)"[^>]*>/g;
126
+ while ((match = receiverRegex.exec(content)) !== null) {
127
+ entrypoints.push({
128
+ name: match[1].split('.').pop() || match[1],
129
+ path: `${manifestPath}#receiver.${match[1]}`,
130
+ type: "other",
131
+ description: `BroadcastReceiver: ${match[1]}`,
132
+ });
133
+ }
134
+
135
+ // Extract permissions
136
+ const permissionRegex = /<uses-permission[^>]+android:name="([^"]+)"[^>]*>/g;
137
+ const permissions: string[] = [];
138
+ while ((match = permissionRegex.exec(content)) !== null) {
139
+ permissions.push(match[1]);
140
+ }
141
+
142
+ if (permissions.length > 0) {
143
+ entrypoints.push({
144
+ name: "Permissions",
145
+ path: `${manifestPath}#permissions`,
146
+ type: "config",
147
+ description: `${permissions.length} permissions declared`,
148
+ });
149
+ }
150
+
72
151
  return entrypoints;
73
152
  }
74
153
 
@@ -0,0 +1,100 @@
1
+ import { FileInfo } from "../core/repoScanner.js";
2
+ import { readFile } from "../utils/fileUtils.js";
3
+ import path from "path";
4
+
5
+ export interface GradleModule {
6
+ name: string;
7
+ path: string;
8
+ isIncluded: boolean;
9
+ }
10
+
11
+ export interface GradleModulesAnalysis {
12
+ isGradle: boolean;
13
+ isMultiModule: boolean;
14
+ modules: GradleModule[];
15
+ rootProjectName?: string;
16
+ settingsFile?: string;
17
+ }
18
+
19
+ /**
20
+ * Analyze Gradle settings to detect modules
21
+ */
22
+ export function analyzeGradleModules(files: FileInfo[], rootDir: string): GradleModulesAnalysis {
23
+ const settingsFiles = files.filter(f =>
24
+ f.name === "settings.gradle" || f.name === "settings.gradle.kts"
25
+ );
26
+
27
+ if (settingsFiles.length === 0) {
28
+ return {
29
+ isGradle: false,
30
+ isMultiModule: false,
31
+ modules: [],
32
+ };
33
+ }
34
+
35
+ const modules: GradleModule[] = [];
36
+ let rootProjectName = "app";
37
+ let settingsContent = "";
38
+
39
+ // Use root settings.gradle or settings.gradle.kts
40
+ const settingsFile = settingsFiles.find(f => f.relativePath === "settings.gradle" || f.relativePath === "settings.gradle.kts");
41
+
42
+ if (settingsFile) {
43
+ try {
44
+ settingsContent = readFile(path.join(rootDir, settingsFile.relativePath));
45
+ } catch {}
46
+ }
47
+
48
+ // Parse include statements: include(":module")
49
+ const includeMatches = settingsContent.matchAll(/include\s*\(\s*["']([^"']+)["']\s*\)/g);
50
+ for (const match of includeMatches) {
51
+ const modulePath = match[1];
52
+ modules.push({
53
+ name: modulePath.replace(/^:/, ""),
54
+ path: modulePath,
55
+ isIncluded: true,
56
+ });
57
+ }
58
+
59
+ // Parse project statements: project(":module")
60
+ const projectMatches = settingsContent.matchAll(/project\s*\(\s*["']([^"']+)["']\s*\)\s*\.+/g);
61
+ for (const match of projectMatches) {
62
+ const modulePath = match[1];
63
+ const moduleName = modulePath.replace(/^:/, "");
64
+ if (!modules.find(m => m.name === moduleName)) {
65
+ modules.push({
66
+ name: moduleName,
67
+ path: modulePath,
68
+ isIncluded: true,
69
+ });
70
+ }
71
+ }
72
+
73
+ // Extract root project name
74
+ const rootNameMatch = settingsContent.match(/rootProject\s*\(\s*["']name["']\s*=\s*["']([^"']+)["']/);
75
+ if (rootNameMatch) {
76
+ rootProjectName = rootNameMatch[1];
77
+ }
78
+
79
+ return {
80
+ isGradle: true,
81
+ isMultiModule: modules.length > 0,
82
+ modules,
83
+ rootProjectName,
84
+ settingsFile: settingsFile?.relativePath,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Generate gradle-modules.json content
90
+ */
91
+ export function generateGradleModulesJson(analysis: GradleModulesAnalysis): string {
92
+ const output = {
93
+ isGradle: analysis.isGradle,
94
+ isMultiModule: analysis.isMultiModule,
95
+ rootProjectName: analysis.rootProjectName,
96
+ settingsFile: analysis.settingsFile,
97
+ modules: analysis.modules,
98
+ };
99
+ return JSON.stringify(output, null, 2);
100
+ }
@@ -12,6 +12,13 @@ export interface TechStack {
12
12
  linters: string[];
13
13
  formatters: string[];
14
14
  description: string;
15
+ android?: {
16
+ minSdk?: string;
17
+ targetSdk?: string;
18
+ compileSdk?: string;
19
+ gradleVersion?: string;
20
+ kotlinVersion?: string;
21
+ };
15
22
  }
16
23
 
17
24
  /**
@@ -34,6 +41,8 @@ export function detectTechStack(files: FileInfo[], rootDir: string): TechStack {
34
41
  languages, frameworks, libraries, tools, packageManagers, testing, linters, formatters
35
42
  );
36
43
 
44
+ const android = detectAndroidSDK(files, rootDir);
45
+
37
46
  return {
38
47
  languages,
39
48
  frameworks,
@@ -44,6 +53,7 @@ export function detectTechStack(files: FileInfo[], rootDir: string): TechStack {
44
53
  linters,
45
54
  formatters,
46
55
  description,
56
+ android,
47
57
  };
48
58
  }
49
59
 
@@ -127,6 +137,10 @@ function detectFrameworks(files: FileInfo[], fileNames: Set<string>, rootDir: st
127
137
  "postcss.config": "PostCSS",
128
138
  "babel.config": "Babel",
129
139
  "tsconfig": "TypeScript",
140
+ "build.gradle": "Android",
141
+ "build.gradle.kts": "Android",
142
+ "settings.gradle": "Android",
143
+ "AndroidManifest.xml": "Android",
130
144
  };
131
145
 
132
146
  for (const [indicator, framework] of Object.entries(frameworkIndicators)) {
@@ -303,6 +317,48 @@ function detectFormatters(files: FileInfo[], fileNames: Set<string>): string[] {
303
317
  return formatters;
304
318
  }
305
319
 
320
+ /**
321
+ * Detect Android SDK versions from build.gradle files
322
+ */
323
+ function detectAndroidSDK(files: FileInfo[], rootDir: string): TechStack["android"] {
324
+ const fileNames = files.map(f => f.name);
325
+ const hasAndroid = fileNames.some(n =>
326
+ n === "build.gradle" || n === "build.gradle.kts" ||
327
+ n === "app/build.gradle" || n === "app/build.gradle.kts"
328
+ );
329
+
330
+ if (!hasAndroid) return undefined;
331
+
332
+ const android: TechStack["android"] = {};
333
+
334
+ const gradleFiles = files.filter(f =>
335
+ f.name === "build.gradle" || f.name === "build.gradle.kts" ||
336
+ f.relativePath.includes("app/build.gradle")
337
+ );
338
+
339
+ for (const gf of gradleFiles) {
340
+ try {
341
+ const content = readFile(path.join(rootDir, gf.relativePath));
342
+ const minSdkMatch = content.match(/minSdk(?:Version)?\s*[=:]\s*(\d+)/);
343
+ const targetSdkMatch = content.match(/targetSdk(?:Version)?\s*[=:]\s*(\d+)/);
344
+ const compileSdkMatch = content.match(/compileSdk(?:Version)?\s*[=:]\s*(\d+)/);
345
+
346
+ if (minSdkMatch && !android.minSdk) android.minSdk = minSdkMatch[1];
347
+ if (targetSdkMatch && !android.targetSdk) android.targetSdk = targetSdkMatch[1];
348
+ if (compileSdkMatch && !android.compileSdk) android.compileSdk = compileSdkMatch[1];
349
+ } catch {}
350
+ }
351
+
352
+ try {
353
+ const propsPath = path.join(rootDir, "gradle/wrapper/gradle-wrapper.properties");
354
+ const props = readFile(propsPath);
355
+ const v = props.match(/gradle-(\d+\.\d+)/);
356
+ if (v) android.gradleVersion = v[1];
357
+ } catch {}
358
+
359
+ return (android.minSdk || android.targetSdk || android.compileSdk || android.gradleVersion) ? android : undefined;
360
+ }
361
+
306
362
  /**
307
363
  * Generate tech stack description
308
364
  */