diff --git a/.eslintrc.yaml b/.eslintrc.yaml index adab085..5ec69ab 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -11,3 +11,24 @@ plugins: extends: - "plugin:@typescript-eslint/recommended" - "htmlacademy/node" +rules: + object-curly-spacing: + - error + - always + "@typescript-eslint/type-annotation-spacing": + - error + - before: false + after: true + semi: + - error + - always +overrides: + - files: + - "*.ts" + - "*.tsx" + rules: + # для TS-файлов + "@typescript-eslint/semi": + - error + - always + semi: off diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8f8b1de --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +# Установка зависимостей +install: + npm install + +# Сборка проекта +build: + npm run build + +# Запуск проекта (сначала build) +start: + npm run start + +# Очистка dist +clean: + npm run clean + +# Линтинг +lint: + npm run lint + +# Линтинг с автоисправлением +lint-fix: + npm run lint:fix + +# цель по умолчанию +default: lint + +# Основные цели +.PHONY: default install build start clean lint lint-fix test diff --git a/package-lock.json b/package-lock.json index cf63002..0399200 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "six-cities", "version": "5.0.0", + "dependencies": { + "chalk": "^5.6.0" + }, "devDependencies": { "@types/node": "18.17.17", "@typescript-eslint/eslint-plugin": "6.7.0", @@ -18,8 +21,8 @@ "typescript": "5.2.2" }, "engines": { - "node": "^18.0.0", - "npm": ">=8" + "node": "^22.18.0", + "npm": ">=10" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -762,22 +765,24 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -818,16 +823,12 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "license": "MIT", "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -900,10 +901,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1262,10 +1264,11 @@ } }, "node_modules/eslint-plugin-node/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -1329,10 +1332,11 @@ } }, "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -1398,6 +1402,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -1525,10 +1546,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1735,10 +1757,11 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1852,6 +1875,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2135,6 +2159,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -2423,12 +2448,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -2490,10 +2516,11 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -3389,6 +3416,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -3419,6 +3447,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -4271,9 +4300,9 @@ "dev": true }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -4281,12 +4310,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "builtin-modules": { @@ -4312,14 +4341,9 @@ "dev": true }, "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==" }, "ci-info": { "version": "3.8.0", @@ -4372,9 +4396,9 @@ "dev": true }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -4574,6 +4598,18 @@ "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } } }, "eslint-config-htmlacademy": { @@ -4644,9 +4680,9 @@ "dev": true }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -4695,9 +4731,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -4844,9 +4880,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -4987,9 +5023,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -5483,12 +5519,12 @@ "dev": true }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, @@ -5538,9 +5574,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } diff --git a/package.json b/package.json index 9909045..8e554fc 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "six-cities", + "author": "Aleksandr Chusovitin", "version": "5.0.0", "description": "Проект «Шесть городов» от HTML Academy", "keywords": [ @@ -12,6 +13,7 @@ "start": "npm run build && node ./dist/main.js", "build": "npm run clean && npm run compile", "lint": "eslint src/ --ext .ts", + "lint:fix": "eslint src/ --ext .ts --fix", "compile": "tsc -p tsconfig.json", "clean": "rimraf dist", "ts": "ts-node" @@ -28,7 +30,10 @@ }, "type": "module", "engines": { - "node": "^18.0.0", - "npm": ">=8" + "node": "^22.18.0", + "npm": ">=10" + }, + "dependencies": { + "chalk": "^5.6.0" } } diff --git a/src/cli/cli-application.ts b/src/cli/cli-application.ts new file mode 100644 index 0000000..fca857b --- /dev/null +++ b/src/cli/cli-application.ts @@ -0,0 +1,40 @@ +import { CommandParser } from './command-parser.js'; +import { Command } from './commands/command.interface.js'; + +type CommandCollection = Record; + +export class CLIApplication { + constructor( + private readonly defaultCommand: string = '--help' + ) {} + + private commands: CommandCollection = {}; + + public registerCommands(commandList: Command[]): void { + commandList.forEach((command) => { + if (Object.hasOwn(this.commands, command.getName())) { + throw new Error(`Command ${command.getName()} is already registered`); + } + this.commands[command.getName()] = command; + }); + } + + public getCommand(commandName: string): Command { + return this.commands[commandName] ?? this.getDefaultCommand(); + } + + public getDefaultCommand(): Command | never { + if (! this.commands[this.defaultCommand]) { + throw new Error(`The default command (${this.defaultCommand}) is not registered.`); + } + return this.commands[this.defaultCommand]; + } + + public processCommand(argv: string[]): void { + const parsedCommand = CommandParser.parse(argv); + const [commandName] = Object.keys(parsedCommand); + const command = this.getCommand(commandName); + const commandArguments = parsedCommand[commandName] ?? []; + command.execute(...commandArguments); + } +} diff --git a/src/cli/command-parser.ts b/src/cli/command-parser.ts new file mode 100644 index 0000000..789ae18 --- /dev/null +++ b/src/cli/command-parser.ts @@ -0,0 +1,19 @@ +type ParsedCommand = Record; + +export class CommandParser { + static parse(cliArguments: string[]): ParsedCommand { + const parsedCommand: ParsedCommand = {}; + let currentCommand = ''; + + for (const argument of cliArguments) { + if (argument.startsWith('--')) { + parsedCommand[argument] = []; + currentCommand = argument; + } else if (currentCommand && argument) { + parsedCommand[currentCommand].push(argument); + } + } + + return parsedCommand; + } +} diff --git a/src/cli/commands/command.interface.ts b/src/cli/commands/command.interface.ts new file mode 100644 index 0000000..97adc62 --- /dev/null +++ b/src/cli/commands/command.interface.ts @@ -0,0 +1,4 @@ +export interface Command { + getName(): string; + execute(...parameters: string[]): Promise; +} diff --git a/src/cli/commands/help.command.ts b/src/cli/commands/help.command.ts new file mode 100644 index 0000000..1da6386 --- /dev/null +++ b/src/cli/commands/help.command.ts @@ -0,0 +1,23 @@ +import chalk from 'chalk'; +import { Command } from './command.interface.js'; + +export class HelpCommand implements Command { + public getName(): string { + return '--help'; + } + + public async execute(..._parameters: string[]): Promise { + console.info(` +${chalk.bold('Программа для подготовки данных для REST API сервера.')} + +${chalk.bold('Пример:')} + ${'cli.js'} ${chalk.green('--')} ${'[--arguments]'} + +${chalk.bold('Команды:')} + ${chalk.green('--version')} выводит номер версии + ${chalk.green('--help')} печатает этот текст + ${chalk.green('--import')} импортирует данные из TSV + ${chalk.green('--generate')} генерирует произвольное количество тестовых данных +`); + } +} diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts new file mode 100644 index 0000000..cb5fd06 --- /dev/null +++ b/src/cli/commands/import.command.ts @@ -0,0 +1,26 @@ +import { TSVFileReader } from '../../shared/libs/file-reader/index.js'; +import { Command } from './command.interface.js'; + +export class ImportCommand implements Command { + public getName(): string { + return '--import'; + } + + public async execute(...parameters: string[]): Promise { + const [filename] = parameters; + const fileReader = new TSVFileReader(filename.trim()); + + try { + fileReader.read(); + console.log(fileReader.toArray()); + } catch (err) { + + if (!(err instanceof Error)) { + throw err; + } + + console.error(`Can't import data from file: ${filename}`); + console.error(`Details: ${err.message}`); + } + } +} diff --git a/src/cli/commands/version.command.ts b/src/cli/commands/version.command.ts new file mode 100644 index 0000000..20128a8 --- /dev/null +++ b/src/cli/commands/version.command.ts @@ -0,0 +1,50 @@ +import chalk from 'chalk'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { Command } from './command.interface.js'; + + +type PackageJSONConfig = { + version: string; +}; + +const isPackageJSONConfig = (value: unknown): value is PackageJSONConfig => ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + Object.hasOwn(value, 'version') +); + +export class VersionCommand implements Command { + constructor( + private readonly filePath: string = './package.json' + ) {} + + private readVersion(): string { + const jsonContent = readFileSync(resolve(this.filePath), 'utf-8'); + const importedContent: unknown = JSON.parse(jsonContent); + + if (!isPackageJSONConfig(importedContent)) { + throw new Error('Failed to parse json content.'); + } + + return importedContent.version; + } + + public getName(): string { + return '--version'; + } + + public async execute(..._parameters: string[]): Promise { + try { + const version = this.readVersion(); + console.info(chalk.bold.green(version)); + } catch (error: unknown) { + console.error(`Failed to read version from ${this.filePath}`); + + if (error instanceof Error) { + console.error(error.message); + } + } + } +} diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 0000000..1cbfe69 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,6 @@ +export * from './cli-application.js'; +export * from './command-parser.js'; +export * from './commands/help.command.js'; +export * from './commands/version.command.js'; +export * from './commands/import.command.js'; + diff --git a/src/main.ts b/src/main.ts index e69de29..0ec5224 100644 --- a/src/main.ts +++ b/src/main.ts @@ -0,0 +1,16 @@ +#!/usr/bin/env node +import { CLIApplication, HelpCommand, ImportCommand, VersionCommand } from './cli/index.js'; + + +function bootstrap() { + const cliApplication = new CLIApplication(); + cliApplication.registerCommands([ + new HelpCommand(), + new VersionCommand(), + new ImportCommand() + ]); + + cliApplication.processCommand(process.argv); +} + +bootstrap(); diff --git a/src/mocks/mock-data.tsv b/src/mocks/mock-data.tsv new file mode 100644 index 0000000..eefb733 --- /dev/null +++ b/src/mocks/mock-data.tsv @@ -0,0 +1,2 @@ +Уютная квартира в центре Светлая и просторная квартира рядом с набережной. Отлично подойдёт для путешественников и семей. 2025-08-18 Paris https://example.com/preview1.jpg https://example.com/photo1.jpg/https://example.com/photo2.jpg/https://example.com/photo3.jpg/https://example.com/photo4.jpg/https://example.com/photo5.jpg/https://example.com/photo6.jpg true false 4.5 apartment 2 4 3500 Breakfast/Washer/Towels/Fridge 0 48.85661 2.351499 Sofia/Kuznetsova sofia.kuznetsova@example.com https://example.com/avatars/sofia.jpg Th1sIsAStr0ng! pro +Домик у озера Уютный домик с видом на озеро, камином и просторной гостиной. Отличный выбор для отдыха на природе. 2025-08-17 Amsterdam https://example.com/preview2.jpg https://example.com/house1.jpg/https://example.com/house2.jpg/https://example.com/house3.jpg/https://example.com/house4.jpg/https://example.com/house5.jpg/https://example.com/house6.jpg false true 4.8 house 3 6 7500 Air conditioning/Laptop friendly workspace/Baby seat/Towels 0 52.370216 4.895168 Maxim/Ivanov maxim.ivanov@example.com https://example.com/avatars/maxim.jpg Secure#Pass2025 regular diff --git a/src/shared/constants/amenity.ts b/src/shared/constants/amenity.ts new file mode 100644 index 0000000..16192bf --- /dev/null +++ b/src/shared/constants/amenity.ts @@ -0,0 +1,9 @@ +export const AmenityEnum = { + Breakfast: 'Breakfast', + AirConditioning: 'Air conditioning', + LaptopFriendlyWorkspace: 'Laptop friendly workspace', + BabySeat: 'Baby seat', + Washer: 'Washer', + Towels: 'Towels', + Fridge: 'Fridge', +} as const; diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts new file mode 100644 index 0000000..853a890 --- /dev/null +++ b/src/shared/constants/index.ts @@ -0,0 +1,3 @@ +export { UserTypeEnum } from './user.js'; +export { OfferTypeEnum } from './offer.js'; +export { AmenityEnum } from './amenity.js'; diff --git a/src/shared/constants/offer.ts b/src/shared/constants/offer.ts new file mode 100644 index 0000000..938acc9 --- /dev/null +++ b/src/shared/constants/offer.ts @@ -0,0 +1,6 @@ +export const OfferTypeEnum = { + Appartment: 'apartment', + House: 'house', + Room: 'room', + Hotel: 'hotel', +} as const; diff --git a/src/shared/constants/user.ts b/src/shared/constants/user.ts new file mode 100644 index 0000000..bd6ec70 --- /dev/null +++ b/src/shared/constants/user.ts @@ -0,0 +1,4 @@ +export const UserTypeEnum = { + Regular: 'regular', + Pro: 'pro' +} as const; diff --git a/src/shared/helpers/boolean.ts b/src/shared/helpers/boolean.ts new file mode 100644 index 0000000..5aefde3 --- /dev/null +++ b/src/shared/helpers/boolean.ts @@ -0,0 +1 @@ +export const parseBoolean = (s: string) => s.trim().toLowerCase() === 'true'; diff --git a/src/shared/helpers/index.ts b/src/shared/helpers/index.ts new file mode 100644 index 0000000..552d33c --- /dev/null +++ b/src/shared/helpers/index.ts @@ -0,0 +1 @@ +export { parseBoolean } from './boolean.js'; diff --git a/src/shared/libs/file-reader/file-reader.interface.ts b/src/shared/libs/file-reader/file-reader.interface.ts new file mode 100644 index 0000000..59f8279 --- /dev/null +++ b/src/shared/libs/file-reader/file-reader.interface.ts @@ -0,0 +1,3 @@ +export interface FileReader { + read(): void; +} diff --git a/src/shared/libs/file-reader/index.ts b/src/shared/libs/file-reader/index.ts new file mode 100644 index 0000000..4e9fa00 --- /dev/null +++ b/src/shared/libs/file-reader/index.ts @@ -0,0 +1,2 @@ +export * from './file-reader.interface.js'; +export * from './tsv-file-reader.js'; diff --git a/src/shared/libs/file-reader/tsv-file-reader.ts b/src/shared/libs/file-reader/tsv-file-reader.ts new file mode 100644 index 0000000..dd45aaf --- /dev/null +++ b/src/shared/libs/file-reader/tsv-file-reader.ts @@ -0,0 +1,71 @@ +import { FileReader } from './file-reader.interface.js'; +import { readFileSync } from 'node:fs'; +import { parseBoolean } from '../../helpers/index.js'; +import { Offer, OfferType } from '../../types/offer.js'; +import { UserType } from '../../types/user.js'; +import { Amenity } from '../../types/amenity.js'; + +export class TSVFileReader implements FileReader { + private rawData = ''; + + constructor( + private readonly filename: string + ) {} + + public read(): void { + this.rawData = readFileSync(this.filename, 'utf-8'); + } + + public toArray(): Offer[] { + if (!this.rawData) { + throw new Error('File was not read'); + } + + return this.rawData + .split('\n') + .filter((row) => row.trim().length > 0) + .map((line) => line.split('\t')) + .map(([ + title, + description, + createdAt, + city, + previewImg, + images, + isPremium, + isFavorite, + rating, + type, + roomsCount, + guestsCount, + price, + amenities, + commentsCount, + latitude, + longitude, + userName, + email, + avatar, + password, + userType + ]) => ({ + title, + description, + createdAt, + city, + previewImg, + images: images.split('/'), + isPremium: parseBoolean(isPremium), + isFavorite: parseBoolean(isFavorite), + rating: Number(rating), + type: type as OfferType, + roomsCount: Number(roomsCount), + guestsCount: Number(guestsCount), + price: Number(price), + amenities: amenities.split('/') as Amenity[], + commentsCount: Number(commentsCount), + coordinate: { latitude: Number(latitude), longitude: Number(longitude) }, + author: { name: userName.split('/').join(' '), email, avatar, password, type: userType as UserType }, + })); + } +} diff --git a/src/shared/types/amenity.ts b/src/shared/types/amenity.ts new file mode 100644 index 0000000..37df888 --- /dev/null +++ b/src/shared/types/amenity.ts @@ -0,0 +1,4 @@ +import { AmenityEnum } from '../constants/index.js'; +import { ValueOf } from './common.js'; + +export type Amenity = ValueOf; diff --git a/src/shared/types/comment.ts b/src/shared/types/comment.ts new file mode 100644 index 0000000..d15105c --- /dev/null +++ b/src/shared/types/comment.ts @@ -0,0 +1,8 @@ +import { User } from './user.js'; + +export type Comment = { + text: string; + rating: number; + author: User; + createdAt: string; +}; diff --git a/src/shared/types/common.ts b/src/shared/types/common.ts new file mode 100644 index 0000000..5f2cf2c --- /dev/null +++ b/src/shared/types/common.ts @@ -0,0 +1 @@ +export type ValueOf = T[keyof T]; diff --git a/src/shared/types/coordinate.ts b/src/shared/types/coordinate.ts new file mode 100644 index 0000000..321769c --- /dev/null +++ b/src/shared/types/coordinate.ts @@ -0,0 +1,4 @@ +export type Coordinate = { + latitude: number; + longitude: number; +}; diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts new file mode 100644 index 0000000..cd1fdc4 --- /dev/null +++ b/src/shared/types/index.ts @@ -0,0 +1 @@ +export { ValueOf } from './common.js'; diff --git a/src/shared/types/offer.ts b/src/shared/types/offer.ts new file mode 100644 index 0000000..53ae540 --- /dev/null +++ b/src/shared/types/offer.ts @@ -0,0 +1,27 @@ +import { OfferTypeEnum } from '../constants/index.js'; +import { Amenity } from './amenity.js'; +import { Coordinate } from './coordinate.js'; +import { ValueOf } from './common.js'; +import { User } from './user.js'; + +export type OfferType = ValueOf; + +export type Offer = { + title: string; + description: string; + createdAt: string; + city: string; + previewImg: string; + images: string[]; + isPremium: boolean; + isFavorite: boolean; + rating: number; + type: OfferType; + roomsCount: number; + guestsCount: number; + price: number; + commentsCount: number; + coordinate: Coordinate; + author: User; + amenities: Amenity[]; +}; diff --git a/src/shared/types/user.ts b/src/shared/types/user.ts new file mode 100644 index 0000000..88b12c0 --- /dev/null +++ b/src/shared/types/user.ts @@ -0,0 +1,12 @@ +import { UserTypeEnum } from '../constants/index.js'; +import { ValueOf } from './common.js'; + +export type UserType = ValueOf; + +export type User = { + name: string; + email: string; + avatar?: string; + password: string; + type: UserType; +}; diff --git a/tsconfig.json b/tsconfig.json index 58f04b3..4850830 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,9 +27,7 @@ "ESNext" ] }, - "include": [ - "src/**/*.ts" - ], + "include": ["src/**/*.ts"], "exclude": [ "node_modules", "**/*.test.ts"