lsh-framework
Version:
A powerful, extensible shell with advanced job management, database persistence, and modern CLI features
247 lines (246 loc) • 8.59 kB
JavaScript
/**
* Enhanced History System with Supabase Integration
* Extends the base history system with cloud persistence
*/
import HistorySystem from './history-system.js';
import DatabasePersistence from './database-persistence.js';
import * as os from 'os';
import * as path from 'path';
export class EnhancedHistorySystem extends HistorySystem {
databasePersistence;
enhancedConfig;
syncTimer;
pendingSync = false;
constructor(config = {}) {
const defaultConfig = {
maxSize: 10000,
filePath: path.join(os.homedir(), '.lsh_history'),
shareHistory: true,
ignoreDups: true,
ignoreSpace: true,
expireDuplicatesFirst: true,
enableCloudSync: true,
userId: undefined,
syncInterval: 30000, // 30 seconds
...config,
};
super(defaultConfig);
this.enhancedConfig = defaultConfig;
this.databasePersistence = new DatabasePersistence(defaultConfig.userId);
if (this.enhancedConfig.enableCloudSync) {
this.initializeCloudSync();
}
}
/**
* Initialize cloud synchronization
*/
async initializeCloudSync() {
try {
const isConnected = await this.databasePersistence.testConnection();
if (isConnected) {
console.log('✅ Cloud history sync enabled');
await this.loadCloudHistory();
this.startSyncTimer();
}
else {
console.log('⚠️ Cloud history sync disabled - database not available');
}
}
catch (error) {
console.error('Failed to initialize cloud sync:', error);
}
}
/**
* Load history from cloud database
*/
async loadCloudHistory() {
try {
const cloudEntries = await this.databasePersistence.getHistoryEntries(1000);
// Convert cloud entries to local format
const localEntries = cloudEntries.map((entry, index) => ({
lineNumber: index + 1,
command: entry.command,
timestamp: new Date(entry.timestamp).getTime(),
exitCode: entry.exit_code,
}));
// Merge with local history (avoid duplicates)
this.mergeHistoryEntries(localEntries);
}
catch (error) {
console.error('Failed to load cloud history:', error);
}
}
/**
* Merge cloud history entries with local history
*/
mergeHistoryEntries(cloudEntries) {
const localEntries = this.getAllEntries();
const mergedEntries = [];
// Create a map of local entries by command and timestamp
const localMap = new Map();
localEntries.forEach(entry => {
const key = `${entry.command}_${entry.timestamp}`;
localMap.set(key, entry);
});
// Add cloud entries that don't exist locally
cloudEntries.forEach(entry => {
const key = `${entry.command}_${entry.timestamp}`;
if (!localMap.has(key)) {
mergedEntries.push(entry);
}
});
// Add all local entries
mergedEntries.push(...localEntries);
// Sort by timestamp and update line numbers
mergedEntries.sort((a, b) => a.timestamp - b.timestamp);
mergedEntries.forEach((entry, index) => {
entry.lineNumber = index + 1;
});
// Update internal entries
this.entries = mergedEntries;
}
/**
* Start periodic synchronization timer
*/
startSyncTimer() {
this.syncTimer = setInterval(() => {
this.syncToCloud();
}, this.enhancedConfig.syncInterval);
}
/**
* Stop synchronization timer
*/
stopSyncTimer() {
if (this.syncTimer) {
clearInterval(this.syncTimer);
this.syncTimer = undefined;
}
}
/**
* Synchronize local history to cloud
*/
async syncToCloud() {
if (this.pendingSync || !this.enhancedConfig.enableCloudSync) {
return;
}
this.pendingSync = true;
try {
const localEntries = this.getAllEntries();
const recentEntries = localEntries.slice(-50); // Sync last 50 entries
for (const entry of recentEntries) {
await this.databasePersistence.saveHistoryEntry({
session_id: this.databasePersistence.getSessionId(),
command: entry.command,
working_directory: process.cwd(),
exit_code: entry.exitCode,
timestamp: new Date(entry.timestamp).toISOString(),
hostname: os.hostname(),
});
}
}
catch (error) {
console.error('Failed to sync history to cloud:', error);
}
finally {
this.pendingSync = false;
}
}
/**
* Override addCommand to include cloud sync
*/
addCommand(command, exitCode) {
super.addCommand(command, exitCode);
// Sync to cloud if enabled
if (this.enhancedConfig.enableCloudSync) {
this.syncToCloud().catch(error => {
console.error('Failed to sync command to cloud:', error);
});
}
}
/**
* Search history across local and cloud data
*/
async searchHistoryCloud(query, limit = 20) {
const localResults = this.searchHistory(query).slice(0, limit);
if (!this.enhancedConfig.enableCloudSync) {
return localResults;
}
try {
// Search cloud history
const cloudEntries = await this.databasePersistence.getHistoryEntries(1000);
const cloudResults = cloudEntries
.filter(entry => entry.command.toLowerCase().includes(query.toLowerCase()))
.slice(0, limit)
.map((entry, index) => ({
lineNumber: index + 1,
command: entry.command,
timestamp: new Date(entry.timestamp).getTime(),
exitCode: entry.exit_code,
}));
// Merge and deduplicate results
const allResults = [...localResults, ...cloudResults];
const uniqueResults = allResults.filter((entry, index, arr) => arr.findIndex(e => e.command === entry.command && e.timestamp === entry.timestamp) === index);
return uniqueResults.slice(0, limit);
}
catch (error) {
console.error('Failed to search cloud history:', error);
return localResults;
}
}
/**
* Get history statistics
*/
async getHistoryStats() {
const localEntries = this.getAllEntries();
let cloudEntries = 0;
if (this.enhancedConfig.enableCloudSync) {
try {
const cloudData = await this.databasePersistence.getHistoryEntries(10000);
cloudEntries = cloudData.length;
}
catch (error) {
console.error('Failed to get cloud history stats:', error);
}
}
// Calculate statistics
const commandCounts = new Map();
let totalLength = 0;
localEntries.forEach(entry => {
commandCounts.set(entry.command, (commandCounts.get(entry.command) || 0) + 1);
totalLength += entry.command.length;
});
const mostUsedCommand = Array.from(commandCounts.entries())
.sort(([, a], [, b]) => b - a)[0]?.[0] || '';
return {
localEntries: localEntries.length,
cloudEntries,
totalCommands: localEntries.length + cloudEntries,
mostUsedCommand,
averageCommandLength: localEntries.length > 0 ? totalLength / localEntries.length : 0,
};
}
/**
* Enable or disable cloud sync
*/
setCloudSyncEnabled(enabled) {
this.enhancedConfig.enableCloudSync = enabled;
if (enabled) {
this.initializeCloudSync();
}
else {
this.stopSyncTimer();
}
}
/**
* Cleanup resources
*/
destroy() {
this.stopSyncTimer();
if (this.enhancedConfig.enableCloudSync) {
this.syncToCloud().catch(error => {
console.error('Failed to final sync on destroy:', error);
});
}
}
}
export default EnhancedHistorySystem;