diff --git a/package-lock.json b/package-lock.json index 45d8af92d..b24f98269 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "csv": "6.4.1", "escape-string-regexp": "5.0.0", "ignore": "7.0.5", + "jsonc-parser": "^3.3.1", "node-ssh": "13.2.1", "tar": "7.5.7", "tmp": "0.2.5", @@ -149,282 +150,10 @@ "node": ">=10.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", "cpu": [ "x64" ], @@ -439,9 +168,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", "cpu": [ "arm64" ], @@ -455,27 +184,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", "cpu": [ "arm64" ], @@ -489,108 +201,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint/eslintrc": { "version": "0.4.3", "dev": true, @@ -2942,40 +2552,6 @@ "node": ">=18" } }, - "node_modules/esbuild/node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", @@ -2993,23 +2569,6 @@ "node": ">=18" } }, - "node_modules/esbuild/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", @@ -3737,9 +3296,7 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.14.1", "dev": true, "license": "MIT", "dependencies": { @@ -3776,6 +3333,12 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, "node_modules/kind-of": { "version": "6.0.3", "dev": true, @@ -5003,24 +4566,24 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -5029,14 +4592,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", - "less": "^4.0.0", + "less": "*", "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -5077,48 +4640,6 @@ } } }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", diff --git a/package.json b/package.json index 543fc33a0..ba7d7aa77 100644 --- a/package.json +++ b/package.json @@ -47,10 +47,17 @@ "jsonValidation": [ { "fileMatch": [ - ".vscode/actions.json" + ".vscode/actions.json", + "/etc/vscode/actions.json" ], "url": "./schemas/actions.json" }, + { + "fileMatch": [ + "/etc/vscode/profiles.json" + ], + "url": "./schemas/profiles.json" + }, { "fileMatch": [ "/etc/vscode/settings.json" @@ -91,7 +98,7 @@ "properties": { "name": { "type": "string", - "description": "Connection name" + "description": "Filter name" }, "library": { "type": "string", @@ -129,7 +136,7 @@ } }, "default": [], - "description": "List of filters for Object List" + "description": "List of filters shown in the Object Browser" }, "libraryList": { "type": "array", @@ -154,7 +161,7 @@ "additionalProperties": true }, "default": [], - "description": "A collection of connection settings to easily switch between them on this system." + "description": "A collection of library lists, filters, and custom variables to easily switch between on this system." }, "ifsShortcuts": { "type": "array", @@ -163,7 +170,7 @@ "title": "Path to directory" }, "default": [], - "description": "List of directories shown in IFS Browser" + "description": "List of directories shown in the IFS Browser" }, "autoSortIFSShortcuts": { "type": "boolean", @@ -1779,6 +1786,24 @@ "category": "IBM i", "title": "Unload active profile", "icon": "$(sign-out)" + }, + { + "command": "code-for-ibmi.resolveProfile.saveChangeToServer", + "enablement": "code-for-ibmi:connected", + "category": "IBM i", + "title": "Save Changes To Server" + }, + { + "command": "code-for-ibmi.resolveProfile.discardChangesAndSyncWithServer", + "enablement": "code-for-ibmi:connected", + "category": "IBM i", + "title": "Discard Changes and Sync with Server" + }, + { + "command": "code-for-ibmi.resolveProfile.overwriteChangesToServer", + "enablement": "code-for-ibmi:connected", + "category": "IBM i", + "title": "Overwrite Changes to Server" } ], "customEditors": [ @@ -1975,6 +2000,11 @@ { "id": "code-for-ibmi.compareWithLocal", "label": "Compare with" + }, + { + "id": "code-for-ibmi.resolveProfile", + "label": "Resolve...", + "icon": "$(edit-sparkle)" } ], "menus": { @@ -2065,6 +2095,23 @@ "when": "!explorerResourceIsFolder" } ], + "code-for-ibmi.resolveProfile": [ + { + "command": "code-for-ibmi.resolveProfile.saveChangeToServer", + "when": "viewItem =~ /^profileItem_active.*_Modified/", + "group": "00_resolve@01" + }, + { + "command": "code-for-ibmi.resolveProfile.discardChangesAndSyncWithServer", + "when": "viewItem =~ /^profileItem_active.*_(Modified|Out of Sync)/", + "group": "00_resolve@02" + }, + { + "command": "code-for-ibmi.resolveProfile.overwriteChangesToServer", + "when": "viewItem =~ /^profileItem_active.*_Out of Sync/", + "group": "00_resolve@01" + } + ], "commandPalette": [ { "command": "code-for-ibmi.testing.connectWithFixture", @@ -2529,6 +2576,18 @@ { "command": "code-for-ibmi.environment.profile.unload", "when": "never" + }, + { + "command": "code-for-ibmi.resolveProfile.saveChangeToServer", + "when": "never" + }, + { + "command": "code-for-ibmi.resolveProfile.discardChangesAndSyncWithServer", + "when": "never" + }, + { + "command": "code-for-ibmi.resolveProfile.overwriteChangesToServer", + "when": "never" } ], "view/title": [ @@ -3168,7 +3227,7 @@ }, { "command": "code-for-ibmi.environment.profile.unload", - "when": "view === environmentView && viewItem =~ /^profilesNode/ && code-for-ibmi:activeProfile", + "when": "view === environmentView && viewItem =~ /^profileItem_active/", "group": "inline@03" }, { @@ -3181,6 +3240,11 @@ "when": "view === environmentView && viewItem =~ /^profileItem_active_command/", "group": "inline@01" }, + { + "submenu": "code-for-ibmi.resolveProfile", + "when": "view === environmentView && viewItem =~ /^profileItem_active.*_(Modified|Out of Sync)/", + "group": "inline@02" + }, { "command": "code-for-ibmi.environment.profile.rename", "when": "view === environmentView && viewItem =~ /^profileItem/", @@ -3261,6 +3325,7 @@ "csv": "6.4.1", "escape-string-regexp": "5.0.0", "ignore": "7.0.5", + "jsonc-parser": "^3.3.1", "node-ssh": "13.2.1", "tar": "7.5.7", "tmp": "0.2.5", diff --git a/schemas/actions.json b/schemas/actions.json index d1c3940da..ebd450e6b 100644 --- a/schemas/actions.json +++ b/schemas/actions.json @@ -1,23 +1,22 @@ { "type": "array", "title": "Actions", - "description": "List of Actions that apply to this Workspace. Actions may be used to run commands on the remote system.", + "description": "List of Code for IBM i Actions which may be used to run commands on the remote system.", "items": { "$ref": "#/$defs/code4iAction" }, "$defs": { "code4iAction": { "type": "object", + "title": "Action", + "description": "A single Action.", + "required": [ + "name", + "command", + "environment", + "extensions" + ], "properties": { - "type": "object", - "title": "Action", - "description": "A single Action.", - "required": [ - "name", - "command", - "environment", - "extensions" - ], "name": { "type": "string", "title": "Name", @@ -29,6 +28,17 @@ "description": "The command that will be run when executing this Action.", "default": "" }, + "type": { + "type": "string", + "title": "File system type", + "description": "File system which supports this Action.", + "default": "ile", + "enum": [ + "file", + "member", + "streamfile" + ] + }, "environment": { "type": "string", "title": "Command Environment", diff --git a/schemas/profiles.json b/schemas/profiles.json new file mode 100644 index 000000000..20502544a --- /dev/null +++ b/schemas/profiles.json @@ -0,0 +1,138 @@ +{ + "type": "array", + "title": "Profiles", + "description": "List of Code for IBM i Profiles which may be used to have collections of library lists, filters, and custom variables to easily switch between on this system", + "items": { + "$ref": "#/$defs/code4iProfile" + }, + "$defs": { + "code4iProfile": { + "type": "object", + "title": "Profile", + "description": "A single profile.", + "required": [ + "name", + "currentLibrary", + "libraryList", + "objectFilters", + "ifsShortcuts", + "customVariables" + ], + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Profile name" + }, + "currentLibrary": { + "type": "string", + "title": "Current Library", + "description": "Library used as the current library and &CURLIB variable when running Actions.", + "default": "" + }, + "libraryList": { + "type": "array", + "title": "Library List", + "description": "Library list used when running Actions.", + "default": [], + "items": { + "type": "string", + "title": "Library" + } + }, + "objectFilters": { + "type": "array", + "title": "Object Filters", + "description": "List of filters shown in the Object Browser.", + "default": [], + "items": { + "type": "object", + "required": [ + "name", + "library", + "object", + "types", + "member", + "memberType" + ], + "properties": { + "name": { + "type": "string", + "description": "Filter name" + }, + "library": { + "type": "string", + "description": "Library filter", + "maxLength": 10 + }, + "object": { + "type": "string", + "description": "Object filter", + "maxLength": 10 + }, + "types": { + "type": "array", + "description": "Object types filter", + "items": { + "type": "string", + "description": "Object type. Usually starts with an asterisk." + }, + "default": [ + "*ALL" + ] + }, + "member": { + "type": "string", + "description": "Member filter", + "maxLength": 10, + "default": "*" + }, + "memberType": { + "type": "string", + "description": "Member type filter", + "maxLength": 10, + "default": "*" + } + } + } + }, + "ifsShortcuts": { + "type": "array", + "title": "IFS Shortcuts", + "description": "List of directories shown in the IFS Browser.", + "default": [], + "items": { + "type": "string", + "title": "Library" + } + }, + "customVariables": { + "type": "array", + "title": "Custom Variables", + "description": "Custom variables used when running Actions.", + "default": [], + "items": { + "type": "object", + "description": "Variable", + "properties": { + "name": { + "type": "string", + "description": "Variable name. Will be forced uppercase." + }, + "value": { + "type": "string", + "description": "Variable value" + } + } + } + }, + "setLibraryListCommand": { + "type": "string", + "title": "Set Library List Command", + "description": "Library List Command can be used to set your library list based on the result of a command like `CHGLIBL`, or your own command that sets the library list. Commands should be as explicit as possible. When refering to commands and objects, both should be qualified with a library. Put `?` in front of the command to prompt it before execution." + } + }, + "additionalProperties": true + } + } +} \ No newline at end of file diff --git a/schemas/settings.json b/schemas/settings.json index f3a97be0d..237183b6f 100644 --- a/schemas/settings.json +++ b/schemas/settings.json @@ -139,6 +139,8 @@ "type": "object", "properties": { "codefori": { + "title": "Connection settings", + "description": "Set of Code for IBM i connection settings.", "$ref": "#/definitions/CodeForI" } } diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index a6dfaa4d8..12b645298 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -11,10 +11,10 @@ import { sshSqlJob } from './components/mapepire/sqlJob'; import * as configVars from './configVars'; import { DebugConfiguration } from "./configuration/DebugConfiguration"; import { ConnectionManager } from './configuration/config/ConnectionManager'; -import { ConnectionConfig, RemoteConfigFile } from './configuration/config/types'; -import { ConfigFile } from './configuration/serverFile'; +import { ConnectionConfig, ConnectionProfile, RemoteConfigFile } from './configuration/config/types'; +import { ConfigFile } from './configuration/configFile'; import { CachedServerSettings, CodeForIStorage } from './configuration/storage/CodeForIStorage'; -import { AspInfo, CommandData, CommandResult, ConnectionData, EditorPath, IBMiMember, RemoteCommand } from './types'; +import { Action, AspInfo, CommandData, CommandResult, ConnectionData, EditorPath, IBMiMember, RemoteCommand } from './types'; export interface MemberParts extends IBMiMember { basename: string @@ -66,6 +66,7 @@ interface ConnectionOptions { interface ConnectionConfigFiles { settings: ConfigFile; + profiles: ConfigFile [key: string]: ConfigFile; } @@ -84,7 +85,8 @@ export default class IBMi { private componentManager = new ComponentManager(this); private configFiles: ConnectionConfigFiles = { - settings: new ConfigFile(this, `settings`, {}) + settings: new ConfigFile(this, `settings`, {}), + profiles: new ConfigFile(this, `profiles`, []), }; /** @@ -169,8 +171,9 @@ export default class IBMi { return this.configFiles[id] as ConfigFile; } - async loadRemoteConfigs() { - for (const configFile in this.configFiles) { + async loadRemoteConfigs(configKeys?: (keyof ConnectionConfigFiles)[]) { + configKeys = configKeys ?? Object.keys(this.configFiles); + for (const configFile of configKeys) { const currentConfig = this.configFiles[configFile as keyof ConnectionConfigFiles]; currentConfig.reset(); diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 9d186da5c..8c2a03454 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -1076,9 +1076,24 @@ export default class IBMiContent { * @param path the full path to the streamfile * @throws an Error if the file could not be correctly created */ - async createStreamFile(path: string) { - path = Tools.escapePath(path); - const result = (await this.ibmi.sendCommand({ command: `echo "" > ${path} && ${this.ibmi.remoteFeatures.attr} ${path} CCSID=1208` })); + async createStreamFile(posixPath: string, createParents: boolean = false) { + posixPath = Tools.escapePath(posixPath); + + const commands: string[] = []; + + if (createParents) { + const dir = path.posix.dirname(posixPath); + if (dir && dir !== '/') { + commands.push(`mkdir -p ${dir}`); + } + } + + commands.push(`echo "" > ${posixPath}`); + commands.push(`${this.ibmi.remoteFeatures.attr} ${posixPath} CCSID=1208`); + + const result = await this.ibmi.sendCommand({ + command: commands.join(' && ') + }); if (result.code !== 0) { throw new Error(result.stderr); } diff --git a/src/api/configuration/config/types.ts b/src/api/configuration/config/types.ts index d52f3ef93..a7f2abe79 100644 --- a/src/api/configuration/config/types.ts +++ b/src/api/configuration/config/types.ts @@ -26,7 +26,7 @@ export interface ConnectionConfig extends ConnectionProfile { debugSepPort: string; debugUpdateProductionFiles: boolean; debugEnableDebugTracing: boolean; - debugIgnoreCertificateErrors:boolean; + debugIgnoreCertificateErrors: boolean; readOnlyMode: boolean; quickConnect: boolean; defaultDeploymentMethod: DeploymentMethod | ''; @@ -35,7 +35,9 @@ export interface ConnectionConfig extends ConnectionProfile { secureSQL: boolean; keepActionSpooledFiles: boolean; mapepireJavaVersion: string - currentProfile?: string + currentProfile?: string; + currentProfileType?: ProfileType; + currentProfileLastKnownUpdate?: number; [name: string]: any; } @@ -59,15 +61,31 @@ export interface CustomVariable { value: string } +export type AnyConnectionProfile = | LocalConnectionProfile | ServerConnectionProfile + +export type ProfileType = 'local' | 'server'; + +export type ProfileState = 'In Sync' | 'Modified' | 'Out of Sync'; + +export interface LocalConnectionProfile extends ConnectionProfile { + type: 'local'; +} + +export interface ServerConnectionProfile extends ConnectionProfile { + type: 'server'; + state: ProfileState; +} + export interface ConnectionProfile { - name: string - homeDirectory: string - currentLibrary: string - libraryList: string[] - objectFilters: ObjectFilters[] - ifsShortcuts: string[] - customVariables: CustomVariable[] - setLibraryListCommand?: string + name: string; + homeDirectory?: string; + currentLibrary: string; + libraryList: string[]; + objectFilters: ObjectFilters[]; + ifsShortcuts: string[]; + customVariables: CustomVariable[]; + setLibraryListCommand?: string; + lastUpdated?: number; } export interface StoredConnection { diff --git a/src/api/configuration/configFile.ts b/src/api/configuration/configFile.ts new file mode 100644 index 000000000..6195e74f7 --- /dev/null +++ b/src/api/configuration/configFile.ts @@ -0,0 +1,84 @@ + +import path from "path"; +import IBMi from "../IBMi"; + +const SERVER_ROOT = path.posix.join(`/`, `etc`, `vscode`); + +type ConfigResult = `not_loaded` | `no_exist` | `failed_to_parse` | `invalid` | `ok`; + +export class ConfigFile { + private state: ConfigResult = `not_loaded`; + private basename: string; + private serverFile: string; + private serverData: T | undefined; + + // Should throw an error if loaded config is invalid + private validateData: ((loadedConfig: T) => T) | undefined; + + constructor(private connection: IBMi, configId: string, readonly fallback: T, validateData?: ((loadedConfig: T) => T)) { + this.basename = configId + `.json`; + this.serverFile = path.posix.join(SERVER_ROOT, this.basename); + this.validateData = validateData; + } + + getPaths() { + return { + server: this.serverFile, + } + } + + async loadFromServer() { + this.state = `no_exist`; + + const content = this.connection.getContent(); + const isAvailable = await content.testStreamFile(this.serverFile, `r`); + if (isAvailable) { + const fileContent = await content.downloadStreamfileRaw(this.serverFile); + try { + const serverConfig: T = JSON.parse(fileContent.toString()); + this.state = `ok`; + + if (this.validateData) { + try { + this.serverData = this.validateData(serverConfig); + } catch (e) { + this.state = `invalid`; + this.serverData = undefined; + } + } else { + this.serverData = serverConfig; + } + } catch (e: any) { + this.state = `failed_to_parse`; + } + } + } + + async writeToServer(newConfig: T): Promise { + try { + const content = this.connection.getContent(); + if (this.state === `no_exist`) { + await content.createStreamFile(this.serverFile, true); + } + + content.writeStreamfileRaw(this.serverFile, JSON.stringify(newConfig, null, 4)); + await this.loadFromServer(); + return true; + } catch (e: any) { + return false; + } + } + + async get() { + return this.serverData || this.fallback; + } + + reset() { + this.serverData = undefined; + this.state = `not_loaded`; + } + + getState() { + return this.state; + } +} \ No newline at end of file diff --git a/src/api/configuration/serverFile.ts b/src/api/configuration/serverFile.ts deleted file mode 100644 index debafb15d..000000000 --- a/src/api/configuration/serverFile.ts +++ /dev/null @@ -1,74 +0,0 @@ - -import path from "path"; -import IBMi from "../IBMi"; - -const WORKSPACE_ROOT = `.vscode`; -const SERVER_ROOT = path.posix.join(`/`, `etc`, `vscode`); - -type ConfigResult = `not_loaded`|`no_exist`|`failed_to_parse`|`invalid`|`ok`; - -interface LoadResult { - server: ConfigResult; -} - -export class ConfigFile { - private state: ConfigResult = `not_loaded`; - private basename: string; - private serverFile: string; - private serverData: T|undefined; - - public validateData: ((loadedConfig: any) => T)|undefined; - - constructor(private connection: IBMi, configId: string, readonly fallback: T) { - this.basename = configId + `.json`; - this.serverFile = path.posix.join(SERVER_ROOT, this.basename); - } - - getPaths() { - return { - server: this.serverFile, - } - } - - async loadFromServer() { - let serverConfig: any|undefined; - - this.state = `no_exist`; - - const isAvailable = await this.connection.getContent().testStreamFile(this.serverFile, `r`); - if (isAvailable) { - const content = await this.connection.getContent().downloadStreamfileRaw(this.serverFile); - try { - serverConfig = JSON.parse(content.toString()); - this.state = `ok`; - } catch (e: any) { - this.state = `failed_to_parse`; - } - - if (this.validateData) { - // Should throw an error. - try { - this.serverData = this.validateData(serverConfig); - } catch (e) { - this.state = `invalid`; - this.serverData = undefined; - } - } else { - this.serverData = serverConfig; - } - } - } - - async get() { - return this.serverData || this.fallback; - } - - reset() { - this.serverData = undefined; - this.state = `not_loaded`; - } - - getState() { - return this.state; - } -} \ No newline at end of file diff --git a/src/api/connectionProfiles.ts b/src/api/connectionProfiles.ts index 32cd15f4a..3f7d4428f 100644 --- a/src/api/connectionProfiles.ts +++ b/src/api/connectionProfiles.ts @@ -1,78 +1,215 @@ import { l10n } from "vscode"; import { instance } from "../instantiate"; import IBMi from "./IBMi"; -import { ConnectionProfile } from "./types"; +import { AnyConnectionProfile, LocalConnectionProfile, ServerConnectionProfile, ConnectionConfig, ConnectionProfile, ProfileType } from "./configuration/config/types"; -export async function updateConnectionProfile(profile: ConnectionProfile, options?: { newName?: string, delete?: boolean }) { - const config = instance.getConnection()?.getConfig(); - if (config) { - const profiles = config.connectionProfiles; - const index = profiles.findIndex(p => p.name === profile.name); +export async function updateConnectionProfile(profile: AnyConnectionProfile, options?: { newName?: string, delete?: boolean, modifiedConfig?: ConnectionConfig, }) { + const connection = instance.getConnection(); + if (connection) { + const config = options?.modifiedConfig || connection.getConfig(); + const isServerProfile = profile.type === 'server'; + + const { localProfiles, serverProfiles } = await getConnectionProfilesInGroups(); + const profiles = isServerProfile ? serverProfiles : localProfiles; + let now: number | undefined; + let oldName: string | undefined + const index = profiles.findIndex(p => p.name === profile.name); if (options?.delete) { if (index < 0) { throw new Error(l10n.t("Profile {0} not found for deletion.", profile.name)); } profiles.splice(index, 1); - } - else { - profile.name = options?.newName || profile.name; + } else { + if (options?.newName) { + oldName = profile.name; + profile.name = options?.newName + } else { + profile.name = profile.name; + } + if (isServerProfile) { + now = Date.now(); + profile.lastUpdated = now; + } profiles[index < 0 ? profiles.length : index] = profile; } - if (isActiveProfile(profile)) { - //Only update the setLibraryListCommand in the current config since the editor is the only place it can be changed + if (isActiveProfile(profile, oldName)) { + // Only update the setLibraryListCommand in the current config since the editor is the only place it can be changed config.setLibraryListCommand = profile.setLibraryListCommand; + + if (options?.newName) { + config.currentProfile = profile.name; + } + + if (now) { + config.currentProfileLastKnownUpdate = now; + } } - await IBMi.connectionManager.update(config); + if (isServerProfile) { + // Map internal server profile type to connection profile + const serverProfiles: ConnectionProfile[] = (profiles as ServerConnectionProfile[]).map(({ type, state, homeDirectory, ...profile }) => profile); + const profilesConfigFile = connection.getConfigFile(`profiles`); + await profilesConfigFile.writeToServer(serverProfiles); + await IBMi.connectionManager.update(config); + } else { + // Map internal local profile type to connection profile + const localProfiles: ConnectionProfile[] = (profiles as LocalConnectionProfile[]).map(({ type, ...profile }) => profile); + config.connectionProfiles = localProfiles; + await IBMi.connectionManager.update(config); + } } } /** - * @returns ann arry of {@link ConnectionProfile} stored in the config; except the default profile (with a blank name), only used internally + * @returns an arry of local (stored in the config) and system (stored in /etc/vscode/profiles.json) {@link ConnectionProfile}; + * except the default profile (with a blank name), only used internally */ -export function getConnectionProfiles() { - const config = instance.getConnection()?.getConfig(); - if (config) { - return config.connectionProfiles.filter(profile => Boolean(profile.name)); - } - else { - throw new Error(l10n.t("Not connected to an IBM i")); - } +export async function getAllConnectionProfiles() { + const { localProfiles, serverProfiles } = await getConnectionProfilesInGroups(); + return [...localProfiles, ...serverProfiles]; } -export function getConnectionProfile(profileName: string) { - return getConnectionProfiles().filter(p => p.name === profileName).at(0); -} +export async function getConnectionProfilesInGroups() { + const connection = instance.getConnection(); + if (connection) { + const config = connection.getConfig(); + const rawLocalProfiles: ConnectionProfile[] = config.connectionProfiles; + + // Map connection profiles to internal local profile type + const localProfiles: LocalConnectionProfile[] = rawLocalProfiles.map(rawlocalProfile => ({ + ...rawlocalProfile, + type: 'local' as const + })); + + // Get server profiles + const profilesConfigFile = connection.getConfigFile(`profiles`); + const rawServerProfiles: ConnectionProfile[] = await profilesConfigFile.get(); + + // Get current profile + const currentProfileName = config.currentProfile; + const currentProfileType = config.currentProfileType; + + // Map connection profiles to internal server profile type + const serverProfiles: ServerConnectionProfile[] = rawServerProfiles.map(rawServerProfile => { + const profileLastUpdated = rawServerProfile.lastUpdated || 0; -export function getDefaultProfile() { - const config = instance.getConnection()?.getConfig(); - if (config) { - let defaultProfile = config.connectionProfiles.filter(profile => !profile.name).at(0); - if (!defaultProfile) { - defaultProfile = { - name: '', - homeDirectory: '', - ifsShortcuts: [], - currentLibrary: '', - objectFilters: [], - customVariables: [], - libraryList: [] + let state: ServerConnectionProfile['state']; + if (currentProfileType === 'server' && currentProfileName === rawServerProfile.name) { + // Current server profile, so compare the local version against the server version + const localVersionOfServerProfile: ConnectionProfile = { + name: currentProfileName, + currentLibrary: config.currentLibrary, + libraryList: config.libraryList, + objectFilters: config.objectFilters, + ifsShortcuts: config.ifsShortcuts, + customVariables: config.customVariables, + setLibraryListCommand: config.setLibraryListCommand + }; + const isInSync = isProfileInSync(rawServerProfile, localVersionOfServerProfile); + const lastKnownUpdate = config.currentProfileLastKnownUpdate || 0; + const isOutdated = profileLastUpdated > lastKnownUpdate; + + if (isOutdated && !isInSync) { + state = 'Out of Sync'; + } else if (!isOutdated && !isInSync) { + state = 'Modified'; + } else { + state = 'In Sync'; + } + } else { + // Not current server profile, so it's in sync + state = 'In Sync'; + } + + return { + ...rawServerProfile, + type: 'server' as const, + state, + lastUpdated: profileLastUpdated }; + }); - config.connectionProfiles.push(defaultProfile); + // Check if current profile is a server profile that no longer exists in the remote file + if (currentProfileName && currentProfileType === 'server') { + const profileExistsOnServer = serverProfiles.some(p => p.name === currentProfileName); + if (!profileExistsOnServer) { + // Add the missing profile with out of sync state + const localVersionOfServerProfile: ServerConnectionProfile = { + name: currentProfileName, + type: 'server' as const, + currentLibrary: config.currentLibrary, + libraryList: config.libraryList, + objectFilters: config.objectFilters, + ifsShortcuts: config.ifsShortcuts, + customVariables: config.customVariables, + setLibraryListCommand: config.setLibraryListCommand, + state: 'Out of Sync', + lastUpdated: 0 + }; + + serverProfiles.push(localVersionOfServerProfile); + } } - return defaultProfile; - } - else { + return { + localProfiles, + serverProfiles + }; + } else { throw new Error(l10n.t("Not connected to an IBM i")); } } +export function isProfileInSync(profile1: ConnectionProfile, profile2: ConnectionProfile): boolean { + return ( + profile1.currentLibrary === profile2.currentLibrary && + JSON.stringify(profile1.libraryList) === JSON.stringify(profile2.libraryList) && + JSON.stringify(profile1.objectFilters) === JSON.stringify(profile2.objectFilters) && + JSON.stringify(profile1.ifsShortcuts) === JSON.stringify(profile2.ifsShortcuts) && + JSON.stringify(profile1.customVariables) === JSON.stringify(profile2.customVariables) && + profile1.setLibraryListCommand === profile2.setLibraryListCommand + ); +} + +export async function getConnectionProfile(profileName: string, type: ProfileType) { + const { localProfiles, serverProfiles } = await getConnectionProfilesInGroups(); + if (type === 'local') { + return localProfiles.find(p => p.name === profileName); + } else { + return serverProfiles.find(p => p.name === profileName); + } +} + +export function getDefaultProfile(config: ConnectionConfig): LocalConnectionProfile { + let defaultProfile = config.connectionProfiles.filter(profile => !profile.name).at(0); + if (!defaultProfile) { + defaultProfile = { + name: '', + homeDirectory: '', + ifsShortcuts: [], + currentLibrary: '', + objectFilters: [], + customVariables: [], + libraryList: [] + }; + + config.connectionProfiles.push(defaultProfile); + } + + return { + ...defaultProfile, + type: 'local' + }; +} + export function assignProfile(fromProfile: ConnectionProfile, toProfile: ConnectionProfile) { - toProfile.homeDirectory = fromProfile.homeDirectory; + if (fromProfile.homeDirectory) { + // Home directory will be undefined when assigning from a server profile + toProfile.homeDirectory = fromProfile.homeDirectory; + } + toProfile.currentLibrary = fromProfile.currentLibrary; toProfile.libraryList = fromProfile.libraryList; toProfile.objectFilters = fromProfile.objectFilters; @@ -86,6 +223,8 @@ export function cloneProfile(fromProfile: ConnectionProfile, newName: string): C return assignProfile(fromProfile, { name: newName } as ConnectionProfile); } -export function isActiveProfile(profile: ConnectionProfile) { - return instance.getConnection()?.getConfig().currentProfile === profile.name; +export function isActiveProfile(profile: AnyConnectionProfile, oldName?: string) { + const connection = instance.getConnection(); + const config = connection?.getConfig(); + return config?.currentProfile === (oldName || profile.name) && config?.currentProfileType === profile.type; } \ No newline at end of file diff --git a/src/editors/connectionProfileEditor.ts b/src/editors/connectionProfileEditor.ts index 75f567162..9989328c7 100644 --- a/src/editors/connectionProfileEditor.ts +++ b/src/editors/connectionProfileEditor.ts @@ -1,8 +1,10 @@ import vscode, { l10n } from "vscode"; import { isActiveProfile, updateConnectionProfile } from "../api/connectionProfiles"; import { instance } from "../instantiate"; -import { ConnectionProfile } from "../typings"; +import { AnyConnectionProfile } from "../typings"; import { CustomEditor } from "./customEditorProvider"; +import { verifyLatestServerProfileState } from "../ui/views/environment/environmentView"; +import IBMi from "../api/IBMi"; type ConnectionProfileData = { homeDirectory: string @@ -11,27 +13,31 @@ type ConnectionProfileData = { setLibraryListCommand: string } -const editedProfiles: Set = new Set; +const editedProfiles: Set<{ name: string, type: string }> = new Set; -export function isProfileEdited(profile: ConnectionProfile) { - return editedProfiles.has(profile.name); +export function isProfileEdited(profile: AnyConnectionProfile) { + return editedProfiles.has({ name: profile.name, type: profile.type }); } -export function editConnectionProfile(profile: ConnectionProfile, doAfterSave?: () => Thenable) { +export function editConnectionProfile(profile: AnyConnectionProfile, doAfterSave?: () => Thenable) { const activeProfile = isActiveProfile(profile); const config = instance.getConnection()?.getConfig(); const objectFilters = (activeProfile && config ? config : profile).objectFilters; const ifsShortcuts = (activeProfile && config ? config : profile).ifsShortcuts; const customVariables = (activeProfile && config ? config : profile).customVariables; - new CustomEditor(`${profile.name}.profile`, data => save(profile, data).then(doAfterSave), () => editedProfiles.delete(profile.name)) - .addInput("homeDirectory", l10n.t("Home Directory"), '', { minlength: 1, default: profile.homeDirectory, readonly: activeProfile }) + const profileEditor = new CustomEditor(`${profile.name}.profile`, data => save(profile, data).then(doAfterSave), () => editedProfiles.delete({ name: profile.name, type: profile.type })); + if (profile.type === `local`) { + profileEditor + .addInput("homeDirectory", l10n.t("Home Directory"), '', { minlength: 1, default: profile.homeDirectory, readonly: activeProfile }) + } + profileEditor .addInput("currentLibrary", l10n.t("Current Library"), '', { minlength: 1, maxlength: 10, default: profile.currentLibrary, readonly: activeProfile }) .addInput("libraryList", l10n.t("Library List"), l10n.t("A comma-separated list of libraries."), { default: profile.libraryList.join(","), readonly: activeProfile }) .addInput("setLibraryListCommand", l10n.t("Library List Command"), l10n.t("Library List Command can be used to set your library list based on the result of a command like CHGLIBL, or your own command that sets the library list.
Commands should be as explicit as possible.
When refering to commands and objects, both should be qualified with a library.
Put ? in front of the command to prompt it before execution."), { default: profile.setLibraryListCommand }) .addHorizontalRule() .addHeading(l10n.t("Object filters"), 3) - .addParagraph(objectFilters.length ? `
    ${objectFilters.map(filter => `
  • ${filter.name}
  • `).join('')}
