track-installation.js•11.7 kB
#!/usr/bin/env node
/**
 * Installation Source Tracking Script
 * Runs during npm install to detect how Desktop Commander was installed
 * 
 * Debug logging can be enabled with:
 * - DEBUG=desktop-commander npm install
 * - DEBUG=* npm install  
 * - NODE_ENV=development npm install
 * - DC_DEBUG=true npm install
 */
import { randomUUID } from 'crypto';
import * as https from 'https';
import { platform } from 'os';
import path from 'path';
import { fileURLToPath } from 'url';
// Get current file directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Debug logging utility - configurable via environment variables
const DEBUG_ENABLED = process.env.DEBUG === 'desktop-commander' || 
                      process.env.DEBUG === '*' || 
                      process.env.NODE_ENV === 'development' ||
                      process.env.DC_DEBUG === 'true';
const debug = (...args) => {
    if (DEBUG_ENABLED) {
        console.log('[Desktop Commander Debug]', ...args);
    }
};
const log = (...args) => {
    // Always show important messages, but prefix differently for debug vs production
    if (DEBUG_ENABLED) {
        console.log('[Desktop Commander]', ...args);
    }
};
/**
 * Get the client ID from the Desktop Commander config file, or generate a new one
 */
async function getClientId() {
    try {
        const { homedir } = await import('os');
        const { join } = await import('path');
        const fs = await import('fs');
        
        const USER_HOME = homedir();
        const CONFIG_DIR = join(USER_HOME, '.claude-server-commander');
        const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
        
        // Try to read existing config
        if (fs.existsSync(CONFIG_FILE)) {
            const configData = fs.readFileSync(CONFIG_FILE, 'utf8');
            const config = JSON.parse(configData);
            if (config.clientId) {
                debug(`Using existing clientId from config: ${config.clientId.substring(0, 8)}...`);
                return config.clientId;
            }
        }
        
        debug('No existing clientId found, generating new one');
        // Fallback to random UUID if config doesn't exist or lacks clientId
        return randomUUID();
    } catch (error) {
        debug(`Error reading config file: ${error.message}, using random UUID`);
        // If anything goes wrong, fall back to random UUID
        return randomUUID();
    }
}
// Google Analytics configuration (same as setup script)
const GA_MEASUREMENT_ID = 'G-NGGDNL0K4L';
const GA_API_SECRET = '5M0mC--2S_6t94m8WrI60A';
const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
/**
 * Detect installation source from environment and process context
 */
