diff --git a/doc/api/repl.md b/doc/api/repl.md index eceea9a4615ec3..55b11c406cc9cc 100644 --- a/doc/api/repl.md +++ b/doc/api/repl.md @@ -43,6 +43,8 @@ The following special commands are supported by all REPL instances: further input or processing of that expression. * `.clear`: Resets the REPL `context` to an empty object and clears any multi-line expression being input. +* `.clearhistory`: Clears the REPL session history, both in memory and in the + persistent history file when one is configured. * `.exit`: Close the I/O stream, causing the REPL to exit. * `.help`: Show this list of special commands. * `.save`: Save the current REPL session to a file: diff --git a/lib/internal/repl/history.js b/lib/internal/repl/history.js index b197c7049cc089..052219c809ab63 100644 --- a/lib/internal/repl/history.js +++ b/lib/internal/repl/history.js @@ -418,6 +418,33 @@ class ReplHistory { return this[kCloseHandle](); } + /** + * Clears the in-memory history and truncates the persisted history file. + * @returns {Promise} + */ + async clearHistory() { + // Cancel any pending debounced flush so it cannot rewrite the file after + // the history has been cleared. + if (this[kTimer]) { + clearTimeout(this[kTimer]); + this[kTimer] = null; + } + this[kPending] = false; + this[kIsFlushing] = false; + + this[kHistory].length = 0; + this[kIndex] = -1; + this[kContext].emit('history', this[kHistory]); + + if (this[kHistoryHandle] !== null) { + try { + await this[kHistoryHandle].truncate(0); + } catch (err) { + debug('Error truncating history file:', err); + } + } + } + [kReplHistoryMessage]() { if (this[kHistory].length === 0) { ReplHistory[kWriteToOutput]( diff --git a/lib/repl.js b/lib/repl.js index a00b7b3f372f38..5853665f36c23e 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -1370,6 +1370,20 @@ function defineDefaultCommands(repl) { }, }); + repl.defineCommand('clearhistory', { + help: 'Clear the REPL session history', + action: function() { + this.clearBufferedCommand(); + if (this.historyManager) { + this.historyManager.clearHistory(); + this.output.write('Clearing history...\n'); + } else { + this.output.write('History is not enabled for this REPL session\n'); + } + this.displayPrompt(); + }, + }); + repl.defineCommand('exit', { help: 'Exit the REPL', action: function() { diff --git a/test/parallel/test-repl-clearhistory.js b/test/parallel/test-repl-clearhistory.js new file mode 100644 index 00000000000000..97131654244193 --- /dev/null +++ b/test/parallel/test-repl-clearhistory.js @@ -0,0 +1,52 @@ +'use strict'; + +const common = require('../common'); +const { startNewREPLServer } = require('../common/repl'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Tests the `.clearhistory` REPL command, which clears the in-memory history +// and truncates the persisted history file. +// Refs: https://github.com/nodejs/node/issues/63905 + +// The command clears the in-memory history and resets the navigation index. +{ + const { replServer, input, output } = startNewREPLServer({ terminal: false }); + + replServer.history = ['const a = 1', 'a + 1', 'process.version']; + replServer.historyIndex = 2; + + input.run(['.clearhistory']); + + assert.strictEqual(replServer.history.length, 0); + assert.strictEqual(replServer.historyIndex, -1); + assert.match(output.accumulator, /Clearing history/); + + replServer.close(); +} + +// The command truncates the persisted history file. +{ + const filePath = path.resolve(tmpdir.path, '.node_repl_history_clear'); + fs.writeFileSync(filePath, 'const a = 1\na + 1\nprocess.version\n'); + + const { replServer, input } = startNewREPLServer({ terminal: false }); + + replServer.setupHistory({ filePath }, common.mustSucceed(() => { + assert.ok(replServer.history.length > 0); + assert.ok(fs.readFileSync(filePath, 'utf8').length > 0); + + input.run(['.clearhistory']); + assert.strictEqual(replServer.history.length, 0); + + // File truncation is asynchronous; await it before reading the file back. + replServer.historyManager.clearHistory().then(common.mustCall(() => { + assert.strictEqual(fs.readFileSync(filePath, 'utf8'), ''); + replServer.close(); + })); + })); +}