Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions cli-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
112 changes: 112 additions & 0 deletions clis/ntes/between.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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] || '',
departTime: lines[2] || '',
departStation: lines[3] || '',
duration: durationIndex >= 0 ? lines[durationIndex].replace(/^--|--$/g, '') : '',
arriveTime: arriveIndex >= 0 ? lines[arriveIndex] || '' : '',
arriveStation: arriveIndex >= 0 ? lines[arriveIndex + 1] || '' : '',
};
}) };
})()`;
}

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,
};
49 changes: 49 additions & 0 deletions clis/ntes/ntes.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading
Loading