diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ef137e0ac910363d7968c48238e4a2517e988bee
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,63 @@
+name: Bug Report
+description: Report something behaving in an unexpected manor.
+body:
+  - type: markdown
+    attributes:
+      value: >
+        **Please check for similar issues before continuing!**
+  - type: dropdown
+    id: OS
+    attributes:
+      label: Which OS are you using?
+      options:
+        - Windows
+        - Linux
+        - MacOS
+        - Windows WSL
+        - Other
+    validations:
+      required: true
+  - type: textarea
+    id: expected
+    attributes:
+      label: Expected Behaviour
+      description: What is the expected behaviour?
+    validations:
+      required: true
+  - type: textarea
+    id: actual
+    attributes:
+      label: Actual Behaviour
+      description: What is actually happening that is unexpected?
+    validations:
+      required: true
+  - type: textarea
+    id: reproduction
+    attributes:
+      label: Reproduction steps
+      description: >
+        Please provide detailed steps to reproduce the error. This will help us
+        to diagnose, test fixes, and fix the issue.
+      value: |
+        1. Go to '...'
+        2. Click '...'
+        3. See error '...'
+    validations:
+      required: true
+  - type: textarea
+    id: additional-notes
+    attributes:
+      label: Additional Notes
+      description: >
+        Please provide any additional notes, context, and media you have.
+  - type: textarea
+    id: log
+    attributes:
+      label: Log
+      description: >
+        Please provide your log. The log can be found in VS Code by opening the
+        `OUTPUT` panel and selecting `Lua Addon Manager` from the dropdown.
+  - type: markdown
+    attributes:
+      value: |
+        Thank you very much for helping us improve! ❤️
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f5152ea5151227f47b0ebb12e9e030f7cfb12413..e916b978fca9de1d111457fbf30206566edcee93 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -44,7 +44,13 @@ jobs:
           curl -sL https://deb.nodesource.com/setup_14.x -o /tmp/nodesource_setup.sh # For nodejs
           sudo bash /tmp/nodesource_setup.sh
           apt-get update
-          apt-get install -y git gcc-9 g++-9 nodejs
+          apt-get install -y git gcc-9 g++-9
+          update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 100
+          update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-9 100
+          cd ~
+          curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh
+          sudo bash /tmp/nodesource_setup.sh
+          sudo apt install nodejs
 
       - name: Install aarch64-linux-gnu
         if: ${{ matrix.platform == 'linux-arm64' }}
@@ -61,7 +67,9 @@ jobs:
       - name: Run luamake
         id: luamake
         working-directory: ./server
-        run: luamake -platform ${{ matrix.platform }}
+        run: |
+          luamake -platform ${{ matrix.platform }}
+          rm -r ./build
 
       - name: Setting up workflow variables
         id: vars
@@ -83,15 +91,23 @@ jobs:
           echo PKG_BASENAME=${PKG_BASENAME} >> $GITHUB_OUTPUT
           echo PKG_NAME=${PKG_NAME} >> $GITHUB_OUTPUT
 
-      - name: Compile clinet
+      - name: Compile client
         shell: bash
         run: |
           npm install -g typescript
           cd client
-          npm install
-          tsc
+          npm ci
+          npm run build
           cd ..
 
+      - name: Build Addon Manager WebVue
+        shell: bash
+        run: |
+          cd client/webvue
+          npm ci
+          npm run build
+          cd ../..
+
       - name: Pack vsix
         id: pack
         shell: bash
diff --git a/.gitmodules b/.gitmodules
index cb78f57376e1ff8e7eee2ddf6bc7765ff9050f39..c11748d08ec3ccd634d82b8847e5990f51d4f1a7 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
 [submodule "client/3rd/vscode-lua-doc"]
 	path = client/3rd/vscode-lua-doc
 	url = https://github.com/LuaLS/vscode-lua-doc
+[submodule "client/webvue"]
+	path = client/webvue
+	url = https://github.com/LuaLS/vscode-lua-webvue
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 1653e33e84d296667a3c1540971f2d1f7a74cb8c..f1dc18332869ed3b969d628f69fdbf3fabb7d404 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -1,31 +1,26 @@
 {
-    "version": "2.0.0",
-    "type": "shell",
-    "windows": {
-        "options": {
-            "shell": {
-                "executable": "C:\\Windows\\System32\\cmd.exe",
-                "args": [
-                    "/c"
-                ]
-            }
-        }
-    },
-    "options": {
-        "cwd": "${workspaceFolder}"
-    },
-    "tasks": [
-        {
-            "type": "typescript",
-            "tsconfig": "client/tsconfig.json",
-            "problemMatcher": [
-                "$tsc"
-            ],
-            "group": {
-                "kind": "build",
-                "isDefault": true
-            },
-            "label": "tsc: 构建 - client/tsconfig.json"
-        }
-    ]
+  "version": "2.0.0",
+  "tasks": [
+    {
+      "type": "shell",
+      "command": "./buildClient.sh",
+      "windows": {
+        "command": ".\\buildClient.bat"
+      },
+      "group": {
+        "kind": "build",
+        "isDefault": true
+      },
+      "presentation": {
+        "echo": true,
+        "reveal": "always",
+        "focus": false,
+        "panel": "dedicated",
+        "showReuseMessage": false,
+        "clear": true
+      },
+      "icon": { "color": "terminal.ansiCyan", "id": "server-process" },
+      "label": "Build Client"
+    }
+  ]
 }
diff --git a/.vscodeignore b/.vscodeignore
index 8ae2300ac38e48a1b57f19abacdf7afa514c2a40..8b7af607bd709e35397c0427c9b69e8d318e4cc2 100644
--- a/.vscodeignore
+++ b/.vscodeignore
@@ -5,6 +5,7 @@
 !client/package.json
 !client/3rd/vscode-lua-doc/doc
 !client/3rd/vscode-lua-doc/extension.js
+!client/webvue/build
 
 !server/bin
 !server/locale
diff --git a/README.md b/README.md
index 9921b43db62f4ced0c9f44573ec6d040effdd551..db6c8d30369e8368134331f117b069bae0b9b702 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,9 @@
 # lua-language-server
 
