UNPKG

humanbehavior-js

Version:

SDK for HumanBehavior session and event recording

1,590 lines (1,361 loc) 54 kB
/** * HumanBehavior SDK Auto-Installation Wizard * * This wizard automatically detects the user's framework and modifies their codebase * to integrate the SDK with minimal user intervention. */ import * as fs from 'fs'; import * as path from 'path'; export interface FrameworkInfo { name: string; type: 'react' | 'vue' | 'angular' | 'svelte' | 'nextjs' | 'nuxt' | 'remix' | 'vanilla' | 'astro' | 'gatsby' | 'node' | 'auto'; bundler?: 'vite' | 'webpack' | 'esbuild' | 'rollup'; packageManager?: 'npm' | 'yarn' | 'pnpm'; hasTypeScript?: boolean; hasRouter?: boolean; projectRoot?: string; version?: string; majorVersion?: number; features?: { hasReact18?: boolean; hasVue3?: boolean; hasNuxt3?: boolean; hasAngularStandalone?: boolean; hasNextAppRouter?: boolean; hasSvelteKit?: boolean; }; } export interface CodeModification { filePath: string; action: 'create' | 'modify' | 'append'; content: string; description: string; } export interface InstallationResult { success: boolean; framework: FrameworkInfo; modifications: CodeModification[]; errors: string[]; nextSteps: string[]; } export class AutoInstallationWizard { protected apiKey: string; protected projectRoot: string; protected framework: FrameworkInfo | null = null; constructor(apiKey: string, projectRoot: string = process.cwd()) { this.apiKey = apiKey; this.projectRoot = projectRoot; } /** * Simple version comparison utility */ private compareVersions(version1: string, version2: string): number { const v1Parts = version1.split('.').map(Number); const v2Parts = version2.split('.').map(Number); for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { const v1 = v1Parts[i] || 0; const v2 = v2Parts[i] || 0; if (v1 > v2) return 1; if (v1 < v2) return -1; } return 0; } private isVersionGte(version: string, target: string): boolean { return this.compareVersions(version, target) >= 0; } private getMajorVersion(version: string): number { return parseInt(version.split('.')[0]) || 0; } /** * Main installation method - detects framework and auto-installs */ async install(): Promise<InstallationResult> { try { // Step 1: Detect framework this.framework = await this.detectFramework(); // Step 2: Install package await this.installPackage(); // Step 3: Generate and apply code modifications const modifications = await this.generateModifications(); await this.applyModifications(modifications); // Step 4: Generate next steps const nextSteps = this.generateNextSteps(); return { success: true, framework: this.framework, modifications, errors: [], nextSteps }; } catch (error) { return { success: false, framework: this.framework || { name: 'unknown', type: 'vanilla' }, modifications: [], errors: [error instanceof Error ? error.message : 'Unknown error'], nextSteps: [] }; } } /** * Detect the current framework and project setup */ public async detectFramework(): Promise<FrameworkInfo> { const packageJsonPath = path.join(this.projectRoot, 'package.json'); if (!fs.existsSync(packageJsonPath)) { return { name: 'vanilla', type: 'vanilla', projectRoot: this.projectRoot }; } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; // Detect framework with version information let framework: FrameworkInfo = { name: 'vanilla', type: 'vanilla', projectRoot: this.projectRoot, features: {} }; if (dependencies.nuxt) { const nuxtVersion = dependencies.nuxt; const isNuxt3 = this.isVersionGte(nuxtVersion, '3.0.0'); framework = { name: 'nuxt', type: 'nuxt', version: nuxtVersion, majorVersion: this.getMajorVersion(nuxtVersion), hasTypeScript: !!dependencies.typescript, hasRouter: true, projectRoot: this.projectRoot, features: { hasNuxt3: isNuxt3 } }; } else if (dependencies.next) { const nextVersion = dependencies.next; const isNext13 = this.isVersionGte(nextVersion, '13.0.0'); framework = { name: 'nextjs', type: 'nextjs', version: nextVersion, majorVersion: this.getMajorVersion(nextVersion), hasTypeScript: !!dependencies.typescript || !!dependencies['@types/node'], hasRouter: true, projectRoot: this.projectRoot, features: { hasNextAppRouter: isNext13 } }; } else if (dependencies['@remix-run/react'] || dependencies['@remix-run/dev']) { const remixVersion = dependencies['@remix-run/react'] || dependencies['@remix-run/dev']; framework = { name: 'remix', type: 'remix', version: remixVersion, majorVersion: this.getMajorVersion(remixVersion), hasTypeScript: !!dependencies.typescript || !!dependencies['@types/react'], hasRouter: true, projectRoot: this.projectRoot, features: {} }; } else if (dependencies.react) { const reactVersion = dependencies.react; const isReact18 = this.isVersionGte(reactVersion, '18.0.0'); framework = { name: 'react', type: 'react', version: reactVersion, majorVersion: this.getMajorVersion(reactVersion), hasTypeScript: !!dependencies.typescript || !!dependencies['@types/react'], hasRouter: !!dependencies['react-router-dom'] || !!dependencies['react-router'], projectRoot: this.projectRoot, features: { hasReact18: isReact18 } }; } else if (dependencies.vue) { const vueVersion = dependencies.vue; const isVue3 = this.isVersionGte(vueVersion, '3.0.0'); framework = { name: 'vue', type: 'vue', version: vueVersion, majorVersion: this.getMajorVersion(vueVersion), hasTypeScript: !!dependencies.typescript || !!dependencies['@vue/cli-service'], hasRouter: !!dependencies['vue-router'], projectRoot: this.projectRoot, features: { hasVue3: isVue3 } }; } else if (dependencies['@angular/core']) { const angularVersion = dependencies['@angular/core']; const isAngular17 = this.isVersionGte(angularVersion, '17.0.0'); framework = { name: 'angular', type: 'angular', version: angularVersion, majorVersion: this.getMajorVersion(angularVersion), hasTypeScript: true, hasRouter: true, projectRoot: this.projectRoot, features: { hasAngularStandalone: isAngular17 } }; } else if (dependencies.svelte) { const svelteVersion = dependencies.svelte; const isSvelteKit = !!dependencies['@sveltejs/kit']; framework = { name: 'svelte', type: 'svelte', version: svelteVersion, majorVersion: this.getMajorVersion(svelteVersion), hasTypeScript: !!dependencies.typescript || !!dependencies['svelte-check'], hasRouter: !!dependencies['svelte-routing'] || !!dependencies['@sveltejs/kit'], projectRoot: this.projectRoot, features: { hasSvelteKit: isSvelteKit } }; } else if (dependencies.astro) { const astroVersion = dependencies.astro; framework = { name: 'astro', type: 'astro', version: astroVersion, majorVersion: this.getMajorVersion(astroVersion), hasTypeScript: !!dependencies.typescript || !!dependencies['@astrojs/ts-plugin'], hasRouter: true, projectRoot: this.projectRoot, features: {} }; } else if (dependencies.gatsby) { const gatsbyVersion = dependencies.gatsby; framework = { name: 'gatsby', type: 'gatsby', version: gatsbyVersion, majorVersion: this.getMajorVersion(gatsbyVersion), hasTypeScript: !!dependencies.typescript || !!dependencies['@types/react'], hasRouter: true, projectRoot: this.projectRoot, features: {} }; } // Detect bundler if (dependencies.vite) { framework.bundler = 'vite'; } else if (dependencies.webpack) { framework.bundler = 'webpack'; } else if (dependencies.esbuild) { framework.bundler = 'esbuild'; } else if (dependencies.rollup) { framework.bundler = 'rollup'; } // Detect package manager if (fs.existsSync(path.join(this.projectRoot, 'yarn.lock'))) { framework.packageManager = 'yarn'; } else if (fs.existsSync(path.join(this.projectRoot, 'pnpm-lock.yaml'))) { framework.packageManager = 'pnpm'; } else { framework.packageManager = 'npm'; } return framework; } /** * Install the SDK package */ protected async installPackage(): Promise<void> { const { execSync } = await import('child_process'); // Build base command let command = this.framework?.packageManager === 'yarn' ? 'yarn add humanbehavior-js' : this.framework?.packageManager === 'pnpm' ? 'pnpm add humanbehavior-js' : 'npm install humanbehavior-js'; // Add legacy peer deps flag for npm to handle dependency conflicts if (this.framework?.packageManager !== 'yarn' && this.framework?.packageManager !== 'pnpm') { command += ' --legacy-peer-deps'; } try { execSync(command, { cwd: this.projectRoot, stdio: 'inherit' }); } catch (error) { throw new Error(`Failed to install humanbehavior-js: ${error}`); } } /** * Generate code modifications based on framework */ protected async generateModifications(): Promise<CodeModification[]> { const modifications: CodeModification[] = []; switch (this.framework?.type) { case 'react': modifications.push(...await this.generateReactModifications()); break; case 'nextjs': modifications.push(...await this.generateNextJSModifications()); break; case 'nuxt': modifications.push(...await this.generateNuxtModifications()); break; case 'astro': modifications.push(...await this.generateAstroModifications()); break; case 'gatsby': modifications.push(...await this.generateGatsbyModifications()); break; case 'remix': modifications.push(...await this.generateRemixModifications()); break; case 'vue': modifications.push(...await this.generateVueModifications()); break; case 'angular': modifications.push(...await this.generateAngularModifications()); break; case 'svelte': modifications.push(...await this.generateSvelteModifications()); break; default: modifications.push(...await this.generateVanillaModifications()); } return modifications; } /** * Generate React-specific modifications */ private async generateReactModifications(): Promise<CodeModification[]> { const modifications: CodeModification[] = []; // Find main App component or index file const appFile = this.findReactAppFile(); if (appFile) { const content = fs.readFileSync(appFile, 'utf8'); const modifiedContent = this.injectReactProvider(content, appFile); modifications.push({ filePath: appFile, action: 'modify', content: modifiedContent, description: 'Added HumanBehaviorProvider to React app' }); } // Create or append to environment file modifications.push(this.createEnvironmentModification(this.framework!)); return modifications; } /** * Generate Next.js-specific modifications */ private async generateNextJSModifications(): Promise<CodeModification[]> { const modifications: CodeModification[] = []; // Check for App Router const appLayoutFile = path.join(this.projectRoot, 'src', 'app', 'layout.tsx'); const pagesLayoutFile = path.join(this.projectRoot, 'src', 'pages', '_app.tsx'); if (fs.existsSync(appLayoutFile)) { // Create providers.tsx file for App Router modifications.push({ filePath: path.join(this.projectRoot, 'src', 'app', 'providers.tsx'), action: 'create', content: `'use client'; import { HumanBehaviorProvider } from 'humanbehavior-js/react'; export function Providers({ children }: { children: React.ReactNode }) { return ( <HumanBehaviorProvider apiKey={process.env.NEXT_PUBLIC_HUMANBEHAVIOR_API_KEY}> {children} </HumanBehaviorProvider> ); }`, description: 'Created providers.tsx file for Next.js App Router' }); // Modify layout.tsx to use the provider const content = fs.readFileSync(appLayoutFile, 'utf8'); const modifiedContent = this.injectNextJSAppRouter(content); modifications.push({ filePath: appLayoutFile, action: 'modify', content: modifiedContent, description: 'Added Providers wrapper to Next.js App Router layout' }); } else if (fs.existsSync(pagesLayoutFile)) { // Create providers.tsx file for Pages Router modifications.push({ filePath: path.join(this.projectRoot, 'src', 'components', 'providers.tsx'), action: 'create', content: `'use client'; import { HumanBehaviorProvider } from 'humanbehavior-js/react'; export function Providers({ children }: { children: React.ReactNode }) { return ( <HumanBehaviorProvider apiKey={process.env.NEXT_PUBLIC_HUMANBEHAVIOR_API_KEY}> {children} </HumanBehaviorProvider> ); }`, description: 'Created providers.tsx file for Pages Router' }); // Modify _app.tsx to use the provider const content = fs.readFileSync(pagesLayoutFile, 'utf8'); const modifiedContent = this.injectNextJSPagesRouter(content); modifications.push({ filePath: pagesLayoutFile, action: 'modify', content: modifiedContent, description: 'Added Providers wrapper to Next.js Pages Router' }); } // Create or append to environment file modifications.push(this.createEnvironmentModification(this.framework!)); return modifications; } /** * Generate Astro-specific modifications */ private async generateAstroModifications(): Promise<CodeModification[]> { const modifications: CodeModification[] = []; // Create Astro component for HumanBehavior const astroComponentPath = path.join(this.projectRoot, 'src', 'components', 'HumanBehavior.astro'); const astroComponentContent = `--- // This component will only run on the client side --- <script> import { HumanBehaviorTracker } from 'humanbehavior-js'; // Get API key from environment variable const apiKey = import.meta.env.PUBLIC_HUMANBEHAVIOR_API_KEY; console.log('HumanBehavior: API key found:', apiKey ? 'Yes' : 'No'); if (apiKey) { try { const tracker = HumanBehaviorTracker.init(apiKey); console.log('HumanBehavior: Tracker initialized successfully'); // Test event to verify tracking is working setTimeout(() => { tracker.customEvent('astro_page_view', { page: window.location.pathname, framework: 'astro' }).then(() => { console.log('HumanBehavior: Test event sent successfully'); }).catch((error) => { console.error('HumanBehavior: Failed to send test event:', error); }); }, 1000); } catch (error) { console.error('HumanBehavior: Failed to initialize tracker:', error); } } else { console.error('HumanBehavior: No API key found'); } </script>`; modifications.push({ filePath: astroComponentPath, action: 'create', content: astroComponentContent, description: 'Created Astro component for HumanBehavior SDK' }); // Find and update layout file const layoutFiles = [ path.join(this.projectRoot, 'src', 'layouts', 'Layout.astro'), path.join(this.projectRoot, 'src', 'layouts', 'layout.astro'), path.join(this.projectRoot, 'src', 'layouts', 'BaseLayout.astro') ]; let layoutFile = null; for (const file of layoutFiles) { if (fs.existsSync(file)) { layoutFile = file; break; } } if (layoutFile) { const content = fs.readFileSync(layoutFile, 'utf8'); const modifiedContent = this.injectAstroLayout(content); modifications.push({ filePath: layoutFile, action: 'modify', content: modifiedContent, description: 'Added HumanBehavior component to Astro layout' }); } // Add environment variable modifications.push(this.createEnvironmentModification(this.framework!)); return modifications; } /** * Generate Nuxt-specific modifications */ private async generateNuxtModifications(): Promise<CodeModification[]> { const modifications: CodeModification[] = []; // Create plugin file for Nuxt (in app directory) const pluginFile = path.join(this.projectRoot, 'app', 'plugins', 'humanbehavior.client.ts'); modifications.push({ filePath: pluginFile, action: 'create', content: `import { HumanBehaviorTracker } from 'humanbehavior-js'; export default defineNuxtPlugin(() => { const config = useRuntimeConfig(); // Initialize HumanBehavior SDK (client-side only) if (typeof window !== 'undefined') { const apiKey = config.public.humanBehaviorApiKey; console.log('HumanBehavior: API key:', apiKey ? 'present' : 'missing'); if (apiKey) { try { const tracker = HumanBehaviorTracker.init(apiKey); console.log('HumanBehavior: Tracker initialized successfully'); } catch (error) { console.error('HumanBehavior: Failed to initialize tracker:', error); } } else { console.error('HumanBehavior: No API key found in runtime config'); } } });`, description: 'Created Nuxt plugin for HumanBehavior SDK in app directory' }); // Create environment configuration const nuxtConfigFile = path.join(this.projectRoot, 'nuxt.config.ts'); if (fs.existsSync(nuxtConfigFile)) { const content = fs.readFileSync(nuxtConfigFile, 'utf8'); const modifiedContent = this.injectNuxtConfig(content); modifications.push({ filePath: nuxtConfigFile, action: 'modify', content: modifiedContent, description: 'Added HumanBehavior runtime config to Nuxt config' }); } // Create or append to environment file modifications.push(this.createEnvironmentModification(this.framework!)); return modifications; } /** * Generate Remix-specific modifications */ private async generateRemixModifications(): Promise<CodeModification[]> { const modifications: CodeModification[] = []; // Find root.tsx file const rootFile = path.join(this.projectRoot, 'app', 'root.tsx'); if (fs.existsSync(rootFile)) { const content = fs.readFileSync(rootFile, 'utf8'); const modifiedContent = this.injectRemixProvider(content); modifications.push({ filePath: rootFile, action: 'modify', content: modifiedContent, description: 'Added HumanBehaviorProvider to Remix root component' }); } // Create or append to environment file modifications.push(this.createEnvironmentModification(this.framework!)); return modifications; } /** * Generate Vue-specific modifications */ private async generateVueModifications(): Promise<CodeModification[]> { const modifications: CodeModification[] = []; // Find main.js or main.ts const mainFile = this.findVueMainFile(); if (mainFile) { const content = fs.readFileSync(mainFile, 'utf8'); const modifiedContent = this.injectVuePlugin(content); modifications.push({ filePath: mainFile, action: 'modify', content: modifiedContent, description: 'Added HumanBehaviorPlugin to Vue app' }); } // Create or append to environment file modifications.push(this.createEnvironmentModification(this.framework!)); return modifications; } /** * Generate Angular-specific modifications */ private async generateAngularModifications(): Promise<CodeModification[]> { const modifications: CodeModification[] = []; // Check for modern Angular (standalone components) vs legacy (NgModule) const appModuleFile = path.join(this.projectRoot, 'src', 'app', 'app.module.ts'); const appComponentFile = path.join(this.projectRoot, 'src', 'app', 'app.ts'); const mainFile = path.join(this.projectRoot, 'src', 'main.ts'); const isModernAngular = fs.existsSync(appComponentFile) && !fs.existsSync(appModuleFile); if (isModernAngular) { // Modern Angular 17+ with standalone components if (fs.existsSync(mainFile)) { const content = fs.readFileSync(mainFile, 'utf8'); const modifiedContent = this.injectAngularStandaloneInit(content); modifications.push({ filePath: mainFile, action: 'modify', content: modifiedContent, description: 'Added HumanBehavior initialization to Angular main.ts' }); } } else if (fs.existsSync(appModuleFile)) { // Legacy Angular with NgModule const content = fs.readFileSync(appModuleFile, 'utf8'); const modifiedContent = this.injectAngularModule(content); modifications.push({ filePath: appModuleFile, action: 'modify', content: modifiedContent, description: 'Added HumanBehaviorModule to Angular app' }); } // Handle Angular environment file (legacy structure) const envFile = path.join(this.projectRoot, 'src', 'environments', 'environment.ts'); if (fs.existsSync(envFile)) { const content = fs.readFileSync(envFile, 'utf8'); if (!content.includes('humanBehaviorApiKey')) { const modifiedContent = content.replace( /export const environment = {([\s\S]*?)};/, `export const environment = { $1, humanBehaviorApiKey: process.env['HUMANBEHAVIOR_API_KEY'] || '' };` ); modifications.push({ filePath: envFile, action: 'modify', content: modifiedContent, description: 'Added API key to Angular environment' }); } } // Create or append to environment file modifications.push(this.createEnvironmentModification(this.framework!)); return modifications; } /** * Generate Svelte-specific modifications */ private async generateSvelteModifications(): Promise<CodeModification[]> { const modifications: CodeModification[] = []; // Check for SvelteKit const svelteConfigFile = path.join(this.projectRoot, 'svelte.config.js'); const isSvelteKit = fs.existsSync(svelteConfigFile); if (isSvelteKit) { // SvelteKit - create layout file const layoutFile = path.join(this.projectRoot, 'src', 'routes', '+layout.svelte'); if (fs.existsSync(layoutFile)) { const content = fs.readFileSync(layoutFile, 'utf8'); const modifiedContent = this.injectSvelteKitLayout(content); modifications.push({ filePath: layoutFile, action: 'modify', content: modifiedContent, description: 'Added HumanBehavior store to SvelteKit layout' }); } } else { // Regular Svelte - modify main file const mainFile = this.findSvelteMainFile(); if (mainFile) { const content = fs.readFileSync(mainFile, 'utf8'); const modifiedContent = this.injectSvelteStore(content); modifications.push({ filePath: mainFile, action: 'modify', content: modifiedContent, description: 'Added HumanBehavior store to Svelte app' }); } } // Create or append to environment file modifications.push(this.createEnvironmentModification(this.framework!)); return modifications; } /** * Generate vanilla JS/TS modifications */ private async generateVanillaModifications(): Promise<CodeModification[]> { const modifications: CodeModification[] = []; // Find HTML file to inject script const htmlFile = this.findHTMLFile(); if (htmlFile) { const content = fs.readFileSync(htmlFile, 'utf8'); const modifiedContent = this.injectVanillaScript(content); modifications.push({ filePath: htmlFile, action: 'modify', content: modifiedContent, description: 'Added HumanBehavior CDN script to HTML file' }); } // Create or append to environment file modifications.push(this.createEnvironmentModification(this.framework!)); return modifications; } /** * Generate Gatsby-specific modifications */ private async generateGatsbyModifications(): Promise<CodeModification[]> { const modifications: CodeModification[] = []; // Modify or create gatsby-browser.js for Gatsby const gatsbyBrowserFile = path.join(this.projectRoot, 'gatsby-browser.js'); if (fs.existsSync(gatsbyBrowserFile)) { const content = fs.readFileSync(gatsbyBrowserFile, 'utf8'); const modifiedContent = this.injectGatsbyBrowser(content); modifications.push({ filePath: gatsbyBrowserFile, action: 'modify', content: modifiedContent, description: 'Added HumanBehavior initialization to Gatsby browser' }); } else { // Create gatsby-browser.js if it doesn't exist modifications.push({ filePath: gatsbyBrowserFile, action: 'create', content: `import { HumanBehaviorTracker } from 'humanbehavior-js'; export const onClientEntry = () => { console.log('Gatsby browser entry point loaded'); const apiKey = process.env.GATSBY_HUMANBEHAVIOR_API_KEY; console.log('API Key found:', apiKey ? 'Yes' : 'No'); if (apiKey) { const tracker = HumanBehaviorTracker.init(apiKey); console.log('HumanBehavior SDK initialized for Gatsby'); } else { console.log('No API key found in environment variables'); } };`, description: 'Created gatsby-browser.js with HumanBehavior initialization' }); } // Create or append to environment file modifications.push(this.createEnvironmentModification(this.framework!)); return modifications; } /** * Apply modifications to the codebase */ protected async applyModifications(modifications: CodeModification[]): Promise<void> { for (const modification of modifications) { try { const dir = path.dirname(modification.filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } switch (modification.action) { case 'create': fs.writeFileSync(modification.filePath, modification.content); break; case 'modify': fs.writeFileSync(modification.filePath, modification.content); break; case 'append': fs.appendFileSync(modification.filePath, '\n' + modification.content); break; } } catch (error) { throw new Error(`Failed to apply modification to ${modification.filePath}: ${error}`); } } } /** * Generate next steps for the user */ private generateNextSteps(): string[] { const steps = [ '✅ SDK installed and configured automatically!', '🚀 Your app is now tracking user behavior', '📊 View sessions in your HumanBehavior dashboard', '🔧 Customize tracking in your code as needed' ]; if (this.framework?.type === 'react' || this.framework?.type === 'nextjs') { steps.push('💡 Use the useHumanBehavior() hook to track custom events'); } return steps; } // Helper methods for file detection and content injection private findReactAppFile(): string | null { const possibleFiles = [ 'src/App.jsx', 'src/App.js', 'src/App.tsx', 'src/App.ts', 'src/index.js', 'src/index.tsx', 'src/main.js', 'src/main.tsx' ]; for (const file of possibleFiles) { const fullPath = path.join(this.projectRoot, file); if (fs.existsSync(fullPath)) { return fullPath; } } return null; } private findVueMainFile(): string | null { const possibleFiles = [ 'src/main.js', 'src/main.ts', 'src/main.jsx', 'src/main.tsx' ]; for (const file of possibleFiles) { const fullPath = path.join(this.projectRoot, file); if (fs.existsSync(fullPath)) { return fullPath; } } return null; } private findSvelteMainFile(): string | null { const possibleFiles = [ 'src/main.js', 'src/main.ts', 'src/main.svelte' ]; for (const file of possibleFiles) { const fullPath = path.join(this.projectRoot, file); if (fs.existsSync(fullPath)) { return fullPath; } } return null; } private findHTMLFile(): string | null { const possibleFiles = ['index.html', 'public/index.html', 'dist/index.html']; for (const file of possibleFiles) { const fullPath = path.join(this.projectRoot, file); if (fs.existsSync(fullPath)) { return fullPath; } } return null; } private injectReactProvider(content: string, filePath: string): string { const isTypeScript = filePath.endsWith('.tsx') || filePath.endsWith('.ts'); // Check if already has HumanBehaviorProvider if (content.includes('HumanBehaviorProvider')) { return content; } // Determine the correct environment variable syntax based on bundler const isVite = this.framework?.bundler === 'vite'; const envVar = isVite ? 'import.meta.env.VITE_HUMANBEHAVIOR_API_KEY!' : 'process.env.HUMANBEHAVIOR_API_KEY!'; const importStatement = `import { HumanBehaviorProvider } from 'humanbehavior-js/react';`; // Enhanced parsing for React 18+ features const hasReact18 = this.framework?.features?.hasReact18; // Handle different React patterns if (content.includes('function App()') || content.includes('const App =')) { // Add import statement let modifiedContent = content.replace( /(import.*?from.*?['"]react['"];?)/, `$1\n${importStatement}` ); // If no React import found, add it at the top if (!modifiedContent.includes(importStatement)) { modifiedContent = `${importStatement}\n\n${modifiedContent}`; } // Wrap the App component return with HumanBehaviorProvider modifiedContent = modifiedContent.replace( /(return\s*\([\s\S]*?\)\s*;)/, `return ( <HumanBehaviorProvider apiKey={${envVar}}> $1 </HumanBehaviorProvider> );` ); return modifiedContent; } // Handle React 18+ createRoot pattern if (hasReact18 && content.includes('createRoot')) { let modifiedContent = content.replace( /(import.*?from.*?['"]react['"];?)/, `$1\n${importStatement}` ); if (!modifiedContent.includes(importStatement)) { modifiedContent = `${importStatement}\n\n${modifiedContent}`; } // Wrap the root render with HumanBehaviorProvider modifiedContent = modifiedContent.replace( /(root\.render\s*\([\s\S]*?\)\s*;)/, `root.render( <HumanBehaviorProvider apiKey={${envVar}}> $1 </HumanBehaviorProvider> );` ); return modifiedContent; } // Fallback: simple injection return `${importStatement}\n\n${content}`; } private injectNextJSAppRouter(content: string): string { if (content.includes('Providers')) { return content; } const importStatement = `import { Providers } from './providers';`; // First, add the import statement let modifiedContent = content.replace( /export default function RootLayout/, `${importStatement}\n\nexport default function RootLayout` ); // Then wrap the body content with Providers // Use a more specific approach to handle the body content modifiedContent = modifiedContent.replace( /<body([^>]*)>([\s\S]*?)<\/body>/, (match, bodyAttrs, bodyContent) => { // Trim whitespace and newlines from bodyContent const trimmedContent = bodyContent.trim(); return `<body${bodyAttrs}> <Providers> ${trimmedContent} </Providers> </body>`; } ); return modifiedContent; } private injectNextJSPagesRouter(content: string): string { if (content.includes('Providers')) { return content; } const importStatement = `import { Providers } from '../components/providers';`; return content.replace( /function MyApp/, `${importStatement}\n\nfunction MyApp` ).replace( /return \(([\s\S]*?)\);/, `return ( <Providers> $1 </Providers> );` ); } private injectRemixProvider(content: string): string { if (content.includes('HumanBehaviorProvider')) { return content; } const importStatement = `import { HumanBehaviorProvider, createHumanBehaviorLoader } from 'humanbehavior-js/remix';`; const useLoaderDataImport = `import { useLoaderData } from "@remix-run/react";`; // Add imports more robustly let modifiedContent = content; // Add HumanBehaviorProvider import - find the last import and add after it if (!content.includes('HumanBehaviorProvider')) { const lastImportIndex = modifiedContent.lastIndexOf('import'); if (lastImportIndex !== -1) { const nextLineIndex = modifiedContent.indexOf('\n', lastImportIndex); if (nextLineIndex !== -1) { modifiedContent = modifiedContent.slice(0, nextLineIndex + 1) + importStatement + '\n' + modifiedContent.slice(nextLineIndex + 1); } else { modifiedContent = modifiedContent + '\n' + importStatement; } } else { modifiedContent = importStatement + '\n' + modifiedContent; } } // Add useLoaderData import - find the last import and add after it if (!content.includes('useLoaderData')) { const lastImportIndex = modifiedContent.lastIndexOf('import'); if (lastImportIndex !== -1) { const nextLineIndex = modifiedContent.indexOf('\n', lastImportIndex); if (nextLineIndex !== -1) { modifiedContent = modifiedContent.slice(0, nextLineIndex + 1) + useLoaderDataImport + '\n' + modifiedContent.slice(nextLineIndex + 1); } else { modifiedContent = modifiedContent + '\n' + useLoaderDataImport; } } else { modifiedContent = useLoaderDataImport + '\n' + modifiedContent; } } // Add loader function before the App component if (!content.includes('export const loader')) { modifiedContent = modifiedContent.replace( /export default function App\(\)/, `export const loader = createHumanBehaviorLoader(); export default function App()` ); } // Wrap the App component content with HumanBehaviorProvider modifiedContent = modifiedContent.replace( /export default function App\(\) \{[\s\S]*?return \(([\s\S]*?)\);[\s\S]*?\}/, `export default function App() { const data = useLoaderData<typeof loader>(); return ( <HumanBehaviorProvider apiKey={data.ENV.HUMANBEHAVIOR_API_KEY}> $1 </HumanBehaviorProvider> ); }` ); return modifiedContent; } private injectVuePlugin(content: string): string { if (content.includes('HumanBehaviorPlugin')) { return content; } const importStatement = `import { HumanBehaviorPlugin } from 'humanbehavior-js/vue';`; // Enhanced Vue 3 support with version detection const hasVue3 = this.framework?.features?.hasVue3; if (hasVue3) { // Vue 3 with Composition API const pluginUsage = `app.use(HumanBehaviorPlugin, { apiKey: import.meta.env.VITE_HUMANBEHAVIOR_API_KEY });`; let modifiedContent = content; // Add import statement if (!content.includes(importStatement)) { modifiedContent = content.replace( /(import.*?from.*?['"]vue['"];?)/, `$1\n${importStatement}` ); // If no Vue import found, add it at the top if (!modifiedContent.includes(importStatement)) { modifiedContent = `${importStatement}\n\n${modifiedContent}`; } } // Handle createApp pattern if (content.includes('createApp')) { modifiedContent = modifiedContent.replace( /(app\.mount\(.*?\))/, `${pluginUsage}\n\n$1` ); } return modifiedContent; } else { // Vue 2 with Options API const pluginUsage = `Vue.use(HumanBehaviorPlugin, { apiKey: process.env.VUE_APP_HUMANBEHAVIOR_API_KEY });`; let modifiedContent = content; // Add import statement if (!content.includes(importStatement)) { modifiedContent = content.replace( /(import.*?from.*?['"]vue['"];?)/, `$1\n${importStatement}` ); if (!modifiedContent.includes(importStatement)) { modifiedContent = `${importStatement}\n\n${modifiedContent}`; } } // Handle new Vue pattern if (content.includes('new Vue')) { modifiedContent = modifiedContent.replace( /(new Vue\(.*?\))/, `${pluginUsage}\n\n$1` ); } return modifiedContent; } } private injectAngularModule(content: string): string { if (content.includes('HumanBehaviorModule')) { return content; } const importStatement = `import { HumanBehaviorModule } from 'humanbehavior-js/angular';`; const environmentImport = `import { environment } from '../environments/environment';`; // Add environment import if not present let modifiedContent = content; if (!content.includes('environment')) { modifiedContent = content.replace( /import.*from.*['"]@angular/, `${environmentImport}\n$&` ); } return modifiedContent.replace( /imports:\s*\[([\s\S]*?)\]/, `imports: [ $1, HumanBehaviorModule.forRoot({ apiKey: environment.humanBehaviorApiKey }) ]` ).replace( /import.*from.*['"]@angular/, `$&\n${importStatement}` ); } private injectAngularStandaloneInit(content: string): string { if (content.includes('initializeHumanBehavior')) { return content; } const importStatement = `import { initializeHumanBehavior } from 'humanbehavior-js/angular';`; // Add import at the top let modifiedContent = content.replace( /import.*from.*['"]@angular/, `${importStatement}\n$&` ); // Add initialization after bootstrapApplication modifiedContent = modifiedContent.replace( /(bootstrapApplication\([^}]+\}?\)(?:\s*\.catch[^;]+;)?)/, `$1 // Initialize HumanBehavior SDK (client-side only) if (typeof window !== 'undefined') { const tracker = initializeHumanBehavior( '${this.apiKey}' ); }` ); return modifiedContent; } private injectSvelteStore(content: string): string { if (content.includes('humanBehaviorStore')) { return content; } const importStatement = `import { humanBehaviorStore } from 'humanbehavior-js/svelte';`; const initCode = `humanBehaviorStore.init(process.env.PUBLIC_HUMANBEHAVIOR_API_KEY || '');`; return `${importStatement}\n${initCode}\n\n${content}`; } private injectSvelteKitLayout(content: string): string { if (content.includes('humanBehaviorStore')) { return content; } const importStatement = `import { humanBehaviorStore } from 'humanbehavior-js/svelte';`; const envImport = `import { PUBLIC_HUMANBEHAVIOR_API_KEY } from '$env/static/public';`; const initCode = `humanBehaviorStore.init(PUBLIC_HUMANBEHAVIOR_API_KEY || '');`; // Add to script section - handle different script tag patterns if (content.includes('<script lang="ts">')) { return content.replace( /<script lang="ts">/, `<script lang="ts">\n\t${envImport}\n\t${importStatement}\n\t${initCode}` ); } else if (content.includes('<script>')) { return content.replace( /<script>/, `<script>\n\t${envImport}\n\t${importStatement}\n\t${initCode}` ); } else if (content.includes('<script context="module">')) { return content.replace( /<script\s+context="module">/, `<script context="module">\n\t${envImport}\n\t${importStatement}\n\t${initCode}` ); } else { // If no script tag found, add one at the beginning return content.replace( /<svelte:head>/, `<script lang="ts">\n\t${envImport}\n\t${importStatement}\n\t${initCode}\n</script>\n\n<svelte:head>` ); } } private injectVanillaScript(content: string): string { if (content.includes('humanbehavior-js')) { return content; } const cdnScript = `<script src="https://unpkg.com/humanbehavior-js@latest/dist/index.min.js"></script>`; const initScript = `<script> // Initialize HumanBehavior SDK // Note: For vanilla HTML, the API key must be hardcoded since env vars aren't available const tracker = HumanBehaviorTracker.init('${this.apiKey}'); </script>`; return content.replace( /<\/head>/, ` ${cdnScript}\n ${initScript}\n</head>` ); } /** * Inject Astro layout with HumanBehavior component */ private injectAstroLayout(content: string): string { // Check if HumanBehavior component is already imported if (content.includes('HumanBehavior') || content.includes('humanbehavior-js')) { return content; // Already has HumanBehavior } // Add import inside frontmatter if not present let modifiedContent = content; if (!content.includes('import HumanBehavior')) { const importStatement = 'import HumanBehavior from \'../components/HumanBehavior.astro\';'; const frontmatterEndIndex = content.indexOf('---', 3); if (frontmatterEndIndex !== -1) { // Insert import inside frontmatter, before the closing --- modifiedContent = content.slice(0, frontmatterEndIndex) + '\n' + importStatement + '\n' + content.slice(frontmatterEndIndex); } else { // No frontmatter, add at the very beginning modifiedContent = '---\n' + importStatement + '\n---\n\n' + content; } } // Find the closing </body> tag and add HumanBehavior component before it const bodyCloseIndex = modifiedContent.lastIndexOf('</body>'); if (bodyCloseIndex === -1) { // No body tag found, append to end return modifiedContent + '\n\n<HumanBehavior />'; } // Add component before closing body tag return modifiedContent.slice(0, bodyCloseIndex) + ' <HumanBehavior />\n' + modifiedContent.slice(bodyCloseIndex); } private injectNuxtConfig(content: string): string { if (content.includes('humanBehaviorApiKey')) { return content; } // Enhanced Nuxt 3 support with version detection const hasNuxt3 = this.framework?.features?.hasNuxt3; if (hasNuxt3) { // Nuxt 3 with runtime config return content.replace( /export default defineNuxtConfig\(\{/, `export default defineNuxtConfig({ runtimeConfig: { public: { humanBehaviorApiKey: process.env.NUXT_PUBLIC_HUMANBEHAVIOR_API_KEY } },` ); } else { // Nuxt 2 with env config return content.replace( /export default \{/, `export default { env: { humanBehaviorApiKey: process.env.HUMANBEHAVIOR_API_KEY },` ); } } private injectGatsbyLayout(content: string): string { if (content.includes('HumanBehavior')) { return content; } const importStatement = `import HumanBehavior from './HumanBehavior';`; const componentUsage = `<HumanBehavior apiKey={process.env.GATSBY_HUMANBEHAVIOR_API_KEY || ''} />`; // Add import at the top let modifiedContent = content.replace( /import.*from.*['"]\./, `${importStatement}\n$&` ); // Add component before closing body tag modifiedContent = modifiedContent.replace( /(\s*<\/body>)/, `\n ${componentUsage}\n$1` ); return modifiedContent; } private injectGatsbyBrowser(content: string): string { if (content.includes('HumanBehaviorTracker')) { return content; } const importStatement = `import { HumanBehaviorTracker } from 'humanbehavior-js';`; const initCode = ` // Initialize HumanBehavior SDK export const onClientEntry = () => { console.log('Gatsby browser entry point loaded'); const apiKey = process.env.GATSBY_HUMANBEHAVIOR_API_KEY; console.log('API Key found:', apiKey ? 'Yes' : 'No'); if (apiKey) { const tracker = HumanBehaviorTracker.init(apiKey); console.log('HumanBehavior SDK initialized for Gatsby'); } else { console.log('No API key found in environment variables'); } };`; // If the file already has content, add the import and init code if (content.trim()) { return `${importStatement}${initCode}\n\n${content}`; } else { // If file is empty, just return the new content return `${importStatement}${initCode}`; } } /** * Helper method to find the best environment file for a framework */ private findBestEnvFile(framework: FrameworkInfo): { filePath: string; envVarName: string } { const possibleEnvFiles = [ '.env.local', '.env.development.local', '.env.development', '.env.local.development', '.env', '.env.production', '.env.staging' ]; // Framework-specific environment variable names const getEnvVarName = (framework: FrameworkInfo) => { // Handle React+Vite specifically if (framework.type === 'react' && framework.bundler === 'vite') { return 'VITE_HUMANBEHAVIOR_API_KEY'; } // Framework-specific mappings const envVarNames = { react: 'HUMANBEHAVIOR_API_KEY', nextjs: 'NEXT_PUBLIC_HUMANBEHAVIOR_API_KEY', vue: 'VITE_HUMANBEHAVIOR_API_KEY', svelte: 'PUBLIC_HUMANBEHAVIOR_API_KEY', angular: 'HUMANBEHAVIOR_API_KEY', nuxt: 'NUXT_PUBLIC_HUMANBEHAVIOR_API_KEY', remix: 'HUMANBEHAVIOR_API_KEY', vanilla: 'HUMANBEHAVIOR_API_KEY', astro: 'PUBLIC_HUMANBEHAVIOR_API_KEY', gatsby: 'GATSBY_HUMANBEHAVIOR_API_KEY', node: 'HUMANBEHAVIOR_API_KEY', auto: 'HUMANBEHAVIOR_API_KEY' }; return envVarNames[framework.type] || 'HUMANBEHAVIOR_API_KEY'; }; const envVarName = getEnvVarName(framework); // Check for existing files for (const envFile of possibleEnvFiles) { const fullPath = path.join(this.projectRoot, envFile); if (fs.existsSync(fullPath)) { return { filePath: fullPath, envVarName }; } } // Framework-specific default file creation const defaultFiles = { react: '.env.local', nextjs: '.env.local', vue: '.env.local', svelte: '.env', angular: '.env', nuxt: '.env', remix: '.env.local', vanilla: '.env', astro: '.env', gatsby: '.env.development', node: '.env', auto: '.env' }; const defaultFile = defaultFiles[framework.type] || '.env'; return { filePath: path.join(this.projectRoot, defaultFile), envVarName }; } /** * Helper method to create or append to environment files */ private createEnvironmentModification(framework: FrameworkInfo): CodeModification { const { filePath, envVarName } = this.findBestEnvFile(framework); // Clean the API key to prevent formatting issues const cleanApiKey = this.apiKey.trim(); if (fs.existsSync(filePath)) { // Check if the variable already exists const content = fs.readFileSync(filePath, 'utf8'); if (content.includes(envVarName)) { // Variable exists, don't modify return { filePath, action: 'modify', content: content, // No change description: `API key already exists in ${path.basename(filePath)}` }; } else { // Append to existing file return { filePath, action: 'append', content: `\n${envVarName}=${cleanApiKey}`, description: `Added API key to existing ${path.basename(filePath)}` }; } } else { // Create new file return { filePath, action: 'create', content: `${envVarName}=${cleanApiKey}`, description: `Created ${path.basename(filePath)} with API key` }; } } } /** * Browser-based auto-installation wizard */ export class BrowserAutoInstallationWizard { private apiKey: string; constructor(apiKey: string) { this.apiKey = apiKey; } async install(): Promise<InstallationResult> { try { // Detect framework in browser const framework = this.detectFram