@ktmcp-cli/airportweb 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT.md ADDED
@@ -0,0 +1,78 @@
1
+ # AGENT.md — Airport Web CLI for AI Agents
2
+
3
+ This document explains how to use the Airport Web CLI as an AI agent.
4
+
5
+ ## Overview
6
+
7
+ The `airportweb` CLI provides access to the Airport Web API. No authentication is required — it is a public API.
8
+
9
+ ## All Commands
10
+
11
+ ### Config
12
+
13
+ ```bash
14
+ airportweb config get <key>
15
+ airportweb config set <key> <value>
16
+ airportweb config list
17
+ ```
18
+
19
+ ### Airports
20
+
21
+ ```bash
22
+ # Search airports
23
+ airportweb airports search "London"
24
+ airportweb airports search "JFK"
25
+
26
+ # Get specific airport
27
+ airportweb airports get JFK
28
+ airportweb airports get LHR
29
+
30
+ # List airports
31
+ airportweb airports list
32
+ airportweb airports list --limit 100
33
+
34
+ # By country code
35
+ airportweb airports by-country US
36
+ airportweb airports by-country GB
37
+ ```
38
+
39
+ ### Airlines
40
+
41
+ ```bash
42
+ # List airlines
43
+ airportweb airlines list
44
+
45
+ # Get specific airline
46
+ airportweb airlines get AA
47
+ airportweb airlines get BA
48
+
49
+ # Search airlines
50
+ airportweb airlines search "American"
51
+ ```
52
+
53
+ ### Routes
54
+
55
+ ```bash
56
+ # Get routes
57
+ airportweb routes get --from JFK
58
+ airportweb routes get --to LHR
59
+ airportweb routes get --from JFK --to LHR
60
+
61
+ # Search routes with filters
62
+ airportweb routes search --from JFK --airline AA
63
+ airportweb routes search --from LAX --to SFO
64
+ ```
65
+
66
+ ## JSON Output
67
+
68
+ All commands support `--json`:
69
+
70
+ ```bash
71
+ airportweb airports get JFK --json
72
+ airportweb airlines list --json
73
+ airportweb routes get --from JFK --json
74
+ ```
75
+
76
+ ## Error Handling
77
+
78
+ The CLI exits with code 1 on error and prints to stderr.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 KTMCP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ > "Six months ago, everyone was talking about MCPs. And I was like, screw MCPs. Every MCP would be better as a CLI."
2
+ >
3
+ > — [Peter Steinberger](https://twitter.com/steipete), Founder of OpenClaw
4
+ > [Watch on YouTube (~2:39:00)](https://www.youtube.com/@lexfridman) | [Lex Fridman Podcast #491](https://lexfridman.com/peter-steinberger/)
5
+
6
+ # Airport Web CLI
7
+
8
+ Production-ready CLI for the Airport Web API. Search airports, airlines, and routes directly from your terminal.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install -g @ktmcp-cli/airportweb
14
+ ```
15
+
16
+ ## Configuration
17
+
18
+ No authentication required — the Airport Web API is public.
19
+
20
+ ```bash
21
+ airportweb config list
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Airports
27
+
28
+ ```bash
29
+ # Search airports by name or city
30
+ airportweb airports search "London"
31
+ airportweb airports search "JFK"
32
+
33
+ # Get airport by IATA code
34
+ airportweb airports get JFK
35
+ airportweb airports get LHR
36
+
37
+ # List airports
38
+ airportweb airports list
39
+ airportweb airports list --limit 20
40
+
41
+ # List airports by country
42
+ airportweb airports by-country US
43
+ airportweb airports by-country DE
44
+ ```
45
+
46
+ ### Airlines
47
+
48
+ ```bash
49
+ # List airlines
50
+ airportweb airlines list
51
+ airportweb airlines list --limit 30
52
+
53
+ # Get airline by IATA code
54
+ airportweb airlines get AA
55
+ airportweb airlines get BA
56
+
57
+ # Search airlines
58
+ airportweb airlines search "Delta"
59
+ airportweb airlines search "Lufthansa"
60
+ ```
61
+
62
+ ### Routes
63
+
64
+ ```bash
65
+ # Get routes from an airport
66
+ airportweb routes get --from JFK
67
+
68
+ # Get routes to an airport
69
+ airportweb routes get --to LHR
70
+
71
+ # Get routes between airports
72
+ airportweb routes get --from JFK --to LHR
73
+
74
+ # Search routes with filters
75
+ airportweb routes search --from JFK --airline AA
76
+ airportweb routes search --from LAX --to SFO
77
+ ```
78
+
79
+ ### JSON Output
80
+
81
+ All commands support `--json` for machine-readable output:
82
+
83
+ ```bash
84
+ airportweb airports get JFK --json
85
+ airportweb airports search "London" --json | jq '.[].iata_code'
86
+ airportweb routes get --from JFK --json
87
+ ```
88
+
89
+ ## Examples
90
+
91
+ ```bash
92
+ # Find all airports in Germany
93
+ airportweb airports by-country DE
94
+
95
+ # Search for Delta airlines
96
+ airportweb airlines search "Delta" --json
97
+
98
+ # Get all routes from LAX
99
+ airportweb routes get --from LAX --json | jq '.[].arr_iata' | sort | uniq
100
+ ```
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ import(join(__dirname, '..', 'src', 'index.js'));
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@ktmcp-cli/airportweb",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready CLI for Airport Web API - Kill The MCP",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "airportweb": "bin/airportweb.js"
9
+ },
10
+ "keywords": ["airportweb", "airports", "airlines", "routes", "cli", "api", "ktmcp"],
11
+ "author": "KTMCP",
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "commander": "^12.0.0",
15
+ "axios": "^1.6.7",
16
+ "chalk": "^5.3.0",
17
+ "ora": "^8.0.1",
18
+ "conf": "^12.0.0"
19
+ },
20
+ "engines": { "node": ">=18.0.0" },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/ktmcp-cli/airportweb.git"
24
+ },
25
+ "homepage": "https://killthemcp.com/airportweb-cli",
26
+ "bugs": { "url": "https://github.com/ktmcp-cli/airportweb/issues" }
27
+ }
package/src/api.js ADDED
@@ -0,0 +1,140 @@
1
+ import axios from 'axios';
2
+
3
+ const BASE_URL = 'https://airport-web.appspot.com';
4
+
5
+ const client = axios.create({
6
+ baseURL: BASE_URL,
7
+ headers: {
8
+ 'Accept': 'application/json',
9
+ 'Content-Type': 'application/json'
10
+ },
11
+ timeout: 15000
12
+ });
13
+
14
+ function handleApiError(error) {
15
+ if (error.response) {
16
+ const status = error.response.status;
17
+ const data = error.response.data;
18
+ if (status === 404) {
19
+ throw new Error('Resource not found.');
20
+ } else if (status === 429) {
21
+ throw new Error('Rate limit exceeded. Please wait before retrying.');
22
+ } else {
23
+ const message = data?.message || data?.error || JSON.stringify(data);
24
+ throw new Error(`API Error (${status}): ${message}`);
25
+ }
26
+ } else if (error.request) {
27
+ throw new Error('No response from Airport Web API. Check your internet connection.');
28
+ } else {
29
+ throw error;
30
+ }
31
+ }
32
+
33
+ // ============================================================
34
+ // AIRPORTS
35
+ // ============================================================
36
+
37
+ export async function listAirports({ search, limit = 50 } = {}) {
38
+ try {
39
+ const params = {};
40
+ if (search) params.q = search;
41
+ if (limit) params.limit = limit;
42
+ const response = await client.get('/api/airports', { params });
43
+ return response.data || [];
44
+ } catch (error) {
45
+ handleApiError(error);
46
+ }
47
+ }
48
+
49
+ export async function getAirportByIata(iata) {
50
+ try {
51
+ const response = await client.get(`/api/airports/${iata.toUpperCase()}`);
52
+ return response.data || null;
53
+ } catch (error) {
54
+ handleApiError(error);
55
+ }
56
+ }
57
+
58
+ export async function listAirportsByCountry(country) {
59
+ try {
60
+ const params = { country };
61
+ const response = await client.get('/api/airports', { params });
62
+ return response.data || [];
63
+ } catch (error) {
64
+ handleApiError(error);
65
+ }
66
+ }
67
+
68
+ export async function searchAirports(query) {
69
+ try {
70
+ const params = { q: query };
71
+ const response = await client.get('/api/airports', { params });
72
+ return response.data || [];
73
+ } catch (error) {
74
+ handleApiError(error);
75
+ }
76
+ }
77
+
78
+ // ============================================================
79
+ // AIRLINES
80
+ // ============================================================
81
+
82
+ export async function listAirlines({ search, limit = 50 } = {}) {
83
+ try {
84
+ const params = {};
85
+ if (search) params.q = search;
86
+ if (limit) params.limit = limit;
87
+ const response = await client.get('/api/airlines', { params });
88
+ return response.data || [];
89
+ } catch (error) {
90
+ handleApiError(error);
91
+ }
92
+ }
93
+
94
+ export async function getAirline(iata) {
95
+ try {
96
+ const response = await client.get(`/api/airlines/${iata.toUpperCase()}`);
97
+ return response.data || null;
98
+ } catch (error) {
99
+ handleApiError(error);
100
+ }
101
+ }
102
+
103
+ export async function searchAirlines(query) {
104
+ try {
105
+ const params = { q: query };
106
+ const response = await client.get('/api/airlines', { params });
107
+ return response.data || [];
108
+ } catch (error) {
109
+ handleApiError(error);
110
+ }
111
+ }
112
+
113
+ // ============================================================
114
+ // ROUTES
115
+ // ============================================================
116
+
117
+ export async function getRoutes({ from, to } = {}) {
118
+ try {
119
+ const params = {};
120
+ if (from) params.dep_iata = from.toUpperCase();
121
+ if (to) params.arr_iata = to.toUpperCase();
122
+ const response = await client.get('/api/routes', { params });
123
+ return response.data || [];
124
+ } catch (error) {
125
+ handleApiError(error);
126
+ }
127
+ }
128
+
129
+ export async function searchRoutes({ from, to, airline } = {}) {
130
+ try {
131
+ const params = {};
132
+ if (from) params.dep_iata = from.toUpperCase();
133
+ if (to) params.arr_iata = to.toUpperCase();
134
+ if (airline) params.airline_iata = airline.toUpperCase();
135
+ const response = await client.get('/api/routes', { params });
136
+ return response.data || [];
137
+ } catch (error) {
138
+ handleApiError(error);
139
+ }
140
+ }
package/src/config.js ADDED
@@ -0,0 +1,22 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({ projectName: '@ktmcp-cli/airportweb' });
4
+
5
+ export function getConfig(key) {
6
+ return config.get(key);
7
+ }
8
+
9
+ export function setConfig(key, value) {
10
+ config.set(key, value);
11
+ }
12
+
13
+ export function isConfigured() {
14
+ // Airport Web API is public — no auth required
15
+ return true;
16
+ }
17
+
18
+ export function getAllConfig() {
19
+ return config.store;
20
+ }
21
+
22
+ export default config;
package/src/index.js ADDED
@@ -0,0 +1,440 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getConfig, setConfig, getAllConfig } from './config.js';
5
+ import {
6
+ listAirports,
7
+ getAirportByIata,
8
+ listAirportsByCountry,
9
+ searchAirports,
10
+ listAirlines,
11
+ getAirline,
12
+ searchAirlines,
13
+ getRoutes,
14
+ searchRoutes
15
+ } from './api.js';
16
+
17
+ const program = new Command();
18
+
19
+ // ============================================================
20
+ // Helpers
21
+ // ============================================================
22
+
23
+ function printSuccess(message) {
24
+ console.log(chalk.green('✓') + ' ' + message);
25
+ }
26
+
27
+ function printError(message) {
28
+ console.error(chalk.red('✗') + ' ' + message);
29
+ }
30
+
31
+ function printTable(data, columns) {
32
+ if (!data || data.length === 0) {
33
+ console.log(chalk.yellow('No results found.'));
34
+ return;
35
+ }
36
+
37
+ const widths = {};
38
+ columns.forEach(col => {
39
+ widths[col.key] = col.label.length;
40
+ data.forEach(row => {
41
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
42
+ if (val.length > widths[col.key]) widths[col.key] = val.length;
43
+ });
44
+ widths[col.key] = Math.min(widths[col.key], 40);
45
+ });
46
+
47
+ const header = columns.map(col => col.label.padEnd(widths[col.key])).join(' ');
48
+ console.log(chalk.bold(chalk.cyan(header)));
49
+ console.log(chalk.dim('─'.repeat(header.length)));
50
+
51
+ data.forEach(row => {
52
+ const line = columns.map(col => {
53
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
54
+ return val.substring(0, widths[col.key]).padEnd(widths[col.key]);
55
+ }).join(' ');
56
+ console.log(line);
57
+ });
58
+
59
+ console.log(chalk.dim(`\n${data.length} result(s)`));
60
+ }
61
+
62
+ function printJson(data) {
63
+ console.log(JSON.stringify(data, null, 2));
64
+ }
65
+
66
+ async function withSpinner(message, fn) {
67
+ const spinner = ora(message).start();
68
+ try {
69
+ const result = await fn();
70
+ spinner.stop();
71
+ return result;
72
+ } catch (error) {
73
+ spinner.stop();
74
+ throw error;
75
+ }
76
+ }
77
+
78
+ // ============================================================
79
+ // Program metadata
80
+ // ============================================================
81
+
82
+ program
83
+ .name('airportweb')
84
+ .description(chalk.bold('Airport Web CLI') + ' - Airports, airlines, and routes from your terminal')
85
+ .version('1.0.0');
86
+
87
+ // ============================================================
88
+ // CONFIG
89
+ // ============================================================
90
+
91
+ const configCmd = program.command('config').description('Manage CLI configuration');
92
+
93
+ configCmd
94
+ .command('get <key>')
95
+ .description('Get a configuration value')
96
+ .action((key) => {
97
+ const value = getConfig(key);
98
+ if (value === undefined) {
99
+ printError(`Key '${key}' not found`);
100
+ } else {
101
+ console.log(value);
102
+ }
103
+ });
104
+
105
+ configCmd
106
+ .command('set <key> <value>')
107
+ .description('Set a configuration value')
108
+ .action((key, value) => {
109
+ setConfig(key, value);
110
+ printSuccess(`Config '${key}' set`);
111
+ });
112
+
113
+ configCmd
114
+ .command('list')
115
+ .description('List all configuration values')
116
+ .action(() => {
117
+ const all = getAllConfig();
118
+ if (Object.keys(all).length === 0) {
119
+ console.log(chalk.yellow('No configuration set. Airport Web API is public — no auth required.'));
120
+ } else {
121
+ console.log(chalk.bold('\nAirport Web CLI Configuration\n'));
122
+ Object.entries(all).forEach(([k, v]) => {
123
+ console.log(`${k}: ${chalk.cyan(String(v))}`);
124
+ });
125
+ }
126
+ });
127
+
128
+ // ============================================================
129
+ // AIRPORTS
130
+ // ============================================================
131
+
132
+ const airportsCmd = program.command('airports').description('Search and browse airports');
133
+
134
+ airportsCmd
135
+ .command('search <query>')
136
+ .description('Search airports by name, city, or IATA code')
137
+ .option('--limit <n>', 'Maximum number of results', '50')
138
+ .option('--json', 'Output as JSON')
139
+ .action(async (query, options) => {
140
+ try {
141
+ const airports = await withSpinner(`Searching airports for "${query}"...`, () =>
142
+ searchAirports(query)
143
+ );
144
+
145
+ if (options.json) {
146
+ printJson(airports);
147
+ return;
148
+ }
149
+
150
+ const data = Array.isArray(airports) ? airports.slice(0, parseInt(options.limit)) : [];
151
+ printTable(data, [
152
+ { key: 'iata_code', label: 'IATA' },
153
+ { key: 'icao_code', label: 'ICAO' },
154
+ { key: 'name', label: 'Name' },
155
+ { key: 'city', label: 'City' },
156
+ { key: 'country', label: 'Country' },
157
+ { key: 'latitude', label: 'Lat', format: (v) => v ? String(parseFloat(v).toFixed(4)) : '' },
158
+ { key: 'longitude', label: 'Lon', format: (v) => v ? String(parseFloat(v).toFixed(4)) : '' }
159
+ ]);
160
+ } catch (error) {
161
+ printError(error.message);
162
+ process.exit(1);
163
+ }
164
+ });
165
+
166
+ airportsCmd
167
+ .command('get <iata>')
168
+ .description('Get airport details by IATA code')
169
+ .option('--json', 'Output as JSON')
170
+ .action(async (iata, options) => {
171
+ try {
172
+ const airport = await withSpinner(`Fetching airport ${iata.toUpperCase()}...`, () =>
173
+ getAirportByIata(iata)
174
+ );
175
+
176
+ if (!airport) {
177
+ printError('Airport not found');
178
+ process.exit(1);
179
+ }
180
+
181
+ if (options.json) {
182
+ printJson(airport);
183
+ return;
184
+ }
185
+
186
+ console.log(chalk.bold('\nAirport Details\n'));
187
+ console.log('IATA Code: ', chalk.cyan(airport.iata_code || iata.toUpperCase()));
188
+ console.log('ICAO Code: ', airport.icao_code || 'N/A');
189
+ console.log('Name: ', chalk.bold(airport.name || 'N/A'));
190
+ console.log('City: ', airport.city || 'N/A');
191
+ console.log('Country: ', airport.country || 'N/A');
192
+ console.log('Latitude: ', airport.latitude || 'N/A');
193
+ console.log('Longitude: ', airport.longitude || 'N/A');
194
+ console.log('Timezone: ', airport.timezone || 'N/A');
195
+ console.log('Elevation: ', airport.altitude ? `${airport.altitude} ft` : 'N/A');
196
+ console.log('');
197
+ } catch (error) {
198
+ printError(error.message);
199
+ process.exit(1);
200
+ }
201
+ });
202
+
203
+ airportsCmd
204
+ .command('list')
205
+ .description('List airports')
206
+ .option('--limit <n>', 'Maximum number of results', '50')
207
+ .option('--json', 'Output as JSON')
208
+ .action(async (options) => {
209
+ try {
210
+ const airports = await withSpinner('Fetching airports...', () =>
211
+ listAirports({ limit: parseInt(options.limit) })
212
+ );
213
+
214
+ if (options.json) {
215
+ printJson(airports);
216
+ return;
217
+ }
218
+
219
+ const data = Array.isArray(airports) ? airports : [];
220
+ printTable(data, [
221
+ { key: 'iata_code', label: 'IATA' },
222
+ { key: 'icao_code', label: 'ICAO' },
223
+ { key: 'name', label: 'Name' },
224
+ { key: 'city', label: 'City' },
225
+ { key: 'country', label: 'Country' }
226
+ ]);
227
+ } catch (error) {
228
+ printError(error.message);
229
+ process.exit(1);
230
+ }
231
+ });
232
+
233
+ airportsCmd
234
+ .command('by-country <country>')
235
+ .description('List airports by country code (e.g. US, DE, GB)')
236
+ .option('--json', 'Output as JSON')
237
+ .action(async (country, options) => {
238
+ try {
239
+ const airports = await withSpinner(`Fetching airports in ${country}...`, () =>
240
+ listAirportsByCountry(country)
241
+ );
242
+
243
+ if (options.json) {
244
+ printJson(airports);
245
+ return;
246
+ }
247
+
248
+ const data = Array.isArray(airports) ? airports : [];
249
+ printTable(data, [
250
+ { key: 'iata_code', label: 'IATA' },
251
+ { key: 'icao_code', label: 'ICAO' },
252
+ { key: 'name', label: 'Name' },
253
+ { key: 'city', label: 'City' },
254
+ { key: 'country', label: 'Country' }
255
+ ]);
256
+ } catch (error) {
257
+ printError(error.message);
258
+ process.exit(1);
259
+ }
260
+ });
261
+
262
+ // ============================================================
263
+ // AIRLINES
264
+ // ============================================================
265
+
266
+ const airlinesCmd = program.command('airlines').description('Search and browse airlines');
267
+
268
+ airlinesCmd
269
+ .command('list')
270
+ .description('List airlines')
271
+ .option('--limit <n>', 'Maximum number of results', '50')
272
+ .option('--json', 'Output as JSON')
273
+ .action(async (options) => {
274
+ try {
275
+ const airlines = await withSpinner('Fetching airlines...', () =>
276
+ listAirlines({ limit: parseInt(options.limit) })
277
+ );
278
+
279
+ if (options.json) {
280
+ printJson(airlines);
281
+ return;
282
+ }
283
+
284
+ const data = Array.isArray(airlines) ? airlines : [];
285
+ printTable(data, [
286
+ { key: 'iata_code', label: 'IATA' },
287
+ { key: 'icao_code', label: 'ICAO' },
288
+ { key: 'name', label: 'Name' },
289
+ { key: 'country', label: 'Country' },
290
+ { key: 'active', label: 'Active', format: (v) => v ? 'Yes' : 'No' }
291
+ ]);
292
+ } catch (error) {
293
+ printError(error.message);
294
+ process.exit(1);
295
+ }
296
+ });
297
+
298
+ airlinesCmd
299
+ .command('get <iata>')
300
+ .description('Get airline details by IATA code')
301
+ .option('--json', 'Output as JSON')
302
+ .action(async (iata, options) => {
303
+ try {
304
+ const airline = await withSpinner(`Fetching airline ${iata.toUpperCase()}...`, () =>
305
+ getAirline(iata)
306
+ );
307
+
308
+ if (!airline) {
309
+ printError('Airline not found');
310
+ process.exit(1);
311
+ }
312
+
313
+ if (options.json) {
314
+ printJson(airline);
315
+ return;
316
+ }
317
+
318
+ console.log(chalk.bold('\nAirline Details\n'));
319
+ console.log('IATA Code: ', chalk.cyan(airline.iata_code || iata.toUpperCase()));
320
+ console.log('ICAO Code: ', airline.icao_code || 'N/A');
321
+ console.log('Name: ', chalk.bold(airline.name || 'N/A'));
322
+ console.log('Country: ', airline.country || 'N/A');
323
+ console.log('Active: ', airline.active ? chalk.green('Yes') : chalk.red('No'));
324
+ console.log('');
325
+ } catch (error) {
326
+ printError(error.message);
327
+ process.exit(1);
328
+ }
329
+ });
330
+
331
+ airlinesCmd
332
+ .command('search <query>')
333
+ .description('Search airlines by name or code')
334
+ .option('--json', 'Output as JSON')
335
+ .action(async (query, options) => {
336
+ try {
337
+ const airlines = await withSpinner(`Searching airlines for "${query}"...`, () =>
338
+ searchAirlines(query)
339
+ );
340
+
341
+ if (options.json) {
342
+ printJson(airlines);
343
+ return;
344
+ }
345
+
346
+ const data = Array.isArray(airlines) ? airlines : [];
347
+ printTable(data, [
348
+ { key: 'iata_code', label: 'IATA' },
349
+ { key: 'icao_code', label: 'ICAO' },
350
+ { key: 'name', label: 'Name' },
351
+ { key: 'country', label: 'Country' },
352
+ { key: 'active', label: 'Active', format: (v) => v ? 'Yes' : 'No' }
353
+ ]);
354
+ } catch (error) {
355
+ printError(error.message);
356
+ process.exit(1);
357
+ }
358
+ });
359
+
360
+ // ============================================================
361
+ // ROUTES
362
+ // ============================================================
363
+
364
+ const routesCmd = program.command('routes').description('Search and browse flight routes');
365
+
366
+ routesCmd
367
+ .command('get')
368
+ .description('Get routes between airports')
369
+ .option('--from <iata>', 'Departure airport IATA code')
370
+ .option('--to <iata>', 'Arrival airport IATA code')
371
+ .option('--json', 'Output as JSON')
372
+ .action(async (options) => {
373
+ if (!options.from && !options.to) {
374
+ printError('Please specify at least --from or --to');
375
+ process.exit(1);
376
+ }
377
+
378
+ try {
379
+ const routes = await withSpinner('Fetching routes...', () =>
380
+ getRoutes({ from: options.from, to: options.to })
381
+ );
382
+
383
+ if (options.json) {
384
+ printJson(routes);
385
+ return;
386
+ }
387
+
388
+ const data = Array.isArray(routes) ? routes : [];
389
+ printTable(data, [
390
+ { key: 'dep_iata', label: 'From' },
391
+ { key: 'arr_iata', label: 'To' },
392
+ { key: 'airline_iata', label: 'Airline' },
393
+ { key: 'codeshare', label: 'Codeshare', format: (v) => v ? 'Yes' : 'No' }
394
+ ]);
395
+ } catch (error) {
396
+ printError(error.message);
397
+ process.exit(1);
398
+ }
399
+ });
400
+
401
+ routesCmd
402
+ .command('search')
403
+ .description('Search routes with filters')
404
+ .option('--from <iata>', 'Departure airport IATA code')
405
+ .option('--to <iata>', 'Arrival airport IATA code')
406
+ .option('--airline <iata>', 'Filter by airline IATA code')
407
+ .option('--json', 'Output as JSON')
408
+ .action(async (options) => {
409
+ try {
410
+ const routes = await withSpinner('Searching routes...', () =>
411
+ searchRoutes({ from: options.from, to: options.to, airline: options.airline })
412
+ );
413
+
414
+ if (options.json) {
415
+ printJson(routes);
416
+ return;
417
+ }
418
+
419
+ const data = Array.isArray(routes) ? routes : [];
420
+ printTable(data, [
421
+ { key: 'dep_iata', label: 'From' },
422
+ { key: 'arr_iata', label: 'To' },
423
+ { key: 'airline_iata', label: 'Airline' },
424
+ { key: 'codeshare', label: 'Codeshare', format: (v) => v ? 'Yes' : 'No' }
425
+ ]);
426
+ } catch (error) {
427
+ printError(error.message);
428
+ process.exit(1);
429
+ }
430
+ });
431
+
432
+ // ============================================================
433
+ // Parse
434
+ // ============================================================
435
+
436
+ program.parse(process.argv);
437
+
438
+ if (process.argv.length <= 2) {
439
+ program.help();
440
+ }