turkiyem 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/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # turkiyem
2
+
3
+ Turkiye Toplu Tasima ve Deprem CLI araci.
4
+
5
+ AFAD deprem verileri, EGO (Ankara) otobus saatleri ve IETT (Istanbul) GTFS hat bilgilerini terminalden sorgulayabilirsiniz.
6
+
7
+ ## Kurulum
8
+
9
+ ```bash
10
+ npm install -g turkiyem
11
+ ```
12
+
13
+ Veya yerel olarak:
14
+
15
+ ```bash
16
+ git clone <repo-url>
17
+ cd turkiyemCLI
18
+ npm install
19
+ npm link
20
+ ```
21
+
22
+ ## Gereksinimler
23
+
24
+ - Node.js 20+
25
+
26
+ ## Kullanim
27
+
28
+ ### Banner ve Yardim
29
+
30
+ ```bash
31
+ turkiyem
32
+ turkiyem help
33
+ ```
34
+
35
+ ### Sehir Secimi
36
+
37
+ Hat sorgulama icin once sehir secmelisiniz:
38
+
39
+ ```bash
40
+ turkiyem sehir ankara
41
+ turkiyem sehir istanbul
42
+ ```
43
+
44
+ ### Hat Sorgulama
45
+
46
+ Secili sehre gore hat bilgilerini sorgular:
47
+
48
+ ```bash
49
+ # Ankara (EGO)
50
+ turkiyem sehir ankara
51
+ turkiyem hat 340
52
+
53
+ # Istanbul (IETT)
54
+ turkiyem sehir istanbul
55
+ turkiyem hat 34AS
56
+ ```
57
+
58
+ Ankara icin EGO web sitesinden sefer saatleri cekilir.
59
+ Istanbul icin IETT GTFS verilerinden hat bilgileri (durak sayisi, ilk/son durak) gosterilir.
60
+
61
+ ### Deprem Sorgulama
62
+
63
+ AFAD API uzerinden gercek zamanli deprem verileri:
64
+
65
+ ```bash
66
+ # Son 24 saat
67
+ turkiyem deprem son24
68
+
69
+ # Son 7 gun
70
+ turkiyem deprem 7gun
71
+
72
+ # Buyukluge gore filtrele (ornegin >= 4.0)
73
+ turkiyem deprem buyukluk 4.0
74
+ ```
75
+
76
+ Buyuklugu 4.0 ve ustu olan depremler kirmizi ile vurgulanir.
77
+
78
+ ### Temizleme
79
+
80
+ Cache ve yapilandirmayi sifirlar:
81
+
82
+ ```bash
83
+ turkiyem temizle
84
+ ```
85
+
86
+ ### Versiyon
87
+
88
+ ```bash
89
+ turkiyem --version
90
+ ```
91
+
92
+ ## Yapilandirma
93
+
94
+ Secili sehir `~/.turkiyem/config.json` dosyasinda saklanir. Bu dosya otomatik olusturulur.
95
+
96
+ ## Veri Kaynaklari
97
+
98
+ | Kaynak | Aciklama |
99
+ |--------|----------|
100
+ | AFAD | Deprem verileri (deprem.afad.gov.tr) |
101
+ | EGO | Ankara otobus sefer saatleri (ego.gov.tr) |
102
+ | IETT | Istanbul GTFS hat verileri (data.ibb.gov.tr) |
103
+
104
+ ## npm Publish
105
+
106
+ 1. npm hesabiniza giris yapin:
107
+
108
+ ```bash
109
+ npm login
110
+ ```
111
+
112
+ 2. package.json icindeki `name`, `version`, `author` alanlarini duzenleyin.
113
+
114
+ 3. Yayinlayin:
115
+
116
+ ```bash
117
+ npm publish
118
+ ```
119
+
120
+ 4. Guncelleme icin versiyonu artirin:
121
+
122
+ ```bash
123
+ npm version patch
124
+ npm publish
125
+ ```
126
+
127
+ ## Lisans
128
+
129
+ MIT
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "turkiyem",
3
+ "version": "1.0.0",
4
+ "description": "Türkiye Toplu Taşıma ve Deprem CLI aracı - AFAD deprem verileri, EGO/IETT hat bilgileri",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "bin": {
8
+ "turkiyem": "./src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js",
12
+ "test": "echo \"No tests yet\""
13
+ },
14
+ "keywords": [
15
+ "turkiye",
16
+ "deprem",
17
+ "afad",
18
+ "iett",
19
+ "ego",
20
+ "toplu-tasima",
21
+ "cli",
22
+ "earthquake",
23
+ "transit"
24
+ ],
25
+ "author": "",
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=20.0.0"
29
+ },
30
+ "dependencies": {
31
+ "axios": "^1.7.9",
32
+ "chalk": "^5.4.1",
33
+ "cheerio": "^1.0.0",
34
+ "cli-table3": "^0.6.5",
35
+ "commander": "^13.1.0",
36
+ "csv-parse": "^5.6.0",
37
+ "dotenv": "^16.4.7",
38
+ "node-cache": "^5.1.2",
39
+ "ora": "^8.2.0"
40
+ },
41
+ "files": [
42
+ "src/**/*",
43
+ "README.md"
44
+ ]
45
+ }
@@ -0,0 +1,67 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { fetchEarthquakes, fetchByMagnitude } from '../services/afadService.js';
4
+ import { createEarthquakeTable } from '../utils/display.js';
5
+
6
+ export async function depremSon24() {
7
+ const spinner = ora('AFAD verileri alınıyor (son 24 saat)...').start();
8
+
9
+ try {
10
+ const earthquakes = await fetchEarthquakes('son24');
11
+
12
+ if (!earthquakes || earthquakes.length === 0) {
13
+ spinner.info('Son 24 saatte kayıtlı deprem bulunamadı.');
14
+ return;
15
+ }
16
+
17
+ spinner.succeed(`${earthquakes.length} deprem bulundu (son 24 saat)`);
18
+ console.log('');
19
+ console.log(createEarthquakeTable(earthquakes));
20
+ } catch (err) {
21
+ spinner.fail(chalk.red(err.message));
22
+ }
23
+ }
24
+
25
+ export async function deprem7Gun() {
26
+ const spinner = ora('AFAD verileri alınıyor (son 7 gün)...').start();
27
+
28
+ try {
29
+ const earthquakes = await fetchEarthquakes('7gun');
30
+
31
+ if (!earthquakes || earthquakes.length === 0) {
32
+ spinner.info('Son 7 günde kayıtlı deprem bulunamadı.');
33
+ return;
34
+ }
35
+
36
+ spinner.succeed(`${earthquakes.length} deprem bulundu (son 7 gün)`);
37
+ console.log('');
38
+ console.log(createEarthquakeTable(earthquakes));
39
+ } catch (err) {
40
+ spinner.fail(chalk.red(err.message));
41
+ }
42
+ }
43
+
44
+ export async function depremBuyukluk(value) {
45
+ const min = parseFloat(value);
46
+ if (isNaN(min)) {
47
+ console.log(chalk.red('Geçerli bir büyüklük değeri girin. Örnek: turkiyem deprem buyukluk 4.0'));
48
+ return;
49
+ }
50
+
51
+ const spinner = ora(`Büyüklüğü >= ${min} olan depremler aranıyor...`).start();
52
+
53
+ try {
54
+ const earthquakes = await fetchByMagnitude(min);
55
+
56
+ if (!earthquakes || earthquakes.length === 0) {
57
+ spinner.info(`Büyüklüğü >= ${min} olan deprem bulunamadı (son 7 gün).`);
58
+ return;
59
+ }
60
+
61
+ spinner.succeed(`${earthquakes.length} deprem bulundu (büyüklük >= ${min})`);
62
+ console.log('');
63
+ console.log(createEarthquakeTable(earthquakes));
64
+ } catch (err) {
65
+ spinner.fail(chalk.red(err.message));
66
+ }
67
+ }
@@ -0,0 +1,70 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getCity } from '../utils/config.js';
4
+ import { fetchEgoSchedule } from '../services/egoService.js';
5
+ import { fetchIettRoute } from '../services/iettService.js';
6
+ import { createEgoInfoTable, createEgoScheduleTable, createRouteTable } from '../utils/display.js';
7
+
8
+ export async function hatSorgula(hatNo) {
9
+ if (!hatNo) {
10
+ console.log(chalk.red('Hat numarası belirtmelisiniz. Örnek: turkiyem hat 340'));
11
+ return;
12
+ }
13
+
14
+ const city = getCity();
15
+
16
+ if (!city) {
17
+ console.log(chalk.yellow('Henüz şehir seçmediniz. Önce şehir seçin:'));
18
+ console.log(chalk.cyan(' turkiyem sehir ankara'));
19
+ console.log(chalk.cyan(' turkiyem sehir istanbul'));
20
+ return;
21
+ }
22
+
23
+ if (city === 'ankara') {
24
+ await queryEgo(hatNo);
25
+ } else if (city === 'istanbul') {
26
+ await queryIett(hatNo);
27
+ } else {
28
+ console.log(chalk.red(`Desteklenmeyen şehir: ${city}. ankara veya istanbul seçin.`));
29
+ }
30
+ }
31
+
32
+ async function queryEgo(hatNo) {
33
+ const spinner = ora(`EGO hat ${hatNo} bilgileri alınıyor...`).start();
34
+
35
+ try {
36
+ const result = await fetchEgoSchedule(hatNo);
37
+
38
+ spinner.succeed(`Hat ${hatNo} bilgileri alındı`);
39
+ console.log('');
40
+
41
+ console.log(chalk.white.bold(' Hat Bilgileri'));
42
+ console.log(createEgoInfoTable(result.info));
43
+ console.log('');
44
+
45
+ if (result.schedule && result.schedule.length > 0) {
46
+ console.log(chalk.white.bold(' Sefer Saatleri'));
47
+ console.log(createEgoScheduleTable(result.schedule));
48
+ } else {
49
+ console.log(chalk.yellow(' Sefer saati bilgisi bulunamadı.'));
50
+ }
51
+ } catch (err) {
52
+ spinner.fail(chalk.red(err.message));
53
+ }
54
+ }
55
+
56
+ async function queryIett(hatNo) {
57
+ const spinner = ora(`IETT hat ${hatNo} bilgileri alınıyor (GTFS verileri indiriliyor)...`).start();
58
+
59
+ try {
60
+ const route = await fetchIettRoute(hatNo);
61
+
62
+ spinner.succeed(`Hat ${hatNo} bilgileri alındı`);
63
+ console.log('');
64
+
65
+ console.log(chalk.white.bold(' Hat Bilgileri (IETT)'));
66
+ console.log(createRouteTable(route));
67
+ } catch (err) {
68
+ spinner.fail(chalk.red(err.message));
69
+ }
70
+ }
@@ -0,0 +1,34 @@
1
+ import chalk from 'chalk';
2
+ import { setCity, getCity } from '../utils/config.js';
3
+
4
+ const SUPPORTED_CITIES = ['ankara', 'istanbul'];
5
+
6
+ export function sehirSec(city) {
7
+ if (!city) {
8
+ const current = getCity();
9
+ if (current) {
10
+ console.log(chalk.green(`Şu an seçili şehir: ${chalk.bold(current)}`));
11
+ } else {
12
+ console.log(chalk.yellow('Henüz şehir seçilmemiş.'));
13
+ }
14
+ console.log('');
15
+ console.log(chalk.white('Kullanım:'));
16
+ console.log(chalk.cyan(' turkiyem sehir ankara'));
17
+ console.log(chalk.cyan(' turkiyem sehir istanbul'));
18
+ return;
19
+ }
20
+
21
+ const normalized = city.toLowerCase().trim();
22
+
23
+ if (!SUPPORTED_CITIES.includes(normalized)) {
24
+ console.log(chalk.red(`"${city}" desteklenmiyor.`));
25
+ console.log(chalk.white('Desteklenen şehirler:'));
26
+ SUPPORTED_CITIES.forEach((c) => {
27
+ console.log(chalk.cyan(` - ${c}`));
28
+ });
29
+ return;
30
+ }
31
+
32
+ setCity(normalized);
33
+ console.log(chalk.green(`Şehir "${chalk.bold(normalized)}" olarak ayarlandı.`));
34
+ }
@@ -0,0 +1,13 @@
1
+ import chalk from 'chalk';
2
+ import { flushCache } from '../utils/cache.js';
3
+ import { resetConfig } from '../utils/config.js';
4
+
5
+ export function temizle() {
6
+ try {
7
+ flushCache();
8
+ resetConfig();
9
+ console.log(chalk.green('Cache ve yapılandırma başarıyla temizlendi.'));
10
+ } catch (err) {
11
+ console.log(chalk.red(`Temizleme sırasında hata oluştu: ${err.message}`));
12
+ }
13
+ }
package/src/index.js ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+
3
+ import 'dotenv/config';
4
+ import { Command } from 'commander';
5
+ import { createRequire } from 'node:module';
6
+ import { printBanner, printHelp } from './utils/banner.js';
7
+ import { sehirSec } from './commands/sehir.js';
8
+ import { hatSorgula } from './commands/hat.js';
9
+ import { depremSon24, deprem7Gun, depremBuyukluk } from './commands/deprem.js';
10
+ import { temizle } from './commands/temizle.js';
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const pkg = require('../package.json');
14
+
15
+ process.on('unhandledRejection', (err) => {
16
+ console.error('Beklenmeyen hata:', err?.message || err);
17
+ process.exit(1);
18
+ });
19
+
20
+ process.on('uncaughtException', (err) => {
21
+ console.error('Kritik hata:', err?.message || err);
22
+ process.exit(1);
23
+ });
24
+
25
+ const program = new Command();
26
+
27
+ program
28
+ .name('turkiyem')
29
+ .description('Türkiye Toplu Taşıma ve Deprem CLI')
30
+ .version(pkg.version, '-v, --version', 'Versiyon göster')
31
+ .helpOption('-h, --help', 'Yardım göster')
32
+ .addHelpCommand(false);
33
+
34
+ program
35
+ .command('sehir [sehir]')
36
+ .description('Şehir seç (ankara veya istanbul)')
37
+ .action((sehir) => {
38
+ sehirSec(sehir);
39
+ });
40
+
41
+ program
42
+ .command('hat <numara>')
43
+ .description('Hat bilgilerini sorgula')
44
+ .action(async (numara) => {
45
+ await hatSorgula(numara);
46
+ });
47
+
48
+ const depremCmd = program
49
+ .command('deprem')
50
+ .description('Deprem verileri sorgula');
51
+
52
+ depremCmd
53
+ .command('son24')
54
+ .description('Son 24 saat depremler')
55
+ .action(async () => {
56
+ await depremSon24();
57
+ });
58
+
59
+ depremCmd
60
+ .command('7gun')
61
+ .description('Son 7 gün depremler')
62
+ .action(async () => {
63
+ await deprem7Gun();
64
+ });
65
+
66
+ depremCmd
67
+ .command('buyukluk <deger>')
68
+ .description('Büyüklüğe göre filtrele (ör: 4.0)')
69
+ .action(async (deger) => {
70
+ await depremBuyukluk(deger);
71
+ });
72
+
73
+ program
74
+ .command('temizle')
75
+ .description('Cache ve yapılandırmayı temizle')
76
+ .action(() => {
77
+ temizle();
78
+ });
79
+
80
+ program
81
+ .command('help')
82
+ .description('Yardım göster')
83
+ .action(() => {
84
+ printHelp();
85
+ });
86
+
87
+ if (process.argv.length <= 2) {
88
+ printHelp();
89
+ } else {
90
+ program.parse(process.argv);
91
+ }
@@ -0,0 +1,110 @@
1
+ import axios from 'axios';
2
+ import { getCached, setCached } from '../utils/cache.js';
3
+
4
+ const AFAD_URL = 'https://deprem.afad.gov.tr/apiv2/event/filter';
5
+
6
+ function formatISODate(date) {
7
+ return date.toISOString().replace(/\.\d{3}Z$/, '');
8
+ }
9
+
10
+ function computeTimeRange(period) {
11
+ const end = new Date();
12
+ const start = new Date();
13
+
14
+ if (period === 'son24') {
15
+ start.setHours(start.getHours() - 24);
16
+ } else if (period === '7gun') {
17
+ start.setDate(start.getDate() - 7);
18
+ } else {
19
+ start.setDate(start.getDate() - 1);
20
+ }
21
+
22
+ return { start: formatISODate(start), end: formatISODate(end) };
23
+ }
24
+
25
+ function normalizeQuake(raw) {
26
+ return {
27
+ magnitude: raw.magnitude ?? raw.mag ?? raw.Magnitude ?? '-',
28
+ depth: raw.depth ?? raw.Depth ?? raw.derinlik ?? '-',
29
+ location: raw.location ?? raw.Location ?? raw.title ?? raw.yer ?? '-',
30
+ date: raw.date ?? raw.Date ?? raw.eventDate ?? '-',
31
+ latitude: raw.latitude ?? raw.Latitude ?? '-',
32
+ longitude: raw.longitude ?? raw.Longitude ?? '-',
33
+ type: raw.type ?? raw.Type ?? '-',
34
+ };
35
+ }
36
+
37
+ export async function fetchEarthquakes(period, limit = 20) {
38
+ const cacheKey = `afad_${period}_${limit}`;
39
+ const cached = getCached(cacheKey);
40
+ if (cached) return cached;
41
+
42
+ const { start, end } = computeTimeRange(period);
43
+
44
+ const params = {
45
+ start,
46
+ end,
47
+ format: 'json',
48
+ limit,
49
+ orderby: 'timedesc',
50
+ };
51
+
52
+ try {
53
+ const response = await axios.get(AFAD_URL, {
54
+ params,
55
+ timeout: 15000,
56
+ maxRedirects: 5,
57
+ headers: {
58
+ 'User-Agent': 'turkiyem-cli/1.0',
59
+ 'Accept': 'application/json',
60
+ },
61
+ validateStatus(status) {
62
+ return status >= 200 && status < 600;
63
+ },
64
+ });
65
+
66
+ if (response.status === 500) {
67
+ const body = response.data;
68
+ if (body && body.status === 400) {
69
+ throw new Error(`AFAD API hata döndürdü: ${body.message || 'Geçersiz istek parametreleri'}`);
70
+ }
71
+ throw new Error(`AFAD API sunucu hatası (HTTP 500)`);
72
+ }
73
+
74
+ if (response.status < 200 || response.status >= 300) {
75
+ throw new Error(`AFAD API HTTP ${response.status} hatası döndürdü`);
76
+ }
77
+
78
+ const data = response.data;
79
+
80
+ if (!Array.isArray(data)) {
81
+ throw new Error('AFAD API beklenmeyen yanıt formatı döndürdü');
82
+ }
83
+
84
+ const earthquakes = data.map(normalizeQuake);
85
+ setCached(cacheKey, earthquakes);
86
+ return earthquakes;
87
+ } catch (err) {
88
+ if (err.code === 'ECONNABORTED') {
89
+ throw new Error('AFAD API isteği zaman aşımına uğradı. Lütfen tekrar deneyin.');
90
+ }
91
+ if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED') {
92
+ throw new Error('AFAD API sunucusuna bağlanılamıyor. İnternet bağlantınızı kontrol edin.');
93
+ }
94
+ throw err;
95
+ }
96
+ }
97
+
98
+ export async function fetchByMagnitude(minMagnitude) {
99
+ const cacheKey = `afad_mag_${minMagnitude}`;
100
+ const cached = getCached(cacheKey);
101
+ if (cached) return cached;
102
+
103
+ const earthquakes = await fetchEarthquakes('7gun', 100);
104
+ const filtered = earthquakes.filter(
105
+ (eq) => parseFloat(eq.magnitude) >= parseFloat(minMagnitude)
106
+ );
107
+
108
+ setCached(cacheKey, filtered);
109
+ return filtered;
110
+ }
@@ -0,0 +1,124 @@
1
+ import axios from 'axios';
2
+ import * as cheerio from 'cheerio';
3
+ import { getCached, setCached } from '../utils/cache.js';
4
+
5
+ const EGO_URL = 'https://www.ego.gov.tr/hareketsaatleri';
6
+
7
+ function parseInfoTable($) {
8
+ const info = {
9
+ hatNo: '',
10
+ hatAdi: '',
11
+ kalkis: '',
12
+ varis: '',
13
+ mesafe: '',
14
+ sure: '',
15
+ };
16
+
17
+ const infoTable = $('.hatbilgileri table');
18
+ if (!infoTable.length) return info;
19
+
20
+ infoTable.find('tr').each((_, row) => {
21
+ const cells = $(row).find('td');
22
+ if (cells.length >= 3) {
23
+ const label = $(cells[0]).text().trim().toLowerCase();
24
+ const value = $(cells[2]).text().trim();
25
+
26
+ if (label.includes('hat no')) info.hatNo = value;
27
+ else if (label.includes('hat adı') || label.includes('hat ismi')) info.hatAdi = value;
28
+ else if (label.includes('kalkış')) info.kalkis = value;
29
+ else if (label.includes('varış')) info.varis = value;
30
+ else if (label.includes('mesafe')) info.mesafe = value;
31
+ else if (label.includes('süre')) info.sure = value;
32
+ }
33
+ });
34
+
35
+ return info;
36
+ }
37
+
38
+ function parseScheduleTable($, hatNo) {
39
+ const schedule = [];
40
+ const scheduleTable = $('.hareket-saatleri table');
41
+ if (!scheduleTable.length) return schedule;
42
+
43
+ const headers = [];
44
+ scheduleTable.find('th').each((_, th) => {
45
+ headers.push($(th).text().trim());
46
+ });
47
+
48
+ const dataRow = scheduleTable.find('tr').last();
49
+ const cells = dataRow.find('td');
50
+
51
+ cells.each((i, cell) => {
52
+ const raw = $(cell).html() || '';
53
+ const times = raw
54
+ .split(/<br\s*\/?>/i)
55
+ .map((t) => t.replace(/<[^>]*>/g, '').trim())
56
+ .filter((t) => t.length > 0)
57
+ .map((t) => t.replace(/\s*-\s*$/, '').trim())
58
+ .filter((t) => t.length > 0);
59
+
60
+ if (times.length > 0) {
61
+ const gunTipi = headers[i] || ['Hafta içi', 'Cumartesi', 'Pazar'][i] || `Sütun ${i + 1}`;
62
+ schedule.push({
63
+ hat: String(hatNo),
64
+ gunTipi,
65
+ saatler: times.join(', '),
66
+ });
67
+ }
68
+ });
69
+
70
+ return schedule;
71
+ }
72
+
73
+ export async function fetchEgoSchedule(hatNo) {
74
+ const cacheKey = `ego_${hatNo}`;
75
+ const cached = getCached(cacheKey);
76
+ if (cached) return cached;
77
+
78
+ try {
79
+ const response = await axios.get(EGO_URL, {
80
+ params: { hat_no: hatNo },
81
+ timeout: 15000,
82
+ maxRedirects: 5,
83
+ headers: {
84
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
85
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
86
+ 'Accept-Language': 'tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7',
87
+ },
88
+ responseType: 'text',
89
+ });
90
+
91
+ if (response.status !== 200) {
92
+ throw new Error(`EGO sunucusu HTTP ${response.status} döndürdü`);
93
+ }
94
+
95
+ const html = response.data;
96
+ if (!html || html.length < 100) {
97
+ throw new Error('EGO sunucusundan geçerli yanıt alınamadı');
98
+ }
99
+
100
+ const $ = cheerio.load(html);
101
+ const info = parseInfoTable($);
102
+
103
+ if (!info.hatNo) {
104
+ info.hatNo = String(hatNo);
105
+ }
106
+
107
+ const schedule = parseScheduleTable($, hatNo);
108
+
109
+ const result = { info, schedule };
110
+ setCached(cacheKey, result);
111
+ return result;
112
+ } catch (err) {
113
+ if (err.code === 'ECONNABORTED') {
114
+ throw new Error('EGO sunucusu isteği zaman aşımına uğradı. Lütfen tekrar deneyin.');
115
+ }
116
+ if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED') {
117
+ throw new Error('EGO sunucusuna bağlanılamıyor. İnternet bağlantınızı kontrol edin.');
118
+ }
119
+ if (err.response && err.response.status === 404) {
120
+ throw new Error(`Hat numarası "${hatNo}" bulunamadı.`);
121
+ }
122
+ throw err;
123
+ }
124
+ }
@@ -0,0 +1,153 @@
1
+ import axios from 'axios';
2
+ import { getCached, setCached } from '../utils/cache.js';
3
+
4
+ const DATASTORE_SQL_URL = 'https://data.ibb.gov.tr/api/3/action/datastore_search_sql';
5
+ const DATASTORE_SEARCH_URL = 'https://data.ibb.gov.tr/api/3/action/datastore_search';
6
+
7
+ const RESOURCE_IDS = {
8
+ routes: '46dbe388-c8c2-45c4-ac72-c06953de56a2',
9
+ trips: '7ff49bdd-b0d2-4a6e-9392-b598f77f5070',
10
+ stopTimes: '23778613-16fe-4d30-b8b8-8ca934ed2978',
11
+ stops: '2299bc82-983b-4bdf-8520-5cef8c555e29',
12
+ };
13
+
14
+ const API_TIMEOUT = 30000;
15
+ const API_HEADERS = { 'User-Agent': 'turkiyem-cli/1.0' };
16
+
17
+ function escapeSQL(str) {
18
+ return str.replace(/'/g, "''");
19
+ }
20
+
21
+ async function datastoreSQL(sql) {
22
+ try {
23
+ const response = await axios.get(DATASTORE_SQL_URL, {
24
+ params: { sql },
25
+ timeout: API_TIMEOUT,
26
+ headers: API_HEADERS,
27
+ });
28
+
29
+ if (!response.data?.success) {
30
+ throw new Error('IETT SQL sorgusu başarısız');
31
+ }
32
+
33
+ return response.data.result.records;
34
+ } catch (err) {
35
+ if (err.response?.status === 403) {
36
+ throw new Error('IETT veri kaynağı erişim engeli (403 Forbidden). Lütfen daha sonra tekrar deneyin.');
37
+ }
38
+ if (err.code === 'ECONNABORTED') {
39
+ throw new Error('IETT veri kaynağı zaman aşımına uğradı. Lütfen tekrar deneyin.');
40
+ }
41
+ throw err;
42
+ }
43
+ }
44
+
45
+ async function datastoreSearch(resourceId, options = {}) {
46
+ const params = {
47
+ resource_id: resourceId,
48
+ limit: options.limit || 100,
49
+ };
50
+
51
+ if (options.filters) params.filters = JSON.stringify(options.filters);
52
+ if (options.sort) params.sort = options.sort;
53
+
54
+ try {
55
+ const response = await axios.get(DATASTORE_SEARCH_URL, {
56
+ params,
57
+ timeout: API_TIMEOUT,
58
+ headers: API_HEADERS,
59
+ });
60
+
61
+ if (!response.data?.success) {
62
+ throw new Error('IETT veri kaynağı başarısız yanıt döndürdü');
63
+ }
64
+
65
+ return response.data.result;
66
+ } catch (err) {
67
+ if (err.response?.status === 403) {
68
+ throw new Error('IETT veri kaynağı erişim engeli (403 Forbidden). Lütfen daha sonra tekrar deneyin.');
69
+ }
70
+ if (err.code === 'ECONNABORTED') {
71
+ throw new Error('IETT veri kaynağı zaman aşımına uğradı. Lütfen tekrar deneyin.');
72
+ }
73
+ throw err;
74
+ }
75
+ }
76
+
77
+ function extractStopsFromName(longName) {
78
+ if (!longName || longName === '-') return { first: '-', last: '-' };
79
+
80
+ const separators = [' - ', ' – ', '-'];
81
+ for (const sep of separators) {
82
+ const parts = longName.split(sep).map((p) => p.trim()).filter(Boolean);
83
+ if (parts.length >= 2) {
84
+ return { first: parts[0], last: parts[parts.length - 1] };
85
+ }
86
+ }
87
+
88
+ return { first: longName, last: '-' };
89
+ }
90
+
91
+ async function tryGetStopCount(routeId) {
92
+ try {
93
+ const tripsResult = await datastoreSearch(RESOURCE_IDS.trips, {
94
+ filters: { route_id: routeId },
95
+ limit: 10,
96
+ });
97
+
98
+ const trips = tripsResult.records;
99
+ if (trips.length === 0) return 0;
100
+
101
+ for (const trip of trips) {
102
+ const stResult = await datastoreSearch(RESOURCE_IDS.stopTimes, {
103
+ filters: { trip_id: trip.trip_id },
104
+ limit: 1,
105
+ });
106
+
107
+ if (stResult.total > 0) {
108
+ return stResult.total;
109
+ }
110
+ }
111
+
112
+ return 0;
113
+ } catch {
114
+ return 0;
115
+ }
116
+ }
117
+
118
+ export async function fetchIettRoute(routeCode) {
119
+ const cacheKey = `iett_route_${routeCode.toUpperCase()}`;
120
+ const cached = getCached(cacheKey);
121
+ if (cached) return cached;
122
+
123
+ const code = routeCode.toUpperCase();
124
+ const sql = `SELECT * FROM "${RESOURCE_IDS.routes}" WHERE UPPER(route_short_name)='${escapeSQL(code)}' LIMIT 20`;
125
+ const routes = await datastoreSQL(sql);
126
+
127
+ if (!routes || routes.length === 0) {
128
+ throw new Error(`"${routeCode}" hat numarası IETT verilerinde bulunamadı.`);
129
+ }
130
+
131
+ const route = routes[0];
132
+ const { first, last } = extractStopsFromName(route.route_long_name);
133
+
134
+ const routeCount = routes.length;
135
+ const directions = routes
136
+ .map((r) => r.route_long_name)
137
+ .filter((v, i, a) => a.indexOf(v) === i);
138
+
139
+ const stopCount = await tryGetStopCount(route.route_id);
140
+
141
+ const result = {
142
+ routeShortName: route.route_short_name || code,
143
+ routeLongName: route.route_long_name || '-',
144
+ stopCount,
145
+ firstStop: first,
146
+ lastStop: last,
147
+ routeVariants: routeCount,
148
+ directions,
149
+ };
150
+
151
+ setCached(cacheKey, result);
152
+ return result;
153
+ }
@@ -0,0 +1,31 @@
1
+ import chalk from 'chalk';
2
+
3
+ const BANNER = `
4
+ ${chalk.red.bold(' ████████╗██╗ ██╗██████╗ ██╗ ██╗██╗██╗ ██╗███████╗███╗ ███╗')}
5
+ ${chalk.red.bold(' ╚══██╔══╝██║ ██║██╔══██╗██║ ██╔╝██║╚██╗ ██╔╝██╔════╝████╗ ████║')}
6
+ ${chalk.red.bold(' ██║ ██║ ██║██████╔╝█████╔╝ ██║ ╚████╔╝ █████╗ ██╔████╔██║')}
7
+ ${chalk.red.bold(' ██║ ██║ ██║██╔══██╗██╔═██╗ ██║ ╚██╔╝ ██╔══╝ ██║╚██╔╝██║')}
8
+ ${chalk.red.bold(' ██║ ╚██████╔╝██║ ██║██║ ██╗██║ ██║ ███████╗██║ ╚═╝ ██║')}
9
+ ${chalk.red.bold(' ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝')}
10
+
11
+ ${chalk.white.bold('☾ ★')}
12
+
13
+ ${chalk.gray('Türkiye Toplu Taşıma ve Deprem CLI')}
14
+ `;
15
+
16
+ export function printBanner() {
17
+ console.log(BANNER);
18
+ }
19
+
20
+ export function printHelp() {
21
+ printBanner();
22
+ console.log(chalk.white.bold(' Komutlar:\n'));
23
+ console.log(chalk.cyan(' turkiyem sehir <ankara|istanbul>') + chalk.gray(' Şehir seç'));
24
+ console.log(chalk.cyan(' turkiyem hat <numara>') + chalk.gray(' Hat sorgula'));
25
+ console.log(chalk.cyan(' turkiyem deprem son24') + chalk.gray(' Son 24 saat depremler'));
26
+ console.log(chalk.cyan(' turkiyem deprem 7gun') + chalk.gray(' Son 7 gün depremler'));
27
+ console.log(chalk.cyan(' turkiyem deprem buyukluk <deger>') + chalk.gray(' Büyüklüğe göre filtrele'));
28
+ console.log(chalk.cyan(' turkiyem temizle') + chalk.gray(' Cache ve config temizle'));
29
+ console.log(chalk.cyan(' turkiyem --version') + chalk.gray(' Versiyon göster'));
30
+ console.log('');
31
+ }
@@ -0,0 +1,21 @@
1
+ import NodeCache from 'node-cache';
2
+
3
+ const cache = new NodeCache({ stdTTL: 300, checkperiod: 60 });
4
+
5
+ export function getCached(key) {
6
+ return cache.get(key) || null;
7
+ }
8
+
9
+ export function setCached(key, value, ttl) {
10
+ if (ttl !== undefined) {
11
+ cache.set(key, value, ttl);
12
+ } else {
13
+ cache.set(key, value);
14
+ }
15
+ }
16
+
17
+ export function flushCache() {
18
+ cache.flushAll();
19
+ }
20
+
21
+ export default cache;
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.turkiyem');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+
8
+ const DEFAULT_CONFIG = { city: null };
9
+
10
+ function ensureDir() {
11
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
12
+ }
13
+
14
+ export function readConfig() {
15
+ try {
16
+ ensureDir();
17
+ if (!fs.existsSync(CONFIG_FILE)) {
18
+ writeConfig(DEFAULT_CONFIG);
19
+ return { ...DEFAULT_CONFIG };
20
+ }
21
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
22
+ const parsed = JSON.parse(raw);
23
+ if (typeof parsed !== 'object' || parsed === null) {
24
+ writeConfig(DEFAULT_CONFIG);
25
+ return { ...DEFAULT_CONFIG };
26
+ }
27
+ return parsed;
28
+ } catch {
29
+ writeConfig(DEFAULT_CONFIG);
30
+ return { ...DEFAULT_CONFIG };
31
+ }
32
+ }
33
+
34
+ export function writeConfig(config) {
35
+ ensureDir();
36
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
37
+ }
38
+
39
+ export function getCity() {
40
+ const config = readConfig();
41
+ return config.city || null;
42
+ }
43
+
44
+ export function setCity(city) {
45
+ const config = readConfig();
46
+ config.city = city;
47
+ writeConfig(config);
48
+ }
49
+
50
+ export function resetConfig() {
51
+ writeConfig({ ...DEFAULT_CONFIG });
52
+ }
53
+
54
+ export { CONFIG_DIR, CONFIG_FILE };
@@ -0,0 +1,103 @@
1
+ import Table from 'cli-table3';
2
+ import chalk from 'chalk';
3
+
4
+ export function createEarthquakeTable(earthquakes) {
5
+ const table = new Table({
6
+ head: [
7
+ chalk.white.bold('Tarih'),
8
+ chalk.white.bold('Büyüklük'),
9
+ chalk.white.bold('Derinlik (km)'),
10
+ chalk.white.bold('Konum'),
11
+ ],
12
+ colWidths: [22, 12, 15, 45],
13
+ style: { head: [], border: ['gray'] },
14
+ });
15
+
16
+ for (const eq of earthquakes) {
17
+ const mag = parseFloat(eq.magnitude);
18
+ const magStr = mag >= 4.0
19
+ ? chalk.red.bold(mag.toFixed(1))
20
+ : chalk.yellow(mag.toFixed(1));
21
+
22
+ const row = [
23
+ eq.date,
24
+ magStr,
25
+ eq.depth,
26
+ eq.location,
27
+ ];
28
+
29
+ table.push(row);
30
+ }
31
+
32
+ return table.toString();
33
+ }
34
+
35
+ export function createEgoScheduleTable(schedule) {
36
+ const table = new Table({
37
+ head: [
38
+ chalk.white.bold('Hat'),
39
+ chalk.white.bold('Gün Tipi'),
40
+ chalk.white.bold('Kalkış Saatleri'),
41
+ ],
42
+ colWidths: [10, 14, 70],
43
+ style: { head: [], border: ['gray'] },
44
+ wordWrap: true,
45
+ });
46
+
47
+ for (const row of schedule) {
48
+ table.push([row.hat, row.gunTipi, row.saatler]);
49
+ }
50
+
51
+ return table.toString();
52
+ }
53
+
54
+ export function createEgoInfoTable(info) {
55
+ const table = new Table({
56
+ style: { head: [], border: ['gray'] },
57
+ });
58
+
59
+ table.push(
60
+ { [chalk.cyan('Hat No')]: info.hatNo || '-' },
61
+ { [chalk.cyan('Hat Adı')]: info.hatAdi || '-' },
62
+ { [chalk.cyan('Kalkış')]: info.kalkis || '-' },
63
+ { [chalk.cyan('Varış')]: info.varis || '-' },
64
+ { [chalk.cyan('Mesafe')]: info.mesafe || '-' },
65
+ { [chalk.cyan('Süre')]: info.sure || '-' },
66
+ );
67
+
68
+ return table.toString();
69
+ }
70
+
71
+ export function createRouteTable(route) {
72
+ const table = new Table({
73
+ style: { head: [], border: ['gray'] },
74
+ });
75
+
76
+ table.push(
77
+ { [chalk.cyan('Hat Kısa Adı')]: route.routeShortName || '-' },
78
+ { [chalk.cyan('Hat Uzun Adı')]: route.routeLongName || '-' },
79
+ );
80
+
81
+ if (route.stopCount > 0) {
82
+ table.push({ [chalk.cyan('Durak Sayısı')]: String(route.stopCount) });
83
+ }
84
+
85
+ table.push(
86
+ { [chalk.cyan('İlk Durak')]: route.firstStop || '-' },
87
+ { [chalk.cyan('Son Durak')]: route.lastStop || '-' },
88
+ );
89
+
90
+ if (route.directions && route.directions.length > 1) {
91
+ table.push({
92
+ [chalk.cyan('Güzergahlar')]: route.directions.join('\n'),
93
+ });
94
+ }
95
+
96
+ if (route.routeVariants && route.routeVariants > 1) {
97
+ table.push({
98
+ [chalk.cyan('Varyant Sayısı')]: String(route.routeVariants),
99
+ });
100
+ }
101
+
102
+ return table.toString();
103
+ }