UNPKG

humanbehavior-js

Version:

SDK for HumanBehavior session and event recording

1,203 lines (1,182 loc) 63.5 kB
#!/usr/bin/env node import * as fs from 'fs'; import * as path from 'path'; import { execSync } from 'child_process'; import * as clack from '@clack/prompts'; /** * 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. */ class AutoInstallationWizard { constructor(apiKey, projectRoot = process.cwd()) { this.framework = null; this.manualNotes = []; this.apiKey = apiKey; this.projectRoot = projectRoot; } /** * Simple version comparison utility */ compareVersions(version1, version2) { 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; } isVersionGte(version, target) { return this.compareVersions(version, target) >= 0; } getMajorVersion(version) { return parseInt(version.split('.')[0]) || 0; } /** * Main installation method - detects framework and auto-installs */ async install() { 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 */ async detectFramework() { 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 = { 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 with latest version range */ async installPackage() { // Build base command with latest version range let command = this.framework?.packageManager === 'yarn' ? 'yarn add humanbehavior-js@latest' : this.framework?.packageManager === 'pnpm' ? 'pnpm add humanbehavior-js@latest' : 'npm install humanbehavior-js@latest'; // 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 */ async generateModifications() { const modifications = []; 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 */ async generateReactModifications() { const modifications = []; // 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 */ async generateNextJSModifications() { const modifications = []; // Check for App Router - try both with and without src directory const appLayoutFileWithSrc = path.join(this.projectRoot, 'src', 'app', 'layout.tsx'); const appLayoutFile = path.join(this.projectRoot, 'app', 'layout.tsx'); const pagesLayoutFileWithSrc = path.join(this.projectRoot, 'src', 'pages', '_app.tsx'); const pagesLayoutFile = path.join(this.projectRoot, 'pages', '_app.tsx'); // Determine which layout file exists and set paths accordingly let actualAppLayoutFile = null; let providersFilePath = null; if (fs.existsSync(appLayoutFileWithSrc)) { actualAppLayoutFile = appLayoutFileWithSrc; providersFilePath = path.join(this.projectRoot, 'src', 'app', 'providers.tsx'); } else if (fs.existsSync(appLayoutFile)) { actualAppLayoutFile = appLayoutFile; providersFilePath = path.join(this.projectRoot, 'app', 'providers.tsx'); } if (actualAppLayoutFile) { // Create providers.tsx file for App Router modifications.push({ filePath: providersFilePath, 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(actualAppLayoutFile, 'utf8'); const modifiedContent = this.injectNextJSAppRouter(content); modifications.push({ filePath: actualAppLayoutFile, action: 'modify', content: modifiedContent, description: 'Added Providers wrapper to Next.js App Router layout' }); } else if (fs.existsSync(pagesLayoutFileWithSrc) || fs.existsSync(pagesLayoutFile)) { const actualPagesLayoutFile = fs.existsSync(pagesLayoutFileWithSrc) ? pagesLayoutFileWithSrc : pagesLayoutFile; const providersPath = fs.existsSync(pagesLayoutFileWithSrc) ? path.join(this.projectRoot, 'src', 'components', 'providers.tsx') : path.join(this.projectRoot, 'components', 'providers.tsx'); const importPath = fs.existsSync(pagesLayoutFileWithSrc) ? '../components/providers' : './components/providers'; // Create providers.tsx file for Pages Router modifications.push({ filePath: providersPath, 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(actualPagesLayoutFile, 'utf8'); const modifiedContent = this.injectNextJSPagesRouter(content, importPath); modifications.push({ filePath: actualPagesLayoutFile, 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 */ async generateAstroModifications() { const modifications = []; // 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'; const apiKey = import.meta.env.PUBLIC_HUMANBEHAVIOR_API_KEY; if (apiKey) { HumanBehaviorTracker.init(apiKey); } </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 */ async generateNuxtModifications() { const modifications = []; // 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(); if (typeof window !== 'undefined') { const apiKey = config.public.humanBehaviorApiKey; if (apiKey) { HumanBehaviorTracker.init(apiKey); } } });`, description: 'Created Nuxt plugin for HumanBehavior SDK in app directory' }); // Create environment configuration const nuxtConfigFile = path.join(this.projectRoot, 'nuxt.config.ts'); { const mod = this.applyOrNotify(nuxtConfigFile, (c) => this.injectNuxtConfig(c), 'Added HumanBehavior runtime config to Nuxt config', 'Nuxt: Add inside defineNuxtConfig({ … }):\nruntimeConfig: { public: { humanBehaviorApiKey: process.env.NUXT_PUBLIC_HUMANBEHAVIOR_API_KEY } },'); if (mod) modifications.push(mod); } // Create or append to environment file modifications.push(this.createEnvironmentModification(this.framework)); return modifications; } /** * Generate Remix-specific modifications */ async generateRemixModifications() { const modifications = []; // 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 */ async generateVueModifications() { const modifications = []; // Find main.js or main.ts const mainFile = this.findVueMainFile(); // Create Vue composable per docs (idempotent) const composableDir = path.join(this.projectRoot, 'src', 'composables'); const composablePath = path.join(composableDir, 'useHumanBehavior.ts'); const composableContent = `import { HumanBehaviorTracker } from 'humanbehavior-js' export function useHumanBehavior() { const apiKey = import.meta.env.VITE_HUMANBEHAVIOR_API_KEY if (apiKey) { const tracker = HumanBehaviorTracker.init(apiKey); return { tracker } } return { tracker: null } } `; try { if (!fs.existsSync(composableDir)) { fs.mkdirSync(composableDir, { recursive: true }); } if (!fs.existsSync(composablePath)) { modifications.push({ filePath: composablePath, action: 'create', content: composableContent, description: 'Created Vue composable useHumanBehavior' }); } } catch { } 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 */ async generateAngularModifications() { const modifications = []; // Create Angular service (docs pattern) const serviceDir = path.join(this.projectRoot, 'src', 'app', 'services'); const servicePath = path.join(serviceDir, 'hb.service.ts'); const serviceContent = `import { Injectable, NgZone, Inject, PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; import { HumanBehaviorTracker } from 'humanbehavior-js'; import { environment } from '../../environments/environment'; @Injectable({ providedIn: 'root' }) export class HumanBehavior { private tracker: ReturnType<typeof HumanBehaviorTracker.init> | null = null; constructor(private ngZone: NgZone, @Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { this.ngZone.runOutsideAngular(() => { this.tracker = HumanBehaviorTracker.init(environment.humanBehaviorApiKey); }); } } capture(event: string, props?: Record<string, any>) { this.tracker?.customEvent(event, props); } identify(user: Record<string, any>) { this.tracker?.identifyUser({ userProperties: user }); } trackPageView(path?: string) { this.tracker?.trackPageView(path); } } `; if (!fs.existsSync(serviceDir)) { fs.mkdirSync(serviceDir, { recursive: true }); } if (!fs.existsSync(servicePath)) { modifications.push({ filePath: servicePath, action: 'create', content: serviceContent, description: 'Created Angular HumanBehavior service (singleton)' }); } // Handle Angular environment files (proper Angular way) const envFile = path.join(this.projectRoot, 'src', 'environments', 'environment.ts'); const envProdFile = path.join(this.projectRoot, 'src', 'environments', 'environment.prod.ts'); // Create environments directory if it doesn't exist const envDir = path.dirname(envFile); if (!fs.existsSync(envDir)) { fs.mkdirSync(envDir, { recursive: true }); } // Create or update development environment 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: '${this.apiKey}' };`); modifications.push({ filePath: envFile, action: 'modify', content: modifiedContent, description: 'Added API key to Angular development environment' }); } } else { // Create new development environment file modifications.push({ filePath: envFile, action: 'create', content: `export const environment = { production: false, humanBehaviorApiKey: '${this.apiKey}' };`, description: 'Created Angular development environment file' }); } // Create or update production environment if (fs.existsSync(envProdFile)) { const content = fs.readFileSync(envProdFile, 'utf8'); if (!content.includes('humanBehaviorApiKey')) { const modifiedContent = content.replace(/export const environment = {([\s\S]*?)};/, `export const environment = { $1, humanBehaviorApiKey: '${this.apiKey}' };`); modifications.push({ filePath: envProdFile, action: 'modify', content: modifiedContent, description: 'Added API key to Angular production environment' }); } } else { // Create new production environment file modifications.push({ filePath: envProdFile, action: 'create', content: `export const environment = { production: true, humanBehaviorApiKey: '${this.apiKey}' };`, description: 'Created Angular production environment file' }); } // For Angular, we don't need .env files since we use environment.ts // The environment files are already created above // Inject service into app component const appComponentPath = path.join(this.projectRoot, 'src', 'app', 'app.ts'); if (fs.existsSync(appComponentPath)) { const appContent = fs.readFileSync(appComponentPath, 'utf8'); // Check if already has HumanBehavior service if (!appContent.includes('HumanBehavior')) { let modifiedAppContent = appContent .replace(/import { Component } from '@angular\/core';/, `import { Component } from '@angular/core'; import { HumanBehavior } from './services/hb.service';`) .replace(/export class App {/, `export class App { constructor(private readonly humanBehavior: HumanBehavior) {}`); // Do not modify standalone setting; leave component decorator unchanged modifications.push({ action: 'modify', filePath: appComponentPath, content: modifiedAppContent, description: 'Injected HumanBehavior service into Angular app component' }); } } return modifications; } /** * Generate Svelte-specific modifications */ async generateSvelteModifications() { const modifications = []; // 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 tracker init 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 tracker init to Svelte app' }); } } // Create or append to environment file modifications.push(this.createEnvironmentModification(this.framework)); return modifications; } /** * Generate vanilla JS/TS modifications */ async generateVanillaModifications() { const modifications = []; // 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 */ async generateGatsbyModifications() { const modifications = []; // 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 = () => { const apiKey = process.env.GATSBY_HUMANBEHAVIOR_API_KEY; if (apiKey) { HumanBehaviorTracker.init(apiKey); } };`, 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 */ async applyModifications(modifications) { 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 */ generateNextSteps() { 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'); } // Append any manual notes gathered during transformation if (this.manualNotes.length) { steps.push(...this.manualNotes.map((n) => `⚠️ ${n}`)); } return steps; } /** * Helper: apply a file transform or record a manual instruction if unchanged/missing */ applyOrNotify(filePath, transform, description, manualNote) { if (!fs.existsSync(filePath)) { this.manualNotes.push(`${manualNote} (file missing: ${path.relative(this.projectRoot, filePath)})`); return null; } const original = fs.readFileSync(filePath, 'utf8'); const updated = transform(original); if (updated !== original) { return { filePath, action: 'modify', content: updated, description }; } this.manualNotes.push(manualNote); return null; } // Helper methods for file detection and content injection findReactAppFile() { 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; } findVueMainFile() { 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; } findSvelteMainFile() { 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; } findHTMLFile() { 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; } injectReactProvider(content, filePath) { 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.REACT_APP_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}`; } injectNextJSAppRouter(content) { 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; } injectNextJSPagesRouter(content, importPath = '../components/providers') { if (content.includes('Providers')) { return content; } const importStatement = `import { Providers } from '${importPath}';`; return content.replace(/function MyApp/, `${importStatement}\n\nfunction MyApp`).replace(/return \(([\s\S]*?)\);/, `return ( <Providers> $1 </Providers> );`); } injectRemixProvider(content) { if (content.includes('HumanBehaviorProvider')) { return content; } let modifiedContent = content; // Step 1: Add useLoaderData import if (!content.includes('useLoaderData')) { modifiedContent = modifiedContent.replace(/(} from ['"]@remix-run\/react['"];?\s*)/, `$1import { useLoaderData } from '@remix-run/react'; `); } // Step 2: Add HumanBehaviorProvider import if (!content.includes('HumanBehaviorProvider')) { modifiedContent = modifiedContent.replace(/(} from ['"]@remix-run\/react['"];?\s*)/, `$1import { HumanBehaviorProvider } from 'humanbehavior-js/react'; `); } // Step 3: Add LoaderFunctionArgs import if (!content.includes('LoaderFunctionArgs')) { modifiedContent = modifiedContent.replace(/(} from ['"]@remix-run\/node['"];?\s*)/, `$1import type { LoaderFunctionArgs } from '@remix-run/node'; `); } // Step 4: Add loader function before Layout function if (!content.includes('export const loader')) { modifiedContent = modifiedContent.replace(/(export function Layout)/, `export const loader = async ({ request }: LoaderFunctionArgs) => { return { ENV: { HUMANBEHAVIOR_API_KEY: process.env.HUMANBEHAVIOR_API_KEY, }, }; }; $1`); } // Step 5: Add useLoaderData call and wrap App function's return content with HumanBehaviorProvider if (!content.includes('const data = useLoaderData')) { modifiedContent = modifiedContent.replace(/(export default function App\(\) \{\s*)(return \(\s*<div[^>]*>[\s\S]*?<\/div>\s*\);\s*\})/, `$1const data = useLoaderData<typeof loader>(); return ( <HumanBehaviorProvider apiKey={data.ENV.HUMANBEHAVIOR_API_KEY}> <div className="min-h-screen bg-gray-50"> <Navigation /> <Outlet /> </div> </HumanBehaviorProvider> ); }`); } return modifiedContent; } injectVuePlugin(content) { // New: use composable/tracker pattern per docs; idempotent and migrates from plugin if (content.includes('useHumanBehavior')) { return content; } const hasVue3 = this.framework?.features?.hasVue3; const isVue3ByContent = content.includes('createApp') || content.includes('import { createApp }'); let modifiedContent = content .replace(/import\s*\{\s*HumanBehaviorPlugin\s*\}\s*from\s*['\"]humanbehavior-js\/vue['\"];?/g, '') .replace(/app\.use\(\s*HumanBehaviorPlugin[\s\S]*?\);?/g, ''); if (hasVue3 || isVue3ByContent) { const importComposable = `import { useHumanBehavior } from './composables/useHumanBehavior';`; if (!modifiedContent.includes(importComposable)) { const lastImportIndex = modifiedContent.lastIndexOf('import'); if (lastImportIndex !== -1) { const nextLineIndex = modifiedContent.indexOf('\n', lastImportIndex); if (nextLineIndex !== -1) { modifiedContent = modifiedContent.slice(0, nextLineIndex + 1) + importComposable + '\n' + modifiedContent.slice(nextLineIndex + 1); } else { modifiedContent = modifiedContent + '\n' + importComposable; } } else { modifiedContent = importComposable + '\n' + modifiedContent; } } if (modifiedContent.includes('createApp')) { modifiedContent = modifiedContent.replace(/(const\s+app\s*=\s*createApp\([^)]*\))/, `$1\nconst { tracker } = useHumanBehavior();`); } return modifiedContent; } else { const trackerImport = `import { HumanBehaviorTracker } from 'humanbehavior-js';`; if (!modifiedContent.includes(trackerImport)) { modifiedContent = `${trackerImport}\n${modifiedContent}`; } if (modifiedContent.includes('new Vue')) { modifiedContent = modifiedContent.replace(/(new\s+Vue\s*\()/, `HumanBehaviorTracker.init(process.env.VUE_APP_HUMANBEHAVIOR_API_KEY || import.meta?.env?.VITE_HUMANBEHAVIOR_API_KEY);\n$1`); } return modifiedContent; } } injectAngularModule(content) { 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}`); } injectAngularStandaloneInit(content) { if (content.includes('initializeHumanBehavior')) { return content; } const importStatement = `import { initializeHumanBehavior } from 'humanbehavior-js/angular';`; const environmentImport = `import { environment } from './environments/environment';`; // Add imports at the top let modifiedContent = content.replace(/import.*from.*['"]@angular/, `${importStatement}\n${environmentImport}\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(environment.humanBehaviorApiKey); }`); return modifiedContent; } injectSvelteStore(content) { // Direct tracker init for non-SSR Svelte if (content.includes('HumanBehaviorTracker.init')) { return content; } const importStatement = `import { HumanBehaviorTracker } from 'humanbehavior-js';`; const initCode = `// Initialize HumanBehavior SDK\nHumanBehaviorTracker.init(import.meta.env?.VITE_HUMANBEHAVIOR_API_KEY || process.env.PUBLIC_HUMANBEHAVIOR_API_KEY || '');`; return `${importStatement}\n${initCode}\n\n${content}`; } injectSvelteKitLayout(content) { // Direct tracker init with browser guard for SvelteKit if (content.includes('HumanBehaviorTracker.init')) { return content; } const envImport = `import { PUBLIC_HUMANBEHAVIOR_API_KEY } from '$env/static/public';`; const hbImport = `import { HumanBehaviorTracker } from 'humanbehavior-js';`; const browserImport = `import { browser } from '$app/environment';`; const initCode = `if (browser) {\n const apiKey = PUBLIC_HUMANBEHAVIOR_API_KEY || import.meta.env.VITE_HUMANBEHAVIOR_API_KEY;\n if (apiKey) {\n HumanBehaviorTracker.init(apiKey);\n }\n}`; if (content.includes('<script lang="ts">')) { return content.replace(/<script lang="ts">/, `<script lang="ts">\n\t${browserImport}\n\t${envImport}\n\t${hbImport}\n\t${initCode}`); } else if (content.includes('<script>')) { return content.replace(/<script>/, `<script>\n\t${browserImport}\n\t${envImport}\n\t${hbImport}\n\t${initCode}`); } else { return `<script lang="ts">\n${browserImport}\n${envImport}\n${hbImport}\n${initCode}\n</script>\n\n${content}`; } } injectVanillaScript(content) { 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 */ injectAstroLayout(content) { // 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) {