Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,7 @@ plugins

e2e-tests/fixtures/.tracking/*

CLAUDE.local.md

# Generated at build time by scripts/generate-version.js
src/lib/version.ts
src/lib/version.ts
2 changes: 0 additions & 2 deletions src/android/android-wizard-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,6 @@ export const ANDROID_AGENT_CONFIG: FrameworkConfig<AndroidContext> = {
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:<VERSION>"). 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)`,
Expand Down
2 changes: 0 additions & 2 deletions src/angular/angular-wizard-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ export const ANGULAR_AGENT_CONFIG: FrameworkConfig<AngularContext> = {

return [
`Framework docs ID: ${frameworkId} (use posthog://docs/frameworks/${frameworkId} for documentation)`,
'Angular uses dependency injection for services. PostHog should be initialized as a service.',
'For standalone components, ensure PostHog is properly provided in the application config.',
];
},
},
Expand Down
5 changes: 3 additions & 2 deletions src/django/django-wizard-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Integration } from '../lib/constants';
import fg from 'fast-glob';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { PYTHON_DETECTION_IGNORES } from '../lib/glob-patterns';
import {
getDjangoVersion,
getDjangoProjectType,
Expand Down Expand Up @@ -48,7 +49,7 @@ export const DJANGO_AGENT_CONFIG: FrameworkConfig<DjangoContext> = {

const managePyMatches = await fg('**/manage.py', {
cwd: installDir,
ignore: ['**/venv/**', '**/.venv/**', '**/env/**', '**/.env/**'],
ignore: PYTHON_DETECTION_IGNORES,
});

if (managePyMatches.length > 0) {
Expand Down Expand Up @@ -77,7 +78,7 @@ export const DJANGO_AGENT_CONFIG: FrameworkConfig<DjangoContext> = {
['**/requirements*.txt', '**/pyproject.toml', '**/setup.py'],
{
cwd: installDir,
ignore: ['**/venv/**', '**/.venv/**', '**/env/**', '**/.env/**'],
ignore: PYTHON_DETECTION_IGNORES,
},
);

Expand Down
54 changes: 7 additions & 47 deletions src/fastapi/fastapi-wizard-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@
import type { FrameworkConfig } from '../lib/framework-config';
import { PYTHON_PACKAGE_INSTALLATION } from '../lib/framework-config';
import { detectPythonPackageManagers } from '../lib/package-manager-detection';
import { enableDebugLogs } from '../utils/debug';
import { runAgentWizard } from '../lib/agent-runner';
import { Integration } from '../lib/constants';
import clack from '../utils/clack';
import chalk from 'chalk';
import * as semver from 'semver';
import {
getFastAPIVersion,
getFastAPIProjectType,
Expand All @@ -20,12 +15,14 @@
import fg from 'fast-glob';
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
PYTHON_DETECTION_IGNORES,
PYTHON_SOURCE_IGNORES,
} from '../lib/glob-patterns';

/**
* FastAPI framework configuration for the universal agent runner
*/
const MINIMUM_FASTAPI_VERSION = '0.100.0';

export const FASTAPI_AGENT_CONFIG: FrameworkConfig = {
metadata: {
name: 'FastAPI',
Expand All @@ -43,12 +40,13 @@
packageName: 'fastapi',
packageDisplayName: 'FastAPI',
usesPackageJson: false,
getVersion: (_packageJson: any) => {

Check warning on line 43 in src/fastapi/fastapi-wizard-agent.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
// For FastAPI, we don't use package.json. Version is extracted separately
// from requirements.txt or pyproject.toml in the wizard entry point
return undefined;
},
getVersionBucket: getFastAPIVersionBucket,
minimumVersion: '0.100.0',
getInstalledVersion: getFastAPIVersion,
detect: async (options) => {
const { installDir } = options;
Expand All @@ -66,7 +64,7 @@
],
{
cwd: installDir,
ignore: ['**/venv/**', '**/.venv/**', '**/env/**', '**/.env/**'],
ignore: PYTHON_DETECTION_IGNORES,
},
);

Expand Down Expand Up @@ -94,13 +92,7 @@
['**/main.py', '**/app.py', '**/application.py', '**/__init__.py'],
{
cwd: installDir,
ignore: [
'**/venv/**',
'**/.venv/**',
'**/env/**',
'**/.env/**',
'**/__pycache__/**',
],
ignore: PYTHON_SOURCE_IGNORES,
},
);

Expand Down Expand Up @@ -136,8 +128,8 @@
},

