From 91a42d7a0a8589faf9078330f99c658072cf7365 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Fri, 6 Feb 2026 16:47:03 -0500 Subject: [PATCH] Android --- src/android/android-wizard-agent.ts | 127 ++++++++++++++++++++++++++++ src/android/utils.ts | 65 ++++++++++++++ src/lib/constants.ts | 1 + src/lib/registry.ts | 2 + 4 files changed, 195 insertions(+) create mode 100644 src/android/android-wizard-agent.ts create mode 100644 src/android/utils.ts diff --git a/src/android/android-wizard-agent.ts b/src/android/android-wizard-agent.ts new file mode 100644 index 0000000..7086b6d --- /dev/null +++ b/src/android/android-wizard-agent.ts @@ -0,0 +1,127 @@ +/* Android (Kotlin) wizard using posthog-agent with PostHog MCP */ +import type { WizardOptions } from '../utils/types'; +import type { FrameworkConfig } from '../lib/framework-config'; +import { Integration } from '../lib/constants'; +import fg from 'fast-glob'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { + getKotlinVersion, + getKotlinVersionBucket, + getMinSdkVersion, +} from './utils'; + +type AndroidContext = { + kotlinVersion?: string; +}; + +export const ANDROID_AGENT_CONFIG: FrameworkConfig = { + metadata: { + name: 'Android (Kotlin)', + integration: Integration.android, + beta: true, + docsUrl: 'https://posthog.com/docs/libraries/android', + gatherContext: (options: WizardOptions) => { + const kotlinVersion = getKotlinVersion(options); + return Promise.resolve({ kotlinVersion }); + }, + }, + + detection: { + packageName: 'posthog-android', + packageDisplayName: 'Android (Kotlin)', + usesPackageJson: false, + getVersion: () => undefined, + getVersionBucket: (version: string) => getKotlinVersionBucket(version), + // This is actually pretty high for a minimum, but android apis aren't super stable. + minimumVersion: '21.0.0', + getInstalledVersion: (options: WizardOptions) => getMinSdkVersion(options), + detect: async (options) => { + const { installDir } = options; + + // Strategy 1: Check for build.gradle(.kts) with Android plugin + for (const name of ['build.gradle', 'build.gradle.kts']) { + const buildGradlePath = path.join(installDir, name); + if (fs.existsSync(buildGradlePath)) { + const content = fs.readFileSync(buildGradlePath, 'utf-8'); + if ( + content.includes('com.android.application') || + content.includes('com.android.library') || + content.includes('com.android.tools.build:gradle') + ) { + return true; + } + } + } + + // Strategy 2: Check for AndroidManifest.xml with Kotlin source files + // This could be an issue if we have Flutter in the mix, but we'll figure that out later. + const manifestFiles = await fg('**/AndroidManifest.xml', { + cwd: installDir, + ignore: ['**/build/**', '**/node_modules/**', '**/.gradle/**'], + }); + + if (manifestFiles.length > 0) { + const kotlinFiles = await fg('**/*.kt', { + cwd: installDir, + ignore: ['**/build/**', '**/node_modules/**', '**/.gradle/**'], + }); + if (kotlinFiles.length > 0) { + return true; + } + } + + return false; + }, + }, + + environment: { + uploadToHosting: false, + getEnvVars: (apiKey: string, host: string) => ({ + POSTHOG_API_KEY: apiKey, + POSTHOG_HOST: host, + }), + }, + + analytics: { + getTags: (context) => ({ + ...(context.kotlinVersion + ? { kotlinVersion: getKotlinVersionBucket(context.kotlinVersion) } + : {}), + }), + }, + + prompts: { + projectTypeDetection: + 'This is an Android/Kotlin project. Look for build.gradle or build.gradle.kts files, AndroidManifest.xml, and Kotlin source files (.kt) to confirm.', + packageInstallation: + 'Add the PostHog Android SDK dependency to the app-level build.gradle(.kts) file. Use implementation("com.posthog:posthog-android:"). Check the existing dependency format (Groovy vs Kotlin DSL) and match it.', + getAdditionalContextLines: (context) => { + const lines = [ + `Framework docs ID: android (use posthog://docs/frameworks/android for documentation)`, + ]; + + if (context.kotlinVersion) { + lines.push(`Kotlin version: ${context.kotlinVersion}`); + } + + return lines; + }, + }, + + ui: { + successMessage: 'PostHog integration complete', + estimatedDurationMinutes: 5, + getOutroChanges: () => [ + `Analyzed your Android project structure`, + `Added the PostHog Android SDK dependency`, + `Configured PostHog initialization in your Application class`, + `Added event capture and user identification`, + ], + getOutroNextSteps: () => [ + 'Build and run your app to see PostHog in action', + 'Visit your PostHog dashboard to see incoming events', + 'Check out the PostHog Android docs for advanced features like feature flags and session replay', + ], + }, +}; diff --git a/src/android/utils.ts b/src/android/utils.ts new file mode 100644 index 0000000..c039e2a --- /dev/null +++ b/src/android/utils.ts @@ -0,0 +1,65 @@ +import type { WizardOptions } from '../utils/types'; +import { createVersionBucket } from '../utils/semver'; +import fg from 'fast-glob'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +const IGNORE_PATTERNS = ['**/build/**', '**/.gradle/**', '**/node_modules/**']; + +/** + * Extract minSdk from the app-level build.gradle(.kts). + * Returns the value as a semver-like string (e.g. "24" from minSdk = 24). + */ +export async function getMinSdkVersion( + options: WizardOptions, +): Promise { + const { installDir } = options; + + const buildFiles = await fg(['**/build.gradle', '**/build.gradle.kts'], { + cwd: installDir, + ignore: IGNORE_PATTERNS, + }); + + for (const file of buildFiles) { + try { + const content = fs.readFileSync(path.join(installDir, file), 'utf-8'); + // Match: minSdk = 24, minSdkVersion 21, minSdkVersion = 21 + const match = content.match(/minSdk(?:Version)?\s*=?\s*(\d+)/); + if (match) return match[1]; + } catch { + continue; + } + } + + return undefined; +} + +export const getKotlinVersionBucket = createVersionBucket(); + +/** + * Read the root or app-level build.gradle(.kts) and extract the Kotlin version. + */ +export function getKotlinVersion(options: WizardOptions): string | undefined { + const { installDir } = options; + + for (const name of [ + 'build.gradle', + 'build.gradle.kts', + 'gradle/libs.versions.toml', + ]) { + const filePath = path.join(installDir, name); + if (!fs.existsSync(filePath)) continue; + + const content = fs.readFileSync(filePath, 'utf-8'); + + // build.gradle: kotlinVersion = "2.0.21" or ext.kotlin_version = '1.9.0' + const match = content.match(/kotlin[_-]?[Vv]ersion\s*=\s*["']([^"']+)["']/); + if (match) return match[1]; + + // libs.versions.toml: kotlin = "2.0.21" + const tomlMatch = content.match(/^kotlin\s*=\s*["']([^"']+)["']/m); + if (tomlMatch) return tomlMatch[1]; + } + + return undefined; +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 9773fab..05208df 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -12,6 +12,7 @@ export enum Integration { fastapi = 'fastapi', laravel = 'laravel', swift = 'swift', + android = 'android', // Language fallbacks python = 'python', diff --git a/src/lib/registry.ts b/src/lib/registry.ts index 1bbfeba..bf7eec3 100644 --- a/src/lib/registry.ts +++ b/src/lib/registry.ts @@ -9,6 +9,7 @@ import { FLASK_AGENT_CONFIG } from '../flask/flask-wizard-agent'; import { FASTAPI_AGENT_CONFIG } from '../fastapi/fastapi-wizard-agent'; import { LARAVEL_AGENT_CONFIG } from '../laravel/laravel-wizard-agent'; import { SWIFT_AGENT_CONFIG } from '../swift/swift-wizard-agent'; +import { ANDROID_AGENT_CONFIG } from '../android/android-wizard-agent'; import { PYTHON_AGENT_CONFIG } from '../python/python-wizard-agent'; export const FRAMEWORK_REGISTRY: Record = { @@ -21,5 +22,6 @@ export const FRAMEWORK_REGISTRY: Record = { [Integration.fastapi]: FASTAPI_AGENT_CONFIG, [Integration.laravel]: LARAVEL_AGENT_CONFIG, [Integration.swift]: SWIFT_AGENT_CONFIG, + [Integration.android]: ANDROID_AGENT_CONFIG, [Integration.python]: PYTHON_AGENT_CONFIG, };