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):
importmenufrom'node-menu';menu.addItem('Ping',()=>console.log('pong')).start();Without esModuleInterop:
importmenu= require('node-menu');In plain ESM (Node with "type": "module"), the same default import works via Node’s CJS interop:
importmenufrom'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):
varmenu=require('node-menu');functionJobStore(){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){varjob={id: this._nextId++,name: name,priority: priority,status: 'queued'};this._jobs.push(job);console.log('Enqueued job #'+job.id);};JobStore.prototype.cancel=function(id){varjob=null;for(vari=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(){varcounts={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);};varstore=newJobStore();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.
varmenu=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).