// src/commands/registry.js // // Single source of truth for every bot command. // // Both dispatch paths in index.js consume this registry: // - Webex chat: framework.hears(/.*/, …) parses the text, then runs the // entry whose name (or alias) matches the first token. // - HTTP API: app.get('/:command', …) looks up the same registry and runs // the entry's handler against a captured-output mock bot. // // Adding a new command means appending one entry below. Both Webex and HTTP // pick it up automatically (subject to `mutating` / `http` flags). import { handleHelp } from './help.js'; import { handleAvStatus } from './avStatus.js'; import { handleVoiceStatus } from './voiceStatus.js'; import { handleWanStatus } from './wanStatus.js'; import { handlePhoneDiag } from './phoneDiag.js'; import { handleDectDiag } from './dectDiag.js'; import { handleProvisionDect } from './provisionDect.js'; import { handleWoHistory } from './woHistory.js'; import { handleWoSummary } from './woSummary.js'; import { handleWoAttachments } from './woAttachments.js'; import { handleJiraHistory } from './jiraHistory.js'; import { handleJiraTicket } from './jiraTicket.js'; import { handleJiraPoll } from './jiraPoll.js'; import { handleProvisionVc } from './vcProvision.js'; import { handleVcMonitor } from './vcMonitor.js'; import { handleOffboardUser } from './offboardUser.js'; import { handleWebexHost } from './webexHost.js'; import { handleVoiceDiag } from './voiceDiag.js'; import { handleBulkAvStatusCSV } from './bulkAvStatusCSV.js'; import { handleBulkAvSwitchCSV } from './bulkAvSwitchCSV.js'; import { handleTestDevicesByModel } from './testDevicesByModel.js'; import { handleCallTest } from './callTest.js'; import { handleCallReport } from './callReport.js'; /** * Each entry: * - name: canonical name (lowercase, no leading slash). * - aliases: additional names that route to the same handler. * - handler: async (bot, trigger) => void * - mutating: when true, the HTTP /:command path requires HTTP_API_TOKEN. * - http: defaults to true. Set false for commands that only make * sense over Webex chat (e.g. `help`). */ export const commands = [ { name: 'help', handler: handleHelp, mutating: false, http: false }, { name: 'avstatus', aliases: ['wostatus'], handler: handleAvStatus, mutating: false }, { name: 'voicestatus', handler: handleVoiceStatus, mutating: false }, { name: 'wanstatus', handler: handleWanStatus, mutating: false }, { name: 'phonediag', handler: handlePhoneDiag, mutating: false }, // /dectdiag — full DECT base dump via the on-prem relay + chat-only // reboot/factory-reset cards. Read path is non-mutating; card submits // only fire over Webex (see commands/dectDiag.js). { name: 'dectdiag', aliases: ['dect'], handler: handleDectDiag, mutating: false }, // /voicediag reads per-user Webex Calling features via // /v1/people/{id}/features/* and offers per-issue adaptive-card // remediation. Reads are non-mutating but the confirm buttons on // the cards perform PUTs, so the HTTP path is gated // (mutating: true). See commands/voiceDiag.js + services/voiceDiag/ // for the full behavior contract + required scopes. { name: 'voicediag', handler: handleVoiceDiag, mutating: true }, { name: 'callreport', aliases: ['voicereport'], handler: handleCallReport, mutating: false }, { name: 'wohistory', handler: handleWoHistory, mutating: false }, { name: 'wosummary', handler: handleWoSummary, mutating: false }, { name: 'woattachments', handler: handleWoAttachments, mutating: false }, { name: 'jirahistory', handler: handleJiraHistory, mutating: false }, { name: 'jiraticket', handler: handleJiraTicket, mutating: false }, // /jirapoll [prime] — trigger the hourly Jira poller on demand. Writes // Jira comments + labels, so mutating: true (HTTP path gated by // HTTP_API_TOKEN). See commands/jiraPoll.js for the behavior contract. { name: 'jirapoll', aliases: ['pollnow'], handler: handleJiraPoll, mutating: true }, { name: 'provision-dect', aliases: ['provisiondect'], handler: handleProvisionDect, mutating: true }, { name: 'provision-vc', aliases: ['vcprovision'], handler: handleProvisionVc, mutating: true }, { name: 'vcmonitor', handler: handleVcMonitor, mutating: true }, // /calltest places real Twilio PSTN calls (store AA path or direct dial). { name: 'calltest', handler: handleCallTest, mutating: true }, { name: 'offboarduser', handler: handleOffboardUser, mutating: true }, { name: 'webexhost', handler: handleWebexHost, mutating: true }, // bulkavstatuscsv delivers a CSV attachment via BotClient.sendWithAttachment, // which requires a Webex roomId. The HTTP mock trigger has no roomId, so // the command is chat-only; the runtime guard in the handler backstops this. { name: 'bulkavstatuscsv', handler: handleBulkAvStatusCSV, mutating: true, http: false }, { name: 'bulkavswitchcsv', handler: handleBulkAvSwitchCSV, mutating: true }, { name: 'devicesbymodel', handler: handleTestDevicesByModel, mutating: true }, ]; const byName = new Map(); for (const cmd of commands) { const all = [cmd.name, ...(cmd.aliases || [])].map(n => n.toLowerCase()); for (const key of all) { if (byName.has(key)) { // Fail loud on misconfiguration — silent alias overwrite is the kind of // drift this registry is designed to prevent. throw new Error(`Duplicate command registration: "${key}"`); } byName.set(key, cmd); } } /** * Look up a command by canonical name or alias. Case-insensitive. * @returns {{name: string, handler: Function, mutating: boolean, http?: boolean, aliases?: string[]} | null} */ export function getCommand(name) { if (!name) return null; return byName.get(String(name).toLowerCase().trim()) || null; } /** Names + aliases of commands exposed over HTTP (used for /:command 404 hints). */ export const ALL_HTTP_COMMAND_KEYS = (() => { const keys = []; for (const cmd of commands) { if (cmd.http === false) continue; keys.push(cmd.name, ...(cmd.aliases || [])); } return keys.sort(); })(); /** Set of names + aliases that require HTTP_API_TOKEN on the HTTP path. */ export const MUTATING_COMMAND_KEYS = (() => { const keys = new Set(); for (const cmd of commands) { if (!cmd.mutating) continue; keys.add(cmd.name); for (const a of cmd.aliases || []) keys.add(a); } return keys; })();