Skip to content

Commit c8c2d59

Browse files
feat: add Java framework support
Adds Java (server) as a supported framework, stacked on the Rust framework PR: - Integration.java enum entry ordered after kmp/swift/android — those claim gradle projects first — and before the language fallbacks - JAVA_AGENT_CONFIG in src/frameworks/java/ — claims pom.xml unconditionally (Maven is unambiguously JVM backend) and root gradle files only when they carry no Android/KMP plugin markers and no AndroidManifest.xml subtree exists; gathers buildTool (maven | gradle) as agent context; installs by editing the build file with com.posthog:posthog-server then resolving with mvn install / gradle build - detectJavaPackageManagers detector (Maven via pom.xml, Gradle fallback reusing gradlePackageManager) + FRAMEWORK_REGISTRY entry - No bash-fence changes needed: mvn/mvnw and gradle/gradlew grammars already exist from Android/KMP - Pins framework: java on a new integration-java entry in the variant-resolution contract test, matching context-mill PR #270 which must release before this merges - Marks pom.xml as a real framework target in the agentic manifest comment, leaving .NET as the only manifest without a target Generated-By: PostHog Code Task-Id: 9c949855-7611-48f2-b8b5-7aa4112543e5
1 parent 1fa8314 commit c8c2d59

8 files changed

Lines changed: 305 additions & 2 deletions

