From 24d84c57da9f56403a2459d0467b52779457ed56 Mon Sep 17 00:00:00 2001 From: Paul Bouchon Date: Sun, 12 Jul 2026 08:37:17 -0400 Subject: [PATCH] repl: add .clearhistory command Add a `.clearhistory` REPL command that clears the in-memory session history and truncates the persisted history file when one is configured. Any pending debounced flush is cancelled first so it cannot rewrite the file after it has been cleared. Fixes: https://github.com/nodejs/node/issues/63905 Signed-off-by: Paul Bouchon --- doc/api/repl.md | 2 + lib/internal/repl/history.js | 27 +++++++++++++ lib/repl.js | 14 +++++++ test/parallel/test-repl-clearhistory.js | 52 +++++++++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 test/parallel/test-repl-clearhistory.js 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(); + })); + })); +}