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
117 changes: 104 additions & 13 deletions src/frameworks/javascript-node/javascript-node-wizard-agent.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
/* Generic Node.js language 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 * as fs from 'node:fs';
import * as path from 'node:path';
import { tryGetPackageJson } from '../../utils/setup-utils';
import { detectNodePackageManagers } from '../../lib/detection/package-manager';
import { detectAllPackageManagers } from '../../utils/package-manager';
import {
detectServerFramework,
detectEntryPoint,
detectProjectType,
} from './utils';

type JavaScriptNodeContext = Record<string, unknown>;
type JavaScriptNodeContext = {
serverFramework?: string;
entryPoint?: string;
hasTypeScript?: boolean;
packageManagerName?: string;
projectType?: string;
};

export const JAVASCRIPT_NODE_AGENT_CONFIG: FrameworkConfig<JavaScriptNodeContext> =
{
Expand All @@ -13,6 +28,34 @@ export const JAVASCRIPT_NODE_AGENT_CONFIG: FrameworkConfig<JavaScriptNodeContext
integration: Integration.javascriptNode,
beta: true,
docsUrl: 'https://posthog.com/docs/libraries/node',
gatherContext: (options: WizardOptions) => {
const { installDir } = options;
const context: JavaScriptNodeContext = {};

const detected = detectAllPackageManagers(options);
if (detected.length > 0) {
context.packageManagerName = detected[0].label;
}

context.hasTypeScript = fs.existsSync(
path.join(installDir, 'tsconfig.json'),
);

try {
const content = fs.readFileSync(
path.join(installDir, 'package.json'),
'utf-8',
);
const pkg = JSON.parse(content) as Record<string, unknown>;
context.serverFramework = detectServerFramework(pkg);
context.entryPoint = detectEntryPoint(installDir, pkg);
context.projectType = detectProjectType(pkg);
} catch {
// No package.json or parse error
}

return Promise.resolve(context);
},
},

detection: {
Expand All @@ -36,29 +79,77 @@ export const JAVASCRIPT_NODE_AGENT_CONFIG: FrameworkConfig<JavaScriptNodeContext
},

analytics: {
getTags: () => ({}),
getTags: (context) => {
const tags: Record<string, string> = {};
if (context.serverFramework) {
tags.serverFramework = context.serverFramework;
}
if (context.projectType) {
tags.projectType = context.projectType;
}
return tags;
},
},

prompts: {
projectTypeDetection:
'This is a server-side Node.js project. Look for package.json and lockfiles to confirm.',
'This is a server-side Node.js project. Check the additional context lines below for detected patterns.',
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)`,
],
getAdditionalContextLines: (context) => {
const lines: string[] = [];

if (context.serverFramework) {
lines.push(
`Server framework: ${context.serverFramework} (detected in package.json)`,
);
}

if (context.entryPoint) {
lines.push(`Entry point: ${context.entryPoint}`);
}

if (context.projectType) {
lines.push(`Project type: ${context.projectType}`);
} else {
lines.push(
`Project type: Node.js application (no specific framework detected)`,
);
}

lines.push(
`Package manager: ${context.packageManagerName ?? 'unknown'}`,
);
lines.push(`Has TypeScript: ${context.hasTypeScript ? 'yes' : 'no'}`);
lines.push(
`Framework docs ID: javascript_node (use posthog://docs/frameworks/javascript_node for documentation)`,
);
lines.push(``);
lines.push(
`Integration approach: Explore the project's file structure to understand its architecture, then integrate posthog-node in the way that best fits the project's existing patterns. If a server framework was detected above, look at how it handles middleware, routes, and error handling to find the right integration points.`,
);

return lines;
},
},

ui: {
successMessage: 'PostHog integration complete',
estimatedDurationMinutes: 5,
getOutroChanges: () => [
`Analyzed your Node.js project structure`,
`Installed the posthog-node package`,
`Created PostHog initialization with proper configuration`,
`Configured graceful shutdown for event flushing`,
`Added example code for events, feature flags, and error capture`,
],
getOutroChanges: (context) => {
const changes = [`Analyzed your Node.js project structure`];
if (context.serverFramework) {
changes.push(
`Detected ${context.serverFramework} and integrated PostHog accordingly`,
);
}
changes.push(
`Installed the posthog-node package`,
`Created PostHog initialization with proper configuration`,
`Configured graceful shutdown for event flushing`,
);
return changes;
},
getOutroNextSteps: () => [
'Use the PostHog client instance for all tracking calls',
'Call posthog.shutdown() on application exit to flush pending events',
Expand Down
87 changes: 87 additions & 0 deletions src/frameworks/javascript-node/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as fs from 'node:fs';
import * as path from 'node:path';

/**
* Server frameworks that don't have dedicated wizard skills.
* Package name → display name.
*/
const SERVER_FRAMEWORKS: Record<string, string> = {
express: 'Express',
fastify: 'Fastify',
koa: 'Koa',
'@hapi/hapi': 'Hapi',
'@nestjs/core': 'Nest.js',
hono: 'Hono',
micro: 'Micro',
restify: 'Restify',
};

