From 8f5b4370f9683d52dc1db42945907d7760b4b538 Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Fri, 1 May 2026 15:18:14 -0400 Subject: [PATCH] chore: fallback v2 --- .../javascript-node-wizard-agent.ts | 117 ++++++++++++-- src/frameworks/javascript-node/utils.ts | 87 +++++++++++ .../javascript-web-wizard-agent.ts | 78 +++++++--- src/frameworks/javascript-web/utils.ts | 54 ++++++- src/frameworks/python/python-wizard-agent.ts | 103 ++++++++++++- src/frameworks/ruby/ruby-wizard-agent.ts | 143 ++++++++++++++---- 6 files changed, 515 insertions(+), 67 deletions(-) create mode 100644 src/frameworks/javascript-node/utils.ts diff --git a/src/frameworks/javascript-node/javascript-node-wizard-agent.ts b/src/frameworks/javascript-node/javascript-node-wizard-agent.ts index d3ff3230..0ddb9428 100644 --- a/src/frameworks/javascript-node/javascript-node-wizard-agent.ts +++ b/src/frameworks/javascript-node/javascript-node-wizard-agent.ts @@ -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; +type JavaScriptNodeContext = { + serverFramework?: string; + entryPoint?: string; + hasTypeScript?: boolean; + packageManagerName?: string; + projectType?: string; +}; export const JAVASCRIPT_NODE_AGENT_CONFIG: FrameworkConfig = { @@ -13,6 +28,34 @@ export const JAVASCRIPT_NODE_AGENT_CONFIG: FrameworkConfig { + 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; + 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: { @@ -36,29 +79,77 @@ export const JAVASCRIPT_NODE_AGENT_CONFIG: FrameworkConfig ({}), + getTags: (context) => { + const tags: Record = {}; + 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', diff --git a/src/frameworks/javascript-node/utils.ts b/src/frameworks/javascript-node/utils.ts new file mode 100644 index 00000000..56660895 --- /dev/null +++ b/src/frameworks/javascript-node/utils.ts @@ -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 = { + 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, +): Record { + return { + ...(packageJson.dependencies as Record | undefined), + ...(packageJson.devDependencies as Record | undefined), + }; +} + +export function detectServerFramework( + packageJson: Record, +): 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 | 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 | 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; +} diff --git a/src/frameworks/javascript-web/javascript-web-wizard-agent.ts b/src/frameworks/javascript-web/javascript-web-wizard-agent.ts index 177540c9..5f4885ef 100644 --- a/src/frameworks/javascript-web/javascript-web-wizard-agent.ts +++ b/src/frameworks/javascript-web/javascript-web-wizard-agent.ts @@ -11,6 +11,8 @@ import { detectJsPackageManager, detectBundler, hasIndexHtml, + detectVanillaWeb, + hasSrcDirectory, type JavaScriptContext, } from './utils'; import { detectNodePackageManagers } from '../../lib/detection/package-manager'; @@ -27,7 +29,16 @@ export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig = { 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, + }); }, }, @@ -39,8 +50,11 @@ export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig = { 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 @@ -52,9 +66,8 @@ export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig = { 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; @@ -67,9 +80,6 @@ export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig = { '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; } @@ -95,27 +105,53 @@ export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig = { 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; }, }, @@ -124,6 +160,14 @@ export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig = { 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 [ diff --git a/src/frameworks/javascript-web/utils.ts b/src/frameworks/javascript-web/utils.ts index 7c7f83c3..78a1df6b 100644 --- a/src/frameworks/javascript-web/utils.ts +++ b/src/frameworks/javascript-web/utils.ts @@ -7,6 +7,9 @@ export type JavaScriptContext = { packageManagerName?: string; hasTypeScript?: boolean; hasBundler?: string; + isVanilla?: boolean; + htmlEntryPoint?: string; + hasSrcDir?: boolean; }; const INDEX_HTML_MAX_DEPTH = 6; @@ -65,7 +68,10 @@ export function detectBundler( path.join(options.installDir, 'package.json'), 'utf-8', ); - const pkg = JSON.parse(content); + const pkg = JSON.parse(content) as { + dependencies?: Record; + devDependencies?: Record; + }; const allDeps: Record = { ...pkg.dependencies, ...pkg.devDependencies, @@ -127,3 +133,49 @@ export function hasIndexHtml( return search(root, 0); } + +/** + * Detect a vanilla web project (HTML/CSS/JS with no package manager). + * Returns the path to the first HTML file found at the root, or undefined. + */ +export function detectVanillaWeb( + options: Pick, +): string | undefined { + const root = options.installDir; + + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(root, { withFileTypes: true }); + } catch { + return undefined; + } + + for (const entry of entries) { + if (entry.isFile() && entry.name.toLowerCase().endsWith('.html')) { + try { + const content = fs.readFileSync(path.join(root, entry.name), 'utf-8'); + if (/