Skip to content

Commit 983dbe0

Browse files
feat: add Kotlin Multiplatform (KMP) framework support (#906)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent da2706e commit 983dbe0

5 files changed

Lines changed: 175 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/* Kotlin Multiplatform (KMP) wizard using posthog-agent with PostHog MCP */
2+
import type { FrameworkConfig } from '../../lib/framework-config';
3+
import { Integration } from '../../lib/constants';
4+
import { gradlePackageManager } from '../../lib/detection/package-manager';
5+
import fg from 'fast-glob';
6+
import * as fs from 'node:fs';
7+
import * as path from 'node:path';
8+
9+
export const KMP_AGENT_CONFIG: FrameworkConfig = {
10+
metadata: {
11+
name: 'Kotlin Multiplatform',
12+
integration: Integration.kmp,
13+
beta: true,
14+
docsUrl: 'https://posthog.com/docs/libraries/kmp',
15+
preRunNotice:
16+
'The PostHog Kotlin Multiplatform SDK is in early access (0.x pre-release). The API may change between minor versions.',
17+
},
18+
19+
detection: {
20+
packageName: 'posthog-kmp',
21+
packageDisplayName: 'Kotlin Multiplatform',
22+
usesPackageJson: false,
23+
getVersion: () => undefined,
24+
detectPackageManager: gradlePackageManager,
25+
// KMP is detected before Android/Swift because a KMP project also looks like
26+
// an Android and/or Swift project. Detection therefore requires the Kotlin
27+
// Multiplatform Gradle plugin or a `commonMain` source set — signals that a
28+
// plain Android or Swift project does not have.
29+
detect: async (options) => {
30+
const { installDir } = options;
31+
32+
// Strategy 1: a Gradle build file that applies the Kotlin Multiplatform plugin.
33+
const buildFiles = await fg(['**/build.gradle', '**/build.gradle.kts'], {
34+
cwd: installDir,
35+
ignore: ['**/build/**', '**/node_modules/**', '**/.gradle/**'],
36+
});
37+
38+
for (const file of buildFiles) {
39+
const content = fs.readFileSync(path.join(installDir, file), 'utf-8');
40+
if (
41+
content.includes('kotlin("multiplatform")') ||
42+
content.includes('org.jetbrains.kotlin.multiplatform') ||
43+
content.includes('kotlin-multiplatform')
44+
) {
45+
return true;
46+
}
47+
}
48+
49+
// Strategy 2: a KMP `commonMain` source set exists.
50+
const commonMain = await fg('**/src/commonMain', {
51+
cwd: installDir,
52+
onlyDirectories: true,
53+
ignore: ['**/build/**', '**/node_modules/**', '**/.gradle/**'],
54+
});
55+
56+
return commonMain.length > 0;
57+
},
58+
},
59+
60+
environment: {
61+
uploadToHosting: false,
62+
getEnvVars: (apiKey: string, host: string) => ({
63+
POSTHOG_PROJECT_TOKEN: apiKey,
64+
POSTHOG_HOST: host,
65+
}),
66+
},
67+
68+
analytics: {
69+
getTags: () => ({}),
70+
},
71+
72+
prompts: {
73+
projectTypeDetection:
74+
'This is a Kotlin Multiplatform (KMP) project. Look for a build.gradle.kts (or build.gradle) that applies the Kotlin Multiplatform plugin (kotlin("multiplatform") or org.jetbrains.kotlin.multiplatform) and a shared module with a src/commonMain source set.',
75+
packageInstallation:
76+
'Add the PostHog KMP SDK to the shared module\'s commonMain dependencies in build.gradle.kts: within the kotlin { sourceSets { ... } } block, add implementation("com.posthog:posthog-kmp:<VERSION>") to commonMain.dependencies. Match the existing dependency format (Groovy vs Kotlin DSL).',
77+
getAdditionalContextLines: () => [
78+
'Framework docs ID: kmp (use posthog://docs/frameworks/kmp for documentation)',
79+
'Initialize once, early in the app lifecycle, from shared code. All PostHog APIs live in the com.posthog.kmp package.',
80+
'Setup call: PostHog.setup(config = PostHogConfig(apiKey = "<POSTHOG_PROJECT_TOKEN>", host = "<POSTHOG_HOST>"), context = PostHogContext())',
81+
'PostHogContext is platform-specific: on Android pass the Application via PostHogContext(application); on iOS and Web use the no-argument PostHogContext().',
82+
'Imports: com.posthog.kmp.PostHog, com.posthog.kmp.PostHogConfig, com.posthog.kmp.PostHogContext.',
83+
],
84+
},
85+
86+
ui: {
87+
successMessage: 'PostHog integration complete',
88+
estimatedDurationMinutes: 5,
89+
getOutroChanges: () => [
90+
'Analyzed your Kotlin Multiplatform project structure',
91+
'Added the PostHog KMP SDK to your shared module',
92+
'Configured PostHog initialization in your shared code',
93+
'Added event capture and user identification',
94+
],
95+
getOutroNextSteps: () => [
96+
'Wire up the platform-specific PostHogContext for each target (Android/iOS/Web)',
97+
'Build and run your app to see PostHog in action',
98+
'Visit your PostHog dashboard to see incoming events',
99+
'Check out the PostHog KMP docs for feature flags, session replay, and more',
100+
],
101+
},
102+
};

src/lib/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const INTEGRATION_ENTRIES = [
5050
{ id: 'integration-elixir' },
5151
{ id: 'integration-go' },
5252
{ id: 'integration-swift', framework: 'swift' },
53+
{ id: 'integration-kmp', framework: 'kmp' },
5354
{ id: 'integration-flutter' },
5455
{ id: 'integration-react-native', framework: 'react-native', default: true },
5556
{ id: 'integration-expo', framework: 'react-native' },

src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export enum Integration {
9696
fastapi = 'fastapi',
9797
laravel = 'laravel',
9898
sveltekit = 'sveltekit',
99+
kmp = 'kmp',
99100
swift = 'swift',
100101
android = 'android',
101102
rails = 'rails',

src/lib/detection/__tests__/framework.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as path from 'node:path';
44
import { detectFramework } from '@lib/detection/framework';
55
import { Integration } from '@lib/constants';
66
import { ANDROID_AGENT_CONFIG } from '../../../frameworks/android/android-wizard-agent';
7+
import { KMP_AGENT_CONFIG } from '../../../frameworks/kmp/kmp-wizard-agent';
78

89
/** A throwaway project dir seeded with the given files. */
910
function makeProject(files: Record<string, string>): string {
@@ -52,6 +53,17 @@ describe('Integration enum order (drives first-match detection)', () => {
5253
// javascriptNode matches any package.json; anything after it is unreachable.
5354
expect(order[order.length - 1]).toBe(Integration.javascriptNode);
5455
});
56+
57+
test('kmp is ordered before android and swift (more specific detection wins)', () => {
58+
// A KMP project also looks like an Android/Swift project, so KMP must be
59+
// checked first for first-match detection to resolve it correctly.
60+
expect(order.indexOf(Integration.kmp)).toBeLessThan(
61+
order.indexOf(Integration.android),
62+
);
63+
expect(order.indexOf(Integration.kmp)).toBeLessThan(
64+
order.indexOf(Integration.swift),
65+
);
66+
});
5567
});
5668

5769
describe('detectFramework (end-to-end over real project dirs)', () => {
@@ -103,6 +115,28 @@ describe('detectFramework (end-to-end over real project dirs)', () => {
103115
);
104116
});
105117

118+
test('a Kotlin Multiplatform project resolves to kmp', async () => {
119+
const opts = project({
120+
'settings.gradle.kts': 'include(":shared")',
121+
'shared/build.gradle.kts': `plugins { kotlin("multiplatform") }\nkotlin {\n sourceSets {\n commonMain.dependencies {}\n }\n}\n`,
122+
'shared/src/commonMain/kotlin/App.kt': 'class App',
123+
});
124+
await expect(detectFramework(opts.installDir)).resolves.toBe(
125+
Integration.kmp,
126+
);
127+
});
128+
129+
test('a plain Android project still resolves to android (kmp ordered ahead does not hijack it)', async () => {
130+
const opts = project({
131+
'build.gradle': `plugins { id 'com.android.application' }`,
132+
'app/src/main/AndroidManifest.xml': '<manifest/>',
133+
'app/src/main/kotlin/MainActivity.kt': 'class MainActivity',
134+
});
135+
await expect(detectFramework(opts.installDir)).resolves.toBe(
136+
Integration.android,
137+
);
138+
});
139+
106140
test('a Flutter project is not claimed by anything', async () => {
107141
const opts = project({
108142
'pubspec.yaml': 'name: my_flutter_app\nenvironment:\n sdk: ^3.0.0\n',
@@ -134,3 +168,38 @@ describe('android detect', () => {
134168
await expect(detect(opts)).resolves.toBe(false);
135169
});
136170
});
171+
172+
describe('kmp detect', () => {
173+
const detect = KMP_AGENT_CONFIG.detection.detect;
174+
175+
test('claims a project applying the Kotlin Multiplatform plugin', async () => {
176+
const opts = project({
177+
'shared/build.gradle.kts': `plugins { kotlin("multiplatform") }`,
178+
});
179+
await expect(detect(opts)).resolves.toBe(true);
180+
});
181+
182+
test('claims a project with a commonMain source set', async () => {
183+
const opts = project({
184+
'shared/src/commonMain/kotlin/App.kt': 'class App',
185+
});
186+
await expect(detect(opts)).resolves.toBe(true);
187+
});
188+
189+
test('does not claim a plain Android project', async () => {
190+
const opts = project({
191+
'build.gradle': `plugins { id 'com.android.application' }`,
192+
'app/src/main/kotlin/MainActivity.kt': 'class MainActivity',
193+
});
194+
await expect(detect(opts)).resolves.toBe(false);
195+
});
196+
197+
test('does not claim a Flutter project', async () => {
198+
const opts = project({
199+
'pubspec.yaml': 'name: my_flutter_app\nenvironment:\n sdk: ^3.0.0\n',
200+
'android/build.gradle': `plugins { id 'com.android.application' }`,
201+
'android/app/src/main/kotlin/MainActivity.kt': 'class MainActivity',
202+
});
203+
await expect(detect(opts)).resolves.toBe(false);
204+
});
205+
});

src/lib/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { FASTAPI_AGENT_CONFIG } from '@frameworks/fastapi/fastapi-wizard-agent';
1515
import { LARAVEL_AGENT_CONFIG } from '@frameworks/laravel/laravel-wizard-agent';
1616
import { SVELTEKIT_AGENT_CONFIG } from '@frameworks/svelte/svelte-wizard-agent';
1717
import { SWIFT_AGENT_CONFIG } from '@frameworks/swift/swift-wizard-agent';
18+
import { KMP_AGENT_CONFIG } from '@frameworks/kmp/kmp-wizard-agent';
1819
import { ANDROID_AGENT_CONFIG } from '@frameworks/android/android-wizard-agent';
1920
import { RAILS_AGENT_CONFIG } from '@frameworks/rails/rails-wizard-agent';
2021
import { PYTHON_AGENT_CONFIG } from '@frameworks/python/python-wizard-agent';
@@ -37,6 +38,7 @@ export const FRAMEWORK_REGISTRY: Record<Integration, FrameworkConfig> = {
3738
[Integration.fastapi]: FASTAPI_AGENT_CONFIG,
3839
[Integration.laravel]: LARAVEL_AGENT_CONFIG,
3940
[Integration.sveltekit]: SVELTEKIT_AGENT_CONFIG,
41+
[Integration.kmp]: KMP_AGENT_CONFIG,
4042
[Integration.swift]: SWIFT_AGENT_CONFIG,
4143
[Integration.android]: ANDROID_AGENT_CONFIG,
4244
[Integration.rails]: RAILS_AGENT_CONFIG,

0 commit comments

Comments
 (0)