-![build](https://github.com/LuaLS/lua-language-server/workflows/build/badge.png)
+[![build](https://github.com/LuaLS/lua-language-server/actions/workflows/build.yml/badge.png?branch=master)](https://github.com/LuaLS/lua-language-server/actions/workflows/build.yml)
 [![version](https://vsmarketplacebadges.dev/version-short/sumneko.lua.png)](https://marketplace.visualstudio.com/items?itemName=sumneko.lua)
 ![installs](https://vsmarketplacebadges.dev/installs-short/sumneko.lua.png)
 ![downloads](https://vsmarketplacebadges.dev/downloads-short/sumneko.lua.png)
-[![Average time to resolve an issue](https://isitmaintained.com/badge/resolution/LuaLS/lua-language-server.png)](https://github.com/LuaLS/lua-language-server/issues "Average time to resolve an issue")
 
 
 ***Lua development just got a whole lot better*** 🧠
@@ -33,25 +32,25 @@ The Lua language server provides various language features for Lua to make devel
 The language server can easily be installed for use in VS Code, but it can also be used by other clients using the command line.
 
 ### Visual Studio Code
-[![Install in VS Code](https://img.shields.io/badge/Install%20For-VS%20Code-blue?style=for-the-badge&logo=visualstudiocode "Install in VS Code")](https://marketplace.visualstudio.com/items?itemName=sumneko.lua)
+[![Install in VS Code](https://img.shields.io/badge/VS%20Code-Install-blue?style=for-the-badge&logo=visualstudiocode "Install in VS Code")](https://marketplace.visualstudio.com/items?itemName=sumneko.lua)
 
 The language server and Visual Studio Code client can be installed from [the VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=sumneko.lua).
 
 ![](https://github.com/LuaLS/vscode-lua/raw/master/images//Install%20In%20VSCode.gif)
 
 ### Command Line
-[![Install for command line](https://img.shields.io/badge/Install%20For-Command%20Line-blue?style=for-the-badge&logo=windowsterminal "Install for command line")](https://github.com/LuaLS/lua-language-server/wiki/Getting-Started#command-line)
+[![Install for command line](https://img.shields.io/badge/Command%20Line-Install-blue?style=for-the-badge&logo=windowsterminal "Install for command line")](https://github.com/LuaLS/lua-language-server/wiki/Getting-Started#command-line)
 
-Check the [wiki for a guide](https://github.com/LuaLS/lua-language-server/wiki/Getting-Started#command-line) to install the language server for use on the command line. This allows the language server to be used for NeoVim and other clients that follow the language server protocol.
+Check the [wiki for a guide](https://github.com/LuaLS/lua-language-server/wiki/Getting-Started#command-line) to install the language server for use on the command line. This allows the language server to be used with NeoVim and [other clients](https://microsoft.github.io/language-server-protocol/implementors/tools/) that follow the [language server protocol](https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/).
 
 ## Supported Lua Versions
-| Version |    Supported   |
+| Version |   Supported    |
 | :-----: | :------------: |
-| Lua 5.1 | ![][checkmark] |
-| Lua 5.2 | ![][checkmark] |
-| Lua 5.3 | ![][checkmark] |
-| Lua 5.4 | ![][checkmark] |
-| LuaJIT  | ![][checkmark] |
+| Lua 5.1 | ![✅][checkmark] |
+| Lua 5.2 | ![✅][checkmark] |
+| Lua 5.3 | ![✅][checkmark] |
+| Lua 5.4 | ![✅][checkmark] |
+| LuaJIT  | ![✅][checkmark] |
 
 ## Links
 - [Changelog](https://github.com/LuaLS/lua-language-server/blob/master/changelog.md)
@@ -76,14 +75,13 @@ Check the [wiki for a guide](https://github.com/LuaLS/lua-language-server/wiki/G
 - `pt-br` 🇧🇷
 
 
-> ℹ Note: All translations are provided and collaborated on by the community. If you find an inappropriate or harmful translation, [please report it immediately](https://github.com/LuaLS/lua-language-server/issues).
+> **Note**
+> All translations are provided and collaborated on by the community. If you find an inappropriate or harmful translation, [please report it immediately](https://github.com/LuaLS/lua-language-server/issues).
 
 Are you able to [provide a translation](https://github.com/LuaLS/lua-language-server/wiki/Translations)? It would be greatly appreciated!
 
 Thank you to [all contributors of translations](https://github.com/LuaLS/lua-language-server/commits/master/locale)!
 
-[en-US]: https://github.com/LuaLS/lua-language-server/tree/master/locale/en-us
-
 ## Configuration
 Configuration of the server can be done in a number of ways, which are explained more in-depth in the [wiki](https://github.com/LuaLS/lua-language-server/wiki/Configuration-File).
 
@@ -95,7 +93,7 @@ See the [configuration file wiki page](https://github.com/LuaLS/lua-language-ser
 
 
 ## Privacy
-This language server has **opt-in** telemetry that collects usage data and sends it to the development team to help improve the extension. Read our [privacy policy](https://github.com/LuaLS/lua-language-server/wiki/Home#privacy) to learn more.
+The language server had **opt-in** telemetry that collected usage data and sent it to the development team to help improve the extension. Read our [privacy policy](https://github.com/LuaLS/lua-language-server/wiki/Home#privacy) to learn more. Telemetry was removed in `v3.6.5` and is no longer part of the language server.
 
 
 ## Contributors
diff --git a/buildClient.bat b/buildClient.bat
new file mode 100644
index 0000000000000000000000000000000000000000..f6ce14b74d4723af295d63754ca87a758aced546
--- /dev/null
+++ b/buildClient.bat
@@ -0,0 +1,15 @@
+@echo off
+
+echo Building VS Code Extension Client...
+
+echo Compiling TypeScript...
+cd client
+call npm i
+call npm run build
+
+echo Building Addon Manager WebVue...
+cd webvue
+call npm i
+call npm run build
+
+echo Build complete!
diff --git a/buildClient.sh b/buildClient.sh
new file mode 100644
index 0000000000000000000000000000000000000000..694f0fe16aced47eb10dc4123f6f2caa6785d255
--- /dev/null
+++ b/buildClient.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+set -e
+
+bold=$(tput bold)
+normal=$(tput sgr0)
+
+black='\e[0;30m'
+red='\e[0;31m'
+green='\e[0;32m'
+cyan='\e[0;36m'
+
+echo -e "${red}${bold}Building VS Code Extension Client..."
+
+echo -e "${cyan}${bold}Compiling TypeScript...${black}${normal}"
+cd client
+npm i
+npm run build
+
+echo -e "${green}${bold}Building Addon Manager WebVue...${black}${normal}"
+cd webvue
+npm i
+npm run build
+
+echo -e "${green}${bold}Build complete!${black}${normal}"
diff --git a/changelog.md b/changelog.md
index 6adb9acc1328d2b5aa2be58f895f62a84202be35..5009ced8f0c60eb9f5851f5b6e3c57f389913f52 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,52 @@
 # changelog
 
+## 3.6.17
+`2023-3-9`
+* `CHG` export documents: export global variables
+* `FIX` [#1715]
+* `FIX` [#1753]
+* `FIX` [#1914]
+* `FIX` [#1922]
+* `FIX` [#1924]
+* `FIX` [#1928]
+* `FIX` [#1945]
+* `FIX` [#1955]
+* `FIX` [#1978]
+
+[#1715]: https://github.com/LuaLS/lua-language-server/issues/1715
+[#1753]: https://github.com/LuaLS/lua-language-server/issues/1753
+[#1914]: https://github.com/LuaLS/lua-language-server/issues/1914
+[#1922]: https://github.com/LuaLS/lua-language-server/issues/1922
+[#1924]: https://github.com/LuaLS/lua-language-server/issues/1924
+[#1928]: https://github.com/LuaLS/lua-language-server/issues/1928
+[#1945]: https://github.com/LuaLS/lua-language-server/issues/1945
+[#1955]: https://github.com/LuaLS/lua-language-server/issues/1955
+[#1978]: https://github.com/LuaLS/lua-language-server/issues/1978
+
+## 3.6.13
+`2023-3-2`
+* `FIX` setting: `Lua.addonManager.enable` should be `true` by default
+* `FIX` failed to publish to Windows
+
+## 3.6.12
+`2023-3-2`
+* `NEW` [Addon Manager](https://github.com/LuaLS/lua-language-server/discussions/1607), try it with command `lua.addon_manager.open`. Thanks to [carsakiller](https://github.com/carsakiller)!
+
+## 3.6.11
+`2023-2-13`
+* `CHG` completion: don't show loading process
+* `FIX` [#1886]
+* `FIX` [#1887]
+* `FIX` [#1889]
+* `FIX` [#1895]
+* `FIX` [#1902]
+
+[#1886]: https://github.com/LuaLS/lua-language-server/issues/1886
+[#1887]: https://github.com/LuaLS/lua-language-server/issues/1887
+[#1889]: https://github.com/LuaLS/lua-language-server/issues/1889
+[#1895]: https://github.com/LuaLS/lua-language-server/issues/1895
+[#1902]: https://github.com/LuaLS/lua-language-server/issues/1902
+
 ## 3.6.10
 `2023-2-7`
 * `FIX` [#1869]
diff --git a/client/.eslintrc.json b/client/.eslintrc.json
new file mode 100644
index 0000000000000000000000000000000000000000..48a0afe3a74134d39d5ea8d23698c5a0b279ec03
--- /dev/null
+++ b/client/.eslintrc.json
@@ -0,0 +1,16 @@
+{
+    "root": true,
+    "parser": "@typescript-eslint/parser",
+    "plugins": ["@typescript-eslint"],
+    "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
+    "rules": {
+        "@typescript-eslint/no-namespace": "off",
+        "linebreak-style": "off",
+        "no-duplicate-imports": "warn",
+        "semi": "error",
+        "default-case": "error",
+        "default-case-last": "error",
+        "eqeqeq": "error"
+    },
+    "ignorePatterns": ["out", "dist", "**/*.d.ts"]
+}
diff --git a/client/.prettierrc.yml b/client/.prettierrc.yml
new file mode 100644
index 0000000000000000000000000000000000000000..af1e1742fe3d3b962f273dd8790398ef8ada4866
--- /dev/null
+++ b/client/.prettierrc.yml
@@ -0,0 +1,2 @@
+tabWidth: 4
+endOfLine: auto
diff --git a/client/3rd/vscode-lua-doc b/client/3rd/vscode-lua-doc
index ac128f4e47c019d41ec0d97adbf1a9a52a916e4e..208d871010255661ea9ea8933f426c12fb173af0 160000
--- a/client/3rd/vscode-lua-doc
+++ b/client/3rd/vscode-lua-doc
@@ -1 +1 @@
-Subproject commit ac128f4e47c019d41ec0d97adbf1a9a52a916e4e
+Subproject commit 208d871010255661ea9ea8933f426c12fb173af0
diff --git a/client/package-lock.json b/client/package-lock.json
index 9fdd087a8cc7a125976951b10bf786713ebd16ec..c6220b6bc7ee72b97f360bdc424a1212325b1eab 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -9,7 +9,12 @@
 			"version": "0.0.1",
 			"license": "MIT",
 			"dependencies": {
-				"vscode-languageclient": "8.0.2"
+				"axios": "^1.2.1",
+				"dayjs": "^1.11.7",
+				"simple-git": "^3.16.0",
+				"triple-beam": "^1.3.0",
+				"vscode-languageclient": "8.0.2",
+				"winston": "^3.8.2"
 			},
 			"devDependencies": {
 				"@types/node": "^17.0.35",
@@ -19,18 +24,69 @@
 				"vscode": "^1.70.0"
 			}
 		},
+		"node_modules/@colors/colors": {
+			"version": "1.5.0",
+			"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
+			"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
+			"engines": {
+				"node": ">=0.1.90"
+			}
+		},
+		"node_modules/@dabh/diagnostics": {
+			"version": "2.0.3",
+			"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
+			"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
+			"dependencies": {
+				"colorspace": "1.1.x",
+				"enabled": "2.0.x",
+				"kuler": "^2.0.0"
+			}
+		},
+		"node_modules/@kwsites/file-exists": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
+			"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
+			"dependencies": {
+				"debug": "^4.1.1"
+			}
+		},
+		"node_modules/@kwsites/promise-deferred": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
+			"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="
+		},
 		"node_modules/@types/node": {
-			"version": "17.0.35",
-			"resolved": "https://registry.npmmirror.com/@types/node/-/node-17.0.35.tgz",
-			"integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==",
+			"version": "17.0.45",
+			"resolved": "https://registry.npmmirror.com/@types/node/-/node-17.0.45.tgz",
+			"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
 			"dev": true
 		},
 		"node_modules/@types/vscode": {
-			"version": "1.73.0",
-			"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.73.0.tgz",
-			"integrity": "sha512-FhkfF7V3fj7S3WqXu7AxFesBLO3uMkdCPJJPbwyZXezv2xJ6xBWHYM2CmkkbO8wT9Fr3KipwxGGOoQRrYq7mHg==",
+			"version": "1.76.0",
+			"resolved": "https://registry.npmmirror.com/@types/vscode/-/vscode-1.76.0.tgz",
+			"integrity": "sha512-CQcY3+Fe5hNewHnOEAVYj4dd1do/QHliXaknAEYSXx2KEHUzFibDZSKptCon+HPgK55xx20pR+PBJjf0MomnBA==",
 			"dev": true
 		},
+		"node_modules/async": {
+			"version": "3.2.4",
+			"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
+			"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
+		},
+		"node_modules/asynckit": {
+			"version": "0.4.0",
+			"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+			"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+		},
+		"node_modules/axios": {
+			"version": "1.2.6",
+			"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.6.tgz",
+			"integrity": "sha512-rC/7F08XxZwjMV4iuWv+JpD3E0Ksqg9nac4IIg6RwNuF0JTeWoCo/mBNG54+tNhhI11G3/VDRbdDQTs9hGp4pQ==",
+			"dependencies": {
+				"follow-redirects": "^1.15.0",
+				"form-data": "^4.0.0",
+				"proxy-from-env": "^1.1.0"
+			}
+		},
 		"node_modules/balanced-match": {
 			"version": "1.0.2",
 			"resolved": "https://registry.nlark.com/balanced-match/download/balanced-match-1.0.2.tgz",
@@ -45,11 +101,181 @@
 				"concat-map": "0.0.1"
 			}
 		},
+		"node_modules/color": {
+			"version": "3.2.1",
+			"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
+			"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
+			"dependencies": {
+				"color-convert": "^1.9.3",
+				"color-string": "^1.6.0"
+			}
+		},
+		"node_modules/color-convert": {
+			"version": "1.9.3",
+			"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+			"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+			"dependencies": {
+				"color-name": "1.1.3"
+			}
+		},
+		"node_modules/color-name": {
+			"version": "1.1.3",
+			"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+			"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+		},
+		"node_modules/color-string": {
+			"version": "1.9.1",
+			"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+			"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+			"dependencies": {
+				"color-name": "^1.0.0",
+				"simple-swizzle": "^0.2.2"
+			}
+		},
+		"node_modules/colorspace": {
+			"version": "1.1.4",
+			"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
+			"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
+			"dependencies": {
+				"color": "^3.1.3",
+				"text-hex": "1.0.x"
+			}
+		},
+		"node_modules/combined-stream": {
+			"version": "1.0.8",
+			"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+			"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+			"dependencies": {
+				"delayed-stream": "~1.0.0"
+			},
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
 		"node_modules/concat-map": {
 			"version": "0.0.1",
 			"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
 			"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
 		},
+		"node_modules/dayjs": {
+			"version": "1.11.7",
+			"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
+			"integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
+		},
+		"node_modules/debug": {
+			"version": "4.3.4",
+			"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+			"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+			"dependencies": {
+				"ms": "2.1.2"
+			},
+			"engines": {
+				"node": ">=6.0"
+			},
+			"peerDependenciesMeta": {
+				"supports-color": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/debug/node_modules/ms": {
+			"version": "2.1.2",
+			"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+			"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+		},
+		"node_modules/delayed-stream": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+			"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+			"engines": {
+				"node": ">=0.4.0"
+			}
+		},
+		"node_modules/enabled": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
+			"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
+		},
+		"node_modules/fecha": {
+			"version": "4.2.3",
+			"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
+			"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
+		},
+		"node_modules/fn.name": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
+			"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
+		},
+		"node_modules/follow-redirects": {
+			"version": "1.15.2",
+			"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+			"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+			"funding": [
+				{
+					"type": "individual",
+					"url": "https://github.com/sponsors/RubenVerborgh"
+				}
+			],
+			"engines": {
+				"node": ">=4.0"
+			},
+			"peerDependenciesMeta": {
+				"debug": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/form-data": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+			"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+			"dependencies": {
+				"asynckit": "^0.4.0",
+				"combined-stream": "^1.0.8",
+				"mime-types": "^2.1.12"
+			},
+			"engines": {
+				"node": ">= 6"
+			}
+		},
+		"node_modules/inherits": {
+			"version": "2.0.4",
+			"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+			"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+		},
+		"node_modules/is-arrayish": {
+			"version": "0.3.2",
+			"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+			"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+		},
+		"node_modules/is-stream": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+			"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+			"engines": {
+				"node": ">=8"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
+		"node_modules/kuler": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
+			"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
+		},
+		"node_modules/logform": {
+			"version": "2.4.2",
+			"resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz",
+			"integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==",
+			"dependencies": {
+				"@colors/colors": "1.5.0",
+				"fecha": "^4.2.0",
+				"ms": "^2.1.1",
+				"safe-stable-stringify": "^2.3.1",
+				"triple-beam": "^1.3.0"
+			}
+		},
 		"node_modules/lru-cache": {
 			"version": "6.0.0",
 			"resolved": "https://registry.npm.taobao.org/lru-cache/download/lru-cache-6.0.0.tgz",
@@ -61,6 +287,25 @@
 				"node": ">=10"
 			}
 		},
+		"node_modules/mime-db": {
+			"version": "1.52.0",
+			"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+			"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
+		"node_modules/mime-types": {
+			"version": "2.1.35",
+			"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+			"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+			"dependencies": {
+				"mime-db": "1.52.0"
+			},
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
 		"node_modules/minimatch": {
 			"version": "3.1.2",
 			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -72,10 +317,68 @@
 				"node": "*"
 			}
 		},
+		"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=="
+		},
+		"node_modules/one-time": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
+			"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
+			"dependencies": {
+				"fn.name": "1.x.x"
+			}
+		},
+		"node_modules/proxy-from-env": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+			"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+		},
+		"node_modules/readable-stream": {
+			"version": "3.6.0",
+			"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+			"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+			"dependencies": {
+				"inherits": "^2.0.3",
+				"string_decoder": "^1.1.1",
+				"util-deprecate": "^1.0.1"
+			},
+			"engines": {
+				"node": ">= 6"
+			}
+		},
+		"node_modules/safe-buffer": {
+			"version": "5.2.1",
+			"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+			"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+			"funding": [
+				{
+					"type": "github",
+					"url": "https://github.com/sponsors/feross"
+				},
+				{
+					"type": "patreon",
+					"url": "https://www.patreon.com/feross"
+				},
+				{
+					"type": "consulting",
+					"url": "https://feross.org/support"
+				}
+			]
+		},
+		"node_modules/safe-stable-stringify": {
+			"version": "2.4.2",
+			"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz",
+			"integrity": "sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==",
+			"engines": {
+				"node": ">=10"
+			}
+		},
 		"node_modules/semver": {
-			"version": "7.3.5",
-			"resolved": "https://registry.nlark.com/semver/download/semver-7.3.5.tgz?cache=0&sync_timestamp=1622685835879&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsemver%2Fdownload%2Fsemver-7.3.5.tgz",
-			"integrity": "sha1-C2Ich5NI2JmOSw5L6Us/EuYBjvc=",
+			"version": "7.3.8",
+			"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+			"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
 			"dependencies": {
 				"lru-cache": "^6.0.0"
 			},
@@ -86,6 +389,59 @@
 				"node": ">=10"
 			}
 		},
+		"node_modules/simple-git": {
+			"version": "3.16.0",
+			"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.16.0.tgz",
+			"integrity": "sha512-zuWYsOLEhbJRWVxpjdiXl6eyAyGo/KzVW+KFhhw9MqEEJttcq+32jTWSGyxTdf9e/YCohxRE+9xpWFj9FdiJNw==",
+			"dependencies": {
+				"@kwsites/file-exists": "^1.1.1",
+				"@kwsites/promise-deferred": "^1.1.1",
+				"debug": "^4.3.4"
+			},
+			"funding": {
+				"type": "github",
+				"url": "https://github.com/steveukx/git-js?sponsor=1"
+			}
+		},
+		"node_modules/simple-swizzle": {
+			"version": "0.2.2",
+			"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+			"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+			"dependencies": {
+				"is-arrayish": "^0.3.1"
+			}
+		},
+		"node_modules/stack-trace": {
+			"version": "0.0.10",
+			"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
+			"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
+			"engines": {
+				"node": "*"
+			}
+		},
+		"node_modules/string_decoder": {
+			"version": "1.3.0",
+			"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+			"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+			"dependencies": {
+				"safe-buffer": "~5.2.0"
+			}
+		},
+		"node_modules/text-hex": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
+			"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
+		},
+		"node_modules/triple-beam": {
+			"version": "1.3.0",
+			"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
+			"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
+		},
+		"node_modules/util-deprecate": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+			"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+		},
 		"node_modules/vscode-jsonrpc": {
 			"version": "8.0.2",
 			"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz",
@@ -121,6 +477,40 @@
 			"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz",
 			"integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA=="
 		},
+		"node_modules/winston": {
+			"version": "3.8.2",
+			"resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz",
+			"integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==",
+			"dependencies": {
+				"@colors/colors": "1.5.0",
+				"@dabh/diagnostics": "^2.0.2",
+				"async": "^3.2.3",
+				"is-stream": "^2.0.0",
+				"logform": "^2.4.0",
+				"one-time": "^1.0.0",
+				"readable-stream": "^3.4.0",
+				"safe-stable-stringify": "^2.3.1",
+				"stack-trace": "0.0.x",
+				"triple-beam": "^1.3.0",
+				"winston-transport": "^4.5.0"
+			},
+			"engines": {
+				"node": ">= 12.0.0"
+			}
+		},
+		"node_modules/winston-transport": {
+			"version": "4.5.0",
+			"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
+			"integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==",
+			"dependencies": {
+				"logform": "^2.3.2",
+				"readable-stream": "^3.6.0",
+				"triple-beam": "^1.3.0"
+			},
+			"engines": {
+				"node": ">= 6.4.0"
+			}
+		},
 		"node_modules/yallist": {
 			"version": "4.0.0",
 			"resolved": "https://registry.npm.taobao.org/yallist/download/yallist-4.0.0.tgz",
@@ -128,18 +518,66 @@
 		}
 	},
 	"dependencies": {
+		"@colors/colors": {
+			"version": "1.5.0",
+			"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
+			"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="
+		},
+		"@dabh/diagnostics": {
+			"version": "2.0.3",
+			"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
+			"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
+			"requires": {
+				"colorspace": "1.1.x",
+				"enabled": "2.0.x",
+				"kuler": "^2.0.0"
+			}
+		},
+		"@kwsites/file-exists": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
+			"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
+			"requires": {
+				"debug": "^4.1.1"
+			}
+		},
+		"@kwsites/promise-deferred": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
+			"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="
+		},
 		"@types/node": {
-			"version": "17.0.35",
-			"resolved": "https://registry.npmmirror.com/@types/node/-/node-17.0.35.tgz",
-			"integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==",
+			"version": "17.0.45",
+			"resolved": "https://registry.npmmirror.com/@types/node/-/node-17.0.45.tgz",
+			"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
 			"dev": true
 		},
 		"@types/vscode": {
-			"version": "1.73.0",
-			"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.73.0.tgz",
-			"integrity": "sha512-FhkfF7V3fj7S3WqXu7AxFesBLO3uMkdCPJJPbwyZXezv2xJ6xBWHYM2CmkkbO8wT9Fr3KipwxGGOoQRrYq7mHg==",
+			"version": "1.76.0",
+			"resolved": "https://registry.npmmirror.com/@types/vscode/-/vscode-1.76.0.tgz",
+			"integrity": "sha512-CQcY3+Fe5hNewHnOEAVYj4dd1do/QHliXaknAEYSXx2KEHUzFibDZSKptCon+HPgK55xx20pR+PBJjf0MomnBA==",
 			"dev": true
 		},
+		"async": {
+			"version": "3.2.4",
+			"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
+			"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
+		},
+		"asynckit": {
+			"version": "0.4.0",
+			"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+			"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+		},
+		"axios": {
+			"version": "1.2.6",
+			"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.6.tgz",
+			"integrity": "sha512-rC/7F08XxZwjMV4iuWv+JpD3E0Ksqg9nac4IIg6RwNuF0JTeWoCo/mBNG54+tNhhI11G3/VDRbdDQTs9hGp4pQ==",
+			"requires": {
+				"follow-redirects": "^1.15.0",
+				"form-data": "^4.0.0",
+				"proxy-from-env": "^1.1.0"
+			}
+		},
 		"balanced-match": {
 			"version": "1.0.2",
 			"resolved": "https://registry.nlark.com/balanced-match/download/balanced-match-1.0.2.tgz",
@@ -154,11 +592,146 @@
 				"concat-map": "0.0.1"
 			}
 		},
+		"color": {
+			"version": "3.2.1",
+			"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
+			"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
+			"requires": {
+				"color-convert": "^1.9.3",
+				"color-string": "^1.6.0"
+			}
+		},
+		"color-convert": {
+			"version": "1.9.3",
+			"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+			"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+			"requires": {
+				"color-name": "1.1.3"
+			}
+		},
+		"color-name": {
+			"version": "1.1.3",
+			"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+			"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+		},
+		"color-string": {
+			"version": "1.9.1",
+			"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+			"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+			"requires": {
+				"color-name": "^1.0.0",
+				"simple-swizzle": "^0.2.2"
+			}
+		},
+		"colorspace": {
+			"version": "1.1.4",
+			"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
+			"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
+			"requires": {
+				"color": "^3.1.3",
+				"text-hex": "1.0.x"
+			}
+		},
+		"combined-stream": {
+			"version": "1.0.8",
+			"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+			"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+			"requires": {
+				"delayed-stream": "~1.0.0"
+			}
+		},
 		"concat-map": {
 			"version": "0.0.1",
 			"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
 			"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
 		},
+		"dayjs": {
+			"version": "1.11.7",
+			"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
+			"integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
+		},
+		"debug": {
+			"version": "4.3.4",
+			"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+			"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+			"requires": {
+				"ms": "2.1.2"
+			},
+			"dependencies": {
+				"ms": {
+					"version": "2.1.2",
+					"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+					"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+				}
+			}
+		},
+		"delayed-stream": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+			"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
+		},
+		"enabled": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
+			"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
+		},
+		"fecha": {
+			"version": "4.2.3",
+			"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
+			"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
+		},
+		"fn.name": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
+			"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
+		},
+		"follow-redirects": {
+			"version": "1.15.2",
+			"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+			"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
+		},
+		"form-data": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+			"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+			"requires": {
+				"asynckit": "^0.4.0",
+				"combined-stream": "^1.0.8",
+				"mime-types": "^2.1.12"
+			}
+		},
+		"inherits": {
+			"version": "2.0.4",
+			"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+			"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+		},
+		"is-arrayish": {
+			"version": "0.3.2",
+			"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+			"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+		},
+		"is-stream": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+			"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
+		},
+		"kuler": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
+			"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
+		},
+		"logform": {
+			"version": "2.4.2",
+			"resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz",
+			"integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==",
+			"requires": {
+				"@colors/colors": "1.5.0",
+				"fecha": "^4.2.0",
+				"ms": "^2.1.1",
+				"safe-stable-stringify": "^2.3.1",
+				"triple-beam": "^1.3.0"
+			}
+		},
 		"lru-cache": {
 			"version": "6.0.0",
 			"resolved": "https://registry.npm.taobao.org/lru-cache/download/lru-cache-6.0.0.tgz",
@@ -167,6 +740,19 @@
 				"yallist": "^4.0.0"
 			}
 		},
+		"mime-db": {
+			"version": "1.52.0",
+			"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+			"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
+		},
+		"mime-types": {
+			"version": "2.1.35",
+			"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+			"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+			"requires": {
+				"mime-db": "1.52.0"
+			}
+		},
 		"minimatch": {
 			"version": "3.1.2",
 			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -175,14 +761,98 @@
 				"brace-expansion": "^1.1.7"
 			}
 		},
+		"ms": {
+			"version": "2.1.3",
+			"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+			"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+		},
+		"one-time": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
+			"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
+			"requires": {
+				"fn.name": "1.x.x"
+			}
+		},
+		"proxy-from-env": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+			"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+		},
+		"readable-stream": {
+			"version": "3.6.0",
+			"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+			"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+			"requires": {
+				"inherits": "^2.0.3",
+				"string_decoder": "^1.1.1",
+				"util-deprecate": "^1.0.1"
+			}
+		},
+		"safe-buffer": {
+			"version": "5.2.1",
+			"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+			"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+		},
+		"safe-stable-stringify": {
+			"version": "2.4.2",
+			"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz",
+			"integrity": "sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA=="
+		},
 		"semver": {
-			"version": "7.3.5",
-			"resolved": "https://registry.nlark.com/semver/download/semver-7.3.5.tgz?cache=0&sync_timestamp=1622685835879&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsemver%2Fdownload%2Fsemver-7.3.5.tgz",
-			"integrity": "sha1-C2Ich5NI2JmOSw5L6Us/EuYBjvc=",
+			"version": "7.3.8",
+			"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+			"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
 			"requires": {
 				"lru-cache": "^6.0.0"
 			}
 		},
+		"simple-git": {
+			"version": "3.16.0",
+			"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.16.0.tgz",
+			"integrity": "sha512-zuWYsOLEhbJRWVxpjdiXl6eyAyGo/KzVW+KFhhw9MqEEJttcq+32jTWSGyxTdf9e/YCohxRE+9xpWFj9FdiJNw==",
+			"requires": {
+				"@kwsites/file-exists": "^1.1.1",
+				"@kwsites/promise-deferred": "^1.1.1",
+				"debug": "^4.3.4"
+			}
+		},
+		"simple-swizzle": {
+			"version": "0.2.2",
+			"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+			"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+			"requires": {
+				"is-arrayish": "^0.3.1"
+			}
+		},
+		"stack-trace": {
+			"version": "0.0.10",
+			"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
+			"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="
+		},
+		"string_decoder": {
+			"version": "1.3.0",
+			"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+			"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+			"requires": {
+				"safe-buffer": "~5.2.0"
+			}
+		},
+		"text-hex": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
+			"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
+		},
+		"triple-beam": {
+			"version": "1.3.0",
+			"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
+			"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
+		},
+		"util-deprecate": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+			"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+		},
 		"vscode-jsonrpc": {
 			"version": "8.0.2",
 			"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz",
@@ -212,6 +882,34 @@
 			"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz",
 			"integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA=="
 		},
+		"winston": {
+			"version": "3.8.2",
+			"resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz",
+			"integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==",
+			"requires": {
+				"@colors/colors": "1.5.0",
+				"@dabh/diagnostics": "^2.0.2",
+				"async": "^3.2.3",
+				"is-stream": "^2.0.0",
+				"logform": "^2.4.0",
+				"one-time": "^1.0.0",
+				"readable-stream": "^3.4.0",
+				"safe-stable-stringify": "^2.3.1",
+				"stack-trace": "0.0.x",
+				"triple-beam": "^1.3.0",
+				"winston-transport": "^4.5.0"
+			}
+		},
+		"winston-transport": {
+			"version": "4.5.0",
+			"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
+			"integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==",
+			"requires": {
+				"logform": "^2.3.2",
+				"readable-stream": "^3.6.0",
+				"triple-beam": "^1.3.0"
+			}
+		},
 		"yallist": {
 			"version": "4.0.0",
 			"resolved": "https://registry.npm.taobao.org/yallist/download/yallist-4.0.0.tgz",
diff --git a/client/package.json b/client/package.json
index 546069d6b55611684f89f4606bc55e68bb285842..871961f5fb6ac2e74c5bba508f43e6bf08473006 100644
--- a/client/package.json
+++ b/client/package.json
@@ -5,6 +5,10 @@
 	"license": "MIT",
 	"version": "0.0.1",
 	"publisher": "vscode",
+	"scripts": {
+		"lint": "npx eslint src/",
+		"build": "tsc"
+	},
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/Microsoft/vscode-extension-samples"
@@ -13,7 +17,12 @@
 		"vscode": "^1.70.0"
 	},
 	"dependencies": {
-		"vscode-languageclient": "8.0.2"
+		"axios": "^1.2.1",
+		"dayjs": "^1.11.7",
+		"simple-git": "^3.16.0",
+		"triple-beam": "^1.3.0",
+		"vscode-languageclient": "8.0.2",
+		"winston": "^3.8.2"
 	},
 	"devDependencies": {
 		"@types/node": "^17.0.35",
diff --git a/client/src/addon_manager/commands/disable.ts b/client/src/addon_manager/commands/disable.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e2b3dcea25a82218ccd83427c62b720dff0b61c4
--- /dev/null
+++ b/client/src/addon_manager/commands/disable.ts
@@ -0,0 +1,54 @@
+import * as vscode from "vscode";
+import addonManager from "../services/addonManager.service";
+import { createChildLogger } from "../services/logging.service";
+import { setConfig } from "../../languageserver";
+
+type Message = {
+    data: {
+        name: string;
+    };
+};
+
+const localLogger = createChildLogger("Disable Addon");
+
+export default async (context: vscode.ExtensionContext, message: Message) => {
+    const addon = addonManager.addons.get(message.data.name);
+    const workspaceFolders = vscode.workspace.workspaceFolders;
+
+    let selectedFolders: vscode.WorkspaceFolder[];
+
+    if (workspaceFolders && workspaceFolders.length === 1) {
+        selectedFolders = [workspaceFolders[0]];
+    } else {
+        const folderOptions = await addon.getQuickPickerOptions(true);
+        const pickResult = await vscode.window.showQuickPick(folderOptions, {
+            canPickMany: true,
+            ignoreFocusOut: true,
+            title: `Disable ${addon.name} in which folders?`,
+        });
+        if (!pickResult) {
+            localLogger.warn("User did not pick workspace folder");
+            await addon.setLock(false);
+            return;
+        }
+        selectedFolders = pickResult.map((selection) => {
+            return workspaceFolders.find(
+                (folder) => folder.name === selection.label
+            );
+        });
+    }
+
+    for (const folder of selectedFolders) {
+        await addon.disable(folder);
+        await setConfig([
+            {
+                action: "set",
+                key: "Lua.workspace.checkThirdParty",
+                value: false,
+                uri: folder.uri,
+            },
+        ]);
+    }
+
+    return addon.setLock(false);
+};
diff --git a/client/src/addon_manager/commands/enable.ts b/client/src/addon_manager/commands/enable.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f71e8029da56281b6b72b55caba7ab11bd4c4702
--- /dev/null
+++ b/client/src/addon_manager/commands/enable.ts
@@ -0,0 +1,67 @@
+import * as vscode from "vscode";
+import addonManager from "../services/addonManager.service";
+import { createChildLogger } from "../services/logging.service";
+import { setConfig } from "../../languageserver";
+import { WebVue } from "../panels/WebVue";
+import { NotificationLevels } from "../types/webvue";
+
+type Message = {
+    data: {
+        name: string;
+    };
+};
+
+const localLogger = createChildLogger("Enable Addon");
+
+export default async (context: vscode.ExtensionContext, message: Message) => {
+    const addon = addonManager.addons.get(message.data.name);
+    const workspaceFolders = vscode.workspace.workspaceFolders;
+    let selectedFolders: vscode.WorkspaceFolder[];
+
+    if (workspaceFolders && workspaceFolders.length === 1) {
+        selectedFolders = [workspaceFolders[0]];
+    } else {
+        const folderOptions = await addon.getQuickPickerOptions(false);
+
+        const pickResult = await vscode.window.showQuickPick(folderOptions, {
+            canPickMany: true,
+            ignoreFocusOut: true,
+            title: `Enable ${addon.name} in which folders?`,
+        });
+        if (!pickResult) {
+            localLogger.warn("User did not pick workspace folder");
+            await addon.setLock(false);
+            return;
+        }
+        selectedFolders = pickResult.map((selection) => {
+            return workspaceFolders.find(
+                (folder) => folder.name === selection.label
+            );
+        });
+    }
+
+    for (const folder of selectedFolders) {
+        try {
+            await addon.enable(folder);
+        } catch (e) {
+            const message = `Failed to enable ${addon.name}!`;
+            localLogger.error(message, { report: false });
+            localLogger.error(e, { report: false });
+            WebVue.sendNotification({
+                level: NotificationLevels.error,
+                message,
+            });
+            continue;
+        }
+        await setConfig([
+            {
+                action: "set",
+                key: "Lua.workspace.checkThirdParty",
+                value: false,
+                uri: folder.uri,
+            },
+        ]);
+    }
+
+    return addon.setLock(false);
+};
diff --git a/client/src/addon_manager/commands/getAddons.ts b/client/src/addon_manager/commands/getAddons.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d6bb3c62b5645e31b3a45d7c2f5816c4dfddfcbf
--- /dev/null
+++ b/client/src/addon_manager/commands/getAddons.ts
@@ -0,0 +1,48 @@
+import * as vscode from "vscode";
+import { createChildLogger } from "../services/logging.service";
+import addonManager from "../services/addonManager.service";
+import { WebVue } from "../panels/WebVue";
+import { ADDONS_DIRECTORY } from "../config";
+
+const localLogger = createChildLogger("Get Remote Addons");
+
+export default async (context: vscode.ExtensionContext) => {
+    WebVue.setLoadingState(true);
+
+    const installLocation = vscode.Uri.joinPath(
+        context.globalStorageUri,
+        "addonManager",
+        ADDONS_DIRECTORY
+    );
+
+    if (addonManager.addons.size < 1) {
+        await addonManager.fetchAddons(installLocation);
+    }
+
+    WebVue.sendMessage("addonStore", {
+        property: "total",
+        value: addonManager.addons.size,
+    });
+
+    if (addonManager.addons.size === 0) {
+        WebVue.setLoadingState(false);
+        localLogger.verbose("No remote addons found");
+        return;
+    }
+
+    /** Number of addons to load per chunk */
+    const CHUNK_SIZE = 30;
+
+    // Get list of addons and sort them alphabetically
+    const addonList = Array.from(addonManager.addons.values());
+    addonList.sort((a, b) => a.displayName.localeCompare(b.displayName));
+
+    // Send addons to client in chunks
+    for (let i = 0; i <= addonList.length / CHUNK_SIZE; i++) {
+        const chunk = addonList.slice(i * CHUNK_SIZE, i * CHUNK_SIZE + CHUNK_SIZE);
+        const addons = await Promise.all(chunk.map((addon) => addon.toJSON()));
+        await WebVue.sendMessage("addAddon", { addons });
+    }
+
+    WebVue.setLoadingState(false);
+};
diff --git a/client/src/addon_manager/commands/index.ts b/client/src/addon_manager/commands/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f9b2188f2a3011548ce05a1cc9c9fbc2eb68198c
--- /dev/null
+++ b/client/src/addon_manager/commands/index.ts
@@ -0,0 +1,19 @@
+import enable from "./enable";
+import disable from "./disable";
+import open from "./open";
+import getAddons from "./getAddons";
+import refreshAddons from "./refreshAddons";
+import openLog from "./openLog";
+import update from "./update";
+import uninstall from "./uninstall";
+
+export const commands = {
+    enable,
+    disable,
+    open,
+    getAddons,
+    refreshAddons,
+    openLog,
+    update,
+    uninstall
+};
diff --git a/client/src/addon_manager/commands/open.ts b/client/src/addon_manager/commands/open.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f693316a27dc60c249e2126759451bf2c2c9e4f6
--- /dev/null
+++ b/client/src/addon_manager/commands/open.ts
@@ -0,0 +1,21 @@
+import * as vscode from "vscode";
+import { createChildLogger } from "../services/logging.service";
+import { ADDONS_DIRECTORY } from "../config";
+
+const localLogger = createChildLogger("Open Addon");
+
+export default async (
+    context: vscode.ExtensionContext,
+    message: { data: { name: string } }
+) => {
+    const extensionStorageURI = context.globalStorageUri;
+    const uri = vscode.Uri.joinPath(
+        extensionStorageURI,
+        "addonManager",
+        ADDONS_DIRECTORY,
+        message.data.name
+    );
+
+    localLogger.info(`Opening "${message.data.name}" addon in file explorer`);
+    vscode.env.openExternal(vscode.Uri.file(uri.fsPath));
+};
diff --git a/client/src/addon_manager/commands/openLog.ts b/client/src/addon_manager/commands/openLog.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b1c460f7369a41958075e83172cd195e3eff6144
--- /dev/null
+++ b/client/src/addon_manager/commands/openLog.ts
@@ -0,0 +1,6 @@
+import * as vscode from "vscode";
+import VSCodeLogFileTransport from "../services/logging/vsCodeLogFileTransport";
+
+export default async () => {
+    vscode.env.openExternal(VSCodeLogFileTransport.currentLogFile);
+};
diff --git a/client/src/addon_manager/commands/refreshAddons.ts b/client/src/addon_manager/commands/refreshAddons.ts
new file mode 100644
index 0000000000000000000000000000000000000000..71bd6608bb9bdb6f4704ccbbbb333bab3543600e
--- /dev/null
+++ b/client/src/addon_manager/commands/refreshAddons.ts
@@ -0,0 +1,18 @@
+import * as vscode from "vscode";
+import addonManager from "../services/addonManager.service";
+import { ADDONS_DIRECTORY } from "../config";
+import { WebVue } from "../panels/WebVue";
+
+export default async (context: vscode.ExtensionContext) => {
+    WebVue.setLoadingState(true);
+
+    const installLocation = vscode.Uri.joinPath(
+        context.globalStorageUri,
+        "addonManager",
+        ADDONS_DIRECTORY
+    );
+
+    await addonManager.fetchAddons(installLocation);
+
+    WebVue.setLoadingState(false);
+};
diff --git a/client/src/addon_manager/commands/uninstall.ts b/client/src/addon_manager/commands/uninstall.ts
new file mode 100644
index 0000000000000000000000000000000000000000..952c26fc01193f8d30e18a0142891b0452bfe03a
--- /dev/null
+++ b/client/src/addon_manager/commands/uninstall.ts
@@ -0,0 +1,10 @@
+import * as vscode from "vscode";
+import addonManagerService from "../services/addonManager.service";
+
+export default async (
+    context: vscode.ExtensionContext,
+    message: { data: { name: string } }
+) => {
+    const addon = addonManagerService.addons.get(message.data.name);
+    addon.uninstall();
+};
diff --git a/client/src/addon_manager/commands/update.ts b/client/src/addon_manager/commands/update.ts
new file mode 100644
index 0000000000000000000000000000000000000000..13aed3b5414e652d6667d80338392cf436294586
--- /dev/null
+++ b/client/src/addon_manager/commands/update.ts
@@ -0,0 +1,34 @@
+import * as vscode from "vscode";
+import addonManager from "../services/addonManager.service";
+import { git } from "../services/git.service";
+import { DiffResultTextFile } from "simple-git";
+import { WebVue } from "../panels/WebVue";
+import { NotificationLevels } from "../types/webvue";
+import { createChildLogger } from "../services/logging.service";
+
+const localLogger = createChildLogger("Update Addon");
+
+type Message = {
+    data: {
+        name: string;
+    };
+};
+
+export default async (context: vscode.ExtensionContext, message: Message) => {
+    const addon = addonManager.addons.get(message.data.name);
+    try {
+        await addon.update();
+    } catch (e) {
+        const message = `Failed to update ${addon.name}`;
+        localLogger.error(message, { report: false });
+        localLogger.error(e, { report: false });
+        WebVue.sendNotification({
+            level: NotificationLevels.error,
+            message,
+        });
+    }
+    await addon.setLock(false);
+
+    const diff = await git.diffSummary(["HEAD", "origin/main"]);
+    addon.checkForUpdate(diff.files as DiffResultTextFile[]);
+};
diff --git a/client/src/addon_manager/config.ts b/client/src/addon_manager/config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ab0afe1c5f98f49364b4809d7cf8c3916f1c3d85
--- /dev/null
+++ b/client/src/addon_manager/config.ts
@@ -0,0 +1,20 @@
+// Development
+export const DEVELOPMENT_IFRAME_URL = "http://127.0.0.1:5173";
+
+// GitHub Repository Info
+export const REPOSITORY_PATH = "https://github.com/LuaLS/LLS-Addons.git";
+export const REPOSITORY_OWNER = "carsakiller";
+export const REPOSITORY_NAME = "LLS-Addons";
+export const REPOSITORY_DEFAULT_BRANCH = "main";
+export const REPOSITORY_ISSUES_URL =
+    "https://github.com/LuaLS/vscode-lua/issues/new?template=bug_report.yml";
+export const ADDONS_DIRECTORY = "addons";
+export const GIT_DOWNLOAD_URL = "https://git-scm.com/downloads";
+
+// settings.json file info
+export const LIBRARY_SETTING = "Lua.workspace.library";
+
+// Addon files
+export const PLUGIN_FILENAME = "plugin.lua";
+export const CONFIG_FILENAME = "config.json";
+export const INFO_FILENAME = "info.json";
diff --git a/client/src/addon_manager/models/addon.ts b/client/src/addon_manager/models/addon.ts
new file mode 100644
index 0000000000000000000000000000000000000000..eae74d309fadda5184b58109e7927d65ee2f6f5b
--- /dev/null
+++ b/client/src/addon_manager/models/addon.ts
@@ -0,0 +1,315 @@
+import * as vscode from "vscode";
+import { createChildLogger } from "../services/logging.service";
+import { CONFIG_FILENAME, INFO_FILENAME, LIBRARY_SETTING } from "../config";
+import { AddonConfig, AddonInfo } from "../types/addon";
+import { WebVue } from "../panels/WebVue";
+import {
+    applyAddonSettings,
+    getLibraryPaths,
+    revokeAddonSettings,
+} from "../services/settings.service";
+import { git } from "../services/git.service";
+import filesystem from "../services/filesystem.service";
+import { DiffResultTextFile } from "simple-git";
+import { getConfig, setConfig } from "../../languageserver";
+
+const localLogger = createChildLogger("Addon");
+
+export class Addon {
+    readonly name: string;
+    readonly uri: vscode.Uri;
+
+    #displayName?: string;
+    /** Whether or not this addon is currently processing an operation. */
+    #processing?: boolean;
+    /** The workspace folders that this addon is enabled in. */
+    #enabled: boolean[];
+    /** Whether or not this addon has an update available from git. */
+    #hasUpdate?: boolean;
+    /** Whether or not this addon is installed */
+    #installed: boolean;
+
+    constructor(name: string, path: vscode.Uri) {
+        this.name = name;
+        this.uri = path;
+
+        this.#enabled = [];
+        this.#hasUpdate = false;
+        this.#installed = false;
+    }
+
+    public get displayName() {
+        return this.#displayName ?? this.name;
+    }
+
+    /** Fetch addon info from `info.json` */
+    public async fetchInfo() {
+        const path = vscode.Uri.joinPath(this.uri, INFO_FILENAME);
+        const rawInfo = await filesystem.readFile(path);
+        const info = JSON.parse(rawInfo) as AddonInfo;
+
+        this.#displayName = info.name;
+
+        return {
+            name: info.name,
+            description: info.description,
+            size: info.size,
+            hasPlugin: info.hasPlugin,
+        };
+    }
+
+    /** Get the `config.json` for this addon. */
+    public async getConfigurationFile() {
+        const configURI = vscode.Uri.joinPath(
+            this.uri,
+            "module",
+            CONFIG_FILENAME
+        );
+
+        try {
+            const rawConfig = await filesystem.readFile(configURI);
+            const config = JSON.parse(rawConfig);
+            return config as AddonConfig;
+        } catch (e) {
+            localLogger.error(
+                `Failed to read config.json file for ${this.name} (${e})`
+            );
+            throw e;
+        }
+    }
+
+    /** Update this addon using git. */
+    public async update() {
+        return git
+            .submoduleUpdate([this.uri.fsPath])
+            .then((message) => localLogger.debug(message));
+    }
+
+    /** Check whether this addon is enabled, given an array of enabled library paths.
+     * @param libraryPaths An array of paths from the `Lua.workspace.library` setting.
+     */
+    public checkIfEnabled(libraryPaths: string[]) {
+        const regex = new RegExp(
+            `[/\\\\]+sumneko.lua[/\\\\]+addonManager[/\\\\]+addons[/\\\\]+${this.name}`,
+            "g"
+        );
+
+        const index = libraryPaths.findIndex((path) => regex.test(path));
+        return index !== -1;
+    }
+
+    /** Get the enabled state for this addon in all opened workspace folders */
+    public async getEnabled() {
+        const folders = await getLibraryPaths();
+
+        // Check all workspace folders for a path that matches this addon
+        const folderStates = folders.map((entry) => {
+            return {
+                folder: entry.folder,
+                enabled: this.checkIfEnabled(entry.paths),
+            };
+        });
+
+        folderStates.forEach(
+            (entry) => (this.#enabled[entry.folder.index] = entry.enabled)
+        );
+
+        const moduleURI = vscode.Uri.joinPath(this.uri, "module");
+        this.#installed =
+            (await filesystem.exists(moduleURI)) &&
+            (await filesystem.readDirectory(moduleURI, { recursive: false }))
+                .length > 0;
+
+        return folderStates;
+    }
+
+    public async enable(folder: vscode.WorkspaceFolder) {
+        const librarySetting = ((await getConfig(
+            LIBRARY_SETTING,
+            folder.uri
+        )) ?? []) as string[];
+
+        const enabled = await this.checkIfEnabled(librarySetting);
+        if (enabled) {
+            localLogger.warn(`${this.name} is already enabled`);
+            this.#enabled[folder.index] = true;
+            return;
+        }
+
+        // Init submodule
+        try {
+            await git.submoduleInit([this.uri.fsPath]);
+            localLogger.debug("Initialized submodule");
+        } catch (e) {
+            localLogger.warn(`Unable to initialize submodule for ${this.name}`);
+            localLogger.warn(e);
+            throw e;
+        }
+
+        try {
+            await git.submoduleUpdate([this.uri.fsPath]);
+            localLogger.debug("Submodule up to date");
+        } catch (e) {
+            localLogger.warn(`Unable to update submodule for ${this.name}`);
+            localLogger.warn(e);
+            throw e;
+        }
+
+        // Apply addon settings
+        const libraryUri = vscode.Uri.joinPath(this.uri, "module", "library");
+
+        const configValues = await this.getConfigurationFile();
+
+        try {
+            await setConfig([
+                {
+                    action: "add",
+                    key: LIBRARY_SETTING,
+                    value: filesystem.unixifyPath(libraryUri),
+                    uri: folder.uri,
+                },
+            ]);
+            if (configValues.settings) {
+                await applyAddonSettings(folder, configValues.settings);
+                localLogger.info(`Applied addon settings for ${this.name}`);
+            }
+        } catch (e) {
+            localLogger.warn(`Failed to apply settings of "${this.name}"`);
+            localLogger.warn(e);
+            return;
+        }
+
+        this.#enabled[folder.index] = true;
+        localLogger.info(`Enabled "${this.name}"`);
+    }
+
+    public async disable(folder: vscode.WorkspaceFolder, silent = false) {
+        const librarySetting = ((await getConfig(
+            LIBRARY_SETTING,
+            folder.uri
+        )) ?? []) as string[];
+
+        const regex = new RegExp(
+            `[/\\\\]+sumneko.lua[/\\\\]+addonManager[/\\\\]+addons[/\\\\]+${this.name}`,
+            "g"
+        );
+        const index = librarySetting.findIndex((path) => regex.test(path));
+
+        if (index === -1) {
+            if (!silent) localLogger.warn(`"${this.name}" is already disabled`);
+            this.#enabled[folder.index] = false;
+            return;
+        }
+
+        // Remove this addon from the library list
+        librarySetting.splice(index, 1);
+        const result = await setConfig([
+            {
+                action: "set",
+                key: LIBRARY_SETTING,
+                value: librarySetting,
+                uri: folder.uri,
+            },
+        ]);
+        if (!result) {
+            localLogger.error(
+                `Failed to update ${LIBRARY_SETTING} when disabling ${this.name}`
+            );
+            return;
+        }
+
+        // Remove addon settings if installed
+        if (this.#installed) {
+            const configValues = await this.getConfigurationFile();
+            try {
+                if (configValues.settings)
+                    await revokeAddonSettings(folder, configValues.settings);
+            } catch (e) {
+                localLogger.error(
+                    `Failed to revoke settings of "${this.name}"`
+                );
+                return;
+            }
+        }
+
+        this.#enabled[folder.index] = false;
+        localLogger.info(`Disabled "${this.name}"`);
+    }
+
+    public async uninstall() {
+        for (const folder of vscode.workspace.workspaceFolders) {
+            await this.disable(folder, true);
+        }
+        const moduleURI = vscode.Uri.joinPath(this.uri, "module");
+        await filesystem.deleteFile(moduleURI, {
+            recursive: true,
+            useTrash: false,
+        });
+        localLogger.info(`Uninstalled ${this.name}`);
+        this.#installed = false;
+        this.setLock(false);
+    }
+
+    /** Convert this addon to an object ready for sending to WebVue. */
+    public async toJSON() {
+        await this.getEnabled();
+
+        const { name, description, size, hasPlugin } = await this.fetchInfo();
+        const enabled = this.#enabled;
+        const installTimestamp = (await git.log()).latest.date;
+        const hasUpdate = this.#hasUpdate;
+
+        return {
+            name: this.name,
+            displayName: name,
+            description,
+            enabled,
+            hasPlugin,
+            installTimestamp,
+            size,
+            hasUpdate,
+            processing: this.#processing,
+            installed: this.#installed,
+        };
+    }
+
+    public checkForUpdate(modified: DiffResultTextFile[]) {
+        this.#hasUpdate = false;
+        if (
+            modified.findIndex((modifiedItem) =>
+                modifiedItem.file.includes(this.name)
+            ) !== -1
+        ) {
+            localLogger.info(`Found update for "${this.name}"`);
+            this.#hasUpdate = true;
+        }
+        return this.#hasUpdate;
+    }
+
+    /** Get a list of options for a quick picker that lists the workspace
+     * folders that the addon is enabled/disabled in.
+     * @param enabledState The state the addon must be in in a folder to be included.
+     * `true` will only return the folders that the addon is **enabled** in.
+     * `false` will only return the folders that the addon is **disabled** in
+     */
+    public async getQuickPickerOptions(enabledState: boolean) {
+        return (await this.getEnabled())
+            .filter((entry) => entry.enabled === enabledState)
+            .map((entry) => {
+                return {
+                    label: entry.folder.name,
+                    detail: entry.folder.uri.path,
+                };
+            });
+    }
+
+    public async setLock(state: boolean) {
+        this.#processing = state;
+        return this.sendToWebVue();
+    }
+
+    /** Send this addon to WebVue. */
+    public async sendToWebVue() {
+        WebVue.sendMessage("addAddon", { addons: await this.toJSON() });
+    }
+}
diff --git a/client/src/addon_manager/panels/WebVue.ts b/client/src/addon_manager/panels/WebVue.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d0b28c031a78cbb50db010cd566c2f775d06efe5
--- /dev/null
+++ b/client/src/addon_manager/panels/WebVue.ts
@@ -0,0 +1,264 @@
+import * as vscode from "vscode";
+
+import { createChildLogger } from "../services/logging.service";
+import { commands } from "../commands";
+import { Notification, WebVueMessage } from "../types/webvue";
+import { DEVELOPMENT_IFRAME_URL } from "../config";
+
+const localLogger = createChildLogger("WebVue");
+const commandLogger = createChildLogger("Command");
+
+export class WebVue {
+    public static currentPanel: WebVue | undefined;
+    private readonly _panel: vscode.WebviewPanel;
+    private readonly _extensionUri: vscode.Uri;
+    private _disposables: vscode.Disposable[] = [];
+
+    private constructor(
+        context: vscode.ExtensionContext,
+        panel: vscode.WebviewPanel
+    ) {
+        const extensionUri = context.extensionUri;
+
+        this._panel = panel;
+        this._extensionUri = extensionUri;
+        this._panel.iconPath = {
+            dark: vscode.Uri.joinPath(extensionUri, "images", "logo.png"),
+            light: vscode.Uri.joinPath(extensionUri, "images", "logo.png"),
+        };
+        this._disposables.push(
+            this._panel.onDidDispose(this.dispose, null, this._disposables),
+            this._setWebviewMessageListener(this._panel.webview, context)
+        );
+        this._panel.webview.html = this._getWebViewContent(
+            this._panel.webview,
+            context
+        );
+    }
+
+    /** Convert a standard file uri to a uri usable by this webview. */
+    private toWebviewUri(pathList: string[]) {
+        return this._panel.webview.asWebviewUri(
+            vscode.Uri.joinPath(this._extensionUri, ...pathList)
+        );
+    }
+
+    /** Send a message to the webview */
+    public static sendMessage(
+        command: string,
+        data: { [index: string]: unknown } | unknown
+    ) {
+        WebVue.currentPanel._panel.webview.postMessage({ command, data });
+    }
+
+    public static sendNotification(message: Notification) {
+        WebVue.sendMessage("notify", message);
+    }
+
+    /** Set the loading state of a store in the webview */
+    public static setLoadingState(loading: boolean) {
+        WebVue.sendMessage("addonStore", {
+            property: "loading",
+            value: loading,
+        });
+    }
+
+    /** Reveal or create a new panel in VS Code */
+    public static render(context: vscode.ExtensionContext) {
+        const extensionUri = context.extensionUri;
+
+        if (WebVue.currentPanel) {
+            WebVue.currentPanel._panel.reveal(vscode.ViewColumn.One);
+        } else {
+            const panel = vscode.window.createWebviewPanel(
+                "lua-addon_manager",
+                "Lua Addon Manager",
+                vscode.ViewColumn.Active,
+                {
+                    enableScripts: true,
+                    enableForms: false,
+                    localResourceRoots: [extensionUri],
+                }
+            );
+
+            WebVue.currentPanel = new WebVue(context, panel);
+        }
+
+        const workspaceOpen =
+            vscode.workspace.workspaceFolders &&
+            vscode.workspace.workspaceFolders.length > 0;
+        const clientVersion = context.extension.packageJSON.version;
+
+        WebVue.sendMessage("appStore", {
+            property: "workspaceState",
+            value: workspaceOpen,
+        });
+        WebVue.sendMessage("appStore", {
+            property: "clientVersion",
+            value: clientVersion,
+        });
+        localLogger.debug(`Workspace Open: ${workspaceOpen}`);
+    }
+
+    /** Dispose of panel to clean up resources when it is closed */
+    public dispose() {
+        WebVue.currentPanel = undefined;
+
+        this._panel?.dispose();
+
+        while (this._disposables.length) {
+            const disposable = this._disposables.pop();
+            if (disposable) {
+                disposable.dispose();
+            }
+        }
+    }
+
+    /** Get the HTML content of the webview */
+    private _getWebViewContent(
+        webview: vscode.Webview,
+        context: vscode.ExtensionContext
+    ) {
+        if (context.extensionMode !== vscode.ExtensionMode.Production) {
+            return `
+            <!DOCTYPE html>
+            <html lang="en">
+            <head>
+                <meta charset="UTF-8">
+                <meta name="viewport" content="width=device-width, initial-scale=1.0">
+                <title>Lua Addon Manager</title>
+                <style>
+                    html,body {
+                        height: 100%;
+                        display: block;
+                        margin: 0;
+                        padding: 0;
+                    }
+                    iframe {
+                        width: 100%;
+                        height: 100%;
+                        display: block;
+                        border: none;
+                        user-select: none;
+                    }
+                </style>
+            </head>
+            <body>
+                <iframe src="${DEVELOPMENT_IFRAME_URL}"></iframe>
+                <script>
+                    const vscode = acquireVsCodeApi();
+
+                    const devIframe = document.querySelector("iframe");
+                    window.addEventListener("message", (message) => {
+                        // If message is from VS Code
+
+
+                        if (message.origin.startsWith("vscode-webview")) {
+                            console.groupCollapsed("DEV: VS Code ➡️ WebVue");
+                            console.log(message.data);
+                            console.groupEnd();
+                            devIframe.contentWindow.postMessage(message.data, devIframe.src)
+                            return;
+                        }
+
+                        if (message.source === devIframe.contentWindow) {
+                            console.groupCollapsed("DEV: WebVue ➡️ VS Code");
+                            console.log(message.data);
+                            console.groupEnd();
+                            vscode.postMessage(message.data);
+                            return;
+                        }
+
+                        console.error("Source unknown");
+                        console.error(message);
+                    })
+                </script>
+            </body>
+            </html>
+            `;
+        } else {
+            const stylesUri = this.toWebviewUri([
+                "client",
+                "webvue",
+                "build",
+                "assets",
+                "index.css",
+            ]);
+            const scriptUri = this.toWebviewUri([
+                "client",
+                "webvue",
+                "build",
+                "assets",
+                "index.js",
+            ]);
+            const codiconUri = this.toWebviewUri([
+                "client",
+                "webvue",
+                "build",
+                "assets",
+                "codicon.ttf",
+            ]);
+
+            const inlineStyleNonce = this.getNonce();
+            const scriptNonce = this.getNonce();
+
+            return `
+            <!DOCTYPE html>
+            <html lang="en">
+            <head>
+                <meta charset="UTF-8">
+                <meta name="viewport" content="width=device-width, initial-scale=1.0">
+                <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'nonce-${inlineStyleNonce}'; font-src ${webview.cspSource}; script-src 'nonce-${scriptNonce}';">
+                <link rel="stylesheet" type="text/css" href="${stylesUri}">
+                <title>Lua Addon Manager</title>
+                <style nonce="${inlineStyleNonce}">
+                    @font-face {
+                        font-family: "codicon";
+                        src: url(${codiconUri}) format("truetype");
+                    }
+                </style>
+            </head>
+            <body>
+                <div id="app"></div>
+                <script type="module" src="${scriptUri}" nonce="${scriptNonce}"></script>
+            </body>
+            </html>
+            `;
+        }
+    }
+
+    /** Get a `nonce` (number used once). Used for the content security policy.
+     *
+     * [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce)
+     */
+    private getNonce() {
+        let text = "";
+        const possible =
+            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+        for (let i = 0; i < 32; i++) {
+            text += possible.charAt(
+                Math.floor(Math.random() * possible.length)
+            );
+        }
+        return text;
+    }
+
+    /** Sets up event listener for messages sent from webview */
+    private _setWebviewMessageListener(
+        webview: vscode.Webview,
+        context: vscode.ExtensionContext
+    ) {
+        return webview.onDidReceiveMessage((message: WebVueMessage) => {
+            const command = message.command;
+            commandLogger.verbose(
+                `Executing "${command}" (${JSON.stringify(message)})`
+            );
+
+            try {
+                commands[command](context, message);
+            } catch (e) {
+                commandLogger.error(e);
+            }
+        });
+    }
+}
diff --git a/client/src/addon_manager/registration.ts b/client/src/addon_manager/registration.ts
new file mode 100644
index 0000000000000000000000000000000000000000..97dc2b4a867962d4e5acc29fb40d1af1f77d5f3b
--- /dev/null
+++ b/client/src/addon_manager/registration.ts
@@ -0,0 +1,93 @@
+import * as vscode from "vscode";
+
+import { WebVue } from "./panels/WebVue";
+import VSCodeLogFileTransport from "./services/logging/vsCodeLogFileTransport";
+import { createChildLogger, logger } from "./services/logging.service";
+import dayjs from "dayjs";
+import RelativeTime from "dayjs/plugin/relativeTime";
+import { git, setupGit } from "./services/git.service";
+import { GIT_DOWNLOAD_URL } from "./config";
+import { NotificationLevels } from "./types/webvue";
+
+dayjs.extend(RelativeTime);
+
+const localLogger = createChildLogger("Registration");
+
+/** Set up the addon manager by registering its commands in VS Code */
+export async function activate(context: vscode.ExtensionContext) {
+    const globalConfig = vscode.workspace.getConfiguration("Lua.addonManager");
+    const isEnabled = globalConfig.get("enable") as boolean;
+
+    if (!isEnabled) {
+        // NOTE: Will only log to OUTPUT, not to log file
+        localLogger.info("Addon manager is disabled");
+        return;
+    }
+
+    const fileLogger = new VSCodeLogFileTransport(context.logUri, {
+        level: "debug",
+    });
+
+    // Register command to open addon manager
+    context.subscriptions.push(
+        vscode.commands.registerCommand("lua.addon_manager.open", async () => {
+            // Set up file logger
+            if (!fileLogger.initialized) {
+                const disposable = await fileLogger.init();
+                context.subscriptions.push(disposable);
+                logger.info(
+                    `This session's log file: ${VSCodeLogFileTransport.currentLogFile}`
+                );
+                logger.add(fileLogger);
+                await fileLogger.logStart();
+            }
+
+            // Check if git is installed
+            if (!(await git.version()).installed) {
+                logger.error("Git does not appear to be installed!", {
+                    report: false,
+                });
+                vscode.window
+                    .showErrorMessage(
+                        "Git does not appear to be installed. Please install Git to use the addon manager",
+                        "Disable Addon Manager",
+                        "Visit Git Website"
+                    )
+                    .then((result) => {
+                        switch (result) {
+                            case "Disable Addon Manager":
+                                globalConfig.update(
+                                    "enable",
+                                    false,
+                                    vscode.ConfigurationTarget.Global
+                                );
+                                break;
+                            case "Visit Git Website":
+                                vscode.env.openExternal(
+                                    vscode.Uri.parse(GIT_DOWNLOAD_URL)
+                                );
+                                break;
+                            default:
+                                break;
+                        }
+                    });
+            }
+
+            // Set up git repository for fetching addons
+            try {
+                setupGit(context);
+            } catch (e) {
+                const message =
+                    "Failed to set up Git repository. Please check your connection to GitHub.";
+                logger.error(message, { report: false });
+                logger.error(e, { report: false });
+                WebVue.sendNotification({
+                    level: NotificationLevels.error,
+                    message,
+                });
+            }
+
+            WebVue.render(context);
+        })
+    );
+}
diff --git a/client/src/addon_manager/services/addonManager.service.ts b/client/src/addon_manager/services/addonManager.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2532f67b453852f76abbf6fe5949c28b27e41272
--- /dev/null
+++ b/client/src/addon_manager/services/addonManager.service.ts
@@ -0,0 +1,57 @@
+import * as vscode from "vscode";
+import filesystem from "./filesystem.service";
+import { createChildLogger } from "./logging.service";
+import { Addon } from "../models/addon";
+import { git } from "./git.service";
+import { DiffResultTextFile } from "simple-git";
+import { WebVue } from "../panels/WebVue";
+import { NotificationLevels } from "../types/webvue";
+
+const localLogger = createChildLogger("Addon Manager");
+
+class AddonManager {
+    readonly addons: Map<string, Addon>;
+
+    constructor() {
+        this.addons = new Map();
+    }
+
+    public async fetchAddons(installLocation: vscode.Uri) {
+        try {
+            await git.fetch();
+            await git.pull();
+        } catch (e) {
+            const message =
+                "Failed to fetch addons! Please check your connection to GitHub.";
+            localLogger.error(message, { report: false });
+            localLogger.error(e, { report: false });
+            WebVue.sendNotification({
+                level: NotificationLevels.error,
+                message,
+            });
+        }
+
+        const addons = await filesystem.readDirectory(installLocation);
+
+        for (const addon of addons) {
+            this.addons.set(addon.name, new Addon(addon.name, addon.uri));
+            localLogger.verbose(`Found ${addon.name}`);
+        }
+
+        return await this.checkUpdated();
+    }
+
+    public async checkUpdated() {
+        const diff = await git.diffSummary(["main", "origin/main"]);
+        this.addons.forEach((addon) => {
+            addon.checkForUpdate(diff.files as DiffResultTextFile[]);
+        });
+    }
+
+    public unlockAddon(name: string) {
+        const addon = this.addons.get(name);
+        return addon.setLock(false);
+    }
+}
+
+export default new AddonManager();
diff --git a/client/src/addon_manager/services/filesystem.service.ts b/client/src/addon_manager/services/filesystem.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..20ab57a6e1ef4c418607c7aef72d2193a27c5eb3
--- /dev/null
+++ b/client/src/addon_manager/services/filesystem.service.ts
@@ -0,0 +1,196 @@
+import * as vscode from "vscode";
+import { stringToByteArray } from "./string.service";
+import { createChildLogger } from "./logging.service";
+import { platform } from "os";
+
+const localLogger = createChildLogger("Filesystem");
+
+type ReadDirectoryOptions = {
+    recursive?: boolean;
+    maxDepth?: number;
+    depth?: number;
+};
+
+namespace filesystem {
+    /** Get a string representation of a URI using UNIX separators
+     * @param uri The URI to get as a UNIX path string
+     */
+    export function unixifyPath(uri: vscode.Uri): string {
+        if (platform() === "win32") {
+            return uri.path.substring(1);
+        } else {
+            return uri.fsPath;
+        }
+    }
+
+    /** Check if a file exists
+     * @param uri - The URI of the file to check the existence of
+     */
+    export async function exists(uri: vscode.Uri): Promise<boolean> {
+        try {
+            await vscode.workspace.fs.stat(uri);
+            return true;
+        } catch (e) {
+            return false;
+        }
+    }
+
+    /** Check if a directory is empty
+     * @param uri - The URI of the directory to check
+     */
+    export async function empty(uri: vscode.Uri): Promise<boolean> {
+        try {
+            const dirContents = await vscode.workspace.fs.readDirectory(uri);
+            return dirContents.length < 1;
+        } catch (e) {
+            localLogger.error(e);
+            return false;
+        }
+    }
+
+    /** Read from a file
+     * @param uri - The URI of the file to read from
+     */
+    export async function readFile(uri: vscode.Uri): Promise<string> {
+        const bytes = await vscode.workspace.fs.readFile(uri);
+        const str = bytes.toString();
+
+        localLogger.debug(`Read "${uri.path}"`);
+
+        return str;
+    }
+
+    /** Write to a file
+     * @param uri - The URI of the file to write to
+     * @param content - The content to write in to the file, overwriting any previous content
+     */
+    export async function writeFile(
+        uri: vscode.Uri,
+        content: string
+    ): Promise<void> {
+        const byteArray = stringToByteArray(content);
+        await vscode.workspace.fs.writeFile(uri, byteArray);
+
+        localLogger.debug(`Wrote to "${uri.path}"`);
+    }
+
+    /** Delete a file
+     * @param uri - The URI of the file to delete
+     * @param options - Options to control if deleting a directory should be recursive and if the system's trash should be used
+     */
+    export async function deleteFile(
+        uri: vscode.Uri,
+        options?: { recursive?: boolean; useTrash?: boolean }
+    ): Promise<void> {
+        await vscode.workspace.fs.delete(uri, {
+            recursive: options?.recursive ?? false,
+            useTrash: options?.useTrash ?? true,
+        });
+
+        localLogger.debug(`Deleted ${uri.path}`);
+    }
+
+    export async function createDirectory(uri: vscode.Uri) {
+        return vscode.workspace.fs
+            .createDirectory(uri)
+            .then(() =>
+                localLogger.debug(`Created directory at "${uri.path}"`)
+            );
+    }
+
+    export type DirectoryNode = {
+        path: string;
+        name: string;
+        type: vscode.FileType;
+        uri: vscode.Uri;
+    };
+
+    /** Read a directory, returning an array of all entries
+     * @param uri - The URI of the directory to read
+     * @param options - Options for controlling recursion
+     */
+    export async function readDirectory(
+        uri: vscode.Uri,
+        options?: ReadDirectoryOptions
+    ) {
+        const tree: DirectoryNode[] = [];
+
+        options = options ?? {};
+
+        options.maxDepth = options.maxDepth ?? 10;
+        options.depth = options.depth ?? 0;
+
+        if (options.depth > options.maxDepth) {
+            localLogger.warn(
+                `Max recursion depth(${options.maxDepth}) reached!`
+            );
+            return;
+        }
+
+        const dirContents = await vscode.workspace.fs.readDirectory(uri);
+
+        for (const item of dirContents) {
+            const name = item[0];
+            const type = item[1];
+            const itemURI = vscode.Uri.joinPath(uri, name);
+
+            const pathSegments = itemURI.path.split("/");
+            const path = pathSegments
+                .slice(pathSegments.length - (options.depth + 1))
+                .join("/");
+
+            switch (type) {
+                case vscode.FileType.File:
+                    tree.push({ path, name, type, uri: itemURI });
+                    break;
+                case vscode.FileType.Directory:
+                    if (!options.recursive) {
+                        tree.push({ path, name, type, uri: itemURI });
+                        continue;
+                    }
+                    tree.push(
+                        ...(await readDirectory(itemURI, {
+                            recursive: true,
+                            maxDepth: options.maxDepth,
+                            depth: options.depth + 1,
+                        }))
+                    );
+                    break;
+                default:
+                    localLogger.warn(`Unsupported file type ${itemURI.path}`);
+                    break;
+            }
+        }
+
+        return tree;
+    }
+
+    export async function getDirectorySize(
+        uri: vscode.Uri,
+        maxDepth = 10
+    ): Promise<number> {
+        const tree = await readDirectory(uri, {
+            maxDepth,
+            recursive: true,
+        });
+
+        const promises = [] as Promise<number>[];
+        for (const node of tree) {
+            if (node.type !== vscode.FileType.File) continue;
+
+            promises.push(
+                new Promise((resolve) => {
+                    vscode.workspace.fs
+                        .stat(node.uri)
+                        .then((stats) => resolve(stats.size));
+                })
+            );
+        }
+
+        return Promise.all(promises).then((results) => {
+            return results.reduce((previous, result) => previous + result, 0);
+        });
+    }
+}
+
+export default filesystem;
diff --git a/client/src/addon_manager/services/git.service.ts b/client/src/addon_manager/services/git.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..54d61f6fbc4a6d62bbc13e792988df3760437bd5
--- /dev/null
+++ b/client/src/addon_manager/services/git.service.ts
@@ -0,0 +1,49 @@
+import * as vscode from "vscode";
+import simpleGit from "simple-git";
+import filesystem from "./filesystem.service";
+import { createChildLogger } from "./logging.service";
+import { REPOSITORY_NAME, REPOSITORY_PATH } from "../config";
+
+const localLogger = createChildLogger("Git");
+
+export const git = simpleGit();
+
+export const setupGit = async (context: vscode.ExtensionContext) => {
+    const storageURI = vscode.Uri.joinPath(
+        context.globalStorageUri,
+        "addonManager"
+    );
+    await filesystem.createDirectory(storageURI);
+
+    // set working directory
+    await git.cwd({ path: storageURI.fsPath, root: true });
+
+    // clone if not already cloned
+    if (await filesystem.empty(storageURI)) {
+        try {
+            localLogger.debug(
+                `Attempting to clone ${REPOSITORY_NAME} to ${storageURI.fsPath}`
+            );
+            const options = { "--depth": 1 };
+            await git.clone(REPOSITORY_PATH, storageURI.fsPath, options);
+            localLogger.debug(
+                `Cloned ${REPOSITORY_NAME} to ${storageURI.fsPath}`
+            );
+        } catch (e) {
+            localLogger.warn(
+                `Failed to clone ${REPOSITORY_NAME} to ${storageURI.fsPath}!`
+            );
+            throw e;
+        }
+    }
+
+    // pull
+    try {
+        await git.fetch();
+        await git.pull();
+        await git.checkout("main");
+    } catch (e) {
+        localLogger.warn(`Failed to pull ${REPOSITORY_NAME}!`);
+        throw e;
+    }
+};
diff --git a/client/src/addon_manager/services/logging.service.ts b/client/src/addon_manager/services/logging.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..732769bea8e15ba5deaee2ba59bac1c017e6a94b
--- /dev/null
+++ b/client/src/addon_manager/services/logging.service.ts
@@ -0,0 +1,116 @@
+/**
+ * Logging using WintonJS
+ * https://github.com/winstonjs/winston
+ */
+
+import winston from "winston";
+import VSCodeOutputTransport from "./logging/vsCodeOutputTransport";
+import axios, { AxiosError } from "axios";
+import { padText } from "./string.service";
+import * as vscode from "vscode";
+import { MESSAGE } from "triple-beam";
+import { REPOSITORY_ISSUES_URL } from "../config";
+import VSCodeLogFileTransport from "./logging/vsCodeLogFileTransport";
+
+// Create logger from winston
+export const logger = winston.createLogger({
+    level: "info",
+    defaultMeta: { category: "General", report: true },
+    format: winston.format.combine(
+        winston.format.timestamp({
+            format: "YYYY-MM-DD HH:mm:ss",
+        }),
+        winston.format.errors({ stack: true }),
+        winston.format.printf((message) => {
+            const level = padText(message.level, 9);
+            const category = padText(
+                message?.defaultMeta?.category ?? "GENERAL",
+                18
+            );
+            if (typeof message.message === "object")
+                return `[${
+                    message.timestamp
+                }] | ${level.toUpperCase()} | ${category} | ${JSON.stringify(
+                    message.message
+                )}`;
+            return `[${
+                message.timestamp
+            }] | ${level.toUpperCase()} | ${category} | ${message.message}`;
+        })
+    ),
+
+    transports: [new VSCodeOutputTransport({ level: "info" })],
+});
+
+// When a error is logged, ask user to report error.
+logger.on("data", async (info) => {
+    if (info.level !== "error" || !info.report) return;
+
+    const choice = await vscode.window.showErrorMessage(
+        `An error occurred with the Lua Addon Manager. Please help us improve by reporting the issue ❤️`,
+        { modal: false },
+        "Report Issue"
+    );
+
+    if (choice !== "Report Issue") return;
+
+    // Open log file
+    await vscode.env.openExternal(VSCodeLogFileTransport.currentLogFile);
+
+    // Read log file and copy to clipboard
+    const log = await vscode.workspace.fs.readFile(
+        VSCodeLogFileTransport.currentLogFile
+    );
+    await vscode.env.clipboard.writeText(
+        "<details><summary>Retrieved Log</summary>\n\n```\n" +
+            log.toString() +
+            "\n```\n\n</details>"
+    );
+    vscode.window.showInformationMessage("Copied log to clipboard");
+
+    // After a delay, open GitHub issues page
+    setTimeout(() => {
+        const base = vscode.Uri.parse(REPOSITORY_ISSUES_URL);
+        const query = [
+            base.query,
+            `actual=...\n\nI also see the following error:\n\n\`\`\`\n${info[MESSAGE]}\n\`\`\``,
+        ];
+        const issueURI = base.with({ query: query.join("&") });
+
+        vscode.env.openExternal(issueURI);
+    }, 2000);
+});
+
+/** Helper that creates a child logger from the main logger. */
+export const createChildLogger = (label: string) => {
+    return logger.child({
+        level: "info",
+        defaultMeta: { category: label },
+        format: winston.format.combine(
+            winston.format.timestamp({
+                format: "YYYY-MM-DD HH:mm:ss",
+            }),
+            winston.format.errors({ stack: true }),
+            winston.format.json()
+        ),
+    });
+};
+
+// Log HTTP requests made through axios
+const axiosLogger = createChildLogger("AXIOS");
+
+axios.interceptors.request.use(
+    (request) => {
+        const method = request.method ?? "???";
+        axiosLogger.debug(`${method.toUpperCase()} requesting ${request.url}`);
+
+        return request;
+    },
+    (error: AxiosError) => {
+        const url = error?.config?.url;
+        const method = error.config?.method?.toUpperCase();
+
+        axiosLogger.error(`${url} ${method} ${error.code} ${error.message}`);
+        return Promise.reject(error);
+    }
+);
diff --git a/client/src/addon_manager/services/logging/vsCodeLogFileTransport.ts b/client/src/addon_manager/services/logging/vsCodeLogFileTransport.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ccf497970dda79dd9d598b7fbb3a416d8cd3e869
--- /dev/null
+++ b/client/src/addon_manager/services/logging/vsCodeLogFileTransport.ts
@@ -0,0 +1,71 @@
+import * as vscode from "vscode";
+import Transport from "winston-transport";
+import winston from "winston";
+import { MESSAGE } from "triple-beam";
+import * as fs from "fs";
+import { stringToByteArray } from "../string.service";
+import dayjs from "dayjs";
+
+export default class VSCodeLogFileTransport extends Transport {
+    public static currentLogFile: vscode.Uri;
+
+    public initialized = false;
+
+    private logDir: vscode.Uri;
+
+    private stream: fs.WriteStream;
+
+    constructor(logDir: vscode.Uri, opts?: Transport.TransportStreamOptions) {
+        super(opts);
+        this.logDir = logDir;
+    }
+
+    /** Initialize transport instance by creating the needed directories and files. */
+    public async init() {
+        // Ensure log directory exists
+        await vscode.workspace.fs.createDirectory(this.logDir);
+        // Create subdirectory
+        const addonLogsDir = vscode.Uri.joinPath(this.logDir, "addonManager");
+        await vscode.workspace.fs.createDirectory(addonLogsDir);
+        // Create log file stream
+        const logFileUri = vscode.Uri.joinPath(
+            addonLogsDir,
+            `${dayjs().format("HH")}.log`
+        );
+        VSCodeLogFileTransport.currentLogFile = logFileUri;
+        this.stream = fs.createWriteStream(logFileUri.fsPath, {
+            flags: "a",
+        });
+        this.initialized = true;
+        return new vscode.Disposable(() => this.stream.close);
+    }
+
+    /** Mark the start of the addon manager in the log */
+    public logStart() {
+        return new Promise((resolve, reject) => {
+            this.stream.write(
+                stringToByteArray("#### STARTUP ####\n"),
+                (err) => {
+                    if (err) reject(err);
+                    resolve(true);
+                }
+            );
+        });
+    }
+
+    public async log(info: winston.LogEntry, callback: winston.LogCallback) {
+        if (!this.initialized) {
+            return;
+        }
+
+        setImmediate(() => {
+            this.emit("logged", info);
+        });
+
+        this.stream.write(
+            stringToByteArray(info[MESSAGE as unknown as string] + "\n")
+        );
+
+        callback();
+    }
+}
diff --git a/client/src/addon_manager/services/logging/vsCodeOutputTransport.ts b/client/src/addon_manager/services/logging/vsCodeOutputTransport.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9a3ba2899dd6f95fe9faaa0787200c0af292268c
--- /dev/null
+++ b/client/src/addon_manager/services/logging/vsCodeOutputTransport.ts
@@ -0,0 +1,26 @@
+import * as vscode from "vscode";
+import Transport from "winston-transport";
+import winston from "winston";
+import { MESSAGE } from "triple-beam";
+
+export default class VSCodeOutputTransport extends Transport {
+    private readonly outputChannel: vscode.OutputChannel;
+
+    constructor(opts?: Transport.TransportStreamOptions) {
+        super(opts);
+        this.outputChannel = vscode.window.createOutputChannel(
+            "Lua Addon Manager",
+            "log"
+        );
+    }
+
+    log(info: winston.LogEntry, callback: winston.LogCallback) {
+        setImmediate(() => {
+            this.emit("logged", info);
+        });
+
+        this.outputChannel.appendLine(info[MESSAGE as unknown as string]);
+
+        callback();
+    }
+}
diff --git a/client/src/addon_manager/services/settings.service.ts b/client/src/addon_manager/services/settings.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0ec2b20dbae16d93a9a247427efb2b42e5f609e7
--- /dev/null
+++ b/client/src/addon_manager/services/settings.service.ts
@@ -0,0 +1,121 @@
+import * as vscode from "vscode";
+import { getConfig, setConfig } from "../../languageserver";
+import { createChildLogger } from "./logging.service";
+import { LIBRARY_SETTING } from "../config";
+
+const localLogger = createChildLogger("Settings");
+
+/** An error with the user's configuration `.vscode/settings.json` or an
+ * addon's `config.json`. */
+class ConfigError extends Error {
+    message: string;
+
+    constructor(message: string) {
+        super(message);
+        localLogger.error(message);
+    }
+}
+
+export const getLibraryPaths = async (): Promise<
+    { folder: vscode.WorkspaceFolder; paths: string[] }[]
+> => {
+    const result = [];
+
+    if (!vscode.workspace.workspaceFolders) return [];
+
+    for (const folder of vscode.workspace.workspaceFolders) {
+        const libraries = await getConfig(LIBRARY_SETTING, folder.uri);
+        result.push({ folder, paths: libraries ?? [] });
+    }
+
+    return result;
+};
+
+export const applyAddonSettings = async (
+    folder: vscode.WorkspaceFolder,
+    config: Record<string, unknown>
+) => {
+    if (!folder) throw new ConfigError(`Workspace is not open!`);
+
+    const changes = [];
+    for (const [newKey, newValue] of Object.entries(config)) {
+        if (Array.isArray(newValue)) {
+            newValue.forEach((val) => {
+                changes.push({
+                    action: "add",
+                    key: newKey,
+                    value: val,
+                    uri: folder.uri,
+                });
+            });
+        } else if (typeof newValue === "object") {
+            changes.push(
+                ...Object.entries(newValue).map(([key, value]) => {
+                    return {
+                        action: "prop",
+                        key: newKey,
+                        prop: key,
+                        value,
+                        uri: folder.uri,
+                    };
+                })
+            );
+        } else {
+            changes.push({
+                action: "set",
+                key: newKey,
+                value: newValue,
+                uri: folder.uri,
+            });
+        }
+    }
+
+    return await setConfig(changes);
+};
+
+export const revokeAddonSettings = async (
+    folder: vscode.WorkspaceFolder,
+    config: Record<string, unknown>
+) => {
+    if (!folder) throw new ConfigError(`Workspace is not open!`);
+
+    const changes = [];
+    for (const [newKey, newValue] of Object.entries(config)) {
+        const currentValue = await getConfig(newKey, folder.uri);
+
+        if (Array.isArray(newValue)) {
+            // Only keep values that the addon settings does not contain
+            const notAddon = currentValue.filter(
+                (oldValue) => !newValue.includes(oldValue)
+            );
+            changes.push({
+                action: "set",
+                key: newKey,
+                value: notAddon,
+                uri: folder.uri,
+            });
+        } else if (typeof newValue === "object") {
+            for (const objectKey of Object.keys(newValue)) {
+                delete currentValue[objectKey];
+            }
+            // If object is now empty, delete it
+            if (Object.keys(currentValue).length === 0) {
+                changes.push({
+                    action: "set",
+                    key: newKey,
+                    value: undefined,
+                    uri: folder.uri,
+                });
+            } else {
+                changes.push({
+                    action: "set",
+                    key: newKey,
+                    value: currentValue,
+                    uri: folder.uri,
+                });
+            }
+        }
+    }
+
+    return await setConfig(changes);
+};
diff --git a/client/src/addon_manager/services/string.service.ts b/client/src/addon_manager/services/string.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..acfeed5f67e51016df5ca4ee9e7478296a4dd16a
--- /dev/null
+++ b/client/src/addon_manager/services/string.service.ts
@@ -0,0 +1,30 @@
+import { TextEncoder } from "util";
+
+/** Pad a string to have spaces on either side, making it a set `length`
+ * @param str The string to add padding to
+ * @param length The new total length the string should be with padding
+ */
+export const padText = (str: string, length: number) => {
+    const paddingLength = Math.max(0, length - str.length);
+    const padding = " ".repeat(paddingLength / 2);
+
+    const paddingLeft = " ".repeat(length - padding.length - str.length);
+
+    return paddingLeft + str + padding;
+};
+
+/** Convert a string to a byte array */
+export const stringToByteArray = (str: string): Uint8Array =>
+    new TextEncoder().encode(str);
+
+/** Convert an object into a query string without the leading `?` */
+export const objectToQueryString = (
+    obj: Record<string, string | boolean | number>
+): string => {
+    return Object.keys(obj)
+        .map(
+            (key) =>
+                `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`
+        )
+        .join("&");
+};
diff --git a/client/src/addon_manager/types/addon.d.ts b/client/src/addon_manager/types/addon.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..eee03fc5f224749d73b44512f29d953fbb77b603
--- /dev/null
+++ b/client/src/addon_manager/types/addon.d.ts
@@ -0,0 +1,25 @@
+import { Uri } from "vscode";
+
+export type AddonConfig = {
+    name: string;
+    description: string;
+    settings: { [index: string]: Object };
+};
+
+export type AddonInfo = {
+    name: string;
+    description: string;
+    size: number;
+    hasPlugin: boolean;
+}
+
+export interface Addon {
+    readonly name: string;
+    readonly uri: Uri;
+
+    displayName?: string;
+    description?: string;
+    size?: number;
+    hasPlugin?: boolean;
+    processing?: boolean;
+}
diff --git a/client/src/addon_manager/types/webvue.ts b/client/src/addon_manager/types/webvue.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5338ba104ae096e1a90e4f4a96855c420ce5383c
--- /dev/null
+++ b/client/src/addon_manager/types/webvue.ts
@@ -0,0 +1,15 @@
+export interface WebVueMessage {
+    command: string;
+    data: { [index: string]: unknown };
+}
+
+export enum NotificationLevels {
+    "error",
+    "warn",
+    "info",
+}
+
+export type Notification = {
+    level: NotificationLevels;
+    message: string;
+};
diff --git a/client/src/extension.ts b/client/src/extension.ts
index 49d1cd9e52cef1055950c32acc862d6876d9a35f..0ca460e8ae1a6256a7e82ef9ec9403ba95627a02 100644
--- a/client/src/extension.ts
+++ b/client/src/extension.ts
@@ -1,36 +1,41 @@
-import * as vscode from 'vscode'
+import * as vscode from 'vscode';
 import * as languageserver from './languageserver';
 import * as psi from './psi/psiViewer';
+import * as addonManager from './addon_manager/registration';
 
-let luadoc = require('../3rd/vscode-lua-doc/extension.js')
+import luadoc from "../3rd/vscode-lua-doc/extension.js";
 
 export function activate(context: vscode.ExtensionContext) {
-    
     languageserver.activate(context);
 
-    let luaDocContext = {
+    const luaDocContext = {
         ViewType:      undefined,
         OpenCommand:   undefined,
         extensionPath: undefined,
-    }
+    };
 
     for (const k in context) {
         try {
             luaDocContext[k] = context[k];
-        } catch (error) {}
+        } catch (error) {
+            console.error(error);
+        }
     }
     luaDocContext.ViewType      = 'lua-doc';
     luaDocContext.OpenCommand   = 'extension.lua.doc';
-    luaDocContext.extensionPath = context.extensionPath + '/client/3rd/vscode-lua-doc'
+    luaDocContext.extensionPath = context.extensionPath + '/client/3rd/vscode-lua-doc';
 
     luadoc.activate(luaDocContext);
     psi.activate(context);
 
+    // Register and activate addon manager
+    addonManager.activate(context);
+
     return {
-        async reportAPIDoc(params: any) {
+        async reportAPIDoc(params: unknown) {
             await languageserver.reportAPIDoc(params);
         }
-    }
+    };
 }
 
 export function deactivate() {
diff --git a/client/src/languageserver.ts b/client/src/languageserver.ts
index 4e78f1cb018e94a4b4c072bbd2a55f01cb8edf8d..a1fb1ded8d138c746c33ce95410bc444474e5c58 100644
--- a/client/src/languageserver.ts
+++ b/client/src/languageserver.ts
@@ -20,24 +20,27 @@ import {
     ExecuteCommandRequest,
 } from 'vscode-languageclient/node';
 
-export let defaultClient: LuaClient;
+export let defaultClient: LuaClient | null;
 
 function registerCustomCommands(context: ExtensionContext) {
     context.subscriptions.push(Commands.registerCommand('lua.config', (changes) => {
-        let propMap: Map<string, Map<string, any>> = new Map();
+        const propMap: Map<string, Map<string, unknown>> = new Map();
+
         for (const data of changes) {
-            let config = Workspace.getConfiguration(undefined, Uri.parse(data.uri));
-            if (data.action == 'add') {
-                let value: any[] = config.get(data.key);
+            const config = Workspace.getConfiguration(undefined, Uri.parse(data.uri));
+
+            if (data.action === 'add') {
+                const value = config.get(data.key);
+                if (!Array.isArray(value)) throw new Error(`${data.key} is not an Array!`);
                 value.push(data.value);
                 config.update(data.key, value, data.global);
                 continue;
             }
-            if (data.action == 'set') {
+            if (data.action === 'set') {
                 config.update(data.key, data.value, data.global);
                 continue;
             }
-            if (data.action == 'prop') {
+            if (data.action === 'prop') {
                 if (!propMap[data.key]) {
                     propMap[data.key] = config.get(data.key);
                 }
@@ -46,13 +49,13 @@ function registerCustomCommands(context: ExtensionContext) {
                 continue;
             }
         }
-    }))
+    }));
 
     context.subscriptions.push(Commands.registerCommand('lua.exportDocument', async () => {
         if (!defaultClient) {
             return;
-        };
-        let outputs = await vscode.window.showOpenDialog({
+        }
+        const outputs = await vscode.window.showOpenDialog({
             defaultUri: vscode.Uri.joinPath(
                 context.extensionUri,
                 'server',
@@ -63,18 +66,19 @@ function registerCustomCommands(context: ExtensionContext) {
             canSelectFolders: true,
             canSelectMany: false,
         });
-        let output = outputs?.[0];
+        const output = outputs?.[0];
         if (!output) {
             return;
-        };
+        }
         defaultClient.client.sendRequest(ExecuteCommandRequest.type, {
             command: 'lua.exportDocument',
             arguments: [output.toString()],
-        })
-    }))
+        });
+    }));
 }
 
 class LuaClient {
+
     public client: LanguageClient;
     private disposables = new Array<Disposable>();
     constructor(private context: ExtensionContext,
@@ -83,7 +87,7 @@ class LuaClient {
 
     async start() {
         // Options to control the language client
-        let clientOptions: LanguageClientOptions = {
+        const clientOptions: LanguageClientOptions = {
             // Register the server for plain text documents
             documentSelector: this.documentSelector,
             progressOnInitialization: true,
@@ -96,11 +100,13 @@ class LuaClient {
             }
         };
 
-        let config = Workspace.getConfiguration(undefined, vscode.workspace.workspaceFolders?.[0]);
-        let commandParam: string[] = config.get("Lua.misc.parameters");
-        let command: string = await this.getCommand(config);
+        const config = Workspace.getConfiguration(undefined, vscode.workspace.workspaceFolders?.[0]);
+        const commandParam = config.get("Lua.misc.parameters");
+        const command = await this.getCommand(config);
 
-        let serverOptions: ServerOptions = {
+        if (!Array.isArray(commandParam)) throw new Error("Lua.misc.parameters must be an Array!");
+
+        const serverOptions: ServerOptions = {
             command: command,
             args:    commandParam,
         };
@@ -119,16 +125,22 @@ class LuaClient {
     }
 
     private async getCommand(config: vscode.WorkspaceConfiguration) {
-        let executablePath: string = config.get("Lua.misc.executablePath");
-        if (executablePath && executablePath != "") {
+        const executablePath = config.get("Lua.misc.executablePath");
+
+        if (typeof executablePath !== "string") throw new Error("Lua.misc.executablePath must be a string!");
+
+        if (executablePath && executablePath !== "") {
             return executablePath;
         }
+
+        const platform: string = os.platform();
         let command: string;
-        let platform: string = os.platform();
-        let binDir: string;
+        let binDir: string | undefined;
+
         if ((await fs.promises.stat(this.context.asAbsolutePath('server/bin'))).isDirectory()) {
             binDir = 'bin';
         }
+
         switch (platform) {
             case "win32":
                 command = this.context.asAbsolutePath(
@@ -159,6 +171,8 @@ class LuaClient {
                 );
                 await fs.promises.chmod(command, '777');
                 break;
+            default:
+                throw new Error(`Unsupported operating system "${platform}"!`);
         }
         return command;
     }
@@ -171,23 +185,23 @@ class LuaClient {
     }
 
     statusBar() {
-        let client = this.client;
-        let bar = window.createStatusBarItem();
+        const client = this.client;
+        const bar = window.createStatusBarItem(vscode.StatusBarAlignment.Right);
         bar.text = 'Lua';
         bar.command = 'Lua.statusBar';
         this.disposables.push(Commands.registerCommand(bar.command, () => {
             client.sendNotification('$/status/click');
-        }))
-        this.disposables.push(client.onNotification('$/status/show', (params) => {
+        }));
+        this.disposables.push(client.onNotification('$/status/show', () => {
             bar.show();
-        }))
-        this.disposables.push(client.onNotification('$/status/hide', (params) => {
+        }));
+        this.disposables.push(client.onNotification('$/status/hide', () => {
             bar.hide();
-        }))
+        }));
         this.disposables.push(client.onNotification('$/status/report', (params) => {
             bar.text    = params.text;
             bar.tooltip = params.tooltip;
-        }))
+        }));
         client.sendNotification('$/status/refresh');
         this.disposables.push(bar);
     }
@@ -229,7 +243,7 @@ export async function deactivate() {
     return undefined;
 }
 
-export async function reportAPIDoc(params: any) {
+export async function reportAPIDoc(params: unknown) {
     if (!defaultClient) {
         return;
     }
@@ -261,17 +275,17 @@ export async function setConfig(changes: ConfigChange[]): Promise<boolean> {
     if (!defaultClient) {
         return false;
     }
-    let params = [];
+    const params = [];
     for (const change of changes) {
         params.push({
             action: change.action,
-            prop:   (change.action == "prop") ? change.prop : undefined,
+            prop:   (change.action === "prop") ? change.prop : undefined as never,
             key:    change.key,
             value:  change.value,
             uri:    change.uri.toString(),
             global: change.global,
-        })
-    };
+        });
+    }
     await defaultClient.client.sendRequest(ExecuteCommandRequest.type, {
         command: 'lua.setConfig',
         arguments: params,
@@ -283,12 +297,11 @@ export async function getConfig(key: string, uri: vscode.Uri): Promise<LSPAny> {
     if (!defaultClient) {
         return undefined;
     }
-    let result = await defaultClient.client.sendRequest(ExecuteCommandRequest.type, {
+    return await defaultClient.client.sendRequest(ExecuteCommandRequest.type, {
         command: 'lua.getConfig',
         arguments: [{
             uri: uri.toString(),
             key: key,
         }]
     });
-    return result;
 }
diff --git a/client/src/psi/psiViewer.ts b/client/src/psi/psiViewer.ts
index 15fa98fac5fbabd14e6c07fc0eda86f661ceb240..0ba0d86edc28f05bb9fc84d7b7adb3b30ef3b955 100644
--- a/client/src/psi/psiViewer.ts
+++ b/client/src/psi/psiViewer.ts
@@ -3,7 +3,7 @@ import * as path from 'path';
 import * as vscode from 'vscode';
 import { defaultClient } from '../languageserver';
 
-const LANGUAGE_ID: string = "lua";
+const LANGUAGE_ID = "lua";
 /**
  * Manages webview panels
  */
@@ -72,7 +72,7 @@ class PsiViewer {
         }
     }
 
-    public post(message: any) {
+    public post(message: unknown) {
         this.panel.webview.postMessage(message);
     }
 
@@ -116,8 +116,8 @@ class PsiViewer {
 
     private requestPsiImpl(editor: vscode.TextEditor) {
         const client = defaultClient.client;
-        let params: any = { uri: editor.document.uri.toString() };
-        client?.sendRequest<{ data: any }>("$/psi/view", params).then(result => {
+        const params = { uri: editor.document.uri.toString() };
+        client?.sendRequest<{ data: unknown }>("$/psi/view", params).then(result => {
             if (result) {
                 this.post({
                     type: "psi",
@@ -129,8 +129,8 @@ class PsiViewer {
 
     private requestPsiSelect(position: vscode.Position, uri: vscode.Uri) {
         const client = defaultClient.client;
-        let params: any = { uri: uri.toString(), position };
-        client?.sendRequest<{ data: any }>("$/psi/select", params).then(result => {
+        const params = { uri: uri.toString(), position };
+        client?.sendRequest<{ data: unknown }>("$/psi/select", params).then(result => {
             if (result) {
                 this.post({
                     type: "psi_select",
@@ -159,11 +159,11 @@ class PsiViewer {
     }
 
     private static onDidChangeSelection(e: vscode.TextEditorSelectionChangeEvent) {
-        if (e.kind == vscode.TextEditorSelectionChangeKind.Mouse
-            || e.kind == vscode.TextEditorSelectionChangeKind.Keyboard) {
+        if (e.kind === vscode.TextEditorSelectionChangeKind.Mouse
+            || e.kind === vscode.TextEditorSelectionChangeKind.Keyboard) {
             const viewer = PsiViewer.currentPanel;
             if (viewer) {
-                viewer.requestPsiSelect(e.selections[0].start, e.textEditor.document.uri)
+                viewer.requestPsiSelect(e.selections[0].start, e.textEditor.document.uri);
             }
         }
     }
@@ -176,4 +176,4 @@ export function activate(context: vscode.ExtensionContext) {
             PsiViewer.createOrShow(context);
         })
     );
-}
\ No newline at end of file
+}
diff --git a/client/tsconfig.json b/client/tsconfig.json
index 7b53801781df65319982e2994f6443d252be9901..3e9b365a9efd9a2df8fd42344d0e2743096a4bd3 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -5,8 +5,9 @@
 		"outDir": "out",
 		"rootDir": "src",
 		"lib": ["es6"],
-		"sourceMap": true
+		"sourceMap": true,
+        "esModuleInterop": true
 	},
-	"include": ["src", "out/patch.js"],
+	"include": ["src"],
 	"exclude": ["node_modules", ".vscode-test"]
 }
diff --git a/client/webvue b/client/webvue
new file mode 160000
index 0000000000000000000000000000000000000000..ac8a4db6fbb2e5aab49dca9aa55e367cec92aadd
--- /dev/null
+++ b/client/webvue
@@ -0,0 +1 @@
+Subproject commit ac8a4db6fbb2e5aab49dca9aa55e367cec92aadd
diff --git a/package-lock.json b/package-lock.json
index 9254e01af6da0154b55998b713560e7a408a25cc..c3b1752d67de253a219508a7dc0ae8bbeacc7d3a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,16 @@
 {
 	"name": "lua",
-	"version": "1.7.4",
-	"lockfileVersion": 1
+	"version": "3.6.12",
+	"lockfileVersion": 3,
+	"requires": true,
+	"packages": {
+		"": {
+			"name": "lua",
+			"version": "3.6.12",
+			"license": "MIT",
+			"engines": {
+				"vscode": "^1.67.0"
+			}
+		}
+	}
 }
diff --git a/package.json b/package.json
index 43e0fe81d0d6c4c3904da3c5ce5affa00daefae2..b44870dd2f7aaaef5ed6f386c0cde437a20dbf5e 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,10 @@
 				"command": "lua.psi.view",
 				"title": "Lua Psi Viewer"
 			},
+			{
+				"command": "lua.addon_manager.open",
+				"title": "%command.addon_manager.open%"
+			},
 			{
 				"command": "lua.exportDocument",
 				"title": "%command.exportDocument%"
@@ -33,6 +37,12 @@
 		],
 		"configuration": {
 			"properties": {
+				"Lua.addonManager.enable": {
+					"default": true,
+					"markdownDescription": "%config.addonManager.enable%",
+					"scope": "resource",
+					"type": "boolean"
+				},
 				"Lua.codeLens.enable": {
 					"default": false,
 					"markdownDescription": "%config.codeLens.enable%",
@@ -2240,7 +2250,7 @@
 				},
 				"Lua.format.defaultConfig": {
 					"additionalProperties": false,
-					"default": [],
+					"default": {},
 					"markdownDescription": "%config.format.defaultConfig%",
 					"patternProperties": {
 						".*": {
@@ -2665,7 +2675,7 @@
 				},
 				"Lua.runtime.special": {
 					"additionalProperties": false,
-					"default": [],
+					"default": {},
 					"markdownDescription": "%config.runtime.special%",
 					"patternProperties": {
 						".*": {
@@ -2682,7 +2692,8 @@
 								"xpcall",
 								"assert",
 								"error",
-								"type"
+								"type",
+								"os.exit"
 							],
 							"type": "string"
 						}
@@ -2907,6 +2918,11 @@
 					"command": "lua.exportDocument",
 					"group": "z_commands",
 					"when": "resourceLangId == lua"
+				},
+				{
+					"command": "lua.addon_manager.open",
+					"group": "z_commands",
+					"when": "resourceLangId == lua"
 				}
 			]
 		},
@@ -3055,5 +3071,5 @@
 	"sponsor": {
 		"url": "https://github.com/LuaLS/lua-language-server/issues/484"
 	},
-	"version": "3.6.10"
+	"version": "3.6.17"
 }
diff --git a/package.nls.json b/package.nls.json
index 6f9d002c063ace449768d2aef4c7c35d0af82a86..d91ebd54a1c81a68dca5781109dba09ce31e747a 100644
--- a/package.nls.json
+++ b/package.nls.json
@@ -1,9 +1,11 @@
 {
-    "command.exportDocument": "Export Document ...",
+    "command.addon_manager.open": "Lua: Open Addon Manager ...",
+    "command.exportDocument": "Lua: Export Document ...",
     "config.IntelliSense.traceBeSetted": "Please read [wiki](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features) to learn more.",
     "config.IntelliSense.traceFieldInject": "Please read [wiki](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features) to learn more.",
     "config.IntelliSense.traceLocalSet": "Please read [wiki](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features) to learn more.",
     "config.IntelliSense.traceReturn": "Please read [wiki](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features) to learn more.",
+    "config.addonManager.enable": "Whether the addon manager is enabled or not.",
     "config.codeLens.enable": "Enable code lens.",
     "config.color.mode": "Color mode.",
     "config.color.mode.Grammar": "Grammar color.",
diff --git a/package.nls.pt-br.json b/package.nls.pt-br.json
index 7095ee3ab49a7ec6f13243b75a4d7cc37f5d7811..9613e1a913f9be74a7357e396576f9c25ae76afa 100644
--- a/package.nls.pt-br.json
+++ b/package.nls.pt-br.json
@@ -1,9 +1,11 @@
 {
-    "command.exportDocument": "Export Document ...",
+    "command.addon_manager.open": "Lua: Open Addon Manager ...",
+    "command.exportDocument": "Lua: Export Document ...",
     "config.IntelliSense.traceBeSetted": "Please read [wiki](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features) to learn more.",
     "config.IntelliSense.traceFieldInject": "Please read [wiki](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features) to learn more.",
     "config.IntelliSense.traceLocalSet": "Please read [wiki](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features) to learn more.",
     "config.IntelliSense.traceReturn": "Please read [wiki](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features) to learn more.",
+    "config.addonManager.enable": "Whether the addon manager is enabled or not.",
     "config.codeLens.enable": "Enable code lens.",
     "config.color.mode": "Color mode.",
     "config.color.mode.Grammar": "Grammar color.",
diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json
index 89a144f06535abee81abccb491283f6a1cfcc273..dc97eea45c89024812ebc65f48bb458f1e5245e0 100644
--- a/package.nls.zh-cn.json
+++ b/package.nls.zh-cn.json
@@ -1,9 +1,11 @@
 {
-    "command.exportDocument": "导出文档...",
+    "command.addon_manager.open": "Lua: 打开插件管理器...",
+    "command.exportDocument": "Lua: 导出文档...",
     "config.IntelliSense.traceBeSetted": "请查阅[文档](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features)了解用法。",
     "config.IntelliSense.traceFieldInject": "请查阅[文档](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features)了解用法。",
     "config.IntelliSense.traceLocalSet": "请查阅[文档](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features)了解用法。",
     "config.IntelliSense.traceReturn": "请查阅[文档](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features)了解用法。",
+    "config.addonManager.enable": "Whether the addon manager is enabled or not.",
     "config.codeLens.enable": "启用代码度量。",
     "config.color.mode": "着色模式。",
     "config.color.mode.Grammar": "语法着色。",
diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json
index 15aa9f782bbc20ecb2c6c279af9e075b467ad809..762e6613cb38ec885a757ca9ea1883cfb6bde77f 100644
--- a/package.nls.zh-tw.json
+++ b/package.nls.zh-tw.json
@@ -1,9 +1,11 @@
 {
-    "command.exportDocument": "Export Document ...",
+    "command.addon_manager.open": "Lua: Open Addon Manager ...",
+    "command.exportDocument": "Lua: Export Document ...",
     "config.IntelliSense.traceBeSetted": "請查閱[文件](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features)瞭解用法。",
     "config.IntelliSense.traceFieldInject": "請查閱[文件](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features)瞭解用法。",
     "config.IntelliSense.traceLocalSet": "請查閱[文件](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features)瞭解用法。",
     "config.IntelliSense.traceReturn": "請查閱[文件](https://github.com/LuaLS/lua-language-server/wiki/IntelliSense-optional-features)瞭解用法。",
+    "config.addonManager.enable": "Whether the addon manager is enabled or not.",
     "config.codeLens.enable": "Enable code lens.",
     "config.color.mode": "著色模式。",
     "config.color.mode.Grammar": "語法著色。",
diff --git a/package/build.lua b/package/build.lua
index fb4603d3b2c349267dce2215529e9bd440b4d767..756520f6d2361801b4cf62dc3a24a89e57daaf07 100644
--- a/package/build.lua
+++ b/package/build.lua
@@ -1,6 +1,6 @@
 local json = require 'json-beautify'
 
-local VERSION = "3.6.10"
+local VERSION = "3.6.17"
 
 local package = require 'package.package'
 local fsu     = require 'fs-utility'
diff --git a/package/package.lua b/package/package.lua
index 9c93cf47fe2dfe09caa5ea8dd8c5abb71d0d2b66..1869b66fde411891e9b0463dfa1870a751384d26 100644
--- a/package/package.lua
+++ b/package/package.lua
@@ -39,6 +39,10 @@ return {
                 command = "lua.psi.view",
                 title = "Lua Psi Viewer"
             },
+            {
+                command = "lua.addon_manager.open",
+                title = "%command.addon_manager.open%",
+            },
             {
                 command = "lua.exportDocument",
                 title = "%command.exportDocument%",
@@ -50,7 +54,12 @@ return {
                     when = "resourceLangId == lua",
                     command = "lua.exportDocument",
                     group = "z_commands"
-                }
+                },
+                {
+                    when = "resourceLangId == lua",
+                    command = "lua.addon_manager.open",
+                    group = "z_commands",
+                },
             }
         },
         configuration = {
diff --git a/setting/schema-pt-br.json b/setting/schema-pt-br.json
index 48c13577d676ccfd5b15bf6fa8e0f7b94651e4d3..7b9d9f2cf6a4cabda77ab84a5590f936f2fdefdd 100644
--- a/setting/schema-pt-br.json
+++ b/setting/schema-pt-br.json
@@ -1,6 +1,19 @@
 {
     "description": "Setting of sumneko.lua",
     "properties": {
+        "addonManager": {
+            "properties": {
+                "enable": {
+                    "$ref": "#/properties/addonManager.enable"
+                }
+            }
+        },
+        "addonManager.enable": {
+            "default": true,
+            "markdownDescription": "Whether the addon manager is enabled or not.",
+            "scope": "resource",
+            "type": "boolean"
+        },
         "codeLens": {
             "properties": {
                 "enable": {
@@ -2318,7 +2331,7 @@
         },
         "format.defaultConfig": {
             "additionalProperties": false,
-            "default": [],
+            "default": {},
             "markdownDescription": "The default format configuration. Has a lower priority than `.editorconfig` file in the workspace.\nRead [formatter docs](https://github.com/CppCXY/EmmyLuaCodeStyle/tree/master/docs) to learn usage.\n",
             "patternProperties": {
                 ".*": {
@@ -2840,7 +2853,7 @@
         },
         "runtime.special": {
             "additionalProperties": false,
-            "default": [],
+            "default": {},
             "markdownDescription": "The custom global variables are regarded as some special built-in variables, and the language server will provide special support\nThe following example shows that 'include' is treated as' require '.\n```json\n\"Lua.runtime.special\" : {\n    \"include\" : \"require\"\n}\n```\n",
             "patternProperties": {
                 ".*": {
@@ -2857,7 +2870,8 @@
                         "xpcall",
                         "assert",
                         "error",
-                        "type"
+                        "type",
+                        "os.exit"
                     ],
                     "type": "string"
                 }
diff --git a/setting/schema-zh-cn.json b/setting/schema-zh-cn.json
index 36176f7b6950c21caa29f9e0502e2be13394174a..4c5f4ba2c598052cc4f7f133a4e897c5cacfd6c0 100644
--- a/setting/schema-zh-cn.json
+++ b/setting/schema-zh-cn.json
@@ -1,6 +1,19 @@
 {
     "description": "Setting of sumneko.lua",
     "properties": {
+        "addonManager": {
+            "properties": {
+                "enable": {
+                    "$ref": "#/properties/addonManager.enable"
+                }
+            }
+        },
+        "addonManager.enable": {
+            "default": true,
+            "markdownDescription": "Whether the addon manager is enabled or not.",
+            "scope": "resource",
+            "type": "boolean"
+        },
         "codeLens": {
             "properties": {
                 "enable": {
@@ -2318,7 +2331,7 @@
         },
         "format.defaultConfig": {
             "additionalProperties": false,
-            "default": [],
+            "default": {},
             "markdownDescription": "默认的格式化配置,优先级低于工作区内的 `.editorconfig` 文件。\n请查阅[格式化文档](https://github.com/CppCXY/EmmyLuaCodeStyle/tree/master/docs)了解用法。\n",
             "patternProperties": {
                 ".*": {
@@ -2840,7 +2853,7 @@
         },
         "runtime.special": {
             "additionalProperties": false,
-            "default": [],
+            "default": {},
             "markdownDescription": "将自定义全局变量视为一些特殊的内置变量,语言服务将提供特殊的支持。\n下面这个例子表示将 `include` 视为 `require` 。\n```json\n\"Lua.runtime.special\" : {\n    \"include\" : \"require\"\n}\n```\n",
             "patternProperties": {
                 ".*": {
@@ -2857,7 +2870,8 @@
                         "xpcall",
                         "assert",
                         "error",
-                        "type"
+                        "type",
+                        "os.exit"
                     ],
                     "type": "string"
                 }
diff --git a/setting/schema-zh-tw.json b/setting/schema-zh-tw.json
index faf922ababd0da7a2d395d13756262be83e49d36..278214d696a4bb61dbd3338b64311b9c64442025 100644
--- a/setting/schema-zh-tw.json
+++ b/setting/schema-zh-tw.json
@@ -1,6 +1,19 @@
 {
     "description": "Setting of sumneko.lua",
     "properties": {
+        "addonManager": {
+            "properties": {
+                "enable": {
+                    "$ref": "#/properties/addonManager.enable"
+                }
+            }
+        },
+        "addonManager.enable": {
+            "default": true,
+            "markdownDescription": "Whether the addon manager is enabled or not.",
+            "scope": "resource",
+            "type": "boolean"
+        },
         "codeLens": {
             "properties": {
                 "enable": {
@@ -2318,7 +2331,7 @@
         },
         "format.defaultConfig": {
             "additionalProperties": false,
-            "default": [],
+            "default": {},
             "markdownDescription": "預設的格式化組態,優先順序低於工作區內的 `.editorconfig` 檔案。\n請查閱[格式化文件](https://github.com/CppCXY/EmmyLuaCodeStyle/tree/master/docs)了解用法。\n",
             "patternProperties": {
                 ".*": {
@@ -2840,7 +2853,7 @@
         },
         "runtime.special": {
             "additionalProperties": false,
-            "default": [],
+            "default": {},
             "markdownDescription": "將自訂全域變數視為一些特殊的內建變數,語言伺服將提供特殊的支援。\n下面這個例子表示將 `include` 視為 `require` 。\n```json\n\"Lua.runtime.special\" : {\n    \"include\" : \"require\"\n}\n```\n",
             "patternProperties": {
                 ".*": {
@@ -2857,7 +2870,8 @@
                         "xpcall",
                         "assert",
                         "error",
-                        "type"
+                        "type",
+                        "os.exit"
                     ],
                     "type": "string"
                 }
diff --git a/setting/schema.json b/setting/schema.json
index e6459fa9aea212ea2781bbac7978cd1b92d005ef..75ca47f267a9128c5dfe0b352de2625b1ab97ffd 100644
--- a/setting/schema.json
+++ b/setting/schema.json
@@ -1,6 +1,19 @@
 {
     "description": "Setting of sumneko.lua",
     "properties": {
+        "addonManager": {
+            "properties": {
+                "enable": {
+                    "$ref": "#/properties/addonManager.enable"
+                }
+            }
+        },
+        "addonManager.enable": {
+            "default": true,
+            "markdownDescription": "Whether the addon manager is enabled or not.",
+            "scope": "resource",
+            "type": "boolean"
+        },
         "codeLens": {
             "properties": {
                 "enable": {
@@ -2318,7 +2331,7 @@
         },
         "format.defaultConfig": {
             "additionalProperties": false,
-            "default": [],
+            "default": {},
             "markdownDescription": "The default format configuration. Has a lower priority than `.editorconfig` file in the workspace.\nRead [formatter docs](https://github.com/CppCXY/EmmyLuaCodeStyle/tree/master/docs) to learn usage.\n",
             "patternProperties": {
                 ".*": {
@@ -2840,7 +2853,7 @@
         },
         "runtime.special": {
             "additionalProperties": false,
-            "default": [],
+            "default": {},
             "markdownDescription": "The custom global variables are regarded as some special built-in variables, and the language server will provide special support\nThe following example shows that 'include' is treated as' require '.\n```json\n\"Lua.runtime.special\" : {\n    \"include\" : \"require\"\n}\n```\n",
             "patternProperties": {
                 ".*": {
@@ -2857,7 +2870,8 @@
                         "xpcall",
                         "assert",
                         "error",
-                        "type"
+                        "type",
+                        "os.exit"
                     ],
                     "type": "string"
                 }