diff --git a/bin/moses.ts b/bin/moses.ts index f5f6991..4862c6b 100755 --- a/bin/moses.ts +++ b/bin/moses.ts @@ -1,11 +1,14 @@ #!/usr/bin/env node import { Command } from 'commander'; -import { runInit } from '../src/commands/init/index.js'; -import { execute } from '../src/commands/validate/index.js'; -import { runSetDiffLimit, runSetFeedbackStyle } from '../src/commands/update-config/index.js'; -import { runListGitlabs } from '../src/commands/gitlab/list.js'; -import { runSwitchGitlab } from '../src/commands/gitlab/switch.js'; +import { InitCommand } from '../src/commands/init/index.js'; +import { ValidateCommand } from '../src/commands/validate/index.js'; +import { + SetDiffLimitCommand, + SetFeedbackStyleCommand, +} from '../src/commands/update-config/index.js'; +import { ListGitlabsCommand } from '../src/commands/gitlab/list.js'; +import { SwitchGitlabCommand } from '../src/commands/gitlab/switch.js'; import type { ValidateOptions } from '../src/types/index.js'; import packageJson from '../package.json' with { type: 'json' }; @@ -18,31 +21,40 @@ program ) .version(packageJson.version); -program.command('init').description('Interactive initial setup').action(runInit); +program + .command('init') + .description('Interactive initial setup') + .action(() => InitCommand.run()); program .command('validate') .description('Validate a GitLab Merge Request with an AI tool') .argument('', 'GitLab Merge Request URL') .option('-p, --prompt ', 'Additional context prompt to send with MR diff') - .action((url: string, options: ValidateOptions) => execute(url, options)); + .action((url: string, options: ValidateOptions) => ValidateCommand.execute(url, options)); const config = program.command('config').description('Manage Moses configuration'); config .command('feedback-style') .description('Update AI feedback style on Merge Requests') - .action(runSetFeedbackStyle); + .action(() => SetFeedbackStyleCommand.run()); config .command('diff-limit') .description('Update maximum allowed diff changes limit') - .action(runSetDiffLimit); + .action(() => SetDiffLimitCommand.run()); const gitlab = program.command('gitlab').description('Manage GitLab instances'); -gitlab.command('list').description('List all configured GitLab instances').action(runListGitlabs); +gitlab + .command('list') + .description('List all configured GitLab instances') + .action(() => ListGitlabsCommand.run()); -gitlab.command('default').description('Change the default GitLab instance').action(runSwitchGitlab); +gitlab + .command('default') + .description('Change the default GitLab instance') + .action(() => SwitchGitlabCommand.run()); void program.parseAsync(process.argv); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..91949a4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2803 @@ +{ + "name": "moses-cli", + "version": "1.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "moses-cli", + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "@inquirer/prompts": "^7.3.2", + "axios": "^1.11.0", + "boxen": "^8.0.1", + "chalk": "^5.4.1", + "commander": "^12.1.0", + "dayjs": "^1.11.13", + "figlet": "^1.8.2", + "ora": "^8.1.1" + }, + "bin": { + "lourran": "dist/bin/moses.js", + "moses": "dist/bin/moses.js" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^25.5.0", + "@typescript-eslint/eslint-plugin": "^8.58.0", + "@typescript-eslint/parser": "^8.58.0", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "globals": "^17.4.0", + "husky": "^9.1.7", + "prettier": "^3.8.1", + "typescript": "^6.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", + "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/type-utils": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz", + "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", + "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.1", + "@typescript-eslint/types": "^8.58.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", + "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", + "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz", + "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", + "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", + "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.1", + "@typescript-eslint/tsconfig-utils": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", + "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", + "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "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", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/figlet": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.11.0.tgz", + "integrity": "sha512-EEx3OS/l2bFqcUNN2NM9FPJp8vAMrgbCxsbl2hbcJNNxOEwVe3mEzrhan7TbJQViZa8mMqhihlbCaqD+LyYKTQ==", + "license": "MIT", + "dependencies": { + "commander": "^14.0.0" + }, + "bin": { + "figlet": "bin/index.js" + }, + "engines": { + "node": ">= 17.0.0" + } + }, + "node_modules/figlet/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "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" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "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/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "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", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/commands/gitlab/list.ts b/src/commands/gitlab/list.ts index 19946ca..e422a6e 100644 --- a/src/commands/gitlab/list.ts +++ b/src/commands/gitlab/list.ts @@ -1,37 +1,39 @@ -import { readConfig } from '../../utils/config-store.js'; -import * as display from '../../utils/display.js'; +import { ConfigStore } from '../../utils/config-store.js'; +import { Display } from '../../utils/display.js'; -export async function runListGitlabs(): Promise { - display.banner(); +export class ListGitlabsCommand { + static async run(): Promise { + Display.banner(); - try { - const config = await readConfig(); + try { + const config = await ConfigStore.readConfig(); - if (!config || config.gitlabs.length === 0) { - display.warn('No GitLab instances configured.'); - display.info('Run "moses init" to add a new instance.'); - return; - } + if (!config || config.gitlabs.length === 0) { + Display.warn('No GitLab instances configured.'); + Display.info('Run "moses init" to add a new instance.'); + return; + } - display.section('šŸ“‹ CONFIGURED GITLAB INSTANCES'); + Display.section('šŸ“‹ CONFIGURED GITLAB INSTANCES'); - config.gitlabs.forEach((gitlab) => { - const isDefault = gitlab.name === config.defaultGitlab; - const indicator = isDefault ? 'ā­ļø ' : 'šŸ”¹ '; - const label = isDefault ? ` (DEFAULT)` : ''; + config.gitlabs.forEach((gitlab) => { + const isDefault = gitlab.name === config.defaultGitlab; + const indicator = isDefault ? 'ā­ļø ' : 'šŸ”¹ '; + const label = isDefault ? ` (DEFAULT)` : ''; - display.info(`${indicator}${gitlab.name}${label}`); - display.info(` URL: ${gitlab.url}`); - display.info( - ` Token: ${gitlab.token.replace(/./g, '*').substring(0, 10)}... (last 4 chars: ${gitlab.token.slice(-4)})`, - ); - console.log(''); - }); + Display.info(`${indicator}${gitlab.name}${label}`); + Display.info(` URL: ${gitlab.url}`); + Display.info( + ` Token: ${gitlab.token.replace(/./g, '*').substring(0, 10)}... (last 4 chars: ${gitlab.token.slice(-4)})`, + ); + console.log(''); + }); - display.info(`TIP: You can use "moses gitlab default" to change the default instance.`); - } catch (error) { - display.error('Could not load Moses configuration.'); - display.info('Run "moses init" if you haven\'t yet.'); - console.log(error); + Display.info(`TIP: You can use "moses gitlab default" to change the default instance.`); + } catch (error) { + Display.error('Could not load Moses configuration.'); + Display.info('Run "moses init" if you haven\'t yet.'); + console.log(error); + } } } diff --git a/src/commands/gitlab/switch.ts b/src/commands/gitlab/switch.ts index 87f4535..b9dedae 100644 --- a/src/commands/gitlab/switch.ts +++ b/src/commands/gitlab/switch.ts @@ -1,45 +1,47 @@ import { select } from '@inquirer/prompts'; -import { readConfig, saveConfig } from '../../utils/config-store.js'; -import * as display from '../../utils/display.js'; +import { ConfigStore } from '../../utils/config-store.js'; +import { Display } from '../../utils/display.js'; import type { MosesConfig } from '../../types/MosesConfig.js'; -export async function runSwitchGitlab(): Promise { - display.banner(); - - try { - const config = await readConfig(); - - if (!config || config.gitlabs.length === 0) { - display.warn('No GitLab instances configured.'); - return; +export class SwitchGitlabCommand { + static async run(): Promise { + Display.banner(); + + try { + const config = await ConfigStore.readConfig(); + + if (!config || config.gitlabs.length === 0) { + Display.warn('No GitLab instances configured.'); + return; + } + + const choices = config.gitlabs.map((gitlab) => ({ + name: `${gitlab.name} (${gitlab.url})`, + value: gitlab.name, + })); + + const nextDefault = await select({ + message: 'Choose the default GitLab instance:', + choices, + default: config.defaultGitlab, + }); + + const updatedGitlabs = config.gitlabs.map((gitlab) => ({ + ...gitlab, + default: gitlab.name === nextDefault, + })); + + const nextConfig: MosesConfig = { + ...config, + defaultGitlab: nextDefault, + gitlabs: updatedGitlabs, + }; + + await ConfigStore.saveConfig(nextConfig); + Display.success(`Default GitLab switched to: ${nextDefault}`); + } catch (error) { + Display.error('Could not switch GitLab instance.'); + console.log(error); } - - const choices = config.gitlabs.map((gitlab) => ({ - name: `${gitlab.name} (${gitlab.url})`, - value: gitlab.name, - })); - - const nextDefault = await select({ - message: 'Choose the default GitLab instance:', - choices, - default: config.defaultGitlab, - }); - - const updatedGitlabs = config.gitlabs.map((gitlab) => ({ - ...gitlab, - default: gitlab.name === nextDefault, - })); - - const nextConfig: MosesConfig = { - ...config, - defaultGitlab: nextDefault, - gitlabs: updatedGitlabs, - }; - - await saveConfig(nextConfig); - display.success(`Default GitLab switched to: ${nextDefault}`); - } catch (error) { - display.error('Could not switch GitLab instance.'); - console.log(error); } } diff --git a/src/commands/init/ai-wizard.ts b/src/commands/init/ai-wizard.ts index 9f54bfa..caee2fe 100644 --- a/src/commands/init/ai-wizard.ts +++ b/src/commands/init/ai-wizard.ts @@ -1,7 +1,7 @@ import { confirm, input, select } from '@inquirer/prompts'; import { AI_TOOLS, DEFAULT_MAX_DIFF_CHANGES, FEEDBACK_STYLES } from '../../constants.js'; -import * as display from '../../utils/display.js'; -import { validateToolInstallation } from '../../utils/tool-validator.js'; +import { Display } from '../../utils/display.js'; +import { ToolValidator } from '../../utils/tool-validator.js'; import type { AiToolKey } from '../../types/AiToolKey.js'; import type { FeedbackStyle } from '../../types/FeedbackStyle.js'; import type { MosesConfig } from '../../types/MosesConfig.js'; @@ -12,79 +12,86 @@ export interface AiSetupData { maxDiffChanges: number; } -export async function promptAiSetup(existingConfig: MosesConfig | null): Promise { - display.section('šŸ¤– AI TOOL CONFIGURATION'); - display.info('šŸ’” TIP: Moses uses local AI tools to process reviews. Make sure your chosen tool'); - display.info(' is installed and configured with the necessary API keys.'); +export class AiWizard { + static async promptAiSetup(existingConfig: MosesConfig | null): Promise { + Display.section('šŸ¤– AI TOOL CONFIGURATION'); + Display.info( + 'šŸ’” TIP: Moses uses local AI tools to process reviews. Make sure your chosen tool', + ); + Display.info(' is installed and configured with the necessary API keys.'); - const tool = await chooseAiTool(existingConfig?.ai?.tool); + const tool = await AiWizard.chooseAiTool(existingConfig?.ai?.tool); - display.info('\nšŸ’” Feedback Style: Choose how you want the AI to post comments on the MR.'); - const feedbackStyle = await chooseFeedbackStyle(existingConfig?.ai?.feedbackStyle); + Display.info('\nšŸ’” Feedback Style: Choose how you want the AI to post comments on the MR.'); + const feedbackStyle = await AiWizard.chooseFeedbackStyle(existingConfig?.ai?.feedbackStyle); - display.info('\nšŸ’” Diff Limits: Large files can be slow and expensive (tokens).'); - display.info(' This limit skips files with too many changes.'); - const maxDiffChanges = await chooseMaxDiffChanges(existingConfig?.ai?.maxDiffChanges); + Display.info('\nšŸ’” Diff Limits: Large files can be slow and expensive (tokens).'); + Display.info(' This limit skips files with too many changes.'); + const maxDiffChanges = await AiWizard.chooseMaxDiffChanges(existingConfig?.ai?.maxDiffChanges); - return { tool, feedbackStyle, maxDiffChanges }; -} + return { tool, feedbackStyle, maxDiffChanges }; + } -export async function chooseAiTool(existingTool: AiToolKey | undefined): Promise { - while (true) { - const chosen = await select({ - message: 'Choose the AI tool for review:', - choices: AI_TOOLS.map((tool) => ({ name: tool.name, value: tool.key })), - default: existingTool, - }); + static async chooseAiTool(existingTool: AiToolKey | undefined): Promise { + while (true) { + const chosen = await select({ + message: 'Choose the AI tool for review:', + choices: AI_TOOLS.map((tool) => ({ name: tool.name, value: tool.key })), + default: existingTool, + }); - const toolInfo = AI_TOOLS.find((tool) => tool.key === chosen); - if (!toolInfo) { - throw new Error(`Unsupported AI tool: ${String(chosen)}`); - } + const toolInfo = AI_TOOLS.find((tool) => tool.key === chosen); + if (!toolInfo) { + throw new Error(`Unsupported AI tool: ${String(chosen)}`); + } - const toolSpinner = display.spinner(`Checking ${toolInfo.name} installation...`); - const validation = validateToolInstallation(toolInfo.key); - toolSpinner.stop(); + const toolSpinner = Display.spinner(`Checking ${toolInfo.name} installation...`); + const validation = ToolValidator.validateToolInstallation(toolInfo.key); + toolSpinner.stop(); - if (validation.installed) { - display.success(`${toolInfo.name} found at ${validation.path}`); - return toolInfo.key; - } + if (validation.installed) { + Display.success(`${toolInfo.name} found at ${validation.path}`); + return toolInfo.key; + } - display.error(`${toolInfo.name} not found!`); - display.info(`\nšŸ“¦ Install with:\n ${validation.installCmd ?? toolInfo.install}`); - display.info(`\nšŸ“– Documentation: ${validation.installUrl}\n`); - const retry = await confirm({ message: 'Do you want to choose another tool?', default: true }); - if (!retry) { - throw new Error('AI tool not installed. Install a supported tool and run again.'); + Display.error(`${toolInfo.name} not found!`); + Display.info(`\nšŸ“¦ Install with:\n ${validation.installCmd ?? toolInfo.install}`); + Display.info(`\nšŸ“– Documentation: ${validation.installUrl}\n`); + const retry = await confirm({ + message: 'Do you want to choose another tool?', + default: true, + }); + if (!retry) { + throw new Error('AI tool not installed. Install a supported tool and run again.'); + } } } -} -export async function chooseFeedbackStyle( - existingStyle: FeedbackStyle | undefined, -): Promise { - const defaultStyle = - FEEDBACK_STYLES.find((item) => item.key === existingStyle)?.key ?? FEEDBACK_STYLES[1].key; - return select({ - message: 'Choose MR feedback style:', - choices: FEEDBACK_STYLES.map((item) => ({ name: item.label, value: item.key })), - default: defaultStyle, - }); -} - -export async function chooseMaxDiffChanges(existingLimit: number | undefined): Promise { - const fallback = - typeof existingLimit === 'number' && Number.isInteger(existingLimit) && existingLimit > 0 - ? existingLimit - : DEFAULT_MAX_DIFF_CHANGES; - while (true) { - const value = await input({ - message: 'Maximum allowed diff changes before interrupting validation:', - default: String(fallback), + static async chooseFeedbackStyle( + existingStyle: FeedbackStyle | undefined, + ): Promise { + const defaultStyle = + FEEDBACK_STYLES.find((item) => item.key === existingStyle)?.key ?? FEEDBACK_STYLES[1].key; + return select({ + message: 'Choose MR feedback style:', + choices: FEEDBACK_STYLES.map((item) => ({ name: item.label, value: item.key })), + default: defaultStyle, }); - const parsed = Number.parseInt(value, 10); - if (Number.isInteger(parsed) && parsed > 0) return parsed; - display.error('Invalid value. Please inform a positive integer.'); + } + + static async chooseMaxDiffChanges(existingLimit: number | undefined): Promise { + const fallback = + typeof existingLimit === 'number' && Number.isInteger(existingLimit) && existingLimit > 0 + ? existingLimit + : DEFAULT_MAX_DIFF_CHANGES; + while (true) { + const value = await input({ + message: 'Maximum allowed diff changes before interrupting validation:', + default: String(fallback), + }); + const parsed = Number.parseInt(value, 10); + if (Number.isInteger(parsed) && parsed > 0) return parsed; + Display.error('Invalid value. Please inform a positive integer.'); + } } } diff --git a/src/commands/init/config-builder.ts b/src/commands/init/config-builder.ts index bae0b0b..a63fdbd 100644 --- a/src/commands/init/config-builder.ts +++ b/src/commands/init/config-builder.ts @@ -1,54 +1,56 @@ import { CONFIG_VERSION, DEFAULT_OUTPUT_DIR } from '../../constants.js'; -import { ensureDefaultContextFiles } from '../../services/context.js'; -import { saveConfig } from '../../utils/config-store.js'; +import { ContextService } from '../../services/context.js'; +import { ConfigStore } from '../../utils/config-store.js'; import type { MosesConfig } from '../../types/MosesConfig.js'; import type { GitlabSetupData } from './gitlab-wizard.js'; import type { AiSetupData } from './ai-wizard.js'; -export async function buildAndSaveConfig( - gitlab: GitlabSetupData, - ai: AiSetupData, - existing: MosesConfig | null, -): Promise<{ configPath: string; contextInfo: { contextDir: string; files: string[] } }> { - const baseConfig: MosesConfig = existing ?? { - version: CONFIG_VERSION, - defaultGitlab: gitlab.name, - gitlabs: [], - ai: { - tool: ai.tool, - customCommand: null, - model: null, - feedbackStyle: ai.feedbackStyle, - maxDiffChanges: ai.maxDiffChanges, - }, - output: { dir: DEFAULT_OUTPUT_DIR, keepFiles: true }, - }; +export class InitConfigBuilder { + static async buildAndSaveConfig( + gitlab: GitlabSetupData, + ai: AiSetupData, + existing: MosesConfig | null, + ): Promise<{ configPath: string; contextInfo: { contextDir: string; files: string[] } }> { + const baseConfig: MosesConfig = existing ?? { + version: CONFIG_VERSION, + defaultGitlab: gitlab.name, + gitlabs: [], + ai: { + tool: ai.tool, + customCommand: null, + model: null, + feedbackStyle: ai.feedbackStyle, + maxDiffChanges: ai.maxDiffChanges, + }, + output: { dir: DEFAULT_OUTPUT_DIR, keepFiles: true }, + }; - const remainingGitlabs = baseConfig.gitlabs.filter((item) => item.name !== gitlab.name); - const gitlabs = [ - ...remainingGitlabs, - { name: gitlab.name, url: gitlab.url, token: gitlab.token, default: true }, - ].map((item) => ({ - ...item, - default: item.name === gitlab.name, - })); + const remainingGitlabs = baseConfig.gitlabs.filter((item) => item.name !== gitlab.name); + const gitlabs = [ + ...remainingGitlabs, + { name: gitlab.name, url: gitlab.url, token: gitlab.token, default: true }, + ].map((item) => ({ + ...item, + default: item.name === gitlab.name, + })); - const config: MosesConfig = { - ...baseConfig, - version: CONFIG_VERSION, - defaultGitlab: gitlab.name, - gitlabs, - ai: { - ...baseConfig.ai, - tool: ai.tool, - customCommand: null, - model: null, - feedbackStyle: ai.feedbackStyle, - maxDiffChanges: ai.maxDiffChanges, - }, - }; + const config: MosesConfig = { + ...baseConfig, + version: CONFIG_VERSION, + defaultGitlab: gitlab.name, + gitlabs, + ai: { + ...baseConfig.ai, + tool: ai.tool, + customCommand: null, + model: null, + feedbackStyle: ai.feedbackStyle, + maxDiffChanges: ai.maxDiffChanges, + }, + }; - const path = await saveConfig(config); - const contextInfo = await ensureDefaultContextFiles(); - return { configPath: path, contextInfo }; + const configPath = await ConfigStore.saveConfig(config); + const contextInfo = await ContextService.ensureDefaultContextFiles(); + return { configPath, contextInfo }; + } } diff --git a/src/commands/init/config-loader.ts b/src/commands/init/config-loader.ts index 7fcdd53..2846f5d 100644 --- a/src/commands/init/config-loader.ts +++ b/src/commands/init/config-loader.ts @@ -1,20 +1,20 @@ -import { - checkAndFixConfigPermissions, - getConfigPath, - readConfig, -} from '../../utils/config-store.js'; -import * as display from '../../utils/display.js'; +import { ConfigStore } from '../../utils/config-store.js'; +import { Display } from '../../utils/display.js'; import type { MosesConfig } from '../../types/MosesConfig.js'; -export async function loadExistingConfig(): Promise { - try { - const config = await readConfig(); - const permissionStatus = await checkAndFixConfigPermissions(); - if (permissionStatus.fixed) { - display.warn(`Permissions were automatically fixed to 600 at ${getConfigPath()}`); +export class InitConfigLoader { + static async loadExistingConfig(): Promise { + try { + const config = await ConfigStore.readConfig(); + const permissionStatus = await ConfigStore.checkAndFixConfigPermissions(); + if (permissionStatus.fixed) { + Display.warn( + `Permissions were automatically fixed to 600 at ${ConfigStore.getConfigPath()}`, + ); + } + return config; + } catch { + return null; } - return config; - } catch { - return null; } } diff --git a/src/commands/init/gitlab-wizard.ts b/src/commands/init/gitlab-wizard.ts index b216fec..338b5e5 100644 --- a/src/commands/init/gitlab-wizard.ts +++ b/src/commands/init/gitlab-wizard.ts @@ -1,8 +1,8 @@ import axios from 'axios'; import { input, password, select } from '@inquirer/prompts'; -import { validateGitlabUrl } from '../../utils/url-parser.js'; -import * as display from '../../utils/display.js'; -import { validateToken } from '../../services/gitlab.js'; +import { UrlParser } from '../../utils/url-parser.js'; +import { Display } from '../../utils/display.js'; +import { GitlabService } from '../../services/gitlab.js'; import type { MosesConfig } from '../../types/MosesConfig.js'; export interface GitlabSetupData { @@ -11,75 +11,78 @@ export interface GitlabSetupData { token: string; } -export async function promptGitlabSetup( - existingConfig: MosesConfig | null, -): Promise { - display.section('šŸ“‹ GITLAB CONFIGURATION'); - display.info( - 'šŸ’” TIP: Use a nickname to Identify this GitLab config (e.g. "work", "gitlab-org").', - ); - display.info(' You can run "init" again later to add more instances.'); +export class GitlabWizard { + static async promptGitlabSetup(existingConfig: MosesConfig | null): Promise { + Display.section('šŸ“‹ GITLAB CONFIGURATION'); + Display.info( + 'šŸ’” TIP: Use a nickname to Identify this GitLab config (e.g. "work", "gitlab-org").', + ); + Display.info(' You can run "init" again later to add more instances.'); - const name = await input({ - message: 'Instance nickname (e.g., "work", "gitlab-com" - to identify this GitLab config):', - default: existingConfig?.defaultGitlab ?? 'gitlab-main', - }); + const name = await input({ + message: 'Instance nickname (e.g., "work", "gitlab-com" - to identify this GitLab config):', + default: existingConfig?.defaultGitlab ?? 'gitlab-main', + }); - const url = await chooseGitlabBaseUrl(); - const settingsBase = url.replace(/\/$/, ''); - display.info(`šŸ’” Create a new Personal Access Token with "api" scope here:`); - display.link(`${settingsBase}/-/user_settings/personal_access_tokens`); + const url = await GitlabWizard.chooseGitlabBaseUrl(); + const settingsBase = url.replace(/\/$/, ''); + Display.info(`šŸ’” Create a new Personal Access Token with "api" scope here:`); + Display.link(`${settingsBase}/-/user_settings/personal_access_tokens`); - let token = ''; - while (true) { - token = await password({ - message: 'Personal Access Token (scope: api):', - mask: '*', - }); + let token = ''; + while (true) { + token = await password({ + message: 'Personal Access Token (scope: api):', + mask: '*', + }); - try { - await validateGitlabToken(url, token); - break; - } catch { - // Just loop back on invalid token + try { + await GitlabWizard.validateGitlabToken(url, token); + break; + } catch { + // Just loop back on invalid token + } } - } - return { name, url, token }; -} + return { name, url, token }; + } -export async function chooseGitlabBaseUrl(): Promise { - const gitlabType = await select({ - message: 'Which GitLab do you want to use?', - choices: [ - { name: 'GitLab.com (gitlab.com) — Default', value: 'default' }, - { name: 'Self-Hosted GitLab (provide a custom URL)', value: 'self' }, - ], - }); + static async chooseGitlabBaseUrl(): Promise { + const gitlabType = await select({ + message: 'Which GitLab do you want to use?', + choices: [ + { name: 'GitLab.com (gitlab.com) — Default', value: 'default' }, + { name: 'Self-Hosted GitLab (provide a custom URL)', value: 'self' }, + ], + }); - if (gitlabType === 'default') { - return 'https://gitlab.com'; - } + if (gitlabType === 'default') { + return 'https://gitlab.com'; + } - while (true) { - const url = await input({ message: 'GitLab URL:', default: 'https://gitlab.your-domain.com' }); - if (validateGitlabUrl(url)) return url; - display.error('Invalid URL. Use https:// and a valid domain.'); + while (true) { + const url = await input({ + message: 'GitLab URL:', + default: 'https://gitlab.your-domain.com', + }); + if (UrlParser.validateGitlabUrl(url)) return url; + Display.error('Invalid URL. Use https:// and a valid domain.'); + } } -} -export async function validateGitlabToken(gitlabUrl: string, token: string) { - const tokenSpinner = display.spinner('Validating token...'); - try { - const user = await validateToken(gitlabUrl, token); - tokenSpinner.succeed(`Valid token! User: @${user.username}`); - return user; - } catch (error: unknown) { - const status = axios.isAxiosError(error) ? error.response?.status : undefined; - const message = status ? `Failed (Status ${status})` : 'Invalid or expired token.'; - tokenSpinner.fail(message); - const settingsBase = gitlabUrl.replace(/\/$/, ''); - display.link(` ${settingsBase}/-/user_settings/personal_access_tokens`); - throw error; + static async validateGitlabToken(gitlabUrl: string, token: string) { + const tokenSpinner = Display.spinner('Validating token...'); + try { + const user = await GitlabService.validateToken(gitlabUrl, token); + tokenSpinner.succeed(`Valid token! User: @${user.username}`); + return user; + } catch (error: unknown) { + const status = axios.isAxiosError(error) ? error.response?.status : undefined; + const message = status ? `Failed (Status ${status})` : 'Invalid or expired token.'; + tokenSpinner.fail(message); + const settingsBase = gitlabUrl.replace(/\/$/, ''); + Display.link(` ${settingsBase}/-/user_settings/personal_access_tokens`); + throw error; + } } } diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index 7711b23..346fe0d 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -1,36 +1,42 @@ import { confirm } from '@inquirer/prompts'; import { MESSAGES } from '../../constants.js'; -import * as display from '../../utils/display.js'; -import { loadExistingConfig } from './config-loader.js'; -import { promptGitlabSetup } from './gitlab-wizard.js'; -import { promptAiSetup } from './ai-wizard.js'; -import { buildAndSaveConfig } from './config-builder.js'; +import { Display } from '../../utils/display.js'; +import { InitConfigLoader } from './config-loader.js'; +import { GitlabWizard } from './gitlab-wizard.js'; +import { AiWizard } from './ai-wizard.js'; +import { InitConfigBuilder } from './config-builder.js'; -export async function runInit(): Promise { - display.banner(); - display.info(MESSAGES.welcome); +export class InitCommand { + static async run(): Promise { + Display.banner(); + Display.info(MESSAGES.welcome); - const existingConfig = await loadExistingConfig(); - if (existingConfig) { - const overwrite = await confirm({ - message: 'Existing configuration found. Do you want to overwrite/add a new instance?', - default: true, - }); - if (!overwrite) { - display.info('No changes applied.'); - return; + const existingConfig = await InitConfigLoader.loadExistingConfig(); + if (existingConfig) { + const overwrite = await confirm({ + message: 'Existing configuration found. Do you want to overwrite/add a new instance?', + default: true, + }); + if (!overwrite) { + Display.info('No changes applied.'); + return; + } } - } - const gitlabData = await promptGitlabSetup(existingConfig); - const aiData = await promptAiSetup(existingConfig); + const gitlabData = await GitlabWizard.promptGitlabSetup(existingConfig); + const aiData = await AiWizard.promptAiSetup(existingConfig); - const { configPath, contextInfo } = await buildAndSaveConfig(gitlabData, aiData, existingConfig); + const { configPath, contextInfo } = await InitConfigBuilder.buildAndSaveConfig( + gitlabData, + aiData, + existingConfig, + ); - display.success(MESSAGES.done); - display.info(`šŸ“ Config saved at ${configPath} (mode 600)`); - display.info(`šŸ“ Context files saved at ${contextInfo.contextDir}:`); - contextInfo.files.forEach((file) => display.info(` - ${file}`)); + Display.success(MESSAGES.done); + Display.info(`šŸ“ Config saved at ${configPath} (mode 600)`); + Display.info(`šŸ“ Context files saved at ${contextInfo.contextDir}:`); + contextInfo.files.forEach((file) => Display.info(` - ${file}`)); - display.info(`\n${MESSAGES.next}`); + Display.info(`\n${MESSAGES.next}`); + } } diff --git a/src/commands/update-config/index.ts b/src/commands/update-config/index.ts index cceb964..3621913 100644 --- a/src/commands/update-config/index.ts +++ b/src/commands/update-config/index.ts @@ -1,2 +1,2 @@ -export { runSetFeedbackStyle } from './set-feedback-style.js'; -export { runSetDiffLimit } from './set-diff-limit.js'; +export { SetFeedbackStyleCommand } from './set-feedback-style.js'; +export { SetDiffLimitCommand } from './set-diff-limit.js'; diff --git a/src/commands/update-config/load-config.ts b/src/commands/update-config/load-config.ts index e973538..6b611df 100644 --- a/src/commands/update-config/load-config.ts +++ b/src/commands/update-config/load-config.ts @@ -1,18 +1,20 @@ import { MESSAGES } from '../../constants.js'; -import { checkAndFixConfigPermissions, readConfig } from '../../utils/config-store.js'; -import * as display from '../../utils/display.js'; +import { ConfigStore } from '../../utils/config-store.js'; +import { Display } from '../../utils/display.js'; import type { MosesConfig } from '../../types/MosesConfig.js'; -export async function loadConfigOrExit(): Promise { - try { - const config = await readConfig(); - const permissionStatus = await checkAndFixConfigPermissions(); - if (permissionStatus.fixed) { - display.warn('Config permissions were incorrect and have been fixed to 600.'); +export class UpdateConfigLoader { + static async loadConfigOrExit(): Promise { + try { + const config = await ConfigStore.readConfig(); + const permissionStatus = await ConfigStore.checkAndFixConfigPermissions(); + if (permissionStatus.fixed) { + Display.warn('Config permissions were incorrect and have been fixed to 600.'); + } + return config; + } catch { + Display.error(MESSAGES.noConfig); + return null; } - return config; - } catch { - display.error(MESSAGES.noConfig); - return null; } } diff --git a/src/commands/update-config/set-diff-limit.ts b/src/commands/update-config/set-diff-limit.ts index b1b350d..30896fc 100644 --- a/src/commands/update-config/set-diff-limit.ts +++ b/src/commands/update-config/set-diff-limit.ts @@ -1,38 +1,40 @@ import { input } from '@inquirer/prompts'; import { DEFAULT_MAX_DIFF_CHANGES } from '../../constants.js'; -import { saveConfig } from '../../utils/config-store.js'; -import * as display from '../../utils/display.js'; +import { ConfigStore } from '../../utils/config-store.js'; +import { Display } from '../../utils/display.js'; import type { MosesConfig } from '../../types/MosesConfig.js'; -import { loadConfigOrExit } from './load-config.js'; +import { UpdateConfigLoader } from './load-config.js'; -export async function runSetDiffLimit(): Promise { - display.banner(); - const config = await loadConfigOrExit(); - if (!config) return; +export class SetDiffLimitCommand { + static async run(): Promise { + Display.banner(); + const config = await UpdateConfigLoader.loadConfigOrExit(); + if (!config) return; - const current = config.ai?.maxDiffChanges; - const fallback = Number.isInteger(current) && current > 0 ? current : DEFAULT_MAX_DIFF_CHANGES; + const current = config.ai?.maxDiffChanges; + const fallback = Number.isInteger(current) && current > 0 ? current : DEFAULT_MAX_DIFF_CHANGES; - while (true) { - const value = await input({ - message: 'Maximum allowed diff changes before interrupting validation:', - default: String(fallback), - }); - const parsed = Number.parseInt(value, 10); - if (!Number.isInteger(parsed) || parsed <= 0) { - display.error('Invalid value. Please inform a positive integer.'); - continue; - } + while (true) { + const value = await input({ + message: 'Maximum allowed diff changes before interrupting validation:', + default: String(fallback), + }); + const parsed = Number.parseInt(value, 10); + if (!Number.isInteger(parsed) || parsed <= 0) { + Display.error('Invalid value. Please inform a positive integer.'); + continue; + } - const nextConfig: MosesConfig = { - ...config, - ai: { - ...config.ai, - maxDiffChanges: parsed, - }, - }; - await saveConfig(nextConfig); - display.success(`Diff limit updated successfully to ${parsed}.`); - return; + const nextConfig: MosesConfig = { + ...config, + ai: { + ...config.ai, + maxDiffChanges: parsed, + }, + }; + await ConfigStore.saveConfig(nextConfig); + Display.success(`Diff limit updated successfully to ${parsed}.`); + return; + } } } diff --git a/src/commands/update-config/set-feedback-style.ts b/src/commands/update-config/set-feedback-style.ts index 9a4131d..5be507a 100644 --- a/src/commands/update-config/set-feedback-style.ts +++ b/src/commands/update-config/set-feedback-style.ts @@ -1,31 +1,33 @@ import { select } from '@inquirer/prompts'; import { FEEDBACK_STYLES } from '../../constants.js'; -import { saveConfig } from '../../utils/config-store.js'; -import * as display from '../../utils/display.js'; +import { ConfigStore } from '../../utils/config-store.js'; +import { Display } from '../../utils/display.js'; import type { FeedbackStyle } from '../../types/FeedbackStyle.js'; import type { MosesConfig } from '../../types/MosesConfig.js'; -import { loadConfigOrExit } from './load-config.js'; +import { UpdateConfigLoader } from './load-config.js'; -export async function runSetFeedbackStyle(): Promise { - display.banner(); - const config = await loadConfigOrExit(); - if (!config) return; +export class SetFeedbackStyleCommand { + static async run(): Promise { + Display.banner(); + const config = await UpdateConfigLoader.loadConfigOrExit(); + if (!config) return; - const current = config.ai?.feedbackStyle; - const style: FeedbackStyle = await select({ - message: 'Choose MR feedback style:', - choices: FEEDBACK_STYLES.map((item) => ({ name: item.label, value: item.key })), - default: FEEDBACK_STYLES.find((item) => item.key === current)?.key ?? FEEDBACK_STYLES[1].key, - }); + const current = config.ai?.feedbackStyle; + const style: FeedbackStyle = await select({ + message: 'Choose MR feedback style:', + choices: FEEDBACK_STYLES.map((item) => ({ name: item.label, value: item.key })), + default: FEEDBACK_STYLES.find((item) => item.key === current)?.key ?? FEEDBACK_STYLES[1].key, + }); - const nextConfig: MosesConfig = { - ...config, - ai: { - ...config.ai, - feedbackStyle: style, - }, - }; + const nextConfig: MosesConfig = { + ...config, + ai: { + ...config.ai, + feedbackStyle: style, + }, + }; - await saveConfig(nextConfig); - display.success('Feedback style updated successfully.'); + await ConfigStore.saveConfig(nextConfig); + Display.success('Feedback style updated successfully.'); + } } diff --git a/src/commands/validate/check-limits.ts b/src/commands/validate/check-limits.ts index 06ed0e9..e9e17c1 100644 --- a/src/commands/validate/check-limits.ts +++ b/src/commands/validate/check-limits.ts @@ -1,16 +1,18 @@ -import { countDiffChanges } from '../../services/markdown.js'; -import * as display from '../../utils/display.js'; +import { MarkdownService } from '../../services/markdown.js'; +import { Display } from '../../utils/display.js'; import type { MosesConfig } from '../../types/MosesConfig.js'; import type { MergeRequestDiff } from '../../types/MergeRequestDiff.js'; -export function isDiffWithinLimits(diffs: MergeRequestDiff[], config: MosesConfig): boolean { - const maxDiffChanges = config.ai?.maxDiffChanges; - const totalChanges = countDiffChanges(diffs); - if (Number.isInteger(maxDiffChanges) && maxDiffChanges > 0 && totalChanges > maxDiffChanges) { - display.warn( - `Diff interrupted: total changes (${totalChanges}) exceeds the configured limit (${maxDiffChanges}). Update the limit with: moses set-diff-limit`, - ); - return false; +export class DiffLimitChecker { + static isDiffWithinLimits(diffs: MergeRequestDiff[], config: MosesConfig): boolean { + const maxDiffChanges = config.ai?.maxDiffChanges; + const totalChanges = MarkdownService.countDiffChanges(diffs); + if (Number.isInteger(maxDiffChanges) && maxDiffChanges > 0 && totalChanges > maxDiffChanges) { + Display.warn( + `Diff interrupted: total changes (${totalChanges}) exceeds the configured limit (${maxDiffChanges}). Update the limit with: moses set-diff-limit`, + ); + return false; + } + return true; } - return true; } diff --git a/src/commands/validate/gitlab.ts b/src/commands/validate/gitlab.ts index a2784b8..e8080bd 100644 --- a/src/commands/validate/gitlab.ts +++ b/src/commands/validate/gitlab.ts @@ -1,51 +1,53 @@ import axios from 'axios'; import dayjs from 'dayjs'; -import { getMergeRequestData } from '../../services/gitlab.js'; -import { findGitlabInstance } from '../../utils/config-store.js'; -import * as display from '../../utils/display.js'; -import { parseMergeRequestUrl } from '../../utils/url-parser.js'; +import { GitlabService } from '../../services/gitlab.js'; +import { ConfigStore } from '../../utils/config-store.js'; +import { Display } from '../../utils/display.js'; +import { UrlParser } from '../../utils/url-parser.js'; import type { MosesConfig } from '../../types/MosesConfig.js'; -export async function fetchMrData(url: string, config: MosesConfig) { - let parsedUrl; - try { - parsedUrl = parseMergeRequestUrl(url); - } catch (error: unknown) { - display.error(error instanceof Error ? error.message : 'Invalid Merge Request URL.'); - return null; - } +export class ValidateGitlabDataProvider { + static async fetchMrData(url: string, config: MosesConfig) { + let parsedUrl; + try { + parsedUrl = UrlParser.parseMergeRequestUrl(url); + } catch (error: unknown) { + Display.error(error instanceof Error ? error.message : 'Invalid Merge Request URL.'); + return null; + } - const gitlabConfig = findGitlabInstance(config, parsedUrl.host); - if (!gitlabConfig) { - display.error(`No GitLab instance configured for host: ${parsedUrl.host}`); - return null; - } + const gitlabConfig = ConfigStore.findGitlabInstance(config, parsedUrl.host); + if (!gitlabConfig) { + Display.error(`No GitLab instance configured for host: ${parsedUrl.host}`); + return null; + } - const spinner = display.spinner('Fetching MR data...'); - try { - const data = await getMergeRequestData( - gitlabConfig.url, - gitlabConfig.token, - parsedUrl.projectId, - parsedUrl.mrIid, - ); - spinner.succeed(`MR #${data.mr.iid} — "${data.mr.title}" loaded`); + const spinner = Display.spinner('Fetching MR data...'); + try { + const data = await GitlabService.getMergeRequestData( + gitlabConfig.url, + gitlabConfig.token, + parsedUrl.projectId, + parsedUrl.mrIid, + ); + spinner.succeed(`MR #${data.mr.iid} — "${data.mr.title}" loaded`); - display.info(`šŸ‘¤ Author: ${data.mr.author?.name ?? data.mr.author?.username ?? 'unknown'}`); - display.info(`🌿 Branch: ${data.mr.source_branch} → ${data.mr.target_branch}`); - display.info(`šŸ“… Date: ${dayjs(data.mr.created_at).format('YYYY-MM-DD')}`); - display.info( - `šŸ“Š Stats: ${data.diffs.length} files | changes_count: ${data.mr.changes_count ?? '?'}`, - ); + Display.info(`šŸ‘¤ Author: ${data.mr.author?.name ?? data.mr.author?.username ?? 'unknown'}`); + Display.info(`🌿 Branch: ${data.mr.source_branch} → ${data.mr.target_branch}`); + Display.info(`šŸ“… Date: ${dayjs(data.mr.created_at).format('YYYY-MM-DD')}`); + Display.info( + `šŸ“Š Stats: ${data.diffs.length} files | changes_count: ${data.mr.changes_count ?? '?'}`, + ); - return data; - } catch (error: unknown) { - spinner.fail('Failed to fetch MR data.'); - if (axios.isAxiosError(error) && error.response?.status === 404) { - display.error('MR not found (404). Check URL and access (VPN, permissions).'); - } else { - display.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); + return data; + } catch (error: unknown) { + spinner.fail('Failed to fetch MR data.'); + if (axios.isAxiosError(error) && error.response?.status === 404) { + Display.error('MR not found (404). Check URL and access (VPN, permissions).'); + } else { + Display.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + return null; } - return null; } } diff --git a/src/commands/validate/index.ts b/src/commands/validate/index.ts index 8175c0c..c48f638 100644 --- a/src/commands/validate/index.ts +++ b/src/commands/validate/index.ts @@ -1,64 +1,61 @@ import { confirm } from '@inquirer/prompts'; -import * as display from '../../utils/display.js'; -import { loadValidatedConfig } from './load-config.js'; -import { fetchMrData } from './gitlab.js'; -import { isDiffWithinLimits } from './check-limits.js'; -import { runReviewTask } from './review.js'; -import { - isCurrentDirMatchingRepo, - getRepoUrlFromMrUrl, - cloneRepository, -} from '../../services/repository.js'; -import { parseMergeRequestUrl } from '../../utils/url-parser.js'; -import { findGitlabInstance } from '../../utils/config-store.js'; +import { Display } from '../../utils/display.js'; +import { ValidateConfigLoader } from './load-config.js'; +import { ValidateGitlabDataProvider } from './gitlab.js'; +import { DiffLimitChecker } from './check-limits.js'; +import { ValidateReviewRunner } from './review.js'; +import { RepositoryService } from '../../services/repository.js'; +import { UrlParser } from '../../utils/url-parser.js'; +import { ConfigStore } from '../../utils/config-store.js'; import type { ValidateOptions } from '../../types/ValidateOptions.js'; -export async function execute(url: string, options: ValidateOptions = {}): Promise { - display.banner(); - display.info(`šŸ”— Analyzing: ${url}`); - - const config = await loadValidatedConfig(); - if (!config) return; - - const data = await fetchMrData(url, config); - if (!data) return; - - if (!isDiffWithinLimits(data.diffs, config)) return; - - // Repository context logic - let repoPath: string | null = null; - const targetRepoUrl = getRepoUrlFromMrUrl(url); - - if (isCurrentDirMatchingRepo(targetRepoUrl)) { - display.success('āœ… Repository detected in current directory. Using local context.'); - repoPath = process.cwd(); - } else { - display.info('šŸ“‚ Current directory does not match the MR repository.'); - const shouldDownload = await confirm({ - message: 'Do you want to download the repository locally to provide more context to the AI?', - default: true, - }); - - if (shouldDownload) { - const parsedUrl = parseMergeRequestUrl(url); - const gitlabConfig = findGitlabInstance(config, parsedUrl.host); - - if (!gitlabConfig) { - display.error(`No GitLab instance configured for host: ${parsedUrl.host}`); - return; - } - - const spinner = display.spinner('Cloning repository...'); - try { - repoPath = await cloneRepository(targetRepoUrl, gitlabConfig.token); - spinner.succeed(`Repository cloned to: ${repoPath}`); - } catch (error) { - spinner.fail('Failed to clone repository.'); - display.error(error instanceof Error ? error.message : 'Unknown error during clone.'); - // Proceeding without repo context if clone fails +export class ValidateCommand { + static async execute(url: string, options: ValidateOptions = {}): Promise { + Display.banner(); + Display.info(`šŸ”— Analyzing: ${url}`); + + const config = await ValidateConfigLoader.loadValidatedConfig(); + if (!config) return; + + const data = await ValidateGitlabDataProvider.fetchMrData(url, config); + if (!data) return; + + if (!DiffLimitChecker.isDiffWithinLimits(data.diffs, config)) return; + + let repoPath: string | null = null; + const targetRepoUrl = RepositoryService.getRepoUrlFromMrUrl(url); + + if (RepositoryService.isCurrentDirMatchingRepo(targetRepoUrl)) { + Display.success('āœ… Repository detected in current directory. Using local context.'); + repoPath = process.cwd(); + } else { + Display.info('šŸ“‚ Current directory does not match the MR repository.'); + const shouldDownload = await confirm({ + message: + 'Do you want to download the repository locally to provide more context to the AI?', + default: true, + }); + + if (shouldDownload) { + const parsedUrl = UrlParser.parseMergeRequestUrl(url); + const gitlabConfig = ConfigStore.findGitlabInstance(config, parsedUrl.host); + + if (!gitlabConfig) { + Display.error(`No GitLab instance configured for host: ${parsedUrl.host}`); + return; + } + + const spinner = Display.spinner('Cloning repository...'); + try { + repoPath = await RepositoryService.cloneRepository(targetRepoUrl, gitlabConfig.token); + spinner.succeed(`Repository cloned to: ${repoPath}`); + } catch (error) { + spinner.fail('Failed to clone repository.'); + Display.error(error instanceof Error ? error.message : 'Unknown error during clone.'); + } } } - } - await runReviewTask(url, data, config, options, repoPath); + await ValidateReviewRunner.runReviewTask(url, data, config, options, repoPath); + } } diff --git a/src/commands/validate/load-config.ts b/src/commands/validate/load-config.ts index ee5735c..a3b4bca 100644 --- a/src/commands/validate/load-config.ts +++ b/src/commands/validate/load-config.ts @@ -1,18 +1,20 @@ import { MESSAGES } from '../../constants.js'; -import { checkAndFixConfigPermissions, readConfig } from '../../utils/config-store.js'; -import * as display from '../../utils/display.js'; +import { ConfigStore } from '../../utils/config-store.js'; +import { Display } from '../../utils/display.js'; import type { MosesConfig } from '../../types/MosesConfig.js'; -export async function loadValidatedConfig(): Promise { - try { - const config = await readConfig(); - const permissionStatus = await checkAndFixConfigPermissions(); - if (permissionStatus.fixed) { - display.warn('Config permissions were incorrect and have been fixed to 600.'); +export class ValidateConfigLoader { + static async loadValidatedConfig(): Promise { + try { + const config = await ConfigStore.readConfig(); + const permissionStatus = await ConfigStore.checkAndFixConfigPermissions(); + if (permissionStatus.fixed) { + Display.warn('Config permissions were incorrect and have been fixed to 600.'); + } + return config; + } catch { + Display.error(MESSAGES.noConfig); + return null; } - return config; - } catch { - display.error(MESSAGES.noConfig); - return null; } } diff --git a/src/commands/validate/review.ts b/src/commands/validate/review.ts index b0d675c..155e5cc 100644 --- a/src/commands/validate/review.ts +++ b/src/commands/validate/review.ts @@ -1,85 +1,87 @@ import { DEFAULT_CONTEXT_DIR } from '../../constants.js'; -import { runAiReview } from '../../services/ai-tools.js'; -import { ensureDefaultContextFiles, readContextPrompt } from '../../services/context.js'; -import { scanRepoForContext } from '../../services/context-scanner.js'; -import { buildMergeRequestMarkdown } from '../../services/markdown.js'; -import * as display from '../../utils/display.js'; +import { AiReviewService } from '../../services/ai-tools.js'; +import { ContextService } from '../../services/context.js'; +import { ContextScannerService } from '../../services/context-scanner.js'; +import { MarkdownService } from '../../services/markdown.js'; +import { Display } from '../../utils/display.js'; import type { MosesConfig } from '../../types/MosesConfig.js'; import type { ValidateOptions } from '../../types/ValidateOptions.js'; import type { MergeRequestBundle } from '../../types/MergeRequestBundle.js'; -export async function runReviewTask( - url: string, - data: MergeRequestBundle, - config: MosesConfig, - options: ValidateOptions, - repoPath: string | null = null, -): Promise { - const markdownSpinner = display.spinner('Preparing context and diff...'); - try { - await ensureDefaultContextFiles(); - let contextPrompt = await readContextPrompt(options.prompt ?? ''); +export class ValidateReviewRunner { + static async runReviewTask( + url: string, + data: MergeRequestBundle, + config: MosesConfig, + options: ValidateOptions, + repoPath: string | null = null, + ): Promise { + const markdownSpinner = Display.spinner('Preparing context and diff...'); + try { + await ContextService.ensureDefaultContextFiles(); + let contextPrompt = await ContextService.readContextPrompt(options.prompt ?? ''); - if (repoPath) { - const repoContext = await scanRepoForContext(repoPath); - if (repoContext) { - contextPrompt = `${contextPrompt}\n${repoContext}`; + if (repoPath) { + const repoContext = await ContextScannerService.scanRepoForContext(repoPath); + if (repoContext) { + contextPrompt = `${contextPrompt}\n${repoContext}`; + } } - } - const markdown = buildMergeRequestMarkdown({ - mr: data.mr, - diffs: data.diffs, - commits: data.commits, - url, - }); + const markdown = MarkdownService.buildMergeRequestMarkdown({ + mr: data.mr, + diffs: data.diffs, + commits: data.commits, + url, + }); - markdownSpinner.succeed(`Context and diff prepared (folder: ${DEFAULT_CONTEXT_DIR})`); + markdownSpinner.succeed(`Context and diff prepared (folder: ${DEFAULT_CONTEXT_DIR})`); - const reviewSpinner = display.spinner('Connecting to AI tool...'); - display.info('\nšŸ¤– Starting review with AI tool...'); - display.info('────────────────────────────────────────────────────────'); + const reviewSpinner = Display.spinner('Connecting to AI tool...'); + Display.info('\nšŸ¤– Starting review with AI tool...'); + Display.info('────────────────────────────────────────────────────────'); - await new Promise((resolve, reject) => { - let firstChunk = true; - const stopSpinnerOnStart = () => { - if (firstChunk) { - reviewSpinner.stop(); - firstChunk = false; - } - }; + await new Promise((resolve, reject) => { + let firstChunk = true; + const stopSpinnerOnStart = () => { + if (firstChunk) { + reviewSpinner.stop(); + firstChunk = false; + } + }; - runAiReview(config.ai?.tool ?? 'copilot', markdown, { - options: { - feedbackStyle: config.ai?.feedbackStyle, - contextPrompt, - }, - onStdout: (chunk: string) => { - stopSpinnerOnStart(); - display.stream(chunk); - }, - onStderr: (chunk: string) => { - stopSpinnerOnStart(); - display.stream(chunk); - }, - onClose: (code: number | null) => { - if (code === 0) { - display.info('\n────────────────────────────────────────────────────────'); - display.success('Analysis completed'); - resolve(); - } else { + AiReviewService.runAiReview(config.ai?.tool ?? 'copilot', markdown, { + options: { + feedbackStyle: config.ai?.feedbackStyle, + contextPrompt, + }, + onStdout: (chunk: string) => { + stopSpinnerOnStart(); + Display.stream(chunk); + }, + onStderr: (chunk: string) => { + stopSpinnerOnStart(); + Display.stream(chunk); + }, + onClose: (code: number | null) => { + if (code === 0) { + Display.info('\n────────────────────────────────────────────────────────'); + Display.success('Analysis completed'); + resolve(); + } else { + reviewSpinner.fail('AI analysis failed'); + reject(new Error(`AI process exited with code ${String(code)}`)); + } + }, + onError: (error: Error) => { reviewSpinner.fail('AI analysis failed'); - reject(new Error(`AI process exited with code ${String(code)}`)); - } - }, - onError: (error: Error) => { - reviewSpinner.fail('AI analysis failed'); - reject(error); - }, + reject(error); + }, + }); }); - }); - } catch (error: unknown) { - markdownSpinner.fail('Failed to generate markdown or run AI review.'); - display.error(error instanceof Error ? error.message : 'Unknown error during AI review.'); + } catch (error: unknown) { + markdownSpinner.fail('Failed to generate markdown or run AI review.'); + Display.error(error instanceof Error ? error.message : 'Unknown error during AI review.'); + } } } diff --git a/src/services/ai-tools.ts b/src/services/ai-tools.ts index 8674a18..5fdde80 100644 --- a/src/services/ai-tools.ts +++ b/src/services/ai-tools.ts @@ -10,40 +10,42 @@ const FEEDBACK_STYLE_GUIDANCE: Record = { 'Be a extremely harsh, blunt and ruthless code reviewer. Use a cynical and critical tone. If the code is bad, say it is "garbage", "trash" or "disposable". Do not sugarcoat anything. You are NOT allowed to insult the person/author, but you MUST be savage when reviewing the code quality.', }; -function buildPrompt( - markdownContent: string, - options: RunAiReviewHandlers['options'] = {}, -): string { - const feedbackStyle = options.feedbackStyle ?? 'pragmatic'; - const toneInstruction = - FEEDBACK_STYLE_GUIDANCE[feedbackStyle] ?? FEEDBACK_STYLE_GUIDANCE.pragmatic; - const context = options.contextPrompt?.trim(); - return `${context ? `${context}\n\n` : ''}${toneInstruction} +export class AiReviewService { + private static buildPrompt( + markdownContent: string, + options: RunAiReviewHandlers['options'] = {}, + ): string { + const feedbackStyle = options.feedbackStyle ?? 'pragmatic'; + const toneInstruction = + FEEDBACK_STYLE_GUIDANCE[feedbackStyle] ?? FEEDBACK_STYLE_GUIDANCE.pragmatic; + const context = options.contextPrompt?.trim(); + return `${context ? `${context}\n\n` : ''}${toneInstruction} Merge Request Diff: ${markdownContent}`; -} - -export function runAiReview( - toolKey: string, - markdownContent: string, - handlers: RunAiReviewHandlers = {}, -): void { - const tool = AI_TOOLS.find((item) => item.key === toolKey); - if (!tool) { - throw new Error(`Invalid AI tool: ${toolKey}`); } - const prompt = buildPrompt(markdownContent, handlers.options); - const args = [...tool.args, prompt]; - - const child = spawn(tool.command, args, { - stdio: ['ignore', 'pipe', 'pipe'], - }); - - child.stdout.on('data', (chunk: Buffer) => handlers.onStdout?.(chunk.toString())); - child.stderr.on('data', (chunk: Buffer) => handlers.onStderr?.(chunk.toString())); - child.on('close', (code: number | null) => handlers.onClose?.(code)); - child.on('error', (error: Error) => handlers.onError?.(error)); + static runAiReview( + toolKey: string, + markdownContent: string, + handlers: RunAiReviewHandlers = {}, + ): void { + const tool = AI_TOOLS.find((item) => item.key === toolKey); + if (!tool) { + throw new Error(`Invalid AI tool: ${toolKey}`); + } + + const prompt = AiReviewService.buildPrompt(markdownContent, handlers.options); + const args = [...tool.args, prompt]; + + const child = spawn(tool.command, args, { + stdio: ['ignore', 'pipe', 'pipe'], + }); + + child.stdout.on('data', (chunk: Buffer) => handlers.onStdout?.(chunk.toString())); + child.stderr.on('data', (chunk: Buffer) => handlers.onStderr?.(chunk.toString())); + child.on('close', (code: number | null) => handlers.onClose?.(code)); + child.on('error', (error: Error) => handlers.onError?.(error)); + } } diff --git a/src/services/context-scanner.ts b/src/services/context-scanner.ts index a8b08fd..3dac34a 100644 --- a/src/services/context-scanner.ts +++ b/src/services/context-scanner.ts @@ -6,24 +6,26 @@ import { CONTEXT_FILE_PATTERNS } from '../constants.js'; * Scans a repository directory for files matching the context patterns. * Returns the contents of these files formatted for LLM context. */ -export async function scanRepoForContext(repoPath: string): Promise { - const contents: string[] = []; +export class ContextScannerService { + static async scanRepoForContext(repoPath: string): Promise { + const contents: string[] = []; - for (const pattern of CONTEXT_FILE_PATTERNS) { - const fullPath = path.join(repoPath, pattern); + for (const pattern of CONTEXT_FILE_PATTERNS) { + const fullPath = path.join(repoPath, pattern); - try { - const stats = await fs.stat(fullPath); - if (stats.isFile()) { - const fileContent = await fs.readFile(fullPath, 'utf-8'); - contents.push(`\n## File: ${pattern}\n\n${fileContent.trim()}`); + try { + const stats = await fs.stat(fullPath); + if (stats.isFile()) { + const fileContent = await fs.readFile(fullPath, 'utf-8'); + contents.push(`\n## File: ${pattern}\n\n${fileContent.trim()}`); + } + } catch { + // File doesn't exist or is not accessible, skip } - } catch { - // File doesn't exist or is not accessible, skip } - } - if (contents.length === 0) return ''; + if (contents.length === 0) return ''; - return `\n# Internal Repository Context\n${contents.join('\n')}\n`; + return `\n# Internal Repository Context\n${contents.join('\n')}\n`; + } } diff --git a/src/services/context.ts b/src/services/context.ts index 0016025..8bb0d09 100644 --- a/src/services/context.ts +++ b/src/services/context.ts @@ -7,45 +7,49 @@ import { DEFAULT_CONTEXT_DIR } from '../constants.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PROMPTS_DIR = path.resolve(__dirname, '../prompts'); -const resolveHome = (value: string): string => value.replace(/^~(?=\/|$)/, os.homedir()); +export class ContextService { + private static resolveHome(value: string): string { + return value.replace(/^~(?=\/|$)/, os.homedir()); + } -export function getContextDir(): string { - return resolveHome(DEFAULT_CONTEXT_DIR); -} + static getContextDir(): string { + return ContextService.resolveHome(DEFAULT_CONTEXT_DIR); + } -export async function ensureDefaultContextFiles(): Promise<{ - contextDir: string; - files: string[]; -}> { - const contextDir = getContextDir(); - await fs.mkdir(contextDir, { recursive: true }); - - const files = await fs.readdir(PROMPTS_DIR); - for (const file of files) { - const srcPath = path.join(PROMPTS_DIR, file); - const destPath = path.join(contextDir, file); - - try { - await fs.access(destPath); - } catch { - await fs.copyFile(srcPath, destPath); + static async ensureDefaultContextFiles(): Promise<{ + contextDir: string; + files: string[]; + }> { + const contextDir = ContextService.getContextDir(); + await fs.mkdir(contextDir, { recursive: true }); + + const files = await fs.readdir(PROMPTS_DIR); + for (const file of files) { + const srcPath = path.join(PROMPTS_DIR, file); + const destPath = path.join(contextDir, file); + + try { + await fs.access(destPath); + } catch { + await fs.copyFile(srcPath, destPath); + } } + return { contextDir, files }; } - return { contextDir, files }; -} -export async function readContextPrompt(extraPrompt = ''): Promise { - const contextDir = getContextDir(); - const files = await fs.readdir(contextDir); - const mdFiles = files.filter((file) => file.endsWith('.md')).sort(); + static async readContextPrompt(extraPrompt = ''): Promise { + const contextDir = ContextService.getContextDir(); + const files = await fs.readdir(contextDir); + const mdFiles = files.filter((file) => file.endsWith('.md')).sort(); - const contents = await Promise.all( - mdFiles.map((file) => fs.readFile(path.join(contextDir, file), 'utf-8')), - ); + const contents = await Promise.all( + mdFiles.map((file) => fs.readFile(path.join(contextDir, file), 'utf-8')), + ); - const segments = contents.map((c) => c.trim()); - if (extraPrompt.trim()) { - segments.push(`# Additional user context\n${extraPrompt.trim()}`); + const segments = contents.map((c) => c.trim()); + if (extraPrompt.trim()) { + segments.push(`# Additional user context\n${extraPrompt.trim()}`); + } + return `${segments.join('\n\n')}\n`; } - return `${segments.join('\n\n')}\n`; } diff --git a/src/services/gitlab.ts b/src/services/gitlab.ts index cd46362..193cb99 100644 --- a/src/services/gitlab.ts +++ b/src/services/gitlab.ts @@ -5,65 +5,69 @@ interface GitlabUser { username: string; } -function createClient(baseURL: string, token: string): AxiosInstance { - return axios.create({ - baseURL, - headers: { - 'PRIVATE-TOKEN': token, - }, - timeout: 15000, - }); -} +export class GitlabService { + private static createClient(baseURL: string, token: string): AxiosInstance { + return axios.create({ + baseURL, + headers: { + 'PRIVATE-TOKEN': token, + }, + timeout: 15000, + }); + } -async function withRetry(fn: () => Promise, retries = 2): Promise { - let lastError: Error | null = null; - for (let i = 0; i <= retries; i += 1) { - try { - return await fn(); - } catch (error: unknown) { - lastError = error instanceof Error ? error : new Error('Unknown request error'); - if (i === retries) break; + private static async withRetry(fn: () => Promise, retries = 2): Promise { + let lastError: Error | null = null; + for (let i = 0; i <= retries; i += 1) { + try { + return await fn(); + } catch (error: unknown) { + lastError = error instanceof Error ? error : new Error('Unknown request error'); + if (i === retries) break; + } } - } - if (lastError) { - throw lastError; - } + if (lastError) { + throw lastError; + } - throw new Error('Unexpected retry failure'); -} + throw new Error('Unexpected retry failure'); + } -export async function validateToken(baseURL: string, token: string): Promise { - const client = createClient(baseURL, token); - const response = await withRetry(() => client.get('/api/v4/user')); - return response.data; -} + static async validateToken(baseURL: string, token: string): Promise { + const client = GitlabService.createClient(baseURL, token); + const response = await GitlabService.withRetry(() => client.get('/api/v4/user')); + return response.data; + } -export async function getMergeRequestData( - baseURL: string, - token: string, - projectId: string, - mrIid: string, -): Promise { - const client = createClient(baseURL, token); - const [mr, diffs, commits] = await Promise.all([ - withRetry(() => - client.get(`/api/v4/projects/${projectId}/merge_requests/${mrIid}`), - ), - withRetry(() => - client.get( - `/api/v4/projects/${projectId}/merge_requests/${mrIid}/diffs`, + static async getMergeRequestData( + baseURL: string, + token: string, + projectId: string, + mrIid: string, + ): Promise { + const client = GitlabService.createClient(baseURL, token); + const [mr, diffs, commits] = await Promise.all([ + GitlabService.withRetry(() => + client.get( + `/api/v4/projects/${projectId}/merge_requests/${mrIid}`, + ), ), - ), - withRetry(() => - client.get( - `/api/v4/projects/${projectId}/merge_requests/${mrIid}/commits`, + GitlabService.withRetry(() => + client.get( + `/api/v4/projects/${projectId}/merge_requests/${mrIid}/diffs`, + ), ), - ), - ]); - return { - mr: mr.data, - diffs: diffs.data, - commits: commits.data, - }; + GitlabService.withRetry(() => + client.get( + `/api/v4/projects/${projectId}/merge_requests/${mrIid}/commits`, + ), + ), + ]); + return { + mr: mr.data, + diffs: diffs.data, + commits: commits.data, + }; + } } diff --git a/src/services/markdown.ts b/src/services/markdown.ts index ee4bd59..3f4cc52 100644 --- a/src/services/markdown.ts +++ b/src/services/markdown.ts @@ -9,26 +9,29 @@ interface BuildMergeRequestMarkdownInput { url: string; } -export function buildMergeRequestMarkdown({ - mr, - diffs, - commits, - url, -}: BuildMergeRequestMarkdownInput): string { - const createdAt = dayjs(mr.created_at).format('YYYY-MM-DD'); - const changedFiles = Array.isArray(diffs) ? diffs.length : 0; - const additions = mr.changes_count ?? '?'; +export class MarkdownService { + static buildMergeRequestMarkdown({ + mr, + diffs, + commits, + url, + }: BuildMergeRequestMarkdownInput): string { + const createdAt = dayjs(mr.created_at).format('YYYY-MM-DD'); + const changedFiles = Array.isArray(diffs) ? diffs.length : 0; + const additions = mr.changes_count ?? '?'; - const commitLines = commits.map((commit) => `- ${commit.short_id} — ${commit.title}`).join('\n'); + const commitLines = commits + .map((commit) => `- ${commit.short_id} — ${commit.title}`) + .join('\n'); - const diffSections = diffs - .map((item) => { - const diff = item.diff ?? ''; - return `### \`${item.new_path ?? item.old_path}\`\n\n\`\`\`diff\n${diff}\n\`\`\`\n`; - }) - .join('\n'); + const diffSections = diffs + .map((item) => { + const diff = item.diff ?? ''; + return `### \`${item.new_path ?? item.old_path}\`\n\n\`\`\`diff\n${diff}\n\`\`\`\n`; + }) + .join('\n'); - return `# MR #${mr.iid} — ${mr.title} + return `# MR #${mr.iid} — ${mr.title} **Author:** @${mr.author?.username ?? 'unknown'} **Branch:** ${mr.source_branch} → ${mr.target_branch} @@ -52,18 +55,19 @@ ${commitLines || '_No commits_'} ${diffSections || '_No diffs_'} `; -} + } -export function countDiffChanges(diffs: MergeRequestDiff[] = []): number { - if (!Array.isArray(diffs)) return 0; - return diffs.reduce((total, item) => { - const diff = item.diff ?? ''; - const lines = diff.split('\n'); - const fileChanges = lines.filter((line) => { - if (!(line.startsWith('+') || line.startsWith('-'))) return false; - if (line.startsWith('+++') || line.startsWith('---')) return false; - return true; - }).length; - return total + fileChanges; - }, 0); + static countDiffChanges(diffs: MergeRequestDiff[] = []): number { + if (!Array.isArray(diffs)) return 0; + return diffs.reduce((total, item) => { + const diff = item.diff ?? ''; + const lines = diff.split('\n'); + const fileChanges = lines.filter((line) => { + if (!(line.startsWith('+') || line.startsWith('-'))) return false; + if (line.startsWith('+++') || line.startsWith('---')) return false; + return true; + }).length; + return total + fileChanges; + }, 0); + } } diff --git a/src/services/repository.ts b/src/services/repository.ts index 7a6ac75..0b121d0 100644 --- a/src/services/repository.ts +++ b/src/services/repository.ts @@ -4,82 +4,66 @@ import fs from 'node:fs/promises'; import os from 'node:os'; import { DEFAULT_REPOS_DIR } from '../constants.js'; -/** - * Checks if the current working directory is a git repository - * and if it matches the target remote URL. - */ -export function isCurrentDirMatchingRepo(targetRemoteUrl: string): boolean { - try { - // Check if .git exists - const remotes = execSync('git remote -v', { - stdio: ['ignore', 'pipe', 'ignore'], - encoding: 'utf-8', - }); +export class RepositoryService { + private static normalize(url: string): string { + return url + .replace(/\.git$/, '') + .replace(/\/$/, '') + .toLowerCase(); + } - // Normalize target URL (remove .git suffix if present, remove trailing slash) - const normalize = (url: string) => - url - .replace(/\.git$/, '') - .replace(/\/$/, '') - .toLowerCase(); - const normalizedTarget = normalize(targetRemoteUrl); + static isCurrentDirMatchingRepo(targetRemoteUrl: string): boolean { + try { + const remotes = execSync('git remote -v', { + stdio: ['ignore', 'pipe', 'ignore'], + encoding: 'utf-8', + }); + const normalizedTarget = RepositoryService.normalize(targetRemoteUrl); - return remotes.split('\n').some((line) => { - const match = line.match(/\t(\S+)\s+\((?:fetch|push)\)/); - if (match) { - return normalize(match[1]) === normalizedTarget; - } + return remotes.split('\n').some((line) => { + const match = line.match(/\t(\S+)\s+\((?:fetch|push)\)/); + if (match) { + return RepositoryService.normalize(match[1]) === normalizedTarget; + } + return false; + }); + } catch { return false; - }); - } catch { - return false; + } } -} -/** - * Clones a repository to the local storage if not already present. - * Returns the path to the cloned repository. - */ -export async function cloneRepository(repoUrl: string, token: string): Promise { - const reposDir = DEFAULT_REPOS_DIR.replace(/^~(?=\/|$)/, os.homedir()); - await fs.mkdir(reposDir, { recursive: true }); + static async cloneRepository(repoUrl: string, token: string): Promise { + const reposDir = DEFAULT_REPOS_DIR.replace(/^~(?=\/|$)/, os.homedir()); + await fs.mkdir(reposDir, { recursive: true }); - // Create a directory name based on the repo URL (e.g. gitlab.com-group-repo) - const url = new URL(repoUrl); - const dirName = `${url.hostname}${url.pathname.replace(/\//g, '-')}`.replace(/\.git$/, ''); - const targetPath = path.join(reposDir, dirName); + const url = new URL(repoUrl); + const dirName = `${url.hostname}${url.pathname.replace(/\//g, '-')}`.replace(/\.git$/, ''); + const targetPath = path.join(reposDir, dirName); - // If directory exists, check if it's a git repo - try { - await fs.access(path.join(targetPath, '.git')); - // Potentially git pull here to keep it updated? - // For now, just return the path to avoid re-cloning. - return targetPath; - } catch { - // Not a repo or doesn't exist, proceed to clone - } + try { + await fs.access(path.join(targetPath, '.git')); + return targetPath; + } catch { + // Continue to clone + } - // Inject token for authentication if needed (GitLab format: https://oauth2:TOKEN@host/group/repo.git) - const authUrl = repoUrl.replace(/^https:\/\//, `https://oauth2:${token}@`); + const authUrl = repoUrl.replace(/^https:\/\//, `https://oauth2:${token}@`); - try { - execSync(`git clone --depth 1 ${authUrl} ${targetPath}`, { stdio: 'inherit' }); - return targetPath; - } catch (error) { - throw new Error( - `Failed to clone repository: ${error instanceof Error ? error.message : String(error)}`, - ); + try { + execSync(`git clone --depth 1 ${authUrl} ${targetPath}`, { stdio: 'inherit' }); + return targetPath; + } catch (error) { + throw new Error( + `Failed to clone repository: ${error instanceof Error ? error.message : String(error)}`, + ); + } } -} -/** - * Extracts the repository URL from a Merge Request URL. - * Example: https://gitlab.com/group/repo/-/merge_requests/1 -> https://gitlab.com/group/repo.git - */ -export function getRepoUrlFromMrUrl(mrUrl: string): string { - const url = new URL(mrUrl); - const parts = url.pathname.split('/-/'); - if (parts.length < 1) throw new Error('Invalid Merge Request URL'); + static getRepoUrlFromMrUrl(mrUrl: string): string { + const url = new URL(mrUrl); + const parts = url.pathname.split('/-/'); + if (parts.length < 1) throw new Error('Invalid Merge Request URL'); - return `${url.origin}${parts[0]}.git`; + return `${url.origin}${parts[0]}.git`; + } } diff --git a/src/utils/config-store.ts b/src/utils/config-store.ts index 012925d..dd65097 100644 --- a/src/utils/config-store.ts +++ b/src/utils/config-store.ts @@ -6,54 +6,66 @@ import type { ConfigPermissionStatus } from '../types/ConfigPermissionStatus.js' import type { GitlabInstance } from '../types/GitlabInstance.js'; import type { MosesConfig } from '../types/MosesConfig.js'; -const resolveHome = (value: string): string => value.replace(/^~(?=\/|$)/, os.homedir()); +export class ConfigStore { + private static resolveHome(value: string): string { + return value.replace(/^~(?=\/|$)/, os.homedir()); + } -export const getConfigDir = (): string => resolveHome(DEFAULT_CONFIG_DIR); -export const getOutputDir = (): string => resolveHome(DEFAULT_OUTPUT_DIR); -export const getConfigPath = (): string => path.join(getConfigDir(), 'config.json'); + static getConfigDir(): string { + return ConfigStore.resolveHome(DEFAULT_CONFIG_DIR); + } -export async function ensureDirectories(): Promise { - await fs.mkdir(getConfigDir(), { recursive: true }); - await fs.mkdir(getOutputDir(), { recursive: true }); -} + static getOutputDir(): string { + return ConfigStore.resolveHome(DEFAULT_OUTPUT_DIR); + } -export async function readConfig(): Promise { - const configPath = getConfigPath(); - const content = await fs.readFile(configPath, 'utf-8'); - return JSON.parse(content) as MosesConfig; -} + static getConfigPath(): string { + return path.join(ConfigStore.getConfigDir(), 'config.json'); + } -export async function saveConfig(config: MosesConfig): Promise { - await ensureDirectories(); - const configPath = getConfigPath(); - await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8'); - await fs.chmod(configPath, 0o600); - return configPath; -} + static async ensureDirectories(): Promise { + await fs.mkdir(ConfigStore.getConfigDir(), { recursive: true }); + await fs.mkdir(ConfigStore.getOutputDir(), { recursive: true }); + } + + static async readConfig(): Promise { + const configPath = ConfigStore.getConfigPath(); + const content = await fs.readFile(configPath, 'utf-8'); + return JSON.parse(content) as MosesConfig; + } -export async function checkAndFixConfigPermissions(): Promise { - const configPath = getConfigPath(); - const stats = await fs.stat(configPath); - const mode = stats.mode & 0o777; - if (mode !== 0o600) { + static async saveConfig(config: MosesConfig): Promise { + await ConfigStore.ensureDirectories(); + const configPath = ConfigStore.getConfigPath(); + await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8'); await fs.chmod(configPath, 0o600); - return { fixed: true, previousMode: mode }; + return configPath; } - return { fixed: false, previousMode: mode }; -} -export function findGitlabInstance(config: MosesConfig, host: string): GitlabInstance | null { - const gitlabs = config.gitlabs ?? []; - return ( - gitlabs.find((item) => { - try { - const urlHost = new URL(item.url).host; - return urlHost === host; - } catch { - return false; - } - }) ?? - gitlabs.find((item) => item.default) ?? - null - ); + static async checkAndFixConfigPermissions(): Promise { + const configPath = ConfigStore.getConfigPath(); + const stats = await fs.stat(configPath); + const mode = stats.mode & 0o777; + if (mode !== 0o600) { + await fs.chmod(configPath, 0o600); + return { fixed: true, previousMode: mode }; + } + return { fixed: false, previousMode: mode }; + } + + static findGitlabInstance(config: MosesConfig, host: string): GitlabInstance | null { + const gitlabs = config.gitlabs ?? []; + return ( + gitlabs.find((item) => { + try { + const urlHost = new URL(item.url).host; + return urlHost === host; + } catch { + return false; + } + }) ?? + gitlabs.find((item) => item.default) ?? + null + ); + } } diff --git a/src/utils/display.ts b/src/utils/display.ts index 5bf7dfd..1d26f28 100644 --- a/src/utils/display.ts +++ b/src/utils/display.ts @@ -5,55 +5,57 @@ import ora, { type Ora } from 'ora'; import { APP_NAME } from '../constants.js'; import packageJson from '../../package.json' with { type: 'json' }; -export function banner(): void { - const title = figlet.textSync(APP_NAME, { font: 'Slant', horizontalLayout: 'default' }); - const subtitle = ' Thou shalt not break production '; - const version = chalk.dim(`v${packageJson?.version || '1.0.0'}`); - - console.log( - boxen(`${chalk.bold.cyan(title)}\n${chalk.italic.gray(subtitle)}\n${version}`, { - padding: 1, - margin: 1, - borderStyle: 'double', - borderColor: 'cyan', - dimBorder: true, - textAlignment: 'center', - title: chalk.yellow(' šŸ¤– MR Review Buddy '), - titleAlignment: 'right', - }), - ); -} - -export function section(title: string): void { - console.log(chalk.bold('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); - console.log(chalk.bold(title)); - console.log(chalk.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')); -} - -export function spinner(text: string): Ora { - return ora({ text, color: 'cyan' }).start(); -} - -export function success(text: string): void { - console.log(chalk.green(`āœ… ${text}`)); -} - -export function error(text: string): void { - console.error(chalk.red(`āŒ ${text}`)); -} - -export function info(text: string): void { - console.log(chalk.blue(text)); -} - -export function warn(text: string): void { - console.log(chalk.yellow(`āš ļø ${text}`)); -} - -export function link(text: string): void { - console.log(chalk.cyan.underline(text)); -} - -export function stream(chunk: string): void { - process.stdout.write(chunk); +export class Display { + static banner(): void { + const title = figlet.textSync(APP_NAME, { font: 'Slant', horizontalLayout: 'default' }); + const subtitle = ' Thou shalt not break production '; + const version = chalk.dim(`v${packageJson?.version || '1.0.0'}`); + + console.log( + boxen(`${chalk.bold.cyan(title)}\n${chalk.italic.gray(subtitle)}\n${version}`, { + padding: 1, + margin: 1, + borderStyle: 'double', + borderColor: 'cyan', + dimBorder: true, + textAlignment: 'center', + title: chalk.yellow(' šŸ¤– MR Review Buddy '), + titleAlignment: 'right', + }), + ); + } + + static section(title: string): void { + console.log(chalk.bold('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(chalk.bold(title)); + console.log(chalk.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')); + } + + static spinner(text: string): Ora { + return ora({ text, color: 'cyan' }).start(); + } + + static success(text: string): void { + console.log(chalk.green(`āœ… ${text}`)); + } + + static error(text: string): void { + console.error(chalk.red(`āŒ ${text}`)); + } + + static info(text: string): void { + console.log(chalk.blue(text)); + } + + static warn(text: string): void { + console.log(chalk.yellow(`āš ļø ${text}`)); + } + + static link(text: string): void { + console.log(chalk.cyan.underline(text)); + } + + static stream(chunk: string): void { + process.stdout.write(chunk); + } } diff --git a/src/utils/tool-validator.ts b/src/utils/tool-validator.ts index 7be5544..63168c2 100644 --- a/src/utils/tool-validator.ts +++ b/src/utils/tool-validator.ts @@ -3,30 +3,32 @@ import { AI_TOOLS } from '../constants.js'; import type { AiToolKey } from '../types/AiToolKey.js'; import type { ToolValidationResult } from '../types/ToolValidationResult.js'; -export function getInstallUrl(toolKey: AiToolKey): string { - const tool = AI_TOOLS.find((item) => item.key === toolKey); - return tool?.docs ?? ''; -} - -export function validateToolInstallation(toolKey: AiToolKey): ToolValidationResult { - const tool = AI_TOOLS.find((item) => item.key === toolKey); - if (!tool) { - return { installed: false, installUrl: '' }; +export class ToolValidator { + static getInstallUrl(toolKey: AiToolKey): string { + const tool = AI_TOOLS.find((item) => item.key === toolKey); + return tool?.docs ?? ''; } - try { - const commandPath = execSync(`which ${tool.command}`, { stdio: 'pipe' }).toString().trim(); + static validateToolInstallation(toolKey: AiToolKey): ToolValidationResult { + const tool = AI_TOOLS.find((item) => item.key === toolKey); + if (!tool) { + return { installed: false, installUrl: '' }; + } + try { - execSync(`${tool.command} --version`, { stdio: 'pipe' }); + const commandPath = execSync(`which ${tool.command}`, { stdio: 'pipe' }).toString().trim(); + try { + execSync(`${tool.command} --version`, { stdio: 'pipe' }); + } catch { + execSync(`${tool.command} --help`, { stdio: 'pipe' }); + } + return { installed: true, path: commandPath }; } catch { - execSync(`${tool.command} --help`, { stdio: 'pipe' }); + return { + installed: false, + installCmd: tool.install, + installUrl: tool.docs, + }; } - return { installed: true, path: commandPath }; - } catch { - return { - installed: false, - installCmd: tool.install, - installUrl: tool.docs, - }; } } diff --git a/src/utils/url-parser.ts b/src/utils/url-parser.ts index 1440f4e..a8159dd 100644 --- a/src/utils/url-parser.ts +++ b/src/utils/url-parser.ts @@ -3,30 +3,32 @@ import type { ParsedMergeRequestUrl } from '../types/ParsedMergeRequestUrl.js'; const URL_REGEX = /^https:\/\/(?[^/]+)\/(?.+?)\/-\/merge_requests\/(?\d+)(?:\/.*)?$/; -export function parseMergeRequestUrl(url: string): ParsedMergeRequestUrl { - const match = url.match(URL_REGEX); - if (!match?.groups) { - throw new Error( - 'Invalid URL. Expected format: https://///-/merge_requests/', - ); +export class UrlParser { + static parseMergeRequestUrl(url: string): ParsedMergeRequestUrl { + const match = url.match(URL_REGEX); + if (!match?.groups) { + throw new Error( + 'Invalid URL. Expected format: https://///-/merge_requests/', + ); + } + const { host, projectPath, mrIid } = match.groups; + if (!host || !projectPath || !mrIid) { + throw new Error('Invalid URL. Missing host, project path, or merge request ID.'); + } + return { + host, + projectPath, + mrIid, + projectId: encodeURIComponent(projectPath), + }; } - const { host, projectPath, mrIid } = match.groups; - if (!host || !projectPath || !mrIid) { - throw new Error('Invalid URL. Missing host, project path, or merge request ID.'); - } - return { - host, - projectPath, - mrIid, - projectId: encodeURIComponent(projectPath), - }; -} -export function validateGitlabUrl(url: string): boolean { - try { - const parsed = new URL(url); - return parsed.protocol === 'https:' && Boolean(parsed.hostname); - } catch { - return false; + static validateGitlabUrl(url: string): boolean { + try { + const parsed = new URL(url); + return parsed.protocol === 'https:' && Boolean(parsed.hostname); + } catch { + return false; + } } }