` : l10n.t("None")) + .addParagraph(objectFilters.length ? `
    ${objectFilters.map(filter => `
  • ${filter.name}: ${`${filter.library}/${filter.object}/${filter.member}.${filter.memberType || `*`} (${filter.types.join(`, `)})`}
  • `).join('')}
` : l10n.t("None")) .addHorizontalRule() .addHeading(l10n.t("IFS shortcuts"), 3) .addParagraph(ifsShortcuts.length ? `
    ${ifsShortcuts.map(shortcut => `
  • ${shortcut}
  • `).join('')}
` : l10n.t("None")) @@ -40,35 +46,48 @@ export function editConnectionProfile(profile: ConnectionProfile, doAfterSave?: .addParagraph(customVariables.length ? `
    ${customVariables.map(variable => `
  • &${variable.name}: ${variable.value}
  • `).join('')}
` : l10n.t("None")) .open(); - editedProfiles.add(profile.name); + editedProfiles.add({ name: profile.name, type: profile.type }); } -async function save(profile: ConnectionProfile, data: ConnectionProfileData) { - const content = instance.getConnection()?.getContent(); - if (content) { - profile.homeDirectory = data.homeDirectory.trim(); - profile.setLibraryListCommand = data.setLibraryListCommand.trim(); +async function save(profile: AnyConnectionProfile, data: ConnectionProfileData) { + const connection = instance.getConnection(); + if (connection) { + const content = connection.getContent(); + const config = connection.getConfig(); + const isActive = isActiveProfile(profile); + if (isActive && profile.type === `server`) { + config.setLibraryListCommand = data.setLibraryListCommand.trim(); + await IBMi.connectionManager.update(config); + } else { + if (profile.type === `local`) { + profile.homeDirectory = data.homeDirectory.trim(); + } + profile.setLibraryListCommand = data.setLibraryListCommand.trim(); - data.currentLibrary = data.currentLibrary.trim(); - if (data.currentLibrary) { - if (await content.checkObject({ library: "QSYS", name: data.currentLibrary, type: "*LIB" })) { - profile.currentLibrary = data.currentLibrary; + data.currentLibrary = data.currentLibrary.trim(); + if (data.currentLibrary) { + if (await content.checkObject({ library: "QSYS", name: data.currentLibrary, type: "*LIB" })) { + profile.currentLibrary = data.currentLibrary; + } + else { + throw new Error(l10n.t("Current library {0} is invalid", data.currentLibrary)); + } } - else { - throw new Error(l10n.t("Current library {0} is invalid", data.currentLibrary)); + + const libraryList = data.libraryList.split(',').map(library => library.trim()); + const badLibraries = await content.validateLibraryList(libraryList); + if (badLibraries.length && !await vscode.window.showWarningMessage(l10n.t("The following libraries are invalid. Do you still want to save that profile?"), { + modal: true, + detail: badLibraries.sort().map(library => `- ${library}`).join("\n") + }, l10n.t("Yes"))) { + throw new Error(l10n.t("Save aborted")); } - } + profile.libraryList = libraryList; - const libraryList = data.libraryList.split(',').map(library => library.trim()); - const badLibraries = await content.validateLibraryList(libraryList); - if (badLibraries.length && !await vscode.window.showWarningMessage(l10n.t("The following libraries are invalid. Do you still want to save that profile?"), { - modal: true, - detail: badLibraries.sort().map(library => `- ${library}`).join("\n") - }, l10n.t("Yes"))) { - throw new Error(l10n.t("Save aborted")); + const canProceed = await verifyLatestServerProfileState(profile); + if (canProceed) { + await updateConnectionProfile(profile); + } } - profile.libraryList = libraryList; - - await updateConnectionProfile(profile); } } \ No newline at end of file diff --git a/src/ui/views/environment/actions.ts b/src/ui/views/environment/actions.ts index c487e7a8a..1144bff18 100644 --- a/src/ui/views/environment/actions.ts +++ b/src/ui/views/environment/actions.ts @@ -139,7 +139,7 @@ export class ActionItem extends EnvironmentItem { static matchedColor = "charts.yellow"; static canRunColor = "charts.blue"; static matchedCanRunColor = "charts.green"; - static context = `actionItem`; + static contextValue = `actionItem`; private context: ActionContext = {} @@ -165,11 +165,11 @@ export class ActionItem extends EnvironmentItem { this.description = this.context.matched ? l10n.t("search match") : undefined; this.tooltip = this.action.command; this.resourceUri = vscode.Uri.from({ - scheme: ActionItem.context, + scheme: ActionItem.contextValue, authority: this.action.name, query: stringify({ matched: this.context.matched || undefined, canRun: this.context.canRun || undefined }) }); - this.contextValue = `${ActionItem.context}${this.context.canRun ? "_canrun" : ""}${this.context.matched ? '_matched' : ''}`; + this.contextValue = `${ActionItem.contextValue}${this.context.canRun ? "_canrun" : ""}${this.context.matched ? '_matched' : ''}`; } } diff --git a/src/ui/views/environment/connectionProfiles.ts b/src/ui/views/environment/connectionProfiles.ts index 66c4bfa9b..e0f117b2b 100644 --- a/src/ui/views/environment/connectionProfiles.ts +++ b/src/ui/views/environment/connectionProfiles.ts @@ -1,7 +1,8 @@ import vscode, { l10n } from "vscode"; -import { getConnectionProfiles } from "../../../api/connectionProfiles"; +import { stringify } from "querystring"; +import { getConnectionProfilesInGroups } from "../../../api/connectionProfiles"; import { instance } from "../../../instantiate"; -import { ConnectionProfile } from "../../../typings"; +import { AnyConnectionProfile, ProfileState, ProfileType } from "../../../api/configuration/config/types"; import { VscodeTools } from "../../Tools"; import { EnvironmentItem } from "./environmentItem"; @@ -22,25 +23,47 @@ export class ProfilesNode extends EnvironmentItem { this.contextValue = "profilesNode"; } - getChildren() { - const currentProfile = instance.getConnection()?.getConfig().currentProfile; - return getConnectionProfiles() + async getChildren() { + const connection = instance.getConnection(); + const config = connection?.getConfig(); + const currentProfile = config?.currentProfile; + const currentProfileType = config?.currentProfileType ?? `local`; + const { localProfiles, serverProfiles } = await getConnectionProfilesInGroups(); + const localProfileItems = localProfiles + .filter(profile => Boolean(profile.name)) .sort((p1, p2) => p1.name.localeCompare(p2.name)) - .map(profile => new ProfileItem(this, profile, profile.name === currentProfile)); + .map(profile => new ProfileItem(this, profile, profile.name === currentProfile && profile.type === currentProfileType)); + const serverProfileItems = serverProfiles + .sort((p1, p2) => p1.name.localeCompare(p2.name)) + .map(profile => new ProfileItem(this, profile, profile.name === currentProfile && profile.type === currentProfileType)); + return [...localProfileItems, ...serverProfileItems]; } } export class ProfileItem extends EnvironmentItem { - static contextValue = `profileItem`; static activeColor = "charts.green"; + static modifiedColor = "charts.blue"; + static outOfSyncColor = "charts.yellow"; + static contextValue = `profileItem`; - constructor(parent: EnvironmentItem, readonly profile: ConnectionProfile, active: boolean) { - super(profile.name, { parent, icon: "person", color: active ? ProfileItem.activeColor : undefined }); + constructor(parent: EnvironmentItem, readonly profile: AnyConnectionProfile, readonly active: boolean) { + const state = profile.type === 'server' ? profile.state : undefined; + const icon = profile.type === 'server' ? `vm` : `person`; + const color = ProfileItem.getColor(active, profile.type, state); + super(profile.name, { parent, icon: icon, color }); + + this.contextValue = `${ProfileItem.contextValue}${active ? '_active' : ''}${profile.setLibraryListCommand ? '_command' : ''}${profile.type === 'server' ? `_${profile.state}` : ''}`; + this.description = active ? l10n.t(`Active`) : ``; + if (active && profile.type === 'server') { + this.description = this.description ? `${this.description} (${profile.state})` : `(${profile.state})`; + } - this.contextValue = `${ProfileItem.contextValue}${active ? '_active' : ''}${profile.setLibraryListCommand ? '_command' : ''}`; - this.description = active ? l10n.t(`Active profile`) : ``; - this.resourceUri = vscode.Uri.from({ scheme: this.contextValue, authority: profile.name, query: active ? "active" : "" }); - this.tooltip = VscodeTools.profileToToolTip(profile) + this.resourceUri = vscode.Uri.from({ + scheme: ProfileItem.contextValue, + authority: profile.name, + query: stringify({ active: active || undefined, type: profile.type, state: state }) + }); + this.tooltip = VscodeTools.profileToToolTip(profile); this.command = { title: "Edit connection profile", @@ -48,4 +71,21 @@ export class ProfileItem extends EnvironmentItem { arguments: [this.profile] } } + + static getColor(active: boolean, type: ProfileType, state?: ProfileState): string | undefined { + if (active) { + if (type === `server` && state) { + switch (state) { + case 'In Sync': + return ProfileItem.activeColor; + case 'Modified': + return ProfileItem.modifiedColor; + case 'Out of Sync': + return ProfileItem.outOfSyncColor; + } + } + + return ProfileItem.activeColor; + } + } } \ No newline at end of file diff --git a/src/ui/views/environment/environmentView.ts b/src/ui/views/environment/environmentView.ts index f618070fb..da191fe22 100644 --- a/src/ui/views/environment/environmentView.ts +++ b/src/ui/views/environment/environmentView.ts @@ -1,18 +1,21 @@ import { parse as parseQuery } from "querystring"; -import vscode, { l10n, QuickPickItem } from 'vscode'; +import vscode, { l10n, QuickPickItem, ThemeIcon, window } from 'vscode'; import { getActions, updateAction } from '../../../api/actions'; import { GetNewLibl } from '../../../api/components/getNewLibl'; -import { assignProfile, cloneProfile, getConnectionProfile, getConnectionProfiles, getDefaultProfile, updateConnectionProfile } from '../../../api/connectionProfiles'; +import { assignProfile, cloneProfile, getConnectionProfile, getAllConnectionProfiles, getDefaultProfile, updateConnectionProfile, isActiveProfile } from '../../../api/connectionProfiles'; import IBMi from '../../../api/IBMi'; import { editAction, isActionEdited } from '../../../editors/actionEditor'; import { editConnectionProfile, isProfileEdited } from '../../../editors/connectionProfileEditor'; import { instance } from '../../../instantiate'; -import { Action, ActionEnvironment, BrowserItem, ConnectionProfile, CustomVariable, FocusOptions } from '../../../typings'; +import { Action, ActionEnvironment, AnyConnectionProfile, BrowserItem, ConnectionConfig, ConnectionProfile, CustomVariable, FocusOptions, ProfileState, ProfileType, ServerConnectionProfile } from '../../../typings'; import { uriToActionTarget } from '../../actions'; import { ActionItem, Actions, ActionsNode, ActionTypeNode } from './actions'; import { ConnectionProfiles, ProfileItem, ProfilesNode } from './connectionProfiles'; import { CustomVariableItem, CustomVariables, CustomVariablesNode } from './customVariables'; +import * as path from 'path'; +import { onCodeForIBMiConfigurationChange } from "../../../config/Configuration"; +import { modify } from 'jsonc-parser'; export function initializeEnvironmentView(context: vscode.ExtensionContext) { const environmentView = new EnvironmentView(); @@ -23,7 +26,6 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { }); const updateUIContext = async (profileName?: string) => { - await vscode.commands.executeCommand(`setContext`, "code-for-ibmi:activeProfile", profileName); environmentTreeViewer.description = profileName ? l10n.t("Current profile: {0}", profileName) : l10n.t("No active profile"); vscode.commands.executeCommand("code-for-ibmi.updateConnectedBar"); }; @@ -33,16 +35,30 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { localActionsWatcher.onDidChange(() => environmentView.actionsNode?.forceRefresh()); localActionsWatcher.onDidDelete(() => environmentView.actionsNode?.forceRefresh()); + + instance.subscribe(context, "connected", "Update active profile status on environment view", () => { + const connection = instance.getConnection(); + if (connection) { + const config = connection.getConfig(); + const currentProfile = config.currentProfile; + updateUIContext(currentProfile); + } + }); + context.subscriptions.push( environmentTreeViewer, localActionsWatcher, vscode.window.onDidChangeActiveTextEditor(async editor => environmentView.actionsNode?.activeEditorChanged(editor)), vscode.window.registerFileDecorationProvider({ provideFileDecoration(uri: vscode.Uri, token: vscode.CancellationToken): vscode.ProviderResult { - if (uri.scheme.startsWith(ProfileItem.contextValue) && uri.query === "active") { - return { color: new vscode.ThemeColor(ProfileItem.activeColor) }; - } - else if (uri.scheme === ActionItem.context) { + if (uri.scheme.startsWith(ProfileItem.contextValue)) { + const query = parseQuery(uri.query); + const active = query.active ? true : false; + const color = ProfileItem.getColor(active, query.type as ProfileType, query.state as ProfileState); + if (color) { + return { color: new vscode.ThemeColor(color) }; + } + } else if (uri.scheme === ActionItem.contextValue) { const query = parseQuery(uri.query); if (query.matched && query.canRun) { return { color: new vscode.ThemeColor(ActionItem.matchedCanRunColor) }; @@ -57,7 +73,16 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { } }), - vscode.commands.registerCommand("code-for-ibmi.environment.refresh", () => environmentView.refresh()), + vscode.commands.registerCommand("code-for-ibmi.environment.refresh", async () => { + await vscode.window.withProgress({ location: { viewId: `environmentView` } }, async (progress) => { + const connection = instance.getConnection(); + if (connection) { + await connection.loadRemoteConfigs([`profiles`]); + } + + environmentView.refresh(); + }); + }), vscode.commands.registerCommand("code-for-ibmi.environment.refresh.item", (item: BrowserItem) => environmentView.refresh(item)), vscode.commands.registerCommand("code-for-ibmi.environment.reveal", (item: BrowserItem, options?: FocusOptions) => environmentTreeViewer.reveal(item, options)), @@ -166,7 +191,6 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { const variable = { name, value: from?.value } as CustomVariable; if (from) { await CustomVariables.update(variable); - environmentView.refresh(variablesNode); } else { vscode.commands.executeCommand("code-for-ibmi.environment.variable.edit", variable, variablesNode); } @@ -177,7 +201,6 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { if (value !== undefined) { variable.value = value; await CustomVariables.update(variable); - environmentView.refresh(variablesNode); } }), vscode.commands.registerCommand("code-for-ibmi.environment.variable.rename", async (variableItem: CustomVariableItem) => { @@ -191,7 +214,6 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { if (newName) { await CustomVariables.update(variable, { newName }); - environmentView.refresh(variableItem.parent); } }), vscode.commands.registerCommand("code-for-ibmi.environment.variable.copy", async (variableItem: CustomVariableItem) => { @@ -201,46 +223,93 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { const variable = variableItem.customVariable; if (await vscode.window.showInformationMessage(l10n.t("Do you really want to delete Custom Variable '{0}' ?", variable.name), { modal: true }, l10n.t("Yes"))) { await CustomVariables.update(variable, { delete: true }); - environmentView.refresh(variableItem.parent); } }), vscode.commands.registerCommand("code-for-ibmi.environment.profile.create", async (node?: ProfilesNode, from?: ConnectionProfile) => { - const existingNames = getConnectionProfiles().map(profile => profile.name); + await vscode.window.withProgress({ location: { viewId: `environmentView` } }, async (progress) => { + const existingNames = (await getAllConnectionProfiles()).map(profile => profile.name); - const name = await vscode.window.showInputBox({ - title: l10n.t("Enter new profile name"), - placeHolder: l10n.t("Profile name..."), - value: from?.name, - validateInput: name => ConnectionProfiles.validateName(name, existingNames) - }); + const name = await vscode.window.showInputBox({ + title: l10n.t("Enter new profile name"), + placeHolder: l10n.t("Profile name..."), + value: from?.name, + validateInput: name => ConnectionProfiles.validateName(name, existingNames) + }); + if (!name) { + return; + } - if (name) { const connection = instance.getConnection(); + const localConfigFilePath = process.env.APPDATA ? path.join(process.env.APPDATA, 'Code', 'User', 'settings.json') : undefined; + const profilesConfigFile = connection?.getConfigFile(`profiles`); + const profilesConfigFilePath = profilesConfigFile?.getPaths().server; + const locationItems = [ + { label: `Local`, description: `Stored on this PC (for your use only)`, detail: localConfigFilePath, iconPath: new ThemeIcon(`person`) }, + { label: `Server`, description: `Stored on IBM i (to be used amongst your team)`, detail: profilesConfigFilePath, iconPath: new ThemeIcon(`vm`) } + ]; + const type = await vscode.window.showQuickPick(locationItems, { + title: `Select what type of profile this is`, + placeHolder: `Profile type` + }); + if (!type) { + return; + } + const isServerProfile = type.label === `Local` ? false : true; + const homeDirectory = connection?.getConfig().homeDirectory || `/home/${connection?.currentUser || 'QPGMR'}`; //QPGMR case should not happen, but better be safe here - const profile: ConnectionProfile = from ? cloneProfile(from, name) : { - name, - homeDirectory, - currentLibrary: 'QGPL', - libraryList: ["QGPL", "QTEMP"], - customVariables: [], - ifsShortcuts: [homeDirectory], - objectFilters: [], - }; + let profile: AnyConnectionProfile; + if (from) { + // Copy existing profile + const { homeDirectory, ...clone } = cloneProfile(from, name); + profile = isServerProfile ? { + ...clone, + type: `server`, + state: `In Sync`, + } : { + ...clone, + type: `local`, + homeDirectory, + } + } else { + // Create new profile + profile = isServerProfile ? { + name, + type: `server`, + state: `In Sync`, + currentLibrary: 'QGPL', + libraryList: ["QGPL", "QTEMP"], + customVariables: [], + ifsShortcuts: [homeDirectory], + objectFilters: [], + } : { + name, + type: `local`, + homeDirectory, + currentLibrary: 'QGPL', + libraryList: ["QGPL", "QTEMP"], + customVariables: [], + ifsShortcuts: [homeDirectory], + objectFilters: [], + }; + } + await updateConnectionProfile(profile); - environmentView.refresh(environmentView.profilesNode); + + // Do an explicit refresh as creation / copy of a profile doesn't impact the active profile so onCodeForIBMiConfigurationChange is not called + environmentView.refresh(); + if (!from) { vscode.commands.executeCommand("code-for-ibmi.environment.profile.edit", profile); - } - else { - vscode.window.showInformationMessage(l10n.t("Created connection Profile '{0}'.", profile.name), l10n.t("Activate profile {0}", profile.name)) + } else { + vscode.window.showInformationMessage(l10n.t("Created {0} profile '{1}'.", isServerProfile ? l10n.t("server") : "local", profile.name), l10n.t("Activate profile")) .then(doSwitch => { if (doSwitch) { vscode.commands.executeCommand("code-for-ibmi.environment.profile.activate", profile); } }) } - } + }); }), vscode.commands.registerCommand("code-for-ibmi.environment.profile.fromCurrent", async (profilesNode: ProfilesNode) => { const config = instance.getConnection()?.getConfig(); @@ -250,92 +319,118 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { vscode.commands.executeCommand("code-for-ibmi.environment.profile.create", undefined, current); } }), - vscode.commands.registerCommand("code-for-ibmi.environment.profile.edit", async (profile: ConnectionProfile) => { - editConnectionProfile(profile, async () => environmentView.refresh(environmentView.profilesNode)) + vscode.commands.registerCommand("code-for-ibmi.environment.profile.edit", async (profile: AnyConnectionProfile) => { + editConnectionProfile(profile) }), vscode.commands.registerCommand("code-for-ibmi.environment.profile.rename", async (item: ProfileItem) => { - if (isProfileEdited(item.profile)) { - vscode.window.showWarningMessage(l10n.t("Profile {0} is being edited. Please close its editor first.", item.profile.name)); - } - else { - const currentName = item.profile.name; - const existingNames = getConnectionProfiles().map(profile => profile.name).filter(name => name !== currentName); - const newName = await vscode.window.showInputBox({ - title: l10n.t('Enter Profile {0} new name', item.profile.name), - placeHolder: l10n.t("Profile name..."), - validateInput: name => ConnectionProfiles.validateName(name, existingNames) - }); - - if (newName) { - await updateConnectionProfile(item.profile, { newName }); - const config = instance.getConnection()?.getConfig(); - if (config?.currentProfile === currentName) { - config.currentProfile = newName; - await IBMi.connectionManager.update(config); - updateUIContext(newName); + await vscode.window.withProgress({ location: { viewId: `environmentView` } }, async (progress) => { + if (isProfileEdited(item.profile)) { + vscode.window.showWarningMessage(l10n.t("{0} profile {1} is being edited. Please close its editor first.", item.profile.type === `server` ? l10n.t("Server") : "Local", item.profile.name)); + } + else { + const currentName = item.profile.name; + const currentType = item.profile.type; + const existingNames = (await getAllConnectionProfiles()).map(profile => profile.name).filter(name => name !== currentName); + const newName = await vscode.window.showInputBox({ + title: l10n.t('Enter {0} profile {1} new name', item.profile.type === `server` ? l10n.t("server") : "local", item.profile.name), + placeHolder: l10n.t("Profile name..."), + validateInput: name => ConnectionProfiles.validateName(name, existingNames) + }); + + if (newName) { + const canProceed = await verifyLatestServerProfileState(item.profile); + if (canProceed) { + await updateConnectionProfile(item.profile, { newName }); + const config = instance.getConnection()?.getConfig(); + if (config?.currentProfile === currentName && config?.currentProfileType === currentType) { + updateUIContext(newName); + } + } } - environmentView.refresh(environmentView.profilesNode); } - } + }); }), vscode.commands.registerCommand("code-for-ibmi.environment.profile.copy", async (item: ProfileItem) => { vscode.commands.executeCommand("code-for-ibmi.environment.profile.create", undefined, item.profile); }), vscode.commands.registerCommand("code-for-ibmi.environment.profile.delete", async (item: ProfileItem) => { - if (isProfileEdited(item.profile)) { - vscode.window.showWarningMessage(l10n.t("Profile {0} is being edited. Please close its editor first.", item.profile.name)); - } - else if (await vscode.window.showInformationMessage(l10n.t("Do you really want to delete profile '{0}' ?", item.profile.name), { modal: true }, l10n.t("Yes"))) { - await updateConnectionProfile(item.profile, { delete: true }); - environmentView.refresh(environmentView.profilesNode); - } - }), - vscode.commands.registerCommand("code-for-ibmi.environment.profile.activate", async (item: ProfileItem | ConnectionProfile) => { - const connection = instance.getConnection(); - const storage = instance.getStorage(); - if (connection && storage) { - const profile = "profile" in item ? item.profile : item; - const config = connection.getConfig(); - const profileToBackup = config.currentProfile ? getConnectionProfile(config.currentProfile) : getDefaultProfile(); - - if (isProfileEdited(profile)) { - vscode.window.showWarningMessage(l10n.t("Profile {0} is being edited. Please close its editor before activating it.", profile.name)); - return; - } - else if (profileToBackup && isProfileEdited(profileToBackup)) { - vscode.window.showWarningMessage(l10n.t("Profile {0} is being edited. Please close its editor before unloading it.", profileToBackup.name)); - return; + await vscode.window.withProgress({ location: { viewId: `environmentView` } }, async (progress) => { + if (isProfileEdited(item.profile)) { + vscode.window.showWarningMessage(l10n.t("{0} profile {1} is being edited. Please close its editor first.", item.profile.type === `server` ? l10n.t("Server") : "Local", item.profile.name)); } + else if (await vscode.window.showInformationMessage(l10n.t("Do you really want to delete {0} profile '{1}' ?", item.profile.type === `server` ? l10n.t("server") : l10n.t("local"), item.profile.name), { modal: true }, l10n.t("Yes"))) { + const canProceed = await verifyLatestServerProfileState(item.profile); + if (canProceed) { + await updateConnectionProfile(item.profile, { delete: true }); - if (profileToBackup) { - assignProfile(config, profileToBackup); + // Do an explicit refresh as creation / copy of a profile doesn't impact the active profile so onCodeForIBMiConfigurationChange is not called + environmentView.refresh(); + } } - assignProfile(profile, config); - config.currentProfile = profile.name || undefined; - await IBMi.connectionManager.update(config); - - await Promise.all([ - vscode.commands.executeCommand(`code-for-ibmi.refreshLibraryListView`), - vscode.commands.executeCommand(`code-for-ibmi.refreshIFSBrowser`), - vscode.commands.executeCommand(`code-for-ibmi.refreshObjectBrowser`) - ]); - environmentView.refresh(); + }); + }), + vscode.commands.registerCommand("code-for-ibmi.environment.profile.activate", async (item: ProfileItem | AnyConnectionProfile) => { + await vscode.window.withProgress({ location: { viewId: `environmentView` } }, async (progress) => { + const connection = instance.getConnection(); + const storage = instance.getStorage(); + if (connection && storage) { + const profile = "profile" in item ? item.profile : item; + const config = connection.getConfig(); + const profileToBackup = config.currentProfile ? + await getConnectionProfile(config.currentProfile, config.currentProfileType || 'local') : + getDefaultProfile(config); + + if (isProfileEdited(profile)) { + vscode.window.showWarningMessage(l10n.t("{0} profile {1} is being edited. Please close its editor before activating it.", config.currentProfileType === `server` ? l10n.t("Server") : "Local", profile.name)); + return; + } else if (profileToBackup && isProfileEdited(profileToBackup)) { + vscode.window.showWarningMessage(l10n.t("{0} profile {1} is being edited. Please close its editor before unloading it.", config.currentProfileType === `server` ? l10n.t("Server") : "Local", profileToBackup.name)); + return; + } - if (profile.name && profile.setLibraryListCommand) { - await vscode.commands.executeCommand("code-for-ibmi.environment.profile.runLiblistCommand", profile); - } + // Back up previous profile + if (profileToBackup) { + const canProceed = await verifyLatestServerProfileState(profileToBackup, { ensureInSync: true }); + if (!canProceed) { + return; + } + assignProfile(config, profileToBackup); + } - await updateUIContext(profile.name); - vscode.window.showInformationMessage(config.currentProfile ? l10n.t(`Switched to profile "{0}".`, profile.name) : l10n.t("Active profile unloaded")); - } + // Activate new profile + assignProfile(profile, config); + config.currentProfile = profile.name || ""; + config.currentProfileType = profile.type; + config.currentProfileLastKnownUpdate = profile.type === 'server' ? profile.lastUpdated : undefined; + + if (profileToBackup) { + await updateConnectionProfile(profileToBackup, { modifiedConfig: config }); + } else { + await IBMi.connectionManager.update(config); + } + + await Promise.all([ + vscode.commands.executeCommand(`code-for-ibmi.refreshLibraryListView`), + vscode.commands.executeCommand(`code-for-ibmi.refreshIFSBrowser`), + vscode.commands.executeCommand(`code-for-ibmi.refreshObjectBrowser`) + ]); + + if (profile.name && profile.setLibraryListCommand) { + await vscode.commands.executeCommand("code-for-ibmi.environment.profile.runLiblistCommand", profile); + } + + await updateUIContext(profile.name); + vscode.window.showInformationMessage(config.currentProfile ? l10n.t(`Switched to {0} profile '{1}'.`, profile.type === `server` ? l10n.t("server") : "local", profile.name) : l10n.t("Active profile unloaded")); + } + }); }), - vscode.commands.registerCommand("code-for-ibmi.environment.profile.runLiblistCommand", async (profileItem?: ProfileItem | ConnectionProfile) => { + vscode.commands.registerCommand("code-for-ibmi.environment.profile.runLiblistCommand", async (item: ProfileItem | ConnectionProfile) => { const connection = instance.getConnection(); const storage = instance.getStorage(); if (connection && storage) { + const profile = "profile" in item ? item.profile : item; const config = connection.getConfig(); - const profile = profileItem && ("profile" in profileItem ? profileItem?.profile : profileItem) || getConnectionProfile(config.get); if (profile?.setLibraryListCommand) { const command = profile.setLibraryListCommand.startsWith(`?`) ? @@ -364,27 +459,109 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { } } }), - vscode.commands.registerCommand("code-for-ibmi.environment.profile.unload", async () => { - vscode.commands.executeCommand("code-for-ibmi.environment.profile.activate", getDefaultProfile()); + vscode.commands.registerCommand("code-for-ibmi.environment.profile.unload", async (item: ProfileItem) => { + const connection = instance.getConnection(); + if (connection) { + const canProceed = await verifyLatestServerProfileState(item.profile, { ensureInSync: true }); + if (canProceed) { + const config = connection.getConfig(); + vscode.commands.executeCommand("code-for-ibmi.environment.profile.activate", getDefaultProfile(config)); + } + } + }), + vscode.commands.registerCommand("code-for-ibmi.resolveProfile.saveChangeToServer", async (item: ProfileItem, overwrite: boolean = false) => { + await vscode.window.withProgress({ location: { viewId: `environmentView` } }, async (progress) => { + const connection = instance.getConnection(); + if (connection) { + const canProceed = await verifyLatestServerProfileState(item.profile); + if (canProceed) { + const config = connection.getConfig(); + const profile = item.profile; + assignProfile(config, profile); + await updateConnectionProfile(profile); + if (overwrite) { + vscode.window.showInformationMessage(l10n.t("Saved changes to server profile '{0}'.", item.profile.name)); + } else { + vscode.window.showInformationMessage(l10n.t("Overwrote server profile '{0}' with local changes.", item.profile.name)); + } + } + } + }); + }), + vscode.commands.registerCommand("code-for-ibmi.resolveProfile.discardChangesAndSyncWithServer", async (item: ProfileItem) => { + await vscode.window.withProgress({ location: { viewId: `environmentView` } }, async (progress) => { + const connection = instance.getConnection(); + if (connection) { + const canProceed = await verifyLatestServerProfileState(item.profile); + if (canProceed) { + const config = connection.getConfig(); + const profile = item.profile; + assignProfile(profile, config); + config.currentProfileLastKnownUpdate = profile.type === 'server' ? profile.lastUpdated : undefined; + + await IBMi.connectionManager.update(config); + + await Promise.all([ + vscode.commands.executeCommand(`code-for-ibmi.refreshLibraryListView`), + vscode.commands.executeCommand(`code-for-ibmi.refreshIFSBrowser`), + vscode.commands.executeCommand(`code-for-ibmi.refreshObjectBrowser`) + ]); + + vscode.window.showInformationMessage(l10n.t("Discarded local changes and synced with server profile '{0}'.", item.profile.name)); + } + } + }); + }), + vscode.commands.registerCommand("code-for-ibmi.resolveProfile.overwriteChangesToServer", async (item: ProfileItem) => { + vscode.commands.executeCommand("code-for-ibmi.resolveProfile.saveChangeToServer", item, true); + }), + + onCodeForIBMiConfigurationChange("connectionSettings", async () => { + const connection = instance.getConnection(); + if (connection) { + environmentView.refresh(); + } }) ); +} - instance.subscribe(context, 'connected', 'Update environment view description', async () => { - const config = instance.getConnection()?.getConfig(); - const storage = instance.getStorage(); - if (config && storage) { - //Retrieve and clear old value for last used profile - const deprecatedLastProfile = storage.getLastProfile(); - if (deprecatedLastProfile) { - if (deprecatedLastProfile.toLocaleLowerCase() !== 'default') { - config.currentProfile = deprecatedLastProfile; - await IBMi.connectionManager.update(config); +export async function verifyLatestServerProfileState(profile: AnyConnectionProfile, options: { ensureInSync: boolean } = { ensureInSync: false }): Promise { + if (profile.type === `server`) { + const isActive = isActiveProfile(profile); + if (isActive) { + // Get current profile state + const currentState: ProfileState = profile.state; + + // Reload server profiles in case another user changed them before the last fetch + await vscode.commands.executeCommand("code-for-ibmi.environment.refresh"); + + // Get updated profile state + let updatedState: ProfileState; + const updatedServerProfile = await getConnectionProfile(profile.name, profile.type); + if (updatedServerProfile) { + updatedState = (updatedServerProfile as ServerConnectionProfile).state; + } else { + updatedState = "Out of Sync"; + } + + if (currentState !== updatedState) { + window.showErrorMessage(l10n.t("Server Profile {0} state changed to \"{1}\" after fetching the latest server profiles. Please try again.", profile.name, updatedState)); + return false; + } + + if (options.ensureInSync) { + if (updatedState === `Modified`) { + window.showErrorMessage(l10n.t("Server Profile {0} has been modified. Resolve this before proceeding.", profile.name, updatedState)); + return false; + } else if (updatedState === `Out of Sync`) { + window.showErrorMessage(l10n.t("Server Profile {0} is out of sync. Resolve this before proceeding.", profile.name, updatedState)); + return false; } - await storage.clearDeprecatedLastProfile(); } - updateUIContext(config.currentProfile); } - }); + } + + return true; } class EnvironmentView implements vscode.TreeDataProvider {