const CLI_PACKAGES = [
'commander',
'yargs',
'meow',
'oclif',
'inquirer',
'vorpal',
];
const WORKER_PACKAGES = ['bullmq', 'bull', 'bee-queue', 'agenda', 'node-cron'];

function getAllDeps(
packageJson: Record<string, unknown>,
): Record<string, string> {
return {
...(packageJson.dependencies as Record<string, string> | undefined),
...(packageJson.devDependencies as Record<string, string> | undefined),
};
}

export function detectServerFramework(
packageJson: Record<string, unknown>,
): string | undefined {
const deps = getAllDeps(packageJson);
for (const [pkg, name] of Object.entries(SERVER_FRAMEWORKS)) {
if (deps[pkg]) return name;
}
return undefined;
}

export function detectProjectType(
packageJson: Record<string, unknown>,
): string | undefined {
const deps = getAllDeps(packageJson);
if (CLI_PACKAGES.some((pkg) => deps[pkg])) return 'CLI tool';
if (WORKER_PACKAGES.some((pkg) => deps[pkg]))
return 'background worker / job processor';
if (Object.keys(SERVER_FRAMEWORKS).some((pkg) => deps[pkg]))
return 'API server';
return undefined;
}

export function detectEntryPoint(
installDir: string,
packageJson: Record<string, unknown>,
): string | undefined {
if (typeof packageJson.main === 'string') return packageJson.main;

const candidates = [
'src/index.ts',
'src/index.js',
'src/server.ts',
'src/server.js',
'src/app.ts',
'src/app.js',
'server.ts',
'server.js',
'index.ts',
'index.js',
'app.ts',
'app.js',
'main.ts',
'main.js',
];

for (const candidate of candidates) {
if (fs.existsSync(path.join(installDir, candidate))) return candidate;
}
return undefined;
}
78 changes: 61 additions & 17 deletions src/frameworks/javascript-web/javascript-web-wizard-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
detectJsPackageManager,
detectBundler,
hasIndexHtml,
detectVanillaWeb,
hasSrcDirectory,
type JavaScriptContext,
} from './utils';
import { detectNodePackageManagers } from '../../lib/detection/package-manager';
Expand All @@ -27,7 +29,16 @@ export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig<JavaScriptContext> = {
path.join(options.installDir, 'tsconfig.json'),
);
const hasBundler = detectBundler(options);
return Promise.resolve({ packageManagerName, hasTypeScript, hasBundler });
const vanillaHtml = detectVanillaWeb(options);
const hasSrcDir = hasSrcDirectory(options);
return Promise.resolve({
packageManagerName,
hasTypeScript,
hasBundler,
isVanilla: !!vanillaHtml,
htmlEntryPoint: vanillaHtml ?? undefined,
hasSrcDir,
});
},
},