analytics: {
getTags: (context: any) => {

Check warning on line 131 in src/fastapi/fastapi-wizard-agent.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const projectType = context.projectType as FastAPIProjectType;

Check warning on line 132 in src/fastapi/fastapi-wizard-agent.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .projectType on an `any` value
return {
projectType: projectType || 'unknown',
};
Expand All @@ -148,7 +140,7 @@
packageInstallation: PYTHON_PACKAGE_INSTALLATION,
projectTypeDetection:
'This is a Python/FastAPI project. Look for requirements.txt, pyproject.toml, setup.py, Pipfile, or main.py/app.py to confirm.',
getAdditionalContextLines: (context: any) => {

Check warning on line 143 in src/fastapi/fastapi-wizard-agent.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const projectType = context.projectType as FastAPIProjectType;
const projectTypeName = projectType
? getFastAPIProjectTypeName(projectType)
Expand Down Expand Up @@ -198,35 +190,3 @@
],
},
};

/**
* FastAPI wizard powered by the universal agent runner.
*/
export async function runFastAPIWizardAgent(
options: WizardOptions,
): Promise<void> {
if (options.debug) {
enableDebugLogs();
}

// Check FastAPI version - agent wizard requires >= 0.100.0
const fastapiVersion = await getFastAPIVersion(options);

if (fastapiVersion) {
const coercedVersion = semver.coerce(fastapiVersion);
if (coercedVersion && semver.lt(coercedVersion, MINIMUM_FASTAPI_VERSION)) {
const docsUrl =
FASTAPI_AGENT_CONFIG.metadata.unsupportedVersionDocsUrl ??
FASTAPI_AGENT_CONFIG.metadata.docsUrl;

clack.log.warn(
`Sorry: the wizard can't help you with FastAPI ${fastapiVersion}. Upgrade to FastAPI ${MINIMUM_FASTAPI_VERSION} or later, or check out the manual setup guide.`,
);
clack.log.info(`Setup FastAPI manually: ${chalk.cyan(docsUrl)}`);
clack.outro('PostHog wizard will see you next time!');
return;
}
}

await runAgentWizard(FASTAPI_AGENT_CONFIG, options);
}
14 changes: 6 additions & 8 deletions src/flask/flask-wizard-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { Integration } from '../lib/constants';
import fg from 'fast-glob';
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
PYTHON_DETECTION_IGNORES,
PYTHON_SOURCE_IGNORES,
} from '../lib/glob-patterns';
import {
getFlaskVersion,
getFlaskProjectType,
Expand Down Expand Up @@ -55,7 +59,7 @@ export const FLASK_AGENT_CONFIG: FrameworkConfig<FlaskContext> = {
],
{
cwd: installDir,
ignore: ['**/venv/**', '**/.venv/**', '**/env/**', '**/.env/**'],
ignore: PYTHON_DETECTION_IGNORES,
},
);

Expand All @@ -80,13 +84,7 @@ export const FLASK_AGENT_CONFIG: FrameworkConfig<FlaskContext> = {
['**/app.py', '**/wsgi.py', '**/application.py', '**/__init__.py'],
{
cwd: installDir,
ignore: [
'**/venv/**',
'**/.venv/**',
'**/env/**',
'**/.env/**',
'**/__pycache__/**',
],
ignore: PYTHON_SOURCE_IGNORES,
},
);