File tree

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/* Java (server) wizard using posthog-agent with PostHog MCP */
2+
import fg from 'fast-glob';
3+
import * as fs from 'node:fs';
4+
import * as path from 'node:path';
5+
import type { WizardRunOptions } from '@utils/types';
6+
import type { FrameworkConfig } from '@lib/framework-config';
7+
import { detectJavaPackageManagers } from '@lib/detection/package-manager';
8+
import { Integration } from '@lib/constants';
9+
10+
type JavaContext = {
11+
buildTool?: 'maven' | 'gradle';
12+
};
13+
14+
const GRADLE_FILES = [
15+
'build.gradle',
16+
'build.gradle.kts',
17+
'settings.gradle',
18+
'settings.gradle.kts',
19+
];
20+
21+
const ANDROID_GRADLE_MARKERS = [
22+
'com.android.application',
23+
'com.android.library',
24+
'com.android.tools.build:gradle',
25+
];
26+
27+
const KMP_GRADLE_MARKERS = [
28+
'kotlin("multiplatform")',
29+
'org.jetbrains.kotlin.multiplatform',
30+
'kotlin-multiplatform',
31+
];
32+
33+
function readRootGradleFiles(installDir: string): string[] {
34+
return GRADLE_FILES.map((name) => path.join(installDir, name))
35+
.filter((p) => fs.existsSync(p))
36+
.map((p) => fs.readFileSync(p, 'utf-8'));
37+
}
38+
39+
function getBuildTool(installDir: string): 'maven' | 'gradle' | undefined {
40+
if (fs.existsSync(path.join(installDir, 'pom.xml'))) {
41+
return 'maven';
42+
}
43+
if (readRootGradleFiles(installDir).length > 0) {
44+
return 'gradle';
45+
}
46+
return undefined;
47+
}
48+
49+
export const JAVA_AGENT_CONFIG: FrameworkConfig<JavaContext> = {
50+
metadata: {
51+
name: 'Java',
52+
integration: Integration.java,
53+
docsUrl: 'https://posthog.com/docs/libraries/java',
54+
gatherContext: (options: WizardRunOptions) => {
55+
const buildTool = getBuildTool(options.installDir);
56+
return Promise.resolve({ buildTool });
57+
},
58+
},
59+
60+
detection: {
61+
packageName: 'posthog-server',
62+
packageDisplayName: 'Java',
63+
usesPackageJson: false,
64+
getVersion: () => undefined,
65+
// Maven projects (pom.xml) are unambiguously JVM backends. Gradle
66+
// projects are only claimed when they are neither Android nor KMP —
67+
// those detectors are ordered earlier, and this re-check keeps a
68+
// direct java detect() call from ever claiming their projects.
69+
detect: async (options) => {
70+
const { installDir } = options;
71+
72+
// pubspec.yaml means Flutter — its gradle subtree is not a JVM backend.
73+
if (fs.existsSync(path.join(installDir, 'pubspec.yaml'))) {
74+
return false;
75+
}
76+
77+
if (fs.existsSync(path.join(installDir, 'pom.xml'))) {
78+
return true;
79+
}
80+
81+
const gradleContents = readRootGradleFiles(installDir);
82+
if (gradleContents.length === 0) {
83+
return false;
84+
}
85+
const combined = gradleContents.join('\n');
86+
if (
87+
[...ANDROID_GRADLE_MARKERS, ...KMP_GRADLE_MARKERS].some((marker) =>
88+
combined.includes(marker),
89+
)
90+
) {
91+
return false;
92+
}
93+
94+
const manifestFiles = await fg('**/AndroidManifest.xml', {
95+
cwd: installDir,
96+
ignore: ['**/build/**', '**/node_modules/**', '**/.gradle/**'],
97+
});
98+
return manifestFiles.length === 0;
99+
},
100+
detectPackageManager: detectJavaPackageManagers,
101+
},
102+
103+
environment: {
104+
uploadToHosting: false,
105+
getEnvVars: (apiKey: string, host: string) => ({
106+
POSTHOG_API_KEY: apiKey,
107+
POSTHOG_HOST: host,
108+
}),
109+
},
110+
111+
analytics: {
112+
getTags: (context) => ({
113+
buildTool: context.buildTool || 'unknown',
114+
}),
115+
},
116+
117+
prompts: {
118+
projectTypeDetection:
119+
'This is a Java server project. Look for pom.xml (Maven) or build.gradle/build.gradle.kts (Gradle), src/main/java/, and the framework in use (Spring Boot, Quarkus, Micronaut, plain servlets) to confirm.',
120+
packageInstallation:
121+
'Neither Maven nor Gradle has a single add command. Add the com.posthog:posthog-server dependency (pinned to the latest release on Maven Central) to pom.xml or build.gradle(.kts), matching the existing dependency format, then run `mvn install` or `gradle build` to resolve it.',
122+
getAdditionalContextLines: (context) => {
123+
const lines = [
124+
`Framework docs ID: java (use posthog://docs/frameworks/java for documentation)`,
125+
'Use the posthog-server SDK (com.posthog:posthog-server) — posthog-android is a different library and must not be used on the server.',
126+
'Create one PostHog client per process with PostHogConfig.builder(...) and PostHog.with(config); call close() during graceful shutdown, and flush() before returning in serverless handlers.',
127+
];
128+
if (context.buildTool) {
129+
lines.push(`Build tool: ${context.buildTool}`);
130+
}
131+
return lines;
132+
},
133+
},
134+
135+
ui: {
136+
successMessage: 'PostHog integration complete',
137+
estimatedDurationMinutes: 5,
138+
getOutroChanges: (context) => [
139+
'Analyzed your Java project structure',
140+
`Added the posthog-server dependency to your ${
141+
context.buildTool === 'maven' ? 'pom.xml' : 'Gradle build'
142+
}`,
143+
'Initialized a shared PostHog client configured from environment variables',
144+
'Instrumented meaningful server events with posthog.capture',
145+
],
146+
getOutroNextSteps: () => [
147+
'Run your Java service and trigger the instrumented code paths',
148+
'Visit your PostHog dashboard to see incoming events',
149+
'Use posthog.capture(distinctId, eventName, options) to track custom events',
150+
'Keep posthog.close() in your graceful shutdown path so queued events flush',
151+
],
152+
},
153+
};

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-ruby', framework: 'ruby' },
5151
{ id: 'integration-elixir' },
5252
{ id: 'integration-go' },
53+
{ id: 'integration-java', framework: 'java' },
5354
{ id: 'integration-swift', framework: 'swift' },
5455
{ id: 'integration-kmp', framework: 'kmp' },
5556
{ id: 'integration-flutter' },

