Fix issue where command history added to shell was incompatible with fish format, corrupting history#142
Fix issue where command history added to shell was incompatible with fish format, corrupting history#142mooz wants to merge 1 commit intoBuilderIO:mainfrom
Conversation
…Fish format, corrupting history
|
thanks @mooz - looks like build not passing though |
darshjme-codes
left a comment
There was a problem hiding this comment.
Excellent architecture! 🏛️ Proper abstraction for shell-specific formats.
Code Quality
The strategy pattern implementation is textbook-perfect:
- ✅
ShellHistoryHandlerinterface provides clean abstraction - ✅ Separate handlers for Simple, Zsh, and Fish formats
- ✅ Inheritance:
ZshHistoryHandler extends SimpleHistoryHandler(DRY) - ✅ Comprehensive shell coverage (bash, zsh, fish, ksh, tcsh)
Fish History Implementation
The Fish handler correctly uses the YAML-like format:
- cmd: command
when: timestampSuggestion: Fish's timestamp is in Unix seconds. The current implementation is correct (Math.floor(Date.now() / 1000)), but consider adding a comment:
const timestamp = Math.floor(Date.now() / 1000); // Unix timestamp in seconds (fish format)Edge Case: Fish history entries can also have a paths: field for command-specific context. While not required, consider documenting this limitation:
// Note: We only write cmd and when fields. Fish supports additional fields (paths, command_duration)
// but they're optional and not critical for basic history functionality.Zsh Timestamp Regex
The regex for zsh extended history is solid:
const match = line.match(/^(?::\s*\d+:\d+;)?(.*)$/);This handles both formats:
- Standard:
command - Extended:
: timestamp:duration;command
Suggestion: Add a test comment explaining the format:
// Zsh extended history format: ": timestamp:duration;command"
// Standard format: "command"
const match = line.match(/^(?::\s*\d+:\d+;)?(.*)$/);Potential Issue: Async File Append Race Condition
The appendCommand methods use async fs.appendFile without waiting:
appendCommand(historyFile: string, command: string): void {
fs.appendFile(historyFile, `${command}\n`, (err) => {
if (err) {
// Ignore any errors
}
});
}If appendToShellHistory() is called multiple times rapidly, writes may interleave incorrectly. Consider:
Option 1: Synchronous writes (simplest, acceptable for small writes):
appendCommand(historyFile: string, command: string): void {
try {
fs.appendFileSync(historyFile, `${command}\n`);
} catch (err) {
// Ignore errors
}
}Option 2: Queue with async/await (if async is important):
private writeQueue = Promise.resolve();
appendCommand(historyFile: string, command: string): void {
this.writeQueue = this.writeQueue.then(() =>
fs.promises.appendFile(historyFile, `${command}\n`).catch(() => {})
);
}Given history writes are rare (per command execution), synchronous writes are likely fine and simpler.
Error Handling: Silent Failures
All methods silently ignore errors. While this is reasonable for history (don't break the CLI if history fails), consider:
- Optional logging for debugging:
catch (err) {
if (process.env.DEBUG_SHELL_HISTORY) {
console.error('History write failed:', err);
}
}- Return success boolean for testing:
appendCommand(historyFile: string, command: string): boolean {
try {
fs.appendFileSync(historyFile, `${command}\n`);
return true;
} catch (err) {
return false;
}
}Testing Recommendations
- Fish format validation:
test('FishHistoryHandler writes correct YAML format', () => {
const handler = new FishHistoryHandler('/tmp/test_fish_history');
handler.appendCommand('/tmp/test_fish_history', 'echo test');
const content = fs.readFileSync('/tmp/test_fish_history', 'utf8');
expect(content).toMatch(/^- cmd: echo test\n when: \d+\n$/);
});- Zsh extended format parsing:
test('ZshHistoryHandler parses extended format', () => {
const testData = ': 1234567890:0;git status\n: 1234567891:5;npm test';
fs.writeFileSync('/tmp/test_zsh', testData);
const handler = new ZshHistoryHandler('/tmp/test_zsh');
expect(handler.getLastCommand('/tmp/test_zsh')).toBe('npm test');
});- Duplicate prevention:
test('appendToShellHistory prevents duplicates', () => {
// Write same command twice, verify only one entry added
});- Race condition test:
test('handles rapid successive appends', async () => {
for (let i = 0; i < 100; i++) {
appendToShellHistory(`command_${i}`);
}
// Verify all 100 commands are in history with no corruption
});Documentation
Add comments to the file explaining:
- The purpose of each handler (which shells they support)
- Why silent error handling is acceptable for history
- Examples of each shell's history format
Example:
/**
* Handles shell history file updates across different shells.
* Each shell uses a different format:
* - bash/sh/ksh/tcsh: Simple newline-separated commands
* - zsh: Optional timestamp prefix (": timestamp:duration;command")
* - fish: YAML-like format with "- cmd:" and " when:" fields
*/Overall Assessment
This is a production-ready fix that properly handles shell-specific formats. The main suggestion is using synchronous writes to avoid race conditions, and adding debug logging for troubleshooting. The architecture is clean and extensible for future shells.
Recommendation: Approve with minor sync/async consideration. Great refactoring! 🎉
Fix fish shell history format handling
Problem
The current implementation corrupts the fish shell history file by appending commands in an incorrect format. The fish shell history file uses a YAML-like format rather than simple newline-separated entries.
Solution
Implemented shell-specific handlers to properly read and write to each shell's history file according to its specific format.
Changes
ShellHistoryHandlerinterfaceSimpleHistoryHandlerclass for basic shells (bash, sh, ksh, tcsh)ZshHistoryHandlerclass for zsh (handling timestamp format)FishHistoryHandlerclass for fish (handling YAML-like format)Testing