Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 121 additions & 28 deletions src/helpers/shell-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,144 @@ import fs from 'fs';
import os from 'os';
import path from 'path';

// Function to get the history file based on the shell
function getHistoryFile(): string | null {
// Interface for shell history handlers
interface ShellHistoryHandler {
getHistoryFile(): string;
getLastCommand(historyFile: string): string | null;
appendCommand(historyFile: string, command: string): void;
}

// Base handler for simple newline-separated history files (bash, sh, ksh, tcsh)
class SimpleHistoryHandler implements ShellHistoryHandler {
constructor(private historyFilePath: string) {}

getHistoryFile(): string {
return this.historyFilePath;
}

getLastCommand(historyFile: string): string | null {
try {
const data = fs.readFileSync(historyFile, 'utf8');
const commands = data.trim().split('\n');
return commands[commands.length - 1];
} catch (err) {
// Ignore any errors
return null;
}
}

appendCommand(historyFile: string, command: string): void {
fs.appendFile(historyFile, `${command}\n`, (err) => {
if (err) {
// Ignore any errors
}
});
}
}

// Handler for zsh history which may include timestamps
class ZshHistoryHandler extends SimpleHistoryHandler {
getLastCommand(historyFile: string): string | null {
try {
const data = fs.readFileSync(historyFile, 'utf8');
const lines = data.trim().split('\n');

// Find the last non-empty line
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i].trim();
if (line) {
// Extract command from zsh history format (may have timestamp prefix)
const match = line.match(/^(?::\s*\d+:\d+;)?(.*)$/);
return match ? match[1] : line;
}
}
return null;
} catch (err) {
// Ignore any errors
return null;
}
}
}

// Handler for fish history which uses a YAML-like format
class FishHistoryHandler implements ShellHistoryHandler {
constructor(private historyFilePath: string) {}

getHistoryFile(): string {
return this.historyFilePath;
}

getLastCommand(historyFile: string): string | null {
try {
const data = fs.readFileSync(historyFile, 'utf8');
const lines = data.trim().split('\n');

// Fish history format is like:
// - cmd: command
// when: timestamp
let lastCommand = null;
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i].trim();
if (line.startsWith('- cmd:')) {
lastCommand = line.substring('- cmd:'.length).trim();
break;
}
}
return lastCommand;
} catch (err) {
// Ignore any errors
return null;
}
}

appendCommand(historyFile: string, command: string): void {
const timestamp = Math.floor(Date.now() / 1000);
const entry = `- cmd: ${command}\n when: ${timestamp}\n`;

fs.appendFile(historyFile, entry, (err) => {
if (err) {
// Ignore any errors
}
});
}
}

// Function to get the appropriate shell history handler
function getShellHistoryHandler(): ShellHistoryHandler | null {
const shell = process.env.SHELL || '';
const homeDir = os.homedir();
const shellName = path.basename(shell);

switch (path.basename(shell)) {
switch (shellName) {
case 'bash':
case 'sh':
return path.join(homeDir, '.bash_history');
return new SimpleHistoryHandler(path.join(homeDir, '.bash_history'));
case 'zsh':
return path.join(homeDir, '.zsh_history');
return new ZshHistoryHandler(path.join(homeDir, '.zsh_history'));
case 'fish':
return path.join(homeDir, '.local', 'share', 'fish', 'fish_history');
return new FishHistoryHandler(path.join(homeDir, '.local', 'share', 'fish', 'fish_history'));
case 'ksh':
return path.join(homeDir, '.ksh_history');
return new SimpleHistoryHandler(path.join(homeDir, '.ksh_history'));
case 'tcsh':
return path.join(homeDir, '.history');
return new SimpleHistoryHandler(path.join(homeDir, '.history'));
default:
return null;
}
}

// Function to get the last command from the history file
function getLastCommand(historyFile: string): string | null {
try {
const data = fs.readFileSync(historyFile, 'utf8');
const commands = data.trim().split('\n');
return commands[commands.length - 1];
} catch (err) {
// Ignore any errors
return null;
}
}

// Function to append the command to the history file if it's not the same as the last command
export function appendToShellHistory(command: string): void {
const historyFile = getHistoryFile();
if (historyFile) {
const lastCommand = getLastCommand(historyFile);
const handler = getShellHistoryHandler();
if (handler) {
const historyFile = handler.getHistoryFile();
const lastCommand = handler.getLastCommand(historyFile);

if (lastCommand !== command) {
fs.appendFile(historyFile, `${command}\n`, (err) => {
if (err) {
// Ignore any errors
}
});
try {
handler.appendCommand(historyFile, command);
} catch (err) {
// Ignore any errors
}
}
}
}
Loading