From f7379e1b873ebc90c73cca4d6f788bde9154440f Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Fri, 3 Jul 2026 15:28:12 +0530 Subject: [PATCH 1/2] Add NTES adapter --- cli-manifest.json | 195 +++++++++++++++++++++++++++++++++++++++++ clis/ntes/between.js | 116 ++++++++++++++++++++++++ clis/ntes/ntes.test.js | 49 +++++++++++ clis/ntes/schedule.js | 83 ++++++++++++++++++ clis/ntes/station.js | 106 ++++++++++++++++++++++ clis/ntes/status.js | 112 +++++++++++++++++++++++ clis/ntes/utils.js | 107 ++++++++++++++++++++++ 7 files changed, 768 insertions(+) create mode 100644 clis/ntes/between.js create mode 100644 clis/ntes/ntes.test.js create mode 100644 clis/ntes/schedule.js create mode 100644 clis/ntes/station.js create mode 100644 clis/ntes/status.js create mode 100644 clis/ntes/utils.js diff --git a/cli-manifest.json b/cli-manifest.json index bfdb71e..09f6f77 100644 --- a/cli-manifest.json +++ b/cli-manifest.json @@ -24870,6 +24870,201 @@ "modulePath": "npm/search.js", "sourceFile": "npm/search.js" }, + { + "site": "ntes", + "name": "between", + "description": "NTES trains between two stations", + "access": "read", + "example": "webcmd ntes between MMCT NDLS --limit 5", + "domain": "enquiry.indianrail.gov.in", + "strategy": "ui", + "browser": true, + "args": [ + { + "name": "from", + "type": "string", + "required": true, + "positional": true, + "help": "Origin station code or NTES station label" + }, + { + "name": "to", + "type": "string", + "required": true, + "positional": true, + "help": "Destination station code or NTES station label" + }, + { + "name": "limit", + "type": "int", + "default": 10, + "required": false, + "help": "Number of trains to return (max 20)" + } + ], + "columns": [ + "rank", + "trainNumber", + "trainName", + "from", + "to", + "days", + "departTime", + "departStation", + "arriveTime", + "arriveStation", + "duration", + "url" + ], + "type": "js", + "modulePath": "ntes/between.js", + "sourceFile": "ntes/between.js", + "navigateBefore": true + }, + { + "site": "ntes", + "name": "schedule", + "description": "NTES train schedule stops", + "access": "read", + "example": "webcmd ntes schedule 12951 --limit 8", + "domain": "enquiry.indianrail.gov.in", + "strategy": "ui", + "browser": true, + "args": [ + { + "name": "train", + "type": "string", + "required": true, + "positional": true, + "help": "5 digit train number" + }, + { + "name": "limit", + "type": "int", + "default": 10, + "required": false, + "help": "Number of stops to return (max 20)" + } + ], + "columns": [ + "rank", + "trainNumber", + "station", + "code", + "day", + "arrival", + "departure", + "halt", + "distanceKm", + "url" + ], + "type": "js", + "modulePath": "ntes/schedule.js", + "sourceFile": "ntes/schedule.js", + "navigateBefore": true + }, + { + "site": "ntes", + "name": "station", + "description": "NTES live station departures and arrivals", + "access": "read", + "example": "webcmd ntes station MMCT --hours 2 --limit 5", + "domain": "enquiry.indianrail.gov.in", + "strategy": "ui", + "browser": true, + "args": [ + { + "name": "station", + "type": "string", + "required": true, + "positional": true, + "help": "Station code or NTES station label" + }, + { + "name": "hours", + "type": "int", + "default": 2, + "required": false, + "help": "Lookahead window in hours", + "choices": [ + "2", + "4", + "8" + ] + }, + { + "name": "limit", + "type": "int", + "default": 10, + "required": false, + "help": "Number of trains to return (max 20)" + } + ], + "columns": [ + "rank", + "station", + "trainNumber", + "trainName", + "route", + "arrival", + "departure", + "status", + "platform", + "url" + ], + "type": "js", + "modulePath": "ntes/station.js", + "sourceFile": "ntes/station.js", + "navigateBefore": true + }, + { + "site": "ntes", + "name": "status", + "description": "NTES live train running status", + "access": "read", + "example": "webcmd ntes status 12951 --station MMCT --limit 8", + "domain": "enquiry.indianrail.gov.in", + "strategy": "ui", + "browser": true, + "args": [ + { + "name": "train", + "type": "string", + "required": true, + "positional": true, + "help": "5 digit train number" + }, + { + "name": "station", + "type": "string", + "default": "MMCT", + "required": false, + "help": "Journey station code or NTES station label" + }, + { + "name": "limit", + "type": "int", + "default": 10, + "required": false, + "help": "Number of status rows to return (max 20)" + } + ], + "columns": [ + "rank", + "trainNumber", + "station", + "code", + "arrival", + "departure", + "status", + "distanceKm", + "url" + ], + "type": "js", + "modulePath": "ntes/status.js", + "sourceFile": "ntes/status.js", + "navigateBefore": true + }, { "site": "nuget", "name": "package", diff --git a/clis/ntes/between.js b/clis/ntes/between.js new file mode 100644 index 0000000..0bb9f5a --- /dev/null +++ b/clis/ntes/between.js @@ -0,0 +1,116 @@ +import { cli, Strategy } from '@agentrhq/webcmd/registry'; +import { CommandExecutionError } from '@agentrhq/webcmd/errors'; +import { HOME_URL, SITE, parseLimit, requireRows, rowsWithRank, scriptHelpers, stationCode, stationInput } from './utils.js'; + +export function buildBetweenActionScript(fromStation, toStation) { + return `(() => { + const form = document.frmTBS; + if (!form || typeof onTBS !== 'function') return { ok: false, error: 'Trains B/w Stations form not found' }; + form.jFromStationInput.value = ${JSON.stringify(fromStation)}; + form.jToStationInput.value = ${JSON.stringify(toStation)}; + onTBS(); + return { ok: true }; + })()`; +} + +export function buildOpenBetweenMenuScript() { + return `(() => { + const menu = Array.from(document.querySelectorAll('a')).find((a) => (a.innerText || '').includes('Trains B/w Stations')); + if (!menu) return { ok: false, error: 'Trains B/w Stations menu not found' }; + menu.click(); + return { ok: true }; + })()`; +} + +export function buildBetweenExtractScript() { + return `(() => { + ${scriptHelpers()} + const table = Array.from(document.querySelectorAll('table')).find((t) => { + const text = clean(t.innerText); + return /Trains found from/.test(text) && /See Train Status/.test(text); + }); + if (!table) return { ok: true, rows: [] }; + const rows = Array.from(table.rows || []) + .map((row) => Array.from(row.cells || [])[0]) + .filter(Boolean) + .map((cell) => cell.innerText || '') + .filter((text) => /^\\d{5}/.test(clean(text))); + return { ok: true, rows: rows.map((text) => { + const lines = text.split('\\n').map(clean).filter(Boolean).filter((line) => line !== 'See Train Status >>'); + const head = lines[0] || ''; + const match = head.match(/^(\\d{5})\\s+(.+)$/); + const meta = (lines[1] || '').split('|').map(clean); + const durationIndex = lines.findIndex((line) => /^--.*Hrs\\.?.*--$/.test(line)); + let arriveIndex = durationIndex >= 0 ? durationIndex + 1 : -1; + const maybeClasses = arriveIndex >= 0 && !/^\\d{1,2}:\\d{2}$/.test(lines[arriveIndex] || '') && /^\\d{1,2}:\\d{2}$/.test(lines[arriveIndex + 1] || ''); + if (maybeClasses) arriveIndex += 1; + return { + trainNumber: match ? match[1] : '', + trainName: match ? match[2] : head, + days: meta[0] || '', + type: meta[1] || '', + departTime: lines[2] || '', + departStation: lines[3] || '', + departCode: lines[4] || '', + duration: durationIndex >= 0 ? lines[durationIndex].replace(/^--|--$/g, '') : '', + classes: maybeClasses ? lines[durationIndex + 1] || '' : '', + arriveTime: arriveIndex >= 0 ? lines[arriveIndex] || '' : '', + arriveStation: arriveIndex >= 0 ? lines[arriveIndex + 1] || '' : '', + arriveCode: arriveIndex >= 0 ? lines[arriveIndex + 2] || '' : '', + }; + }) }; + })()`; +} + +export const command = cli({ + site: SITE, + name: 'between', + access: 'read', + description: 'NTES trains between two stations', + example: 'webcmd ntes between MMCT NDLS --limit 5', + domain: 'enquiry.indianrail.gov.in', + strategy: Strategy.UI, + browser: true, + args: [ + { name: 'from', type: 'string', required: true, positional: true, help: 'Origin station code or NTES station label' }, + { name: 'to', type: 'string', required: true, positional: true, help: 'Destination station code or NTES station label' }, + { name: 'limit', type: 'int', default: 10, help: 'Number of trains to return (max 20)' }, + ], + columns: ['rank', 'trainNumber', 'trainName', 'from', 'to', 'days', 'departTime', 'departStation', 'arriveTime', 'arriveStation', 'duration', 'url'], + func: async (page, kwargs) => { + const fromValue = stationInput(kwargs.from); + const toValue = stationInput(kwargs.to); + const from = stationCode(fromValue); + const to = stationCode(toValue); + const limit = parseLimit(kwargs.limit, 'ntes between'); + await page.goto(HOME_URL); + await page.wait(1); + await page.evaluate(buildOpenBetweenMenuScript()); + await page.wait(2); + const action = await page.evaluate(buildBetweenActionScript(fromValue, toValue)); + if (action && action.ok === false) + throw new CommandExecutionError(`ntes between UI action failed: ${action.error || 'Trains B/w Stations submit failed'}`); + await page.wait(4); + const rows = requireRows(await page.evaluate(buildBetweenExtractScript()), 'ntes between'); + return rowsWithRank(rows, limit, (row) => ({ + trainNumber: row.trainNumber || '', + trainName: row.trainName || '', + from, + to, + days: row.days || '', + departTime: row.departTime || '', + departStation: row.departStation || '', + arriveTime: row.arriveTime || '', + arriveStation: row.arriveStation || '', + duration: row.duration || '', + url: HOME_URL, + })); + }, +}); + +export const __test__ = { + command, + buildOpenBetweenMenuScript, + buildBetweenActionScript, + buildBetweenExtractScript, +}; diff --git a/clis/ntes/ntes.test.js b/clis/ntes/ntes.test.js new file mode 100644 index 0000000..a05f5bb --- /dev/null +++ b/clis/ntes/ntes.test.js @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest'; +import { ArgumentError, EmptyResultError } from '@agentrhq/webcmd/errors'; +import { createPageMock } from '../test-utils.js'; +import { __test__ as utils } from './utils.js'; +import { __test__ as status } from './status.js'; +import { __test__ as station } from './station.js'; +import { __test__ as schedule } from './schedule.js'; +import { __test__ as between } from './between.js'; + +describe('ntes utils', () => { + it('validates train numbers and station codes', () => { + expect(utils.requireTrainNumber('12951')).toBe('12951'); + expect(utils.stationInput('MMCT')).toBe('MMCT - MUMBAI CENTRAL'); + expect(utils.parseLimit(5, 'ntes status')).toBe(5); + expect(() => utils.requireTrainNumber('abc')).toThrow(ArgumentError); + }); +}); + +describe('ntes commands', () => { + it('schedule returns station stop rows', async () => { + const page = createPageMock([{ ok: true }, { ok: true, rows: [{ station: 'MUMBAI CENTRAL', code: 'MMCT', day: '1', arrival: 'SRC', departure: '17:00', halt: '', distanceKm: '0' }] }]); + const rows = await schedule.command.func(page, { train: '12951', limit: 1 }); + expect(page.goto).toHaveBeenCalledWith('https://enquiry.indianrail.gov.in/mntes/'); + expect(rows[0]).toMatchObject({ rank: 1, trainNumber: '12951', station: 'MUMBAI CENTRAL', code: 'MMCT' }); + }); + + it('status returns visible running status rows', async () => { + const page = createPageMock([{ ok: true }, { ok: true }, { ok: true, rows: [{ station: 'BORIVALI', code: 'BVI', arrival: '17:20 03-Jul', departure: '17:22 03-Jul', status: 'On Time', distanceKm: '30' }] }]); + const rows = await status.command.func(page, { train: '12951', station: 'MMCT', limit: 1 }); + expect(rows[0]).toMatchObject({ trainNumber: '12951', station: 'BORIVALI', status: 'On Time' }); + }); + + it('station returns live station rows', async () => { + const page = createPageMock([{ ok: true }, { ok: true }, { ok: true, rows: [{ trainNumber: '22961', trainName: 'VANDE BHARAT EXP', arrival: 'Source', departure: '15:45', platform: '4', status: 'On Time' }] }]); + const rows = await station.command.func(page, { station: 'MMCT', hours: 2, limit: 1 }); + expect(rows[0]).toMatchObject({ rank: 1, station: 'MMCT', trainNumber: '22961' }); + }); + + it('between returns trains between two stations', async () => { + const page = createPageMock([{ ok: true }, { ok: true }, { ok: true, rows: [{ trainNumber: '12951', trainName: 'NDLS TEJAS RAJ', days: 'Daily', type: 'Rajdhani', departTime: '17:00', departStation: 'Mumbai Central', departCode: 'MMCT', duration: '15:32 Hrs.', arriveTime: '08:32', arriveStation: 'New Delhi', arriveCode: 'NDLS', classes: '1A,2A,3A' }] }]); + const rows = await between.command.func(page, { from: 'MMCT', to: 'NDLS', limit: 1 }); + expect(rows[0]).toMatchObject({ trainNumber: '12951', from: 'MMCT', to: 'NDLS' }); + }); + + it('throws EmptyResultError for empty extraction', async () => { + const page = createPageMock([{ ok: true }, { ok: true }, { ok: true, rows: [] }]); + await expect(station.command.func(page, { station: 'MMCT', hours: 2, limit: 1 })).rejects.toBeInstanceOf(EmptyResultError); + }); +}); diff --git a/clis/ntes/schedule.js b/clis/ntes/schedule.js new file mode 100644 index 0000000..fe2aa21 --- /dev/null +++ b/clis/ntes/schedule.js @@ -0,0 +1,83 @@ +import { cli, Strategy } from '@agentrhq/webcmd/registry'; +import { HOME_URL, SITE, requireTrainNumber, parseLimit, rowsWithRank, runNtesPage, scriptHelpers } from './utils.js'; + +export function buildScheduleActionScript(trainNumber) { + return `(() => { + const input = document.querySelector('#trainNo'); + if (!input || typeof showTrainSchedule !== 'function') return { ok: false, error: 'schedule form not found' }; + input.value = ${JSON.stringify(trainNumber)}; + showTrainSchedule('B'); + return { ok: true }; + })()`; +} + +export function buildScheduleExtractScript() { + return `(() => { + ${scriptHelpers()} + const table = Array.from(document.querySelectorAll('table')).find((t) => { + const text = clean(t.innerText); + return text.includes('Sr.') && text.includes('Station') && text.includes('Dist.'); + }); + if (!table) return { ok: true, rows: [] }; + const rows = tableRows(table).slice(1).filter((cells) => cells.length >= 6 && /^\\d+$/.test(cells[0])); + return { ok: true, rows: rows.map((cells) => { + let stationParts = cells[1].split(/\\n| {2,}/).map(clean).filter(Boolean); + if (stationParts.length === 1) { + const match = stationParts[0].match(/^(.+)\\s+([A-Z]{2,5})$/); + if (match) stationParts = [match[1], match[2]]; + } + let times = cells[3].split(/\\n| {2,}/).map(clean).filter(Boolean); + if (times.length === 1) { + const match = times[0].match(/^(SRC|DSTN|\\d{1,2}:\\d{2})\\s+(SRC|DSTN|\\d{1,2}:\\d{2})$/); + if (match) times = [match[1], match[2]]; + } + return { + station: stationParts[0] || '', + code: stationParts[1] || '', + day: cells[2] || '', + arrival: times[0] || '', + departure: times[1] || '', + halt: cells[4] || '', + distanceKm: cells[5] || '', + }; + }) }; + })()`; +} + +export const command = cli({ + site: SITE, + name: 'schedule', + access: 'read', + description: 'NTES train schedule stops', + example: 'webcmd ntes schedule 12951 --limit 8', + domain: 'enquiry.indianrail.gov.in', + strategy: Strategy.UI, + browser: true, + args: [ + { name: 'train', type: 'string', required: true, positional: true, help: '5 digit train number' }, + { name: 'limit', type: 'int', default: 10, help: 'Number of stops to return (max 20)' }, + ], + columns: ['rank', 'trainNumber', 'station', 'code', 'day', 'arrival', 'departure', 'halt', 'distanceKm', 'url'], + func: async (page, kwargs) => { + const trainNumber = requireTrainNumber(kwargs.train); + const limit = parseLimit(kwargs.limit, 'ntes schedule'); + const rows = await runNtesPage(page, buildScheduleActionScript(trainNumber), buildScheduleExtractScript(), 'ntes schedule'); + return rowsWithRank(rows, limit, (row) => ({ + trainNumber, + station: row.station || '', + code: row.code || '', + day: row.day || '', + arrival: row.arrival || '', + departure: row.departure || '', + halt: row.halt || '', + distanceKm: row.distanceKm || '', + url: HOME_URL, + })); + }, +}); + +export const __test__ = { + command, + buildScheduleActionScript, + buildScheduleExtractScript, +}; diff --git a/clis/ntes/station.js b/clis/ntes/station.js new file mode 100644 index 0000000..1587d4f --- /dev/null +++ b/clis/ntes/station.js @@ -0,0 +1,106 @@ +import { cli, Strategy } from '@agentrhq/webcmd/registry'; +import { CommandExecutionError } from '@agentrhq/webcmd/errors'; +import { HOME_URL, SITE, parseHours, parseLimit, requireRows, rowsWithRank, scriptHelpers, stationCode, stationInput } from './utils.js'; + +export function buildStationActionScript(stationValue, hours) { + return `(() => { + const form = document.frmSTN; + if (!form || typeof onLiveStationSubmit !== 'function') return { ok: false, error: 'Live Station form not found' }; + form.jFromStationInput.value = ${JSON.stringify(stationValue)}; + form.jToStationInput.value = ''; + const radio = Array.from(form.elements.nHr || []).find((el) => String(el.value) === ${JSON.stringify(String(hours))}); + if (radio) radio.checked = true; + onLiveStationSubmit(); + return { ok: true }; + })()`; +} + +export function buildOpenStationMenuScript() { + return `(() => { + const menu = Array.from(document.querySelectorAll('a')).find((a) => (a.innerText || '').includes('Live Station')); + if (!menu) return { ok: false, error: 'Live Station menu not found' }; + menu.click(); + return { ok: true }; + })()`; +} + +export function buildStationExtractScript() { + return `(() => { + ${scriptHelpers()} + if (/Requested service.*un-available/i.test(document.body.innerText || '')) { + return { ok: false, error: 'NTES live station service unavailable' }; + } + const table = Array.from(document.querySelectorAll('table')).find((t) => { + const text = clean(t.innerText); + return text.includes('Trains departing from/arriving at') && text.includes('Train No./Name'); + }); + if (!table) return { ok: true, rows: [] }; + const rows = tableRows(table).filter((cells) => cells.length >= 5 && /^\\d+$/.test(cells[0])); + return { ok: true, rows: rows.map((cells) => { + const lines = cells[1].split('\\n').map(clean).filter(Boolean); + const head = lines[0] || ''; + const match = head.match(/^(\\d{5})\\s*\\|\\s*(.+)$/); + const depLines = cells[3].split('\\n').map(clean).filter(Boolean); + const arrLines = cells[2].split('\\n').map(clean).filter(Boolean); + return { + trainNumber: match ? match[1] : '', + trainName: match ? match[2] : head, + route: lines[1] || '', + arrival: arrLines[0] || '', + departure: depLines[0] || '', + status: depLines.find((line) => /time|mins?|late|early/i.test(line)) || arrLines.find((line) => /time|mins?|late|early/i.test(line)) || '', + platform: clean(cells[4]).replace(/Coach Position/i, '').replace(/\\*/g, ''), + }; + }) }; + })()`; +} + +export const command = cli({ + site: SITE, + name: 'station', + access: 'read', + description: 'NTES live station departures and arrivals', + example: 'webcmd ntes station MMCT --hours 2 --limit 5', + domain: 'enquiry.indianrail.gov.in', + strategy: Strategy.UI, + browser: true, + args: [ + { name: 'station', type: 'string', required: true, positional: true, help: 'Station code or NTES station label' }, + { name: 'hours', type: 'int', default: 2, choices: ['2', '4', '8'], help: 'Lookahead window in hours' }, + { name: 'limit', type: 'int', default: 10, help: 'Number of trains to return (max 20)' }, + ], + columns: ['rank', 'station', 'trainNumber', 'trainName', 'route', 'arrival', 'departure', 'status', 'platform', 'url'], + func: async (page, kwargs) => { + const stationValue = stationInput(kwargs.station); + const code = stationCode(stationValue); + const hours = parseHours(kwargs.hours); + const limit = parseLimit(kwargs.limit, 'ntes station'); + await page.goto(HOME_URL); + await page.wait(1); + await page.evaluate(buildOpenStationMenuScript()); + await page.wait(2); + const action = await page.evaluate(buildStationActionScript(stationValue, hours)); + if (action && action.ok === false) + throw new CommandExecutionError(`ntes station UI action failed: ${action.error || 'Live Station submit failed'}`); + await page.wait(4); + const rows = requireRows(await page.evaluate(buildStationExtractScript()), 'ntes station'); + return rowsWithRank(rows, limit, (row) => ({ + station: code, + trainNumber: row.trainNumber || '', + trainName: row.trainName || '', + route: row.route || '', + arrival: row.arrival || '', + departure: row.departure || '', + status: row.status || '', + platform: row.platform || '', + url: HOME_URL, + })); + }, +}); + +export const __test__ = { + command, + buildOpenStationMenuScript, + buildStationActionScript, + buildStationExtractScript, +}; diff --git a/clis/ntes/status.js b/clis/ntes/status.js new file mode 100644 index 0000000..ff29361 --- /dev/null +++ b/clis/ntes/status.js @@ -0,0 +1,112 @@ +import { cli, Strategy } from '@agentrhq/webcmd/registry'; +import { CommandExecutionError } from '@agentrhq/webcmd/errors'; +import { HOME_URL, SITE, parseLimit, requireRows, requireTrainNumber, rowsWithRank, stationCode, stationInput } from './utils.js'; + +export function buildStatusFindTrainScript(trainNumber) { + return `(() => { + const input = document.querySelector('#trainNo'); + if (!input || typeof onTrainFindInput !== 'function') return { ok: false, error: 'Spot Your Train form not found' }; + input.value = ${JSON.stringify(trainNumber)}; + onTrainFindInput('B'); + return { ok: true }; + })()`; +} + +export function buildStatusSubmitScript(journeyStation) { + return `(() => { + const form = document.frmTRN; + const select = form && form.jStation; + if (!select || select.options.length <= 1 || typeof onTrainFindInput !== 'function') { + return { ok: false, error: 'Journey station list not found' }; + } + const wanted = ${JSON.stringify(stationCode(journeyStation))}; + const options = Array.from(select.options); + const match = options.find((o) => String(o.text || '').toUpperCase().includes(wanted) || String(o.value || '').toUpperCase().startsWith(wanted + '#')); + select.value = match ? match.value : options[1].value; + onTrainFindInput('A'); + return { ok: true }; + })()`; +} + +export function buildStatusExtractScript() { + return `(() => { + const clean = (value) => String(value || '').replace(/\\u00a0/g, ' ').replace(/\\s+/g, ' ').trim(); + const lines = (document.body.innerText || '').split('\\n').map(clean).filter(Boolean); + const rows = []; + for (let i = 0; i < lines.length; i += 1) { + const station = lines[i]; + const codeLine = lines[i + 1] || ''; + const code = (codeLine.match(/^([A-Z]{2,5})\\b/) || [])[1] || ''; + if (!code || !/^[A-Z][A-Z .()/-]+$/.test(station) || station.includes('National Train')) continue; + if (station === 'SRC' || station === 'DSTN') continue; + const before = lines.slice(Math.max(0, i - 8), i); + const window = lines.slice(i + 2, i + 10); + const distance = (window.find((line) => /\\d+\\s*KMs?/i.test(line)) || '').replace(/\\s*KMs?.*/i, ''); + const beforeTimes = before.filter((line) => /^(SRC|DSTN|\\d{1,2}:\\d{2}\\s+\\d{2}-[A-Za-z]{3})/.test(line)); + const afterTimes = window.filter((line) => /^(SRC|DSTN|\\d{1,2}:\\d{2}\\s+\\d{2}-[A-Za-z]{3})/.test(line)); + const status = window.find((line) => /On Time|Mins?\\.|Late|Early|Cancelled|Diverted/i.test(line)) + || before.slice().reverse().find((line) => /On Time|Mins?\\.|Late|Early|Cancelled|Diverted/i.test(line)) + || ''; + if (!beforeTimes.length && !afterTimes.length && !status) continue; + rows.push({ + station, + code, + arrival: beforeTimes[beforeTimes.length - 1] || afterTimes[0] || '', + departure: (afterTimes[0] === 'SRC' || afterTimes[0] === 'DSTN') ? afterTimes[1] || afterTimes[0] : afterTimes[0] || '', + status, + distanceKm: distance, + }); + } + return { ok: true, rows }; + })()`; +} + +export const command = cli({ + site: SITE, + name: 'status', + access: 'read', + description: 'NTES live train running status', + example: 'webcmd ntes status 12951 --station MMCT --limit 8', + domain: 'enquiry.indianrail.gov.in', + strategy: Strategy.UI, + browser: true, + args: [ + { name: 'train', type: 'string', required: true, positional: true, help: '5 digit train number' }, + { name: 'station', type: 'string', default: 'MMCT', help: 'Journey station code or NTES station label' }, + { name: 'limit', type: 'int', default: 10, help: 'Number of status rows to return (max 20)' }, + ], + columns: ['rank', 'trainNumber', 'station', 'code', 'arrival', 'departure', 'status', 'distanceKm', 'url'], + func: async (page, kwargs) => { + const trainNumber = requireTrainNumber(kwargs.train); + const journeyStation = stationInput(kwargs.station || 'MMCT'); + const limit = parseLimit(kwargs.limit, 'ntes status'); + await page.goto(HOME_URL); + await page.wait(1); + let action = await page.evaluate(buildStatusFindTrainScript(trainNumber)); + if (action && action.ok === false) + throw new CommandExecutionError(`ntes status UI action failed: ${action.error || 'train search failed'}`); + await page.wait(3); + action = await page.evaluate(buildStatusSubmitScript(journeyStation)); + if (action && action.ok === false) + throw new CommandExecutionError(`ntes status UI action failed: ${action.error || 'status submit failed'}`); + await page.wait(5); + const rows = requireRows(await page.evaluate(buildStatusExtractScript()), 'ntes status'); + return rowsWithRank(rows, limit, (row) => ({ + trainNumber, + station: row.station || '', + code: row.code || '', + arrival: row.arrival || '', + departure: row.departure || '', + status: row.status || '', + distanceKm: row.distanceKm || '', + url: HOME_URL, + })); + }, +}); + +export const __test__ = { + command, + buildStatusFindTrainScript, + buildStatusSubmitScript, + buildStatusExtractScript, +}; diff --git a/clis/ntes/utils.js b/clis/ntes/utils.js new file mode 100644 index 0000000..2e4a7f2 --- /dev/null +++ b/clis/ntes/utils.js @@ -0,0 +1,107 @@ +import { ArgumentError, CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors'; + +export const SITE = 'ntes'; +export const HOME_URL = 'https://enquiry.indianrail.gov.in/mntes/'; +const MAX_LIMIT = 20; + +const STATIONS = new Map([ + ['MMCT', 'MMCT - MUMBAI CENTRAL'], + ['BCT', 'MMCT - MUMBAI CENTRAL'], + ['NDLS', 'NDLS - NEW DELHI'], + ['NZM', 'NZM - HAZRAT NIZAMUDDIN JN'], + ['BVI', 'BVI - BORIVALI'], + ['ST', 'ST - SURAT'], + ['BRC', 'BRC - VADODARA JN'], + ['CSMT', 'CSMT - CHHATRAPATI SHIVAJI MAHARAJ TERMINUS'], + ['LTT', 'LTT - LOKMANYATILAK'], +]); + +export function cleanText(value) { + return String(value ?? '').replace(/\u00a0/g, ' ').replace(/\s+/g, ' ').trim(); +} + +export function requireTrainNumber(value) { + const raw = cleanText(value); + if (!/^\d{5}$/.test(raw)) { + throw new ArgumentError('ntes train must be a 5 digit train number', 'Example: webcmd ntes status 12951 --station MMCT'); + } + return raw; +} + +export function stationInput(value) { + const raw = cleanText(value).toUpperCase(); + if (!raw) { + throw new ArgumentError('ntes station code cannot be empty', 'Example: webcmd ntes station MMCT'); + } + if (/^[A-Z]{2,5}$/.test(raw)) + return STATIONS.get(raw) || raw; + return raw; +} + +export function stationCode(value) { + const raw = cleanText(value).toUpperCase(); + const match = raw.match(/^([A-Z]{2,5})\b/) || raw.match(/\b([A-Z]{2,5})$/); + return match ? match[1] : raw; +} + +export function parseLimit(value, commandName) { + const n = value == null || value === '' ? 10 : Number(value); + if (!Number.isInteger(n) || n < 1 || n > MAX_LIMIT) { + throw new ArgumentError(`${commandName} --limit must be an integer between 1 and ${MAX_LIMIT}`, `Example: webcmd ${commandName} --limit 5`); + } + return n; +} + +export function parseHours(value) { + const n = value == null || value === '' ? 2 : Number(value); + if (![2, 4, 8].includes(n)) { + throw new ArgumentError('ntes station --hours must be one of 2, 4, or 8', 'Example: webcmd ntes station MMCT --hours 2'); + } + return n; +} + +export function requireRows(result, commandName) { + if (!result || typeof result !== 'object') { + throw new CommandExecutionError(`${commandName} page returned malformed extraction data`, 'NTES may have changed its page structure.'); + } + if (result.ok === false) { + throw new CommandExecutionError(`${commandName} extraction failed: ${result.error || 'unknown error'}`, 'Open the official NTES page in Chrome and retry.'); + } + const rows = Array.isArray(result.rows) ? result.rows : []; + if (!rows.length) { + throw new EmptyResultError(commandName, 'No visible NTES rows were found. Try another train/station or retry later if NTES is unavailable.'); + } + return rows; +} + +export async function runNtesPage(page, actionScript, extractScript, commandName) { + await page.goto(HOME_URL); + await page.wait(1); + const action = await page.evaluate(actionScript); + if (action && action.ok === false) + throw new CommandExecutionError(`${commandName} UI action failed: ${action.error || 'unknown error'}`); + await page.wait(4); + return requireRows(await page.evaluate(extractScript), commandName); +} + +export function rowsWithRank(rows, limit, mapRow) { + return rows.slice(0, limit).map((row, index) => ({ rank: index + 1, ...mapRow(row) })); +} + +export function scriptHelpers() { + return ` + const clean = (value) => String(value || '').replace(/\\u00a0/g, ' ').replace(/\\s+/g, ' ').trim(); + const cellText = (cell) => clean(cell && cell.innerText); + const tableRows = (table) => Array.from(table.rows || []).map((row) => Array.from(row.cells || []).map(cellText)); + `; +} + +export const __test__ = { + cleanText, + requireTrainNumber, + stationInput, + stationCode, + parseLimit, + parseHours, + requireRows, +}; From e9f4eea588cfe149a47657ff3203f5f4b83bcbc2 Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Fri, 3 Jul 2026 15:32:51 +0530 Subject: [PATCH 2/2] Fix NTES between column audit --- clis/ntes/between.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/clis/ntes/between.js b/clis/ntes/between.js index 0bb9f5a..e7bcc09 100644 --- a/clis/ntes/between.js +++ b/clis/ntes/between.js @@ -48,15 +48,11 @@ export function buildBetweenExtractScript() { trainNumber: match ? match[1] : '', trainName: match ? match[2] : head, days: meta[0] || '', - type: meta[1] || '', departTime: lines[2] || '', departStation: lines[3] || '', - departCode: lines[4] || '', duration: durationIndex >= 0 ? lines[durationIndex].replace(/^--|--$/g, '') : '', - classes: maybeClasses ? lines[durationIndex + 1] || '' : '', arriveTime: arriveIndex >= 0 ? lines[arriveIndex] || '' : '', arriveStation: arriveIndex >= 0 ? lines[arriveIndex + 1] || '' : '', - arriveCode: arriveIndex >= 0 ? lines[arriveIndex + 2] || '' : '', }; }) }; })()`;