src/lib/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ export enum Integration {
100100
swift = 'swift',
101101
android = 'android',
102102
rails = 'rails',
103+
// Must stay after kmp/swift/android: those claim gradle projects first.
104+
java = 'java',
103105

104106
// Language fallbacks. Keep javascriptNode last: it matches any package.json.
105107
python = 'python',

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { detectFramework } from '@lib/detection/framework';
55
import { Integration } from '@lib/constants';
66
import { ANDROID_AGENT_CONFIG } from '../../../frameworks/android/android-wizard-agent';
77
import { KMP_AGENT_CONFIG } from '../../../frameworks/kmp/kmp-wizard-agent';
8+
import { JAVA_AGENT_CONFIG } from '../../../frameworks/java/java-wizard-agent';
89

910
/** A throwaway project dir seeded with the given files. */
1011
function makeProject(files: Record<string, string>): string {
@@ -54,6 +55,18 @@ describe('Integration enum order (drives first-match detection)', () => {
5455
expect(order[order.length - 1]).toBe(Integration.javascriptNode);
5556
});
5657

58+
test('java is ordered after kmp, swift, and android (they claim gradle projects first)', () => {
59+
for (const specific of [
60+
Integration.kmp,
61+
Integration.swift,
62+
Integration.android,
63+
]) {
64+
expect(order.indexOf(Integration.java)).toBeGreaterThan(
65+
order.indexOf(specific),
66+
);
67+
}
68+
});
69+
5770
test('kmp is ordered before android and swift (more specific detection wins)', () => {
5871
// A KMP project also looks like an Android/Swift project, so KMP must be
5972
// checked first for first-match detection to resolve it correctly.
@@ -137,6 +150,27 @@ describe('detectFramework (end-to-end over real project dirs)', () => {
137150
);
138151
});
139152

153+
test('a Maven project resolves to java', async () => {
154+
const opts = project({
155+
'pom.xml':
156+
'<project><groupId>com.example</groupId><artifactId>app</artifactId></project>',
157+
'src/main/java/App.java': 'class App {}',
158+
});
159+
await expect(detectFramework(opts.installDir)).resolves.toBe(
160+
Integration.java,
161+
);
162+
});
163+
164+
test('a plain Gradle JVM project resolves to java, not android', async () => {
165+
const opts = project({
166+
'build.gradle': `plugins { id 'java' id 'org.springframework.boot' }`,
167+
'src/main/java/App.java': 'class App {}',
168+
});
169+
await expect(detectFramework(opts.installDir)).resolves.toBe(
170+
Integration.java,
171+
);
172+
});
173+
140174
test('a Flutter project is not claimed by anything', async () => {
141175
const opts = project({
142176
'pubspec.yaml': 'name: my_flutter_app\nenvironment:\n sdk: ^3.0.0\n',
@@ -169,6 +203,58 @@ describe('android detect', () => {
169203
});
170204
});
171205

206+
describe('java detect', () => {
207+
const detect = JAVA_AGENT_CONFIG.detection.detect;
208+
209+
test('claims a Maven project', async () => {
210+
const opts = project({ 'pom.xml': '<project></project>' });
211+
await expect(detect(opts)).resolves.toBe(true);
212+
});
213+
214+
test('claims a plain Gradle JVM project', async () => {
215+
const opts = project({
216+
'build.gradle.kts': `plugins { java }\ndependencies {}`,
217+
});
218+
await expect(detect(opts)).resolves.toBe(true);
219+
});
220+
221+
test('does not claim an Android gradle project', async () => {
222+
const opts = project({
223+
'build.gradle': `plugins { id 'com.android.application' }`,
224+
});
225+
await expect(detect(opts)).resolves.toBe(false);
226+
});
227+
228+
test('does not claim a gradle project with an AndroidManifest.xml subtree', async () => {
229+
const opts = project({
230+
'build.gradle': `plugins { id 'java' }`,
231+
'app/src/main/AndroidManifest.xml': '<manifest/>',
232+
});
233+
await expect(detect(opts)).resolves.toBe(false);
234+
});
235+
236+
test('does not claim a KMP project', async () => {
237+
const opts = project({
238+
'build.gradle.kts': `plugins { kotlin("multiplatform") }`,
239+
});
240+
await expect(detect(opts)).resolves.toBe(false);
241+
});
242+
243+
test('does not claim a Flutter project', async () => {
244+
const opts = project({
245+
'pubspec.yaml': 'name: my_flutter_app\n',
246+
'android/build.gradle': `plugins { id 'com.android.application' }`,
247+
'settings.gradle': 'include ":app"',
248+
});
249+
await expect(detect(opts)).resolves.toBe(false);
250+
});
251+
252+
test('does not claim a project with no build files', async () => {
253+
const opts = project({ 'src/main/java/App.java': 'class App {}' });
254+
await expect(detect(opts)).resolves.toBe(false);
255+
});
256+
});
257+
172258
describe('kmp detect', () => {
173259
const detect = KMP_AGENT_CONFIG.detection.detect;
174260

src/lib/detection/__tests__/package-manager.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
composerPackageManager,
88
swiftPackageManager,
99
gradlePackageManager,
10+
detectJavaPackageManagers,
1011
} from '@lib/detection/package-manager';
1112

1213
vi.mock('../../../utils/debug');
@@ -179,6 +180,33 @@ describe('detectPythonPackageManagers', () => {
179180
});
180181
});
181182

183+
// ---------------------------------------------------------------------------
184+
// Java detection
185+
// ---------------------------------------------------------------------------
186+
187+
describe('detectJavaPackageManagers', () => {
188+
let tmpDir: string;
189+
190+
beforeEach(() => {
191+
tmpDir = makeTmpDir();
192+
});
193+
afterEach(() => cleanup(tmpDir));
194+
195+
it('detects maven via pom.xml', async () => {
196+
fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
197+
const result = await detectJavaPackageManagers(tmpDir);
198+
expect(result.primary?.name).toBe('maven');
199+
expect(result.primary?.installCommand).toBe('mvn install');
200+
expect(result.recommendation).toContain('pom.xml');
201+
});
202+
203+
it('falls back to gradle without a pom.xml', async () => {
204+
fs.writeFileSync(path.join(tmpDir, 'build.gradle'), '');
205+
const result = await detectJavaPackageManagers(tmpDir);
206+
expect(result.primary?.name).toBe('gradle');
207+
});
208+
});
209+
182210
// ---------------------------------------------------------------------------
183211
// Static helpers
184212
// ---------------------------------------------------------------------------

src/lib/detection/agentic.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,13 @@ export const PROJECT_MANIFESTS: readonly string[] = [
7474
// Ruby / PHP
7575
'Gemfile',
7676
'composer.json',
77-
// Rust / Go / Elixir / JVM / .NET: no framework targets yet, but found so
77+
// Java
78+
'pom.xml',
79+
// Rust / Go / Elixir / .NET: no framework targets yet, but found so
7880
// an existing PostHog SDK is reported (feeds self-driving's "continue" path).
7981
'Cargo.toml',
8082
'go.mod',
8183
'mix.exs',
82-
'pom.xml',
8384
'*.csproj',
8485
// Mobile / native
8586
'Package.swift',

src/lib/detection/package-manager.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* current framework supplies.
88
*/
99

10+
import * as fs from 'node:fs';
11+
import * as path from 'node:path';
1012
import {
1113
detectAllPackageManagers,
1214
type PackageManager,
@@ -233,3 +235,31 @@ export function gradlePackageManager(): Promise<PackageManagerInfo> {
233235
'Add dependencies to build.gradle(.kts) using implementation().',
234236
});
235237
}
238+
239+
// ---------------------------------------------------------------------------
240+
// Java (Maven or Gradle) helper
241+
// ---------------------------------------------------------------------------
242+
243+
const MAVEN: DetectedPackageManager = {
244+
name: 'maven',
245+
label: 'Maven',
246+
installCommand: 'mvn install',
247+
};
248+
249+
/**
250+
* Java backends split between Maven and Gradle; pom.xml decides.
251+
* Defaults to Gradle when neither manifest is present.
252+
*/
253+
export function detectJavaPackageManagers(
254+
installDir: string,
255+
): Promise<PackageManagerInfo> {
256+
if (fs.existsSync(path.join(installDir, 'pom.xml'))) {
257+
return Promise.resolve({
258+
detected: [MAVEN],
259+
primary: MAVEN,
260+
recommendation:
261+
'Use Maven. Add the dependency to pom.xml, then run mvn install to resolve it.',
262+
});
263+
}
264+
return gradlePackageManager();
265+
}

src/lib/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { SWIFT_AGENT_CONFIG } from '@frameworks/swift/swift-wizard-agent';
1818
import { KMP_AGENT_CONFIG } from '@frameworks/kmp/kmp-wizard-agent';
1919
import { ANDROID_AGENT_CONFIG } from '@frameworks/android/android-wizard-agent';
2020
import { RAILS_AGENT_CONFIG } from '@frameworks/rails/rails-wizard-agent';
21+
import { JAVA_AGENT_CONFIG } from '@frameworks/java/java-wizard-agent';
2122
import { PYTHON_AGENT_CONFIG } from '@frameworks/python/python-wizard-agent';
2223
import { RUBY_AGENT_CONFIG } from '@frameworks/ruby/ruby-wizard-agent';
2324
import { JAVASCRIPT_NODE_AGENT_CONFIG } from '@frameworks/javascript-node/javascript-node-wizard-agent';
@@ -42,6 +43,7 @@ export const FRAMEWORK_REGISTRY: Record<Integration, FrameworkConfig> = {
4243
[Integration.swift]: SWIFT_AGENT_CONFIG,
4344
[Integration.android]: ANDROID_AGENT_CONFIG,
4445
[Integration.rails]: RAILS_AGENT_CONFIG,
46+
[Integration.java]: JAVA_AGENT_CONFIG,
4547
[Integration.python]: PYTHON_AGENT_CONFIG,
4648
[Integration.ruby]: RUBY_AGENT_CONFIG,
4749
[Integration.javascriptNode]: JAVASCRIPT_NODE_AGENT_CONFIG,

0 commit comments

Comments
 (0)