diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 0000000..1df49e4 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,6 @@ +{ + "extension": ["ts"], + "spec": "test/**/*.test.ts", + "require": "ts-node/register", + "project": "tsconfig.test.json" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7103b94..2ac84b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "devchat", - "version": "0.0.45", + "version": "0.0.47", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "devchat", - "version": "0.0.45", + "version": "0.0.47", "dependencies": { "@emotion/react": "^11.10.8", "@mantine/core": "^6.0.10", @@ -37,19 +37,24 @@ "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.5", + "@types/chai": "^4.3.5", "@types/glob": "^8.1.0", - "@types/jest": "^29.5.1", + "@types/mocha": "^10.0.1", + "@types/mock-fs": "^4.13.1", "@types/ncp": "^2.0.5", "@types/node": "16.x", + "@types/proxyquire": "^1.3.28", "@types/react-dom": "^18.2.3", "@types/react-syntax-highlighter": "^15.5.6", "@types/shell-escape": "^0.2.1", + "@types/sinon": "^10.0.15", "@types/uuid": "^9.0.1", "@types/vscode": "^1.77.0", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", "@vscode/test-electron": "^2.3.0", "babel-loader": "^9.1.2", + "chai": "^4.3.7", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.7.3", "dotenv": "^16.0.3", @@ -59,15 +64,21 @@ "html-webpack-plugin": "^5.5.1", "jest": "^29.5.0", "json-loader": "^0.5.7", + "mocha": "^10.2.0", + "mock-fs": "^5.2.0", + "proxyquire": "^2.1.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.0.5", "redux": "^4.2.1", + "sinon": "^15.1.0", "style-loader": "^3.3.2", "ts-jest": "^29.1.0", "ts-loader": "^9.4.2", + "ts-node": "^10.9.1", "typescript": "^4.9.5", "url-loader": "^4.1.1", + "vscode-test": "^1.6.1", "webpack": "^5.76.3", "webpack-cli": "^5.0.1", "webpack-dev-server": "^4.13.3" @@ -1964,6 +1975,28 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -3112,6 +3145,35 @@ "dev": true }, "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.2.0.tgz", + "integrity": "sha512-OPwQlEdg40HAj5KNF8WW6q2KG4Z+cBCZb3m4ninfTZKaBmbIJodviQsDBoYMPHkOyJJMHnOJo5j2+LKDOhOACg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", @@ -3120,14 +3182,11 @@ "type-detect": "4.0.8" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", - "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^2.0.0" - } + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true }, "node_modules/@tabler/icons": { "version": "2.17.0", @@ -3525,6 +3584,30 @@ "node": ">= 6" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@types/babel__core": { "version": "7.20.0", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", @@ -3585,6 +3668,12 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", + "dev": true + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -3738,16 +3827,6 @@ "@types/istanbul-lib-report": "*" } }, - "node_modules/@types/jest": { - "version": "29.5.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.1.tgz", - "integrity": "sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==", - "dev": true, - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -3774,6 +3853,21 @@ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", "dev": true }, + "node_modules/@types/mocha": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", + "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", + "dev": true + }, + "node_modules/@types/mock-fs": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.1.tgz", + "integrity": "sha512-m6nFAJ3lBSnqbvDZioawRvpLXSaPyn52Srf7OfzjubYbYX8MTUdIgDxQl0wEapm4m/pNYSd9TXocpQ0TvZFlYA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -3820,6 +3914,12 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, + "node_modules/@types/proxyquire": { + "version": "1.3.28", + "resolved": "https://registry.npmjs.org/@types/proxyquire/-/proxyquire-1.3.28.tgz", + "integrity": "sha512-SQaNzWQ2YZSr7FqAyPPiA3FYpux2Lqh3HWMZQk47x3xbMCqgC/w0dY3dw9rGqlweDDkrySQBcaScXWeR+Yb11Q==", + "dev": true + }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -3912,6 +4012,21 @@ "integrity": "sha512-95hZXmBvwtvsLMPefKT9xquUSAJXsVDUaipyUiYoYi3ZdLhZ3w30w230Ugs96IdoJQb5ECvj0D82Jj/op00qWQ==", "dev": true }, + "node_modules/@types/sinon": { + "version": "10.0.15", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.15.tgz", + "integrity": "sha512-3lrFNQG0Kr2LDzvjyjB6AMJk4ge+8iYhQfdnSwIwlG88FUOV43kPcQqDZkDa/h3WSZy6i8Fr0BSjfQtB1B3xuQ==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", + "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", + "dev": true + }, "node_modules/@types/sockjs": { "version": "0.3.33", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", @@ -4427,6 +4542,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -4503,6 +4627,15 @@ "ajv": "^6.9.1" } }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -4579,6 +4712,12 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -4615,6 +4754,15 @@ "node": ">=8" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4893,6 +5041,15 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -4902,6 +5059,19 @@ "node": "*" } }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dev": true, + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -4911,6 +5081,12 @@ "node": ">=8" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "dev": true + }, "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -4999,6 +5175,12 @@ "node": ">=8" } }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "node_modules/browserslist": { "version": "4.21.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", @@ -5053,6 +5235,24 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "dev": true, + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -5141,6 +5341,36 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dev": true, + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5193,6 +5423,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -5274,6 +5513,17 @@ "node": ">= 10.0" } }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -5632,6 +5882,12 @@ "node": ">=10" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/crelt": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", @@ -5751,6 +6007,18 @@ } } }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/decode-named-character-reference": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", @@ -5778,6 +6046,18 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6016,6 +6296,15 @@ "node": ">=12" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.2" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6659,6 +6948,19 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, + "node_modules/fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==", + "dev": true, + "dependencies": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -6742,6 +7044,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -6856,6 +7167,53 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -6878,6 +7236,15 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", @@ -7631,6 +7998,15 @@ "node": ">=0.12.0" } }, + "node_modules/is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -7640,6 +8016,15 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -7663,6 +8048,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -8550,6 +8947,12 @@ "setimmediate": "^1.0.5" } }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -8627,6 +9030,12 @@ "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.1.tgz", "integrity": "sha512-zFN/CTVmbcVef+WaDXT63dNzzkfRBKT1j464NJQkV7iSgJU0sLBus9W0HBwnXK13/hf168pbrx/V/bjEHOXNHA==" }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "dev": true + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -8677,6 +9086,12 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -8689,6 +9104,22 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8700,6 +9131,15 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -9319,6 +9759,165 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/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 + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mock-fs": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz", + "integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==", + "dev": true + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -9345,6 +9944,18 @@ "multicast-dns": "cli.js" } }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -9380,6 +9991,43 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/nise/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -9828,6 +10476,15 @@ "node": ">=8" } }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -10354,6 +11011,17 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/proxyquire": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", + "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", + "dev": true, + "dependencies": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.1", + "resolve": "^1.11.1" + } + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -11630,6 +12298,24 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sinon": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.1.0.tgz", + "integrity": "sha512-cS5FgpDdE9/zx7no8bxROHymSlPLZzq0ChbbLk1DrxBfc+eTeBK3y8nIL+nu/0QeYydhhbLIr7ecHJpywjQaoQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^10.2.0", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.4", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -12084,6 +12770,15 @@ "node": ">=0.6" } }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -12164,6 +12859,58 @@ "webpack": "^5.0.0" } }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -12298,6 +13045,24 @@ "node": ">= 0.8" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", @@ -12514,6 +13279,12 @@ "node": ">=6" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -12543,6 +13314,22 @@ "node": ">= 0.8" } }, + "node_modules/vscode-test": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.6.1.tgz", + "integrity": "sha512-086q88T2ca1k95mUzffvbzb7esqQNvJgiwY4h29ukPhFo8u+vXOOmelUoU5EQUHs3Of8+JuQ3oGdbVCqaxuTXA==", + "deprecated": "This package has been renamed to @vscode/test-electron, please update to the new name", + "dev": true, + "dependencies": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "rimraf": "^3.0.2", + "unzipper": "^0.10.11" + }, + "engines": { + "node": ">=8.9.3" + } + }, "node_modules/w3c-keyname": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", @@ -12951,6 +13738,12 @@ "node": ">=0.10.0" } }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -13039,6 +13832,57 @@ "node": ">= 6" } }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -14376,6 +15220,27 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, "@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -15275,23 +16140,51 @@ "dev": true }, "@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", "dev": true, "requires": { "type-detect": "4.0.8" } }, "@sinonjs/fake-timers": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", - "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.2.0.tgz", + "integrity": "sha512-OPwQlEdg40HAj5KNF8WW6q2KG4Z+cBCZb3m4ninfTZKaBmbIJodviQsDBoYMPHkOyJJMHnOJo5j2+LKDOhOACg==", "dev": true, "requires": { - "@sinonjs/commons": "^2.0.0" + "@sinonjs/commons": "^3.0.0" } }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "@tabler/icons": { "version": "2.17.0", "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-2.17.0.tgz", @@ -15510,6 +16403,30 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "@types/babel__core": { "version": "7.20.0", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", @@ -15570,6 +16487,12 @@ "@types/node": "*" } }, + "@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", + "dev": true + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -15723,16 +16646,6 @@ "@types/istanbul-lib-report": "*" } }, - "@types/jest": { - "version": "29.5.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.1.tgz", - "integrity": "sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==", - "dev": true, - "requires": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -15759,6 +16672,21 @@ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", "dev": true }, + "@types/mocha": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", + "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", + "dev": true + }, + "@types/mock-fs": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.1.tgz", + "integrity": "sha512-m6nFAJ3lBSnqbvDZioawRvpLXSaPyn52Srf7OfzjubYbYX8MTUdIgDxQl0wEapm4m/pNYSd9TXocpQ0TvZFlYA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -15805,6 +16733,12 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, + "@types/proxyquire": { + "version": "1.3.28", + "resolved": "https://registry.npmjs.org/@types/proxyquire/-/proxyquire-1.3.28.tgz", + "integrity": "sha512-SQaNzWQ2YZSr7FqAyPPiA3FYpux2Lqh3HWMZQk47x3xbMCqgC/w0dY3dw9rGqlweDDkrySQBcaScXWeR+Yb11Q==", + "dev": true + }, "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -15897,6 +16831,21 @@ "integrity": "sha512-95hZXmBvwtvsLMPefKT9xquUSAJXsVDUaipyUiYoYi3ZdLhZ3w30w230Ugs96IdoJQb5ECvj0D82Jj/op00qWQ==", "dev": true }, + "@types/sinon": { + "version": "10.0.15", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.15.tgz", + "integrity": "sha512-3lrFNQG0Kr2LDzvjyjB6AMJk4ge+8iYhQfdnSwIwlG88FUOV43kPcQqDZkDa/h3WSZy6i8Fr0BSjfQtB1B3xuQ==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", + "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", + "dev": true + }, "@types/sockjs": { "version": "0.3.33", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", @@ -16284,6 +17233,12 @@ "dev": true, "requires": {} }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -16341,6 +17296,12 @@ "dev": true, "requires": {} }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -16389,6 +17350,12 @@ "picomatch": "^2.0.4" } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -16421,6 +17388,12 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -16638,18 +17611,40 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, + "big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true }, + "binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dev": true, + "requires": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + } + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "dev": true + }, "body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -16730,6 +17725,12 @@ "fill-range": "^7.0.1" } }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "browserslist": { "version": "4.21.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", @@ -16765,6 +17766,18 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "dev": true + }, + "buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "dev": true + }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -16820,6 +17833,30 @@ "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz", "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==" }, + "chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dev": true, + "requires": { + "traverse": ">=0.3.0 <0.4" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -16851,6 +17888,12 @@ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==" }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -16905,6 +17948,17 @@ "source-map": "~0.6.0" } }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -17174,6 +18228,12 @@ "yaml": "^1.10.0" } }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "crelt": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", @@ -17254,6 +18314,12 @@ "ms": "2.1.2" } }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, "decode-named-character-reference": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", @@ -17275,6 +18341,15 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, + "deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -17454,6 +18529,15 @@ "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", "dev": true }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -17938,6 +19022,16 @@ } } }, + "fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==", + "dev": true, + "requires": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -18005,6 +19099,12 @@ "path-exists": "^4.0.0" } }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, "flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -18080,6 +19180,43 @@ "dev": true, "optional": true }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -18096,6 +19233,12 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true + }, "get-intrinsic": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", @@ -18629,12 +19772,24 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true + }, "is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -18649,6 +19804,12 @@ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, "is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -19341,6 +20502,12 @@ "setimmediate": "^1.0.5" } }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -19406,6 +20573,12 @@ "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.1.tgz", "integrity": "sha512-zFN/CTVmbcVef+WaDXT63dNzzkfRBKT1j464NJQkV7iSgJU0sLBus9W0HBwnXK13/hf168pbrx/V/bjEHOXNHA==" }, + "listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "dev": true + }, "loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -19444,6 +20617,12 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -19456,6 +20635,16 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -19464,6 +20653,15 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } + }, "lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -19831,6 +21029,130 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + } + } + }, + "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 + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "mock-fs": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz", + "integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==", + "dev": true + }, + "module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==", + "dev": true + }, "mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -19851,6 +21173,12 @@ "thunky": "^1.0.2" } }, + "nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -19880,6 +21208,45 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, "no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -20222,6 +21589,12 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -20641,6 +22014,17 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "proxyquire": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", + "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", + "dev": true, + "requires": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.1", + "resolve": "^1.11.1" + } + }, "punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -21576,6 +22960,20 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "sinon": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.1.0.tgz", + "integrity": "sha512-cS5FgpDdE9/zx7no8bxROHymSlPLZzq0ChbbLk1DrxBfc+eTeBK3y8nIL+nu/0QeYydhhbLIr7ecHJpywjQaoQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^10.2.0", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.4", + "supports-color": "^7.2.0" + } + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -21916,6 +23314,12 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true + }, "trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -21957,6 +23361,35 @@ "semver": "^7.3.4" } }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -22048,6 +23481,24 @@ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true }, + "unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dev": true, + "requires": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, "update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", @@ -22176,6 +23627,12 @@ } } }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -22201,6 +23658,18 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true }, + "vscode-test": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.6.1.tgz", + "integrity": "sha512-086q88T2ca1k95mUzffvbzb7esqQNvJgiwY4h29ukPhFo8u+vXOOmelUoU5EQUHs3Of8+JuQ3oGdbVCqaxuTXA==", + "dev": true, + "requires": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "rimraf": "^3.0.2", + "unzipper": "^0.10.11" + } + }, "w3c-keyname": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", @@ -22487,6 +23956,12 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -22543,6 +24018,45 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 0cfd3b6..22b92d6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "devchat", "displayName": "DevChat", "description": "Write prompts, not code", - "version": "0.0.45", + "version": "0.0.47", "icon": "assets/devchat.png", "publisher": "merico", "engines": { @@ -155,7 +155,7 @@ }, { "command": "DevChat.OPENAI_API_KEY", - "title": "DEVCHAT_API_KEY", + "title": "Input Access Key", "category": "DevChat" }, { @@ -314,7 +314,7 @@ "watch-tests": "tsc -p . -w --outDir out", "pretest": "npm run compile-tests && npm run compile && npm run lint", "lint": "eslint src --ext ts", - "test": "node ./out/test/runTest.js", + "test": "mocha", "build": "webpack --config webpack.config.js", "dev": "webpack serve --config webpack.config.js --open" }, @@ -323,19 +323,24 @@ "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.5", + "@types/chai": "^4.3.5", "@types/glob": "^8.1.0", - "@types/jest": "^29.5.1", + "@types/mocha": "^10.0.1", + "@types/mock-fs": "^4.13.1", "@types/ncp": "^2.0.5", "@types/node": "16.x", + "@types/proxyquire": "^1.3.28", "@types/react-dom": "^18.2.3", "@types/react-syntax-highlighter": "^15.5.6", "@types/shell-escape": "^0.2.1", + "@types/sinon": "^10.0.15", "@types/uuid": "^9.0.1", "@types/vscode": "^1.77.0", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", "@vscode/test-electron": "^2.3.0", "babel-loader": "^9.1.2", + "chai": "^4.3.7", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.7.3", "dotenv": "^16.0.3", @@ -345,15 +350,21 @@ "html-webpack-plugin": "^5.5.1", "jest": "^29.5.0", "json-loader": "^0.5.7", + "mocha": "^10.2.0", + "mock-fs": "^5.2.0", + "proxyquire": "^2.1.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.0.5", "redux": "^4.2.1", + "sinon": "^15.1.0", "style-loader": "^3.3.2", "ts-jest": "^29.1.0", "ts-loader": "^9.4.2", + "ts-node": "^10.9.1", "typescript": "^4.9.5", "url-loader": "^4.1.1", + "vscode-test": "^1.6.1", "webpack": "^5.76.3", "webpack-cli": "^5.0.1", "webpack-dev-server": "^4.13.3" diff --git a/src/command/commandManager.ts b/src/command/commandManager.ts index c3cae0e..b0600d0 100644 --- a/src/command/commandManager.ts +++ b/src/command/commandManager.ts @@ -1,8 +1,4 @@ -import { vs } from "react-syntax-highlighter/dist/esm/styles/hljs"; import CustomCommands from "./customCommand"; -import { logger } from "../util/logger"; -import * as vscode from 'vscode'; -import * as path from 'path'; export interface Command { name: string; diff --git a/src/command/customCommand.ts b/src/command/customCommand.ts index eeec6c3..381005c 100644 --- a/src/command/customCommand.ts +++ b/src/command/customCommand.ts @@ -2,88 +2,92 @@ import fs from 'fs'; import path from 'path'; import { logger } from '../util/logger'; -interface Command { - name: string; - pattern: string; - description: string; - message: string; - default: boolean; - show: boolean; - instructions: string[]; +export interface Command { + name: string; + pattern: string; + description: string; + message: string; + default: boolean; + show: boolean; + instructions: string[]; } class CustomCommands { private static instance: CustomCommands | null = null; private commands: Command[] = []; - private constructor() { - } + private constructor() { + } - public static getInstance(): CustomCommands { - if (!CustomCommands.instance) { - CustomCommands.instance = new CustomCommands(); - } - return CustomCommands.instance; - } - - public parseCommands(workflowsDir: string): void { - this.commands = []; - - try { - const subDirs = fs.readdirSync(workflowsDir, { withFileTypes: true }) - .filter(dirent => dirent.isDirectory()) - .map(dirent => dirent.name); - - for (const dir of subDirs) { - const settingsPath = path.join(workflowsDir, dir, '_setting_.json'); - if (fs.existsSync(settingsPath)) { - const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); - const command: Command = { - name: dir, - pattern: settings.pattern, - description: settings.description, - message: settings.message, - default: settings.default, - show: settings.show === undefined ? "true": settings.show, - instructions: settings.instructions - }; - this.commands.push(command); + public static getInstance(): CustomCommands { + if (!CustomCommands.instance) { + CustomCommands.instance = new CustomCommands(); } + return CustomCommands.instance; + } + + public parseCommands(workflowsDir: string): void { + this.commands = []; + + try { + const subDirs = fs.readdirSync(workflowsDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name); + + for (const dir of subDirs) { + const settingsPath = path.join(workflowsDir, dir, '_setting_.json'); + if (fs.existsSync(settingsPath)) { + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); + const command: Command = { + name: dir, + pattern: settings.pattern, + description: settings.description, + message: settings.message, + default: settings.default, + show: settings.show === undefined ? "true" : settings.show, + instructions: settings.instructions + }; + this.commands.push(command); + } + } + } catch (error) { + // 显示错误消息 + logger.channel()?.error(`Failed to parse commands: ${error}`); + logger.channel()?.show(); } - } catch (error) { - // 显示错误消息 - logger.channel()?.error(`Failed to parse commands: ${error}`); - logger.channel()?.show(); - } -} + } - public getCommands(): Command[] { - return this.commands; - } + public regCommand(command: Command) { + this.commands.push(command); + } - public getCommand(commandName: string): Command | null { - const foundCommand = this.commands.find(command => command.name === commandName); - return foundCommand ? foundCommand : null; + public getCommands(): Command[] { + return this.commands; + } + + public getCommand(commandName: string): Command | null { + const foundCommand = this.commands.find(command => command.name === commandName); + return foundCommand ? foundCommand : null; } - public handleCommand(commandName: string): string { - // 获取命令对象,这里假设您已经有一个方法或属性可以获取到命令对象 - const command = this.getCommand(commandName); - if (!command) { - logger.channel()?.error(`Command ${commandName} not found!`); - logger.channel()?.show(); - return ''; + public handleCommand(commandName: string): string { + // 获取命令对象,这里假设您已经有一个方法或属性可以获取到命令对象 + const command = this.getCommand(commandName); + if (!command) { + logger.channel()?.error(`Command ${commandName} not found!`); + logger.channel()?.show(); + return ''; + } + + // 构建instructions列表字符串 + const instructions = command!.instructions + .map((instruction: string) => `[instruction|./.chat/workflows/${command.name}/${instruction}]`) + .join(' '); + + // 返回结果字符串 + return `${instructions} ${command!.message}`; } - - // 构建instructions列表字符串 - const instructions = command!.instructions - .map((instruction: string) => `[instruction|./.chat/workflows/${command.name}/${instruction}]`) - .join(' '); - - // 返回结果字符串 - return `${instructions} ${command!.message}`; - } } export default CustomCommands; diff --git a/src/context/contextCodeSelected.ts b/src/context/contextCodeSelected.ts index abf920b..569aa40 100644 --- a/src/context/contextCodeSelected.ts +++ b/src/context/contextCodeSelected.ts @@ -1,7 +1,7 @@ -import * as vscode from 'vscode'; import * as path from 'path'; import { createTempSubdirectory, getLanguageIdByFileName } from '../util/commonUtil'; +import { UiUtilWrapper } from '../util/uiUtil'; export async function handleCodeSelected(fileSelected: string, codeSelected: string) { // get file name from fileSelected @@ -15,7 +15,7 @@ export async function handleCodeSelected(fileSelected: string, codeSelected: str const languageId = await getLanguageIdByFileName(fileSelected); // get relative path of workspace - const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath; + const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); const relativePath = path.relative(workspaceDir!, fileSelected); // convert fileContent to markdown code block with languageId and file path @@ -27,7 +27,7 @@ export async function handleCodeSelected(fileSelected: string, codeSelected: str const jsonData = JSON.stringify(data); // save markdownCodeBlock to temp file - await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFile), Buffer.from(jsonData)); + await UiUtilWrapper.writeFile(tempFile, jsonData); return `[context|${tempFile}]`; } \ No newline at end of file diff --git a/src/context/contextCustomCommand.ts b/src/context/contextCustomCommand.ts index 54fa938..0f8d502 100644 --- a/src/context/contextCustomCommand.ts +++ b/src/context/contextCustomCommand.ts @@ -1,15 +1,16 @@ import * as path from 'path'; -import * as vscode from 'vscode'; import { ChatContext } from './contextManager'; import { createTempSubdirectory, runCommandStringAndWriteOutput } from '../util/commonUtil'; import { logger } from '../util/logger'; +import { UiUtilWrapper } from '../util/uiUtil'; + export const customCommandContext: ChatContext = { name: '', description: 'custorm command', handler: async () => { // popup a dialog to ask for the command line to run - const customCommand = await vscode.window.showInputBox({ + const customCommand = await UiUtilWrapper.showInputBox({ prompt: 'Input your custom command', placeHolder: 'for example: ls -l' }); @@ -17,15 +18,15 @@ export const customCommandContext: ChatContext = { // 检查用户是否输入了命令 if (customCommand) { const tempDir = await createTempSubdirectory('devchat/context'); - const diff_file = path.join(tempDir, 'custom.txt'); + const diffFile = path.join(tempDir, 'custom.txt'); logger.channel()?.info(`custom command: ${customCommand}`); - const result = await runCommandStringAndWriteOutput(customCommand, diff_file); + const result = await runCommandStringAndWriteOutput(customCommand, diffFile); logger.channel()?.info(`custom command: ${customCommand} exit code:`, result.exitCode); logger.channel()?.debug(`custom command: ${customCommand} stdout:`, result.stdout); logger.channel()?.debug(`custom command: ${customCommand} stderr:`, result.stderr); - return `[context|${diff_file}]`; + return `[context|${diffFile}]`; } return ''; }, diff --git a/src/context/contextFileSelected.ts b/src/context/contextFileSelected.ts index 845bb6c..94c1a8f 100644 --- a/src/context/contextFileSelected.ts +++ b/src/context/contextFileSelected.ts @@ -1,8 +1,8 @@ -import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; import { createTempSubdirectory, getLanguageIdByFileName } from '../util/commonUtil'; +import { UiUtilWrapper } from '../util/uiUtil'; export async function handleFileSelected(fileSelected: string) { // get file name from fileSelected @@ -18,7 +18,7 @@ export async function handleFileSelected(fileSelected: string) { const languageId = await getLanguageIdByFileName(fileSelected); // get relative path of workspace - const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath; + const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); const relativePath = path.relative(workspaceDir!, fileSelected); // convert fileContent to markdown code block with languageId and file path @@ -30,7 +30,7 @@ export async function handleFileSelected(fileSelected: string) { const jsonData = JSON.stringify(data); // save markdownCodeBlock to temp file - await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFile), Buffer.from(jsonData)); + await UiUtilWrapper.writeFile(tempFile, jsonData); return `[context|${tempFile}]`; } \ No newline at end of file diff --git a/src/context/contextRef.ts b/src/context/contextRef.ts index efbdc11..ec52f91 100644 --- a/src/context/contextRef.ts +++ b/src/context/contextRef.ts @@ -1,5 +1,4 @@ -import * as vscode from 'vscode'; import * as path from 'path'; import { createTempSubdirectory, runCommandStringAndWriteOutput } from '../util/commonUtil'; import { logger } from '../util/logger'; diff --git a/src/contributes/commands.ts b/src/contributes/commands.ts index bfdc629..48d6bc2 100644 --- a/src/contributes/commands.ts +++ b/src/contributes/commands.ts @@ -1,186 +1,184 @@ import * as vscode from 'vscode'; -import ChatPanel from '../panel/chatPanel'; import { sendFileSelectMessage, sendCodeSelectMessage } from './util'; -import { logger } from '../util/logger'; -import * as childProcess from 'child_process'; -import { DevChatViewProvider } from '../panel/devchatView'; import ExtensionContextHolder from '../util/extensionContext'; +import { TopicManager } from '../topic/topicManager'; +import { TopicTreeDataProvider, TopicTreeItem } from '../panel/topicView'; +import { FilePairManager } from '../util/diffFilePairs'; +import { ApiKeyManager } from '../util/apiKey'; -import * as process from 'process'; - -export function checkDevChatDependency(): boolean { - try { - // Get pipx environment - const pipxEnvOutput = childProcess.execSync('python3 -m pipx environment').toString(); - const binPathRegex = /PIPX_BIN_DIR=\s*(.*)/; - - // Get BIN path from pipx environment - const match = pipxEnvOutput.match(binPathRegex); - if (match && match[1]) { - const binPath = match[1]; - - // Add BIN path to PATH - process.env.PATH = `${binPath}:${process.env.PATH}`; - - // Check if DevChat is installed - childProcess.execSync('devchat --help'); - return true; - } else { - return false; - } - } catch (error) { - // DevChat dependency check failed - return false; - } -} - -export async function checkOpenaiApiKey() { - const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets; - let openaiApiKey = await secretStorage.get("devchat_OPENAI_API_KEY"); - if (!openaiApiKey) { - openaiApiKey = vscode.workspace.getConfiguration('DevChat').get('API_KEY'); - } - if (!openaiApiKey) { - openaiApiKey = process.env.OPENAI_API_KEY; - } - if (!openaiApiKey) { - return false; - } - return true; -} - -function checkOpenaiKey() { - let openaiApiKey = vscode.workspace.getConfiguration('DevChat').get('API_KEY'); - if (!openaiApiKey) { - openaiApiKey = process.env.OPENAI_API_KEY; - } - if (!openaiApiKey) { - // OpenAI key not set - vscode.window.showInputBox({ - placeHolder: 'Please input your OpenAI API key (or DevChat access key)' - }).then((value) => { - if (value) { - // 设置用户输入的API Key - vscode.workspace.getConfiguration('DevChat').update('API_KEY', value, true); - } - }); - return false; - } - return true; -} - -function checkDependencyPackage() { - const dependencyInstalled = checkDevChatDependency(); - if (!dependencyInstalled) { - // Prompt the user, whether to install devchat using pip3 install devchat - const installPrompt = 'devchat is not installed. Do you want to install it using pip3 install devchat?'; - const installAction = 'Install'; - - vscode.window.showInformationMessage(installPrompt, installAction).then((selectedAction) => { - if (selectedAction === installAction) { - // Install devchat using pip3 install devchat - const terminal = vscode.window.createTerminal("DevChat Install"); - terminal.sendText("pip3 install --upgrade devchat"); - terminal.show(); - } - }); - } - - if (!checkOpenaiKey()) { - return; - } -} function registerOpenChatPanelCommand(context: vscode.ExtensionContext) { - let disposable = vscode.commands.registerCommand('devchat.openChatPanel',async () => { + let disposable = vscode.commands.registerCommand('devchat.openChatPanel', async () => { await vscode.commands.executeCommand('devchat-view.focus'); - }); - context.subscriptions.push(disposable); + }); + context.subscriptions.push(disposable); } async function ensureChatPanel(context: vscode.ExtensionContext): Promise { - await vscode.commands.executeCommand('devchat-view.focus'); - return true; + await vscode.commands.executeCommand('devchat-view.focus'); + return true; } function registerAddContextCommand(context: vscode.ExtensionContext) { - const disposableAddContext = vscode.commands.registerCommand('devchat.addConext', async (uri: { path: any; }) => { - if (!await ensureChatPanel(context)) { - return; - } + const callback = async (uri: { path: any; }) => { + if (!await ensureChatPanel(context)) { + return; + } - await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, uri.path); - }); - context.subscriptions.push(disposableAddContext); - - const disposableAddContextChinese = vscode.commands.registerCommand('devchat.addConext_chinese', async (uri: { path: any; }) => { - if (!await ensureChatPanel(context)) { - return; - } - - await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, uri.path); - }); - context.subscriptions.push(disposableAddContextChinese); + await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, uri.path); + }; + context.subscriptions.push(vscode.commands.registerCommand('devchat.addConext', callback)); + context.subscriptions.push(vscode.commands.registerCommand('devchat.addConext_chinese', callback)); } function registerAskForCodeCommand(context: vscode.ExtensionContext) { - const disposableCodeContext = vscode.commands.registerCommand('devchat.askForCode', async () => { - const editor = vscode.window.activeTextEditor; - if (editor) { - if (!await ensureChatPanel(context)) { - return; - } + const callback = async () => { + const editor = vscode.window.activeTextEditor; + if (editor) { + if (!await ensureChatPanel(context)) { + return; + } - const selectedText = editor.document.getText(editor.selection); - await sendCodeSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName, selectedText); - } - }); - context.subscriptions.push(disposableCodeContext); - - const disposableCodeContextChinese = vscode.commands.registerCommand('devchat.askForCode_chinese', async () => { - const editor = vscode.window.activeTextEditor; - if (editor) { - if (!await ensureChatPanel(context)) { - return; - } - - const selectedText = editor.document.getText(editor.selection); - await sendCodeSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName, selectedText); - } - }); - context.subscriptions.push(disposableCodeContextChinese); + const selectedText = editor.document.getText(editor.selection); + await sendCodeSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName, selectedText); + } + }; + context.subscriptions.push(vscode.commands.registerCommand('devchat.askForCode', callback)); + context.subscriptions.push(vscode.commands.registerCommand('devchat.askForCode_chinese', callback)); } function registerAskForFileCommand(context: vscode.ExtensionContext) { - const disposableAskFile = vscode.commands.registerCommand('devchat.askForFile', async () => { - const editor = vscode.window.activeTextEditor; - if (editor) { - if (!await ensureChatPanel(context)) { - return; - } + const callback = async () => { + const editor = vscode.window.activeTextEditor; + if (editor) { + if (!await ensureChatPanel(context)) { + return; + } - await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName); - } - }); - context.subscriptions.push(disposableAskFile); + await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName); + } + }; + context.subscriptions.push(vscode.commands.registerCommand('devchat.askForFile', callback)); + context.subscriptions.push(vscode.commands.registerCommand('devchat.askForFile_chinese', callback)); +} - const disposableAskFileChinese = vscode.commands.registerCommand('devchat.askForFile_chinese', async () => { - const editor = vscode.window.activeTextEditor; - if (editor) { - if (!await ensureChatPanel(context)) { - return; - } +export function registerApiKeySettingCommand(context: vscode.ExtensionContext) { + const secretStorage: vscode.SecretStorage = context.secrets; + context.subscriptions.push( + vscode.commands.registerCommand('DevChat.OPENAI_API_KEY', async () => { + const passwordInput: string = await vscode.window.showInputBox({ + password: true, + title: "Input Access Key", + placeHolder: "Set OPENAI_API_KEY (or DevChat Access Key)" + }) ?? ''; - await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName); - } - }); - context.subscriptions.push(disposableAskFileChinese); + ApiKeyManager.writeApiKeySecret(passwordInput); + }) + ); +} + +export function registerStatusBarItemClickCommand(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand('devcaht.onStatusBarClick', async () => { + await vscode.commands.executeCommand('devchat-view.focus'); + }) + ); +} + +const topicDeleteCallback = async (item: TopicTreeItem) => { + const confirm = 'Delete'; + const cancel = 'Cancel'; + const label = typeof item.label === 'string' ? item.label : item.label!.label; + const truncatedLabel = label.substring(0, 20) + (label.length > 20 ? '...' : ''); + const result = await vscode.window.showWarningMessage( + `Are you sure you want to delete the topic "${truncatedLabel}"?`, + { modal: true }, + confirm, + cancel + ); + + if (result === confirm) { + TopicManager.getInstance().deleteTopic(item.id); + } +}; +; + +export function regTopicDeleteCommand(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand('devchat-topicview.deleteTopic', topicDeleteCallback) + ); +} + +export function regAddTopicCommand(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand('devchat-topicview.addTopic', () => { + const topic = TopicManager.getInstance().createTopic(); + TopicManager.getInstance().setCurrentTopic(topic.topicId); + }) + ); +} + +export function regDeleteSelectTopicCommand(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand('devchat-topicview.deleteSelectedTopic', () => { + const selectedItem = TopicTreeDataProvider.getInstance().selectedItem; + if (selectedItem) { + topicDeleteCallback(selectedItem); + } else { + vscode.window.showErrorMessage('No item selected'); + } + }) + ); +} + +export function regSelectTopicCommand(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand('devchat-topicview.selectTopic', (item: TopicTreeItem) => { + TopicTreeDataProvider.getInstance().setSelectedItem(item); + TopicManager.getInstance().setCurrentTopic(item.id); + }) + ); +} + +export function regReloadTopicCommand(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand('devchat-topicview.reloadTopic', async () => { + TopicManager.getInstance().loadTopics(); + }) + ); +} + +export function regApplyDiffResultCommand(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand('devchat.applyDiffResult', async () => { + const activeEditor = vscode.window.activeTextEditor; + const fileName = activeEditor!.document.fileName; + + const [leftUri, rightUri] = FilePairManager.getInstance().findPair(fileName) || [undefined, undefined]; + if (leftUri && rightUri) { + // 获取对比的两个文件 + const leftDoc = await vscode.workspace.openTextDocument(leftUri); + const rightDoc = await vscode.workspace.openTextDocument(rightUri); + + // 将右边文档的内容替换到左边文档 + const leftEditor = await vscode.window.showTextDocument(leftDoc); + await leftEditor.edit(editBuilder => { + const fullRange = new vscode.Range(0, 0, leftDoc.lineCount, 0); + editBuilder.replace(fullRange, rightDoc.getText()); + }); + + // 保存左边文档 + await leftDoc.save(); + } else { + vscode.window.showErrorMessage('No file to apply diff result.'); + } + }) + ); } export { - checkDependencyPackage, - registerOpenChatPanelCommand, - registerAddContextCommand, - registerAskForCodeCommand, - registerAskForFileCommand, + registerOpenChatPanelCommand, + registerAddContextCommand, + registerAskForCodeCommand, + registerAskForFileCommand, }; diff --git a/src/contributes/commandsBase.ts b/src/contributes/commandsBase.ts new file mode 100644 index 0000000..866e1aa --- /dev/null +++ b/src/contributes/commandsBase.ts @@ -0,0 +1,42 @@ +// src/contributes/commandsBase.ts + +import { runCommand } from "../util/commonUtil"; + + +export function checkDevChatDependency(): boolean { + try { + const binPath = getPipxEnvironmentPath(); + + if (binPath) { + updateEnvironmentPath(binPath); + + // Check if DevChat is installed + runCommand('devchat --help'); + return true; + } else { + return false; + } + } catch (error) { + // DevChat dependency check failed + return false; + } +} + +export function getPipxEnvironmentPath(): string | null { + // Get pipx environment + const pipxEnvOutput = runCommand('python3 -m pipx environment').toString(); + const binPathRegex = /PIPX_BIN_DIR=\s*(.*)/; + + // Get BIN path from pipx environment + const match = pipxEnvOutput.match(binPathRegex); + if (match && match[1]) { + return match[1]; + } else { + return null; + } +} + +function updateEnvironmentPath(binPath: string): void { + // Add BIN path to PATH + process.env.PATH = `${binPath}:${process.env.PATH}`; +} \ No newline at end of file diff --git a/src/contributes/context.ts b/src/contributes/context.ts new file mode 100644 index 0000000..8a4e23b --- /dev/null +++ b/src/contributes/context.ts @@ -0,0 +1,10 @@ +import * as vscode from 'vscode'; + +export function regLanguageContext() { + const currentLocale = vscode.env.language; + if (currentLocale === 'zh-cn' || currentLocale === 'zh-tw') { + vscode.commands.executeCommand('setContext', 'isChineseLocale', true); + } else { + vscode.commands.executeCommand('setContext', 'isChineseLocale', false); + } +} \ No newline at end of file diff --git a/src/contributes/util.ts b/src/contributes/util.ts index 625c2f7..0a30a60 100644 --- a/src/contributes/util.ts +++ b/src/contributes/util.ts @@ -4,15 +4,18 @@ import { handleCodeSelected } from '../context/contextCodeSelected'; import { handleFileSelected } from '../context/contextFileSelected'; import { MessageHandler } from '../handler/messageHandler'; import { regInMessage, regOutMessage } from '../util/reg_messages'; +import { logger } from '../util/logger'; regOutMessage({command: 'appendContext', context: ''}); export async function sendFileSelectMessage(panel: vscode.WebviewPanel|vscode.WebviewView, filePath: string): Promise { + logger.channel()?.info(`Append context: ${filePath}`); const codeContext = await handleFileSelected(filePath); MessageHandler.sendMessage(panel, { command: 'appendContext', context: codeContext }); } regOutMessage({command: 'appendContext', context: ''}); export async function sendCodeSelectMessage(panel: vscode.WebviewPanel|vscode.WebviewView, filePath: string, codeBlock: string): Promise { + logger.channel()?.info(`Append context: ${filePath}`); const codeContext = await handleCodeSelected(filePath, codeBlock); MessageHandler.sendMessage(panel, { command: 'appendContext', context: codeContext }); } \ No newline at end of file diff --git a/src/contributes/views.ts b/src/contributes/views.ts new file mode 100644 index 0000000..911b77f --- /dev/null +++ b/src/contributes/views.ts @@ -0,0 +1,21 @@ +import * as vscode from 'vscode'; +import { DevChatViewProvider } from '../panel/devchatView'; +import { TopicTreeDataProvider } from '../panel/topicView'; +import ExtensionContextHolder from '../util/extensionContext'; + + +export function regDevChatView(context: vscode.ExtensionContext) { + ExtensionContextHolder.provider = new DevChatViewProvider(context); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider('devchat-view', ExtensionContextHolder.provider, { + webviewOptions: { retainContextWhenHidden: true } + }) + ); +} + +export function regTopicView(context: vscode.ExtensionContext) { + const yourTreeView = vscode.window.createTreeView('devchat-topicview', { + treeDataProvider: TopicTreeDataProvider.getInstance(), + }); + context.subscriptions.push(yourTreeView); +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index a4dd23f..d9cdbf9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,358 +1,55 @@ import * as vscode from 'vscode'; -import * as fs from 'fs'; import { - checkOpenaiApiKey, - checkDevChatDependency, - checkDependencyPackage, registerOpenChatPanelCommand, registerAddContextCommand, registerAskForCodeCommand, registerAskForFileCommand, + registerApiKeySettingCommand, + regTopicDeleteCommand, + regAddTopicCommand, + regDeleteSelectTopicCommand, + regSelectTopicCommand, + regReloadTopicCommand, + regApplyDiffResultCommand, + registerStatusBarItemClickCommand, } from './contributes/commands'; +import { regLanguageContext } from './contributes/context'; +import { regDevChatView, regTopicView } from './contributes/views'; import ExtensionContextHolder from './util/extensionContext'; import { logger } from './util/logger'; -import { DevChatViewProvider } from './panel/devchatView'; -import path from 'path'; -import { FilePairManager } from './util/diffFilePairs'; -import { Topic, TopicManager } from './topic/topicManager'; - - -class TopicTreeItem extends vscode.TreeItem { - id: string; - date: number | undefined; - constructor(label: string, id: string, date: number | undefined, collapsibleState: vscode.TreeItemCollapsibleState) { - super(label, collapsibleState); - this.id = id; - this.date = date; - this.iconPath = new vscode.ThemeIcon('symbol-variable'); - this.contextValue = 'yourTreeItem'; // 添加这一行 - } -} - -class TopicTreeDataProvider implements vscode.TreeDataProvider { - private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); - readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - - public selectedItem: TopicTreeItem | null = null; - private items: TopicTreeItem[] = []; - - // reg listeners to TopicManager in constructor - constructor() { - TopicManager.getInstance().addOnCreateTopicListener(this.addItem.bind(this)); - TopicManager.getInstance().addOnDeleteTopicListener(this.onDeleteTopic.bind(this)); - TopicManager.getInstance().addOnReloadTopicsListener(this.onReloadTopics.bind(this)); - TopicManager.getInstance().addOnUpdateTopicListener(this.onUpdateTopics.bind(this)); - } - - // sort items - private sortItems() { - this.items.sort((a, b) => { - if (a.date && b.date) { - return b.date - a.date; - } else if (!a.date) { - return -1; - } else if (!b.date) { - return 1; - } else { - return 0; - } - }); - } - - onUpdateTopics(topicId: string) { - const items = this.items.filter(i => i.id === topicId); - const topic = TopicManager.getInstance().getTopic(topicId); - items.map((item) => { - item.label = topic?.name; - item.date = topic?.lastUpdated; - }); - this.sortItems(); - this._onDidChangeTreeData.fire(); - } - - onReloadTopics(topics: Topic[]) { - const items = topics.map((topic) => { - return new TopicTreeItem(topic.name ? topic.name : "new topic", topic.topicId, topic.lastUpdated, vscode.TreeItemCollapsibleState.None); - }); - this.items = items; - this.sortItems(); - this._onDidChangeTreeData.fire(); - } - - onDeleteTopic(topicId: string) { - this.items = this.items.filter(i => i.id !== topicId); - this.sortItems(); - this._onDidChangeTreeData.fire(); - } - - setSelectedItem(item: TopicTreeItem): void { - this.selectedItem = item; - } - - getChildren(element?: TopicTreeItem): vscode.ProviderResult { - return this.items; - } - - getTreeItem(element: TopicTreeItem): vscode.TreeItem | Thenable { - element.command = { - title: 'Select Item', - command: 'devchat-topicview.selectTopic', - arguments: [element], - }; - return element; - } - - reload(): void { - const topicList = TopicManager.getInstance().getTopicList(); - this.onReloadTopics(topicList); - } - - addItem(topic: Topic): void { - const newItem = new TopicTreeItem(topic.name ? topic.name : "new topic", topic.topicId, topic.lastUpdated, vscode.TreeItemCollapsibleState.None); - this.items.push(newItem); - this.sortItems(); - this._onDidChangeTreeData.fire(); - } - - - deleteItem(item: TopicTreeItem): void { - this.items = this.items.filter(i => i !== item); - this.sortItems(); - this._onDidChangeTreeData.fire(); - } -} - -function getExtensionVersion(context: vscode.ExtensionContext): string { - const packageJsonPath = path.join(context.extensionUri.fsPath, 'package.json'); - const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8'); - const packageJson = JSON.parse(packageJsonContent); - - return packageJson.version; -} +import { LoggerChannelVscode } from './util/logger_vscode'; +import { createStatusBarItem } from './panel/statusBarView'; +import { UiUtilWrapper } from './util/uiUtil'; +import { UiUtilVscode } from './util/uiUtil_vscode'; function activate(context: vscode.ExtensionContext) { ExtensionContextHolder.context = context; - const extensionVersion = getExtensionVersion(context); - logger.init(context); - const secretStorage: vscode.SecretStorage = context.secrets; - vscode.commands.registerCommand('DevChat.OPENAI_API_KEY', async () => { - const passwordInput: string = await vscode.window.showInputBox({ - password: true, - title: "OPENAI_API_KEY" - }) ?? ''; + logger.init(LoggerChannelVscode.getInstance()); + UiUtilWrapper.init(new UiUtilVscode()); - secretStorage.store("devchat_OPENAI_API_KEY", passwordInput); - }); + regLanguageContext(); - const currentLocale = vscode.env.language; - if (currentLocale === 'zh-cn' || currentLocale === 'zh-tw') { - vscode.commands.executeCommand('setContext', 'isChineseLocale', true); - } else { - vscode.commands.executeCommand('setContext', 'isChineseLocale', false); - } + regDevChatView(context); + regTopicView(context); + registerApiKeySettingCommand(context); registerOpenChatPanelCommand(context); registerAddContextCommand(context); registerAskForCodeCommand(context); registerAskForFileCommand(context); + registerStatusBarItemClickCommand(context); - const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); + createStatusBarItem(context); - // Set the status bar item properties - // const iconPath = context.asAbsolutePath(path.join('assets', 'tank.png')); - - // Set the status bar item properties - statusBarItem.text = `$(warning)DevChat`; - statusBarItem.tooltip = 'DevChat checking ..., please wait.'; - statusBarItem.command = ''; - - // add a timer to update the status bar item - let devchatStatus = ''; - let apiKeyStatus = ''; - let isVersionChangeCompare: boolean|undefined = undefined; - setInterval(async () => { - const versionOld = await secretStorage.get("DevChatVersionOld"); - const versionNew = extensionVersion; - const versionChanged = versionOld !== versionNew; - await secretStorage.store("DevChatVersionOld", versionNew!); - - // status item has three status type - // 1. not in a folder - // 2. dependence is invalid - // 3. ready - if (devchatStatus === '' || devchatStatus === 'waiting install devchat') { - let bOk = true; - let devChat: string | undefined = vscode.workspace.getConfiguration('DevChat').get('DevChatPath'); - if (!devChat) { - bOk = false; - } - - if (!bOk) { - bOk = checkDevChatDependency(); - } - if (bOk && versionChanged && !isVersionChangeCompare) { - logger.channel()?.info(`versionOld: ${versionOld}, versionNew: ${versionNew}, versionChanged: ${versionChanged}`); - bOk = false; - } - - if (bOk) { - devchatStatus = 'ready'; - TopicManager.getInstance().loadTopics(); - } else { - if (devchatStatus === '') { - devchatStatus = 'not ready'; - } - } - } - if (devchatStatus === 'not ready') { - // auto install devchat - const terminal = vscode.window.createTerminal("DevChat Install"); - terminal.sendText(`python ${context.extensionUri.fsPath + "/tools/install.py"}`); - terminal.show(); - devchatStatus = 'waiting install devchat'; - isVersionChangeCompare = true; - } - - if (devchatStatus !== 'ready') { - statusBarItem.text = `$(warning)DevChat`; - statusBarItem.tooltip = `${devchatStatus}`; - statusBarItem.command = undefined; - // set statusBarItem warning color - return; - } - - // check api key - if (apiKeyStatus === '' || apiKeyStatus === 'please set api key') { - const bOk = await checkOpenaiApiKey(); - if (bOk) { - apiKeyStatus = 'ready'; - } else { - apiKeyStatus = 'please set api key'; - } - } - if (apiKeyStatus !== 'ready') { - statusBarItem.text = `$(warning)DevChat`; - statusBarItem.tooltip = `${apiKeyStatus}`; - statusBarItem.command = 'DevChat.OPENAI_API_KEY'; - return; - } - - statusBarItem.text = `$(pass)DevChat`; - statusBarItem.tooltip = `ready to chat`; - statusBarItem.command = 'devcaht.onStatusBarClick'; - }, 3000); - - // Add the status bar item to the status bar - statusBarItem.show(); - context.subscriptions.push(statusBarItem); - - // Register the command - context.subscriptions.push( - vscode.commands.registerCommand('devcaht.onStatusBarClick', async () => { - await vscode.commands.executeCommand('devchat-view.focus'); - }) - ); - - ExtensionContextHolder.provider = new DevChatViewProvider(context); - context.subscriptions.push( - vscode.window.registerWebviewViewProvider('devchat-view', ExtensionContextHolder.provider, { - webviewOptions: { retainContextWhenHidden: true } - }) - ); - - const yourTreeDataProvider = new TopicTreeDataProvider(); - const yourTreeView = vscode.window.createTreeView('devchat-topicview', { - treeDataProvider: yourTreeDataProvider, - }); - context.subscriptions.push(yourTreeView); - - const topicDeleteCallback = async (item: TopicTreeItem) => { - const confirm = 'Delete'; - const cancel = 'Cancel'; - const label = typeof item.label === 'string' ? item.label : item.label!.label; - const truncatedLabel = label.substring(0, 20) + (label.length > 20 ? '...' : ''); - const result = await vscode.window.showWarningMessage( - `Are you sure you want to delete the topic "${truncatedLabel}"?`, - { modal: true }, - confirm, - cancel - ); - - if (result === confirm) { - TopicManager.getInstance().deleteTopic(item.id); - } - }; - vscode.commands.registerCommand('devchat-topicview.deleteTopic', topicDeleteCallback); - - context.subscriptions.push( - vscode.languages.registerCodeActionsProvider( - { pattern: '**', scheme: 'file' }, - { - provideCodeActions: (document, range, context, token) => { - const deleteAction = new vscode.CodeAction('Delete Item', vscode.CodeActionKind.QuickFix); - deleteAction.command = { - title: 'Delete Item', - command: 'devchat-topicview.deleteTopic', - arguments: [context.diagnostics[0].code], - }; - return [deleteAction]; - }, - }, - { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] } - ) - ); - - vscode.commands.registerCommand('devchat-topicview.addTopic', () => { - const topic = TopicManager.getInstance().createTopic(); - TopicManager.getInstance().setCurrentTopic(topic.topicId); - }); - - vscode.commands.registerCommand('devchat-topicview.deleteSelectedTopic', () => { - const selectedItem = yourTreeDataProvider.selectedItem; - if (selectedItem) { - topicDeleteCallback(selectedItem); - } else { - vscode.window.showErrorMessage('No item selected'); - } - }); - - vscode.commands.registerCommand('devchat-topicview.selectTopic', (item: TopicTreeItem) => { - yourTreeDataProvider.setSelectedItem(item); - TopicManager.getInstance().setCurrentTopic(item.id); - }); - - vscode.commands.registerCommand('devchat-topicview.reloadTopic', async (item: TopicTreeItem) => { - TopicManager.getInstance().loadTopics(); - }); - - context.subscriptions.push( - vscode.commands.registerCommand('devchat.applyDiffResult', async (data) => { - const activeEditor = vscode.window.activeTextEditor; - const fileName = activeEditor!.document.fileName; - - const [leftUri, rightUri] = FilePairManager.getInstance().findPair(fileName) || [undefined, undefined]; - if (leftUri && rightUri) { - // 获取对比的两个文件 - const leftDoc = await vscode.workspace.openTextDocument(leftUri); - const rightDoc = await vscode.workspace.openTextDocument(rightUri); - - // 将右边文档的内容替换到左边文档 - const leftEditor = await vscode.window.showTextDocument(leftDoc); - await leftEditor.edit(editBuilder => { - const fullRange = new vscode.Range(0, 0, leftDoc.lineCount, 0); - editBuilder.replace(fullRange, rightDoc.getText()); - }); - - // 保存左边文档 - await leftDoc.save(); - } else { - vscode.window.showErrorMessage('No file to apply diff result.'); - } - }) - ); + regTopicDeleteCommand(context); + regAddTopicCommand(context); + regDeleteSelectTopicCommand(context); + regSelectTopicCommand(context); + regReloadTopicCommand(context); + regApplyDiffResultCommand(context); } exports.activate = activate; diff --git a/src/handler/contextDetail.ts b/src/handler/contextDetail.ts index fce2a2a..c6caf34 100644 --- a/src/handler/contextDetail.ts +++ b/src/handler/contextDetail.ts @@ -2,15 +2,19 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import { MessageHandler } from './messageHandler'; import { regInMessage, regOutMessage } from '../util/reg_messages'; +import { logger } from '../util/logger'; -regInMessage({command: 'contextDetail', file: ''}); -regOutMessage({command: 'contextDetailResponse', file: '', result: ''}); +regInMessage({ command: 'contextDetail', file: '' }); +regOutMessage({ command: 'contextDetailResponse', file: '', result: '' }); // message: { command: 'contextDetail', file: string } // read detail context information from file // return json string -export async function contextDetail(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { - const fileContent = fs.readFileSync(message.file, 'utf-8'); - MessageHandler.sendMessage(panel, { command: 'contextDetailResponse', 'file':message.file, result: fileContent }); - return; +export async function contextDetail(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise { + try { + const fileContent = fs.readFileSync(message.file, 'utf-8'); + MessageHandler.sendMessage(panel, { command: 'contextDetailResponse', 'file': message.file, result: fileContent }); + } catch (error) { + logger.channel()?.error(`Error reading file ${ message.file }:, ${error}`); + } } diff --git a/src/handler/historyMessages.ts b/src/handler/historyMessages.ts index d53bdd2..9c73ea1 100644 --- a/src/handler/historyMessages.ts +++ b/src/handler/historyMessages.ts @@ -1,141 +1,21 @@ import * as vscode from 'vscode'; -import DevChat, { LogOptions, LogEntry } from '../toolwrapper/devchat'; import { MessageHandler } from './messageHandler'; -import messageHistory from '../util/messageHistory'; import { regInMessage, regOutMessage } from '../util/reg_messages'; -import { checkOpenaiApiKey } from '../contributes/commands'; -import ExtensionContextHolder from '../util/extensionContext'; -import { TopicManager } from '../topic/topicManager'; +import { historyMessagesBase, onApiKeyBase } from './historyMessagesBase'; -let isApiSet: boolean | undefined = undefined; - -interface LoadHistoryMessages { - command: string; - entries: Array; -} - -function welcomeMessage(): LogEntry { - // create default logEntry to show welcome message - return { - hash: 'message', - parent: '', - user: 'system', - date: '', - request: 'How do I use DevChat?', - response: ` -Do you want to write some code or have a question about the project? Simply right-click on your chosen files or code snippets and add them to DevChat. Feel free to ask me anything or let me help you with coding. - -Don't forget to check out the "+" button on the left of the input to add more context. To see a list of workflows you can run in the context, just type "/". Happy prompting! - `, - context: [] - } as LogEntry; -} - -function apiKeyMissedMessage(): LogEntry { - // create default logEntry to show welcome message - return { - hash: 'message', - parent: '', - user: 'system', - date: '', - request: 'Is OPENAI_API_KEY ready?', - response: ` -OPENAI_API_KEY is missing from your environment or settings. Kindly input your OpenAI or DevChat key, and I'll ensure DevChat is all set for you. - `, - context: [] - } as LogEntry; -} - regInMessage({command: 'historyMessages', options: { skip: 0, maxCount: 0 }}); regOutMessage({command: 'loadHistoryMessages', entries: [{hash: '',user: '',date: '',request: '',response: '',context: [{content: '',role: ''}]}]}); export async function historyMessages(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { - const topicId = TopicManager.getInstance().currentTopicId; - let logEntriesFlat: Array = []; - if (topicId) { - logEntriesFlat = await TopicManager.getInstance().getTopicHistory(topicId); + const historyMessage = await historyMessagesBase(); + if (historyMessage) { + MessageHandler.sendMessage(panel, historyMessage); } - messageHistory.clear(); - - // TODO handle context - const logEntriesFlatFiltered = logEntriesFlat.map((entry) => { - return { - date: entry.date, - hash: entry.hash, - request: entry.request, - text: entry.response, - user: entry.user, - parentHash: '', - }; - }); - - for (let i = 0; i < logEntriesFlat.length; i++) { - let entryOld = logEntriesFlat[i]; - let entryNew = { - date: entryOld.date, - hash: entryOld.hash, - request: entryOld.request, - text: entryOld.response, - user: entryOld.user, - parentHash: '', - }; - if (i > 0) { - entryNew.parentHash = logEntriesFlat[i - 1].hash; - } - messageHistory.add(entryNew); - } - - const isApiKeyReady = await checkOpenaiApiKey(); - isApiSet = true; - if (!isApiKeyReady) { - const startMessage = [ apiKeyMissedMessage() ]; - isApiSet = false; - - MessageHandler.sendMessage(panel, { - command: 'loadHistoryMessages', - entries: startMessage, - } as LoadHistoryMessages); - return; - } - - const loadHistoryMessages: LoadHistoryMessages = { - command: 'loadHistoryMessages', - entries: logEntriesFlat.length>0? logEntriesFlat : [welcomeMessage()], - }; - - MessageHandler.sendMessage(panel, loadHistoryMessages); - return; -} - - -export function isValidApiKey(apiKey: string) { - let apiKeyStrim = apiKey.trim(); - if (apiKeyStrim.indexOf('sk-') !== 0 && apiKeyStrim.indexOf('DC.') !== 0) { - return false; - } - return true; -} - -export async function isWaitForApiKey() { - if (isApiSet === undefined) { - isApiSet = await checkOpenaiApiKey(); - } - return !isApiSet; } export async function onApiKey(apiKey: string, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { - if (!isValidApiKey(apiKey)) { - MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: 'Your API key is invalid. We support OpenAI and DevChat keys. Please reset the key.', hash: '', user: 'system', date: '', isError: false }); - return; - } - - isApiSet = true; - - const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context?.secrets!; - secretStorage.store("devchat_OPENAI_API_KEY", apiKey); - - const welcomeMessageText = welcomeMessage().response; - MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: `Your OPENAI_API_KEY is set. Enjoy DevChat!\n${welcomeMessageText}`, hash: '', user: 'system', date: '', isError: false }); + const resMessage = await onApiKeyBase(apiKey); + MessageHandler.sendMessage(panel, resMessage); } diff --git a/src/handler/historyMessagesBase.ts b/src/handler/historyMessagesBase.ts new file mode 100644 index 0000000..ef86724 --- /dev/null +++ b/src/handler/historyMessagesBase.ts @@ -0,0 +1,143 @@ + + +import { TopicManager } from '../topic/topicManager'; +import { LogEntry } from '../toolwrapper/devchat'; +import messageHistory from '../util/messageHistory'; +import { ApiKeyManager } from '../util/apiKey'; +import { logger } from '../util/logger'; + +let isApiSet: boolean | undefined = undefined; + +interface LoadHistoryMessages { + command: string; + entries: Array; +} + +function welcomeMessage(): LogEntry { + // create default logEntry to show welcome message + return { + hash: 'message', + parent: '', + user: 'system', + date: '', + request: 'How do I use DevChat?', + response: ` +Do you want to write some code or have a question about the project? Simply right-click on your chosen files or code snippets and add them to DevChat. Feel free to ask me anything or let me help you with coding. + +Don't forget to check out the "+" button on the left of the input to add more context. To see a list of workflows you can run in the context, just type "/". Happy prompting! + `, + context: [] + } as LogEntry; +} + +function apiKeyMissedMessage(): LogEntry { + // create default logEntry to show welcome message + return { + hash: 'message', + parent: '', + user: 'system', + date: '', + request: 'Is OPENAI_API_KEY ready?', + response: ` +OPENAI_API_KEY is missing from your environment or settings. Kindly input your OpenAI or DevChat key, and I'll ensure DevChat is all set for you. + `, + context: [] + } as LogEntry; +} + +export function isValidApiKey(apiKey: string) { + let apiKeyStrim = apiKey.trim(); + if (ApiKeyManager.getKeyType(apiKeyStrim) === undefined) { + return false; + } + return true; +} + +export async function isWaitForApiKey() { + if (isApiSet === undefined) { + const apiKey = await ApiKeyManager.getApiKey(); + isApiSet = apiKey !== undefined; + } + return !isApiSet; +} + +export async function loadTopicHistoryLogs() : Promise | undefined> { + const topicId = TopicManager.getInstance().currentTopicId; + let logEntriesFlat: Array = []; + if (topicId) { + logEntriesFlat = await TopicManager.getInstance().getTopicHistory(topicId); + } + + if (topicId !== TopicManager.getInstance().currentTopicId) { + logger.channel()?.info(`Current topic changed dure load topic hsitory!`) + return undefined; + } + return logEntriesFlat; +} + +export function updateCurrentMessageHistory(logEntries: Array): void { + messageHistory.clear(); + + for (let i = 0; i < logEntries.length; i++) { + let entryOld = logEntries[i]; + let entryNew = { + date: entryOld.date, + hash: entryOld.hash, + request: entryOld.request, + text: entryOld.response, + user: entryOld.user, + parentHash: '', + }; + if (i > 0) { + entryNew.parentHash = logEntries[i - 1].hash; + } + messageHistory.add(entryNew); + } +} + +export async function apiKeyInvalidMessage(): Promise { + const apiKey = await ApiKeyManager.getApiKey(); + isApiSet = true; + if (!apiKey) { + const startMessage = [ apiKeyMissedMessage() ]; + isApiSet = false; + + return { + command: 'loadHistoryMessages', + entries: startMessage, + } as LoadHistoryMessages; + } else { + return undefined; + } +} + +export async function historyMessagesBase(): Promise { + const logEntriesFlat = await loadTopicHistoryLogs(); + if (!logEntriesFlat) { + return undefined; + } + + updateCurrentMessageHistory(logEntriesFlat); + + const apiKeyMessage = await apiKeyInvalidMessage(); + if (apiKeyMessage !== undefined) { + return apiKeyMessage; + } + + return { + command: 'loadHistoryMessages', + entries: logEntriesFlat.length>0? logEntriesFlat : [welcomeMessage()], + } as LoadHistoryMessages; +} + +export async function onApiKeyBase(apiKey: string): Promise<{command: string, text: string, hash: string, user: string, date: string, isError: boolean}> { + if (!isValidApiKey(apiKey)) { + return { command: 'receiveMessage', text: 'Your API key is invalid. We support OpenAI and DevChat keys. Please reset the key.', hash: '', user: 'system', date: '', isError: false }; + } + + isApiSet = true; + ApiKeyManager.writeApiKeySecret(apiKey); + + const welcomeMessageText = welcomeMessage().response; + return { command: 'receiveMessage', text: `Your OPENAI_API_KEY is set. Enjoy DevChat!\n${welcomeMessageText}`, hash: '', user: 'system', date: '', isError: false }; +} \ No newline at end of file diff --git a/src/handler/messageHandler.ts b/src/handler/messageHandler.ts index ac42951..6b2f09a 100644 --- a/src/handler/messageHandler.ts +++ b/src/handler/messageHandler.ts @@ -5,9 +5,9 @@ import * as vscode from 'vscode'; import '../command/loadCommands'; import '../context/loadContexts'; import { logger } from '../util/logger'; -import { on } from 'events'; -import { isWaitForApiKey, onApiKey } from './historyMessages'; -import { checkOpenaiApiKey } from '../contributes/commands'; +import { isWaitForApiKey } from './historyMessagesBase'; +import { onApiKey } from './historyMessages'; +import { ApiKeyManager } from '../util/apiKey'; export class MessageHandler { @@ -20,6 +20,8 @@ export class MessageHandler { this.handlers[command] = handler; } + + async handleMessage(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { let isNeedSendResponse = false; if (message.command === 'sendMessage') { @@ -34,7 +36,7 @@ export class MessageHandler { } } if (message.command === 'sendMessage') { - if (await isWaitForApiKey() && !await checkOpenaiApiKey()) { + if (await isWaitForApiKey() && !await ApiKeyManager.getApiKey()) { onApiKey(message.text, panel); return; } diff --git a/src/handler/sendMessage.ts b/src/handler/sendMessage.ts index 1db92c2..8fbfd53 100644 --- a/src/handler/sendMessage.ts +++ b/src/handler/sendMessage.ts @@ -1,76 +1,12 @@ import * as vscode from 'vscode'; -import * as fs from 'fs'; -import * as path from 'path'; -import DevChat, { ChatResponse } from '../toolwrapper/devchat'; -import CommandManager from '../command/commandManager'; -import { logger } from '../util/logger'; import { MessageHandler } from './messageHandler'; -import messageHistory from '../util/messageHistory'; -import CustomCommands from '../command/customCommand'; import { regInMessage, regOutMessage } from '../util/reg_messages'; -import { TopicManager } from '../topic/topicManager'; +import { stopDevChatBase, sendMessageBase } from './sendMessageBase'; + let _lastMessage: any = undefined; - -// Add this function to messageHandler.ts -function parseMessage(message: string): { context: string[]; instruction: string[]; reference: string[]; text: string } { - const contextRegex = /\[context\|(.*?)\]/g; - const instructionRegex = /\[instruction\|(.*?)\]/g; - const referenceRegex = /\[reference\|(.*?)\]/g; - - const contextPaths = []; - const instructionPaths = []; - const referencePaths = []; - - let match; - - // 提取 context - while ((match = contextRegex.exec(message)) !== null) { - contextPaths.push(match[1]); - } - - // 提取 instruction - while ((match = instructionRegex.exec(message)) !== null) { - instructionPaths.push(match[1]); - } - - // 提取 reference - while ((match = referenceRegex.exec(message)) !== null) { - referencePaths.push(match[1]); - } - - // 移除标签,保留纯文本 - const text = message - .replace(contextRegex, '') - .replace(instructionRegex, '') - .replace(referenceRegex, '') - .trim(); - - return { context: contextPaths, instruction: instructionPaths, reference: referencePaths, text }; -} - -function getInstructionFiles(): string[] { - const instructionFiles: string[] = []; - - const customCommands = CustomCommands.getInstance().getCommands(); - // visit customCommands, get default command - for (const command of customCommands) { - if (command.default) { - for (const instruction of command.instructions) { - instructionFiles.push(`./.chat/workflows/${command.name}/${instruction}`); - } - } - } - - return instructionFiles; -} - -const devChat = new DevChat(); -let userStop = false; - - regInMessage({command: 'sendMessage', text: '', hash: undefined}); regOutMessage({ command: 'receiveMessage', text: 'xxxx', hash: 'xxx', user: 'xxx', date: 'xxx'}); regOutMessage({ command: 'receiveMessagePartial', text: 'xxxx', user: 'xxx', date: 'xxx'}); @@ -81,72 +17,12 @@ regOutMessage({ command: 'receiveMessagePartial', text: 'xxxx', user: 'xxx', dat export async function sendMessage(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { _lastMessage = message; - const newText2 = await CommandManager.getInstance().processText(message.text); - const parsedMessage = parseMessage(newText2); - const chatOptions: any = {}; - - let parentHash = undefined; - logger.channel()?.info(`request message hash: ${message.hash}`); - if (message.hash) { - const hmessage = messageHistory.find(message.hash); - parentHash = hmessage ? message.parentHash : undefined; - } else { - const hmessage = messageHistory.findLast(); - parentHash = hmessage ? hmessage.hash : undefined; + const responseMessage = await sendMessageBase(message, (data: { command: string, text: string, user: string, date: string}) => { + MessageHandler.sendMessage(panel, data, false); + }); + if (responseMessage) { + MessageHandler.sendMessage(panel, responseMessage); } - if (parentHash) { - chatOptions.parent = parentHash; - } - logger.channel()?.info(`parent hash: ${parentHash}`); - - if (parsedMessage.context.length > 0) { - chatOptions.context = parsedMessage.context; - } - - chatOptions.header = getInstructionFiles(); - if (parsedMessage.instruction.length > 0) { - chatOptions.header = parsedMessage.instruction; - } - - if (parsedMessage.reference.length > 0) { - chatOptions.reference = parsedMessage.reference; - } - - let partialDataText = ''; - const onData = (partialResponse: ChatResponse) => { - const responseText = partialResponse.response.replace(/```\ncommitmsg/g, "```commitmsg"); - partialDataText = responseText; - MessageHandler.sendMessage(panel, { command: 'receiveMessagePartial', text: responseText, user: partialResponse.user, date: partialResponse.date }, false); - }; - - const chatResponse = await devChat.chat(parsedMessage.text, chatOptions, onData); - - if (!chatResponse.isError) { - messageHistory.add({request: message.text, text: chatResponse.response, parentHash, hash: chatResponse['prompt-hash'], user: chatResponse.user, date: chatResponse.date }); - - let topicId = TopicManager.getInstance().currentTopicId; - if (!topicId) { - // create new topic - const topic = TopicManager.getInstance().createTopic(); - topicId = topic.topicId; - } - - TopicManager.getInstance().updateTopic(topicId!, chatResponse['prompt-hash'], Number(chatResponse.date), message.text, chatResponse.response); - } - - let responseText = chatResponse.response.replace(/```\ncommitmsg/g, "```commitmsg"); - if (userStop) { - userStop = false; - if (responseText.indexOf('Exit code: undefined') >= 0) { - return; - } - } - if (chatResponse.isError) { - responseText = partialDataText + responseText; - } - - MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: responseText, hash: chatResponse['prompt-hash'], user: chatResponse.user, date: chatResponse.date, isError: chatResponse.isError }); - return; } // regeneration last message again @@ -160,9 +36,7 @@ export async function regeneration(message: any, panel: vscode.WebviewPanel|vsco regInMessage({command: 'stopDevChat'}); export async function stopDevChat(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { - logger.channel()?.info(`Stopping devchat`); - userStop = true; - devChat.stop(); + stopDevChatBase(message); } diff --git a/src/handler/sendMessageBase.ts b/src/handler/sendMessageBase.ts new file mode 100644 index 0000000..90fca3d --- /dev/null +++ b/src/handler/sendMessageBase.ts @@ -0,0 +1,160 @@ +import DevChat, { ChatResponse } from '../toolwrapper/devchat'; +import CommandManager from '../command/commandManager'; +import { logger } from '../util/logger'; +import messageHistory from '../util/messageHistory'; +import { TopicManager } from '../topic/topicManager'; +import CustomCommands from '../command/customCommand'; + + +// Add this function to messageHandler.ts +export function parseMessage(message: string): { context: string[]; instruction: string[]; reference: string[]; text: string } { + const contextRegex = /\[context\|(.*?)\]/g; + const instructionRegex = /\[instruction\|(.*?)\]/g; + const referenceRegex = /\[reference\|(.*?)\]/g; + + const contextPaths = []; + const instructionPaths = []; + const referencePaths = []; + + let match; + + // 提取 context + while ((match = contextRegex.exec(message)) !== null) { + contextPaths.push(match[1]); + } + + // 提取 instruction + while ((match = instructionRegex.exec(message)) !== null) { + instructionPaths.push(match[1]); + } + + // 提取 reference + while ((match = referenceRegex.exec(message)) !== null) { + referencePaths.push(match[1]); + } + + // 移除标签,保留纯文本 + const text = message + .replace(contextRegex, '') + .replace(instructionRegex, '') + .replace(referenceRegex, '') + .trim(); + + return { context: contextPaths, instruction: instructionPaths, reference: referencePaths, text }; +} + +export function getInstructionFiles(): string[] { + const instructionFiles: string[] = []; + + const customCommands = CustomCommands.getInstance().getCommands(); + // visit customCommands, get default command + for (const command of customCommands) { + if (command.default) { + for (const instruction of command.instructions) { + instructionFiles.push(`./.chat/workflows/${command.name}/${instruction}`); + } + } + } + + return instructionFiles; +} + +const devChat = new DevChat(); +let userStop = false; + + +// 将解析消息的部分提取到一个单独的函数中 +export async function parseMessageAndSetOptions(message: any, chatOptions: any): Promise<{ context: string[]; instruction: string[]; reference: string[]; text: string }> { + const newText2 = await CommandManager.getInstance().processText(message.text); + const parsedMessage = parseMessage(newText2); + + if (parsedMessage.context.length > 0) { + chatOptions.context = parsedMessage.context; + } + + chatOptions.header = getInstructionFiles(); + if (parsedMessage.instruction.length > 0) { + chatOptions.header = parsedMessage.instruction; + } + + if (parsedMessage.reference.length > 0) { + chatOptions.reference = parsedMessage.reference; + } + return parsedMessage; +} + +// 将处理父哈希的部分提取到一个单独的函数中 +export function getParentHash(message: any): string|undefined { + let parentHash = undefined; + logger.channel()?.info(`request message hash: ${message.hash}`); + if (message.hash) { + const hmessage = messageHistory.find(message.hash); + parentHash = hmessage ? hmessage.parentHash : undefined; + } else { + const hmessage = messageHistory.findLast(); + parentHash = hmessage ? hmessage.hash : undefined; + } + logger.channel()?.info(`parent hash: ${parentHash}`); + return parentHash; +} + +export async function handleTopic(parentHash:string, message: any, chatResponse: ChatResponse) { + if (!chatResponse.isError) { + messageHistory.add({ request: message.text, text: chatResponse.response, parentHash, hash: chatResponse['prompt-hash'], user: chatResponse.user, date: chatResponse.date }); + + let topicId = TopicManager.getInstance().currentTopicId; + if (!topicId) { + // create new topic + const topic = TopicManager.getInstance().createTopic(); + topicId = topic.topicId; + } + + TopicManager.getInstance().updateTopic(topicId!, chatResponse['prompt-hash'], Number(chatResponse.date), message.text, chatResponse.response); + } +} + +export async function handlerResponseText(partialDataText: string, chatResponse: ChatResponse) : Promise { + let responseText = chatResponse.response.replace(/```\ncommitmsg/g, "```commitmsg"); + if (userStop) { + userStop = false; + if (responseText == '' && chatResponse.isError) { + return undefined; + } + } + if (chatResponse.isError) { + responseText = partialDataText + '\n\n' + responseText; + } + return responseText; +} + +// 重构后的sendMessage函数 +export async function sendMessageBase(message: any, handlePartialData: (data: { command: string, text: string, user: string, date: string}) => void): Promise<{ command: string, text: string, hash: string, user: string, date: string, isError: boolean }|undefined> { + const chatOptions: any = {}; + const parsedMessage = await parseMessageAndSetOptions(message, chatOptions); + + const parentHash = getParentHash(message); + if (parentHash) { + chatOptions.parent = parentHash; + } + + let partialDataText = ''; + const onData = (partialResponse: ChatResponse) => { + partialDataText = partialResponse.response.replace(/```\ncommitmsg/g, "```commitmsg"); + handlePartialData({ command: 'receiveMessagePartial', text: partialDataText!, user: partialResponse.user, date: partialResponse.date }); + }; + + const chatResponse = await devChat.chat(parsedMessage.text, chatOptions, onData); + await handleTopic(parentHash!, message, chatResponse); + const responseText = await handlerResponseText(partialDataText, chatResponse); + if (responseText === undefined) { + return; + } + + return { command: 'receiveMessage', text: responseText, hash: chatResponse['prompt-hash'], user: chatResponse.user, date: chatResponse.date, isError: chatResponse.isError }; +} + +export async function stopDevChatBase(message: any): Promise { + logger.channel()?.info(`Stopping devchat`); + userStop = true; + devChat.stop(); +} \ No newline at end of file diff --git a/src/handler/showDiff.ts b/src/handler/showDiff.ts index 84ce537..9fcb2c5 100644 --- a/src/handler/showDiff.ts +++ b/src/handler/showDiff.ts @@ -8,6 +8,7 @@ import { regInMessage, regOutMessage } from '../util/reg_messages'; import { applyCodeChanges, isValidActionString } from '../util/appyDiff'; import { logger } from '../util/logger'; import { FilePairManager } from '../util/diffFilePairs'; +import { UiUtilWrapper } from '../util/uiUtil'; @@ -64,7 +65,7 @@ export async function diffView(code: string, tarFile: string) { const tempFile = path.join(tempDir, fileName); // save code to temp file - await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFile), Buffer.from(code)); + await UiUtilWrapper.writeFile(tempFile, code); // open diff view FilePairManager.getInstance().addFilePair(curFile, tempFile); diff --git a/src/init/chatConfig.ts b/src/init/chatConfig.ts index 3681ce2..ba281c1 100644 --- a/src/init/chatConfig.ts +++ b/src/init/chatConfig.ts @@ -1,9 +1,9 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; -import * as ncp from 'ncp'; import { logger } from '../util/logger'; +import { UiUtilWrapper } from '../util/uiUtil'; function copyFileSync(source: string, target: string) { @@ -32,13 +32,11 @@ function copyFileSync(source: string, target: string) { } export function createChatDirectoryAndCopyInstructionsSync(extensionUri: vscode.Uri) { - - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders) { + const workspaceRoot = UiUtilWrapper.workspaceFoldersFirstPath(); + if (!workspaceRoot) { return; } - const workspaceRoot = workspaceFolders[0].uri.fsPath; const chatWorkflowsDirPath = path.join(workspaceRoot, '.chat', 'workflows'); const instructionsSrcPath = path.join(extensionUri.fsPath, 'workflows'); diff --git a/src/panel/chatPanel.ts b/src/panel/chatPanel.ts index b1325c3..6271587 100644 --- a/src/panel/chatPanel.ts +++ b/src/panel/chatPanel.ts @@ -9,6 +9,7 @@ import WebviewManager from './webviewManager'; import CustomCommands from '../command/customCommand'; import CommandManager from '../command/commandManager'; import { createChatDirectoryAndCopyInstructionsSync } from '../init/chatConfig'; +import { UiUtilWrapper } from '../util/uiUtil'; export default class ChatPanel { private static _instance: ChatPanel | undefined; @@ -20,7 +21,7 @@ export default class ChatPanel { // 创建 .chat 目录并复制 workflows createChatDirectoryAndCopyInstructionsSync(extensionUri); - const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath; + const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); if (workspaceDir) { const workflowsDir = path.join(workspaceDir!, '.chat', 'workflows'); CustomCommands.getInstance().parseCommands(workflowsDir); diff --git a/src/panel/devchatView.ts b/src/panel/devchatView.ts index 44b58a1..5ce44da 100644 --- a/src/panel/devchatView.ts +++ b/src/panel/devchatView.ts @@ -8,6 +8,7 @@ import { createChatDirectoryAndCopyInstructionsSync } from '../init/chatConfig'; import ExtensionContextHolder from '../util/extensionContext'; import CustomCommands from '../command/customCommand'; import { TopicManager } from '../topic/topicManager'; +import { UiUtilWrapper } from '../util/uiUtil'; export class DevChatViewProvider implements vscode.WebviewViewProvider { @@ -27,7 +28,7 @@ export class DevChatViewProvider implements vscode.WebviewViewProvider { // 创建 .chat 目录并复制 workflows createChatDirectoryAndCopyInstructionsSync(ExtensionContextHolder.context?.extensionUri!); - const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath; + const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); if (workspaceDir) { const workflowsDir = path.join(workspaceDir!, '.chat', 'workflows'); CustomCommands.getInstance().parseCommands(workflowsDir); diff --git a/src/panel/statusBarView.ts b/src/panel/statusBarView.ts new file mode 100644 index 0000000..ffa7a6d --- /dev/null +++ b/src/panel/statusBarView.ts @@ -0,0 +1,42 @@ +import * as vscode from 'vscode'; + +import { dependencyCheck } from './statusBarViewBase'; + + +export function createStatusBarItem(context: vscode.ExtensionContext): vscode.StatusBarItem { + const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); + + // Set the status bar item properties + statusBarItem.text = `$(warning)DevChat`; + statusBarItem.tooltip = 'DevChat checking ..., please wait.'; + statusBarItem.command = ''; + + // add a timer to update the status bar item + setInterval(async () => { + const [devchatStatus, apiKeyStatus] = await dependencyCheck(); + if (devchatStatus !== 'ready') { + statusBarItem.text = `$(warning)DevChat`; + statusBarItem.tooltip = `${devchatStatus}`; + statusBarItem.command = undefined; + // set statusBarItem warning color + return; + } + + if (apiKeyStatus !== 'ready') { + statusBarItem.text = `$(warning)DevChat`; + statusBarItem.tooltip = `${apiKeyStatus}`; + statusBarItem.command = 'DevChat.OPENAI_API_KEY'; + return; + } + + statusBarItem.text = `$(pass)DevChat`; + statusBarItem.tooltip = `ready to chat`; + statusBarItem.command = 'devcaht.onStatusBarClick'; + }, 3000); + + // Add the status bar item to the status bar + statusBarItem.show(); + + context.subscriptions.push(statusBarItem); + return statusBarItem; +} \ No newline at end of file diff --git a/src/panel/statusBarViewBase.ts b/src/panel/statusBarViewBase.ts new file mode 100644 index 0000000..cd99598 --- /dev/null +++ b/src/panel/statusBarViewBase.ts @@ -0,0 +1,81 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { logger } from "../util/logger"; + +import { UiUtilWrapper } from "../util/uiUtil"; +import { TopicManager } from "../topic/topicManager"; +import { checkDevChatDependency } from "../contributes/commandsBase"; +import { ApiKeyManager } from '../util/apiKey'; + + + +function getExtensionVersion(): string { + const packageJsonPath = path.join(UiUtilWrapper.extensionPath(), 'package.json'); + const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8'); + const packageJson = JSON.parse(packageJsonContent); + + return packageJson.version; +} + +let devchatStatus = ''; +let apiKeyStatus = ''; +let isVersionChangeCompare: boolean|undefined = undefined; +export async function dependencyCheck(): Promise<[string, string]> { + let versionChanged = false; + if (isVersionChangeCompare === undefined) { + const versionOld = await UiUtilWrapper.secretStorageGet("DevChatVersionOld"); + const versionNew = getExtensionVersion(); + const versionChanged = versionOld !== versionNew; + UiUtilWrapper.storeSecret("DevChatVersionOld", versionNew!); + + isVersionChangeCompare = true; + logger.channel()?.info(`versionOld: ${versionOld}, versionNew: ${versionNew}, versionChanged: ${versionChanged}`); + } + + + // status item has three status type + // 1. not in a folder + // 2. dependence is invalid + // 3. ready + if (devchatStatus === '' || devchatStatus === 'waiting install devchat') { + let bOk = true; + let devChat: string | undefined = UiUtilWrapper.getConfiguration('DevChat', 'DevChatPath'); + if (!devChat) { + bOk = false; + } + + if (!bOk) { + bOk = checkDevChatDependency(); + } + if (bOk && versionChanged) { + bOk = false; + } + + if (bOk) { + devchatStatus = 'ready'; + TopicManager.getInstance().loadTopics(); + } else { + if (devchatStatus === '') { + devchatStatus = 'not ready'; + } + } + } + if (devchatStatus === 'not ready') { + // auto install devchat + UiUtilWrapper.runTerminal('DevChat Install', `python ${UiUtilWrapper.extensionPath() + "/tools/install.py"}`); + devchatStatus = 'waiting install devchat'; + isVersionChangeCompare = true; + } + + // check api key + if (apiKeyStatus === '' || apiKeyStatus === 'please set api key') { + const bOk = await ApiKeyManager.getApiKey(); + if (bOk) { + apiKeyStatus = 'ready'; + } else { + apiKeyStatus = 'please set api key'; + } + } + + return [devchatStatus, apiKeyStatus]; +} \ No newline at end of file diff --git a/src/panel/topicView.ts b/src/panel/topicView.ts new file mode 100644 index 0000000..a07555a --- /dev/null +++ b/src/panel/topicView.ts @@ -0,0 +1,129 @@ +import * as vscode from 'vscode'; + +import { TopicManager, Topic } from '../topic/topicManager'; + + +export class TopicTreeItem extends vscode.TreeItem { + id: string; + date: number | undefined; + constructor(label: string, id: string, date: number | undefined, collapsibleState: vscode.TreeItemCollapsibleState) { + super(label, collapsibleState); + this.id = id; + this.date = date; + this.iconPath = new vscode.ThemeIcon('symbol-variable'); + this.contextValue = 'yourTreeItem'; + } + uncheck() { + this.iconPath = new vscode.ThemeIcon('symbol-variable'); + } + check() { + this.iconPath = new vscode.ThemeIcon('check'); + } +} + +export class TopicTreeDataProvider implements vscode.TreeDataProvider { + private static instance: TopicTreeDataProvider; + + public static getInstance(): TopicTreeDataProvider { + if (!TopicTreeDataProvider.instance) { + TopicTreeDataProvider.instance = new TopicTreeDataProvider(); + } + return TopicTreeDataProvider.instance; + } + + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + public selectedItem: TopicTreeItem | null = null; + private items: TopicTreeItem[] = []; + + // reg listeners to TopicManager in constructor + private constructor() { + TopicManager.getInstance().addOnCreateTopicListener(this.addItem.bind(this)); + TopicManager.getInstance().addOnDeleteTopicListener(this.onDeleteTopic.bind(this)); + TopicManager.getInstance().addOnReloadTopicsListener(this.onReloadTopics.bind(this)); + TopicManager.getInstance().addOnUpdateTopicListener(this.onUpdateTopics.bind(this)); + } + + // sort items + private sortItems() { + this.items.sort((a, b) => { + if (a.date && b.date) { + return b.date - a.date; + } else if (!a.date) { + return -1; + } else if (!b.date) { + return 1; + } else { + return 0; + } + }); + } + + onUpdateTopics(topicId: string) { + const items = this.items.filter(i => i.id === topicId); + const topic = TopicManager.getInstance().getTopic(topicId); + items.map((item) => { + item.label = topic?.name; + item.date = topic?.lastUpdated; + }); + this.sortItems(); + this._onDidChangeTreeData.fire(); + } + + onReloadTopics(topics: Topic[]) { + const items = topics.map((topic) => { + return new TopicTreeItem(topic.name ? topic.name : "new topic", topic.topicId, topic.lastUpdated, vscode.TreeItemCollapsibleState.None); + }); + this.items = items; + this.sortItems(); + this._onDidChangeTreeData.fire(); + } + + onDeleteTopic(topicId: string) { + this.items = this.items.filter(i => i.id !== topicId); + this.sortItems(); + this._onDidChangeTreeData.fire(); + } + + setSelectedItem(item: TopicTreeItem): void { + this.items.map((item) => { + item.uncheck(); + }); + item.check(); + this.selectedItem = item; + this._onDidChangeTreeData.fire(); + } + + getChildren(element?: TopicTreeItem): vscode.ProviderResult { + return this.items; + } + + getTreeItem(element: TopicTreeItem): vscode.TreeItem | Thenable { + element.command = { + title: 'Select Item', + command: 'devchat-topicview.selectTopic', + arguments: [element], + }; + return element; + } + + reload(): void { + const topicList = TopicManager.getInstance().getTopicList(); + this.onReloadTopics(topicList); + } + + addItem(topic: Topic): void { + const newItem = new TopicTreeItem(topic.name ? topic.name : "new topic", topic.topicId, topic.lastUpdated, vscode.TreeItemCollapsibleState.None); + this.items.push(newItem); + this.sortItems(); + this._onDidChangeTreeData.fire(); + } + + + deleteItem(item: TopicTreeItem): void { + this.items = this.items.filter(i => i !== item); + this.sortItems(); + this._onDidChangeTreeData.fire(); + } +} \ No newline at end of file diff --git a/src/toolwrapper/devchat.ts b/src/toolwrapper/devchat.ts index aca9dd6..f458f6b 100644 --- a/src/toolwrapper/devchat.ts +++ b/src/toolwrapper/devchat.ts @@ -1,5 +1,4 @@ // devchat.ts -import * as vscode from 'vscode'; import * as dotenv from 'dotenv'; import * as path from 'path'; import * as fs from 'fs'; @@ -7,6 +6,8 @@ import * as fs from 'fs'; import { logger } from '../util/logger'; import { CommandRun } from "../util/commonUtil"; import ExtensionContextHolder from '../util/extensionContext'; +import { UiUtilWrapper } from '../util/uiUtil'; +import { ApiKeyManager } from '../util/apiKey'; @@ -84,18 +85,6 @@ class DevChat { return args; } - async getOpenaiApiKey(): Promise { - const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets; - let openaiApiKey = await secretStorage.get("devchat_OPENAI_API_KEY"); - if (!openaiApiKey) { - openaiApiKey = vscode.workspace.getConfiguration('DevChat').get('API_KEY'); - } - if (!openaiApiKey) { - openaiApiKey = process.env.OPENAI_API_KEY; - } - return openaiApiKey; - } - private parseOutData(stdout: string, isPartial: boolean): ChatResponse { const responseLines = stdout.trim().split("\n"); @@ -148,15 +137,7 @@ class DevChat { } apiEndpoint(apiKey: string | undefined): any { - let openAiApiBase: string | undefined = undefined; - if (apiKey?.startsWith("DC.")) { - // TODO add devchat proxy - openAiApiBase = "https://xw4ymuy6qj.ap-southeast-1.awsapprunner.com/api/v1"; - } - - if (vscode.workspace.getConfiguration('DevChat').get('API_ENDPOINT')) { - openAiApiBase = vscode.workspace.getConfiguration('DevChat').get('API_ENDPOINT'); - } + const openAiApiBase = ApiKeyManager.getEndPoint(apiKey); const openAiApiBaseObject = openAiApiBase ? { OPENAI_API_BASE: openAiApiBase } : {}; return openAiApiBaseObject; @@ -166,8 +147,8 @@ class DevChat { const args = await this.buildArgs(options); args.push(content); - const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath; - let openaiApiKey = await this.getOpenaiApiKey(); + const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); + let openaiApiKey = await ApiKeyManager.getApiKey(); if (!openaiApiKey) { logger.channel()?.error('OpenAI key is invalid!'); logger.channel()?.show(); @@ -177,13 +158,13 @@ class DevChat { // 如果配置了devchat的TOKEN,那么就需要使用默认的代理 let openAiApiBaseObject = this.apiEndpoint(openaiApiKey); - const openaiModel = vscode.workspace.getConfiguration('DevChat').get('OpenAI.model'); - const openaiTemperature = vscode.workspace.getConfiguration('DevChat').get('OpenAI.temperature'); - const openaiStream = vscode.workspace.getConfiguration('DevChat').get('OpenAI.stream'); - const llmModel = vscode.workspace.getConfiguration('DevChat').get('llmModel'); - const tokensPerPrompt = vscode.workspace.getConfiguration('DevChat').get('OpenAI.tokensPerPrompt'); + const openaiModel = UiUtilWrapper.getConfiguration('DevChat', 'OpenAI.model'); + const openaiTemperature = UiUtilWrapper.getConfiguration('DevChat', 'OpenAI.temperature'); + const openaiStream = UiUtilWrapper.getConfiguration('DevChat', 'OpenAI.stream'); + const llmModel = UiUtilWrapper.getConfiguration('DevChat', 'llmModel'); + const tokensPerPrompt = UiUtilWrapper.getConfiguration('DevChat', 'OpenAI.tokensPerPrompt'); - let devChat: string | undefined = vscode.workspace.getConfiguration('DevChat').get('DevChatPath'); + let devChat: string | undefined = UiUtilWrapper.getConfiguration('DevChat', 'DevChatPath'); if (!devChat) { devChat = 'devchat'; } @@ -227,12 +208,11 @@ class DevChat { const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(devChat, args, spawnAsyncOptions, onStdoutPartial, undefined, undefined, undefined); if (stderr) { - const errorMessage = stderr.trim().match(/Error:(.+)/)?.[1]; return { "prompt-hash": "", user: "", date: "", - response: errorMessage ? `Error: ${errorMessage}` : "Unknown error", + response: stderr, isError: true, }; } @@ -253,7 +233,7 @@ class DevChat { async log(options: LogOptions = {}): Promise { const args = this.buildLogArgs(options); const devChat = this.getDevChatPath(); - const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath; + const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); const openaiApiKey = process.env.OPENAI_API_KEY; logger.channel()?.info(`Running devchat with args: ${args.join(" ")}`); @@ -286,7 +266,7 @@ class DevChat { if (options.maxCount) { args.push('--max-count', `${options.maxCount}`); } else { - const maxLogCount = vscode.workspace.getConfiguration('DevChat').get('maxLogCount'); + const maxLogCount = UiUtilWrapper.getConfiguration('DevChat', 'maxLogCount'); args.push('--max-count', `${maxLogCount}`); } @@ -294,7 +274,7 @@ class DevChat { } private getDevChatPath(): string { - let devChat: string | undefined = vscode.workspace.getConfiguration('DevChat').get('DevChatPath'); + let devChat: string | undefined = UiUtilWrapper.getConfiguration('DevChat', 'DevChatPath'); if (!devChat) { devChat = 'devchat'; } diff --git a/src/toolwrapper/dtm.ts b/src/toolwrapper/dtm.ts index b266acc..6d0a277 100644 --- a/src/toolwrapper/dtm.ts +++ b/src/toolwrapper/dtm.ts @@ -1,10 +1,10 @@ import { spawn } from "child_process"; -import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; import { logger } from "../util/logger"; import { CommandRun } from "../util/commonUtil"; +import { UiUtilWrapper } from "../util/uiUtil"; interface DtmResponse { status: number; @@ -17,7 +17,7 @@ class DtmWrapper { private commandRun: CommandRun; constructor() { - this.workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath || '.'; + this.workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath() || '.'; this.commandRun = new CommandRun(); } diff --git a/src/topic/topicManager.ts b/src/topic/topicManager.ts index e400194..22bf6a8 100644 --- a/src/topic/topicManager.ts +++ b/src/topic/topicManager.ts @@ -1,11 +1,11 @@ import { v4 as uuidv4 } from 'uuid'; -import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; import DevChat, { LogEntry, LogOptions } from '../toolwrapper/devchat'; import { loadTopicList } from './loadTopics'; +import { UiUtilWrapper } from '../util/uiUtil'; export class Topic { name: string | undefined; @@ -194,7 +194,7 @@ export class TopicManager { if (topic.firstMessageHash) { // get ${WORKSPACE_ROOT}/.chat/.deletedTopics - const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath; + const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); const deletedTopicsPath = path.join(workspaceDir!, '.chat', '.deletedTopics'); // read ${WORKSPACE_ROOT}/.chat/.deletedTopics as String[] @@ -218,7 +218,7 @@ export class TopicManager { } isDeleteTopic(topicId: string) { - const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath; + const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); const deletedTopicsPath = path.join(workspaceDir!, '.chat', '.deletedTopics'); if (!fs.existsSync(deletedTopicsPath)) { diff --git a/src/util/apiKey.ts b/src/util/apiKey.ts new file mode 100644 index 0000000..7f4be65 --- /dev/null +++ b/src/util/apiKey.ts @@ -0,0 +1,41 @@ +// src/apiKey.ts + +import { UiUtilWrapper } from './uiUtil'; + +export class ApiKeyManager { + static async getApiKey(): Promise { + let apiKey = await UiUtilWrapper.secretStorageGet("devchat_OPENAI_API_KEY"); + if (!apiKey) { + apiKey = UiUtilWrapper.getConfiguration('DevChat', 'API_KEY'); + } + if (!apiKey) { + apiKey = process.env.OPENAI_API_KEY; + } + return apiKey; + } + + static getKeyType(apiKey: string): string | undefined { + if (apiKey.startsWith("sk.")) { + return "sk"; + } else if (apiKey.startsWith("DC.")) { + return "DC"; + } else { + return undefined; + } + } + + static async writeApiKeySecret(apiKey: string): Promise { + await UiUtilWrapper.storeSecret("devchat_OPENAI_API_KEY", apiKey); + } + + static getEndPoint(apiKey: string | undefined): string | undefined { + let endPoint = UiUtilWrapper.getConfiguration('DevChat', 'API_ENDPOINT'); + if (!endPoint) { + endPoint = process.env.OPENAI_API_BASE; + } + if (!endPoint && apiKey?.startsWith("DC.")) { + endPoint = "https://xw4ymuy6qj.ap-southeast-1.awsapprunner.com/api/v1"; + } + return endPoint; + } +} \ No newline at end of file diff --git a/src/util/appyDiff.ts b/src/util/appyDiff.ts index 02bc9e6..e2c01ba 100644 --- a/src/util/appyDiff.ts +++ b/src/util/appyDiff.ts @@ -19,9 +19,10 @@ function findMatchingIndex(list1: string[], list2: string[]): number[] { let isMatch = true; for (let j = 0; j < list2.length; j++) { if (list1[i + j].trim() !== list2[j].trim()) { - if (j > 0) { + + //if (j > 0) { logger.channel()?.info(`findMatchingIndex end at ${j} ${list1[i + j].trim()} != ${list2[j].trim()}`); - } + //} isMatch = false; break; diff --git a/src/util/commonUtil.ts b/src/util/commonUtil.ts index 7655d8a..d6285a3 100644 --- a/src/util/commonUtil.ts +++ b/src/util/commonUtil.ts @@ -1,11 +1,13 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import * as vscode from 'vscode'; +import * as childProcess from 'child_process'; + import { parseArgsStringToArgv } from 'string-argv'; import { logger } from './logger'; import { spawn, exec } from 'child_process'; +import { UiUtilWrapper } from './uiUtil'; export function createTempSubdirectory(subdir: string): string { // 获取系统临时目录 @@ -77,7 +79,7 @@ export class CommandRun { if (code === 0) { resolve({ exitCode: code, stdout, stderr }); } else { - reject({ exitCode: code, stdout, stderr }); + resolve({ exitCode: code, stdout, stderr }); } }); @@ -90,7 +92,7 @@ export class CommandRun { logger.channel()?.error(`Error occurred: ${error.message}`); logger.channel()?.show(); } - reject({ exitCode: error.code, stdout: "", stderr: error.message }); + resolve({ exitCode: error.code, stdout: "", stderr: error.message }); }); }); }; @@ -110,7 +112,7 @@ export async function runCommandAndWriteOutput( ): Promise { const run = new CommandRun(); const options = { - cwd: vscode.workspace.workspaceFolders?.[0].uri.fsPath || '.', + cwd: UiUtilWrapper.workspaceFoldersFirstPath() || '.', }; return run.spawnAsync(command, args, options, undefined, undefined, undefined, outputFile); @@ -122,7 +124,7 @@ export async function runCommandStringAndWriteOutput( ): Promise { const run = new CommandRun(); const options = { - cwd: vscode.workspace.workspaceFolders?.[0].uri.fsPath || '.' + cwd: UiUtilWrapper.workspaceFoldersFirstPath() || '.' }; // Split the commandString into command and args array using string-argv @@ -143,13 +145,14 @@ export async function runCommandStringAndWriteOutput( export async function getLanguageIdByFileName(fileName: string): Promise { try { - // 打开指定的文件 - const document = await vscode.workspace.openTextDocument(fileName); - // 获取文件的语言标识符 - const languageId = document.languageId; + const languageId = await UiUtilWrapper.languageId(fileName); return languageId; } catch (error) { // 如果无法打开文件或发生其他错误,返回undefined return undefined; } +} + +export function runCommand(command: string): string { + return childProcess.execSync(command).toString(); } \ No newline at end of file diff --git a/src/util/logger.ts b/src/util/logger.ts index 3530271..b8027c1 100644 --- a/src/util/logger.ts +++ b/src/util/logger.ts @@ -1,12 +1,19 @@ -import * as vscode from 'vscode' + +export interface LogChannel { + info(message: string, ...args: any[]): void; + warn(message: string, ...args: any[]): void; + error(message: string | Error, ...args: any[]): void; + debug(message: string, ...args: any[]): void; + show(): void; +} export class logger { - private static _channel: vscode.LogOutputChannel | undefined; - public static init(context: vscode.ExtensionContext): void { - this._channel = vscode.window.createOutputChannel('DevChat', { log: true }); + private static _channel: LogChannel | undefined; + public static init(channel: LogChannel): void { + this._channel = channel; } - public static channel(): vscode.LogOutputChannel | undefined { + public static channel(): LogChannel | undefined { return this._channel; } } diff --git a/src/util/logger_vscode.ts b/src/util/logger_vscode.ts new file mode 100644 index 0000000..caca8f9 --- /dev/null +++ b/src/util/logger_vscode.ts @@ -0,0 +1,39 @@ +import { LogChannel } from "./logger"; +import * as vscode from 'vscode'; + +export class LoggerChannelVscode implements LogChannel { + _channel: vscode.LogOutputChannel; + + private static _instance: LoggerChannelVscode; + + private constructor() { + this._channel = vscode.window.createOutputChannel('DevChat', { log: true }); + } + + public static getInstance(): LoggerChannelVscode { + if (!this._instance) { + this._instance = new LoggerChannelVscode(); + } + return this._instance; + } + + info(message: string, ...args: any[]): void { + this._channel.info(message, ...args); + } + + warn(message: string, ...args: any[]): void { + this._channel.warn(message, ...args); + } + + error(message: string | Error, ...args: any[]): void { + this._channel.error(message, ...args); + } + + debug(message: string, ...args: any[]): void { + this._channel.debug(message, ...args); + } + + show(): void { + this._channel.show(); + } +} \ No newline at end of file diff --git a/src/util/messageHistory.ts b/src/util/messageHistory.ts index 2f9fa07..6bd0d08 100644 --- a/src/util/messageHistory.ts +++ b/src/util/messageHistory.ts @@ -1,6 +1,5 @@ -import * as vscode from 'vscode'; -class MessageHistory { +export class MessageHistory { private history: any[]; private lastmessage: any | null; @@ -21,10 +20,6 @@ class MessageHistory { return this.lastmessage; } - remove() { - return; - } - clear() { this.history = []; this.lastmessage = null; diff --git a/src/util/uiUtil.ts b/src/util/uiUtil.ts new file mode 100644 index 0000000..12e1a7a --- /dev/null +++ b/src/util/uiUtil.ts @@ -0,0 +1,48 @@ + +export interface UiUtil { + languageId(uri: string): Promise; + workspaceFoldersFirstPath(): string | undefined; + getConfiguration(key1: string, key2: string): string | undefined; + secretStorageGet(key: string): Promise; + writeFile(uri: string, content: string): Promise; + showInputBox(option: object): Promise; + storeSecret(key: string, value: string): Promise; + extensionPath(): string; + runTerminal(terminalName:string, command: string): void; +} + +export class UiUtilWrapper { + private static _uiUtil: UiUtil | undefined; + public static init(uiUtil: UiUtil): void { + this._uiUtil = uiUtil; + } + + public static async languageId(uri: string): Promise { + return await this._uiUtil?.languageId(uri); + } + public static workspaceFoldersFirstPath(): string | undefined { + return this._uiUtil?.workspaceFoldersFirstPath(); + } + public static getConfiguration(key1: string, key2: string): string | undefined { + return this._uiUtil?.getConfiguration(key1, key2); + } + public static async secretStorageGet(key: string): Promise { + return await this._uiUtil?.secretStorageGet(key); + } + public static async writeFile(uri: string, content: string): Promise { + return await this._uiUtil?.writeFile(uri, content); + } + public static async showInputBox(option: object): Promise { + return await this._uiUtil?.showInputBox(option); + } + public static async storeSecret(key: string, value: string): Promise { + return await this._uiUtil?.storeSecret(key, value); + } + public static extensionPath(): string { + return this._uiUtil?.extensionPath()!; + } + public static runTerminal(terminalName: string, command: string): void { + this._uiUtil?.runTerminal(terminalName, command); + } +} + diff --git a/src/util/uiUtil_vscode.ts b/src/util/uiUtil_vscode.ts new file mode 100644 index 0000000..2d4b453 --- /dev/null +++ b/src/util/uiUtil_vscode.ts @@ -0,0 +1,42 @@ +import * as vscode from 'vscode'; + +import ExtensionContextHolder from './extensionContext'; +import { UiUtil } from './uiUtil'; + + +export class UiUtilVscode implements UiUtil { + public async languageId(uri: string): Promise { + const document = await vscode.workspace.openTextDocument(uri); + return document.languageId; + } + public workspaceFoldersFirstPath(): string | undefined { + return vscode.workspace.workspaceFolders?.[0].uri.fsPath; + } + + public getConfiguration(key1: string, key2: string): string | undefined { + return vscode.workspace.getConfiguration(key1).get(key2); + } + public async secretStorageGet(key: string): Promise { + const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets; + let openaiApiKey = await secretStorage.get(key); + return openaiApiKey; + } + public async writeFile(uri: string, content: string): Promise { + await vscode.workspace.fs.writeFile(vscode.Uri.file(uri), Buffer.from(content)); + } + public async showInputBox(option: object): Promise { + return vscode.window.showInputBox(option); + } + public async storeSecret(key: string, value: string): Promise { + const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets; + await secretStorage.store(key, value); + } + public extensionPath(): string { + return ExtensionContextHolder.context!.extensionUri.fsPath; + } + public runTerminal(terminalName: string, command: string): void { + const terminal = vscode.window.createTerminal(terminalName); + terminal.sendText(command); + terminal.show(); + } +} diff --git a/test/command/commandManager.test.ts b/test/command/commandManager.test.ts new file mode 100644 index 0000000..92ac69d --- /dev/null +++ b/test/command/commandManager.test.ts @@ -0,0 +1,76 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import CommandManager, { Command } from '../../src/command/commandManager'; +import CustomCommands, { Command as CCommand} from '../../src/command/customCommand'; + +describe('CommandManager', () => { + let commandManager: CommandManager; + + beforeEach(() => { + commandManager = CommandManager.getInstance(); + }); + + afterEach(() => { + // Reset the command list after each test + commandManager['commands'] = []; + }); + + it('should register a command', () => { + const command: Command = { + name: 'test', + pattern: 'test', + description: 'Test command', + handler: async (commandName: string, userInput: string) => { + return 'Test result'; + }, + }; + + commandManager.registerCommand(command); + expect(commandManager['commands']).to.include(command); + }); + + it('should return the command list', () => { + const command: Command = { + name: 'test', + pattern: 'test', + description: 'Test command', + handler: async (commandName: string, userInput: string) => { + return 'Test result'; + }, + }; + + commandManager.registerCommand(command); + expect(commandManager.getCommandList()).to.include(command); + }); + + it('should process text with a command', async () => { + const command: Command = { + name: 'test', + pattern: 'test', + description: 'Test command', + handler: async (commandName: string, userInput: string) => { + return 'Test result'; + }, + }; + + commandManager.registerCommand(command); + const result = await commandManager.processText('/test'); + expect(result).to.equal('Test result'); + }); + + it('should process text with a custom command', async () => { + const customCommand: CCommand = { + name: 'customTest', + pattern: 'customTest', + description: 'Custom test command', + message: 'Custom test result', + show: true, + default: false, + instructions: [] + }; + + CustomCommands.getInstance().regCommand(customCommand); + const result = await commandManager.processText('/customTest'); + expect(result).to.equal(' Custom test result'); + }); +}); \ No newline at end of file diff --git a/test/command/customCommand.test.ts b/test/command/customCommand.test.ts new file mode 100644 index 0000000..b0afdc6 --- /dev/null +++ b/test/command/customCommand.test.ts @@ -0,0 +1,106 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import mockFs from 'mock-fs'; +import * as fs from 'fs'; +import * as path from 'path'; +import CustomCommands, { Command } from '../../src/command/customCommand'; + + +describe('CustomCommands', () => { + let customCommands: CustomCommands; + + beforeEach(() => { + customCommands = CustomCommands.getInstance(); + }); + + afterEach(() => { + // Reset the command list after each test + customCommands['commands'] = []; + mockFs.restore(); + }); + + it('should parse commands from workflows directory', () => { + // Mock the file system with two directories, one with _setting_.json and one without + mockFs({ + 'workflows': { + 'command1': { + '_setting_.json': JSON.stringify({ + pattern: 'command1', + description: 'Command 1', + message: 'Command 1 message', + default: false, + show: true, + instructions: ['instruction1', 'instruction2'], + }), + }, + 'command2': { + // No _setting_.json file + }, + }, + }); + + const workflowsDir = path.join(process.cwd(), 'workflows'); + customCommands.parseCommands(workflowsDir); + + const expectedResult: Command[] = [ + { + name: 'command1', + pattern: 'command1', + description: 'Command 1', + message: 'Command 1 message', + default: false, + show: true, + instructions: ['instruction1', 'instruction2'], + }, + ]; + + expect(customCommands['commands']).to.deep.equal(expectedResult); + }); + + it('should register a custom command', () => { + const command: Command = { + name: 'test', + pattern: 'test', + description: 'Test command', + message: 'Test message', + default: false, + show: true, + instructions: ['instruction1', 'instruction2'], + }; + + customCommands.regCommand(command); + expect(customCommands['commands']).to.include(command); + }); + + it('should get a custom command by name', () => { + const command: Command = { + name: 'test', + pattern: 'test', + description: 'Test command', + message: 'Test message', + default: false, + show: true, + instructions: ['instruction1', 'instruction2'], + }; + + customCommands.regCommand(command); + const foundCommand = customCommands.getCommand('test'); + expect(foundCommand).to.deep.equal(command); + }); + + it('should handle a custom command', () => { + const command: Command = { + name: 'test', + pattern: 'test', + description: 'Test command', + message: 'Test message', + default: false, + show: true, + instructions: ['instruction1', 'instruction2'], + }; + + customCommands.regCommand(command); + const result = customCommands.handleCommand('test'); + expect(result).to.equal('[instruction|./.chat/workflows/test/instruction1] [instruction|./.chat/workflows/test/instruction2] Test message'); + }); +}); \ No newline at end of file diff --git a/test/context/contextCodeSelected.test.ts b/test/context/contextCodeSelected.test.ts new file mode 100644 index 0000000..751785a --- /dev/null +++ b/test/context/contextCodeSelected.test.ts @@ -0,0 +1,43 @@ +import { expect } from 'chai'; +import { describe, it, afterEach, beforeEach } from 'mocha'; +import { handleCodeSelected } from '../../src/context/contextCodeSelected'; +import * as path from 'path'; +import { UiUtilWrapper } from '../../src/util/uiUtil'; +import sinon from 'sinon'; + +describe('handleCodeSelected', () => { + let languageIdStub: sinon.SinonStub; + let workspaceFoldersFirstPathStub: sinon.SinonStub; + let writeFileStub: sinon.SinonStub; + + beforeEach(() => { + // Mock UiUtilWrapper functions + languageIdStub = sinon.stub(UiUtilWrapper, 'languageId').resolves('typescript'); + workspaceFoldersFirstPathStub = sinon.stub(UiUtilWrapper, 'workspaceFoldersFirstPath').returns('test'); + writeFileStub = sinon.stub(UiUtilWrapper, 'writeFile').resolves(); + }); + + afterEach(() => { + // Restore the original functions after each test + languageIdStub.restore(); + workspaceFoldersFirstPathStub.restore(); + writeFileStub.restore(); + }); + + it('should create a context file with the correct content', async () => { + const fileSelected = path.join(__dirname, 'testFile.ts'); + const codeSelected = 'console.log("Hello, world!");'; + + const contextFile = await handleCodeSelected(fileSelected, codeSelected); + + // Check if the mocked functions were called with the correct arguments + expect(languageIdStub.calledWith(fileSelected)).to.be.true; + expect(workspaceFoldersFirstPathStub.called).to.be.true; + expect(writeFileStub.called).to.be.true; + + // Extract the temp file path from the context string + const tempFilePath = contextFile.match(/\[context\|(.*?)\]/)?.[1]; + + expect(tempFilePath).to.not.be.undefined; + }); +}); \ No newline at end of file diff --git a/test/context/loadContexts.test.ts b/test/context/loadContexts.test.ts new file mode 100644 index 0000000..223a97d --- /dev/null +++ b/test/context/loadContexts.test.ts @@ -0,0 +1,18 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import '../../src/context/loadContexts'; +import ChatContextManager from '../../src/context/contextManager'; +import { gitDiffCachedContext } from '../../src/context/contextGitDiffCached'; +import { gitDiffContext } from '../../src/context/contextGitDiff'; +import { customCommandContext } from '../../src/context/contextCustomCommand'; + +describe('loadContexts', () => { + it('should register all contexts', () => { + const chatContextManager = ChatContextManager.getInstance(); + const contextList = chatContextManager.getContextList(); + + expect(contextList).to.include(gitDiffCachedContext); + expect(contextList).to.include(gitDiffContext); + expect(contextList).to.include(customCommandContext); + }); +}); \ No newline at end of file diff --git a/test/contributes/commandsBase.test.ts b/test/contributes/commandsBase.test.ts new file mode 100644 index 0000000..8e16ab3 --- /dev/null +++ b/test/contributes/commandsBase.test.ts @@ -0,0 +1,84 @@ +// test/commandsBase.test.ts + +import { expect } from 'chai'; +import * as commonUtil from '../../src/util/commonUtil'; +import * as commandsBase from '../../src/contributes/commandsBase'; +import sinon from 'sinon'; + +describe('commandsBase', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('checkDevChatDependency', () => { + it('should return true if DevChat is installed', () => { + sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => { + if (command === 'python3 -m pipx environment') { + return 'PIPX_BIN_DIR=/path/to/bin'; + } else if (command === 'devchat --help') { + return 'DevChat help text'; + } + return ''; + }); + + const result = commandsBase.checkDevChatDependency(); + expect(result).to.be.true; + }); + + it('should return false if DevChat is not installed', () => { + sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => { + if (command === 'python3 -m pipx environment') { + return 'PIPX_BIN_DIR=/path/to/bin'; + } else if (command === 'devchat --help') { + throw new Error('Command not found'); + } + return ''; + }); + + const result = commandsBase.checkDevChatDependency(); + expect(result).to.be.false; + }); + + it('should return false if pipx environment is not found', () => { + sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => { + if (command === 'python3 -m pipx environment') { + return 'No pipx environment found'; + } + return ''; + }); + + const result = commandsBase.checkDevChatDependency(); + expect(result).to.be.false; + }); + }); + + describe('getPipxEnvironmentPath', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should return the pipx environment path if found', () => { + sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => { + if (command === 'python3 -m pipx environment') { + return 'PIPX_BIN_DIR=/path/to/bin'; + } + return ''; + }); + + const result = commandsBase.getPipxEnvironmentPath(); + expect(result).to.equal('/path/to/bin'); + }); + + it('should return null if pipx environment path is not found', () => { + sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => { + if (command === 'python3 -m pipx environment') { + return 'No pipx environment found'; + } + return ''; + }); + + const result = commandsBase.getPipxEnvironmentPath(); + expect(result).to.be.null; + }); + }); +}); \ No newline at end of file diff --git a/test/handler/sendMessageBase.test.ts b/test/handler/sendMessageBase.test.ts new file mode 100644 index 0000000..d6ef5e9 --- /dev/null +++ b/test/handler/sendMessageBase.test.ts @@ -0,0 +1,309 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { Context } from 'mocha'; +import sinon from 'sinon'; +import { parseMessage, getInstructionFiles, parseMessageAndSetOptions, getParentHash, handleTopic, handlerResponseText, sendMessageBase, stopDevChatBase } from '../../src/handler/sendMessageBase'; +import DevChat, { ChatResponse } from '../../src/toolwrapper/devchat'; +import CommandManager from '../../src/command/commandManager'; +import messageHistory from '../../src/util/messageHistory'; +import { TopicManager } from '../../src/topic/topicManager'; +import CustomCommands from '../../src/command/customCommand'; +import { UiUtilWrapper } from '../../src/util/uiUtil'; + + +describe('sendMessageBase', () => { + let workspaceFoldersFirstPathStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + + beforeEach(() => { + workspaceFoldersFirstPathStub = sinon.stub(UiUtilWrapper, 'workspaceFoldersFirstPath'); + getConfigurationStub = sinon.stub(UiUtilWrapper, 'getConfiguration'); + }); + + afterEach(() => { + workspaceFoldersFirstPathStub.restore(); + getConfigurationStub.restore(); + }); + + describe('parseMessage', () => { + it('should parse message correctly', () => { + const message = '[context|path/to/context] [instruction|path/to/instruction] [reference|path/to/reference] Hello, world!'; + const result = parseMessage(message); + + expect(result.context).to.deep.equal(['path/to/context']); + expect(result.instruction).to.deep.equal(['path/to/instruction']); + expect(result.reference).to.deep.equal(['path/to/reference']); + expect(result.text).to.equal('Hello, world!'); + }); + }); + + describe('getInstructionFiles', () => { + it('should return instruction files', () => { + const result = getInstructionFiles(); + expect(result).to.be.an('array'); + }); + }); + + describe('parseMessageAndSetOptions', () => { + it('should parse message and set options correctly', async () => { + const message = { + text: '[context|path/to/context] [instruction|path/to/instruction] [reference|path/to/reference] Hello, world!' + }; + const chatOptions: any = {}; + + const result = await parseMessageAndSetOptions(message, chatOptions); + + expect(result.context).to.deep.equal(['path/to/context']); + expect(result.instruction).to.deep.equal(['path/to/instruction']); + expect(result.reference).to.deep.equal(['path/to/reference']); + expect(result.text).to.equal('Hello, world!'); + expect(chatOptions.context).to.deep.equal(['path/to/context']); + expect(chatOptions.header).to.deep.equal(['path/to/instruction']); + expect(chatOptions.reference).to.deep.equal(['path/to/reference']); + }); + }); + + describe('getParentHash', () => { + beforeEach(() => { + messageHistory.clear(); + }); + + it('should return parent hash when message hash is provided and found in history', () => { + const message1 = { + hash: 'somehash1', + parentHash: 'parentHash1' + }; + const message2 = { + hash: 'somehash2', + parentHash: 'parentHash2' + }; + messageHistory.add(message1); + messageHistory.add(message2); + + const message = { + hash: 'somehash1' + }; + + const result = getParentHash(message); + expect(result).to.equal('parentHash1'); + }); + + it('should return undefined when message hash is provided but not found in history', () => { + const message1 = { + hash: 'somehash1', + parentHash: 'parentHash1' + }; + const message2 = { + hash: 'somehash2', + parentHash: 'parentHash2' + }; + messageHistory.add(message1); + messageHistory.add(message2); + + const message = { + hash: 'nonexistenthash' + }; + + const result = getParentHash(message); + expect(result).to.be.undefined; + }); + + it('should return last message hash when message hash is not provided', () => { + const message1 = { + hash: 'somehash1', + parentHash: 'parentHash1' + }; + const message2 = { + hash: 'somehash2', + parentHash: 'parentHash2' + }; + messageHistory.add(message1); + messageHistory.add(message2); + + const message = {}; + + const result = getParentHash(message); + expect(result).to.equal('somehash2'); + }); + + it('should return undefined when message hash is not provided and history is empty', () => { + const message = {}; + + const result = getParentHash(message); + expect(result).to.be.undefined; + }); + }); + + describe('handleTopic', () => { + it('should handle topic correctly', async () => { + const parentHash = 'somehash'; + const message = { + text: 'Hello, world!' + }; + const chatResponse: ChatResponse = { + response: 'Hello, user!', + isError: false, + user: 'user', + date: '2022-01-01T00:00:00.000Z', + 'prompt-hash': 'responsehash' + }; + + await handleTopic(parentHash, message, chatResponse); + // Check if the topic was updated correctly + }); + }); + + describe('handlerResponseText', () => { + it('should handle response text correctly when isError is false', async () => { + const partialDataText = 'Partial data'; + const chatResponse: ChatResponse = { + response: 'Hello, user!', + isError: false, + user: 'user', + date: '2022-01-01T00:00:00.000Z', + 'prompt-hash': 'responsehash' + }; + + const result = await handlerResponseText(partialDataText, chatResponse); + expect(result).to.equal('Hello, user!'); + }); + + it('should handle response text correctly when isError is true', async () => { + const partialDataText = 'Partial data'; + const chatResponse: ChatResponse = { + response: 'Error occurred!', + isError: true, + user: 'user', + date: '2022-01-01T00:00:00.000Z', + 'prompt-hash': 'responsehash' + }; + + const result = await handlerResponseText(partialDataText, chatResponse); + expect(result).to.equal('Partial data\n\nError occurred!'); + }); + }); + + describe('sendMessageBase', async () => { + it('should send message correct with openai api key', async () => { + const message = { + text: 'Hello, world!' + }; + const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => { + // Handle partial data + }; + + workspaceFoldersFirstPathStub.returns('./'); + + getConfigurationStub.withArgs('DevChat', 'API_KEY').returns('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'); + getConfigurationStub.withArgs('DevChat', 'OpenAI.model').returns('gpt-4'); + getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns(0); + getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true'); + getConfigurationStub.withArgs('DevChat', 'llmModel').returns('OpenAI'); + getConfigurationStub.withArgs('DevChat', 'OpenAI.tokensPerPrompt').returns(9000); + + const result = await sendMessageBase(message, handlePartialData); + expect(result).to.be.an('object'); + expect(result!.command).to.equal('receiveMessage'); + expect(result!.text).to.be.a('string'); + expect(result!.hash).to.be.a('string'); + expect(result!.user).to.be.a('string'); + expect(result!.date).to.be.a('string'); + expect(result!.isError).to.be.false; + }).timeout(10000); + + it('should send message correct with DevChat access key', async () => { + const message = { + text: 'Hello, world!' + }; + const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => { + // Handle partial data + }; + + workspaceFoldersFirstPathStub.returns('./'); + + getConfigurationStub.withArgs('DevChat', 'API_KEY').returns('DC.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOjY2MTI0NDU1ODE2LCJqdGkiOjcyMzc4ODIzMjI3Mjc4MzI2NTJ9.qGWJ_NyWjjj66oa5mbfi3Hjawe-Yp8syCDLkpyu4yS0'); + getConfigurationStub.withArgs('DevChat', 'OpenAI.model').returns('gpt-4'); + getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns(0); + getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true'); + getConfigurationStub.withArgs('DevChat', 'llmModel').returns('OpenAI'); + getConfigurationStub.withArgs('DevChat', 'OpenAI.tokensPerPrompt').returns(9000); + + const result = await sendMessageBase(message, handlePartialData); + expect(result).to.be.an('object'); + expect(result!.command).to.equal('receiveMessage'); + expect(result!.text).to.be.a('string'); + expect(result!.hash).to.be.a('string'); + expect(result!.user).to.be.a('string'); + expect(result!.date).to.be.a('string'); + expect(result!.isError).to.be.false; + }).timeout(10000); + + it('should send message error with invalid api key', async () => { + const message = { + text: 'Hello, world!' + }; + const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => { + // Handle partial data + }; + + workspaceFoldersFirstPathStub.returns('./'); + + getConfigurationStub.withArgs('DevChat', 'API_KEY').returns('sk-KvH7ZCtHmFDCBTqH0jUv'); + getConfigurationStub.withArgs('DevChat', 'OpenAI.model').returns('gpt-4'); + getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns('0'); + getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true'); + getConfigurationStub.withArgs('DevChat', 'llmModel').returns('OpenAI'); + getConfigurationStub.withArgs('DevChat', 'OpenAI.tokensPerPrompt').returns('9000'); + + const result = await sendMessageBase(message, handlePartialData); + expect(result).to.be.an('object'); + expect(result!.command).to.equal('receiveMessage'); + expect(result!.text).to.be.a('string'); + expect(result!.hash).to.be.a('string'); + expect(result!.user).to.be.a('string'); + expect(result!.date).to.be.a('string'); + expect(result!.isError).to.be.true; + }).timeout(10000); + }); + + describe('stopDevChatBase', () => { + it('should stop sendMessageBase correctly', async () => { + const message = { + text: 'Hello, world!' + }; + const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => { + // Handle partial data + }; + + workspaceFoldersFirstPathStub.returns('./'); + + getConfigurationStub.withArgs('DevChat', 'API_KEY').returns('DC.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOjY2MTI0NDU1ODE2LCJqdGkiOjcyMzc4ODIzMjI3Mjc4MzI2NTJ9.qGWJ_NyWjjj66oa5mbfi3Hjawe-Yp8syCDLkpyu4yS0'); + getConfigurationStub.withArgs('DevChat', 'OpenAI.model').returns('gpt-4'); + getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns(0); + getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true'); + getConfigurationStub.withArgs('DevChat', 'llmModel').returns('OpenAI'); + getConfigurationStub.withArgs('DevChat', 'OpenAI.tokensPerPrompt').returns(9000); + + + // Start sendMessageBase in a separate Promise + const sendMessagePromise = sendMessageBase(message, handlePartialData); + + // Wait for a short period to ensure sendMessageBase has started + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Call stopDevChatBase + const stopMessage = { + text: 'stop' + }; + await stopDevChatBase(stopMessage); + + // Check if sendMessageBase has been stopped and returns an error + try { + const result = await sendMessagePromise; + expect(result).to.undefined; + } catch (error) { + expect(error).to.be.an('error'); + } + }); + }); +}); \ No newline at end of file diff --git a/test/toolwrapper/devchat.test.ts b/test/toolwrapper/devchat.test.ts new file mode 100644 index 0000000..40b8289 --- /dev/null +++ b/test/toolwrapper/devchat.test.ts @@ -0,0 +1,60 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import sinon from 'sinon'; +import DevChat, { ChatOptions } from '../../src/toolwrapper/devchat'; +import { CommandRun } from '../../src/util/commonUtil'; +import { UiUtilWrapper } from '../../src/util/uiUtil'; + +describe('DevChat', () => { + let devChat: DevChat; + let spawnAsyncStub: sinon.SinonStub; + let workspaceFoldersFirstPathStub: sinon.SinonStub; + + beforeEach(() => { + devChat = new DevChat(); + spawnAsyncStub = sinon.stub(CommandRun.prototype, 'spawnAsync'); + workspaceFoldersFirstPathStub = sinon.stub(UiUtilWrapper, 'workspaceFoldersFirstPath'); + }); + + afterEach(() => { + spawnAsyncStub.restore(); + workspaceFoldersFirstPathStub.restore(); + }); + + + describe('chat', () => { + it('should return a ChatResponse object with isError false when the chat is successful', async () => { + const content = 'Test chat content'; + const options: ChatOptions = { + // Provide mock values for the options + parent: 'parent_value', + reference: ['ref1', 'ref2'], + header: ['header1', 'header2'], + context: ['context1', 'context2'], + }; + const mockResponse = { + exitCode: 0, + stdout: 'User: Test user\nDate: 2022-01-01\nprompt-hash: 12345\nTest chat response', + stderr: '', + }; + const mockWorkspacePath = './'; + + spawnAsyncStub.resolves(mockResponse); + workspaceFoldersFirstPathStub.returns(mockWorkspacePath); + + const response = await devChat.chat(content, options, (data)=>{}); + + expect(response).to.have.property('prompt-hash', '12345'); + expect(response).to.have.property('user', 'Test user'); + expect(response).to.have.property('date', '2022-01-01'); + expect(response).to.have.property('response', 'Test chat response'); + expect(response).to.have.property('isError', false); + expect(spawnAsyncStub.calledOnce).to.be.true; + expect(workspaceFoldersFirstPathStub.calledOnce).to.be.true; + }); + + // Add more test cases for the chat method here + }); + + // ... other test cases +}); \ No newline at end of file diff --git a/test/toolwrapper/dtm.test.ts b/test/toolwrapper/dtm.test.ts new file mode 100644 index 0000000..229bc3d --- /dev/null +++ b/test/toolwrapper/dtm.test.ts @@ -0,0 +1,64 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import sinon from 'sinon'; +import DtmWrapper from '../../src/toolwrapper/dtm'; + + +describe('DtmWrapper', () => { + let dtmWrapper: DtmWrapper; + let commitStub: sinon.SinonStub; + let commitAllStub: sinon.SinonStub; + + beforeEach(() => { + dtmWrapper = new DtmWrapper(); + commitStub = sinon.stub(dtmWrapper, 'commit'); + commitAllStub = sinon.stub(dtmWrapper, 'commitall'); + }); + + afterEach(() => { + commitStub.restore(); + commitAllStub.restore(); + }); + + describe('commit', () => { + it('should return a DtmResponse object with status 0 when the commit is successful', async () => { + const commitMsg = 'Test commit message'; + const mockResponse = { + status: 0, + message: 'Commit successful', + log: 'Commit log', + }; + + commitStub.resolves(mockResponse); + + const response = await dtmWrapper.commit(commitMsg); + + expect(response).to.have.property('status', 0); + expect(response).to.have.property('message'); + expect(response).to.have.property('log'); + expect(commitStub.calledOnce).to.be.true; + }); + + // Add more test cases for the commit method here + }); + + describe('commitall', () => { + it('should return a DtmResponse object with status 0 when the commit is successful', async () => { + const commitMsg = 'Test commit message'; + const mockResponse = { + status: 0, + message: 'Commit all successful', + log: 'Commit all log', + }; + + commitAllStub.resolves(mockResponse); + + const response = await dtmWrapper.commitall(commitMsg); + + expect(response).to.have.property('status', 0); + expect(response).to.have.property('message'); + expect(response).to.have.property('log'); + expect(commitAllStub.calledOnce).to.be.true; + }); + }); +}); \ No newline at end of file diff --git a/test/topic/loadTopics.test.ts b/test/topic/loadTopics.test.ts new file mode 100644 index 0000000..c363fcc --- /dev/null +++ b/test/topic/loadTopics.test.ts @@ -0,0 +1,49 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { LogEntry } from '../../src/toolwrapper/devchat'; +import { loadTopicList } from '../../src/topic/loadTopics'; + +describe('loadTopicList', () => { + it('should create topic lists from chat logs', () => { + const chatLogs: LogEntry[] = [ + { hash: '1', parent: '', user: 'user1', date: '2022-01-01', request: 'request1', response: 'response1', context: []}, + { hash: '2', parent: '1', user: 'user2', date: '2022-01-02', request: 'request2', response: 'response2', context: []}, + { hash: '3', parent: '2', user: 'user3', date: '2022-01-03', request: 'request3', response: 'response3', context: []}, + { hash: '4', parent: '', user: 'user4', date: '2022-01-04', request: 'request4', response: 'response4', context: []}, + { hash: '5', parent: '4', user: 'user5', date: '2022-01-05', request: 'request5', response: 'response5', context: []}, + ]; + + const expectedTopicLists = { + '1': [ + { hash: '1', parent: '', user: 'user1', date: '2022-01-01', request: 'request1', response: 'response1', context: []}, + { hash: '2', parent: '1', user: 'user2', date: '2022-01-02', request: 'request2', response: 'response2', context: []}, + { hash: '3', parent: '2', user: 'user3', date: '2022-01-03', request: 'request3', response: 'response3', context: []}, + ], + '4': [ + { hash: '4', parent: '', user: 'user4', date: '2022-01-04', request: 'request4', response: 'response4', context: []}, + { hash: '5', parent: '4', user: 'user5', date: '2022-01-05', request: 'request5', response: 'response5', context: []}, + ], + }; + + const topicLists = loadTopicList(chatLogs); + expect(topicLists).to.deep.equal(expectedTopicLists); + }); + + it('should handle empty chat logs', () => { + const chatLogs: LogEntry[] = []; + const expectedTopicLists = {}; + const topicLists = loadTopicList(chatLogs); + expect(topicLists).to.deep.equal(expectedTopicLists); + }); + + it('should handle chat logs with no root entries', () => { + const chatLogs: LogEntry[] = [ + { hash: '1', parent: '0', user: 'user1', date: '2022-01-01', request: 'request1', response: 'response1', context: []}, + { hash: '2', parent: '1', user: 'user2', date: '2022-01-02', request: 'request2', response: 'response2', context: []}, + ]; + + const expectedTopicLists = {}; + const topicLists = loadTopicList(chatLogs); + expect(topicLists).to.deep.equal(expectedTopicLists); + }); +}); \ No newline at end of file diff --git a/test/topic/topicManager.test.ts b/test/topic/topicManager.test.ts new file mode 100644 index 0000000..a17901c --- /dev/null +++ b/test/topic/topicManager.test.ts @@ -0,0 +1,97 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { TopicManager } from '../../src/topic/topicManager'; + +describe('TopicManager', () => { + let topicManager: TopicManager; + + beforeEach(() => { + topicManager = TopicManager.getInstance(); + }); + + afterEach(() => { + // Reset topics and currentTopicId after each test + topicManager['_topics'] = {}; + topicManager.currentTopicId = undefined; + }); + + it('getInstance should return a singleton instance', () => { + const instance1 = TopicManager.getInstance(); + const instance2 = TopicManager.getInstance(); + expect(instance1).to.equal(instance2); + }); + + it('createTopic should create a new topic', () => { + const topic = topicManager.createTopic(); + expect(topic).to.be.not.undefined; + expect(topic.topicId).to.be.not.undefined; + expect(topicManager.getTopic(topic.topicId)).to.equal(topic); + }); + + it('getTopicList should return a list of topics', () => { + const topic1 = topicManager.createTopic(); + const topic2 = topicManager.createTopic(); + const topicList = topicManager.getTopicList(); + expect(topicList).to.include(topic1); + expect(topicList).to.include(topic2); + }); + + it('setCurrentTopic should set the current topic ID', () => { + const topic = topicManager.createTopic(); + topicManager.setCurrentTopic(topic.topicId); + expect(topicManager.currentTopicId).to.equal(topic.topicId); + }); + + it('updateTopic should update the topic with the given properties', () => { + const topic = topicManager.createTopic(); + const newMessageHash = 'new-message-hash'; + const messageDate = Date.now(); + const requestMessage = 'Request message'; + const responseMessage = 'Response message'; + + topicManager.updateTopic(topic.topicId, newMessageHash, messageDate, requestMessage, responseMessage); + + const updatedTopic = topicManager.getTopic(topic.topicId); + expect(updatedTopic!.name).to.equal(`${requestMessage} - ${responseMessage}`); + expect(updatedTopic!.firstMessageHash).to.equal(newMessageHash); + expect(updatedTopic!.lastMessageHash).to.equal(newMessageHash); + expect(updatedTopic!.lastUpdated).to.equal(messageDate); + }); + + it('updateTopic should not update the topic if the topic does not exist', () => { + const nonExistentTopicId = 'non-existent-topic-id'; + const newMessageHash = 'new-message-hash'; + const messageDate = Date.now(); + const requestMessage = 'Request message'; + const responseMessage = 'Response message'; + + topicManager.updateTopic(nonExistentTopicId, newMessageHash, messageDate, requestMessage, responseMessage); + + const nonExistentTopic = topicManager.getTopic(nonExistentTopicId); + expect(nonExistentTopic).to.be.undefined; + }); + + it('deleteTopic should delete the topic with the given ID', () => { + const topic = topicManager.createTopic(); + topicManager.deleteTopic(topic.topicId); + expect(topicManager.getTopic(topic.topicId)).to.be.undefined; + }); + + it('deleteTopic should not throw an error if the topic does not exist', () => { + const nonExistentTopicId = 'non-existent-topic-id'; + expect(() => { + topicManager.deleteTopic(nonExistentTopicId); + }).to.not.throw(); + }); + + it('deleteTopic should set the currentTopicId to undefined if the deleted topic was the current topic', () => { + const topic = topicManager.createTopic(); + topicManager.setCurrentTopic(topic.topicId); + topicManager.deleteTopic(topic.topicId); + expect(topicManager.currentTopicId).to.be.undefined; + }); + + + // Add more test cases for other methods in TopicManager +}); \ No newline at end of file diff --git a/test/util/apiKey.test.ts b/test/util/apiKey.test.ts new file mode 100644 index 0000000..5a0e4b3 --- /dev/null +++ b/test/util/apiKey.test.ts @@ -0,0 +1,91 @@ +// test/apiKey.test.ts + +import { expect } from 'chai'; +import { ApiKeyManager } from '../../src/util/apiKey'; +import { UiUtilWrapper } from '../../src/util/uiUtil'; +import sinon from 'sinon'; + +describe('ApiKeyManager', () => { + afterEach(() => { + sinon.restore(); + delete process.env.OPENAI_API_KEY; + delete process.env.OPENAI_API_BASE; + }); + + describe('getApiKey', () => { + it('should return the secret storage API key', async () => { + sinon.stub(UiUtilWrapper, 'secretStorageGet').resolves('sk.secret'); + sinon.stub(UiUtilWrapper, 'getConfiguration').returns(undefined); + + const apiKey = await ApiKeyManager.getApiKey(); + expect(apiKey).to.equal('sk.secret'); + }); + + it('should return the configuration API key', async () => { + sinon.stub(UiUtilWrapper, 'secretStorageGet').resolves(undefined); + sinon.stub(UiUtilWrapper, 'getConfiguration').returns('sk.config'); + + const apiKey = await ApiKeyManager.getApiKey(); + expect(apiKey).to.equal('sk.config'); + }); + + it('should return the environment variable API key', async () => { + sinon.stub(UiUtilWrapper, 'secretStorageGet').resolves(undefined); + sinon.stub(UiUtilWrapper, 'getConfiguration').returns(undefined); + process.env.OPENAI_API_KEY = 'sk.env'; + + const apiKey = await ApiKeyManager.getApiKey(); + expect(apiKey).to.equal('sk.env'); + }); + }); + + describe('getEndPoint', () => { + it('should return the configuration endpoint', () => { + sinon.stub(UiUtilWrapper, 'getConfiguration').returns('https://config-endpoint.com'); + + const endPoint = ApiKeyManager.getEndPoint('sk.key'); + expect(endPoint).to.equal('https://config-endpoint.com'); + }); + + it('should return the environment variable endpoint', () => { + sinon.stub(UiUtilWrapper, 'getConfiguration').returns(undefined); + process.env.OPENAI_API_BASE = 'https://env-endpoint.com'; + + const endPoint = ApiKeyManager.getEndPoint('sk.key'); + expect(endPoint).to.equal('https://env-endpoint.com'); + }); + + it('should return the default endpoint for DC keys', () => { + sinon.stub(UiUtilWrapper, 'getConfiguration').returns(undefined); + + const endPoint = ApiKeyManager.getEndPoint('DC.key'); + expect(endPoint).to.equal('https://xw4ymuy6qj.ap-southeast-1.awsapprunner.com/api/v1'); + }); + }); + + describe('getKeyType', () => { + it('should return "sk" for sk keys', () => { + const keyType = ApiKeyManager.getKeyType('sk.key'); + expect(keyType).to.equal('sk'); + }); + + it('should return "DC" for DC keys', () => { + const keyType = ApiKeyManager.getKeyType('DC.key'); + expect(keyType).to.equal('DC'); + }); + + it('should return undefined for invalid keys', () => { + const keyType = ApiKeyManager.getKeyType('invalid.key'); + expect(keyType).to.be.undefined; + }); + }); + + describe('writeApiKeySecret', () => { + it('should store the API key in secret storage', async () => { + const storeSecretStub = sinon.stub(UiUtilWrapper, 'storeSecret').resolves(); + + await ApiKeyManager.writeApiKeySecret('sk.secret'); + expect(storeSecretStub.calledWith('devchat_OPENAI_API_KEY', 'sk.secret')).to.be.true; + }); + }); +}); \ No newline at end of file diff --git a/test/util/commonUtil.test.ts b/test/util/commonUtil.test.ts new file mode 100644 index 0000000..53d8558 --- /dev/null +++ b/test/util/commonUtil.test.ts @@ -0,0 +1,138 @@ +// test/commonUtil.test.ts + +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { + createTempSubdirectory, + CommandRun, + runCommandAndWriteOutput, + runCommandStringAndWriteOutput, + getLanguageIdByFileName, +} from '../../src/util/commonUtil'; +import { UiUtilWrapper } from '../../src/util/uiUtil'; + +import sinon from 'sinon'; + +describe('commonUtil', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('createTempSubdirectory', () => { + it('should create a temporary subdirectory', () => { + const tempDir = os.tmpdir(); + const subdir = 'test-subdir'; + const targetDir = createTempSubdirectory(subdir); + + expect(targetDir.startsWith(path.join(tempDir, subdir))).to.be.true; + expect(fs.existsSync(targetDir)).to.be.true; + fs.rmdirSync(targetDir, { recursive: true }); + }); + }); + + describe('CommandRun', () => { + it('should run a command and capture stdout and stderr', async () => { + const command = 'echo'; + const args = ['hello', 'world']; + const options = { shell: true }; + + const run = new CommandRun(); + const result = await run.spawnAsync(command, args, options, undefined, undefined, undefined, undefined); + + expect(result.exitCode).to.equal(0); + expect(result.stdout.trim()).to.equal('hello world'); + expect(result.stderr).to.equal(''); + }); + + it('should run a command and write output to a file', async () => { + const command = 'echo'; + const args = ['hello', 'world']; + const options = { shell: true }; + const outputFile = path.join(os.tmpdir(), 'test-output.txt'); + + const run = new CommandRun(); + const result = await run.spawnAsync(command, args, options, undefined, undefined, undefined, outputFile); + + expect(result.exitCode).to.equal(0); + expect(result.stdout.trim()).to.equal('hello world'); + expect(result.stderr).to.equal(''); + expect(fs.readFileSync(outputFile, 'utf-8').trim()).to.equal('hello world'); + fs.unlinkSync(outputFile); + }); + + it('should handle command not found error and output the error message', async () => { + const command = 'nonexistent-command'; + const args: string[] = []; + const options = { shell: true }; + + const run = new CommandRun(); + + const result = await run.spawnAsync( + command, + args, + options, + undefined, + undefined, + undefined, + undefined + ); + + expect(result.exitCode).to.not.equal(0); + expect(result.stderr).to.include(`${command}: command not found`); + }); + }); + + describe('runCommandAndWriteOutput', () => { + it('should run a command and write output to a file', async () => { + const command = 'echo'; + const args: string[] = ['hello', 'world']; + const outputFile = path.join(os.tmpdir(), 'test-output.txt'); + + await runCommandAndWriteOutput(command, args, outputFile); + + expect(fs.readFileSync(outputFile, 'utf-8').trim()).to.equal('hello world'); + fs.unlinkSync(outputFile); + }); + }); + describe('runCommandStringAndWriteOutput', () => { + it('should run a command string and write output to a file', async () => { + const commandString = 'echo hello world'; + const outputFile = path.join(os.tmpdir(), 'test-output.txt'); + + await runCommandStringAndWriteOutput(commandString, outputFile); + + const fileContent = fs.readFileSync(outputFile, 'utf-8').trim(); + const parsedContent = JSON.parse(fileContent); + + expect(parsedContent.command).to.equal(commandString); + expect(parsedContent.content.trim()).to.equal('hello world'); + fs.unlinkSync(outputFile); + }); + }); + + describe('getLanguageIdByFileName', () => { + beforeEach(() => { + sinon.stub(UiUtilWrapper, 'languageId').callsFake(async (fileName: string) => { + const languageIds: { [key: string]: string } = { + 'test.py': 'python', + 'test.js': 'javascript', + 'test.ts': 'typescript', + }; + return languageIds[fileName]; + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should return the correct language ID for a given file name', async () => { + expect(await getLanguageIdByFileName('test.py')).to.equal('python'); + expect(await getLanguageIdByFileName('test.js')).to.equal('javascript'); + expect(await getLanguageIdByFileName('test.ts')).to.equal('typescript'); + expect(await getLanguageIdByFileName('test.unknown')).to.equal(undefined); + }); + }); +}); \ No newline at end of file diff --git a/test/util/filePairManager.test.ts b/test/util/filePairManager.test.ts new file mode 100644 index 0000000..246e851 --- /dev/null +++ b/test/util/filePairManager.test.ts @@ -0,0 +1,45 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { FilePairManager } from '../../src/util/diffFilePairs'; + +describe('FilePairManager', () => { + let filePairManager: FilePairManager; + + beforeEach(() => { + filePairManager = FilePairManager.getInstance(); + }); + + afterEach(() => { + // Clear the filePairs map after each test + (filePairManager as any).filePairs.clear(); + }); + + it('add file pair', () => { + const file1 = 'file1.txt'; + const file2 = 'file2.txt'; + filePairManager.addFilePair(file1, file2); + expect(filePairManager.findPair(file1)).to.deep.equal([file1, file2]); + expect(filePairManager.findPair(file2)).to.deep.equal([file1, file2]); + }); + + it('find pair', () => { + const file1 = 'file1.txt'; + const file2 = 'file2.txt'; + const file3 = 'file3.txt'; + const file4 = 'file4.txt'; + filePairManager.addFilePair(file1, file2); + filePairManager.addFilePair(file3, file4); + expect(filePairManager.findPair(file1)).to.deep.equal([file1, file2]); + expect(filePairManager.findPair(file2)).to.deep.equal([file1, file2]); + expect(filePairManager.findPair(file3)).to.deep.equal([file3, file4]); + expect(filePairManager.findPair(file4)).to.deep.equal([file3, file4]); + }); + + it('find non-existent pair', () => { + const file1 = 'file1.txt'; + const file2 = 'file2.txt'; + const file3 = 'file3.txt'; + filePairManager.addFilePair(file1, file2); + expect(filePairManager.findPair(file3)).to.be.undefined; + }); +}); \ No newline at end of file diff --git a/test/util/logger.test.ts b/test/util/logger.test.ts new file mode 100644 index 0000000..03cf2d0 --- /dev/null +++ b/test/util/logger.test.ts @@ -0,0 +1,64 @@ +// test/util/logger.test.ts + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { logger, LogChannel } from '../../src/util/logger'; + +class MockLogChannel implements LogChannel { + logs: string[] = []; + + info(message: string, ...args: any[]): void { + this.logs.push(`[INFO] ${message} ${args.join(' ')}`); + } + + warn(message: string, ...args: any[]): void { + this.logs.push(`[WARN] ${message} ${args.join(' ')}`); + } + + error(message: string | Error, ...args: any[]): void { + this.logs.push(`[ERROR] ${message} ${args.join(' ')}`); + } + + debug(message: string, ...args: any[]): void { + this.logs.push(`[DEBUG] ${message} ${args.join(' ')}`); + } + + show(): void { + // Do nothing + } +} + +describe('logger', () => { + it('should initialize the logger and create a channel', () => { + // Arrange + const mockChannel = new MockLogChannel(); + + // Act + logger.init(mockChannel); + + // Assert + const channel = logger.channel(); + expect(channel).to.not.be.undefined; + expect(channel).to.equal(mockChannel); + }); + + it('should log messages using the initialized channel', () => { + // Arrange + const mockChannel = new MockLogChannel(); + logger.init(mockChannel); + + // Act + logger.channel()?.info('Test info message'); + logger.channel()?.warn('Test warn message'); + logger.channel()?.error('Test error message'); + logger.channel()?.debug('Test debug message'); + + // Assert + expect(mockChannel.logs).to.deep.equal([ + '[INFO] Test info message ', + '[WARN] Test warn message ', + '[ERROR] Test error message ', + '[DEBUG] Test debug message ', + ]); + }); +}); \ No newline at end of file diff --git a/test/util/messageHistory.test.ts b/test/util/messageHistory.test.ts new file mode 100644 index 0000000..159df12 --- /dev/null +++ b/test/util/messageHistory.test.ts @@ -0,0 +1,45 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { MessageHistory } from '../../src/util/messageHistory'; + +describe('MessageHistory', () => { + let messageHistory: MessageHistory; + + beforeEach(() => { + messageHistory = new MessageHistory(); + }); + + it('add message', () => { + const message = { hash: '123', content: 'Hello' }; + messageHistory.add(message); + expect(messageHistory.find('123')).to.deep.equal(message); + }); + + it('find message by hash', () => { + const message1 = { hash: '123', content: 'Hello' }; + const message2 = { hash: '456', content: 'World' }; + messageHistory.add(message1); + messageHistory.add(message2); + expect(messageHistory.find('123')).to.deep.equal(message1); + expect(messageHistory.find('456')).to.deep.equal(message2); + }); + + it('find last message', () => { + const message1 = { hash: '123', content: 'Hello' }; + const message2 = { hash: '456', content: 'World' }; + messageHistory.add(message1); + messageHistory.add(message2); + expect(messageHistory.findLast()).to.deep.equal(message2); + }); + + it('clear history', () => { + const message1 = { hash: '123', content: 'Hello' }; + const message2 = { hash: '456', content: 'World' }; + messageHistory.add(message1); + messageHistory.add(message2); + messageHistory.clear(); + expect(messageHistory.find('123')).to.be.undefined; + expect(messageHistory.find('456')).to.be.undefined; + expect(messageHistory.findLast()).to.be.null; + }); +}); \ No newline at end of file diff --git a/test/util/utils.test.ts b/test/util/utils.test.ts new file mode 100644 index 0000000..d60892e --- /dev/null +++ b/test/util/utils.test.ts @@ -0,0 +1,13 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +describe('yourFunction', () => { + it('should return the correct result for input 1', () => { + const input = 1; + const expectedResult = 'expectedResult'; + const result = 'expectedResult'; + expect(result).to.equal(expectedResult); + }); + + // Add more test cases here +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 9c5ae0e..525d19d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,5 +13,6 @@ "strict": true, "jsx": "react", "esModuleInterop": true - } + }, + "exclude": ["test"] } \ No newline at end of file diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..c10ef32 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "out-test", + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} \ No newline at end of file