Expand Down
22 changes: 19 additions & 3 deletions src/javascript-node/javascript-node-wizard-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type { FrameworkConfig } from '../lib/framework-config';
import { Integration } from '../lib/constants';
import { tryGetPackageJson } from '../utils/clack-utils';
import { detectNodePackageManagers } from '../lib/package-manager-detection';
import { hasPackageInstalled } from '../utils/package-json';
import { FRAMEWORK_PACKAGES } from '../javascript-web/utils';
import { hasLockfileOrDeps } from '../utils/js-detection';

type JavaScriptNodeContext = Record<string, unknown>;

Expand All @@ -23,7 +26,22 @@ export const JAVASCRIPT_NODE_AGENT_CONFIG: FrameworkConfig<JavaScriptNodeContext
detectPackageManager: detectNodePackageManagers,
detect: async (options) => {
const packageJson = await tryGetPackageJson(options);
return !!packageJson;
if (!packageJson) {
return false;
}

// Exclude projects with known framework packages (handled by
// their dedicated detectors earlier in the enum)
for (const frameworkPkg of FRAMEWORK_PACKAGES) {
if (hasPackageInstalled(frameworkPkg, packageJson)) {
return false;
}
}

// Catch-all for JS projects without browser signals (those
// matched javascript_web already). Require a lockfile or real
// dependencies so we don't match bare tooling package.json files.
return hasLockfileOrDeps(options.installDir, packageJson);
},
},

Expand All @@ -42,8 +60,6 @@ export const JAVASCRIPT_NODE_AGENT_CONFIG: FrameworkConfig<JavaScriptNodeContext
prompts: {
projectTypeDetection:
'This is a server-side Node.js project. Look for package.json and lockfiles to confirm.',
packageInstallation:
'Use npm, yarn, pnpm, or bun based on the existing lockfile (package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb). Install posthog-node as a regular dependency.',
getAdditionalContextLines: () => [
`Framework docs ID: javascript_node (use posthog://docs/frameworks/javascript_node for documentation)`,
],
Expand Down
43 changes: 22 additions & 21 deletions src/javascript-web/javascript-web-wizard-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,30 +49,31 @@ export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig<JavaScriptContext> = {
}
}

// Ensure this is actually a JS project, not just a package.json for tooling
const { installDir } = options;
// Require a positive browser signal — without one, the project is
// more likely a Node.js server/CLI/worker and should fall through
// to the javascript_node catch-all (posthog-node is the safer
// default since posthog-js crashes without window/document).
//
// Bundlers alone are NOT a reliable browser signal — Vite/esbuild
// are commonly used for server-side builds (Cloudflare Workers, SSR,
// Vitest, etc.). Instead we check for:
// 1. An HTML entry point (fundamental to browser apps)
// 2. A "browser" field in package.json (standard npm browser flag)
const hasHtmlEntry = [
'index.html',
'public/index.html',
'src/index.html',
].some((f) => fs.existsSync(path.join(options.installDir, f)));

// Check for a lockfile
const hasLockfile = [
'package-lock.json',
'yarn.lock',
'pnpm-lock.yaml',
'bun.lockb',
'bun.lock',
].some((lockfile) => fs.existsSync(path.join(installDir, lockfile)));
const hasBrowserField = 'browser' in packageJson;

if (hasLockfile) {
return true;
}

// Fallback: check if package.json has actual dependencies
const hasDeps =
(packageJson.dependencies &&
Object.keys(packageJson.dependencies).length > 0) ||
(packageJson.devDependencies &&
Object.keys(packageJson.devDependencies).length > 0);
// Known browser frameworks without dedicated integrations
const BROWSER_FRAMEWORK_PACKAGES = ['gatsby'];
const hasBrowserFramework = BROWSER_FRAMEWORK_PACKAGES.some((pkg) =>
hasPackageInstalled(pkg, packageJson),
);

return !!hasDeps;
return hasHtmlEntry || hasBrowserField || hasBrowserFramework;
},
},