async function detectInstallationSource() {
    // Check npm environment variables for clues
    const npmConfigUserAgent = process.env.npm_config_user_agent || '';
    const npmExecpath = process.env.npm_execpath || '';
    const npmCommand = process.env.npm_command || '';
    const npmLifecycleEvent = process.env.npm_lifecycle_event || '';
    
    // Check process arguments and parent commands
    const processArgs = process.argv.join(' ');
    const processTitle = process.title || '';
    debug('Installation source detection...');
    debug(`npm_config_user_agent: ${npmConfigUserAgent}`);
    debug(`npm_execpath: ${npmExecpath}`);
    debug(`npm_command: ${npmCommand}`);
    debug(`npm_lifecycle_event: ${npmLifecycleEvent}`);
    debug(`process.argv: ${processArgs}`);
    debug(`process.title: ${processTitle}`);
    
    // Try to get parent process information
    let parentProcessInfo = null;
    try {
        const { execSync } = await import('child_process');
        const ppid = process.ppid;
        if (ppid && process.platform !== 'win32') {
            // Get parent process command line on Unix systems
            const parentCmd = execSync(`ps -p ${ppid} -o command=`, { encoding: 'utf8' }).trim();
            parentProcessInfo = parentCmd;
            debug(`parent process: ${parentCmd}`);
        }
    } catch (error) {
        debug(`Could not get parent process info: ${error.message}`);
    }
    
    // Smithery detection - look for smithery in the process chain
    const smitheryIndicators = [
        npmConfigUserAgent.includes('smithery'),
        npmExecpath.includes('smithery'),
        processArgs.includes('smithery'),
        processArgs.includes('@smithery/cli'),
        processTitle.includes('smithery'),
        parentProcessInfo && parentProcessInfo.includes('smithery'),
        parentProcessInfo && parentProcessInfo.includes('@smithery/cli')
    ];
    
    if (smitheryIndicators.some(indicator => indicator)) {
        return {
            source: 'smithery',
            details: {
                detection_method: 'process_chain',
                user_agent: npmConfigUserAgent,
                exec_path: npmExecpath,
                command: npmCommand,
                parent_process: parentProcessInfo || 'unknown',
                process_args: processArgs
            }
        };
    }
    
    // Direct NPX usage
    if (npmCommand === 'exec' || processArgs.includes('npx')) {
        return {
            source: 'npx-direct',
            details: {
                user_agent: npmConfigUserAgent,
                command: npmCommand,
                lifecycle_event: npmLifecycleEvent
            }
        };
    }
    
    // Regular npm install
    if (npmCommand === 'install' || npmLifecycleEvent === 'postinstall') {
        return {
            source: 'npm-install',
            details: {
                user_agent: npmConfigUserAgent,
                command: npmCommand,
                lifecycle_event: npmLifecycleEvent
            }
        };
    }
    
    // GitHub Codespaces
    if (process.env.CODESPACES) {
        return {
            source: 'github-codespaces',
            details: {
                codespace: process.env.CODESPACE_NAME || 'unknown'
            }
        };
    }
    
    // VS Code
    if (process.env.VSCODE_PID || process.env.TERM_PROGRAM === 'vscode') {
        return {
            source: 'vscode',
            details: {
                term_program: process.env.TERM_PROGRAM,
                vscode_pid: process.env.VSCODE_PID
            }
        };
    }
    
    // GitPod
    if (process.env.GITPOD_WORKSPACE_ID) {
        return {
            source: 'gitpod',
            details: {
                workspace_id: process.env.GITPOD_WORKSPACE_ID.substring(0, 8) + '...' // Truncate for privacy
            }
        };
    }
    
    // CI/CD environments
    if (process.env.CI) {
        if (process.env.GITHUB_ACTIONS) {
            return {
                source: 'github-actions',
                details: {
                    repository: process.env.GITHUB_REPOSITORY,
                    workflow: process.env.GITHUB_WORKFLOW
                }
            };
        }
        if (process.env.GITLAB_CI) {
            return {
                source: 'gitlab-ci',
                details: {
                    project: process.env.CI_PROJECT_NAME
                }
            };
        }
        if (process.env.JENKINS_URL) {
            return {
                source: 'jenkins',
                details: {
                    job: process.env.JOB_NAME
                }
            };
        }
        return {
            source: 'ci-cd-other',
            details: {
                ci_env: 'unknown'
            }
        };
    }
    
    // Docker detection
    if (process.env.DOCKER_CONTAINER) {
        return {
            source: 'docker',
            details: {
                container_id: process.env.HOSTNAME?.substring(0, 8) + '...' || 'unknown'
            }
        };
    }
    
    // Check for .dockerenv file (need to use fs import)
    try {
        const fs = await import('fs');
        if (fs.existsSync('/.dockerenv')) {
            return {
                source: 'docker',
                details: {
                    container_id: process.env.HOSTNAME?.substring(0, 8) + '...' || 'unknown'
                }
            };
        }
    } catch (error) {
        // Ignore fs errors
    }
    
    // Default fallback
    return {
        source: 'unknown',
        details: {
            user_agent: npmConfigUserAgent || 'none',
            command: npmCommand || 'none',
            lifecycle: npmLifecycleEvent || 'none'
        }
    };
}
/**
 * Send installation tracking to analytics
 */
async function trackInstallation(installationData) {
    if (!GA_MEASUREMENT_ID || !GA_API_SECRET) {
        debug('Analytics not configured, skipping tracking');
        return;
    }
    try {
        const uniqueUserId = await getClientId();
        log("user id", uniqueUserId)
        // Prepare GA4 payload
        const payload = {
            client_id: uniqueUserId,
            non_personalized_ads: false,
            timestamp_micros: Date.now() * 1000,
            events: [{
                name: 'package_installed',
                params: {
                    timestamp: new Date().toISOString(),
                    platform: platform(),
                    installation_source: installationData.source,
                    installation_details: JSON.stringify(installationData.details),
                    package_name: '@wonderwhy-er/desktop-commander',
                    install_method: 'npm-lifecycle',
                    node_version: process.version,
                    npm_version: process.env.npm_version || 'unknown'
                }
            }]
        };
        const postData = JSON.stringify(payload);
        
        const options = {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(postData)
            }
        };
        await new Promise((resolve, reject) => {
            const req = https.request(GA_BASE_URL, options);
            
            const timeoutId = setTimeout(() => {
                req.destroy();
                reject(new Error('Request timeout'));
            }, 5000);
            req.on('error', (error) => {
                clearTimeout(timeoutId);
                debug(`Analytics error: ${error.message}`);
                resolve(); // Don't fail installation on analytics error
            });
            req.on('response', (res) => {
                clearTimeout(timeoutId);
                // Consume the response data to complete the request
                res.on('data', () => {}); // Ignore response data
                res.on('end', () => {
                    log(`Installation tracked: ${installationData.source}`);
                    resolve();
                });
            });
            req.write(postData);
            req.end();
        });
    } catch (error) {
        debug(`Failed to track installation: ${error.message}`);
        // Don't fail the installation process
    }
}
/**
 * Main execution
 */
async function main() {
    try {
        log('Package installation detected');
        
        const installationData = await detectInstallationSource();
        log(`Installation source: ${installationData.source}`);
        
        await trackInstallation(installationData);
        
    } catch (error) {
        debug(`Installation tracking error: ${error.message}`);
        // Don't fail the installation
    }
}
// Only run if this script is executed directly (not imported)
if (import.meta.url === `file://${process.argv[1]}`) {
    main();
}
export { detectInstallationSource, trackInstallation };