diff --git a/doc/api/repl.md b/doc/api/repl.md index 7a57af574422..bce3dbc781ea 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. +* `.history clear`: 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 85d79876a25d..d618da9cf109 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 4c227a0c17da..1a4a4c03f25e 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -1141,6 +1141,24 @@ function defineDefaultCommands(repl) { }, }); + repl.defineCommand('history', { + help: 'Manage the REPL session history, e.g. `.history clear`', + action: function(cmd) { + this.clearBufferedCommand(); + if (cmd === 'clear') { + 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'); + } + } else { + this.output.write('Usage: .history clear\n'); + } + this.displayPrompt(); + }, + }); + repl.defineCommand('exit', { help: 'Exit the REPL', action: function() { diff --git a/test/parallel/test-repl-definecommand.mjs b/test/parallel/test-repl-definecommand.mjs index e8c34db34fcb..fb6ef1c9c9d0 100644 --- a/test/parallel/test-repl-definecommand.mjs +++ b/test/parallel/test-repl-definecommand.mjs @@ -23,7 +23,7 @@ replServer.defineCommand('say2', function() { }); await run(['.help\n']); -assert.match(output.accumulator, /\n\.say1 {5}help for say1\n/); +assert.match(output.accumulator, /\n\.say1 {6}help for say1\n/); assert.match(output.accumulator, /\n\.say2\n/); await run(['.say1 node developer\n']); diff --git a/test/parallel/test-repl-history-clear.js b/test/parallel/test-repl-history-clear.js new file mode 100644 index 000000000000..b34085ca39e7 --- /dev/null +++ b/test/parallel/test-repl-history-clear.js @@ -0,0 +1,65 @@ +'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 `.history clear` 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(['.history clear']); + + assert.strictEqual(replServer.history.length, 0); + assert.strictEqual(replServer.historyIndex, -1); + assert.match(output.accumulator, /Clearing history/); + + replServer.close(); +} + +// A bare `.history` (or an unknown subcommand) prints usage and clears nothing. +{ + const { replServer, input, output } = startNewREPLServer({ terminal: false }); + + replServer.history = ['const a = 1']; + input.run(['.history']); + + assert.match(output.accumulator, /Usage: \.history clear/); + assert.strictEqual(replServer.history.length, 1); + + 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(['.history clear']); + 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(); + })); + })); +} diff --git a/test/parallel/test-repl.js b/test/parallel/test-repl.js index 95591ca1bfcb..5736fa2a49d3 100644 --- a/test/parallel/test-repl.js +++ b/test/parallel/test-repl.js @@ -444,6 +444,7 @@ const errorTests = [ /\.clear/, /\.exit/, /\.help/, + /\.history/, /\.load/, /\.save/, '',