Expand Down
2 changes: 2 additions & 0 deletions src/javascript-web/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const FRAMEWORK_PACKAGES = [
'nuxt',
'vue',
'react-router',
'@remix-run/react',
'@remix-run/node',
'@tanstack/react-start',
'@tanstack/react-router',
'react-native',
Expand Down
13 changes: 0 additions & 13 deletions src/laravel/laravel-wizard-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,6 @@ export const LARAVEL_AGENT_CONFIG: FrameworkConfig<LaravelContext> = {
prompts: {
projectTypeDetection:
'This is a PHP/Laravel project. Look for composer.json, artisan CLI, and app/ directory structure to confirm. Check for Laravel-specific packages like laravel/framework.',
packageInstallation:
'Use Composer to install packages. Run `composer require posthog/posthog-php` without pinning a specific version.',
getAdditionalContextLines: (context) => {
const projectTypeName = context.projectType
? getLaravelProjectTypeName(context.projectType)
Expand All @@ -135,17 +133,6 @@ export const LARAVEL_AGENT_CONFIG: FrameworkConfig<LaravelContext> = {
lines.push(`Bootstrap file: ${context.bootstrapFile}`);
}

// Add Laravel-specific guidance based on version structure
if (context.laravelStructure === 'latest') {
lines.push(
'Note: Laravel 11+ uses simplified bootstrap/app.php for middleware and providers',
);
} else {
lines.push(
'Note: Use app/Http/Kernel.php for middleware, app/Providers for service providers',
);
}

return lines;
},
},
Expand Down
21 changes: 21 additions & 0 deletions src/lib/__tests__/wizard-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,27 @@ describe('resolveEnvPath', () => {
// edge case: filePath resolves to exactly workingDirectory
expect(() => resolveEnvPath('/project', '.')).not.toThrow();
});

it('strips redundant subdirectory prefix from relative path', () => {
// Agent passes "services/mcp/.env" when workingDir is already "/ws/services/mcp"
const result = resolveEnvPath('/ws/services/mcp', 'services/mcp/.env');
expect(result).toBe(path.resolve('/ws/services/mcp', '.env'));
});

it('strips single-level redundant prefix', () => {
const result = resolveEnvPath('/ws/frontend', 'frontend/.env.local');
expect(result).toBe(path.resolve('/ws/frontend', '.env.local'));
});

it('does not strip when there is no redundant prefix', () => {
const result = resolveEnvPath('/ws/services/mcp', '.env');
expect(result).toBe(path.resolve('/ws/services/mcp', '.env'));
});

it('does not strip legitimate nested paths', () => {
const result = resolveEnvPath('/ws/services/mcp', 'config/.env');
expect(result).toBe(path.resolve('/ws/services/mcp', 'config/.env'));
});
});

// ---------------------------------------------------------------------------
Expand Down
7 changes: 6 additions & 1 deletion src/lib/agent-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ export async function runAgent(
onMessage(message: any): void;
finalize(resultMessage: any, totalDurationMs: number): any;
},
onStatus?: (message: string) => void,
): Promise<{ error?: AgentErrorType; message?: string }> {
const {
estimatedDurationMinutes = 8,
Expand Down Expand Up @@ -624,6 +625,7 @@ export async function runAgent(
spinner,
collectedText,
receivedSuccessResult,
onStatus,
);

try {
Expand Down Expand Up @@ -734,6 +736,7 @@ function handleSDKMessage(
spinner: ReturnType<typeof clack.spinner>,
collectedText: string[],
receivedSuccessResult = false,
onStatus?: (message: string) => void,
): void {
logToFile(`SDK Message: ${message.type}`, JSON.stringify(message, null, 2));

Expand All @@ -760,8 +763,10 @@ function handleSDKMessage(
);
const statusMatch = block.text.match(statusRegex);
if (statusMatch) {
spinner.stop(statusMatch[1].trim());
const statusMessage = statusMatch[1].trim();
spinner.stop(statusMessage);
spinner.start('Integrating PostHog...');
onStatus?.(statusMessage);
}
}
}
Expand Down
Loading
Loading