diff --git a/README.md b/README.md
index 1b010a6..50179fc 100644
--- a/README.md
+++ b/README.md
@@ -106,9 +106,12 @@ By integrating the loopback-console you also gain the ability to configure its f
The following configuration directives are supported,
- `quiet`: Suppresses the help text on startup and the automatic printing of `result`.
+- `historyPath`: The path to a file to persist command history. Use an empty string (`''`) to disable history.
- All built-in configuration options for Node.js REPL, such as `prompt`.
- `handles`: Disable any default handles, or pass additional handles that you would like available on the console.
+Note, command history path can also be configured with the env-var `LOOPBACK_CONSOLE_HISTORY`.
+
## Contributors
- Heath Morrison (doublemarked)
diff --git a/history.js b/history.js
new file mode 100644
index 0000000..6f5e359
--- /dev/null
+++ b/history.js
@@ -0,0 +1,185 @@
+/*
+ * HM 2017-01-04:
+ * History processing was extracted from the internal Node REPL code with minimal changes,
+ * https://github.com/nodejs/node/blob/master/lib/internal/repl.js
+ *
+ * This internal history functionality is currently unpublished for various reasons
+ * discussed in the following PR:
+ * https://github.com/nodejs/node/pull/5789
+ *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ *
+ * All extracted code remains under the general nodejs license terms:
+ * https://github.com/nodejs/node/blob/master/LICENSE
+ *
+ * Copyright Node.js contributors. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ */
+
+'use strict';
+
+const Interface = require('readline').Interface;
+const path = require('path');
+const fs = require('fs');
+const os = require('os');
+const util = require('util');
+const debug = util.debuglog('repl');
+
+// XXX(chrisdickinson): The 15ms debounce value is somewhat arbitrary.
+// The debounce is to guard against code pasted into the REPL.
+const kDebounceHistoryMS = 15;
+
+const DEFAULT_HISTORY_FILE = '.loopback_console_history';
+
+module.exports = function initReplHistory(repl, historyPath) {
+ return new Promise((resolve, reject) => {
+ setupHistory(repl, historyPath, (err, repl) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(repl);
+ }
+ });
+ });
+};
+
+function setupHistory(repl, historyPath, ready) {
+ // Empty string disables persistent history.
+ if (typeof historyPath === 'string')
+ historyPath = historyPath.trim();
+
+ if (historyPath === '') {
+ repl._historyPrev = _replHistoryMessage;
+ return ready(null, repl);
+ }
+
+ if (!historyPath) {
+ try {
+ historyPath = path.join(os.homedir(), DEFAULT_HISTORY_FILE);
+ } catch (err) {
+ repl._writeToOutput('\nError: Could not get the home directory.\n' +
+ 'REPL session history will not be persisted.\n');
+ repl._refreshLine();
+
+ debug(err.stack);
+ repl._historyPrev = _replHistoryMessage;
+ return ready(null, repl);
+ }
+ }
+
+ var timer = null;
+ var writing = false;
+ var pending = false;
+ repl.pause();
+ // History files are conventionally not readable by others:
+ // https://github.com/nodejs/node/issues/3392
+ // https://github.com/nodejs/node/pull/3394
+ fs.open(historyPath, 'a+', 0o0600, oninit);
+
+ function oninit(err, hnd) {
+ if (err) {
+ // Cannot open history file.
+ // Don't crash, just don't persist history.
+ repl._writeToOutput('\nError: Could not open history file.\n' +
+ 'REPL session history will not be persisted.\n');
+ repl._refreshLine();
+ debug(err.stack);
+
+ repl._historyPrev = _replHistoryMessage;
+ repl.resume();
+ return ready(null, repl);
+ }
+ fs.close(hnd, onclose);
+ }
+
+ function onclose(err) {
+ if (err) {
+ return ready(err);
+ }
+ fs.readFile(historyPath, 'utf8', onread);
+ }
+
+ function onread(err, data) {
+ if (err) {
+ return ready(err);
+ }
+
+ if (data) {
+ repl.history = data.split(/[\n\r]+/, repl.historySize);
+ }
+
+ fs.open(historyPath, 'w', onhandle);
+ }
+
+ function onhandle(err, hnd) {
+ if (err) {
+ return ready(err);
+ }
+ repl._historyHandle = hnd;
+ repl.on('line', online);
+
+ // reading the file data out erases it
+ repl.once('flushHistory', function() {
+ repl.resume();
+ ready(null, repl);
+ });
+ flushHistory();
+ }
+
+ // ------ history listeners ------
+ function online() {
+ repl._flushing = true;
+
+ if (timer) {
+ clearTimeout(timer);
+ }
+
+ timer = setTimeout(flushHistory, kDebounceHistoryMS);
+ }
+
+ function flushHistory() {
+ timer = null;
+ if (writing) {
+ pending = true;
+ return;
+ }
+ writing = true;
+ const historyData = repl.history.join(os.EOL);
+ fs.write(repl._historyHandle, historyData, 0, 'utf8', onwritten);
+ }
+
+ function onwritten(err, data) {
+ writing = false;
+ if (pending) {
+ pending = false;
+ online();
+ } else {
+ repl._flushing = Boolean(timer);
+ if (!repl._flushing) {
+ repl.emit('flushHistory');
+ }
+ }
+ }
+};
+
+function _replHistoryMessage() {
+ if (this.history.length === 0) {
+ this._writeToOutput(
+ '\nPersistent history support disabled. ' +
+ 'Set the history path to ' +
+ 'a valid, user-writable path to enable.\n'
+ );
+ this._refreshLine();
+ }
+ this._historyPrev = Interface.prototype._historyPrev;
+ return this._historyPrev();
+}
diff --git a/index.js b/index.js
index e11d3c4..b44eb78 100644
--- a/index.js
+++ b/index.js
@@ -7,6 +7,7 @@ const DEFAULT_REPL_CONFIG = {
prompt: 'loopback > ',
useGlobal: true,
ignoreUndefined: true,
+ historyPath: process.env.LOOPBACK_CONSOLE_HISTORY,
};
const DEFAULT_HANDLE_INFO = {
@@ -24,7 +25,7 @@ const LoopbackConsole = module.exports = {
start(app, config) {
if (this._started) {
- return this._ctx;
+ return Promise.resolve(this._ctx);
}
this._started = true;
@@ -58,9 +59,10 @@ const LoopbackConsole = module.exports = {
console.log(repl.usage(ctx));
}
- ctx.repl = repl.start(ctx);
-
- return ctx;
+ return repl.start(ctx).then(repl => {
+ ctx.repl = repl;
+ return ctx;
+ });
},
};
diff --git a/package.json b/package.json
index 941786c..585b08b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "loopback-console",
- "version": "1.0.0",
+ "version": "1.1.0",
"description": "A command-line tool for Loopback app debugging and administration",
"main": "index.js",
"license": "MIT",
diff --git a/repl.js b/repl.js
index 6e77a75..154fd64 100644
--- a/repl.js
+++ b/repl.js
@@ -1,6 +1,7 @@
'use strict';
const repl = require('repl');
+const replHistory = require('./history');
const LoopbackRepl = module.exports = {
start(ctx) {
@@ -9,7 +10,6 @@ const LoopbackRepl = module.exports = {
Object.assign(replServer.context, ctx.handles);
- replServer.on('exit', process.exit);
replServer.eval = wrapReplEval(replServer);
if (ctx.handles.cb === true) {
@@ -43,7 +43,17 @@ const LoopbackRepl = module.exports = {
},
});
- return replServer;
+ replServer.on('exit', function() {
+ if (replServer._flushing) {
+ replServer.pause();
+ return replServer.once('flushHistory', function() {
+ process.exit();
+ });
+ }
+ process.exit();
+ });
+
+ return replHistory(replServer, config.historyPath).then(() => replServer);
},
usage(ctx, details) {