diff --git a/jest.config.js b/jest.config.js index f2b22b7..001bae0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,4 +7,8 @@ module.exports = { moduleNameMapper: { '^@/(.*)$': '/src/$1' }, + transformIgnorePatterns: [ + 'node_modules/(?!(ora|chalk|strip-ansi|ansi-regex|cli-cursor|restore-cursor|onetime|mimic-fn|is-unicode-supported|is-interactive|get-east-asian-width)/)' + ], + moduleDirectories: ['node_modules', 'src'] }; diff --git a/package-lock.json b/package-lock.json index b170ea2..4b695e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,11 @@ "dependencies": { "blockhash-core": "^0.1.0", "canvas": "^3.0.1", + "ora": "^5.4.1", "png-js": "^1.0.0", "sharp": "^0.33.5", - "sharp-phash": "^2.2.0" + "sharp-phash": "^2.2.0", + "winston": "^3.17.0" }, "devDependencies": { "@types/jest": "^29.5.12", @@ -530,6 +532,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", @@ -1625,6 +1645,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -1972,7 +1997,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1982,7 +2006,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2032,7 +2055,6 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, "license": "MIT" }, "node_modules/babel-jest": { @@ -2378,7 +2400,6 @@ "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", @@ -2455,6 +2476,28 @@ "dev": true, "license": "MIT" }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2470,6 +2513,14 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2529,6 +2580,37 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/colorspace/node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/colorspace/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/colorspace/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -2664,6 +2746,17 @@ "node": ">=0.10.0" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -2762,6 +2855,11 @@ "dev": true, "license": "MIT" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -3258,6 +3356,11 @@ "bser": "2.1.1" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3353,6 +3456,11 @@ "dev": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -3524,7 +3632,6 @@ "version": "4.0.0", "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" @@ -3766,6 +3873,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3790,7 +3905,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3799,6 +3913,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4600,6 +4725,11 @@ "node": ">=6" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -4658,6 +4788,37 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4749,7 +4910,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4799,7 +4959,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mylas": { @@ -4905,11 +5064,18 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -4939,6 +5105,28 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5431,6 +5619,18 @@ "node": ">=10" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -5503,6 +5703,14 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5603,7 +5811,6 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, "license": "ISC" }, "node_modules/simple-concat": { @@ -5711,6 +5918,14 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -5766,7 +5981,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -5812,7 +6026,6 @@ "version": "7.2.0", "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" @@ -5894,6 +6107,11 @@ "node": ">=8" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5921,6 +6139,14 @@ "node": ">=8.0" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -6162,6 +6388,14 @@ "makeerror": "1.0.12" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6178,6 +6412,40 @@ "node": ">= 8" } }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 434258c..36f41ec 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,10 @@ "dependencies": { "blockhash-core": "^0.1.0", "canvas": "^3.0.1", + "ora": "^5.4.1", "png-js": "^1.0.0", "sharp": "^0.33.5", - "sharp-phash": "^2.2.0" + "sharp-phash": "^2.2.0", + "winston": "^3.17.0" } } diff --git a/src/actions/CopilotStepPerformer.test.ts b/src/actions/CopilotStepPerformer.test.ts index 0b8bbe5..dc9ef07 100644 --- a/src/actions/CopilotStepPerformer.test.ts +++ b/src/actions/CopilotStepPerformer.test.ts @@ -18,6 +18,7 @@ import { } from "../test-utils/APICatalogTestUtils"; import { CopilotAPISearchPromptCreator } from "@/utils/CopilotAPISearchPromptCreator"; import { ViewAnalysisPromptCreator } from "@/utils/ViewAnalysisPromptCreator"; +import logger from "@/utils/logger"; jest.mock("fs"); jest.mock("crypto"); @@ -461,7 +462,7 @@ describe("CopilotStepPerformer", () => { }); it("should log when a context key is overridden", async () => { - jest.spyOn(console, "log").mockImplementation(() => {}); + jest.spyOn(logger, "warn").mockImplementation(() => {}); copilotStepPerformer.extendJSContext(dummyBarContext1); @@ -480,8 +481,8 @@ describe("CopilotStepPerformer", () => { ); copilotStepPerformer.extendJSContext(dummyBarContext2); - expect(console.log).toHaveBeenCalledWith( - "Notice: Context bar is overridden by the new context value", + expect(logger.warn).toHaveBeenCalledWith( + "Copilot's variable from context `bar` is overridden by a new value from `extendJSContext`", ); await copilotStepPerformer.perform(INTENT, [], screenCapture, 2); diff --git a/src/actions/CopilotStepPerformer.ts b/src/actions/CopilotStepPerformer.ts index b51e88e..cb31315 100644 --- a/src/actions/CopilotStepPerformer.ts +++ b/src/actions/CopilotStepPerformer.ts @@ -16,6 +16,7 @@ import { } from "@/types"; import * as crypto from "crypto"; import { extractCodeBlock } from "@/utils/extractCodeBlock"; +import logger from "@/utils/logger"; export class CopilotStepPerformer { private readonly cacheMode: CacheMode; @@ -41,8 +42,8 @@ export class CopilotStepPerformer { extendJSContext(newContext: any): void { for (const key in newContext) { if (key in this.context) { - console.log( - `Notice: Context ${key} is overridden by the new context value`, + logger.warn( + `Copilot's variable from context \`${key}\` is overridden by a new value from \`extendJSContext\``, ); break; } @@ -55,7 +56,6 @@ export class CopilotStepPerformer { previous: PreviousStep[], ): string | undefined { if (this.cacheMode === "disabled") { - // Return a unique key that won't match any cached value return undefined; } @@ -204,17 +204,20 @@ export class CopilotStepPerformer { step: string, previous: PreviousStep[] = [], screenCapture: ScreenCapturerResult, - attempts: number = 2, + maxAttempts: number = 2, ): Promise { - // TODO: replace with the user's logger - console.log("\x1b[90m%s\x1b[0m%s", "Copilot performing:", `"${step}"`); + const loggerSpinner = logger.startSpinner(`🤖 Copilot performing step:`, { + message: step, + isBold: true, + color: "whiteBright", + }); this.cacheHandler.loadCacheFromFile(); let lastError: any = null; let lastCode: string | undefined; - for (let attempt = 1; attempt <= attempts; attempt++) { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const { snapshot, viewHierarchy, isSnapshotImageAttached } = screenCapture; @@ -229,7 +232,15 @@ export class CopilotStepPerformer { lastCode = code; if (!code) { - throw new Error("Failed to generate code from intent"); + loggerSpinner.update(`🤖 Copilot retrying step:`, { + message: step, + isBold: true, + color: "whiteBright", + }); + + throw new Error( + "Failed to generate code from intent, please retry generating the code or provide a code that throws a descriptive error.", + ); } const result = await this.codeEvaluator.evaluate( @@ -238,16 +249,22 @@ export class CopilotStepPerformer { this.sharedContext, ); this.sharedContext = result.sharedContext || this.sharedContext; + + loggerSpinner.stop("success", `🦾 Copilot performed step:`, { + message: step, + isBold: true, + color: "greenBright", + }); + return result; } catch (error) { lastError = error; - console.log( - "\x1b[33m%s\x1b[0m", - `Attempt ${attempt} failed for step "${step}": ${error instanceof Error ? error.message : error}`, + logger.warn( + `💥 Attempt ${attempt}/${maxAttempts} failed for step "${step}": ${error instanceof Error ? error.message : error}`, ); - if (attempt < attempts) { - console.log("\x1b[33m%s\x1b[0m", "Copilot is retrying..."); + if (attempt < maxAttempts) { + loggerSpinner.update(`Retrying step: "${step}"`); const resultMessage = lastCode ? `Caught an error while evaluating "${step}", tried with generated code: "${lastCode}". Validate the code against the APIs and hierarchy and continue with a different approach. If can't, return a code that throws a descriptive error.` @@ -265,6 +282,10 @@ export class CopilotStepPerformer { } } + loggerSpinner.stop( + "failure", + `😓 Failed to perform step: "${step}", max attempts exhausted! (${maxAttempts})`, + ); throw lastError; } } diff --git a/src/actions/PilotPerformer.test.ts b/src/actions/PilotPerformer.test.ts index 5285a9d..84c8842 100644 --- a/src/actions/PilotPerformer.test.ts +++ b/src/actions/PilotPerformer.test.ts @@ -132,12 +132,12 @@ describe("PilotPerformer", () => { review: { ux: { summary: "The review of UX", - findings: ["- UX finding one", "- UX finding two"], + findings: ["UX finding one", "UX finding two"], score: "7/10", }, a11y: { summary: "The review of accessibility", - findings: ["- ACC finding one", "- ACC finding two"], + findings: ["ACC finding one", "ACC finding two"], score: "8/10", }, }, @@ -173,12 +173,12 @@ describe("PilotPerformer", () => { review: { ux: { summary: "The review of UX", - findings: ["- UX finding one", "- UX finding two"], + findings: ["UX finding one", "UX finding two"], score: "7/10", }, a11y: { summary: "The review of accessibility", - findings: ["- ACC finding one", "- ACC finding two"], + findings: ["ACC finding one", "ACC finding two"], score: "8/10", }, }, @@ -214,12 +214,12 @@ describe("PilotPerformer", () => { review: { ux: { summary: "The review of UX", - findings: ["- UX finding one", "- UX finding two"], + findings: ["UX finding one", "UX finding two"], score: "7/10", }, a11y: { summary: "The review of accessibility", - findings: ["- ACC finding one", "- ACC finding two"], + findings: ["ACC finding one", "ACC finding two"], score: "8/10", }, }, @@ -264,12 +264,12 @@ describe("PilotPerformer", () => { review: { ux: { summary: "The review of UX", - findings: ["- UX finding one", "- UX finding two"], + findings: ["UX finding one", "UX finding two"], score: "7/10", }, a11y: { summary: "The review of accessibility", - findings: ["- ACC finding one", "- ACC finding two"], + findings: ["ACC finding one", "ACC finding two"], score: "8/10", }, }, diff --git a/src/actions/PilotPerformer.ts b/src/actions/PilotPerformer.ts index f5dac4c..6fddd30 100644 --- a/src/actions/PilotPerformer.ts +++ b/src/actions/PilotPerformer.ts @@ -9,10 +9,12 @@ import { PilotPreviousStep, PilotReview, PreviousStep, + LoggerMessageColor, } from "@/types"; import { extractOutputs, OUTPUTS_MAPPINGS } from "@/utils/extractOutputs"; import { CopilotStepPerformer } from "@/actions/CopilotStepPerformer"; import { ScreenCapturer } from "@/utils/ScreenCapturer"; +import logger from "@/utils/logger"; export class PilotPerformer { constructor( @@ -27,7 +29,49 @@ export class PilotPerformer { text, outputsMapper: OUTPUTS_MAPPINGS.PILOT_REVIEW_SECTION, }); - return { summary, findings: findings?.split("\n"), score }; + + return { + summary, + findings: findings + ?.split("\n") + .map((finding: string) => finding.replace(/^- /, "").trim()), + score, + }; + } + + private logReviewSection(review: PilotReviewSection, type: "ux" | "a11y") { + const config: { + [key: string]: { + emoji: string; + color: LoggerMessageColor; + findingColor: LoggerMessageColor; + }; + } = { + ux: { + emoji: "🎨", + color: "magentaBright", + findingColor: "magenta", + }, + a11y: { + emoji: "👁️ ", + color: "yellowBright", + findingColor: "yellow", + }, + }; + + logger.info({ + message: `📝${config[type].emoji} Pilot ${type.toUpperCase()} review: ${review?.summary} (Score: ${review?.score})`, + isBold: true, + color: config[type].color, + }); + + review.findings?.forEach((finding) => { + logger.info({ + message: `🔍 ${finding}`, + isBold: false, + color: config[type].findingColor, + }); + }); } async analyseScreenAndCreateCopilotStep( @@ -35,6 +79,10 @@ export class PilotPerformer { previous: PilotPreviousStep[] = [], screenCapture: ScreenCapturerResult, ): Promise { + const analysisLoggerSpinner = logger.startSpinner( + "🤔 Thinking on next step", + ); + try { const { snapshot, viewHierarchy, isSnapshotImageAttached } = screenCapture; @@ -44,6 +92,7 @@ export class PilotPerformer { isSnapshotImageAttached, previous, ); + const generatedPilotTaskDetails: string = await this.promptHandler.runPrompt(prompt, snapshot); @@ -51,17 +100,27 @@ export class PilotPerformer { text: generatedPilotTaskDetails, outputsMapper: OUTPUTS_MAPPINGS.PILOT_STEP, }); + + analysisLoggerSpinner.stop("success", `💭 Thoughts:`, { + message: thoughts, + isBold: true, + color: "whiteBright", + }); + const plan: PilotStepPlan = { action, thoughts }; const review: PilotReview = { ux: this.extractReviewOutput(ux), a11y: this.extractReviewOutput(a11y), }; + review.ux && this.logReviewSection(review.ux, "ux"); + review.a11y && this.logReviewSection(review.a11y, "a11y"); + return { plan, review }; } catch (error) { - console.error( - "\x1b[33m%s\x1b[0m", - `pilot encountered an error: ${error instanceof Error ? error.message : error}`, + analysisLoggerSpinner.stop( + "failure", + `😓 Pilot encountered an error, ${error instanceof Error ? error.message : error}`, ); throw error; } @@ -71,7 +130,20 @@ export class PilotPerformer { const maxSteps = 100; let previousSteps: PilotPreviousStep[] = []; let copilotSteps: PreviousStep[] = []; - let report: PilotReport = { goal, steps: [] }; + const report: PilotReport = { goal, steps: [] }; + + logger.info( + { + message: `🛫 Pilot is trying to reach goal:`, + isBold: false, + color: "cyan", + }, + { + message: goal, + isBold: true, + color: "cyanBright", + }, + ); for (let step = 0; step < maxSteps; step++) { const screenCapture: ScreenCapturerResult = @@ -87,9 +159,9 @@ export class PilotPerformer { text: plan.thoughts, outputsMapper: OUTPUTS_MAPPINGS.PILOT_SUMMARY, }); - report = { summary, goal, steps: [...report.steps], review }; - console.log(JSON.stringify(report, null, 2)); - return report; + + logger.info(`🛬 Pilot reached goal: "${goal}"! 🎉 Summary: ${summary}`); + return { goal, summary, steps: [...report.steps], review }; } const { code, result } = await this.copilotStepPerformer.perform( @@ -97,6 +169,7 @@ export class PilotPerformer { [...copilotSteps], screenCapture, ); + copilotSteps = [...copilotSteps, { step: plan.action, code, result }]; previousSteps = [...previousSteps, { step: plan.action, review }]; @@ -104,10 +177,9 @@ export class PilotPerformer { report.steps = [...report.steps, stepReport]; } - console.warn( - `pilot finished execution due to maxSteps limit of ${maxSteps} has been reached`, + logger.warn( + `🛬 Pilot finished execution due to limit of ${maxSteps} steps has been reached`, ); - console.log(JSON.stringify(report, null, 2)); return report; } } diff --git a/src/types/index.ts b/src/types/index.ts index a855c30..7381ad2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,3 +3,4 @@ export * from "@/types/framework"; export * from "@/types/prompt"; export * from "@/types/pilot"; export * from "@/types/cache"; +export * from "@/types/logger"; diff --git a/src/types/logger.ts b/src/types/logger.ts new file mode 100644 index 0000000..fcda6ab --- /dev/null +++ b/src/types/logger.ts @@ -0,0 +1,50 @@ +/** + * Optional color for the logger message. + */ +export declare type LoggerMessageColor = + | "black" + | "red" + | "green" + | "yellow" + | "blue" + | "magenta" + | "cyan" + | "white" + | "gray" + | "grey" + | "blackBright" + | "redBright" + | "greenBright" + | "yellowBright" + | "blueBright" + | "magentaBright" + | "cyanBright" + | "whiteBright"; + +/** + * Logger message component, subsequent messages will be concatenated. String components will be white colored. + * @property message - The message to log. + * @property isBold - Whether to make the message bold. + * @property color - The color to use for the message. + */ +export type LoggerMessageComponent = + | string + | { message: string; isBold: boolean; color: LoggerMessageColor }; + +/** + * Operation outcome type. + */ +export type LoggerOperationResultType = "success" | "failure" | "warn" | "info"; + +/** + * Interface for the logger spinner. + * @property update - Updates the spinner with given components. + * @property stop - Stops the spinner with given result type and logs the result. + */ +export type LoggerSpinner = { + update: (...components: LoggerMessageComponent[]) => void; + stop: ( + type: LoggerOperationResultType, + ...components: LoggerMessageComponent[] + ) => void; +}; diff --git a/src/utils/CodeEvaluator.ts b/src/utils/CodeEvaluator.ts index c56eb30..f77509d 100644 --- a/src/utils/CodeEvaluator.ts +++ b/src/utils/CodeEvaluator.ts @@ -1,5 +1,6 @@ import { CodeEvaluationError } from "@/errors/CodeEvaluationError"; import { CodeEvaluationResult } from "@/types"; +import logger from "@/utils/logger"; export class CodeEvaluator { async evaluate( @@ -7,14 +8,32 @@ export class CodeEvaluator { context: any, sharedContext: Record = {}, ): Promise { + const loggerSpinner = logger.startSpinner({ + message: `Copilot evaluating code: \n\`\`\`\n${code}\n\`\`\``, + isBold: false, + color: "gray", + }); + const asyncFunction = this.createAsyncFunction( code, context, sharedContext, ); - const result = await asyncFunction(); - return { code, result, sharedContext }; + try { + const result = await asyncFunction(); + loggerSpinner.stop("success", `Copilot evaluated the code successfully`); + + return { code, result, sharedContext }; + } catch (error) { + loggerSpinner.stop("failure", { + message: `Copilot failed to evaluate the code: \n\`\`\`\n${code}\n\`\`\``, + isBold: false, + color: "gray", + }); + + throw error; + } } private createAsyncFunction( @@ -22,13 +41,6 @@ export class CodeEvaluator { context: any, sharedContext: Record, ): () => Promise { - // todo: this is a temp log for debugging, we'll need to pass a logging mechanism from the framework. - console.log( - "\x1b[90m%s\x1b[0m\x1b[92m%s\x1b[0m", - "Copilot evaluating code block:\n", - `${code}\n`, - ); - try { const contextValues = Object.values(context); diff --git a/src/utils/PilotPromptCreator.ts b/src/utils/PilotPromptCreator.ts index dda8c66..7057f66 100644 --- a/src/utils/PilotPromptCreator.ts +++ b/src/utils/PilotPromptCreator.ts @@ -162,10 +162,10 @@ export class PilotPromptCreator { "#### Next Action with Thoughts:", "", ` -To complete the registration process, tapping on the 'Submit' button (ID: btn_submit) is necessary. +To complete the registration process, tapping on the 'Submit' button is necessary. -Tap on the 'Submit' button (ID: btn_submit) +Tap on the 'Submit' button, which has the ID 'btn_submit' and is located at the bottom of the registration form. @@ -212,7 +212,7 @@ success An overall UX review summary based on the previous steps' reviews. - +Summary of UX findings from previous steps. 6/10 - This is an overall score for the entire flow. @@ -223,7 +223,7 @@ An overall UX review summary based on the previous steps' reviews. An overall accessibility review summary based on the previous steps' reviews. - + Summary of accessibility findings from previous steps. 5/10 - This is an overall score for the entire flow. diff --git a/src/utils/ScreenCapturer.ts b/src/utils/ScreenCapturer.ts index b4a105e..365003d 100644 --- a/src/utils/ScreenCapturer.ts +++ b/src/utils/ScreenCapturer.ts @@ -1,5 +1,6 @@ import { SnapshotManager } from "@/utils/SnapshotManager"; import { PromptHandler } from "@/types"; +import logger from "@/utils/logger"; export class ScreenCapturer { constructor( @@ -8,6 +9,9 @@ export class ScreenCapturer { ) {} async capture() { + const loggerSpinner = logger.startSpinner( + `Capturing ${this.promptHandler.isSnapshotImageSupported() ? "snapshot image and " : ""}view hierarchy.`, + ); const snapshot = this.promptHandler.isSnapshotImageSupported() ? await this.snapshotManager.captureSnapshotImage() : undefined; @@ -17,6 +21,10 @@ export class ScreenCapturer { const isSnapshotImageAttached = snapshot != null && this.promptHandler.isSnapshotImageSupported(); + loggerSpinner.stop( + "success", + "Captured snapshot image and view hierarchy.", + ); return { snapshot, viewHierarchy, isSnapshotImageAttached }; } } diff --git a/src/utils/__snapshots__/PilotPromptCreator.test.ts.snap b/src/utils/__snapshots__/PilotPromptCreator.test.ts.snap index 26c7543..aeb65b4 100644 --- a/src/utils/__snapshots__/PilotPromptCreator.test.ts.snap +++ b/src/utils/__snapshots__/PilotPromptCreator.test.ts.snap @@ -60,10 +60,10 @@ If you encounter any issues or have questions, please throw an informative error #### Next Action with Thoughts: -To complete the registration process, tapping on the 'Submit' button (ID: btn_submit) is necessary. +To complete the registration process, tapping on the 'Submit' button is necessary. -Tap on the 'Submit' button (ID: btn_submit) +Tap on the 'Submit' button, which has the ID 'btn_submit' and is located at the bottom of the registration form. @@ -109,7 +109,7 @@ success An overall UX review summary based on the previous steps' reviews. - +Summary of UX findings from previous steps. 6/10 - This is an overall score for the entire flow. @@ -120,7 +120,7 @@ An overall UX review summary based on the previous steps' reviews. An overall accessibility review summary based on the previous steps' reviews. - + Summary of accessibility findings from previous steps. 5/10 - This is an overall score for the entire flow. @@ -229,10 +229,10 @@ If you encounter any issues or have questions, please throw an informative error #### Next Action with Thoughts: -To complete the registration process, tapping on the 'Submit' button (ID: btn_submit) is necessary. +To complete the registration process, tapping on the 'Submit' button is necessary. -Tap on the 'Submit' button (ID: btn_submit) +Tap on the 'Submit' button, which has the ID 'btn_submit' and is located at the bottom of the registration form. @@ -278,7 +278,7 @@ success An overall UX review summary based on the previous steps' reviews. - +Summary of UX findings from previous steps. 6/10 - This is an overall score for the entire flow. @@ -289,7 +289,7 @@ An overall UX review summary based on the previous steps' reviews. An overall accessibility review summary based on the previous steps' reviews. - + Summary of accessibility findings from previous steps. 5/10 - This is an overall score for the entire flow. @@ -407,10 +407,10 @@ If you encounter any issues or have questions, please throw an informative error #### Next Action with Thoughts: -To complete the registration process, tapping on the 'Submit' button (ID: btn_submit) is necessary. +To complete the registration process, tapping on the 'Submit' button is necessary. -Tap on the 'Submit' button (ID: btn_submit) +Tap on the 'Submit' button, which has the ID 'btn_submit' and is located at the bottom of the registration form. @@ -456,7 +456,7 @@ success An overall UX review summary based on the previous steps' reviews. - +Summary of UX findings from previous steps. 6/10 - This is an overall score for the entire flow. @@ -467,7 +467,7 @@ An overall UX review summary based on the previous steps' reviews. An overall accessibility review summary based on the previous steps' reviews. - + Summary of accessibility findings from previous steps. 5/10 - This is an overall score for the entire flow. diff --git a/src/utils/logger/index.ts b/src/utils/logger/index.ts new file mode 100644 index 0000000..a126d26 --- /dev/null +++ b/src/utils/logger/index.ts @@ -0,0 +1,134 @@ +import { + createLogger, + format, + transports, + Logger as WinstonLogger, +} from "winston"; +import ora from "ora"; +import chalk from "chalk"; +import { + LoggerMessageComponent, + LoggerSpinner, + LoggerMessageColor, + LoggerOperationResultType, +} from "@/types/logger"; + +class Logger { + private static instance: Logger; + private readonly logger: WinstonLogger; + private readonly logLevels = ["info", "warn", "error", "debug"] as const; + private readonly colorMap: Record< + (typeof this.logLevels)[number], + LoggerMessageColor + > = { + info: "whiteBright", + warn: "yellow", + error: "red", + debug: "gray", + }; + + private constructor() { + this.logger = createLogger({ + level: "info", + format: format.combine( + format.colorize(), + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + format.printf( + ({ timestamp, level, message }) => + `[${timestamp}] ${level}: ${message}`, + ), + ), + transports: [new transports.Console()], + }); + } + + static getInstance(): Logger { + if (!Logger.instance) { + Logger.instance = new Logger(); + } + return Logger.instance; + } + + private colorizeMessage(...components: LoggerMessageComponent[]): string { + return components + .map((component) => { + if (typeof component === "string") { + return chalk.white(component); + } + + let coloredMessage = chalk[component.color](component.message); + + if (component.isBold) { + coloredMessage = chalk.bold(coloredMessage); + } + + return coloredMessage; + }) + .join(" "); + } + + private log( + level: (typeof this.logLevels)[number], + ...components: LoggerMessageComponent[] + ): void { + const newComponents = components.map((component) => { + if (typeof component === "string") { + // Overriding the component to include the specified color + return { + message: component, + isBold: false, + color: this.colorMap[level], + }; + } + + return component; + }); + + this.logger[level](this.colorizeMessage(...newComponents)); + } + + public info(...components: LoggerMessageComponent[]): void { + this.log("info", ...components); + } + + public warn(...components: LoggerMessageComponent[]): void { + this.log("warn", ...components); + } + + public error(...components: LoggerMessageComponent[]): void { + this.log("error", ...components); + } + + public debug(...components: LoggerMessageComponent[]): void { + this.log("debug", ...components); + } + + public startSpinner(...components: LoggerMessageComponent[]): LoggerSpinner { + const spinner = ora(this.colorizeMessage(...components)).start(); + + const stop = ( + result: LoggerOperationResultType, + ...components: LoggerMessageComponent[] + ) => { + spinner.prefixText = ""; + const message = this.colorizeMessage(...components); + + const spinnerActions: Record void> = { + success: () => spinner.succeed(message), + failure: () => spinner.fail(message), + warn: () => spinner.warn(message), + info: () => spinner.info(message), + }; + + spinnerActions[result]!(); + }; + + const update = (...components: LoggerMessageComponent[]) => { + spinner.text = this.colorizeMessage(...components); + }; + + return { update, stop }; + } +} + +export default Logger.getInstance();