Expand All @@ -39,8 +50,11 @@ export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig<JavaScriptContext> = {
detectPackageManager: detectNodePackageManagers,
detect: async (options) => {
const packageJson = await tryGetPackageJson(options);

// Path 1: Vanilla web project (no package.json at all)
if (!packageJson) {
return false;
const vanillaHtml = detectVanillaWeb(options);
return !!vanillaHtml;
}

// Exclude projects with known framework packages
Expand All @@ -52,9 +66,8 @@ export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig<JavaScriptContext> = {

const { installDir } = options;

// Has (index.html OR has a bundler) AND is a JavaScript project
// Path 2: Bundled web project (package.json + lockfile + frontend signal)
const hasIndexHtmlFlag = hasIndexHtml(options);

const bundler = detectBundler(options);
const hasBundler = !!bundler;

Expand All @@ -67,9 +80,6 @@ export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig<JavaScriptContext> = {
'deno.lock',
].some((lockfile) => fs.existsSync(path.join(installDir, lockfile)));

// We only treat this as JS Web if there's BOTH:
// - a lockfile, and
// - at least one frontend signal (index.html or bundler)
if (hasLockfile && (hasIndexHtmlFlag || hasBundler)) {
return true;
}
Expand All @@ -95,27 +105,53 @@ export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig<JavaScriptContext> = {
if (context.hasBundler) {
tags.bundler = context.hasBundler;
}
if (context.isVanilla) {
tags.projectVariant = 'vanilla';
}
return tags;
},
},

prompts: {
projectTypeDetection:
'This is a JavaScript/TypeScript project. Look for package.json and lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb) to confirm.',
'This is a client-side web project. It may use a package manager and bundler, or it may be a vanilla HTML/CSS/JS site — check the additional context lines below.',
packageInstallation:
'Look for lockfiles to determine the package manager (npm, yarn, pnpm, bun). Do not manually edit package.json.',
'Check the additional context below for whether a package manager is available. If no package manager is detected, use a CDN script tag for posthog-js instead of npm install.',
getAdditionalContextLines: (context) => {
const lines = [
`Package manager: ${context.packageManagerName ?? 'unknown'}`,
`Has TypeScript: ${context.hasTypeScript ? 'yes' : 'no'}`,
`Framework docs ID: js (use posthog://docs/frameworks/js for documentation if available)`,
`Project type: Generic JavaScript/TypeScript application (no specific framework detected)`,
];
const lines: string[] = [];

if (context.hasBundler) {
lines.unshift(`Bundler: ${context.hasBundler}`);
if (context.isVanilla) {
lines.push(
`Project type: Vanilla web (no package manager — use CDN script tag for posthog-js)`,
);
if (context.htmlEntryPoint) {
lines.push(`HTML entry point: ${context.htmlEntryPoint}`);
}
} else {
lines.push(
`Project type: JavaScript/TypeScript web application (no specific framework detected)`,
);
lines.push(
`Package manager: ${context.packageManagerName ?? 'unknown'}`,
);
if (context.hasBundler) {
lines.push(`Bundler: ${context.hasBundler}`);
}
lines.push(`Has TypeScript: ${context.hasTypeScript ? 'yes' : 'no'}`);
}

if (context.hasSrcDir) {
lines.push(`Project structure: has src/ directory`);
}

lines.push(
`Framework docs ID: js (use posthog://docs/frameworks/js for documentation if available)`,
);
lines.push(``);
lines.push(
`Integration approach: No specific framework was detected. Explore the project's file structure to understand its architecture, then integrate posthog-js in the way that best fits the project's existing patterns.`,
);

return lines;
},
},
Expand All @@ -124,6 +160,14 @@ export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig<JavaScriptContext> = {
successMessage: 'PostHog integration complete',
estimatedDurationMinutes: 5,
getOutroChanges: (context) => {
if (context.isVanilla) {
return [
`Analyzed your vanilla web project structure`,
`Added posthog-js via script tag`,
`Created PostHog initialization code`,
`Configured autocapture, error tracking, and event capture`,
];
}
const packageManagerName =
context.packageManagerName ?? 'package manager';
return [
Expand Down
Loading
Loading