humanbehavior-js
Version:
SDK for HumanBehavior session and event recording
1,203 lines (1,182 loc) • 63.5 kB
JavaScript
#!/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) {