├── .gitignore ├── .npmignore ├── helpers └── args.js ├── package-lock.json ├── package.json ├── services ├── api.service.js ├── log.service.js └── storage.service.js └── weather.js /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /node_modules -------------------------------------------------------------------------------- /helpers/args.js: -------------------------------------------------------------------------------- 1 | const getArgs = (args) => { 2 | const res = {}; 3 | const [executer, file, ...rest] = args; 4 | rest.forEach((value, index, array) => { 5 | if (value.charAt(0) == '-') { 6 | if (index == array.length - 1) { 7 | res[value.substring(1)] = true; 8 | } else if (array[index + 1].charAt(0) != '-') { 9 | res[value.substring(1)] = array[index + 1]; 10 | } else { 11 | res[value.substring(1)] = true; 12 | } 13 | } 14 | }); 15 | return res; 16 | }; 17 | 18 | export { getArgs } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weather-cli", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "weather-cli", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^0.21.4", 13 | "chalk": "^4.1.2", 14 | "dedent-js": "^1.0.1" 15 | }, 16 | "bin": { 17 | "weather": "weather.js" 18 | } 19 | }, 20 | "node_modules/ansi-styles": { 21 | "version": "4.3.0", 22 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 23 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 24 | "dependencies": { 25 | "color-convert": "^2.0.1" 26 | }, 27 | "engines": { 28 | "node": ">=8" 29 | }, 30 | "funding": { 31 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 32 | } 33 | }, 34 | "node_modules/axios": { 35 | "version": "0.21.4", 36 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", 37 | "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", 38 | "dependencies": { 39 | "follow-redirects": "^1.14.0" 40 | } 41 | }, 42 | "node_modules/chalk": { 43 | "version": "4.1.2", 44 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 45 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 46 | "dependencies": { 47 | "ansi-styles": "^4.1.0", 48 | "supports-color": "^7.1.0" 49 | }, 50 | "engines": { 51 | "node": ">=10" 52 | }, 53 | "funding": { 54 | "url": "https://github.com/chalk/chalk?sponsor=1" 55 | } 56 | }, 57 | "node_modules/color-convert": { 58 | "version": "2.0.1", 59 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 60 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 61 | "dependencies": { 62 | "color-name": "~1.1.4" 63 | }, 64 | "engines": { 65 | "node": ">=7.0.0" 66 | } 67 | }, 68 | "node_modules/color-name": { 69 | "version": "1.1.4", 70 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 71 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 72 | }, 73 | "node_modules/dedent-js": { 74 | "version": "1.0.1", 75 | "resolved": "https://registry.npmjs.org/dedent-js/-/dedent-js-1.0.1.tgz", 76 | "integrity": "sha1-vuX7fJ5yfYXf+iRZDRDsGrElUwU=" 77 | }, 78 | "node_modules/follow-redirects": { 79 | "version": "1.14.3", 80 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.3.tgz", 81 | "integrity": "sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==", 82 | "funding": [ 83 | { 84 | "type": "individual", 85 | "url": "https://github.com/sponsors/RubenVerborgh" 86 | } 87 | ], 88 | "engines": { 89 | "node": ">=4.0" 90 | }, 91 | "peerDependenciesMeta": { 92 | "debug": { 93 | "optional": true 94 | } 95 | } 96 | }, 97 | "node_modules/has-flag": { 98 | "version": "4.0.0", 99 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 100 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 101 | "engines": { 102 | "node": ">=8" 103 | } 104 | }, 105 | "node_modules/supports-color": { 106 | "version": "7.2.0", 107 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 108 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 109 | "dependencies": { 110 | "has-flag": "^4.0.0" 111 | }, 112 | "engines": { 113 | "node": ">=8" 114 | } 115 | } 116 | }, 117 | "dependencies": { 118 | "ansi-styles": { 119 | "version": "4.3.0", 120 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 121 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 122 | "requires": { 123 | "color-convert": "^2.0.1" 124 | } 125 | }, 126 | "axios": { 127 | "version": "0.21.4", 128 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", 129 | "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", 130 | "requires": { 131 | "follow-redirects": "^1.14.0" 132 | } 133 | }, 134 | "chalk": { 135 | "version": "4.1.2", 136 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 137 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 138 | "requires": { 139 | "ansi-styles": "^4.1.0", 140 | "supports-color": "^7.1.0" 141 | } 142 | }, 143 | "color-convert": { 144 | "version": "2.0.1", 145 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 146 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 147 | "requires": { 148 | "color-name": "~1.1.4" 149 | } 150 | }, 151 | "color-name": { 152 | "version": "1.1.4", 153 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 154 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 155 | }, 156 | "dedent-js": { 157 | "version": "1.0.1", 158 | "resolved": "https://registry.npmjs.org/dedent-js/-/dedent-js-1.0.1.tgz", 159 | "integrity": "sha1-vuX7fJ5yfYXf+iRZDRDsGrElUwU=" 160 | }, 161 | "follow-redirects": { 162 | "version": "1.14.3", 163 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.3.tgz", 164 | "integrity": "sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==" 165 | }, 166 | "has-flag": { 167 | "version": "4.0.0", 168 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 169 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 170 | }, 171 | "supports-color": { 172 | "version": "7.2.0", 173 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 174 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 175 | "requires": { 176 | "has-flag": "^4.0.0" 177 | } 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weather-cli-demo", 3 | "version": "1.0.0", 4 | "description": "CLI for getting weather", 5 | "main": "weather.js", 6 | "bin": { 7 | "weather": "weather.js" 8 | }, 9 | "type": "module", 10 | "scripts": { 11 | "start": "node weather.js", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "keywords": [ 15 | "cli", 16 | "weather" 17 | ], 18 | "author": "Anton Larichev", 19 | "license": "ISC", 20 | "homepage": "https://purpleschool.ru", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://purpleschool.ru" 24 | }, 25 | "bugs": { 26 | "url": "https://purpleschool.ru", 27 | "email": "antonlarichev@gmail.com" 28 | }, 29 | "dependencies": { 30 | "axios": "^0.21.4", 31 | "chalk": "^4.1.2", 32 | "dedent-js": "^1.0.1" 33 | } 34 | } -------------------------------------------------------------------------------- /services/api.service.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { getKeyValue, TOKEN_DICTIONARY } from './storage.service.js'; 3 | 4 | const getIcon = (icon) => { 5 | switch (icon.slice(0, -1)) { 6 | case '01': 7 | return '☀️'; 8 | case '02': 9 | return '🌤️'; 10 | case '03': 11 | return '☁️'; 12 | case '04': 13 | return '☁️'; 14 | case '09': 15 | return '🌧️'; 16 | case '10': 17 | return '🌦️'; 18 | case '11': 19 | return '🌩️'; 20 | case '13': 21 | return '❄️'; 22 | case '50': 23 | return '🌫️'; 24 | } 25 | }; 26 | 27 | const getWeather = async (city) => { 28 | const token = process.env.TOKEN ?? await getKeyValue(TOKEN_DICTIONARY.token); 29 | if (!token) { 30 | throw new Error('Не задан ключ API, задайте его через команду -t [API_KEY]'); 31 | } 32 | const { data } = await axios.get('https://api.openweathermap.org/data/2.5/weather', { 33 | params: { 34 | q: city, 35 | appid: token, 36 | lang: 'ru', 37 | units: 'metric' 38 | } 39 | }); 40 | return data; 41 | }; 42 | 43 | export { getWeather, getIcon }; -------------------------------------------------------------------------------- /services/log.service.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import dedent from 'dedent-js'; 3 | 4 | const printError = (error) => { 5 | console.log(chalk.bgRed(' ERROR ') + ' ' + error); 6 | }; 7 | 8 | const printSuccess = (message) => { 9 | console.log(chalk.bgGreen(' SUCCESS ') + ' ' + message); 10 | }; 11 | 12 | const printHelp = () => { 13 | console.log( 14 | dedent`${chalk.bgCyan(' HELP ')} 15 | Без параметров - вывод погоды 16 | -s [CITY] для установки города 17 | -h для вывода помощи 18 | -t [API_KEY] для сохранения токена 19 | ` 20 | ); 21 | }; 22 | 23 | const printWeather = (res, icon) => { 24 | console.log( 25 | dedent`${chalk.bgYellow(' WEATHER ')} Погода в городе ${res.name} 26 | ${icon} ${res.weather[0].description} 27 | Температура: ${res.main.temp} (ощущается как ${res.main.feels_like}) 28 | Влажность: ${res.main.humidity}% 29 | Скорость ветра: ${res.wind.speed} 30 | ` 31 | ); 32 | }; 33 | 34 | export { printError, printSuccess, printHelp, printWeather }; -------------------------------------------------------------------------------- /services/storage.service.js: -------------------------------------------------------------------------------- 1 | import { homedir } from 'os'; 2 | import { join } from 'path'; 3 | import { promises } from 'fs'; 4 | 5 | const filePath = join(homedir(), 'weather-data.json'); 6 | 7 | const TOKEN_DICTIONARY = { 8 | token: 'token', 9 | city: 'city' 10 | } 11 | 12 | const saveKeyValue = async (key, value) => { 13 | let data = {}; 14 | if (await isExist(filePath)) { 15 | const file = await promises.readFile(filePath); 16 | data = JSON.parse(file); 17 | } 18 | data[key] = value; 19 | await promises.writeFile(filePath, JSON.stringify(data)); 20 | }; 21 | 22 | const getKeyValue = async (key) => { 23 | if (await isExist(filePath)) { 24 | const file = await promises.readFile(filePath); 25 | const data = JSON.parse(file); 26 | return data[key]; 27 | } 28 | return undefined; 29 | }; 30 | 31 | const isExist = async (path) => { 32 | try { 33 | await promises.stat(path); 34 | return true; 35 | } catch (e) { 36 | return false; 37 | } 38 | }; 39 | 40 | export { saveKeyValue, getKeyValue, TOKEN_DICTIONARY }; -------------------------------------------------------------------------------- /weather.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { getArgs } from './helpers/args.js'; 3 | import { getWeather, getIcon } from './services/api.service.js'; 4 | import { printHelp, printSuccess, printError, printWeather } from './services/log.service.js'; 5 | import { saveKeyValue, TOKEN_DICTIONARY, getKeyValue } from './services/storage.service.js'; 6 | 7 | const saveToken = async (token) => { 8 | if (!token.length) { 9 | printError('Не передан token'); 10 | return; 11 | } 12 | try { 13 | await saveKeyValue(TOKEN_DICTIONARY.token, token); 14 | printSuccess('Токен сохранён'); 15 | } catch (e) { 16 | printError(e.message); 17 | } 18 | } 19 | 20 | const saveCity = async (city) => { 21 | if (!city.length) { 22 | printError('Не передан город'); 23 | return; 24 | } 25 | try { 26 | await saveKeyValue(TOKEN_DICTIONARY.city, city); 27 | printSuccess('Город сохранён'); 28 | } catch (e) { 29 | printError(e.message); 30 | } 31 | } 32 | 33 | const getForcast = async () => { 34 | try { 35 | const city = process.env.CITY ?? await getKeyValue(TOKEN_DICTIONARY.city); 36 | const weather = await getWeather(city); 37 | printWeather(weather, getIcon(weather.weather[0].icon)); 38 | } catch (e) { 39 | if (e?.response?.status == 404) { 40 | printError('Неверно указан город'); 41 | } else if (e?.response?.status == 401) { 42 | printError('Неверно указан токен'); 43 | } else { 44 | printError(e.message); 45 | } 46 | } 47 | } 48 | 49 | const initCLI = () => { 50 | const args = getArgs(process.argv); 51 | if (args.h) { 52 | return printHelp(); 53 | } 54 | if (args.s) { 55 | return saveCity(args.s); 56 | } 57 | if (args.t) { 58 | return saveToken(args.t); 59 | } 60 | return getForcast(); 61 | }; 62 | 63 | initCLI(); --------------------------------------------------------------------------------