Create interactive console menus for REPL-style and ops Node.js apps. Register menu items with handlers, optional this owners, and typed arguments (string, numeric, bool).
npm install node-menunode-menu is a CommonJS package that exports a singleton menu instance (not a class). Bundled typings use export =.
With esModuleInterop: true (default in many modern tsconfigs):
import menu from 'node-menu';
menu
.addItem('Ping', () => console.log('pong'))
.start();Without esModuleInterop:
import menu = require('node-menu');In plain ESM (Node with "type": "module"), the same default import works via Node’s CJS interop:
import menu from 'node-menu';Do not call new on the import — the module already exports a ready-to-use instance. Use node-menu@1.3.7 or later for typings that match this singleton export.
Run the full example:
node examples/admin-jobs.jsA trimmed version of the same idea (full store + get-by-id live in examples/admin-jobs.js):
var menu = require('node-menu');
function JobStore() {
this._nextId = 1;
this._jobs = [];
}
JobStore.prototype.listJobs = function() {
this._jobs.forEach(function(job) {
console.log('#' + job.id + ' ' + job.name + ' [' + job.status + ']');
});
};
JobStore.prototype.enqueue = function(name, priority) {
var job = {
id: this._nextId++,
name: name,
priority: priority,
status: 'queued'
};
this._jobs.push(job);
console.log('Enqueued job #' + job.id);
};
JobStore.prototype.cancel = function(id) {
var job = null;
for (var i = 0; i < this._jobs.length; i++) {
if (this._jobs[i].id === id) {
job = this._jobs[i];
break;
}
}
if (!job) {
console.log('Job not found: ' + id);
return;
}
if (job.status === 'done' || job.status === 'cancelled') {
console.log('Cannot cancel job #' + id + ' (status=' + job.status + ')');
return;
}
job.status = 'cancelled';
console.log('Cancelled job #' + id);
};
JobStore.prototype.stats = function() {
var counts = { queued: 0, running: 0, done: 0, cancelled: 0 };
this._jobs.forEach(function(job) {
if (counts[job.status] !== undefined) {
counts[job.status]++;
}
});
console.log(
'queued=' + counts.queued +
' running=' + counts.running +
' done=' + counts.done +
' cancelled=' + counts.cancelled
);
};
var store = new JobStore();
menu
.addDelimiter('-', 40, 'Browse')
.addItem('List jobs', store.listJobs, store)
.addDelimiter('-', 40, 'Mutate')
.addItem(
'Enqueue job',
store.enqueue,
store,
[
{ name: 'name', type: 'string' },
{ name: 'priority', type: 'numeric' }
]
)
.addItem(
'Cancel job',
store.cancel,
store,
[{ name: 'id', type: 'numeric' }]
)
.addDelimiter('-', 40, 'System')
.addItem('Stats', store.stats, store)
.start();Sample session from examples/admin-jobs.js (abridged):
----------------Browse-----------------
1. List jobs
2. Get job by id: "id"
----------------Mutate-----------------
3. Enqueue job: "name" "priority"
4. Cancel job: "id"
----------------System-----------------
5. Stats
6. Quit
>> 3 "resize-images" 8
Enqueued job #4 "resize-images"
Press Enter to continue...
>> 1
#1 reindex-search priority=5 status=running
#2 send-digest priority=2 status=queued
#3 purge-temp priority=1 status=done
#4 resize-images priority=8 status=queued
Invoke an item with no arguments by typing its number. For arguments, type the number then values separated by spaces. Quote strings that contain spaces.
| File | What it shows |
|---|---|
examples/admin-jobs.js |
In-memory job store: list, get, enqueue, cancel, stats (owner + typed args) |
examples/custom-chrome.js |
customHeader and customPrompt |
examples/cancel-job.js |
Long-running work cancelled via continueCallback when Enter is pressed |
examples/history-persist.js |
Optional command history persistence (configureHistory) |
examples/ai-gateway-ops.js |
AI gateway / RAG ops: traffic, indexes, caps; custom chrome, cancel-in-flight, history persist |
Simulated internal ops console for a Node AI gateway (traffic, RAG indexes, rate/cost caps).
node examples/ai-gateway-ops.jsSample session (abridged):
=== AI Gateway Ops ===
open=1 maxRpm=60 budget=100000 used=12500
History: /tmp/node-menu-ai-gateway-history
----------------Traffic----------------
1. List requests
2. Get request by id: "id"
3. Kill request: "id"
------------------RAG------------------
4. List indexes
5. Reindex: "name"
6. Flush cache
7. Query index: "name" "query"
----------------Limits-----------------
8. Show caps
9. Set max RPM: "maxRpm"
10. Set daily token budget: "dailyTokenBudget"
----------------System-----------------
11. Stats
12. Quit
gateway>
>> 3 1
Killed request #1
Press Enter to continue...
>> 7 docs "rate limits"
Query "rate limits" on index "docs":
1. [0.92] Matching chunk about: rate limits
2. [0.81] Related note in docs
Press Enter to continue...
Full script: examples/ai-gateway-ops.js.
var menu = require('node-menu');Chaining methods (addItem, addDelimiter, customHeader, and so on) return the menu object. start() starts the menu and does not return a value for chaining.
Add an item to the menu.
- title — title of the menu item
- handler — item handler function
- owner — owner object for the handler (
this); optional - args — array of
{ name, type }argument descriptors. Types:numeric,bool,string
menu.addItem(
'Enqueue job',
store.enqueue,
store,
[
{ name: 'name', type: 'string' },
{ name: 'priority', type: 'numeric' }
]
);Add a delimiter line. title is printed in the middle when provided.
menu.addDelimiter('-', 33, 'Main Menu')
------------Main Menu------------
menu.addDelimiter('*', 33)
*********************************
Turn on the default header (on by default).
Turn off the default header.
Turn off the default header and print a custom header via the callback.
menu.customHeader(function() {
process.stdout.write('\nCustom header\n');
});Turn on the default prompt (on by default).
Turn off the default prompt.
Turn off the default prompt and print a custom prompt via the callback.
menu.customPrompt(function() {
process.stdout.write('Select an action > ');
});Clear menu data and listeners so the object can be rebuilt and reused.
Set a callback invoked when Enter is pressed on the “Press Enter to continue…” step (useful to cancel in-flight work).
menu.continueCallback(function() {
console.log('Continuing...');
});Configure in-session command history and optional persistence across restarts. In-session history is always on: every non-empty >> line is recorded for up/down recall, with whole-list dedupe (most recent wins) and a default cap of 100 entries (sessionMaxEntries).
Persistence is off by default. When persist: true, only validated handler runs that complete without throwing are saved. The default file path is ~/.node-menu_history; the persisted list defaults to 100 entries (maxEntries).
- sessionMaxEntries — max in-session history entries; default 100
- persist — save successful commands to disk; default false
- path — history file path when persisting; default
~/.node-menu_history - maxEntries — max persisted entries; default 100
menu.configureHistory({
sessionMaxEntries: 100,
persist: true,
path: '/tmp/my-menu-history',
maxEntries: 100
});Start the menu (also registers a Quit item).