From bb90ced5fa1bfabd62032d75641e3209bc3fa072 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 23 Sep 2025 17:01:45 +0300 Subject: [PATCH 01/45] Fix global flags escaping when starting a workspace using the CLI (#592) Closes #591 --- src/api/workspace.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/api/workspace.ts b/src/api/workspace.ts index 3da5f150..45fa9156 100644 --- a/src/api/workspace.ts +++ b/src/api/workspace.ts @@ -4,6 +4,7 @@ import { Workspace } from "coder/site/src/api/typesGenerated"; import * as vscode from "vscode"; import { FeatureSet } from "../featureSet"; import { getGlobalFlags } from "../globalFlags"; +import { escapeCommandArg } from "../util"; import { errToStr, createWorkspaceIdentifier } from "./api-helper"; import { CoderApi } from "./coderApi"; @@ -36,7 +37,9 @@ export async function startWorkspaceIfStoppedOrFailed( startArgs.push(...["--reason", "vscode_connection"]); } - const startProcess = spawn(binPath, startArgs, { shell: true }); + // { shell: true } requires one shell-safe command string, otherwise we lose all escaping + const cmd = `${escapeCommandArg(binPath)} ${startArgs.join(" ")}`; + const startProcess = spawn(cmd, { shell: true }); startProcess.stdout.on("data", (data: Buffer) => { data From a09d254175bb73f47b4ff7bf9c91212bf9c131e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:44:46 +0300 Subject: [PATCH 02/45] chore(deps-dev): bump @vscode/test-cli from 0.0.10 to 0.0.11 (#568) --- package.json | 2 +- yarn.lock | 145 ++++++++++++++------------------------------------- 2 files changed, 41 insertions(+), 106 deletions(-) diff --git a/package.json b/package.json index c250c02f..9aa5d05d 100644 --- a/package.json +++ b/package.json @@ -336,7 +336,7 @@ "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^6.21.0", - "@vscode/test-cli": "^0.0.10", + "@vscode/test-cli": "^0.0.11", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^3.6.0", "bufferutil": "^4.0.9", diff --git a/yarn.lock b/yarn.lock index 5cc462f3..f30780a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1233,10 +1233,10 @@ loupe "^2.3.6" pretty-format "^29.5.0" -"@vscode/test-cli@^0.0.10": - version "0.0.10" - resolved "https://registry.yarnpkg.com/@vscode/test-cli/-/test-cli-0.0.10.tgz#35f0e81c2e0ff8daceb223e99d1b65306c15822c" - integrity sha512-B0mMH4ia+MOOtwNiLi79XhA+MLmUItIC8FckEuKrVAVriIuSWjt7vv4+bF8qVFiNFe4QRfzPaIZk39FZGWEwHA== +"@vscode/test-cli@^0.0.11": + version "0.0.11" + resolved "https://registry.yarnpkg.com/@vscode/test-cli/-/test-cli-0.0.11.tgz#043b2c920ef1b115626eaabc5b02cd956044a51d" + integrity sha512-qO332yvzFqGhBMJrp6TdwbIydiHgCtxXc2Nl6M58mbH/Z+0CyLR76Jzv4YWPEthhrARprzCRJUqzFvTHFhTj7Q== dependencies: "@types/mocha" "^10.0.2" c8 "^9.1.0" @@ -1244,7 +1244,7 @@ enhanced-resolve "^5.15.0" glob "^10.3.10" minimatch "^9.0.3" - mocha "^10.2.0" + mocha "^11.1.0" supports-color "^9.4.0" yargs "^17.7.2" @@ -1588,11 +1588,6 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.17.1, ajv@^8.9.0: json-schema-traverse "^1.0.0" require-from-string "^2.0.2" -ansi-colors@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== - ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -2201,6 +2196,13 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +chokidar@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -2249,15 +2251,6 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -2625,10 +2618,10 @@ diff-sequences@^29.4.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== -diff@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" - integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== +diff@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" + integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== dir-glob@^3.0.1: version "3.0.1" @@ -2748,15 +2741,7 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1: - version "5.17.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" - integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -enhanced-resolve@^5.15.0: +enhanced-resolve@^5.0.0, enhanced-resolve@^5.15.0, enhanced-resolve@^5.17.1: version "5.18.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf" integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg== @@ -3816,7 +3801,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^10.3.10: +glob@^10.3.10, glob@^10.4.2, glob@^10.4.5: version "10.4.5" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== @@ -3828,18 +3813,6 @@ glob@^10.3.10: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^10.4.2: - version "10.4.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.2.tgz#bed6b95dade5c1f80b4434daced233aee76160e5" - integrity sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w== - dependencies: - foreground-child "^3.1.0" - jackspeak "^3.1.2" - minimatch "^9.0.4" - minipass "^7.1.2" - package-json-from-dist "^1.0.0" - path-scurry "^1.11.1" - glob@^11.0.0: version "11.0.3" resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.3.tgz#9d8087e6d72ddb3c4707b1d2778f80ea3eaefcd6" @@ -3864,17 +3837,6 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -5175,27 +5137,13 @@ minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatc dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1, minimatch@^5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.3: +minimatch@^9.0.3, minimatch@^9.0.4, minimatch@^9.0.5: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.4: - version "9.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" - integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== - dependencies: - brace-expansion "^2.0.1" - minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" @@ -5228,30 +5176,30 @@ mlly@^1.2.0, mlly@^1.4.0: pkg-types "^1.0.3" ufo "^1.3.0" -mocha@^10.2.0: - version "10.8.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.8.2.tgz#8d8342d016ed411b12a429eb731b825f961afb96" - integrity sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg== +mocha@^11.1.0: + version "11.7.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.7.2.tgz#3c0079fe5cc2f8ea86d99124debcc42bb1ab22b5" + integrity sha512-lkqVJPmqqG/w5jmmFtiRvtA2jkDyNVUcefFJKb2uyX4dekk8Okgqop3cgbFiaIvj8uCRJVTP5x9dfxGyXm2jvQ== dependencies: - ansi-colors "^4.1.3" browser-stdout "^1.3.1" - chokidar "^3.5.3" + chokidar "^4.0.1" debug "^4.3.5" - diff "^5.2.0" + diff "^7.0.0" escape-string-regexp "^4.0.0" find-up "^5.0.0" - glob "^8.1.0" + glob "^10.4.5" he "^1.2.0" js-yaml "^4.1.0" log-symbols "^4.1.0" - minimatch "^5.1.6" + minimatch "^9.0.5" ms "^2.1.3" + picocolors "^1.1.1" serialize-javascript "^6.0.2" strip-json-comments "^3.1.1" supports-color "^8.1.1" - workerpool "^6.5.1" - yargs "^16.2.0" - yargs-parser "^20.2.9" + workerpool "^9.2.0" + yargs "^17.7.2" + yargs-parser "^21.1.1" yargs-unparser "^2.0.0" ms@^2.1.1, ms@^2.1.3: @@ -6014,6 +5962,11 @@ readable-stream@^3.1.1, readable-stream@^3.4.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -8184,10 +8137,10 @@ word-wrap@1.2.5, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -workerpool@^6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" - integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== +workerpool@^9.2.0: + version "9.3.4" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-9.3.4.tgz#f6c92395b2141afd78e2a889e80cb338fe9fca41" + integrity sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg== "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" @@ -8322,11 +8275,6 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2, yargs-parser@^20.2.9: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" @@ -8364,19 +8312,6 @@ yargs@^15.0.2: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" From dc4d6d472fba848c254ab9f087f0ebe5e5daa68a Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 24 Sep 2025 12:09:41 +0300 Subject: [PATCH 03/45] v1.11.0 (#593) --- CHANGELOG.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4170b73d..35649a76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## [v1.11.0](https://github.com/coder/vscode-coder/releases/tag/v1.11.0) 2025-09-24 + ### Changed - Always enable verbose (`-v`) flag when a log directory is configured (`coder.proxyLogDir`). diff --git a/package.json b/package.json index 9aa5d05d..b07e754f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coder-remote", "displayName": "Coder", - "version": "1.10.1", + "version": "1.11.0", "description": "Open any workspace with a single click.", "categories": [ "Other" From 52df12cbadde5d6fb87d78e1e2c5726958ee33f3 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Thu, 25 Sep 2025 13:09:55 +0300 Subject: [PATCH 04/45] refactor(storage): split storage.ts into isolated modules; add unit tests; upgrade vitest (#589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #588 This PR refactors `storage.ts` into small, focused modules that are straightforward to unit test (with mocks). It also upgrades `vitest` to a version that plays nicely with VS Code extensions so we can view coverage and run/debug tests directly in VS Code. Key changes - Extract path resolution from `storage.ts` → dedicated module - Extract memento & secrets management from `storage.ts` → dedicated module - Extract and fully separate CLI management logic → dedicated module - Remove `storage.ts` entirely in favor of the new modules - Add unit tests for the split modules - Upgrade `vitest` and related tooling for VS Code extension testing Why mock `vscode`? - Unit tests (mocked `vscode`): fast, reliable, deterministic validation of module behavior without depending on VS Code APIs or external calls (e.g., Axios). - Integration/E2E tests (real VS Code): cover end-to-end flows by launching VS Code (and eventually a server). Valuable but slower and harder to automate; we reserve these for scenarios that require the actual runtime. --- package.json | 5 +- src/__mocks__/testHelpers.ts | 274 ++++ src/__mocks__/vscode.runtime.ts | 142 ++ src/{cliManager.test.ts => cliUtils.test.ts} | 12 +- src/{cliManager.ts => cliUtils.ts} | 14 - src/commands.ts | 62 +- src/core/cliManager.test.ts | 795 +++++++++++ src/{storage.ts => core/cliManager.ts} | 283 +--- src/core/mementoManager.test.ts | 81 ++ src/core/mementoManager.ts | 71 + src/core/pathResolver.test.ts | 48 + src/core/pathResolver.ts | 115 ++ src/core/secretsManager.test.ts | 42 + src/core/secretsManager.ts | 29 + src/error.test.ts | 446 +++--- src/extension.ts | 82 +- src/headers.test.ts | 16 +- src/inbox.ts | 17 +- src/pgp.ts | 8 +- src/remote.ts | 140 +- src/workspaceMonitor.ts | 10 +- src/workspacesProvider.ts | 8 +- vitest.config.ts | 9 + yarn.lock | 1275 ++++++++++-------- 24 files changed, 2760 insertions(+), 1224 deletions(-) create mode 100644 src/__mocks__/testHelpers.ts create mode 100644 src/__mocks__/vscode.runtime.ts rename src/{cliManager.test.ts => cliUtils.test.ts} (95%) rename src/{cliManager.ts => cliUtils.ts} (92%) create mode 100644 src/core/cliManager.test.ts rename src/{storage.ts => core/cliManager.ts} (68%) create mode 100644 src/core/mementoManager.test.ts create mode 100644 src/core/mementoManager.ts create mode 100644 src/core/pathResolver.test.ts create mode 100644 src/core/pathResolver.ts create mode 100644 src/core/secretsManager.test.ts create mode 100644 src/core/secretsManager.ts diff --git a/package.json b/package.json index b07e754f..9fb96fcb 100644 --- a/package.json +++ b/package.json @@ -316,7 +316,6 @@ "eventsource": "^3.0.6", "find-process": "https://github.com/coder/find-process#fix/sequoia-compat", "jsonc-parser": "^3.3.1", - "memfs": "^4.17.1", "node-forge": "^1.3.1", "openpgp": "^6.2.0", "pretty-bytes": "^7.0.0", @@ -336,6 +335,7 @@ "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^6.21.0", + "@vitest/coverage-v8": "^3.2.4", "@vscode/test-cli": "^0.0.11", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^3.6.0", @@ -350,12 +350,13 @@ "eslint-plugin-prettier": "^5.4.1", "glob": "^10.4.2", "jsonc-eslint-parser": "^2.4.0", + "memfs": "^4.46.0", "nyc": "^17.1.0", "prettier": "^3.5.3", "ts-loader": "^9.5.1", "typescript": "^5.8.3", "utf-8-validate": "^6.0.5", - "vitest": "^0.34.6", + "vitest": "^3.2.4", "vscode-test": "^1.5.0", "webpack": "^5.99.6", "webpack-cli": "^5.1.4" diff --git a/src/__mocks__/testHelpers.ts b/src/__mocks__/testHelpers.ts new file mode 100644 index 00000000..3a4ce407 --- /dev/null +++ b/src/__mocks__/testHelpers.ts @@ -0,0 +1,274 @@ +import { vi } from "vitest"; +import * as vscode from "vscode"; + +/** + * Mock configuration provider that integrates with the vscode workspace configuration mock. + * Use this to set configuration values that will be returned by vscode.workspace.getConfiguration(). + */ +export class MockConfigurationProvider { + private config = new Map(); + + constructor() { + this.setupVSCodeMock(); + } + + /** + * Set a configuration value that will be returned by vscode.workspace.getConfiguration().get() + */ + set(key: string, value: unknown): void { + this.config.set(key, value); + } + + /** + * Get a configuration value (for testing purposes) + */ + get(key: string): T | undefined; + get(key: string, defaultValue: T): T; + get(key: string, defaultValue?: T): T | undefined { + const value = this.config.get(key); + return value !== undefined ? (value as T) : defaultValue; + } + + /** + * Clear all configuration values + */ + clear(): void { + this.config.clear(); + } + + /** + * Setup the vscode.workspace.getConfiguration mock to return our values + */ + private setupVSCodeMock(): void { + vi.mocked(vscode.workspace.getConfiguration).mockImplementation( + (section?: string) => { + // Create a snapshot of the current config when getConfiguration is called + const snapshot = new Map(this.config); + const getFullKey = (part: string) => + section ? `${section}.${part}` : part; + + return { + get: vi.fn((key: string, defaultValue?: unknown) => { + const value = snapshot.get(getFullKey(key)); + return value !== undefined ? value : defaultValue; + }), + has: vi.fn((key: string) => { + return snapshot.has(getFullKey(key)); + }), + inspect: vi.fn(), + update: vi.fn((key: string, value: unknown) => { + this.config.set(getFullKey(key), value); + return Promise.resolve(); + }), + }; + }, + ); + } +} + +/** + * Mock progress reporter that integrates with vscode.window.withProgress. + * Use this to control progress reporting behavior and cancellation in tests. + */ +export class MockProgressReporter { + private shouldCancel = false; + private progressReports: Array<{ message?: string; increment?: number }> = []; + + constructor() { + this.setupVSCodeMock(); + } + + /** + * Set whether the progress should be cancelled + */ + setCancellation(cancel: boolean): void { + this.shouldCancel = cancel; + } + + /** + * Get all progress reports that were made + */ + getProgressReports(): Array<{ message?: string; increment?: number }> { + return [...this.progressReports]; + } + + /** + * Clear all progress reports + */ + clearProgressReports(): void { + this.progressReports = []; + } + + /** + * Setup the vscode.window.withProgress mock + */ + private setupVSCodeMock(): void { + vi.mocked(vscode.window.withProgress).mockImplementation( + async ( + _options: vscode.ProgressOptions, + task: ( + progress: vscode.Progress<{ message?: string; increment?: number }>, + token: vscode.CancellationToken, + ) => Thenable, + ): Promise => { + const progress = { + report: vi.fn((value: { message?: string; increment?: number }) => { + this.progressReports.push(value); + }), + }; + + const cancellationToken: vscode.CancellationToken = { + isCancellationRequested: this.shouldCancel, + onCancellationRequested: vi.fn((listener: (x: unknown) => void) => { + if (this.shouldCancel) { + setTimeout(listener, 0); + } + return { dispose: vi.fn() }; + }), + }; + + return task(progress, cancellationToken); + }, + ); + } +} + +/** + * Mock user interaction that integrates with vscode.window message dialogs. + * Use this to control user responses in tests. + */ +export class MockUserInteraction { + private responses = new Map(); + private externalUrls: string[] = []; + + constructor() { + this.setupVSCodeMock(); + } + + /** + * Set a response for a specific message + */ + setResponse(message: string, response: string | undefined): void { + this.responses.set(message, response); + } + + /** + * Get all URLs that were opened externally + */ + getExternalUrls(): string[] { + return [...this.externalUrls]; + } + + /** + * Clear all external URLs + */ + clearExternalUrls(): void { + this.externalUrls = []; + } + + /** + * Clear all responses + */ + clearResponses(): void { + this.responses.clear(); + } + + /** + * Setup the vscode.window message dialog mocks + */ + private setupVSCodeMock(): void { + const getResponse = (message: string): string | undefined => { + return this.responses.get(message); + }; + + vi.mocked(vscode.window.showErrorMessage).mockImplementation( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: string): Thenable => { + const response = getResponse(message); + return Promise.resolve(response); + }, + ); + + vi.mocked(vscode.window.showWarningMessage).mockImplementation( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: string): Thenable => { + const response = getResponse(message); + return Promise.resolve(response); + }, + ); + + vi.mocked(vscode.window.showInformationMessage).mockImplementation( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: string): Thenable => { + const response = getResponse(message); + return Promise.resolve(response); + }, + ); + + vi.mocked(vscode.env.openExternal).mockImplementation( + (target: vscode.Uri): Promise => { + this.externalUrls.push(target.toString()); + return Promise.resolve(true); + }, + ); + } +} + +// Simple in-memory implementation of Memento +export class InMemoryMemento implements vscode.Memento { + private storage = new Map(); + + get(key: string): T | undefined; + get(key: string, defaultValue: T): T; + get(key: string, defaultValue?: T): T | undefined { + return this.storage.has(key) ? (this.storage.get(key) as T) : defaultValue; + } + + async update(key: string, value: unknown): Promise { + if (value === undefined) { + this.storage.delete(key); + } else { + this.storage.set(key, value); + } + return Promise.resolve(); + } + + keys(): readonly string[] { + return Array.from(this.storage.keys()); + } +} + +// Simple in-memory implementation of SecretStorage +export class InMemorySecretStorage implements vscode.SecretStorage { + private secrets = new Map(); + private isCorrupted = false; + + onDidChange: vscode.Event = () => ({ + dispose: () => {}, + }); + + async get(key: string): Promise { + if (this.isCorrupted) { + return Promise.reject(new Error("Storage corrupted")); + } + return this.secrets.get(key); + } + + async store(key: string, value: string): Promise { + if (this.isCorrupted) { + return Promise.reject(new Error("Storage corrupted")); + } + this.secrets.set(key, value); + } + + async delete(key: string): Promise { + if (this.isCorrupted) { + return Promise.reject(new Error("Storage corrupted")); + } + this.secrets.delete(key); + } + + corruptStorage(): void { + this.isCorrupted = true; + } +} diff --git a/src/__mocks__/vscode.runtime.ts b/src/__mocks__/vscode.runtime.ts new file mode 100644 index 00000000..2201a851 --- /dev/null +++ b/src/__mocks__/vscode.runtime.ts @@ -0,0 +1,142 @@ +import { vi } from "vitest"; + +// enum-like helpers +const E = >(o: T) => Object.freeze(o); + +export const ProgressLocation = E({ + SourceControl: 1, + Window: 10, + Notification: 15, +}); +export const ViewColumn = E({ + Active: -1, + Beside: -2, + One: 1, + Two: 2, + Three: 3, +}); +export const ConfigurationTarget = E({ + Global: 1, + Workspace: 2, + WorkspaceFolder: 3, +}); +export const TreeItemCollapsibleState = E({ + None: 0, + Collapsed: 1, + Expanded: 2, +}); +export const StatusBarAlignment = E({ Left: 1, Right: 2 }); +export const ExtensionMode = E({ Production: 1, Development: 2, Test: 3 }); +export const UIKind = E({ Desktop: 1, Web: 2 }); + +export class Uri { + constructor( + public scheme: string, + public path: string, + ) {} + static file(p: string) { + return new Uri("file", p); + } + static parse(v: string) { + if (v.startsWith("file://")) { + return Uri.file(v.slice("file://".length)); + } + const [scheme, ...rest] = v.split(":"); + return new Uri(scheme, rest.join(":")); + } + toString() { + return this.scheme === "file" + ? `file://${this.path}` + : `${this.scheme}:${this.path}`; + } + static joinPath(base: Uri, ...paths: string[]) { + const sep = base.path.endsWith("/") ? "" : "/"; + return new Uri(base.scheme, base.path + sep + paths.join("/")); + } +} + +// mini event +const makeEvent = () => { + const listeners = new Set<(e: T) => void>(); + const event = (listener: (e: T) => void) => { + listeners.add(listener); + return { dispose: () => listeners.delete(listener) }; + }; + return { event, fire: (e: T) => listeners.forEach((l) => l(e)) }; +}; + +const onDidChangeConfiguration = makeEvent(); +const onDidChangeWorkspaceFolders = makeEvent(); + +export const window = { + showInformationMessage: vi.fn(), + showWarningMessage: vi.fn(), + showErrorMessage: vi.fn(), + showQuickPick: vi.fn(), + showInputBox: vi.fn(), + withProgress: vi.fn(), + createOutputChannel: vi.fn(() => ({ + appendLine: vi.fn(), + append: vi.fn(), + show: vi.fn(), + hide: vi.fn(), + dispose: vi.fn(), + clear: vi.fn(), + })), +}; + +export const commands = { + registerCommand: vi.fn(), + executeCommand: vi.fn(), +}; + +export const workspace = { + getConfiguration: vi.fn(), // your helpers override this + workspaceFolders: [] as unknown[], + fs: { + readFile: vi.fn(), + writeFile: vi.fn(), + stat: vi.fn(), + readDirectory: vi.fn(), + }, + onDidChangeConfiguration: onDidChangeConfiguration.event, + onDidChangeWorkspaceFolders: onDidChangeWorkspaceFolders.event, + + // test-only triggers: + __fireDidChangeConfiguration: onDidChangeConfiguration.fire, + __fireDidChangeWorkspaceFolders: onDidChangeWorkspaceFolders.fire, +}; + +export const env = { + appName: "Visual Studio Code", + appRoot: "/app", + language: "en", + machineId: "test-machine-id", + sessionId: "test-session-id", + remoteName: undefined as string | undefined, + shell: "/bin/bash", + openExternal: vi.fn(), +}; + +export const extensions = { + getExtension: vi.fn(), + all: [] as unknown[], +}; + +const vscode = { + ProgressLocation, + ViewColumn, + ConfigurationTarget, + TreeItemCollapsibleState, + StatusBarAlignment, + ExtensionMode, + UIKind, + Uri, + window, + commands, + workspace, + env, + extensions, +}; + +export default vscode; diff --git a/src/cliManager.test.ts b/src/cliUtils.test.ts similarity index 95% rename from src/cliManager.test.ts rename to src/cliUtils.test.ts index 87540a61..aec78e87 100644 --- a/src/cliManager.test.ts +++ b/src/cliUtils.test.ts @@ -2,9 +2,9 @@ import fs from "fs/promises"; import os from "os"; import path from "path"; import { beforeAll, describe, expect, it } from "vitest"; -import * as cli from "./cliManager"; +import * as cli from "./cliUtils"; -describe("cliManager", () => { +describe("cliUtils", () => { const tmp = path.join(os.tmpdir(), "vscode-coder-tests"); beforeAll(async () => { @@ -25,14 +25,6 @@ describe("cliManager", () => { expect((await cli.stat(binPath))?.size).toBe(4); }); - it("rm", async () => { - const binPath = path.join(tmp, "rm"); - await cli.rm(binPath); - - await fs.writeFile(binPath, "test"); - await cli.rm(binPath); - }); - // TODO: CI only runs on Linux but we should run it on Windows too. it("version", async () => { const binPath = path.join(tmp, "version"); diff --git a/src/cliManager.ts b/src/cliUtils.ts similarity index 92% rename from src/cliManager.ts rename to src/cliUtils.ts index 60b63f92..cc92a345 100644 --- a/src/cliManager.ts +++ b/src/cliUtils.ts @@ -21,20 +21,6 @@ export async function stat(binPath: string): Promise { } } -/** - * Remove the path. Throw if unable to remove. - */ -export async function rm(binPath: string): Promise { - try { - await fs.rm(binPath, { force: true }); - } catch (error) { - // Just in case; we should never get an ENOENT because of force: true. - if ((error as NodeJS.ErrnoException)?.code !== "ENOENT") { - throw error; - } - } -} - // util.promisify types are dynamic so there is no concrete type we can import // and we have to make our own. type ExecException = ExecFileException & { stdout?: string; stderr?: string }; diff --git a/src/commands.ts b/src/commands.ts index 9961c82b..914adbfc 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -5,14 +5,17 @@ import { Workspace, WorkspaceAgent, } from "coder/site/src/api/typesGenerated"; -import path from "node:path"; import * as vscode from "vscode"; import { createWorkspaceIdentifier, extractAgents } from "./api/api-helper"; import { CoderApi } from "./api/coderApi"; import { needToken } from "./api/utils"; +import { CliManager } from "./core/cliManager"; +import { MementoManager } from "./core/mementoManager"; +import { PathResolver } from "./core/pathResolver"; +import { SecretsManager } from "./core/secretsManager"; import { CertificateError } from "./error"; import { getGlobalFlags } from "./globalFlags"; -import { Storage } from "./storage"; +import { Logger } from "./logging/logger"; import { escapeCommandArg, toRemoteAuthority, toSafeHost } from "./util"; import { AgentTreeItem, @@ -35,7 +38,11 @@ export class Commands { public constructor( private readonly vscodeProposed: typeof vscode, private readonly restClient: Api, - private readonly storage: Storage, + private readonly logger: Logger, + private readonly pathResolver: PathResolver, + private readonly mementoManager: MementoManager, + private readonly secretsManager: SecretsManager, + private readonly cliManager: CliManager, ) {} /** @@ -103,7 +110,7 @@ export class Commands { quickPick.title = "Enter the URL of your Coder deployment."; // Initial items. - quickPick.items = this.storage + quickPick.items = this.mementoManager .withUrlHistory(defaultURL, process.env.CODER_URL) .map((url) => ({ alwaysShow: true, @@ -114,7 +121,7 @@ export class Commands { // an option in case the user wants to connect to something that is not in // the list. quickPick.onDidChangeValue((value) => { - quickPick.items = this.storage + quickPick.items = this.mementoManager .withUrlHistory(defaultURL, process.env.CODER_URL, value) .map((url) => ({ alwaysShow: true, @@ -194,11 +201,11 @@ export class Commands { this.restClient.setSessionToken(res.token); // Store these to be used in later sessions. - await this.storage.setUrl(url); - await this.storage.setSessionToken(res.token); + await this.mementoManager.setUrl(url); + await this.secretsManager.setSessionToken(res.token); // Store on disk to be used by the cli. - await this.storage.configureCli(label, url, res.token); + await this.cliManager.configure(label, url, res.token); // These contexts control various menu items and the sidebar. await vscode.commands.executeCommand( @@ -240,7 +247,7 @@ export class Commands { token: string, isAutologin: boolean, ): Promise<{ user: User; token: string } | null> { - const client = CoderApi.create(url, token, this.storage.output, () => + const client = CoderApi.create(url, token, this.logger, () => vscode.workspace.getConfiguration(), ); if (!needToken(vscode.workspace.getConfiguration())) { @@ -252,10 +259,7 @@ export class Commands { } catch (err) { const message = getErrorMessage(err, "no response from the server"); if (isAutologin) { - this.storage.output.warn( - "Failed to log in to Coder server:", - message, - ); + this.logger.warn("Failed to log in to Coder server:", message); } else { this.vscodeProposed.window.showErrorMessage( "Failed to log in to Coder server", @@ -283,7 +287,7 @@ export class Commands { title: "Coder API Key", password: true, placeHolder: "Paste your API key.", - value: token || (await this.storage.getSessionToken()), + value: token || (await this.secretsManager.getSessionToken()), ignoreFocusOut: true, validateInput: async (value) => { client.setSessionToken(value); @@ -349,7 +353,7 @@ export class Commands { * Log out from the currently logged-in deployment. */ public async logout(): Promise { - const url = this.storage.getUrl(); + const url = this.mementoManager.getUrl(); if (!url) { // Sanity check; command should not be available if no url. throw new Error("You are not logged in"); @@ -361,8 +365,8 @@ export class Commands { this.restClient.setSessionToken(""); // Clear from memory. - await this.storage.setUrl(undefined); - await this.storage.setSessionToken(undefined); + await this.mementoManager.setUrl(undefined); + await this.secretsManager.setSessionToken(undefined); await vscode.commands.executeCommand( "setContext", @@ -387,7 +391,7 @@ export class Commands { * Must only be called if currently logged in. */ public async createWorkspace(): Promise { - const uri = this.storage.getUrl() + "/templates"; + const uri = this.mementoManager.getUrl() + "/templates"; await vscode.commands.executeCommand("vscode.open", uri); } @@ -402,7 +406,7 @@ export class Commands { public async navigateToWorkspace(item: OpenableTreeItem) { if (item) { const workspaceId = createWorkspaceIdentifier(item.workspace); - const uri = this.storage.getUrl() + `/@${workspaceId}`; + const uri = this.mementoManager.getUrl() + `/@${workspaceId}`; await vscode.commands.executeCommand("vscode.open", uri); } else if (this.workspace && this.workspaceRestClient) { const baseUrl = @@ -425,7 +429,7 @@ export class Commands { public async navigateToWorkspaceSettings(item: OpenableTreeItem) { if (item) { const workspaceId = createWorkspaceIdentifier(item.workspace); - const uri = this.storage.getUrl() + `/@${workspaceId}/settings`; + const uri = this.mementoManager.getUrl() + `/@${workspaceId}/settings`; await vscode.commands.executeCommand("vscode.open", uri); } else if (this.workspace && this.workspaceRestClient) { const baseUrl = @@ -503,17 +507,17 @@ export class Commands { // If workspace_name is provided, run coder ssh before the command - const url = this.storage.getUrl(); + const url = this.mementoManager.getUrl(); if (!url) { throw new Error("No coder url found for sidebar"); } - const binary = await this.storage.fetchBinary( + const binary = await this.cliManager.fetchBinary( this.restClient, toSafeHost(url), ); - const configDir = path.dirname( - this.storage.getSessionTokenPath(toSafeHost(url)), + const configDir = this.pathResolver.getGlobalConfigDir( + toSafeHost(url), ); const globalFlags = getGlobalFlags( vscode.workspace.getConfiguration(), @@ -645,8 +649,8 @@ export class Commands { newWindow = false; } - // Only set the memento if when opening a new folder - await this.storage.setFirstConnect(); + // Only set the memento when opening a new folder + await this.mementoManager.setFirstConnect(); await vscode.commands.executeCommand( "vscode.openFolder", vscode.Uri.from({ @@ -755,7 +759,7 @@ export class Commands { // If we have no agents, the workspace may not be running, in which case // we need to fetch the agents through the resources API, as the // workspaces query does not include agents when off. - this.storage.output.info("Fetching agents from template version"); + this.logger.info("Fetching agents from template version"); const resources = await this.restClient.getTemplateVersionResources( workspace.latest_build.template_version_id, ); @@ -826,8 +830,8 @@ export class Commands { } } - // Only set the memento if when opening a new folder/window - await this.storage.setFirstConnect(); + // Only set the memento when opening a new folder/window + await this.mementoManager.setFirstConnect(); if (folderPath) { await vscode.commands.executeCommand( "vscode.openFolder", diff --git a/src/core/cliManager.test.ts b/src/core/cliManager.test.ts new file mode 100644 index 00000000..676de44c --- /dev/null +++ b/src/core/cliManager.test.ts @@ -0,0 +1,795 @@ +import globalAxios, { AxiosInstance } from "axios"; +import { Api } from "coder/site/src/api/api"; +import EventEmitter from "events"; +import * as fs from "fs"; +import { IncomingMessage } from "http"; +import { fs as memfs, vol } from "memfs"; +import * as os from "os"; +import * as path from "path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import * as vscode from "vscode"; +import { + MockConfigurationProvider, + MockProgressReporter, + MockUserInteraction, +} from "../__mocks__/testHelpers"; +import * as cli from "../cliUtils"; +import { Logger } from "../logging/logger"; +import * as pgp from "../pgp"; +import { CliManager } from "./cliManager"; +import { PathResolver } from "./pathResolver"; + +vi.mock("os"); +vi.mock("axios"); +vi.mock("../pgp"); + +vi.mock("fs", async () => { + const memfs: { fs: typeof fs } = await vi.importActual("memfs"); + return { + ...memfs.fs, + default: memfs.fs, + }; +}); + +vi.mock("fs/promises", async () => { + const memfs: { fs: typeof fs } = await vi.importActual("memfs"); + return { + ...memfs.fs.promises, + default: memfs.fs.promises, + }; +}); + +// Only mock the platform detection functions from CLI manager +vi.mock("../cliUtils", async () => { + const actual = + await vi.importActual("../cliUtils"); + return { + ...actual, + // No need to test script execution here + version: vi.fn(), + }; +}); + +describe("CliManager", () => { + let manager: CliManager; + let mockConfig: MockConfigurationProvider; + let mockProgress: MockProgressReporter; + let mockUI: MockUserInteraction; + let mockApi: Api; + let mockAxios: AxiosInstance; + + const TEST_VERSION = "1.2.3"; + const TEST_URL = "https://test.coder.com"; + const BASE_PATH = "/path/base"; + const BINARY_DIR = `${BASE_PATH}/test/bin`; + const PLATFORM = "linux"; + const ARCH = "amd64"; + const BINARY_NAME = `coder-${PLATFORM}-${ARCH}`; + const BINARY_PATH = `${BINARY_DIR}/${BINARY_NAME}`; + + beforeEach(() => { + vi.resetAllMocks(); + vol.reset(); + + // Core setup + mockApi = createMockApi(TEST_VERSION, TEST_URL); + mockAxios = mockApi.getAxiosInstance(); + vi.mocked(globalAxios.create).mockReturnValue(mockAxios); + mockConfig = new MockConfigurationProvider(); + mockProgress = new MockProgressReporter(); + mockUI = new MockUserInteraction(); + manager = new CliManager( + vscode, + createMockLogger(), + new PathResolver(BASE_PATH, "/code/log"), + ); + + // Mock only what's necessary + vi.mocked(os.platform).mockReturnValue(PLATFORM); + vi.mocked(os.arch).mockReturnValue(ARCH); + vi.mocked(pgp.readPublicKeys).mockResolvedValue([]); + }); + + afterEach(async () => { + mockProgress?.setCancellation(false); + vi.clearAllTimers(); + // memfs internally schedules some FS operations so we have to wait for them to finish + await new Promise((resolve) => setImmediate(resolve)); + vol.reset(); + }); + + describe("Configure CLI", () => { + it("should write both url and token to correct paths", async () => { + await manager.configure( + "deployment", + "https://coder.example.com", + "test-token", + ); + + expect(memfs.readFileSync("/path/base/deployment/url", "utf8")).toBe( + "https://coder.example.com", + ); + expect(memfs.readFileSync("/path/base/deployment/session", "utf8")).toBe( + "test-token", + ); + }); + + it("should skip URL when undefined but write token", async () => { + await manager.configure("deployment", undefined, "test-token"); + + // No entry for the url + expect(memfs.existsSync("/path/base/deployment/url")).toBe(false); + expect(memfs.readFileSync("/path/base/deployment/session", "utf8")).toBe( + "test-token", + ); + }); + + it("should skip token when null but write URL", async () => { + await manager.configure("deployment", "https://coder.example.com", null); + + // No entry for the session + expect(memfs.readFileSync("/path/base/deployment/url", "utf8")).toBe( + "https://coder.example.com", + ); + expect(memfs.existsSync("/path/base/deployment/session")).toBe(false); + }); + + it("should write empty string for token when provided", async () => { + await manager.configure("deployment", "https://coder.example.com", ""); + + expect(memfs.readFileSync("/path/base/deployment/url", "utf8")).toBe( + "https://coder.example.com", + ); + expect(memfs.readFileSync("/path/base/deployment/session", "utf8")).toBe( + "", + ); + }); + + it("should use base path directly when label is empty", async () => { + await manager.configure("", "https://coder.example.com", "token"); + + expect(memfs.readFileSync("/path/base/url", "utf8")).toBe( + "https://coder.example.com", + ); + expect(memfs.readFileSync("/path/base/session", "utf8")).toBe("token"); + }); + }); + + describe("Read CLI Configuration", () => { + it("should read and trim stored configuration", async () => { + // Create directories and write files with whitespace + vol.mkdirSync("/path/base/deployment", { recursive: true }); + memfs.writeFileSync( + "/path/base/deployment/url", + " https://coder.example.com \n", + ); + memfs.writeFileSync( + "/path/base/deployment/session", + "\t test-token \r\n", + ); + + const result = await manager.readConfig("deployment"); + + expect(result).toEqual({ + url: "https://coder.example.com", + token: "test-token", + }); + }); + + it("should return empty strings for missing files", async () => { + const result = await manager.readConfig("deployment"); + + expect(result).toEqual({ + url: "", + token: "", + }); + }); + + it("should handle partial configuration", async () => { + vol.mkdirSync("/path/base/deployment", { recursive: true }); + memfs.writeFileSync( + "/path/base/deployment/url", + "https://coder.example.com", + ); + + const result = await manager.readConfig("deployment"); + + expect(result).toEqual({ + url: "https://coder.example.com", + token: "", + }); + }); + }); + + describe("Binary Version Validation", () => { + it("rejects invalid server versions", async () => { + mockApi.getBuildInfo = vi.fn().mockResolvedValue({ version: "invalid" }); + await expect(manager.fetchBinary(mockApi, "test")).rejects.toThrow( + "Got invalid version from deployment", + ); + }); + + it("accepts valid semver versions", async () => { + withExistingBinary(TEST_VERSION); + const result = await manager.fetchBinary(mockApi, "test"); + expect(result).toBe(BINARY_PATH); + }); + }); + + describe("Existing Binary Handling", () => { + beforeEach(() => { + // Disable signature verification for these tests + mockConfig.set("coder.disableSignatureVerification", true); + }); + + it("reuses matching binary without downloading", async () => { + withExistingBinary(TEST_VERSION); + const result = await manager.fetchBinary(mockApi, "test"); + expect(result).toBe(BINARY_PATH); + expect(mockAxios.get).not.toHaveBeenCalled(); + // Verify binary still exists + expect(memfs.existsSync(BINARY_PATH)).toBe(true); + }); + + it("downloads when versions differ", async () => { + withExistingBinary("1.0.0"); + withSuccessfulDownload(); + const result = await manager.fetchBinary(mockApi, "test"); + expect(result).toBe(BINARY_PATH); + expect(mockAxios.get).toHaveBeenCalled(); + // Verify new binary exists + expect(memfs.existsSync(BINARY_PATH)).toBe(true); + expect(memfs.readFileSync(BINARY_PATH).toString()).toBe( + mockBinaryContent(TEST_VERSION), + ); + }); + + it("keeps mismatched binary when downloads disabled", async () => { + mockConfig.set("coder.enableDownloads", false); + withExistingBinary("1.0.0"); + const result = await manager.fetchBinary(mockApi, "test"); + expect(result).toBe(BINARY_PATH); + expect(mockAxios.get).not.toHaveBeenCalled(); + // Should still have the old version + expect(memfs.existsSync(BINARY_PATH)).toBe(true); + expect(memfs.readFileSync(BINARY_PATH).toString()).toBe( + mockBinaryContent("1.0.0"), + ); + }); + + it("downloads fresh binary when corrupted", async () => { + withCorruptedBinary(); + withSuccessfulDownload(); + const result = await manager.fetchBinary(mockApi, "test"); + expect(result).toBe(BINARY_PATH); + expect(mockAxios.get).toHaveBeenCalled(); + expect(memfs.existsSync(BINARY_PATH)).toBe(true); + expect(memfs.readFileSync(BINARY_PATH).toString()).toBe( + mockBinaryContent(TEST_VERSION), + ); + }); + + it("downloads when no binary exists", async () => { + // Ensure directory doesn't exist initially + expect(memfs.existsSync(BINARY_DIR)).toBe(false); + + withSuccessfulDownload(); + const result = await manager.fetchBinary(mockApi, "test"); + expect(result).toBe(BINARY_PATH); + expect(mockAxios.get).toHaveBeenCalled(); + + // Verify directory was created and binary exists + expect(memfs.existsSync(BINARY_DIR)).toBe(true); + expect(memfs.existsSync(BINARY_PATH)).toBe(true); + expect(memfs.readFileSync(BINARY_PATH).toString()).toBe( + mockBinaryContent(TEST_VERSION), + ); + }); + + it("fails when downloads disabled and no binary", async () => { + mockConfig.set("coder.enableDownloads", false); + await expect(manager.fetchBinary(mockApi, "test")).rejects.toThrow( + "Unable to download CLI because downloads are disabled", + ); + expect(memfs.existsSync(BINARY_PATH)).toBe(false); + }); + }); + + describe("Binary Download Behavior", () => { + beforeEach(() => { + mockConfig.set("coder.disableSignatureVerification", true); + }); + + it("downloads with correct headers", async () => { + withSuccessfulDownload(); + await manager.fetchBinary(mockApi, "test"); + expect(mockAxios.get).toHaveBeenCalledWith( + `/bin/${BINARY_NAME}`, + expect.objectContaining({ + responseType: "stream", + headers: expect.objectContaining({ + "Accept-Encoding": "gzip", + "If-None-Match": '""', + }), + }), + ); + }); + + it("uses custom binary source", async () => { + mockConfig.set("coder.binarySource", "/custom/path"); + withSuccessfulDownload(); + await manager.fetchBinary(mockApi, "test"); + expect(mockAxios.get).toHaveBeenCalledWith( + "/custom/path", + expect.objectContaining({ + responseType: "stream", + decompress: true, + validateStatus: expect.any(Function), + }), + ); + }); + + it("uses ETag for existing binaries", async () => { + withExistingBinary("1.0.0"); + withSuccessfulDownload(); + await manager.fetchBinary(mockApi, "test"); + + // Verify ETag was computed from actual file content + expect(mockAxios.get).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: expect.objectContaining({ + "If-None-Match": '"0c95a175da8afefd2b52057908a2e30ba2e959b3"', + }), + }), + ); + }); + + it("cleans up old files before download", async () => { + // Create old temporary files and signature files + vol.mkdirSync(BINARY_DIR, { recursive: true }); + memfs.writeFileSync(path.join(BINARY_DIR, "coder.old-xyz"), "old"); + memfs.writeFileSync(path.join(BINARY_DIR, "coder.temp-abc"), "temp"); + memfs.writeFileSync(path.join(BINARY_DIR, "coder.asc"), "signature"); + memfs.writeFileSync(path.join(BINARY_DIR, "keeper.txt"), "keep"); + + withSuccessfulDownload(); + await manager.fetchBinary(mockApi, "test"); + + // Verify old files were actually removed but other files kept + expect(memfs.existsSync(path.join(BINARY_DIR, "coder.old-xyz"))).toBe( + false, + ); + expect(memfs.existsSync(path.join(BINARY_DIR, "coder.temp-abc"))).toBe( + false, + ); + expect(memfs.existsSync(path.join(BINARY_DIR, "coder.asc"))).toBe(false); + expect(memfs.existsSync(path.join(BINARY_DIR, "keeper.txt"))).toBe(true); + }); + + it("moves existing binary to backup file before writing new version", async () => { + withExistingBinary("1.0.0"); + withSuccessfulDownload(); + + await manager.fetchBinary(mockApi, "test"); + + // Verify the old binary was backed up + const files = readdir(BINARY_DIR); + const backupFile = files.find( + (f) => f.startsWith(BINARY_NAME) && f.match(/\.old-[a-z0-9]+$/), + ); + expect(backupFile).toBeDefined(); + }); + }); + + describe("Download HTTP Response Handling", () => { + beforeEach(() => { + mockConfig.set("coder.disableSignatureVerification", true); + }); + + it("handles 304 Not Modified", async () => { + withExistingBinary("1.0.0"); + withHttpResponse(304); + const result = await manager.fetchBinary(mockApi, "test"); + expect(result).toBe(BINARY_PATH); + // No change + expect(memfs.readFileSync(BINARY_PATH).toString()).toBe( + mockBinaryContent("1.0.0"), + ); + }); + + it("handles 404 platform not supported", async () => { + withHttpResponse(404); + mockUI.setResponse( + "Coder isn't supported for your platform. Please open an issue, we'd love to support it!", + "Open an Issue", + ); + await expect(manager.fetchBinary(mockApi, "test")).rejects.toThrow( + "Platform not supported", + ); + expect(vscode.env.openExternal).toHaveBeenCalledWith( + expect.objectContaining({ + path: expect.stringContaining( + "github.com/coder/vscode-coder/issues/new?", + ), + }), + ); + }); + + it("handles server errors", async () => { + withHttpResponse(500); + mockUI.setResponse( + "Failed to download binary. Please open an issue.", + "Open an Issue", + ); + await expect(manager.fetchBinary(mockApi, "test")).rejects.toThrow( + "Failed to download binary", + ); + expect(vscode.env.openExternal).toHaveBeenCalledWith( + expect.objectContaining({ + path: expect.stringContaining( + "github.com/coder/vscode-coder/issues/new?", + ), + }), + ); + }); + }); + + describe("Download Stream Handling", () => { + beforeEach(() => { + mockConfig.set("coder.disableSignatureVerification", true); + }); + + it("handles write stream errors", async () => { + withStreamError("write", "disk full"); + await expect(manager.fetchBinary(mockApi, "test")).rejects.toThrow( + "Unable to download binary: disk full", + ); + expect(memfs.existsSync(BINARY_PATH)).toBe(false); + }); + + it("handles read stream errors", async () => { + withStreamError("read", "network timeout"); + await expect(manager.fetchBinary(mockApi, "test")).rejects.toThrow( + "Unable to download binary: network timeout", + ); + expect(memfs.existsSync(BINARY_PATH)).toBe(false); + }); + + it("handles missing content-length", async () => { + withSuccessfulDownload({ headers: {} }); + const result = await manager.fetchBinary(mockApi, "test"); + expect(result).toBe(BINARY_PATH); + expect(memfs.existsSync(BINARY_PATH)).toBe(true); + }); + }); + + describe("Download Progress Tracking", () => { + beforeEach(() => { + mockConfig.set("coder.disableSignatureVerification", true); + }); + + it("shows download progress", async () => { + withSuccessfulDownload(); + await manager.fetchBinary(mockApi, "test"); + expect(vscode.window.withProgress).toHaveBeenCalledWith( + expect.objectContaining({ title: `Downloading ${TEST_URL}` }), + expect.any(Function), + ); + }); + + it("handles user cancellation", async () => { + mockProgress.setCancellation(true); + withSuccessfulDownload(); + await expect(manager.fetchBinary(mockApi, "test")).rejects.toThrow( + "Download aborted", + ); + expect(memfs.existsSync(BINARY_PATH)).toBe(false); + }); + }); + + describe("Binary Signature Verification", () => { + it("verifies valid signatures", async () => { + withSuccessfulDownload(); + withSignatureResponses([200]); + const result = await manager.fetchBinary(mockApi, "test"); + expect(result).toBe(BINARY_PATH); + expect(pgp.verifySignature).toHaveBeenCalled(); + const sigFile = expectFileInDir(BINARY_DIR, ".asc"); + expect(sigFile).toBeDefined(); + }); + + it("tries fallback signature on 404", async () => { + withSuccessfulDownload(); + withSignatureResponses([404, 200]); + mockUI.setResponse("Signature not found", "Download signature"); + const result = await manager.fetchBinary(mockApi, "test"); + expect(result).toBe(BINARY_PATH); + expect(mockAxios.get).toHaveBeenCalledTimes(3); + const sigFile = expectFileInDir(BINARY_DIR, ".asc"); + expect(sigFile).toBeDefined(); + }); + + it("allows running despite invalid signature", async () => { + withSuccessfulDownload(); + withSignatureResponses([200]); + vi.mocked(pgp.verifySignature).mockRejectedValueOnce( + createVerificationError("Invalid signature"), + ); + mockUI.setResponse("Signature does not match", "Run anyway"); + const result = await manager.fetchBinary(mockApi, "test"); + expect(result).toBe(BINARY_PATH); + expect(memfs.existsSync(BINARY_PATH)).toBe(true); + }); + + it("aborts on signature rejection", async () => { + withSuccessfulDownload(); + withSignatureResponses([200]); + vi.mocked(pgp.verifySignature).mockRejectedValueOnce( + createVerificationError("Invalid signature"), + ); + mockUI.setResponse("Signature does not match", undefined); + await expect(manager.fetchBinary(mockApi, "test")).rejects.toThrow( + "Signature verification aborted", + ); + }); + + it("skips verification when disabled", async () => { + mockConfig.set("coder.disableSignatureVerification", true); + withSuccessfulDownload(); + const result = await manager.fetchBinary(mockApi, "test"); + expect(result).toBe(BINARY_PATH); + expect(pgp.verifySignature).not.toHaveBeenCalled(); + const files = readdir(BINARY_DIR); + expect(files.find((file) => file.includes(".asc"))).toBeUndefined(); + }); + + it.each([ + [404, "Signature not found"], + [500, "Failed to download signature"], + ])("allows skipping verification on %i", async (status, message) => { + withSuccessfulDownload(); + withHttpResponse(status); + mockUI.setResponse(message, "Run without verification"); + const result = await manager.fetchBinary(mockApi, "test"); + expect(result).toBe(BINARY_PATH); + expect(pgp.verifySignature).not.toHaveBeenCalled(); + }); + + it.each([ + [404, "Signature not found"], + [500, "Failed to download signature"], + ])( + "aborts when user declines missing signature on %i", + async (status, message) => { + withSuccessfulDownload(); + withHttpResponse(status); + mockUI.setResponse(message, undefined); // User cancels + await expect(manager.fetchBinary(mockApi, "test")).rejects.toThrow( + "Signature download aborted", + ); + }, + ); + }); + + describe("File System Operations", () => { + beforeEach(() => { + mockConfig.set("coder.disableSignatureVerification", true); + }); + + it("creates binary directory", async () => { + expect(memfs.existsSync(BINARY_DIR)).toBe(false); + withSuccessfulDownload(); + await manager.fetchBinary(mockApi, "test"); + expect(memfs.existsSync(BINARY_DIR)).toBe(true); + const stats = memfs.statSync(BINARY_DIR); + expect(stats.isDirectory()).toBe(true); + }); + + it("validates downloaded binary version", async () => { + withSuccessfulDownload(); + await manager.fetchBinary(mockApi, "test"); + expect(memfs.readFileSync(BINARY_PATH).toString()).toBe( + mockBinaryContent(TEST_VERSION), + ); + }); + + it("sets correct file permissions", async () => { + withSuccessfulDownload(); + await manager.fetchBinary(mockApi, "test"); + const stats = memfs.statSync(BINARY_PATH); + expect(stats.mode & 0o777).toBe(0o755); + }); + }); + + describe("Path Pecularities", () => { + beforeEach(() => { + mockConfig.set("coder.disableSignatureVerification", true); + }); + + it("handles binary with spaces in path", async () => { + const pathWithSpaces = "/path with spaces/bin"; + const resolver = new PathResolver(pathWithSpaces, "/log"); + const manager = new CliManager(vscode, createMockLogger(), resolver); + + withSuccessfulDownload(); + const result = await manager.fetchBinary(mockApi, "test label"); + expect(result).toBe(`${pathWithSpaces}/test label/bin/${BINARY_NAME}`); + }); + + it("handles empty deployment label", async () => { + withExistingBinary(TEST_VERSION, "/path/base/bin"); + const result = await manager.fetchBinary(mockApi, ""); + expect(result).toBe(path.join(BASE_PATH, "bin", BINARY_NAME)); + }); + }); + + function createMockLogger(): Logger { + return { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + } + + function createMockApi(version: string, url: string): Api { + const axios = { + defaults: { baseURL: url }, + get: vi.fn(), + } as unknown as AxiosInstance; + return { + getBuildInfo: vi.fn().mockResolvedValue({ version }), + getAxiosInstance: () => axios, + } as unknown as Api; + } + + function withExistingBinary(version: string, dir: string = BINARY_DIR) { + vol.mkdirSync(dir, { recursive: true }); + memfs.writeFileSync(`${dir}/${BINARY_NAME}`, mockBinaryContent(version), { + mode: 0o755, + }); + + // Mock version to return the specified version + vi.mocked(cli.version).mockResolvedValueOnce(version); + } + + function withCorruptedBinary() { + vol.mkdirSync(BINARY_DIR, { recursive: true }); + memfs.writeFileSync(BINARY_PATH, "corrupted-binary-content", { + mode: 0o755, + }); + + // Mock version to fail + vi.mocked(cli.version).mockRejectedValueOnce(new Error("corrupted")); + } + + function withSuccessfulDownload(opts?: { + headers?: Record; + }) { + const stream = createMockStream(mockBinaryContent(TEST_VERSION)); + withHttpResponse( + 200, + opts?.headers ?? { "content-length": "1024" }, + stream, + ); + + // Mock version to return TEST_VERSION after download + vi.mocked(cli.version).mockResolvedValue(TEST_VERSION); + } + + function withSignatureResponses(statuses: number[]): void { + statuses.forEach((status) => { + const data = + status === 200 ? createMockStream("mock-signature-content") : undefined; + withHttpResponse(status, {}, data); + }); + } + + function withHttpResponse( + status: number, + headers: Record = {}, + data?: unknown, + ) { + vi.mocked(mockAxios.get).mockResolvedValueOnce({ + status, + headers, + data, + }); + } + + function withStreamError(type: "read" | "write", message: string) { + if (type === "write") { + vi.spyOn(fs, "createWriteStream").mockImplementation(() => { + const stream = new EventEmitter(); + (stream as unknown as fs.WriteStream).write = vi.fn(); + (stream as unknown as fs.WriteStream).close = vi.fn(); + // Emit error on next tick after stream is returned + setImmediate(() => { + stream.emit("error", new Error(message)); + }); + + return stream as ReturnType; + }); + + // Provide a normal read stream + withHttpResponse( + 200, + { "content-length": "256" }, + createMockStream("data"), + ); + } else { + // Create a read stream that emits error + const errorStream = { + on: vi.fn((event: string, callback: (...args: unknown[]) => void) => { + if (event === "error") { + setImmediate(() => callback(new Error(message))); + } + return errorStream; + }), + destroy: vi.fn(), + } as unknown as IncomingMessage; + + withHttpResponse(200, { "content-length": "1024" }, errorStream); + } + } + + function createMockStream( + content: string, + options: { chunkSize?: number; delay?: number } = {}, + ): IncomingMessage { + const { chunkSize = 8, delay = 0 } = options; + + const buffer = Buffer.from(content); + let position = 0; + + return { + on: vi.fn((event: string, callback: (...args: unknown[]) => void) => { + if (event === "data") { + // Send data in chunks + const sendChunk = () => { + if (position < buffer.length) { + const chunk = buffer.subarray( + position, + Math.min(position + chunkSize, buffer.length), + ); + position += chunkSize; + callback(chunk); + if (position < buffer.length) { + setTimeout(sendChunk, delay); + } + } + }; + setTimeout(sendChunk, delay); + } else if (event === "close") { + // Just close after a delay + setTimeout(() => callback(), 10); + } + }), + destroy: vi.fn(), + } as unknown as IncomingMessage; + } + + function createVerificationError(msg: string): pgp.VerificationError { + const error = new pgp.VerificationError( + pgp.VerificationErrorCode.Invalid, + msg, + ); + vi.mocked(error.summary).mockReturnValue("Signature does not match"); + return error; + } + + function mockBinaryContent(version: string): string { + return `mock-binary-v${version}`; + } + + function expectFileInDir(dir: string, pattern: string): string | undefined { + const files = readdir(dir); + return files.find((f) => f.includes(pattern)); + } + + function readdir(dir: string): string[] { + return memfs.readdirSync(dir) as string[]; + } +}); diff --git a/src/storage.ts b/src/core/cliManager.ts similarity index 68% rename from src/storage.ts rename to src/core/cliManager.ts index 97d62ff7..e8a7ab25 100644 --- a/src/storage.ts +++ b/src/core/cliManager.ts @@ -3,141 +3,27 @@ import globalAxios, { type AxiosRequestConfig, } from "axios"; import { Api } from "coder/site/src/api/api"; -import { createWriteStream, type WriteStream } from "fs"; +import { createWriteStream, WriteStream } from "fs"; import fs from "fs/promises"; import { IncomingMessage } from "http"; import path from "path"; import prettyBytes from "pretty-bytes"; import * as semver from "semver"; -import * as vscode from "vscode"; -import { errToStr } from "./api/api-helper"; -import * as cli from "./cliManager"; -import { getHeaderCommand, getHeaders } from "./headers"; -import * as pgp from "./pgp"; -// Maximium number of recent URLs to store. -const MAX_URLS = 10; +import * as vscode from "vscode"; +import { errToStr } from "../api/api-helper"; +import * as cli from "../cliUtils"; +import { Logger } from "../logging/logger"; +import * as pgp from "../pgp"; +import { PathResolver } from "./pathResolver"; -export class Storage { +export class CliManager { constructor( private readonly vscodeProposed: typeof vscode, - public readonly output: vscode.LogOutputChannel, - private readonly memento: vscode.Memento, - private readonly secrets: vscode.SecretStorage, - private readonly globalStorageUri: vscode.Uri, - private readonly logUri: vscode.Uri, + private readonly output: Logger, + private readonly pathResolver: PathResolver, ) {} - /** - * Add the URL to the list of recently accessed URLs in global storage, then - * set it as the last used URL. - * - * If the URL is falsey, then remove it as the last used URL and do not touch - * the history. - */ - public async setUrl(url?: string): Promise { - await this.memento.update("url", url); - if (url) { - const history = this.withUrlHistory(url); - await this.memento.update("urlHistory", history); - } - } - - /** - * Get the last used URL. - */ - public getUrl(): string | undefined { - return this.memento.get("url"); - } - - /** - * Get the most recently accessed URLs (oldest to newest) with the provided - * values appended. Duplicates will be removed. - */ - public withUrlHistory(...append: (string | undefined)[]): string[] { - const val = this.memento.get("urlHistory"); - const urls = Array.isArray(val) ? new Set(val) : new Set(); - for (const url of append) { - if (url) { - // It might exist; delete first so it gets appended. - urls.delete(url); - urls.add(url); - } - } - // Slice off the head if the list is too large. - return urls.size > MAX_URLS - ? Array.from(urls).slice(urls.size - MAX_URLS, urls.size) - : Array.from(urls); - } - - /** - * Mark this as the first connection to a workspace, which influences whether - * the workspace startup confirmation is shown to the user. - */ - public async setFirstConnect(): Promise { - return this.memento.update("firstConnect", true); - } - - /** - * Check if this is the first connection to a workspace and clear the flag. - * Used to determine whether to automatically start workspaces without - * prompting the user for confirmation. - */ - public async getAndClearFirstConnect(): Promise { - const isFirst = this.memento.get("firstConnect"); - if (isFirst !== undefined) { - await this.memento.update("firstConnect", undefined); - } - return isFirst === true; - } - - /** - * Set or unset the last used token. - */ - public async setSessionToken(sessionToken?: string): Promise { - if (!sessionToken) { - await this.secrets.delete("sessionToken"); - } else { - await this.secrets.store("sessionToken", sessionToken); - } - } - - /** - * Get the last used token. - */ - public async getSessionToken(): Promise { - try { - return await this.secrets.get("sessionToken"); - } catch (ex) { - // The VS Code session store has become corrupt before, and - // will fail to get the session token... - return undefined; - } - } - - /** - * Returns the log path for the "Remote - SSH" output panel. There is no VS - * Code API to get the contents of an output panel. We use this to get the - * active port so we can display network information. - */ - public async getRemoteSSHLogPath(): Promise { - const upperDir = path.dirname(this.logUri.fsPath); - // Node returns these directories sorted already! - const dirs = await fs.readdir(upperDir); - const latestOutput = dirs - .reverse() - .filter((dir) => dir.startsWith("output_logging_")); - if (latestOutput.length === 0) { - return undefined; - } - const dir = await fs.readdir(path.join(upperDir, latestOutput[0])); - const remoteSSH = dir.filter((file) => file.indexOf("Remote - SSH") !== -1); - if (remoteSSH.length === 0) { - return undefined; - } - return path.join(upperDir, latestOutput[0], remoteSSH[0]); - } - /** * Download and return the path to a working binary for the deployment with * the provided label using the provided client. If the label is empty, use @@ -151,7 +37,6 @@ export class Storage { */ public async fetchBinary(restClient: Api, label: string): Promise { const cfg = vscode.workspace.getConfiguration("coder"); - // Settings can be undefined when set to their defaults (true in this case), // so explicitly check against false. const enableDownloads = cfg.get("enableDownloads") !== false; @@ -171,7 +56,10 @@ export class Storage { // Check if there is an existing binary and whether it looks valid. If it // is valid and matches the server, or if it does not match the server but // downloads are disabled, we can return early. - const binPath = path.join(this.getBinaryCachePath(label), cli.name()); + const binPath = path.join( + this.pathResolver.getBinaryCachePath(label), + cli.name(), + ); this.output.info("Using binary path", binPath); const stat = await cli.stat(binPath); if (stat === undefined) { @@ -318,8 +206,7 @@ export class Storage { body: `I'd like to use the \`${os}-${arch}\` architecture with the VS Code extension.`, }); const uri = vscode.Uri.parse( - `https://github.com/coder/vscode-coder/issues/new?` + - params.toString(), + `https://github.com/coder/vscode-coder/issues/new?${params.toString()}`, ); vscode.env.openExternal(uri); }); @@ -340,8 +227,7 @@ export class Storage { body: `Received status code \`${status}\` when downloading the binary.`, }); const uri = vscode.Uri.parse( - `https://github.com/coder/vscode-coder/issues/new?` + - params.toString(), + `https://github.com/coder/vscode-coder/issues/new?${params.toString()}`, ); vscode.env.openExternal(uri); }); @@ -585,109 +471,13 @@ export class Storage { return status; } - /** - * Return the directory for a deployment with the provided label to where its - * binary is cached. - * - * If the label is empty, read the old deployment-unaware config instead. - * - * The caller must ensure this directory exists before use. - */ - public getBinaryCachePath(label: string): string { - const configPath = vscode.workspace - .getConfiguration() - .get("coder.binaryDestination"); - return configPath && String(configPath).trim().length > 0 - ? path.resolve(String(configPath)) - : label - ? path.join(this.globalStorageUri.fsPath, label, "bin") - : path.join(this.globalStorageUri.fsPath, "bin"); - } - - /** - * Return the path where network information for SSH hosts are stored. - * - * The CLI will write files here named after the process PID. - */ - public getNetworkInfoPath(): string { - return path.join(this.globalStorageUri.fsPath, "net"); - } - - /** - * - * Return the path where log data from the connection is stored. - * - * The CLI will write files here named after the process PID. - */ - public getLogPath(): string { - return path.join(this.globalStorageUri.fsPath, "log"); - } - - /** - * Get the path to the user's settings.json file. - * - * Going through VSCode's API should be preferred when modifying settings. - */ - public getUserSettingsPath(): string { - return path.join( - this.globalStorageUri.fsPath, - "..", - "..", - "..", - "User", - "settings.json", - ); - } - - /** - * Return the directory for the deployment with the provided label to where - * its session token is stored. - * - * If the label is empty, read the old deployment-unaware config instead. - * - * The caller must ensure this directory exists before use. - */ - public getSessionTokenPath(label: string): string { - return label - ? path.join(this.globalStorageUri.fsPath, label, "session") - : path.join(this.globalStorageUri.fsPath, "session"); - } - - /** - * Return the directory for the deployment with the provided label to where - * its session token was stored by older code. - * - * If the label is empty, read the old deployment-unaware config instead. - * - * The caller must ensure this directory exists before use. - */ - public getLegacySessionTokenPath(label: string): string { - return label - ? path.join(this.globalStorageUri.fsPath, label, "session_token") - : path.join(this.globalStorageUri.fsPath, "session_token"); - } - - /** - * Return the directory for the deployment with the provided label to where - * its url is stored. - * - * If the label is empty, read the old deployment-unaware config instead. - * - * The caller must ensure this directory exists before use. - */ - public getUrlPath(label: string): string { - return label - ? path.join(this.globalStorageUri.fsPath, label, "url") - : path.join(this.globalStorageUri.fsPath, "url"); - } - /** * Configure the CLI for the deployment with the provided label. * * Falsey URLs and null tokens are a no-op; we avoid unconfiguring the CLI to * avoid breaking existing connections. */ - public async configureCli( + public async configure( label: string, url: string | undefined, token: string | null, @@ -709,7 +499,7 @@ export class Storage { url: string | undefined, ): Promise { if (url) { - const urlPath = this.getUrlPath(label); + const urlPath = this.pathResolver.getUrlPath(label); await fs.mkdir(path.dirname(urlPath), { recursive: true }); await fs.writeFile(urlPath, url); } @@ -727,7 +517,7 @@ export class Storage { token: string | undefined | null, ) { if (token !== null) { - const tokenPath = this.getSessionTokenPath(label); + const tokenPath = this.pathResolver.getSessionTokenPath(label); await fs.mkdir(path.dirname(tokenPath), { recursive: true }); await fs.writeFile(tokenPath, token ?? ""); } @@ -740,11 +530,11 @@ export class Storage { * * If the label is empty, read the old deployment-unaware config. */ - public async readCliConfig( + public async readConfig( label: string, ): Promise<{ url: string; token: string }> { - const urlPath = this.getUrlPath(label); - const tokenPath = this.getSessionTokenPath(label); + const urlPath = this.pathResolver.getUrlPath(label); + const tokenPath = this.pathResolver.getSessionTokenPath(label); const [url, token] = await Promise.allSettled([ fs.readFile(urlPath, "utf8"), fs.readFile(tokenPath, "utf8"), @@ -754,33 +544,4 @@ export class Storage { token: token.status === "fulfilled" ? token.value.trim() : "", }; } - - /** - * Migrate the session token file from "session_token" to "session", if needed. - */ - public async migrateSessionToken(label: string) { - const oldTokenPath = this.getLegacySessionTokenPath(label); - const newTokenPath = this.getSessionTokenPath(label); - try { - await fs.rename(oldTokenPath, newTokenPath); - } catch (error) { - if ((error as NodeJS.ErrnoException)?.code === "ENOENT") { - return; - } - throw error; - } - } - - /** - * Run the header command and return the generated headers. - */ - public async getHeaders( - url: string | undefined, - ): Promise> { - return getHeaders( - url, - getHeaderCommand(vscode.workspace.getConfiguration()), - this.output, - ); - } } diff --git a/src/core/mementoManager.test.ts b/src/core/mementoManager.test.ts new file mode 100644 index 00000000..f1cd6a2d --- /dev/null +++ b/src/core/mementoManager.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { InMemoryMemento } from "../__mocks__/testHelpers"; +import { MementoManager } from "./mementoManager"; + +describe("MementoManager", () => { + let memento: InMemoryMemento; + let mementoManager: MementoManager; + + beforeEach(() => { + memento = new InMemoryMemento(); + mementoManager = new MementoManager(memento); + }); + + describe("setUrl", () => { + it("should store URL and add to history", async () => { + await mementoManager.setUrl("https://coder.example.com"); + + expect(mementoManager.getUrl()).toBe("https://coder.example.com"); + expect(memento.get("urlHistory")).toEqual(["https://coder.example.com"]); + }); + + it("should not update history for falsy values", async () => { + await mementoManager.setUrl(undefined); + expect(mementoManager.getUrl()).toBeUndefined(); + expect(memento.get("urlHistory")).toBeUndefined(); + + await mementoManager.setUrl(""); + expect(mementoManager.getUrl()).toBe(""); + expect(memento.get("urlHistory")).toBeUndefined(); + }); + + it("should deduplicate URLs in history", async () => { + await mementoManager.setUrl("url1"); + await mementoManager.setUrl("url2"); + await mementoManager.setUrl("url1"); // Re-add first URL + + expect(memento.get("urlHistory")).toEqual(["url2", "url1"]); + }); + }); + + describe("withUrlHistory", () => { + it("should append URLs and remove duplicates", async () => { + await memento.update("urlHistory", ["existing1", "existing2"]); + + const result = mementoManager.withUrlHistory("existing2", "new1"); + + expect(result).toEqual(["existing1", "existing2", "new1"]); + }); + + it("should limit to 10 URLs", async () => { + const urls = Array.from({ length: 10 }, (_, i) => `url${i}`); + await memento.update("urlHistory", urls); + + const result = mementoManager.withUrlHistory("url20"); + + expect(result).toHaveLength(10); + expect(result[0]).toBe("url1"); + expect(result[9]).toBe("url20"); + }); + + it("should handle non-array storage gracefully", async () => { + await memento.update("urlHistory", "not-an-array"); + const result = mementoManager.withUrlHistory("url1"); + expect(result).toEqual(["url1"]); + }); + }); + + describe("firstConnect", () => { + it("should return true only once", async () => { + await mementoManager.setFirstConnect(); + + expect(await mementoManager.getAndClearFirstConnect()).toBe(true); + expect(await mementoManager.getAndClearFirstConnect()).toBe(false); + }); + + it("should return false for non-boolean values", async () => { + await memento.update("firstConnect", "truthy-string"); + expect(await mementoManager.getAndClearFirstConnect()).toBe(false); + }); + }); +}); diff --git a/src/core/mementoManager.ts b/src/core/mementoManager.ts new file mode 100644 index 00000000..f79be46c --- /dev/null +++ b/src/core/mementoManager.ts @@ -0,0 +1,71 @@ +import type { Memento } from "vscode"; + +// Maximum number of recent URLs to store. +const MAX_URLS = 10; + +export class MementoManager { + constructor(private readonly memento: Memento) {} + + /** + * Add the URL to the list of recently accessed URLs in global storage, then + * set it as the last used URL. + * + * If the URL is falsey, then remove it as the last used URL and do not touch + * the history. + */ + public async setUrl(url?: string): Promise { + await this.memento.update("url", url); + if (url) { + const history = this.withUrlHistory(url); + await this.memento.update("urlHistory", history); + } + } + + /** + * Get the last used URL. + */ + public getUrl(): string | undefined { + return this.memento.get("url"); + } + + /** + * Get the most recently accessed URLs (oldest to newest) with the provided + * values appended. Duplicates will be removed. + */ + public withUrlHistory(...append: (string | undefined)[]): string[] { + const val = this.memento.get("urlHistory"); + const urls = Array.isArray(val) ? new Set(val) : new Set(); + for (const url of append) { + if (url) { + // It might exist; delete first so it gets appended. + urls.delete(url); + urls.add(url); + } + } + // Slice off the head if the list is too large. + return urls.size > MAX_URLS + ? Array.from(urls).slice(urls.size - MAX_URLS, urls.size) + : Array.from(urls); + } + + /** + * Mark this as the first connection to a workspace, which influences whether + * the workspace startup confirmation is shown to the user. + */ + public async setFirstConnect(): Promise { + return this.memento.update("firstConnect", true); + } + + /** + * Check if this is the first connection to a workspace and clear the flag. + * Used to determine whether to automatically start workspaces without + * prompting the user for confirmation. + */ + public async getAndClearFirstConnect(): Promise { + const isFirst = this.memento.get("firstConnect"); + if (isFirst !== undefined) { + await this.memento.update("firstConnect", undefined); + } + return isFirst === true; + } +} diff --git a/src/core/pathResolver.test.ts b/src/core/pathResolver.test.ts new file mode 100644 index 00000000..8216a547 --- /dev/null +++ b/src/core/pathResolver.test.ts @@ -0,0 +1,48 @@ +import * as path from "path"; +import { describe, it, expect, beforeEach } from "vitest"; +import { MockConfigurationProvider } from "../__mocks__/testHelpers"; +import { PathResolver } from "./pathResolver"; + +describe("PathResolver", () => { + const basePath = + "/home/user/.vscode-server/data/User/globalStorage/coder.coder-remote"; + const codeLogPath = "/home/user/.vscode-server/data/logs/coder.coder-remote"; + let pathResolver: PathResolver; + let mockConfig: MockConfigurationProvider; + + beforeEach(() => { + pathResolver = new PathResolver(basePath, codeLogPath); + mockConfig = new MockConfigurationProvider(); + }); + + it("should use base path for empty labels", () => { + expect(pathResolver.getGlobalConfigDir("")).toBe(basePath); + expect(pathResolver.getSessionTokenPath("")).toBe( + path.join(basePath, "session"), + ); + expect(pathResolver.getUrlPath("")).toBe(path.join(basePath, "url")); + }); + + describe("getBinaryCachePath", () => { + it("should use custom binary destination when configured", () => { + mockConfig.set("coder.binaryDestination", "/custom/binary/path"); + expect(pathResolver.getBinaryCachePath("deployment")).toBe( + "/custom/binary/path", + ); + }); + + it("should use default path when custom destination is empty or whitespace", () => { + mockConfig.set("coder.binaryDestination", " "); + expect(pathResolver.getBinaryCachePath("deployment")).toBe( + path.join(basePath, "deployment", "bin"), + ); + }); + + it("should normalize custom paths", () => { + mockConfig.set("coder.binaryDestination", "/custom/../binary/./path"); + expect(pathResolver.getBinaryCachePath("deployment")).toBe( + path.normalize("/custom/../binary/./path"), + ); + }); + }); +}); diff --git a/src/core/pathResolver.ts b/src/core/pathResolver.ts new file mode 100644 index 00000000..6c1ee7ef --- /dev/null +++ b/src/core/pathResolver.ts @@ -0,0 +1,115 @@ +import * as path from "path"; +import * as vscode from "vscode"; + +export class PathResolver { + constructor( + private readonly basePath: string, + private readonly codeLogPath: string, + ) {} + + /** + * Return the directory for the deployment with the provided label to where + * the global Coder configs are stored. + * + * If the label is empty, read the old deployment-unaware config instead. + * + * The caller must ensure this directory exists before use. + */ + public getGlobalConfigDir(label: string): string { + return label ? path.join(this.basePath, label) : this.basePath; + } + + /** + * Return the directory for a deployment with the provided label to where its + * binary is cached. + * + * If the label is empty, read the old deployment-unaware config instead. + * + * The caller must ensure this directory exists before use. + */ + public getBinaryCachePath(label: string): string { + const configPath = vscode.workspace + .getConfiguration() + .get("coder.binaryDestination"); + return configPath && configPath.trim().length > 0 + ? path.normalize(configPath) + : path.join(this.getGlobalConfigDir(label), "bin"); + } + + /** + * Return the path where network information for SSH hosts are stored. + * + * The CLI will write files here named after the process PID. + */ + public getNetworkInfoPath(): string { + return path.join(this.basePath, "net"); + } + + /** + * Return the path where log data from the connection is stored. + * + * The CLI will write files here named after the process PID. + * + * Note: This directory is not currently used. + */ + public getLogPath(): string { + return path.join(this.basePath, "log"); + } + + /** + * Get the path to the user's settings.json file. + * + * Going through VSCode's API should be preferred when modifying settings. + */ + public getUserSettingsPath(): string { + return path.join(this.basePath, "..", "..", "..", "User", "settings.json"); + } + + /** + * Return the directory for the deployment with the provided label to where + * its session token is stored. + * + * If the label is empty, read the old deployment-unaware config instead. + * + * The caller must ensure this directory exists before use. + */ + public getSessionTokenPath(label: string): string { + return path.join(this.getGlobalConfigDir(label), "session"); + } + + /** + * Return the directory for the deployment with the provided label to where + * its session token was stored by older code. + * + * If the label is empty, read the old deployment-unaware config instead. + * + * The caller must ensure this directory exists before use. + */ + public getLegacySessionTokenPath(label: string): string { + return path.join(this.getGlobalConfigDir(label), "session_token"); + } + + /** + * Return the directory for the deployment with the provided label to where + * its url is stored. + * + * If the label is empty, read the old deployment-unaware config instead. + * + * The caller must ensure this directory exists before use. + */ + public getUrlPath(label: string): string { + return path.join(this.getGlobalConfigDir(label), "url"); + } + + /** + * The URI of a directory in which the extension can create log files. + * + * The directory might not exist on disk and creation is up to the extension. + * However, the parent directory is guaranteed to be existent. + * + * This directory is provided by VS Code and may not be the same as the directory where the Coder CLI writes its log files. + */ + public getCodeLogDir(): string { + return this.codeLogPath; + } +} diff --git a/src/core/secretsManager.test.ts b/src/core/secretsManager.test.ts new file mode 100644 index 00000000..a6487e0f --- /dev/null +++ b/src/core/secretsManager.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { InMemorySecretStorage } from "../__mocks__/testHelpers"; +import { SecretsManager } from "./secretsManager"; + +describe("SecretsManager", () => { + let secretStorage: InMemorySecretStorage; + let secretsManager: SecretsManager; + + beforeEach(() => { + secretStorage = new InMemorySecretStorage(); + secretsManager = new SecretsManager(secretStorage); + }); + + describe("setSessionToken", () => { + it("should store and retrieve tokens", async () => { + await secretsManager.setSessionToken("test-token"); + expect(await secretsManager.getSessionToken()).toBe("test-token"); + + await secretsManager.setSessionToken("new-token"); + expect(await secretsManager.getSessionToken()).toBe("new-token"); + }); + + it("should delete token when empty or undefined", async () => { + await secretsManager.setSessionToken("test-token"); + await secretsManager.setSessionToken(""); + expect(await secretsManager.getSessionToken()).toBeUndefined(); + + await secretsManager.setSessionToken("test-token"); + await secretsManager.setSessionToken(undefined); + expect(await secretsManager.getSessionToken()).toBeUndefined(); + }); + }); + + describe("getSessionToken", () => { + it("should return undefined for corrupted storage", async () => { + await secretStorage.store("sessionToken", "valid-token"); + secretStorage.corruptStorage(); + + expect(await secretsManager.getSessionToken()).toBeUndefined(); + }); + }); +}); diff --git a/src/core/secretsManager.ts b/src/core/secretsManager.ts new file mode 100644 index 00000000..7fd98f8f --- /dev/null +++ b/src/core/secretsManager.ts @@ -0,0 +1,29 @@ +import type { SecretStorage } from "vscode"; + +export class SecretsManager { + constructor(private readonly secrets: SecretStorage) {} + + /** + * Set or unset the last used token. + */ + public async setSessionToken(sessionToken?: string): Promise { + if (!sessionToken) { + await this.secrets.delete("sessionToken"); + } else { + await this.secrets.store("sessionToken", sessionToken); + } + } + + /** + * Get the last used token. + */ + public async getSessionToken(): Promise { + try { + return await this.secrets.get("sessionToken"); + } catch (ex) { + // The VS Code session store has become corrupt before, and + // will fail to get the session token... + return undefined; + } + } +} diff --git a/src/error.test.ts b/src/error.test.ts index 2d591d89..84c1e14b 100644 --- a/src/error.test.ts +++ b/src/error.test.ts @@ -2,260 +2,274 @@ import axios from "axios"; import * as fs from "fs/promises"; import https from "https"; import * as path from "path"; -import { afterAll, beforeAll, it, expect, vi } from "vitest"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { CertificateError, X509_ERR, X509_ERR_CODE } from "./error"; import { Logger } from "./logging/logger"; -// Before each test we make a request to sanity check that we really get the -// error we are expecting, then we run it through CertificateError. +describe("Certificate errors", () => { + // Before each test we make a request to sanity check that we really get the + // error we are expecting, then we run it through CertificateError. -// TODO: These sanity checks need to be ran in an Electron environment to -// reflect real usage in VS Code. We should either revert back to the standard -// extension testing framework which I believe runs in a headless VS Code -// instead of using vitest or at least run the tests through Electron running as -// Node (for now I do this manually by shimming Node). -const isElectron = - process.versions.electron || process.env.ELECTRON_RUN_AS_NODE; + // TODO: These sanity checks need to be ran in an Electron environment to + // reflect real usage in VS Code. We should either revert back to the standard + // extension testing framework which I believe runs in a headless VS Code + // instead of using vitest or at least run the tests through Electron running as + // Node (for now I do this manually by shimming Node). + const isElectron = + (process.versions.electron || process.env.ELECTRON_RUN_AS_NODE) && + !process.env.VSCODE_PID; // Running from the test explorer in VS Code -// TODO: Remove the vscode mock once we revert the testing framework. -beforeAll(() => { - vi.mock("vscode", () => { - return {}; + beforeAll(() => { + vi.mock("vscode", () => { + return {}; + }); }); -}); -const throwingLog = (message: string) => { - throw new Error(message); -}; + const throwingLog = (message: string) => { + throw new Error(message); + }; -const logger: Logger = { - trace: throwingLog, - debug: throwingLog, - info: throwingLog, - warn: throwingLog, - error: throwingLog, -}; + const logger: Logger = { + trace: throwingLog, + debug: throwingLog, + info: throwingLog, + warn: throwingLog, + error: throwingLog, + }; -const disposers: (() => void)[] = []; -afterAll(() => { - disposers.forEach((d) => d()); -}); + const disposers: (() => void)[] = []; + afterAll(() => { + disposers.forEach((d) => d()); + }); -async function startServer(certName: string): Promise { - const server = https.createServer( - { - key: await fs.readFile( - path.join(__dirname, `../fixtures/tls/${certName}.key`), - ), - cert: await fs.readFile( - path.join(__dirname, `../fixtures/tls/${certName}.crt`), - ), - }, - (req, res) => { - if (req.url?.endsWith("/error")) { - res.writeHead(500); - res.end("error"); - return; - } - res.writeHead(200); - res.end("foobar"); - }, - ); - disposers.push(() => server.close()); - return new Promise((resolve, reject) => { - server.on("error", reject); - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - if (!address) { - throw new Error("Server has no address"); - } - if (typeof address !== "string") { - const host = - address.family === "IPv6" ? `[${address.address}]` : address.address; - return resolve(`https://${host}:${address.port}`); - } - resolve(address); + async function startServer(certName: string): Promise { + const server = https.createServer( + { + key: await fs.readFile( + path.join(__dirname, `../fixtures/tls/${certName}.key`), + ), + cert: await fs.readFile( + path.join(__dirname, `../fixtures/tls/${certName}.crt`), + ), + }, + (req, res) => { + if (req.url?.endsWith("/error")) { + res.writeHead(500); + res.end("error"); + return; + } + res.writeHead(200); + res.end("foobar"); + }, + ); + disposers.push(() => server.close()); + return new Promise((resolve, reject) => { + server.on("error", reject); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (!address) { + throw new Error("Server has no address"); + } + if (typeof address !== "string") { + const host = + address.family === "IPv6" + ? `[${address.address}]` + : address.address; + return resolve(`https://${host}:${address.port}`); + } + resolve(address); + }); + }); + } + + // Both environments give the "unable to verify" error with partial chains. + it("detects partial chains", async () => { + const address = await startServer("chain-leaf"); + const request = axios.get(address, { + httpsAgent: new https.Agent({ + ca: await fs.readFile( + path.join(__dirname, "../fixtures/tls/chain-leaf.crt"), + ), + }), }); + await expect(request).rejects.toHaveProperty( + "code", + X509_ERR_CODE.UNABLE_TO_VERIFY_LEAF_SIGNATURE, + ); + try { + await request; + } catch (error) { + const wrapped = await CertificateError.maybeWrap(error, address, logger); + expect(wrapped instanceof CertificateError).toBeTruthy(); + expect((wrapped as CertificateError).x509Err).toBe( + X509_ERR.PARTIAL_CHAIN, + ); + } }); -} -// Both environments give the "unable to verify" error with partial chains. -it("detects partial chains", async () => { - const address = await startServer("chain-leaf"); - const request = axios.get(address, { - httpsAgent: new https.Agent({ - ca: await fs.readFile( - path.join(__dirname, "../fixtures/tls/chain-leaf.crt"), - ), - }), + it("can bypass partial chain", async () => { + const address = await startServer("chain-leaf"); + const request = axios.get(address, { + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), + }); + await expect(request).resolves.toHaveProperty("data", "foobar"); }); - await expect(request).rejects.toHaveProperty( - "code", - X509_ERR_CODE.UNABLE_TO_VERIFY_LEAF_SIGNATURE, - ); - try { - await request; - } catch (error) { - const wrapped = await CertificateError.maybeWrap(error, address, logger); - expect(wrapped instanceof CertificateError).toBeTruthy(); - expect((wrapped as CertificateError).x509Err).toBe(X509_ERR.PARTIAL_CHAIN); - } -}); -it("can bypass partial chain", async () => { - const address = await startServer("chain-leaf"); - const request = axios.get(address, { - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - }), + // In Electron a self-issued certificate without the signing capability fails + // (again with the same "unable to verify" error) but in Node self-issued + // certificates are not required to have the signing capability. + it("detects self-signed certificates without signing capability", async () => { + const address = await startServer("no-signing"); + const request = axios.get(address, { + httpsAgent: new https.Agent({ + ca: await fs.readFile( + path.join(__dirname, "../fixtures/tls/no-signing.crt"), + ), + servername: "localhost", + }), + }); + if (isElectron) { + await expect(request).rejects.toHaveProperty( + "code", + X509_ERR_CODE.UNABLE_TO_VERIFY_LEAF_SIGNATURE, + ); + try { + await request; + } catch (error) { + const wrapped = await CertificateError.maybeWrap( + error, + address, + logger, + ); + expect(wrapped instanceof CertificateError).toBeTruthy(); + expect((wrapped as CertificateError).x509Err).toBe( + X509_ERR.NON_SIGNING, + ); + } + } else { + await expect(request).resolves.toHaveProperty("data", "foobar"); + } }); - await expect(request).resolves.toHaveProperty("data", "foobar"); -}); -// In Electron a self-issued certificate without the signing capability fails -// (again with the same "unable to verify" error) but in Node self-issued -// certificates are not required to have the signing capability. -it("detects self-signed certificates without signing capability", async () => { - const address = await startServer("no-signing"); - const request = axios.get(address, { - httpsAgent: new https.Agent({ - ca: await fs.readFile( - path.join(__dirname, "../fixtures/tls/no-signing.crt"), - ), - servername: "localhost", - }), + it("can bypass self-signed certificates without signing capability", async () => { + const address = await startServer("no-signing"); + const request = axios.get(address, { + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), + }); + await expect(request).resolves.toHaveProperty("data", "foobar"); }); - if (isElectron) { + + // Both environments give the same error code when a self-issued certificate is + // untrusted. + it("detects self-signed certificates", async () => { + const address = await startServer("self-signed"); + const request = axios.get(address); await expect(request).rejects.toHaveProperty( "code", - X509_ERR_CODE.UNABLE_TO_VERIFY_LEAF_SIGNATURE, + X509_ERR_CODE.DEPTH_ZERO_SELF_SIGNED_CERT, ); try { await request; } catch (error) { const wrapped = await CertificateError.maybeWrap(error, address, logger); expect(wrapped instanceof CertificateError).toBeTruthy(); - expect((wrapped as CertificateError).x509Err).toBe(X509_ERR.NON_SIGNING); + expect((wrapped as CertificateError).x509Err).toBe( + X509_ERR.UNTRUSTED_LEAF, + ); } - } else { - await expect(request).resolves.toHaveProperty("data", "foobar"); - } -}); - -it("can bypass self-signed certificates without signing capability", async () => { - const address = await startServer("no-signing"); - const request = axios.get(address, { - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - }), }); - await expect(request).resolves.toHaveProperty("data", "foobar"); -}); -// Both environments give the same error code when a self-issued certificate is -// untrusted. -it("detects self-signed certificates", async () => { - const address = await startServer("self-signed"); - const request = axios.get(address); - await expect(request).rejects.toHaveProperty( - "code", - X509_ERR_CODE.DEPTH_ZERO_SELF_SIGNED_CERT, - ); - try { - await request; - } catch (error) { - const wrapped = await CertificateError.maybeWrap(error, address, logger); - expect(wrapped instanceof CertificateError).toBeTruthy(); - expect((wrapped as CertificateError).x509Err).toBe(X509_ERR.UNTRUSTED_LEAF); - } -}); - -// Both environments have no problem if the self-issued certificate is trusted -// and has the signing capability. -it("is ok with trusted self-signed certificates", async () => { - const address = await startServer("self-signed"); - const request = axios.get(address, { - httpsAgent: new https.Agent({ - ca: await fs.readFile( - path.join(__dirname, "../fixtures/tls/self-signed.crt"), - ), - servername: "localhost", - }), + // Both environments have no problem if the self-issued certificate is trusted + // and has the signing capability. + it("is ok with trusted self-signed certificates", async () => { + const address = await startServer("self-signed"); + const request = axios.get(address, { + httpsAgent: new https.Agent({ + ca: await fs.readFile( + path.join(__dirname, "../fixtures/tls/self-signed.crt"), + ), + servername: "localhost", + }), + }); + await expect(request).resolves.toHaveProperty("data", "foobar"); }); - await expect(request).resolves.toHaveProperty("data", "foobar"); -}); -it("can bypass self-signed certificates", async () => { - const address = await startServer("self-signed"); - const request = axios.get(address, { - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - }), + it("can bypass self-signed certificates", async () => { + const address = await startServer("self-signed"); + const request = axios.get(address, { + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), + }); + await expect(request).resolves.toHaveProperty("data", "foobar"); }); - await expect(request).resolves.toHaveProperty("data", "foobar"); -}); -// Both environments give the same error code when the chain is complete but the -// root is not trusted. -it("detects an untrusted chain", async () => { - const address = await startServer("chain"); - const request = axios.get(address); - await expect(request).rejects.toHaveProperty( - "code", - X509_ERR_CODE.SELF_SIGNED_CERT_IN_CHAIN, - ); - try { - await request; - } catch (error) { - const wrapped = await CertificateError.maybeWrap(error, address, logger); - expect(wrapped instanceof CertificateError).toBeTruthy(); - expect((wrapped as CertificateError).x509Err).toBe( - X509_ERR.UNTRUSTED_CHAIN, + // Both environments give the same error code when the chain is complete but the + // root is not trusted. + it("detects an untrusted chain", async () => { + const address = await startServer("chain"); + const request = axios.get(address); + await expect(request).rejects.toHaveProperty( + "code", + X509_ERR_CODE.SELF_SIGNED_CERT_IN_CHAIN, ); - } -}); + try { + await request; + } catch (error) { + const wrapped = await CertificateError.maybeWrap(error, address, logger); + expect(wrapped instanceof CertificateError).toBeTruthy(); + expect((wrapped as CertificateError).x509Err).toBe( + X509_ERR.UNTRUSTED_CHAIN, + ); + } + }); -// Both environments have no problem if the chain is complete and the root is -// trusted. -it("is ok with chains with a trusted root", async () => { - const address = await startServer("chain"); - const request = axios.get(address, { - httpsAgent: new https.Agent({ - ca: await fs.readFile( - path.join(__dirname, "../fixtures/tls/chain-root.crt"), - ), - servername: "localhost", - }), + // Both environments have no problem if the chain is complete and the root is + // trusted. + it("is ok with chains with a trusted root", async () => { + const address = await startServer("chain"); + const request = axios.get(address, { + httpsAgent: new https.Agent({ + ca: await fs.readFile( + path.join(__dirname, "../fixtures/tls/chain-root.crt"), + ), + servername: "localhost", + }), + }); + await expect(request).resolves.toHaveProperty("data", "foobar"); }); - await expect(request).resolves.toHaveProperty("data", "foobar"); -}); -it("can bypass chain", async () => { - const address = await startServer("chain"); - const request = axios.get(address, { - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - }), + it("can bypass chain", async () => { + const address = await startServer("chain"); + const request = axios.get(address, { + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), + }); + await expect(request).resolves.toHaveProperty("data", "foobar"); }); - await expect(request).resolves.toHaveProperty("data", "foobar"); -}); -it("falls back with different error", async () => { - const address = await startServer("chain"); - const request = axios.get(address + "/error", { - httpsAgent: new https.Agent({ - ca: await fs.readFile( - path.join(__dirname, "../fixtures/tls/chain-root.crt"), - ), - servername: "localhost", - }), + it("falls back with different error", async () => { + const address = await startServer("chain"); + const request = axios.get(address + "/error", { + httpsAgent: new https.Agent({ + ca: await fs.readFile( + path.join(__dirname, "../fixtures/tls/chain-root.crt"), + ), + servername: "localhost", + }), + }); + await expect(request).rejects.toThrow(/failed with status code 500/); + try { + await request; + } catch (error) { + const wrapped = await CertificateError.maybeWrap(error, "1", logger); + expect(wrapped instanceof CertificateError).toBeFalsy(); + expect((wrapped as Error).message).toMatch(/failed with status code 500/); + } }); - await expect(request).rejects.toMatch(/failed with status code 500/); - try { - await request; - } catch (error) { - const wrapped = await CertificateError.maybeWrap(error, "1", logger); - expect(wrapped instanceof CertificateError).toBeFalsy(); - expect((wrapped as Error).message).toMatch(/failed with status code 500/); - } }); diff --git a/src/extension.ts b/src/extension.ts index 9d1531db..bd8a09c6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,11 +7,14 @@ import { errToStr } from "./api/api-helper"; import { CoderApi } from "./api/coderApi"; import { needToken } from "./api/utils"; import { Commands } from "./commands"; +import { CliManager } from "./core/cliManager"; +import { MementoManager } from "./core/mementoManager"; +import { PathResolver } from "./core/pathResolver"; +import { SecretsManager } from "./core/secretsManager"; import { CertificateError, getErrorDetail } from "./error"; import { Remote } from "./remote"; -import { Storage } from "./storage"; import { toSafeHost } from "./util"; -import { WorkspaceQuery, WorkspaceProvider } from "./workspacesProvider"; +import { WorkspaceProvider, WorkspaceQuery } from "./workspacesProvider"; export async function activate(ctx: vscode.ExtensionContext): Promise { // The Remote SSH extension's proposed APIs are used to override the SSH host @@ -48,40 +51,39 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { ); } - const output = vscode.window.createOutputChannel("Coder", { log: true }); - const storage = new Storage( - vscodeProposed, - output, - ctx.globalState, - ctx.secrets, - ctx.globalStorageUri, - ctx.logUri, + const pathResolver = new PathResolver( + ctx.globalStorageUri.fsPath, + ctx.logUri.fsPath, ); + const mementoManager = new MementoManager(ctx.globalState); + const secretsManager = new SecretsManager(ctx.secrets); - // Try to clear this flag ASAP then pass it around if needed - const isFirstConnect = await storage.getAndClearFirstConnect(); + const output = vscode.window.createOutputChannel("Coder", { log: true }); + + // Try to clear this flag ASAP + const isFirstConnect = await mementoManager.getAndClearFirstConnect(); // This client tracks the current login and will be used through the life of // the plugin to poll workspaces for the current login, as well as being used // in commands that operate on the current login. - const url = storage.getUrl(); + const url = mementoManager.getUrl(); const client = CoderApi.create( url || "", - await storage.getSessionToken(), - storage.output, + await secretsManager.getSessionToken(), + output, () => vscode.workspace.getConfiguration(), ); const myWorkspacesProvider = new WorkspaceProvider( WorkspaceQuery.Mine, client, - storage, + output, 5, ); const allWorkspacesProvider = new WorkspaceProvider( WorkspaceQuery.All, client, - storage, + output, ); // createTreeView, unlike registerTreeDataProvider, gives us the tree view API @@ -129,11 +131,11 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // hit enter and move on. const url = await commands.maybeAskUrl( params.get("url"), - storage.getUrl(), + mementoManager.getUrl(), ); if (url) { client.setHost(url); - await storage.setUrl(url); + await mementoManager.setUrl(url); } else { throw new Error( "url must be provided or specified as a query parameter", @@ -151,11 +153,11 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { : (params.get("token") ?? ""); if (token) { client.setSessionToken(token); - await storage.setSessionToken(token); + await secretsManager.setSessionToken(token); } // Store on disk to be used by the cli. - await storage.configureCli(toSafeHost(url), url, token); + await cliManager.configure(toSafeHost(url), url, token); vscode.commands.executeCommand( "coder.open", @@ -211,11 +213,11 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // hit enter and move on. const url = await commands.maybeAskUrl( params.get("url"), - storage.getUrl(), + mementoManager.getUrl(), ); if (url) { client.setHost(url); - await storage.setUrl(url); + await mementoManager.setUrl(url); } else { throw new Error( "url must be provided or specified as a query parameter", @@ -233,7 +235,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { : (params.get("token") ?? ""); // Store on disk to be used by the cli. - await storage.configureCli(toSafeHost(url), url, token); + await cliManager.configure(toSafeHost(url), url, token); vscode.commands.executeCommand( "coder.openDevContainer", @@ -251,9 +253,19 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { }, }); + const cliManager = new CliManager(vscodeProposed, output, pathResolver); + // Register globally available commands. Many of these have visibility // controlled by contexts, see `when` in the package.json. - const commands = new Commands(vscodeProposed, client, storage); + const commands = new Commands( + vscodeProposed, + client, + output, + pathResolver, + mementoManager, + secretsManager, + cliManager, + ); vscode.commands.registerCommand("coder.login", commands.login.bind(commands)); vscode.commands.registerCommand( "coder.logout", @@ -309,9 +321,11 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { if (remoteSSHExtension && vscodeProposed.env.remoteAuthority) { const remote = new Remote( vscodeProposed, - storage, + output, commands, ctx.extensionMode, + pathResolver, + cliManager, ); try { const details = await remote.setup( @@ -326,7 +340,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { } } catch (ex) { if (ex instanceof CertificateError) { - storage.output.warn(ex.x509Err || ex.message); + output.warn(ex.x509Err || ex.message); await ex.showModal("Failed to open workspace"); } else if (isAxiosError(ex)) { const msg = getErrorMessage(ex, "None"); @@ -335,7 +349,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { const method = ex.config?.method?.toUpperCase() || "request"; const status = ex.response?.status || "None"; const message = `API ${method} to '${urlString}' failed.\nStatus code: ${status}\nMessage: ${msg}\nDetail: ${detail}`; - storage.output.warn(message); + output.warn(message); await vscodeProposed.window.showErrorMessage( "Failed to open workspace", { @@ -346,7 +360,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { ); } else { const message = errToStr(ex, "No error message was provided"); - storage.output.warn(message); + output.warn(message); await vscodeProposed.window.showErrorMessage( "Failed to open workspace", { @@ -365,12 +379,12 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // See if the plugin client is authenticated. const baseUrl = client.getAxiosInstance().defaults.baseURL; if (baseUrl) { - storage.output.info(`Logged in to ${baseUrl}; checking credentials`); + output.info(`Logged in to ${baseUrl}; checking credentials`); client .getAuthenticatedUser() .then(async (user) => { if (user && user.roles) { - storage.output.info("Credentials are valid"); + output.info("Credentials are valid"); vscode.commands.executeCommand( "setContext", "coder.authenticated", @@ -388,13 +402,13 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { myWorkspacesProvider.fetchAndRefresh(); allWorkspacesProvider.fetchAndRefresh(); } else { - storage.output.warn("No error, but got unexpected response", user); + output.warn("No error, but got unexpected response", user); } }) .catch((error) => { // This should be a failure to make the request, like the header command // errored. - storage.output.warn("Failed to check user authentication", error); + output.warn("Failed to check user authentication", error); vscode.window.showErrorMessage( `Failed to check user authentication: ${error.message}`, ); @@ -403,7 +417,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { vscode.commands.executeCommand("setContext", "coder.loaded", true); }); } else { - storage.output.info("Not currently logged in"); + output.info("Not currently logged in"); vscode.commands.executeCommand("setContext", "coder.loaded", true); // Handle autologin, if not already logged in. diff --git a/src/headers.test.ts b/src/headers.test.ts index 10e77f8d..84c39d36 100644 --- a/src/headers.test.ts +++ b/src/headers.test.ts @@ -66,25 +66,25 @@ it("should return headers", async () => { it("should error on malformed or empty lines", async () => { await expect( getHeaders("localhost", "printf 'foo=bar\\r\\n\\r\\n'", logger), - ).rejects.toMatch(/Malformed/); + ).rejects.toThrow(/Malformed/); await expect( getHeaders("localhost", "printf '\\r\\nfoo=bar'", logger), - ).rejects.toMatch(/Malformed/); + ).rejects.toThrow(/Malformed/); await expect( getHeaders("localhost", "printf '=foo'", logger), - ).rejects.toMatch(/Malformed/); - await expect(getHeaders("localhost", "printf 'foo'", logger)).rejects.toMatch( + ).rejects.toThrow(/Malformed/); + await expect(getHeaders("localhost", "printf 'foo'", logger)).rejects.toThrow( /Malformed/, ); await expect( getHeaders("localhost", "printf ' =foo'", logger), - ).rejects.toMatch(/Malformed/); + ).rejects.toThrow(/Malformed/); await expect( getHeaders("localhost", "printf 'foo =bar'", logger), - ).rejects.toMatch(/Malformed/); + ).rejects.toThrow(/Malformed/); await expect( getHeaders("localhost", "printf 'foo foo=bar'", logger), - ).rejects.toMatch(/Malformed/); + ).rejects.toThrow(/Malformed/); }); it("should have access to environment variables", async () => { @@ -101,7 +101,7 @@ it("should have access to environment variables", async () => { }); it("should error on non-zero exit", async () => { - await expect(getHeaders("localhost", "exit 10", logger)).rejects.toMatch( + await expect(getHeaders("localhost", "exit 10", logger)).rejects.toThrow( /exited unexpectedly with code 10/, ); }); diff --git a/src/inbox.ts b/src/inbox.ts index 3141b661..e12263bf 100644 --- a/src/inbox.ts +++ b/src/inbox.ts @@ -4,7 +4,7 @@ import { } from "coder/site/src/api/typesGenerated"; import * as vscode from "vscode"; import { CoderApi } from "./api/coderApi"; -import { type Storage } from "./storage"; +import { Logger } from "./logging/logger"; import { OneWayWebSocket } from "./websocket/oneWayWebSocket"; // These are the template IDs of our notifications. @@ -14,12 +14,12 @@ const TEMPLATE_WORKSPACE_OUT_OF_MEMORY = "a9d027b4-ac49-4fb1-9f6d-45af15f64e7a"; const TEMPLATE_WORKSPACE_OUT_OF_DISK = "f047f6a3-5713-40f7-85aa-0394cce9fa3a"; export class Inbox implements vscode.Disposable { - readonly #storage: Storage; + readonly #logger: Logger; #disposed = false; #socket: OneWayWebSocket; - constructor(workspace: Workspace, client: CoderApi, storage: Storage) { - this.#storage = storage; + constructor(workspace: Workspace, client: CoderApi, logger: Logger) { + this.#logger = logger; const watchTemplates = [ TEMPLATE_WORKSPACE_OUT_OF_DISK, @@ -31,7 +31,7 @@ export class Inbox implements vscode.Disposable { this.#socket = client.watchInboxNotifications(watchTemplates, watchTargets); this.#socket.addEventListener("open", () => { - this.#storage.output.info("Listening to Coder Inbox"); + this.#logger.info("Listening to Coder Inbox"); }); this.#socket.addEventListener("error", () => { @@ -41,10 +41,7 @@ export class Inbox implements vscode.Disposable { this.#socket.addEventListener("message", (data) => { if (data.parseError) { - this.#storage.output.error( - "Failed to parse inbox message", - data.parseError, - ); + this.#logger.error("Failed to parse inbox message", data.parseError); } else { vscode.window.showInformationMessage( data.parsedMessage.notification.title, @@ -55,7 +52,7 @@ export class Inbox implements vscode.Disposable { dispose() { if (!this.#disposed) { - this.#storage.output.info("No longer listening to Coder Inbox"); + this.#logger.info("No longer listening to Coder Inbox"); this.#socket.close(); this.#disposed = true; } diff --git a/src/pgp.ts b/src/pgp.ts index c707c5b4..2e82fb79 100644 --- a/src/pgp.ts +++ b/src/pgp.ts @@ -2,8 +2,8 @@ import { createReadStream, promises as fs } from "fs"; import * as openpgp from "openpgp"; import * as path from "path"; import { Readable } from "stream"; -import * as vscode from "vscode"; import { errToStr } from "./api/api-helper"; +import { Logger } from "./logging/logger"; export type Key = openpgp.Key; @@ -35,9 +35,7 @@ export class VerificationError extends Error { /** * Return the public keys bundled with the plugin. */ -export async function readPublicKeys( - logger?: vscode.LogOutputChannel, -): Promise { +export async function readPublicKeys(logger?: Logger): Promise { const keyFile = path.join(__dirname, "../pgp-public.key"); logger?.info("Reading public key", keyFile); const armoredKeys = await fs.readFile(keyFile, "utf8"); @@ -53,7 +51,7 @@ export async function verifySignature( publicKeys: openpgp.Key[], cliPath: string, signaturePath: string, - logger?: vscode.LogOutputChannel, + logger?: Logger, ): Promise { try { logger?.info("Reading signature", signaturePath); diff --git a/src/remote.ts b/src/remote.ts index 172074ee..c9765fb8 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -19,14 +19,16 @@ import { createWorkspaceIdentifier, extractAgents } from "./api/api-helper"; import { CoderApi } from "./api/coderApi"; import { needToken } from "./api/utils"; import { startWorkspaceIfStoppedOrFailed, waitForBuild } from "./api/workspace"; -import * as cli from "./cliManager"; +import * as cliUtils from "./cliUtils"; import { Commands } from "./commands"; +import { CliManager } from "./core/cliManager"; +import { PathResolver } from "./core/pathResolver"; import { featureSetForVersion, FeatureSet } from "./featureSet"; import { getGlobalFlags } from "./globalFlags"; import { Inbox } from "./inbox"; +import { Logger } from "./logging/logger"; import { SSHConfig, SSHValues, mergeSSHConfigValues } from "./sshConfig"; import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport"; -import { Storage } from "./storage"; import { AuthorityPrefix, escapeCommandArg, @@ -45,9 +47,11 @@ export class Remote { public constructor( // We use the proposed API to get access to useCustom in dialogs. private readonly vscodeProposed: typeof vscode, - private readonly storage: Storage, + private readonly logger: Logger, private readonly commands: Commands, private readonly mode: vscode.ExtensionMode, + private readonly pathResolver: PathResolver, + private readonly cliManager: CliManager, ) {} private async confirmStart(workspaceName: string): Promise { @@ -111,9 +115,7 @@ export class Remote { title: "Waiting for workspace build...", }, async () => { - const globalConfigDir = path.dirname( - this.storage.getSessionTokenPath(label), - ); + const globalConfigDir = this.pathResolver.getGlobalConfigDir(label); while (workspace.latest_build.status !== "running") { ++attempts; switch (workspace.latest_build.status) { @@ -121,7 +123,7 @@ export class Remote { case "starting": case "stopping": writeEmitter = initWriteEmitterAndTerminal(); - this.storage.output.info(`Waiting for ${workspaceName}...`); + this.logger.info(`Waiting for ${workspaceName}...`); workspace = await waitForBuild(client, writeEmitter, workspace); break; case "stopped": @@ -132,7 +134,7 @@ export class Remote { return undefined; } writeEmitter = initWriteEmitterAndTerminal(); - this.storage.output.info(`Starting ${workspaceName}...`); + this.logger.info(`Starting ${workspaceName}...`); workspace = await startWorkspaceIfStoppedOrFailed( client, globalConfigDir, @@ -153,7 +155,7 @@ export class Remote { return undefined; } writeEmitter = initWriteEmitterAndTerminal(); - this.storage.output.info(`Starting ${workspaceName}...`); + this.logger.info(`Starting ${workspaceName}...`); workspace = await startWorkspaceIfStoppedOrFailed( client, globalConfigDir, @@ -177,7 +179,7 @@ export class Remote { ); } } - this.storage.output.info( + this.logger.info( `${workspaceName} status is now`, workspace.latest_build.status, ); @@ -213,10 +215,10 @@ export class Remote { const workspaceName = `${parts.username}/${parts.workspace}`; // Migrate "session_token" file to "session", if needed. - await this.storage.migrateSessionToken(parts.label); + await this.migrateSessionToken(parts.label); // Get the URL and token belonging to this host. - const { url: baseUrlRaw, token } = await this.storage.readCliConfig( + const { url: baseUrlRaw, token } = await this.cliManager.readConfig( parts.label, ); @@ -250,8 +252,8 @@ export class Remote { return; } - this.storage.output.info("Using deployment URL", baseUrlRaw); - this.storage.output.info("Using deployment label", parts.label || "n/a"); + this.logger.info("Using deployment URL", baseUrlRaw); + this.logger.info("Using deployment label", parts.label || "n/a"); // We could use the plugin client, but it is possible for the user to log // out or log into a different deployment while still connected, which would @@ -261,7 +263,7 @@ export class Remote { const workspaceClient = CoderApi.create( baseUrlRaw, token, - this.storage.output, + this.logger, () => vscode.workspace.getConfiguration(), ); // Store for use in commands. @@ -269,7 +271,10 @@ export class Remote { let binaryPath: string | undefined; if (this.mode === vscode.ExtensionMode.Production) { - binaryPath = await this.storage.fetchBinary(workspaceClient, parts.label); + binaryPath = await this.cliManager.fetchBinary( + workspaceClient, + parts.label, + ); } else { try { // In development, try to use `/tmp/coder` as the binary path. @@ -277,7 +282,7 @@ export class Remote { binaryPath = path.join(os.tmpdir(), "coder"); await fs.stat(binaryPath); } catch (ex) { - binaryPath = await this.storage.fetchBinary( + binaryPath = await this.cliManager.fetchBinary( workspaceClient, parts.label, ); @@ -289,7 +294,7 @@ export class Remote { let version: semver.SemVer | null = null; try { - version = semver.parse(await cli.version(binaryPath)); + version = semver.parse(await cliUtils.version(binaryPath)); } catch (e) { version = semver.parse(buildInfo.version); } @@ -315,12 +320,12 @@ export class Remote { // Next is to find the workspace from the URI scheme provided. let workspace: Workspace; try { - this.storage.output.info(`Looking for workspace ${workspaceName}...`); + this.logger.info(`Looking for workspace ${workspaceName}...`); workspace = await workspaceClient.getWorkspaceByOwnerAndName( parts.username, parts.workspace, ); - this.storage.output.info( + this.logger.info( `Found workspace ${workspaceName} with status`, workspace.latest_build.status, ); @@ -406,7 +411,7 @@ export class Remote { this.commands.workspace = workspace; // Pick an agent. - this.storage.output.info(`Finding agent for ${workspaceName}...`); + this.logger.info(`Finding agent for ${workspaceName}...`); const agents = extractAgents(workspace.latest_build.resources); const gotAgent = await this.commands.maybeAskAgent(agents, parts.agent); if (!gotAgent) { @@ -415,13 +420,10 @@ export class Remote { return; } let agent = gotAgent; // Reassign so it cannot be undefined in callbacks. - this.storage.output.info( - `Found agent ${agent.name} with status`, - agent.status, - ); + this.logger.info(`Found agent ${agent.name} with status`, agent.status); // Do some janky setting manipulation. - this.storage.output.info("Modifying settings..."); + this.logger.info("Modifying settings..."); const remotePlatforms = this.vscodeProposed.workspace .getConfiguration() .get>("remote.SSH.remotePlatform", {}); @@ -437,7 +439,7 @@ export class Remote { let settingsContent = "{}"; try { settingsContent = await fs.readFile( - this.storage.getUserSettingsPath(), + this.pathResolver.getUserSettingsPath(), "utf8", ); } catch (ex) { @@ -486,14 +488,17 @@ export class Remote { if (mungedPlatforms || mungedConnTimeout) { try { - await fs.writeFile(this.storage.getUserSettingsPath(), settingsContent); + await fs.writeFile( + this.pathResolver.getUserSettingsPath(), + settingsContent, + ); } catch (ex) { // This could be because the user's settings.json is read-only. This is // the case when using home-manager on NixOS, for example. Failure to // write here is not necessarily catastrophic since the user will be // asked for the platform and the default timeout might be sufficient. mungedPlatforms = mungedConnTimeout = false; - this.storage.output.warn("Failed to configure settings", ex); + this.logger.warn("Failed to configure settings", ex); } } @@ -501,7 +506,7 @@ export class Remote { const monitor = new WorkspaceMonitor( workspace, workspaceClient, - this.storage, + this.logger, this.vscodeProposed, ); disposables.push(monitor); @@ -510,12 +515,12 @@ export class Remote { ); // Watch coder inbox for messages - const inbox = new Inbox(workspace, workspaceClient, this.storage); + const inbox = new Inbox(workspace, workspaceClient, this.logger); disposables.push(inbox); // Wait for the agent to connect. if (agent.status === "connecting") { - this.storage.output.info(`Waiting for ${workspaceName}/${agent.name}...`); + this.logger.info(`Waiting for ${workspaceName}/${agent.name}...`); await vscode.window.withProgress( { title: "Waiting for the agent to connect...", @@ -544,10 +549,7 @@ export class Remote { }); }, ); - this.storage.output.info( - `Agent ${agent.name} status is now`, - agent.status, - ); + this.logger.info(`Agent ${agent.name} status is now`, agent.status); } // Make sure the agent is connected. @@ -577,7 +579,7 @@ export class Remote { // If we didn't write to the SSH config file, connecting would fail with // "Host not found". try { - this.storage.output.info("Updating SSH config..."); + this.logger.info("Updating SSH config..."); await this.updateSSHConfig( workspaceClient, parts.label, @@ -587,7 +589,7 @@ export class Remote { featureSet, ); } catch (error) { - this.storage.output.warn("Failed to configure SSH", error); + this.logger.warn("Failed to configure SSH", error); throw error; } @@ -631,7 +633,7 @@ export class Remote { ...this.createAgentMetadataStatusBar(agent, workspaceClient), ); - this.storage.output.info("Remote setup complete"); + this.logger.info("Remote setup complete"); // Returning the URL and token allows the plugin to authenticate its own // client, for example to display the list of workspaces belonging to this @@ -646,6 +648,22 @@ export class Remote { }; } + /** + * Migrate the session token file from "session_token" to "session", if needed. + */ + private async migrateSessionToken(label: string) { + const oldTokenPath = this.pathResolver.getLegacySessionTokenPath(label); + const newTokenPath = this.pathResolver.getSessionTokenPath(label); + try { + await fs.rename(oldTokenPath, newTokenPath); + } catch (error) { + if ((error as NodeJS.ErrnoException)?.code === "ENOENT") { + return; + } + throw error; + } + } + /** * Return the --log-dir argument value for the ProxyCommand. It may be an * empty string if the setting is not set or the cli does not support it. @@ -672,10 +690,7 @@ export class Remote { return ""; } await fs.mkdir(logDir, { recursive: true }); - this.storage.output.info( - "SSH proxy diagnostics are being written to", - logDir, - ); + this.logger.info("SSH proxy diagnostics are being written to", logDir); return ` --log-dir ${escapeCommandArg(logDir)} -v`; } @@ -765,11 +780,11 @@ export class Remote { const globalConfigs = this.globalConfigs(label); const proxyCommand = featureSet.wildcardSSH - ? `${escapeCommandArg(binaryPath)}${globalConfigs} ssh --stdio --usage-app=vscode --disable-autostart --network-info-dir ${escapeCommandArg(this.storage.getNetworkInfoPath())}${await this.formatLogArg(logDir)} --ssh-host-prefix ${hostPrefix} %h` + ? `${escapeCommandArg(binaryPath)}${globalConfigs} ssh --stdio --usage-app=vscode --disable-autostart --network-info-dir ${escapeCommandArg(this.pathResolver.getNetworkInfoPath())}${await this.formatLogArg(logDir)} --ssh-host-prefix ${hostPrefix} %h` : `${escapeCommandArg(binaryPath)}${globalConfigs} vscodessh --network-info-dir ${escapeCommandArg( - this.storage.getNetworkInfoPath(), - )}${await this.formatLogArg(logDir)} --session-token-file ${escapeCommandArg(this.storage.getSessionTokenPath(label))} --url-file ${escapeCommandArg( - this.storage.getUrlPath(label), + this.pathResolver.getNetworkInfoPath(), + )}${await this.formatLogArg(logDir)} --session-token-file ${escapeCommandArg(this.pathResolver.getSessionTokenPath(label))} --url-file ${escapeCommandArg( + this.pathResolver.getUrlPath(label), )} %h`; const sshValues: SSHValues = { @@ -828,7 +843,7 @@ export class Remote { const vscodeConfig = vscode.workspace.getConfiguration(); const args = getGlobalFlags( vscodeConfig, - path.dirname(this.storage.getSessionTokenPath(label)), + this.pathResolver.getGlobalConfigDir(label), ); return ` ${args.join(" ")}`; } @@ -841,7 +856,7 @@ export class Remote { 1000, ); const networkInfoFile = path.join( - this.storage.getNetworkInfoPath(), + this.pathResolver.getNetworkInfoPath(), `${sshPid}.json`, ); @@ -964,7 +979,7 @@ export class Remote { return undefined; } // Loop until we find the remote SSH log for this window. - const filePath = await this.storage.getRemoteSSHLogPath(); + const filePath = await this.getRemoteSSHLogPath(); if (!filePath) { return new Promise((resolve) => setTimeout(() => resolve(loop()), 500)); } @@ -978,6 +993,29 @@ export class Remote { return loop(); } + /** + * Returns the log path for the "Remote - SSH" output panel. There is no VS + * Code API to get the contents of an output panel. We use this to get the + * active port so we can display network information. + */ + private async getRemoteSSHLogPath(): Promise { + const upperDir = path.dirname(this.pathResolver.getCodeLogDir()); + // Node returns these directories sorted already! + const dirs = await fs.readdir(upperDir); + const latestOutput = dirs + .reverse() + .filter((dir) => dir.startsWith("output_logging_")); + if (latestOutput.length === 0) { + return undefined; + } + const dir = await fs.readdir(path.join(upperDir, latestOutput[0])); + const remoteSSH = dir.filter((file) => file.indexOf("Remote - SSH") !== -1); + if (remoteSSH.length === 0) { + return undefined; + } + return path.join(upperDir, latestOutput[0], remoteSSH[0]); + } + /** * Creates and manages a status bar item that displays metadata information for a given workspace agent. * The status bar item updates dynamically based on changes to the agent's metadata, @@ -997,7 +1035,7 @@ export class Remote { const onChangeDisposable = agentWatcher.onChange(() => { if (agentWatcher.error) { const errMessage = formatMetadataError(agentWatcher.error); - this.storage.output.warn(errMessage); + this.logger.warn(errMessage); statusBarItem.text = "$(warning) Agent Status Unavailable"; statusBarItem.tooltip = errMessage; diff --git a/src/workspaceMonitor.ts b/src/workspaceMonitor.ts index 16c1ecde..ece765a6 100644 --- a/src/workspaceMonitor.ts +++ b/src/workspaceMonitor.ts @@ -3,7 +3,7 @@ import { formatDistanceToNowStrict } from "date-fns"; import * as vscode from "vscode"; import { createWorkspaceIdentifier, errToStr } from "./api/api-helper"; import { CoderApi } from "./api/coderApi"; -import { Storage } from "./storage"; +import { Logger } from "./logging/logger"; import { OneWayWebSocket } from "./websocket/oneWayWebSocket"; /** @@ -34,7 +34,7 @@ export class WorkspaceMonitor implements vscode.Disposable { constructor( workspace: Workspace, private readonly client: CoderApi, - private readonly storage: Storage, + private readonly logger: Logger, // We use the proposed API to get access to useCustom in dialogs. private readonly vscodeProposed: typeof vscode, ) { @@ -42,7 +42,7 @@ export class WorkspaceMonitor implements vscode.Disposable { const socket = this.client.watchWorkspace(workspace); socket.addEventListener("open", () => { - this.storage.output.info(`Monitoring ${this.name}...`); + this.logger.info(`Monitoring ${this.name}...`); }); socket.addEventListener("message", (event) => { @@ -83,7 +83,7 @@ export class WorkspaceMonitor implements vscode.Disposable { */ dispose() { if (!this.disposed) { - this.storage.output.info(`Unmonitoring ${this.name}...`); + this.logger.info(`Unmonitoring ${this.name}...`); this.statusBarItem.dispose(); this.socket.close(); this.disposed = true; @@ -209,7 +209,7 @@ export class WorkspaceMonitor implements vscode.Disposable { error, "Got empty error while monitoring workspace", ); - this.storage.output.error(message); + this.logger.error(message); } private updateContext(workspace: Workspace) { diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index f344eb0f..23f5705a 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -13,11 +13,11 @@ import { } from "./agentMetadataHelper"; import { AgentMetadataEvent, - extractAllAgents, extractAgents, + extractAllAgents, } from "./api/api-helper"; import { CoderApi } from "./api/coderApi"; -import { Storage } from "./storage"; +import { Logger } from "./logging/logger"; export enum WorkspaceQuery { Mine = "owner:me", @@ -46,7 +46,7 @@ export class WorkspaceProvider constructor( private readonly getWorkspacesQuery: WorkspaceQuery, private readonly client: CoderApi, - private readonly storage: Storage, + private readonly logger: Logger, private readonly timerSeconds?: number, ) { // No initialization. @@ -92,7 +92,7 @@ export class WorkspaceProvider */ private async fetch(): Promise { if (vscode.env.logLevel <= vscode.LogLevel.Debug) { - this.storage.output.info( + this.logger.info( `Fetching workspaces: ${this.getWorkspacesQuery || "no filter"}...`, ); } diff --git a/vitest.config.ts b/vitest.config.ts index 2007fb45..af067d95 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,3 +1,4 @@ +import path from "path"; import { defineConfig } from "vitest/config"; export default defineConfig({ @@ -13,5 +14,13 @@ export default defineConfig({ "./src/test/**", ], environment: "node", + coverage: { + provider: "v8", + }, + }, + resolve: { + alias: { + vscode: path.resolve(__dirname, "src/__mocks__/vscode.runtime.ts"), + }, }, }); diff --git a/yarn.lock b/yarn.lock index f30780a2..62565608 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,7 +12,7 @@ resolved "https://registry.yarnpkg.com/@altano/repository-tools/-/repository-tools-1.0.1.tgz#969bb94cc80f8b4d62c7d6956466edc3f3c3817a" integrity sha512-/FFHQOMp5TZWplkDWbbLIjmANDr9H/FtqUm+hfJMK76OBut0Ht0cNfd0ZXd/6LXf4pWUTzvpgVjcin7EEHSznA== -"@ampproject/remapping@^2.2.0": +"@ampproject/remapping@^2.2.0", "@ampproject/remapping@^2.3.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== @@ -232,6 +232,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + "@babel/helper-validator-identifier@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" @@ -276,6 +281,13 @@ dependencies: "@babel/types" "^7.26.0" +"@babel/parser@^7.25.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.4.tgz#da25d4643532890932cc03f7705fe19637e03fa8" + integrity sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg== + dependencies: + "@babel/types" "^7.28.4" + "@babel/template@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" @@ -298,6 +310,14 @@ debug "^4.3.1" globals "^11.1.0" +"@babel/types@^7.25.4", "@babel/types@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.4.tgz#0a4e618f4c60a7cd6c11cb2d48060e4dbe38ac3a" + integrity sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/types@^7.25.9", "@babel/types@^7.26.0": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" @@ -311,125 +331,145 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@bcoe/v8-coverage@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz#bbe12dca5b4ef983a0d0af4b07b9bc90ea0ababa" + integrity sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA== + "@discoveryjs/json-ext@^0.5.0": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@esbuild/aix-ppc64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" - integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== - -"@esbuild/android-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" - integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== - -"@esbuild/android-arm@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" - integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== - -"@esbuild/android-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" - integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== - -"@esbuild/darwin-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" - integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== - -"@esbuild/darwin-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" - integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== - -"@esbuild/freebsd-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" - integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== - -"@esbuild/freebsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" - integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== - -"@esbuild/linux-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" - integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== - -"@esbuild/linux-arm@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" - integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== - -"@esbuild/linux-ia32@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" - integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== - -"@esbuild/linux-loong64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" - integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== - -"@esbuild/linux-mips64el@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" - integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== - -"@esbuild/linux-ppc64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" - integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== - -"@esbuild/linux-riscv64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" - integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== - -"@esbuild/linux-s390x@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" - integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== - -"@esbuild/linux-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" - integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== - -"@esbuild/netbsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" - integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== - -"@esbuild/openbsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" - integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== - -"@esbuild/sunos-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" - integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== - -"@esbuild/win32-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" - integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== - -"@esbuild/win32-ia32@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" - integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== - -"@esbuild/win32-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" - integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== +"@esbuild/aix-ppc64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9" + integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA== + +"@esbuild/android-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz#d2e70be7d51a529425422091e0dcb90374c1546c" + integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg== + +"@esbuild/android-arm@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.9.tgz#d2a753fe2a4c73b79437d0ba1480e2d760097419" + integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ== + +"@esbuild/android-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.9.tgz#5278836e3c7ae75761626962f902a0d55352e683" + integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw== + +"@esbuild/darwin-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz#f1513eaf9ec8fa15dcaf4c341b0f005d3e8b47ae" + integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg== + +"@esbuild/darwin-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz#e27dbc3b507b3a1cea3b9280a04b8b6b725f82be" + integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ== + +"@esbuild/freebsd-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz#364e3e5b7a1fd45d92be08c6cc5d890ca75908ca" + integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q== + +"@esbuild/freebsd-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz#7c869b45faeb3df668e19ace07335a0711ec56ab" + integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg== + +"@esbuild/linux-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz#48d42861758c940b61abea43ba9a29b186d6cb8b" + integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw== + +"@esbuild/linux-arm@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz#6ce4b9cabf148274101701d112b89dc67cc52f37" + integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw== + +"@esbuild/linux-ia32@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz#207e54899b79cac9c26c323fc1caa32e3143f1c4" + integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A== + +"@esbuild/linux-loong64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz#0ba48a127159a8f6abb5827f21198b999ffd1fc0" + integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ== + +"@esbuild/linux-mips64el@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz#a4d4cc693d185f66a6afde94f772b38ce5d64eb5" + integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA== + +"@esbuild/linux-ppc64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz#0f5805c1c6d6435a1dafdc043cb07a19050357db" + integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w== + +"@esbuild/linux-riscv64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz#6776edece0f8fca79f3386398b5183ff2a827547" + integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg== + +"@esbuild/linux-s390x@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz#3f6f29ef036938447c2218d309dc875225861830" + integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA== + +"@esbuild/linux-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz#831fe0b0e1a80a8b8391224ea2377d5520e1527f" + integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg== + +"@esbuild/netbsd-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz#06f99d7eebe035fbbe43de01c9d7e98d2a0aa548" + integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q== + +"@esbuild/netbsd-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz#db99858e6bed6e73911f92a88e4edd3a8c429a52" + integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g== + +"@esbuild/openbsd-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz#afb886c867e36f9d86bb21e878e1185f5d5a0935" + integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ== + +"@esbuild/openbsd-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz#30855c9f8381fac6a0ef5b5f31ac6e7108a66ecf" + integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA== + +"@esbuild/openharmony-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz#2f2144af31e67adc2a8e3705c20c2bd97bd88314" + integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg== + +"@esbuild/sunos-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz#69b99a9b5bd226c9eb9c6a73f990fddd497d732e" + integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw== + +"@esbuild/win32-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz#d789330a712af916c88325f4ffe465f885719c6b" + integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ== + +"@esbuild/win32-ia32@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz#52fc735406bd49688253e74e4e837ac2ba0789e3" + integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww== + +"@esbuild/win32-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz#585624dc829cfb6e7c0aa6c3ca7d7e6daa87e34f" + integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ== "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" @@ -522,13 +562,6 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/schemas@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" - integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== - dependencies: - "@sinclair/typebox" "^0.27.8" - "@jridgewell/gen-mapping@^0.3.0": version "0.3.2" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" @@ -580,11 +613,16 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": +"@jridgewell/sourcemap-codec@^1.4.14": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@^1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" @@ -593,6 +631,14 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.30": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" @@ -601,25 +647,49 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@jsonjoy.com/base64@^1.1.1": +"@jsonjoy.com/base64@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== -"@jsonjoy.com/json-pack@^1.0.3": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.0.4.tgz#ab59c642a2e5368e8bcfd815d817143d4f3035d0" - integrity sha512-aOcSN4MeAtFROysrbqG137b7gaDDSmVrl5mpo6sT/w+kcXpWnzhMjmY/Fh/sDx26NBxyIE7MB1seqLeCAzy9Sg== - dependencies: - "@jsonjoy.com/base64" "^1.1.1" - "@jsonjoy.com/util" "^1.1.2" +"@jsonjoy.com/buffers@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz#ade6895b7d3883d70f87b5743efaa12c71dfef7a" + integrity sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q== + +"@jsonjoy.com/codegen@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz#5c23f796c47675f166d23b948cdb889184b93207" + integrity sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g== + +"@jsonjoy.com/json-pack@^1.11.0": + version "1.14.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.14.0.tgz#eda5255ccdaeafb3aa811ff1ae4814790b958b4f" + integrity sha512-LpWbYgVnKzphN5S6uss4M25jJ/9+m6q6UJoeN6zTkK4xAGhKsiBRPVeF7OYMWonn5repMQbE5vieRXcMUrKDKw== + dependencies: + "@jsonjoy.com/base64" "^1.1.2" + "@jsonjoy.com/buffers" "^1.0.0" + "@jsonjoy.com/codegen" "^1.0.0" + "@jsonjoy.com/json-pointer" "^1.0.1" + "@jsonjoy.com/util" "^1.9.0" hyperdyperid "^1.2.0" - thingies "^1.20.0" + thingies "^2.5.0" -"@jsonjoy.com/util@^1.1.2", "@jsonjoy.com/util@^1.3.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.5.0.tgz#6008e35b9d9d8ee27bc4bfaa70c8cbf33a537b4c" - integrity sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA== +"@jsonjoy.com/json-pointer@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz#049cb530ac24e84cba08590c5e36b431c4843408" + integrity sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg== + dependencies: + "@jsonjoy.com/codegen" "^1.0.0" + "@jsonjoy.com/util" "^1.9.0" + +"@jsonjoy.com/util@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.9.0.tgz#7ee95586aed0a766b746cd8d8363e336c3c47c46" + integrity sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ== + dependencies: + "@jsonjoy.com/buffers" "^1.0.0" + "@jsonjoy.com/codegen" "^1.0.0" "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -652,105 +722,110 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.7.tgz#eb5014dfd0b03e7f3ba2eeeff506eed89b028058" integrity sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg== -"@rollup/rollup-android-arm-eabi@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.39.0.tgz#1d8cc5dd3d8ffe569d8f7f67a45c7909828a0f66" - integrity sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA== - -"@rollup/rollup-android-arm64@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.39.0.tgz#9c136034d3d9ed29d0b138c74dd63c5744507fca" - integrity sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ== - -"@rollup/rollup-darwin-arm64@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.39.0.tgz#830d07794d6a407c12b484b8cf71affd4d3800a6" - integrity sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q== - -"@rollup/rollup-darwin-x64@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.39.0.tgz#b26f0f47005c1fa5419a880f323ed509dc8d885c" - integrity sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ== - -"@rollup/rollup-freebsd-arm64@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.39.0.tgz#2b60c81ac01ff7d1bc8df66aee7808b6690c6d19" - integrity sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ== - -"@rollup/rollup-freebsd-x64@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.39.0.tgz#4826af30f4d933d82221289068846c9629cc628c" - integrity sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q== - -"@rollup/rollup-linux-arm-gnueabihf@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.39.0.tgz#a1f4f963d5dcc9e5575c7acf9911824806436bf7" - integrity sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g== - -"@rollup/rollup-linux-arm-musleabihf@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.39.0.tgz#e924b0a8b7c400089146f6278446e6b398b75a06" - integrity sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw== - -"@rollup/rollup-linux-arm64-gnu@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.39.0.tgz#cb43303274ec9a716f4440b01ab4e20c23aebe20" - integrity sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ== - -"@rollup/rollup-linux-arm64-musl@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.39.0.tgz#531c92533ce3d167f2111bfcd2aa1a2041266987" - integrity sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA== - -"@rollup/rollup-linux-loongarch64-gnu@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.39.0.tgz#53403889755d0c37c92650aad016d5b06c1b061a" - integrity sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw== - -"@rollup/rollup-linux-powerpc64le-gnu@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.39.0.tgz#f669f162e29094c819c509e99dbeced58fc708f9" - integrity sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ== - -"@rollup/rollup-linux-riscv64-gnu@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.39.0.tgz#4bab37353b11bcda5a74ca11b99dea929657fd5f" - integrity sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ== - -"@rollup/rollup-linux-riscv64-musl@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.39.0.tgz#4d66be1ce3cfd40a7910eb34dddc7cbd4c2dd2a5" - integrity sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA== - -"@rollup/rollup-linux-s390x-gnu@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.39.0.tgz#7181c329395ed53340a0c59678ad304a99627f6d" - integrity sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA== - -"@rollup/rollup-linux-x64-gnu@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.39.0.tgz#00825b3458094d5c27cb4ed66e88bfe9f1e65f90" - integrity sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA== - -"@rollup/rollup-linux-x64-musl@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.39.0.tgz#81caac2a31b8754186f3acc142953a178fcd6fba" - integrity sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg== - -"@rollup/rollup-win32-arm64-msvc@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.39.0.tgz#3a3f421f5ce9bd99ed20ce1660cce7cee3e9f199" - integrity sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ== - -"@rollup/rollup-win32-ia32-msvc@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.39.0.tgz#a44972d5cdd484dfd9cf3705a884bf0c2b7785a7" - integrity sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ== - -"@rollup/rollup-win32-x64-msvc@4.39.0": - version "4.39.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.39.0.tgz#bfe0214e163f70c4fec1c8f7bb8ce266f4c05b7e" - integrity sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug== +"@rollup/rollup-android-arm-eabi@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz#52d66eba5198155f265f54aed94d2489c49269f6" + integrity sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A== + +"@rollup/rollup-android-arm64@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz#137e8153fc9ce6757531ce300b8d2262299f758e" + integrity sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g== + +"@rollup/rollup-darwin-arm64@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz#d4afd904386d37192cf5ef7345fdb0dd1bac0bc3" + integrity sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q== + +"@rollup/rollup-darwin-x64@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz#6dbe83431fc7cbc09a2b6ed2b9fb7a62dd66ebc2" + integrity sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A== + +"@rollup/rollup-freebsd-arm64@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz#d35afb9f66154b557b3387d12450920f8a954b96" + integrity sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow== + +"@rollup/rollup-freebsd-x64@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz#849303ecdc171a420317ad9166a70af308348f34" + integrity sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog== + +"@rollup/rollup-linux-arm-gnueabihf@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz#ab36199ca613376232794b2f3ba10e2b547a447c" + integrity sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w== + +"@rollup/rollup-linux-arm-musleabihf@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz#f3704bc2eaecd176f558dc47af64197fcac36e8a" + integrity sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw== + +"@rollup/rollup-linux-arm64-gnu@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz#dda0b06fd1daedd00b34395a2fb4aaaa2ed6c32b" + integrity sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg== + +"@rollup/rollup-linux-arm64-musl@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz#a018de66209051dad0c58e689e080326c3dd15b0" + integrity sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ== + +"@rollup/rollup-linux-loong64-gnu@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz#6e514f09988615e0c98fa5a34a88a30fec64d969" + integrity sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw== + +"@rollup/rollup-linux-ppc64-gnu@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz#9b2efebc7b4a1951e684a895fdee0fef26319e0d" + integrity sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag== + +"@rollup/rollup-linux-riscv64-gnu@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz#a7104270e93d75789d1ba857b2c68ddf61f24f68" + integrity sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ== + +"@rollup/rollup-linux-riscv64-musl@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz#42d153f734a7b9fcacd764cc9bee6c207dca4db6" + integrity sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw== + +"@rollup/rollup-linux-s390x-gnu@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz#826ad73099f6fd57c083dc5329151b25404bc67d" + integrity sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w== + +"@rollup/rollup-linux-x64-gnu@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz#b9ec17bf0ca3f737d0895fca2115756674342142" + integrity sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA== + +"@rollup/rollup-linux-x64-musl@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz#29fe0adb45a1d99042f373685efbac9cdd5354d9" + integrity sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw== + +"@rollup/rollup-openharmony-arm64@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz#29648f11e202736b74413f823b71e339e3068d60" + integrity sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA== + +"@rollup/rollup-win32-arm64-msvc@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz#91e7edec80542fd81ab1c2581a91403ac63458ae" + integrity sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA== + +"@rollup/rollup-win32-ia32-msvc@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz#9b7cd9779f1147a3e8d3ddad432ae64dd222c4e9" + integrity sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA== + +"@rollup/rollup-win32-x64-msvc@4.50.2": + version "4.50.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz#40ecd1357526fe328c7af704a283ee8533ca7ad6" + integrity sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA== "@rtsao/scc@^1.1.0": version "1.1.0" @@ -859,11 +934,6 @@ resolved "https://registry.yarnpkg.com/@secretlint/types/-/types-10.2.1.tgz#018f252a3754a9ff2371b3e132226d281be8515b" integrity sha512-F5k1qpoMoUe7rrZossOBgJ3jWKv/FGDBZIwepqnefgPmNienBdInxhtZeXiGwjcxXHVhsdgp6I5Fi/M8PMgwcw== -"@sinclair/typebox@^0.27.8": - version "0.27.8" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" - integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== - "@sindresorhus/merge-streams@^2.1.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958" @@ -921,22 +991,17 @@ resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== -"@types/chai-subset@^1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94" - integrity sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw== +"@types/chai@^5.2.2": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.2.tgz#6f14cea18180ffc4416bc0fd12be05fdd73bdd6b" + integrity sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg== dependencies: - "@types/chai" "*" + "@types/deep-eql" "*" -"@types/chai@*": - version "4.3.4" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4" - integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw== - -"@types/chai@^4.3.5": - version "4.3.6" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.6.tgz#7b489e8baf393d5dd1266fb203ddd4ea941259e6" - integrity sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw== +"@types/deep-eql@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" + integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== "@types/eslint-scope@^3.7.7": version "3.7.7" @@ -954,11 +1019,16 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@1.0.7", "@types/estree@^1.0.6": +"@types/estree@*", "@types/estree@^1.0.6": version "1.0.7" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== +"@types/estree@1.0.8", "@types/estree@^1.0.0": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + "@types/eventsource@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/eventsource/-/eventsource-3.0.0.tgz#6b1b50c677032fd3be0b5c322e8ae819b3df62eb" @@ -1190,48 +1260,85 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vitest/expect@0.34.6": - version "0.34.6" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.34.6.tgz#608a7b7a9aa3de0919db99b4cc087340a03ea77e" - integrity sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw== +"@vitest/coverage-v8@^3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz#a2d8d040288c1956a1c7d0a0e2cdcfc7a3319f13" + integrity sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ== dependencies: - "@vitest/spy" "0.34.6" - "@vitest/utils" "0.34.6" - chai "^4.3.10" - -"@vitest/runner@0.34.6": - version "0.34.6" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.34.6.tgz#6f43ca241fc96b2edf230db58bcde5b974b8dcaf" - integrity sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ== - dependencies: - "@vitest/utils" "0.34.6" - p-limit "^4.0.0" - pathe "^1.1.1" - -"@vitest/snapshot@0.34.6": - version "0.34.6" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.34.6.tgz#b4528cf683b60a3e8071cacbcb97d18b9d5e1d8b" - integrity sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w== - dependencies: - magic-string "^0.30.1" - pathe "^1.1.1" - pretty-format "^29.5.0" - -"@vitest/spy@0.34.6": - version "0.34.6" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.34.6.tgz#b5e8642a84aad12896c915bce9b3cc8cdaf821df" - integrity sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ== - dependencies: - tinyspy "^2.1.1" - -"@vitest/utils@0.34.6": - version "0.34.6" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.34.6.tgz#38a0a7eedddb8e7291af09a2409cb8a189516968" - integrity sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A== - dependencies: - diff-sequences "^29.4.3" - loupe "^2.3.6" - pretty-format "^29.5.0" + "@ampproject/remapping" "^2.3.0" + "@bcoe/v8-coverage" "^1.0.2" + ast-v8-to-istanbul "^0.3.3" + debug "^4.4.1" + istanbul-lib-coverage "^3.2.2" + istanbul-lib-report "^3.0.1" + istanbul-lib-source-maps "^5.0.6" + istanbul-reports "^3.1.7" + magic-string "^0.30.17" + magicast "^0.3.5" + std-env "^3.9.0" + test-exclude "^7.0.1" + tinyrainbow "^2.0.0" + +"@vitest/expect@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.2.4.tgz#8362124cd811a5ee11c5768207b9df53d34f2433" + integrity sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig== + dependencies: + "@types/chai" "^5.2.2" + "@vitest/spy" "3.2.4" + "@vitest/utils" "3.2.4" + chai "^5.2.0" + tinyrainbow "^2.0.0" + +"@vitest/mocker@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.2.4.tgz#4471c4efbd62db0d4fa203e65cc6b058a85cabd3" + integrity sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ== + dependencies: + "@vitest/spy" "3.2.4" + estree-walker "^3.0.3" + magic-string "^0.30.17" + +"@vitest/pretty-format@3.2.4", "@vitest/pretty-format@^3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.2.4.tgz#3c102f79e82b204a26c7a5921bf47d534919d3b4" + integrity sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA== + dependencies: + tinyrainbow "^2.0.0" + +"@vitest/runner@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.2.4.tgz#5ce0274f24a971f6500f6fc166d53d8382430766" + integrity sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ== + dependencies: + "@vitest/utils" "3.2.4" + pathe "^2.0.3" + strip-literal "^3.0.0" + +"@vitest/snapshot@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.2.4.tgz#40a8bc0346ac0aee923c0eefc2dc005d90bc987c" + integrity sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ== + dependencies: + "@vitest/pretty-format" "3.2.4" + magic-string "^0.30.17" + pathe "^2.0.3" + +"@vitest/spy@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.2.4.tgz#cc18f26f40f3f028da6620046881f4e4518c2599" + integrity sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw== + dependencies: + tinyspy "^4.0.3" + +"@vitest/utils@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.2.4.tgz#c0813bc42d99527fb8c5b138c7a88516bca46fea" + integrity sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA== + dependencies: + "@vitest/pretty-format" "3.2.4" + loupe "^3.1.4" + tinyrainbow "^2.0.0" "@vscode/test-cli@^0.0.11": version "0.0.11" @@ -1507,17 +1614,12 @@ acorn-jsx@^5.2.0, acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== - acorn@^7.1.1: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.10.0, acorn@^8.14.0, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.14.0, acorn@^8.8.2, acorn@^8.9.0: version "8.14.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== @@ -1631,11 +1733,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - ansi-styles@^6.1.0, ansi-styles@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" @@ -1765,10 +1862,10 @@ arraybuffer.prototype.slice@^1.0.3: is-array-buffer "^3.0.4" is-shared-array-buffer "^1.0.2" -assertion-error@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" - integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== ast-types@^0.13.4: version "0.13.4" @@ -1777,6 +1874,15 @@ ast-types@^0.13.4: dependencies: tslib "^2.0.1" +ast-v8-to-istanbul@^0.3.3: + version "0.3.5" + resolved "https://registry.yarnpkg.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.5.tgz#9fba217c272dd8c2615603da5de3e1a460b4b9af" + integrity sha512-9SdXjNheSiE8bALAQCQQuT6fgQaoxJh7IRYrRGZ8/9nv8WhJeC1aXAwN8TbaOssGOukUvyvnkgD9+Yuykvl1aA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.30" + estree-walker "^3.0.3" + js-tokens "^9.0.1" + astral-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" @@ -2072,18 +2178,16 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== -chai@^4.3.10: - version "4.3.10" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.10.tgz#d784cec635e3b7e2ffb66446a63b4e33bd390384" - integrity sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g== +chai@^5.2.0: + version "5.3.3" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.3.3.tgz#dd3da955e270916a4bd3f625f4b919996ada7e06" + integrity sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw== dependencies: - assertion-error "^1.1.0" - check-error "^1.0.3" - deep-eql "^4.1.3" - get-func-name "^2.0.2" - loupe "^2.3.6" - pathval "^1.1.1" - type-detect "^4.0.8" + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" chainsaw@~0.1.0: version "0.1.0" @@ -2149,12 +2253,10 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -check-error@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" - integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== - dependencies: - get-func-name "^2.0.2" +check-error@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" + integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== cheerio-select@^2.1.0: version "2.1.0" @@ -2498,12 +2600,10 @@ decompress-response@^6.0.0: dependencies: mimic-response "^3.1.0" -deep-eql@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" - integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== - dependencies: - type-detect "^4.0.0" +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== deep-extend@^0.6.0: version "0.6.0" @@ -2613,11 +2713,6 @@ detect-newline@4.0.1, detect-newline@^4.0.1: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-4.0.1.tgz#fcefdb5713e1fb8cb2839b8b6ee22e6716ab8f23" integrity sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog== -diff-sequences@^29.4.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" - integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== - diff@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" @@ -2928,6 +3023,11 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1" integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q== +es-module-lexer@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + es-object-atoms@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" @@ -2998,34 +3098,37 @@ es6-error@^4.0.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -esbuild@^0.21.3: - version "0.21.5" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" - integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== +esbuild@^0.25.0: + version "0.25.9" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.9.tgz#15ab8e39ae6cdc64c24ff8a2c0aef5b3fd9fa976" + integrity sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g== optionalDependencies: - "@esbuild/aix-ppc64" "0.21.5" - "@esbuild/android-arm" "0.21.5" - "@esbuild/android-arm64" "0.21.5" - "@esbuild/android-x64" "0.21.5" - "@esbuild/darwin-arm64" "0.21.5" - "@esbuild/darwin-x64" "0.21.5" - "@esbuild/freebsd-arm64" "0.21.5" - "@esbuild/freebsd-x64" "0.21.5" - "@esbuild/linux-arm" "0.21.5" - "@esbuild/linux-arm64" "0.21.5" - "@esbuild/linux-ia32" "0.21.5" - "@esbuild/linux-loong64" "0.21.5" - "@esbuild/linux-mips64el" "0.21.5" - "@esbuild/linux-ppc64" "0.21.5" - "@esbuild/linux-riscv64" "0.21.5" - "@esbuild/linux-s390x" "0.21.5" - "@esbuild/linux-x64" "0.21.5" - "@esbuild/netbsd-x64" "0.21.5" - "@esbuild/openbsd-x64" "0.21.5" - "@esbuild/sunos-x64" "0.21.5" - "@esbuild/win32-arm64" "0.21.5" - "@esbuild/win32-ia32" "0.21.5" - "@esbuild/win32-x64" "0.21.5" + "@esbuild/aix-ppc64" "0.25.9" + "@esbuild/android-arm" "0.25.9" + "@esbuild/android-arm64" "0.25.9" + "@esbuild/android-x64" "0.25.9" + "@esbuild/darwin-arm64" "0.25.9" + "@esbuild/darwin-x64" "0.25.9" + "@esbuild/freebsd-arm64" "0.25.9" + "@esbuild/freebsd-x64" "0.25.9" + "@esbuild/linux-arm" "0.25.9" + "@esbuild/linux-arm64" "0.25.9" + "@esbuild/linux-ia32" "0.25.9" + "@esbuild/linux-loong64" "0.25.9" + "@esbuild/linux-mips64el" "0.25.9" + "@esbuild/linux-ppc64" "0.25.9" + "@esbuild/linux-riscv64" "0.25.9" + "@esbuild/linux-s390x" "0.25.9" + "@esbuild/linux-x64" "0.25.9" + "@esbuild/netbsd-arm64" "0.25.9" + "@esbuild/netbsd-x64" "0.25.9" + "@esbuild/openbsd-arm64" "0.25.9" + "@esbuild/openbsd-x64" "0.25.9" + "@esbuild/openharmony-arm64" "0.25.9" + "@esbuild/sunos-x64" "0.25.9" + "@esbuild/win32-arm64" "0.25.9" + "@esbuild/win32-ia32" "0.25.9" + "@esbuild/win32-x64" "0.25.9" escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" @@ -3308,6 +3411,13 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -3335,6 +3445,11 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +expect-type@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.2.2.tgz#c030a329fb61184126c8447585bc75a7ec6fbff3" + integrity sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA== + extend@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -3427,6 +3542,11 @@ fdir@^6.4.4: resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.6.tgz#2b268c0232697063111bbf3f64810a2a741ba281" integrity sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w== +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -3670,11 +3790,6 @@ get-east-asian-width@^1.0.0: resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389" integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== -get-func-name@^2.0.0, get-func-name@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" - integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== - get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: version "1.2.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" @@ -3796,12 +3911,17 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob-to-regex.js@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz#f71cc9cb8441471a9318626160bc8a35e1306b21" + integrity sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg== + glob-to-regexp@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^10.3.10, glob@^10.4.2, glob@^10.4.5: +glob@^10.3.10, glob@^10.4.1, glob@^10.4.2, glob@^10.4.5: version "10.4.5" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== @@ -4539,6 +4659,11 @@ istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== +istanbul-lib-coverage@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + istanbul-lib-hook@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz#8f84c9434888cc6b1d0a9d7092a76d239ebf0cc6" @@ -4596,6 +4721,15 @@ istanbul-lib-source-maps@^4.0.0: istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" +istanbul-lib-source-maps@^5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz#acaef948df7747c8eb5fbf1265cb980f6353a441" + integrity sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A== + dependencies: + "@jridgewell/trace-mapping" "^0.3.23" + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + istanbul-reports@^3.0.2: version "3.1.5" resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.5.tgz#cc9a6ab25cb25659810e4785ed9d9fb742578bae" @@ -4612,6 +4746,14 @@ istanbul-reports@^3.1.6: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +istanbul-reports@^3.1.7: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93" + integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + istextorbinary@^9.5.0: version "9.5.0" resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-9.5.0.tgz#e6e13febf1c1685100ae264809a4f8f46e01dfd3" @@ -4651,6 +4793,11 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-tokens@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-9.0.1.tgz#2ec43964658435296f6761b34e10671c2d9527f4" + integrity sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ== + js-yaml@^3.13.1, js-yaml@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" @@ -4833,11 +4980,6 @@ loader-runner@^4.2.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== -local-pkg@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.4.3.tgz#0ff361ab3ae7f1c19113d9bb97b98b905dbc4963" - integrity sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g== - locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -4933,12 +5075,10 @@ longest-streak@^2.0.1: resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== -loupe@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53" - integrity sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA== - dependencies: - get-func-name "^2.0.0" +loupe@^3.1.0, loupe@^3.1.4: + version "3.2.1" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.2.1.tgz#0095cf56dc5b7a9a7c08ff5b1a8796ec8ad17e76" + integrity sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ== lru-cache@^10.0.1: version "10.4.3" @@ -4974,12 +5114,21 @@ lru-cache@^7.14.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== -magic-string@^0.30.1: - version "0.30.4" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.4.tgz#c2c683265fc18dda49b56fc7318d33ca0332c98c" - integrity sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg== +magic-string@^0.30.17: + version "0.30.19" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.19.tgz#cebe9f104e565602e5d2098c5f2e79a77cc86da9" + integrity sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw== dependencies: - "@jridgewell/sourcemap-codec" "^1.4.15" + "@jridgewell/sourcemap-codec" "^1.5.5" + +magicast@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.3.5.tgz#8301c3c7d66704a0771eb1bad74274f0ec036739" + integrity sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ== + dependencies: + "@babel/parser" "^7.25.4" + "@babel/types" "^7.25.4" + source-map-js "^1.2.0" make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" @@ -5056,14 +5205,16 @@ mdurl@^2.0.0: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== -memfs@^4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.17.1.tgz#3112332cbc2b055da3f1c0ba1fd29fdcb863621a" - integrity sha512-thuTRd7F4m4dReCIy7vv4eNYnU6XI/tHMLSMMHLiortw/Y0QxqKtinG523U2aerzwYWGi606oBP4oMPy4+edag== +memfs@^4.46.0: + version "4.46.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.46.0.tgz#7b110f7a47cdf28b524072b9dd028c9752e4a29c" + integrity sha512-//IxqL9OO/WMpm2kE2aq+y7vO7/xS9xgVIbFM8RUIfW7TY7lowtnuS1j9MwLGm0OwcHUa4p8Bp+40W7f1BiWGQ== dependencies: - "@jsonjoy.com/json-pack" "^1.0.3" - "@jsonjoy.com/util" "^1.3.0" - tree-dump "^1.0.1" + "@jsonjoy.com/json-pack" "^1.11.0" + "@jsonjoy.com/util" "^1.9.0" + glob-to-regex.js "^1.0.1" + thingies "^2.5.0" + tree-dump "^1.0.3" tslib "^2.0.0" merge-stream@^2.0.0: @@ -5166,16 +5317,6 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: dependencies: minimist "^1.2.6" -mlly@^1.2.0, mlly@^1.4.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.2.tgz#7cf406aa319ff6563d25da6b36610a93f2a8007e" - integrity sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg== - dependencies: - acorn "^8.10.0" - pathe "^1.1.1" - pkg-types "^1.0.3" - ufo "^1.3.0" - mocha@^11.1.0: version "11.7.2" resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.7.2.tgz#3c0079fe5cc2f8ea86d99124debcc42bb1ab22b5" @@ -5212,7 +5353,7 @@ mute-stream@0.0.8, mute-stream@~0.0.4: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nanoid@^3.3.8: +nanoid@^3.3.11: version "3.3.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== @@ -5495,13 +5636,6 @@ p-limit@^3.0.2: dependencies: yocto-queue "^0.1.0" -p-limit@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" - integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== - dependencies: - yocto-queue "^1.0.0" - p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" @@ -5683,20 +5817,15 @@ path-type@^6.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-6.0.0.tgz#2f1bb6791a91ce99194caede5d6c5920ed81eb51" integrity sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ== -pathe@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.0.tgz#e2e13f6c62b31a3289af4ba19886c230f295ec03" - integrity sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w== - -pathe@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a" - integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q== +pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== -pathval@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" - integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== +pathval@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.1.tgz#8855c5a2899af072d6ac05d11e46045ad0dc605d" + integrity sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ== pend@~1.2.0: version "1.2.0" @@ -5723,6 +5852,11 @@ picomatch@^4.0.2: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== +picomatch@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -5730,15 +5864,6 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -pkg-types@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.3.tgz#988b42ab19254c01614d13f4f65a2cfc7880f868" - integrity sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A== - dependencies: - jsonc-parser "^3.2.0" - mlly "^1.2.0" - pathe "^1.1.0" - plur@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/plur/-/plur-3.1.1.tgz#60267967866a8d811504fe58f2faaba237546a5b" @@ -5761,12 +5886,12 @@ possible-typed-array-names@^1.0.0: resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== -postcss@^8.4.43: - version "8.5.3" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb" - integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== +postcss@^8.5.6: + version "8.5.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== dependencies: - nanoid "^3.3.8" + nanoid "^3.3.11" picocolors "^1.1.1" source-map-js "^1.2.1" @@ -5815,15 +5940,6 @@ pretty-bytes@^7.0.0: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-7.0.0.tgz#8652cbf0aa81daeeaf72802e0fd059e5e1046cdb" integrity sha512-U5otLYPR3L0SVjHGrkEUx5mf7MxV2ceXeE7VwWPk+hyzC5drNohsOGNPDZqxCqyX1lkbEN4kl1LiI8QFd7r0ZA== -pretty-format@^29.5.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" - integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== - dependencies: - "@jest/schemas" "^29.6.3" - ansi-styles "^5.0.0" - react-is "^18.0.0" - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -5917,11 +6033,6 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-is@^18.0.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" - integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== - read-pkg@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-9.0.1.tgz#b1b81fb15104f5dbb121b6bbdee9bbc9739f569b" @@ -6668,33 +6779,34 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rollup@^4.20.0: - version "4.39.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.39.0.tgz#9dc1013b70c0e2cb70ef28350142e9b81b3f640c" - integrity sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g== +rollup@^4.43.0: + version "4.50.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.50.2.tgz#938d898394939f3386d1e367ee6410a796b8f268" + integrity sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w== dependencies: - "@types/estree" "1.0.7" + "@types/estree" "1.0.8" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.39.0" - "@rollup/rollup-android-arm64" "4.39.0" - "@rollup/rollup-darwin-arm64" "4.39.0" - "@rollup/rollup-darwin-x64" "4.39.0" - "@rollup/rollup-freebsd-arm64" "4.39.0" - "@rollup/rollup-freebsd-x64" "4.39.0" - "@rollup/rollup-linux-arm-gnueabihf" "4.39.0" - "@rollup/rollup-linux-arm-musleabihf" "4.39.0" - "@rollup/rollup-linux-arm64-gnu" "4.39.0" - "@rollup/rollup-linux-arm64-musl" "4.39.0" - "@rollup/rollup-linux-loongarch64-gnu" "4.39.0" - "@rollup/rollup-linux-powerpc64le-gnu" "4.39.0" - "@rollup/rollup-linux-riscv64-gnu" "4.39.0" - "@rollup/rollup-linux-riscv64-musl" "4.39.0" - "@rollup/rollup-linux-s390x-gnu" "4.39.0" - "@rollup/rollup-linux-x64-gnu" "4.39.0" - "@rollup/rollup-linux-x64-musl" "4.39.0" - "@rollup/rollup-win32-arm64-msvc" "4.39.0" - "@rollup/rollup-win32-ia32-msvc" "4.39.0" - "@rollup/rollup-win32-x64-msvc" "4.39.0" + "@rollup/rollup-android-arm-eabi" "4.50.2" + "@rollup/rollup-android-arm64" "4.50.2" + "@rollup/rollup-darwin-arm64" "4.50.2" + "@rollup/rollup-darwin-x64" "4.50.2" + "@rollup/rollup-freebsd-arm64" "4.50.2" + "@rollup/rollup-freebsd-x64" "4.50.2" + "@rollup/rollup-linux-arm-gnueabihf" "4.50.2" + "@rollup/rollup-linux-arm-musleabihf" "4.50.2" + "@rollup/rollup-linux-arm64-gnu" "4.50.2" + "@rollup/rollup-linux-arm64-musl" "4.50.2" + "@rollup/rollup-linux-loong64-gnu" "4.50.2" + "@rollup/rollup-linux-ppc64-gnu" "4.50.2" + "@rollup/rollup-linux-riscv64-gnu" "4.50.2" + "@rollup/rollup-linux-riscv64-musl" "4.50.2" + "@rollup/rollup-linux-s390x-gnu" "4.50.2" + "@rollup/rollup-linux-x64-gnu" "4.50.2" + "@rollup/rollup-linux-x64-musl" "4.50.2" + "@rollup/rollup-openharmony-arm64" "4.50.2" + "@rollup/rollup-win32-arm64-msvc" "4.50.2" + "@rollup/rollup-win32-ia32-msvc" "4.50.2" + "@rollup/rollup-win32-x64-msvc" "4.50.2" fsevents "~2.3.2" run-applescript@^7.0.0: @@ -7008,7 +7120,7 @@ sort-package-json@^3.0.0: sort-object-keys "^1.1.3" tinyglobby "^0.2.12" -source-map-js@^1.2.1: +source-map-js@^1.2.0, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== @@ -7089,10 +7201,10 @@ state-toggle@^1.0.0: resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ== -std-env@^3.3.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.4.3.tgz#326f11db518db751c83fd58574f449b7c3060910" - integrity sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q== +std-env@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1" + integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw== stdin-discarder@^0.2.2: version "0.2.2" @@ -7263,12 +7375,12 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== -strip-literal@^1.0.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-1.3.0.tgz#db3942c2ec1699e6836ad230090b84bb458e3a07" - integrity sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg== +strip-literal@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-3.0.0.tgz#ce9c452a91a0af2876ed1ae4e583539a353df3fc" + integrity sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA== dependencies: - acorn "^8.10.0" + js-tokens "^9.0.1" structured-source@^4.0.0: version "4.0.0" @@ -7408,6 +7520,15 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +test-exclude@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-7.0.1.tgz#20b3ba4906ac20994e275bbcafd68d510264c2a2" + integrity sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^10.4.1" + minimatch "^9.0.4" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -7420,20 +7541,25 @@ textextensions@^6.11.0: dependencies: editions "^6.21.0" -thingies@^1.20.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/thingies/-/thingies-1.21.0.tgz#e80fbe58fd6fdaaab8fad9b67bd0a5c943c445c1" - integrity sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g== +thingies@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/thingies/-/thingies-2.5.0.tgz#5f7b882c933b85989f8466b528a6247a6881e04f" + integrity sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw== through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== -tinybench@^2.5.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.5.1.tgz#3408f6552125e53a5a48adee31261686fd71587e" - integrity sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg== +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinyexec@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" + integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== tinyglobby@^0.2.12: version "0.2.14" @@ -7443,15 +7569,28 @@ tinyglobby@^0.2.12: fdir "^6.4.4" picomatch "^4.0.2" -tinypool@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.7.0.tgz#88053cc99b4a594382af23190c609d93fddf8021" - integrity sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww== +tinyglobby@^0.2.14, tinyglobby@^0.2.15: + version "0.2.15" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" + integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.3" -tinyspy@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.0.tgz#9dc04b072746520b432f77ea2c2d17933de5d6ce" - integrity sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg== +tinypool@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591" + integrity sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg== + +tinyrainbow@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294" + integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== + +tinyspy@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-4.0.3.tgz#d1d0f0602f4c15f1aae083a34d6d0df3363b1b52" + integrity sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A== tmp@^0.0.33: version "0.0.33" @@ -7477,10 +7616,10 @@ to-regex-range@^5.0.1: resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ== -tree-dump@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.2.tgz#c460d5921caeb197bde71d0e9a7b479848c5b8ac" - integrity sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ== +tree-dump@^1.0.3: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.1.0.tgz#ab29129169dc46004414f5a9d4a3c6e89f13e8a4" + integrity sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA== trim-trailing-lines@^1.0.0: version "1.1.4" @@ -7559,11 +7698,6 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@^4.0.0, type-detect@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" - integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== - type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -7698,11 +7832,6 @@ uc.micro@^2.0.0, uc.micro@^2.1.0: resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== -ufo@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.1.tgz#e085842f4627c41d4c1b60ebea1f75cdab4ce86b" - integrity sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw== - unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -7924,58 +8053,59 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" -vite-node@0.34.6: - version "0.34.6" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.34.6.tgz#34d19795de1498562bf21541a58edcd106328a17" - integrity sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA== +vite-node@3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07" + integrity sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg== dependencies: cac "^6.7.14" - debug "^4.3.4" - mlly "^1.4.0" - pathe "^1.1.1" - picocolors "^1.0.0" - vite "^3.0.0 || ^4.0.0 || ^5.0.0-0" - -"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0": - version "5.4.19" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.19.tgz#20efd060410044b3ed555049418a5e7d1998f959" - integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA== - dependencies: - esbuild "^0.21.3" - postcss "^8.4.43" - rollup "^4.20.0" + debug "^4.4.1" + es-module-lexer "^1.7.0" + pathe "^2.0.3" + vite "^5.0.0 || ^6.0.0 || ^7.0.0-0" + +"vite@^5.0.0 || ^6.0.0 || ^7.0.0-0": + version "7.1.5" + resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.5.tgz#4dbcb48c6313116689be540466fc80faa377be38" + integrity sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ== + dependencies: + esbuild "^0.25.0" + fdir "^6.5.0" + picomatch "^4.0.3" + postcss "^8.5.6" + rollup "^4.43.0" + tinyglobby "^0.2.15" optionalDependencies: fsevents "~2.3.3" -vitest@^0.34.6: - version "0.34.6" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.34.6.tgz#44880feeeef493c04b7f795ed268f24a543250d7" - integrity sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q== - dependencies: - "@types/chai" "^4.3.5" - "@types/chai-subset" "^1.3.3" - "@types/node" "*" - "@vitest/expect" "0.34.6" - "@vitest/runner" "0.34.6" - "@vitest/snapshot" "0.34.6" - "@vitest/spy" "0.34.6" - "@vitest/utils" "0.34.6" - acorn "^8.9.0" - acorn-walk "^8.2.0" - cac "^6.7.14" - chai "^4.3.10" - debug "^4.3.4" - local-pkg "^0.4.3" - magic-string "^0.30.1" - pathe "^1.1.1" - picocolors "^1.0.0" - std-env "^3.3.3" - strip-literal "^1.0.1" - tinybench "^2.5.0" - tinypool "^0.7.0" - vite "^3.1.0 || ^4.0.0 || ^5.0.0-0" - vite-node "0.34.6" - why-is-node-running "^2.2.2" +vitest@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.2.4.tgz#0637b903ad79d1539a25bc34c0ed54b5c67702ea" + integrity sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A== + dependencies: + "@types/chai" "^5.2.2" + "@vitest/expect" "3.2.4" + "@vitest/mocker" "3.2.4" + "@vitest/pretty-format" "^3.2.4" + "@vitest/runner" "3.2.4" + "@vitest/snapshot" "3.2.4" + "@vitest/spy" "3.2.4" + "@vitest/utils" "3.2.4" + chai "^5.2.0" + debug "^4.4.1" + expect-type "^1.2.1" + magic-string "^0.30.17" + pathe "^2.0.3" + picomatch "^4.0.2" + std-env "^3.9.0" + tinybench "^2.9.0" + tinyexec "^0.3.2" + tinyglobby "^0.2.14" + tinypool "^1.1.1" + tinyrainbow "^2.0.0" + vite "^5.0.0 || ^6.0.0 || ^7.0.0-0" + vite-node "3.2.4" + why-is-node-running "^2.3.0" vscode-test@^1.5.0: version "1.6.1" @@ -8119,10 +8249,10 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -why-is-node-running@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.2.2.tgz#4185b2b4699117819e7154594271e7e344c9973e" - integrity sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA== +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== dependencies: siginfo "^2.0.0" stackback "0.0.2" @@ -8357,11 +8487,6 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -yocto-queue@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" - integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== - zod@^3.25.65: version "3.25.65" resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.65.tgz#190cb604e1b45e0f789a315f65463953d4d4beee" From 6eca6ec143b2c1300c22d5bd1410ba33c613371f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 12:18:17 -0800 Subject: [PATCH 05/45] chore(deps-dev): bump @types/node-forge from 1.3.11 to 1.3.14 (#576) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9fb96fcb..d349c95d 100644 --- a/package.json +++ b/package.json @@ -329,7 +329,7 @@ "@types/eventsource": "^3.0.0", "@types/glob": "^7.1.3", "@types/node": "^22.14.1", - "@types/node-forge": "^1.3.11", + "@types/node-forge": "^1.3.14", "@types/ua-parser-js": "0.7.36", "@types/vscode": "^1.73.0", "@types/ws": "^8.18.1", diff --git a/yarn.lock b/yarn.lock index 62565608..38603017 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1074,10 +1074,10 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.10.tgz#91f62905e8d23cbd66225312f239454a23bebfa0" integrity sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q== -"@types/node-forge@^1.3.11": - version "1.3.11" - resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" - integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== +"@types/node-forge@^1.3.14": + version "1.3.14" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.14.tgz#006c2616ccd65550560c2757d8472eb6d3ecea0b" + integrity sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw== dependencies: "@types/node" "*" From 84ee1d7e9cfea0932d9fbb5e3068ed96c00081f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:16:49 +0300 Subject: [PATCH 06/45] chore(deps): bump tar-fs from 2.1.3 to 2.1.4 (#596) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 38603017..8fe29eaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7462,9 +7462,9 @@ tapable@^2.1.1, tapable@^2.2.0: integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== tar-fs@^2.0.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92" - integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg== + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930" + integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ== dependencies: chownr "^1.1.1" mkdirp-classic "^0.5.2" From 8324a07a978c3fd2a3628fe51f3b69b803aa7b10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:39:22 +0300 Subject: [PATCH 07/45] chore(deps): bump actions/checkout from 4 to 5 (#567) --- .github/workflows/ci.yaml | 4 ++-- .github/workflows/release.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a94e7cbe..b731210d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 756a2eaa..a73ce17d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -14,7 +14,7 @@ jobs: package: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: From a4bd95d829ef966b8f2c4f448c259366727400a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:32:41 +0300 Subject: [PATCH 08/45] chore(deps): bump actions/setup-node from 4 to 5 (#579) --- .github/workflows/ci.yaml | 4 ++-- .github/workflows/release.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b731210d..59a03e0a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: "22" @@ -34,7 +34,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: "22" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a73ce17d..a6bf5fa4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: "22" From 43b33599c47b51baee192d61c0ce828ae4ae8b0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:33:31 +0300 Subject: [PATCH 09/45] chore(deps-dev): bump eslint-plugin-prettier from 5.4.1 to 5.5.4 (#572) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d349c95d..fd8bca58 100644 --- a/package.json +++ b/package.json @@ -347,7 +347,7 @@ "eslint-plugin-import": "^2.31.0", "eslint-plugin-md": "^1.0.19", "eslint-plugin-package-json": "^0.40.1", - "eslint-plugin-prettier": "^5.4.1", + "eslint-plugin-prettier": "^5.5.4", "glob": "^10.4.2", "jsonc-eslint-parser": "^2.4.0", "memfs": "^4.46.0", diff --git a/yarn.lock b/yarn.lock index 8fe29eaf..2aae97fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3236,10 +3236,10 @@ eslint-plugin-package-json@^0.40.1: sort-package-json "^3.0.0" validate-npm-package-name "^6.0.0" -eslint-plugin-prettier@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.1.tgz#99b55d7dd70047886b2222fdd853665f180b36af" - integrity sha512-9dF+KuU/Ilkq27A8idRP7N2DH8iUR6qXcjF3FR2wETY21PZdBrIjwCau8oboyGj9b7etWmTGEeM8e7oOed6ZWg== +eslint-plugin-prettier@^5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz#9d61c4ea11de5af704d4edf108c82ccfa7f2e61c" + integrity sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg== dependencies: prettier-linter-helpers "^1.0.0" synckit "^0.11.7" From 9cfb742974d3242ec4be9024cb51310b6bd0834e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:37:21 +0300 Subject: [PATCH 10/45] chore(deps-dev): bump webpack-cli from 5.1.4 to 6.0.1 (#571) --- package.json | 2 +- yarn.lock | 97 +++++++++++++++++++++++----------------------------- 2 files changed, 43 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index fd8bca58..41878cc9 100644 --- a/package.json +++ b/package.json @@ -359,7 +359,7 @@ "vitest": "^3.2.4", "vscode-test": "^1.5.0", "webpack": "^5.99.6", - "webpack-cli": "^5.1.4" + "webpack-cli": "^6.0.1" }, "extensionPack": [ "ms-vscode-remote.remote-ssh" diff --git a/yarn.lock b/yarn.lock index 2aae97fb..a3a699ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -336,10 +336,10 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz#bbe12dca5b4ef983a0d0af4b07b9bc90ea0ababa" integrity sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA== -"@discoveryjs/json-ext@^0.5.0": - version "0.5.7" - resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" - integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@discoveryjs/json-ext@^0.6.1": + version "0.6.3" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz#f13c7c205915eb91ae54c557f5e92bddd8be0e83" + integrity sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ== "@esbuild/aix-ppc64@0.25.9": version "0.25.9" @@ -1584,20 +1584,20 @@ "@webassemblyjs/ast" "1.14.1" "@xtuc/long" "4.2.2" -"@webpack-cli/configtest@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" - integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== +"@webpack-cli/configtest@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-3.0.1.tgz#76ac285b9658fa642ce238c276264589aa2b6b57" + integrity sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA== -"@webpack-cli/info@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" - integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== +"@webpack-cli/info@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-3.0.1.tgz#3cff37fabb7d4ecaab6a8a4757d3826cf5888c63" + integrity sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ== -"@webpack-cli/serve@^2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" - integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== +"@webpack-cli/serve@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-3.0.1.tgz#bd8b1f824d57e30faa19eb78e4c0951056f72f00" + integrity sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg== "@xtuc/ieee754@^1.2.0": version "1.2.0" @@ -2435,11 +2435,6 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" - integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== - commander@^12.1.0: version "12.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" @@ -2486,16 +2481,7 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -cross-spawn@^7.0.6: +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -2849,10 +2835,10 @@ entities@^4.2.0, entities@^4.3.0, entities@^4.4.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== -envinfo@^7.7.3: - version "7.8.1" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" - integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== +envinfo@^7.14.0: + version "7.14.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.14.0.tgz#26dac5db54418f2a4c1159153a0b2ae980838aae" + integrity sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg== environment@^1.0.0: version "1.1.0" @@ -8125,32 +8111,33 @@ watchpack@^2.4.1: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" -webpack-cli@^5.1.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b" - integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg== +webpack-cli@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-6.0.1.tgz#a1ce25da5ba077151afd73adfa12e208e5089207" + integrity sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw== dependencies: - "@discoveryjs/json-ext" "^0.5.0" - "@webpack-cli/configtest" "^2.1.1" - "@webpack-cli/info" "^2.0.2" - "@webpack-cli/serve" "^2.0.5" + "@discoveryjs/json-ext" "^0.6.1" + "@webpack-cli/configtest" "^3.0.1" + "@webpack-cli/info" "^3.0.1" + "@webpack-cli/serve" "^3.0.1" colorette "^2.0.14" - commander "^10.0.1" + commander "^12.1.0" cross-spawn "^7.0.3" - envinfo "^7.7.3" + envinfo "^7.14.0" fastest-levenshtein "^1.0.12" import-local "^3.0.2" interpret "^3.1.1" rechoir "^0.8.0" - webpack-merge "^5.7.3" + webpack-merge "^6.0.1" -webpack-merge@^5.7.3: - version "5.8.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" - integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== +webpack-merge@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-6.0.1.tgz#50c776868e080574725abc5869bd6e4ef0a16c6a" + integrity sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg== dependencies: clone-deep "^4.0.1" - wildcard "^2.0.0" + flat "^5.0.2" + wildcard "^2.0.1" webpack-sources@^3.2.3: version "3.2.3" @@ -8257,10 +8244,10 @@ why-is-node-running@^2.3.0: siginfo "^2.0.0" stackback "0.0.2" -wildcard@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" - integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== +wildcard@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== word-wrap@1.2.5, word-wrap@~1.2.3: version "1.2.5" From 8fccd760084ad7b41facd8e92a24d846767eefb2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:55:33 +0300 Subject: [PATCH 11/45] chore(deps-dev): bump webpack from 5.99.6 to 5.101.3 (#598) --- package.json | 2 +- yarn.lock | 77 ++++++++++++++++++++++------------------------------ 2 files changed, 33 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index 41878cc9..83d6e15d 100644 --- a/package.json +++ b/package.json @@ -358,7 +358,7 @@ "utf-8-validate": "^6.0.5", "vitest": "^3.2.4", "vscode-test": "^1.5.0", - "webpack": "^5.99.6", + "webpack": "^5.101.3", "webpack-cli": "^6.0.1" }, "extensionPack": [ diff --git a/yarn.lock b/yarn.lock index a3a699ed..cae6bd6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1019,12 +1019,7 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@^1.0.6": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" - integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== - -"@types/estree@1.0.8", "@types/estree@^1.0.0": +"@types/estree@*", "@types/estree@1.0.8", "@types/estree@^1.0.0", "@types/estree@^1.0.8": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== @@ -1049,16 +1044,11 @@ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== -"@types/json-schema@*", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/json-schema@^7.0.12": - version "7.0.13" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" - integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ== - "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -1609,6 +1599,11 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +acorn-import-phases@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" + integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ== + acorn-jsx@^5.2.0, acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1619,12 +1614,7 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.14.0, acorn@^8.8.2, acorn@^8.9.0: - version "8.14.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" - integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== - -acorn@^8.5.0: +acorn@^8.15.0, acorn@^8.5.0, acorn@^8.8.2, acorn@^8.9.0: version "8.15.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== @@ -2822,10 +2812,10 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -enhanced-resolve@^5.0.0, enhanced-resolve@^5.15.0, enhanced-resolve@^5.17.1: - version "5.18.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf" - integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg== +enhanced-resolve@^5.0.0, enhanced-resolve@^5.15.0, enhanced-resolve@^5.17.3: + version "5.18.3" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz#9b5f4c5c076b8787c78fe540392ce76a88855b44" + integrity sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -3004,12 +2994,7 @@ es-errors@^1.0.0, es-errors@^1.2.1, es-errors@^1.3.0: resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-module-lexer@^1.2.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1" - integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q== - -es-module-lexer@^1.7.0: +es-module-lexer@^1.2.1, es-module-lexer@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== @@ -6877,10 +6862,10 @@ sax@>=0.6.0: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -schema-utils@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.0.tgz#3b669f04f71ff2dfb5aba7ce2d5a9d79b35622c0" - integrity sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g== +schema-utils@^4.3.0, schema-utils@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.2.tgz#0c10878bf4a73fd2b1dfd14b9462b26788c806ae" + integrity sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ== dependencies: "@types/json-schema" "^7.0.9" ajv "^8.9.0" @@ -8139,25 +8124,27 @@ webpack-merge@^6.0.1: flat "^5.0.2" wildcard "^2.0.1" -webpack-sources@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" - integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== +webpack-sources@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723" + integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg== -webpack@^5.99.6: - version "5.99.6" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.6.tgz#0d6ba7ce1d3609c977f193d2634d54e5cf36379d" - integrity sha512-TJOLrJ6oeccsGWPl7ujCYuc0pIq2cNsuD6GZDma8i5o5Npvcco/z+NKvZSFsP0/x6SShVb0+X2JK/JHUjKY9dQ== +webpack@^5.101.3: + version "5.101.3" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.101.3.tgz#3633b2375bb29ea4b06ffb1902734d977bc44346" + integrity sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A== dependencies: "@types/eslint-scope" "^3.7.7" - "@types/estree" "^1.0.6" + "@types/estree" "^1.0.8" + "@types/json-schema" "^7.0.15" "@webassemblyjs/ast" "^1.14.1" "@webassemblyjs/wasm-edit" "^1.14.1" "@webassemblyjs/wasm-parser" "^1.14.1" - acorn "^8.14.0" + acorn "^8.15.0" + acorn-import-phases "^1.0.3" browserslist "^4.24.0" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.17.1" + enhanced-resolve "^5.17.3" es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" @@ -8167,11 +8154,11 @@ webpack@^5.99.6: loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^4.3.0" + schema-utils "^4.3.2" tapable "^2.1.1" terser-webpack-plugin "^5.3.11" watchpack "^2.4.1" - webpack-sources "^3.2.3" + webpack-sources "^3.3.3" which-boxed-primitive@^1.0.2: version "1.0.2" From ee5d7e00c251acf21ced18e0116676d54a8f7dbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:01:16 +0300 Subject: [PATCH 12/45] chore(deps-dev): bump eslint-plugin-package-json from 0.40.1 to 0.56.3 (#602) --- package.json | 2 +- yarn.lock | 70 +++++++++++++++++++++++++++------------------------- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 83d6e15d..314dee58 100644 --- a/package.json +++ b/package.json @@ -346,7 +346,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-md": "^1.0.19", - "eslint-plugin-package-json": "^0.40.1", + "eslint-plugin-package-json": "^0.56.3", "eslint-plugin-prettier": "^5.5.4", "glob": "^10.4.2", "jsonc-eslint-parser": "^2.4.0", diff --git a/yarn.lock b/yarn.lock index cae6bd6b..f5edf496 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== -"@altano/repository-tools@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@altano/repository-tools/-/repository-tools-1.0.1.tgz#969bb94cc80f8b4d62c7d6956466edc3f3c3817a" - integrity sha512-/FFHQOMp5TZWplkDWbbLIjmANDr9H/FtqUm+hfJMK76OBut0Ht0cNfd0ZXd/6LXf4pWUTzvpgVjcin7EEHSznA== +"@altano/repository-tools@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@altano/repository-tools/-/repository-tools-2.0.1.tgz#22b43b5ee9dde190a055c281059d57ac665128df" + integrity sha512-YE/52CkFtb+YtHPgbWPai7oo5N9AKnMuP5LM+i2AG7G1H2jdYBCO1iDnkDE3dZ3C1MIgckaF+d5PNRulgt0bdw== "@ampproject/remapping@^2.2.0", "@ampproject/remapping@^2.3.0": version "2.3.0" @@ -2674,7 +2674,7 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -detect-indent@7.0.1, detect-indent@^7.0.1: +detect-indent@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-7.0.1.tgz#cbb060a12842b9c4d333f1cac4aa4da1bb66bc25" integrity sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g== @@ -2684,7 +2684,7 @@ detect-libc@^2.0.0: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== -detect-newline@4.0.1, detect-newline@^4.0.1: +detect-newline@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-4.0.1.tgz#fcefdb5713e1fb8cb2839b8b6ee22e6716ab8f23" integrity sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog== @@ -3132,10 +3132,10 @@ eslint-config-prettier@^9.1.0: resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== -eslint-fix-utils@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/eslint-fix-utils/-/eslint-fix-utils-0.3.0.tgz#5643ae3c47c49ab247afc1565b2fe7b64ca4fbab" - integrity sha512-0wAVRhCkSCSu4goaIb05gKjFxTd/FC3Jee0ptvWYHS2gBh1mDhsrFyg6JyK47wvM10az/Ns4BlATbTW9HIoQ+Q== +eslint-fix-utils@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/eslint-fix-utils/-/eslint-fix-utils-0.4.0.tgz#e1085b4f94f41e7448a80b774d8ed5cbbe7f7e31" + integrity sha512-nCEciwqByGxsKiWqZjqK7xfL+7dUX9Pi0UL3J0tOwfxVN9e6Y59UxEt1ZYsc3XH0ce6T1WQM/QU2DbKK/6IG7g== eslint-import-resolver-node@^0.3.9: version "0.3.9" @@ -3191,21 +3191,21 @@ eslint-plugin-md@^1.0.19: remark-preset-lint-markdown-style-guide "^2.1.3" requireindex "~1.1.0" -eslint-plugin-package-json@^0.40.1: - version "0.40.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-package-json/-/eslint-plugin-package-json-0.40.1.tgz#73fb3138840d4de232bb87d228024f62db4d7cda" - integrity sha512-e5BcFpqLORfOZQS+Ygo307b1pCzvhzx+LQgzOd+qi9Uyj3J1UPDMPp5NBjli+l6SD9p9D794aiEwohwbHIPNDA== +eslint-plugin-package-json@^0.56.3: + version "0.56.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-package-json/-/eslint-plugin-package-json-0.56.3.tgz#dcf50aaf3a3bc377396d3df72bb63819b02e8d73" + integrity sha512-ArN3wnOAsduM/6a0egB83DQQfF/4KzxE53U8qcvELCXT929TnBy2IeCli4+in3QSHxcVYSIDa2Y5T2vVAXbe6A== dependencies: - "@altano/repository-tools" "^1.0.0" + "@altano/repository-tools" "^2.0.1" change-case "^5.4.4" - detect-indent "7.0.1" - detect-newline "4.0.1" - eslint-fix-utils "^0.3.0" - package-json-validator "~0.13.1" + detect-indent "^7.0.1" + detect-newline "^4.0.1" + eslint-fix-utils "~0.4.0" + package-json-validator "~0.30.0" semver "^7.5.4" sort-object-keys "^1.1.3" - sort-package-json "^3.0.0" - validate-npm-package-name "^6.0.0" + sort-package-json "^3.3.0" + validate-npm-package-name "^6.0.2" eslint-plugin-prettier@^5.5.4: version "5.5.4" @@ -5675,11 +5675,13 @@ package-json-from-dist@^1.0.0: resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== -package-json-validator@~0.13.1: - version "0.13.3" - resolved "https://registry.yarnpkg.com/package-json-validator/-/package-json-validator-0.13.3.tgz#f661fb1a54643de999133f2c41e90d2f947e88c2" - integrity sha512-/BeP6SFebqXJS27aLrTMjpmF0OZtsptoxYVU9pUGPdUNTc1spFfNcnOOhvT4Cghm1OQ75CyMM11H5jtQbe7bAQ== +package-json-validator@~0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/package-json-validator/-/package-json-validator-0.30.0.tgz#31613a3e4a2455599c7ad3a97f134707f13de1e0" + integrity sha512-gOLW+BBye32t+IB2trIALIcL3DZBy3s4G4ZV6dAgDM+qLs/7jUNOV7iO7PwXqyf+3izI12qHBwtS4kOSJp5Tdg== dependencies: + semver "^7.7.2" + validate-npm-package-license "^3.0.4" yargs "~18.0.0" pako@~1.0.2: @@ -6885,7 +6887,7 @@ secretlint@^10.1.1: globby "^14.1.0" read-pkg "^9.0.1" -semver@7.7.1, semver@^5.1.0, semver@^5.5.0, semver@^6.0.0, semver@^6.1.2, semver@^6.3.1, semver@^7.3.4, semver@^7.3.5, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.2, semver@^7.7.1: +semver@7.7.1, semver@^5.1.0, semver@^5.5.0, semver@^6.0.0, semver@^6.1.2, semver@^6.3.1, semver@^7.3.4, semver@^7.3.5, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.2, semver@^7.7.1, semver@^7.7.2: version "7.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== @@ -7078,10 +7080,10 @@ sort-object-keys@^1.1.3: resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45" integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg== -sort-package-json@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-3.2.1.tgz#889f3bdf43ceeff5fa4278a7c53ae5b1520d287e" - integrity sha512-rTfRdb20vuoAn7LDlEtCqOkYfl2X+Qze6cLbNOzcDpbmKEhJI30tTN44d5shbKJnXsvz24QQhlCm81Bag7EOKg== +sort-package-json@^3.3.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-3.4.0.tgz#98e42b78848c517736b069f8aa4fa322fae56677" + integrity sha512-97oFRRMM2/Js4oEA9LJhjyMlde+2ewpZQf53pgue27UkbEXfHJnDzHlUxQ/DWUkzqmp7DFwJp8D+wi/TYeQhpA== dependencies: detect-indent "^7.0.1" detect-newline "^4.0.1" @@ -7991,10 +7993,10 @@ validate-npm-package-license@^3.0.4: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -validate-npm-package-name@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-6.0.1.tgz#7b928e5fe23996045a6de5b5a22eedb3611264dd" - integrity sha512-OaI//3H0J7ZkR1OqlhGA8cA+Cbk/2xFOQpJOt5+s27/ta9eZwpeervh4Mxh4w0im/kdgktowaqVNR7QOrUd7Yg== +validate-npm-package-name@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz#4e8d2c4d939975a73dd1b7a65e8f08d44c85df96" + integrity sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ== version-range@^4.13.0: version "4.14.0" From c859b8eb180004cc13a544b94629d059e64580eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:07:28 +0300 Subject: [PATCH 13/45] chore(deps-dev): bump typescript from 5.8.3 to 5.9.2 (#600) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 314dee58..95c6cd60 100644 --- a/package.json +++ b/package.json @@ -354,7 +354,7 @@ "nyc": "^17.1.0", "prettier": "^3.5.3", "ts-loader": "^9.5.1", - "typescript": "^5.8.3", + "typescript": "^5.9.2", "utf-8-validate": "^6.0.5", "vitest": "^3.2.4", "vscode-test": "^1.5.0", diff --git a/yarn.lock b/yarn.lock index f5edf496..c2d79036 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7790,10 +7790,10 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^5.8.3: - version "5.8.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" - integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== +typescript@^5.9.2: + version "5.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" + integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== ua-parser-js@1.0.40: version "1.0.40" From 2c7974ce81aabb2b32dc9a670e5eacc71da8de5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:10:46 +0300 Subject: [PATCH 14/45] chore(deps-dev): bump memfs from 4.46.0 to 4.47.0 (#601) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 95c6cd60..9a666aba 100644 --- a/package.json +++ b/package.json @@ -350,7 +350,7 @@ "eslint-plugin-prettier": "^5.5.4", "glob": "^10.4.2", "jsonc-eslint-parser": "^2.4.0", - "memfs": "^4.46.0", + "memfs": "^4.47.0", "nyc": "^17.1.0", "prettier": "^3.5.3", "ts-loader": "^9.5.1", diff --git a/yarn.lock b/yarn.lock index c2d79036..581e7d3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5176,10 +5176,10 @@ mdurl@^2.0.0: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== -memfs@^4.46.0: - version "4.46.0" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.46.0.tgz#7b110f7a47cdf28b524072b9dd028c9752e4a29c" - integrity sha512-//IxqL9OO/WMpm2kE2aq+y7vO7/xS9xgVIbFM8RUIfW7TY7lowtnuS1j9MwLGm0OwcHUa4p8Bp+40W7f1BiWGQ== +memfs@^4.47.0: + version "4.47.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.47.0.tgz#410291da6dcce89a0d6c9cab23b135231a5ed44c" + integrity sha512-Xey8IZA57tfotV/TN4d6BmccQuhFP+CqRiI7TTNdipZdZBzF2WnzUcH//Cudw6X4zJiUbo/LTuU/HPA/iC/pNg== dependencies: "@jsonjoy.com/json-pack" "^1.11.0" "@jsonjoy.com/util" "^1.9.0" From 923298195c12db1e29f8a7c4f3f667bbe0dedd33 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 1 Oct 2025 10:58:27 +0300 Subject: [PATCH 15/45] Add support to CODER_BINARY_DESTINATION environment variable (#597) Fixes #256 --- CHANGELOG.md | 4 ++++ package.json | 2 +- src/commands.ts | 9 ++++++--- src/core/pathResolver.test.ts | 27 +++++++++++++++++++++++++-- src/core/pathResolver.ts | 11 +++++++---- src/extension.ts | 4 +++- src/headers.test.ts | 4 ++-- src/headers.ts | 9 ++++----- 8 files changed, 52 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35649a76..e9da9987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- Support for `CODER_BINARY_DESTINATION` environment variable to set CLI download location (overridden by extension setting `coder.binaryDestination` if configured). + ## [v1.11.0](https://github.com/coder/vscode-coder/releases/tag/v1.11.0) 2025-09-24 ### Changed diff --git a/package.json b/package.json index 9a666aba..7c7b60ca 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "default": "" }, "coder.binaryDestination": { - "markdownDescription": "The full path of the directory into which the Coder CLI will be downloaded. Defaults to the extension's global storage directory.", + "markdownDescription": "The full path of the directory into which the Coder CLI will be downloaded. Defaults to the value of `CODER_BINARY_DESTINATION` if not set, otherwise the extension's global storage directory.", "type": "string", "default": "" }, diff --git a/src/commands.ts b/src/commands.ts index 914adbfc..b9dcf10d 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -102,10 +102,13 @@ export class Commands { * CODER_URL or enter a new one. Undefined means the user aborted. */ private async askURL(selection?: string): Promise { - const defaultURL = - vscode.workspace.getConfiguration().get("coder.defaultUrl") ?? ""; + const defaultURL = vscode.workspace + .getConfiguration() + .get("coder.defaultUrl") + ?.trim(); const quickPick = vscode.window.createQuickPick(); - quickPick.value = selection || defaultURL || process.env.CODER_URL || ""; + quickPick.value = + selection || defaultURL || process.env.CODER_URL?.trim() || ""; quickPick.placeholder = "https://example.coder.com"; quickPick.title = "Enter the URL of your Coder deployment."; diff --git a/src/core/pathResolver.test.ts b/src/core/pathResolver.test.ts index 8216a547..3c331a26 100644 --- a/src/core/pathResolver.test.ts +++ b/src/core/pathResolver.test.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect, beforeEach, vi } from "vitest"; import { MockConfigurationProvider } from "../__mocks__/testHelpers"; import { PathResolver } from "./pathResolver"; @@ -11,6 +11,7 @@ describe("PathResolver", () => { let mockConfig: MockConfigurationProvider; beforeEach(() => { + vi.unstubAllEnvs(); pathResolver = new PathResolver(basePath, codeLogPath); mockConfig = new MockConfigurationProvider(); }); @@ -32,6 +33,7 @@ describe("PathResolver", () => { }); it("should use default path when custom destination is empty or whitespace", () => { + vi.stubEnv("CODER_BINARY_DESTINATION", " "); mockConfig.set("coder.binaryDestination", " "); expect(pathResolver.getBinaryCachePath("deployment")).toBe( path.join(basePath, "deployment", "bin"), @@ -41,7 +43,28 @@ describe("PathResolver", () => { it("should normalize custom paths", () => { mockConfig.set("coder.binaryDestination", "/custom/../binary/./path"); expect(pathResolver.getBinaryCachePath("deployment")).toBe( - path.normalize("/custom/../binary/./path"), + "/binary/path", + ); + }); + + it("should use CODER_BINARY_DESTINATION environment variable with proper precedence", () => { + // Use the global storage when the environment variable and setting are unset/blank + vi.stubEnv("CODER_BINARY_DESTINATION", ""); + mockConfig.set("coder.binaryDestination", ""); + expect(pathResolver.getBinaryCachePath("deployment")).toBe( + path.join(basePath, "deployment", "bin"), + ); + + // Test environment variable takes precedence over global storage + vi.stubEnv("CODER_BINARY_DESTINATION", " /env/binary/path "); + expect(pathResolver.getBinaryCachePath("deployment")).toBe( + "/env/binary/path", + ); + + // Test setting takes precedence over environment variable + mockConfig.set("coder.binaryDestination", " /setting/path "); + expect(pathResolver.getBinaryCachePath("deployment")).toBe( + "/setting/path", ); }); }); diff --git a/src/core/pathResolver.ts b/src/core/pathResolver.ts index 6c1ee7ef..514e64fb 100644 --- a/src/core/pathResolver.ts +++ b/src/core/pathResolver.ts @@ -28,11 +28,14 @@ export class PathResolver { * The caller must ensure this directory exists before use. */ public getBinaryCachePath(label: string): string { - const configPath = vscode.workspace + const settingPath = vscode.workspace .getConfiguration() - .get("coder.binaryDestination"); - return configPath && configPath.trim().length > 0 - ? path.normalize(configPath) + .get("coder.binaryDestination") + ?.trim(); + const binaryPath = + settingPath || process.env.CODER_BINARY_DESTINATION?.trim(); + return binaryPath + ? path.normalize(binaryPath) : path.join(this.getGlobalConfigDir(label), "bin"); } diff --git a/src/extension.ts b/src/extension.ts index bd8a09c6..678ea3b7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -423,7 +423,9 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // Handle autologin, if not already logged in. const cfg = vscode.workspace.getConfiguration(); if (cfg.get("coder.autologin") === true) { - const defaultUrl = cfg.get("coder.defaultUrl") || process.env.CODER_URL; + const defaultUrl = + cfg.get("coder.defaultUrl")?.trim() || + process.env.CODER_URL?.trim(); if (defaultUrl) { vscode.commands.executeCommand( "coder.login", diff --git a/src/headers.test.ts b/src/headers.test.ts index 84c39d36..6f2933a3 100644 --- a/src/headers.test.ts +++ b/src/headers.test.ts @@ -123,9 +123,9 @@ describe("getHeaderCommand", () => { expect(getHeaderCommand(config)).toBeUndefined(); }); - it("should return undefined if coder.headerCommand is not a string", () => { + it("should return undefined if coder.headerCommand is a blank string", () => { const config = { - get: () => 1234, + get: () => " ", } as unknown as WorkspaceConfiguration; expect(getHeaderCommand(config)).toBeUndefined(); diff --git a/src/headers.ts b/src/headers.ts index d259c9e1..1aad4258 100644 --- a/src/headers.ts +++ b/src/headers.ts @@ -19,11 +19,10 @@ export function getHeaderCommand( config: WorkspaceConfiguration, ): string | undefined { const cmd = - config.get("coder.headerCommand") || process.env.CODER_HEADER_COMMAND; - if (!cmd || typeof cmd !== "string") { - return undefined; - } - return cmd; + config.get("coder.headerCommand")?.trim() || + process.env.CODER_HEADER_COMMAND?.trim(); + + return cmd ? cmd : undefined; } export function getHeaderArgs(config: WorkspaceConfiguration): string[] { From 67e85e6106000d6729f20cac5d35b3db1e58df9c Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 1 Oct 2025 11:29:19 +0300 Subject: [PATCH 16/45] Refactor test structure, improve linting, and enhance code organization (#594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This refactor reorganizes the test layout, strengthens linting configuration, and introduces path aliases in tests to improve code maintainability. Test Structure Changes: - Move tests from /src to top-level /test directory - /test/unit: unit tests with mocks - /test/integration: end-to-end tests with real VS Code API - /test/mocks: shared mocks - /test/fixtures: relocated from /fixtures - /test/utils: various test utils - Prevent test file imports in src - Unify fixture path resolution Development Tooling: - Configure ESLint and TypeScript "fix on save" in VS Code - Update ESLint-related dependencies - Address multiple ESLint rule violations (imports, typing) Code Organization: - Add path aliases for tests (@ → /src) - Introduce ServiceContainer to initialize and track services in src/core - Improve .vscodeignore to ship only necessary files --- .eslintrc.json | 56 +- .vscode-test.mjs | 2 +- .vscode/settings.json | 14 + .vscodeignore | 50 +- package.json | 11 +- src/agentMetadataHelper.ts | 7 +- src/api/api-helper.ts | 6 +- src/api/coderApi.ts | 25 +- src/{ => api}/proxy.ts | 0 src/api/utils.ts | 4 +- src/api/workspace.ts | 10 +- src/commands.ts | 46 +- src/core/cliManager.ts | 37 +- src/{ => core}/cliUtils.ts | 0 src/core/container.ts | 69 + src/core/secretsManager.ts | 2 +- src/error.ts | 3 +- src/extension.ts | 47 +- src/featureSet.ts | 2 +- src/globalFlags.ts | 3 +- src/headers.ts | 6 +- src/inbox.ts | 12 +- src/logging/formatters.ts | 3 +- src/logging/httpLogger.ts | 12 +- src/logging/wsLogger.ts | 5 +- src/pgp.ts | 3 +- src/{ => remote}/remote.ts | 74 +- src/{ => remote}/sshConfig.ts | 5 +- src/{ => remote}/sshSupport.ts | 2 +- src/websocket/oneWayWebSocket.ts | 33 +- src/{ => workspace}/workspaceMonitor.ts | 14 +- src/{ => workspace}/workspacesProvider.ts | 21 +- {fixtures => test/fixtures}/bin.bash | 0 {fixtures => test/fixtures}/bin.old.bash | 0 {fixtures => test/fixtures}/pgp/cli | 0 .../fixtures}/pgp/cli.invalid.asc | 0 {fixtures => test/fixtures}/pgp/cli.valid.asc | 0 {fixtures => test/fixtures}/pgp/private.pgp | 0 {fixtures => test/fixtures}/pgp/public.pgp | 0 .../fixtures}/tls/chain-intermediate.crt | 0 .../fixtures}/tls/chain-intermediate.key | 0 .../fixtures}/tls/chain-leaf.crt | 0 .../fixtures}/tls/chain-leaf.key | 0 .../fixtures}/tls/chain-root.crt | 0 .../fixtures}/tls/chain-root.key | 0 {fixtures => test/fixtures}/tls/chain.crt | 0 {fixtures => test/fixtures}/tls/chain.key | 0 {fixtures => test/fixtures}/tls/generate.bash | 0 .../fixtures}/tls/no-signing.crt | 0 .../fixtures}/tls/no-signing.key | 0 .../fixtures}/tls/self-signed.crt | 0 .../fixtures}/tls/self-signed.key | 0 .../integration}/extension.test.ts | 0 {src/__mocks__ => test/mocks}/testHelpers.ts | 26 +- .../mocks}/vscode.runtime.ts | 0 test/tsconfig.json | 10 + {src => test/unit}/core/cliManager.test.ts | 33 +- {src => test/unit/core}/cliUtils.test.ts | 51 +- .../unit}/core/mementoManager.test.ts | 8 +- {src => test/unit}/core/pathResolver.test.ts | 8 +- .../unit}/core/secretsManager.test.ts | 8 +- {src => test/unit}/error.test.ts | 36 +- {src => test/unit}/featureSet.test.ts | 3 +- {src => test/unit}/globalFlags.test.ts | 5 +- {src => test/unit}/headers.test.ts | 9 +- {src => test/unit}/pgp.test.ts | 19 +- {src => test/unit/remote}/sshConfig.test.ts | 4 +- {src => test/unit/remote}/sshSupport.test.ts | 3 +- {src => test/unit}/util.test.ts | 3 +- test/utils/fixtures.ts | 5 + tsconfig.json | 3 +- vitest.config.ts | 17 +- yarn.lock | 1344 +++++++++++++---- 73 files changed, 1584 insertions(+), 595 deletions(-) create mode 100644 .vscode/settings.json rename src/{ => api}/proxy.ts (100%) rename src/{ => core}/cliUtils.ts (100%) create mode 100644 src/core/container.ts rename src/{ => remote}/remote.ts (95%) rename src/{ => remote}/sshConfig.ts (99%) rename src/{ => remote}/sshSupport.ts (99%) rename src/{ => workspace}/workspaceMonitor.ts (94%) rename src/{ => workspace}/workspacesProvider.ts (97%) rename {fixtures => test/fixtures}/bin.bash (100%) rename {fixtures => test/fixtures}/bin.old.bash (100%) rename {fixtures => test/fixtures}/pgp/cli (100%) rename {fixtures => test/fixtures}/pgp/cli.invalid.asc (100%) rename {fixtures => test/fixtures}/pgp/cli.valid.asc (100%) rename {fixtures => test/fixtures}/pgp/private.pgp (100%) rename {fixtures => test/fixtures}/pgp/public.pgp (100%) rename {fixtures => test/fixtures}/tls/chain-intermediate.crt (100%) rename {fixtures => test/fixtures}/tls/chain-intermediate.key (100%) rename {fixtures => test/fixtures}/tls/chain-leaf.crt (100%) rename {fixtures => test/fixtures}/tls/chain-leaf.key (100%) rename {fixtures => test/fixtures}/tls/chain-root.crt (100%) rename {fixtures => test/fixtures}/tls/chain-root.key (100%) rename {fixtures => test/fixtures}/tls/chain.crt (100%) rename {fixtures => test/fixtures}/tls/chain.key (100%) rename {fixtures => test/fixtures}/tls/generate.bash (100%) rename {fixtures => test/fixtures}/tls/no-signing.crt (100%) rename {fixtures => test/fixtures}/tls/no-signing.key (100%) rename {fixtures => test/fixtures}/tls/self-signed.crt (100%) rename {fixtures => test/fixtures}/tls/self-signed.key (100%) rename {src/test => test/integration}/extension.test.ts (100%) rename {src/__mocks__ => test/mocks}/testHelpers.ts (90%) rename {src/__mocks__ => test/mocks}/vscode.runtime.ts (100%) create mode 100644 test/tsconfig.json rename {src => test/unit}/core/cliManager.test.ts (96%) rename {src => test/unit/core}/cliUtils.test.ts (70%) rename {src => test/unit}/core/mementoManager.test.ts (93%) rename {src => test/unit}/core/pathResolver.test.ts (92%) rename {src => test/unit}/core/secretsManager.test.ts (87%) rename {src => test/unit}/error.test.ts (90%) rename {src => test/unit}/featureSet.test.ts (94%) rename {src => test/unit}/globalFlags.test.ts (95%) rename {src => test/unit}/headers.test.ts (95%) rename {src => test/unit}/pgp.test.ts (85%) rename {src => test/unit/remote}/sshConfig.test.ts (99%) rename {src => test/unit/remote}/sshSupport.test.ts (99%) rename {src => test/unit}/util.test.ts (99%) create mode 100644 test/utils/fixtures.ts diff --git a/.eslintrc.json b/.eslintrc.json index a9665178..91d67601 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,9 +4,9 @@ "parserOptions": { "ecmaVersion": 6, "sourceType": "module", - "project": "./tsconfig.json" + "project": true }, - "plugins": ["@typescript-eslint", "prettier"], + "plugins": ["@typescript-eslint", "prettier", "import"], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", @@ -15,12 +15,38 @@ "plugin:md/prettier", "prettier" ], + "ignorePatterns": ["out", "dist", "**/*.d.ts"], + "settings": { + "import/resolver": { + "typescript": { "project": "./tsconfig.json" } + }, + "import/internal-regex": "^@/" + }, "overrides": [ + { + "files": ["test/**/*.{ts,tsx}", "**/*.{test,spec}.ts?(x)"], + "settings": { + "import/resolver": { + "typescript": { + // In tests, resolve using the test tsconfig + "project": "test/tsconfig.json" + } + } + } + }, { "files": ["*.ts"], "rules": { "require-await": "off", - "@typescript-eslint/require-await": "error" + "@typescript-eslint/require-await": "error", + "@typescript-eslint/consistent-type-imports": [ + "error", + { + "disallowTypeAnnotations": false, // Used in tests + "prefer": "type-imports", + "fixStyle": "inline-type-imports" + } + ] } }, { @@ -42,12 +68,25 @@ "import/order": [ "error", { - "alphabetize": { - "order": "asc" - }, - "groups": [["builtin", "external", "internal"], "parent", "sibling"] + "groups": [ + ["builtin", "external"], + "internal", + "parent", + ["sibling", "index"], + "type" + ], + "pathGroups": [ + { "pattern": "@/**", "group": "internal", "position": "before" } + ], + "pathGroupsExcludedImportTypes": ["builtin", "external"], + "newlines-between": "always", + "alphabetize": { "order": "asc", "caseInsensitive": true }, + "sortTypesGroup": true } ], + // Prevent duplicates and prefer merging into a single import + "no-duplicate-imports": "off", + "import/no-duplicates": ["error", { "prefer-inline": true }], "import/no-unresolved": [ "error", { @@ -68,6 +107,5 @@ } } ] - }, - "ignorePatterns": ["out", "dist", "**/*.d.ts"] + } } diff --git a/.vscode-test.mjs b/.vscode-test.mjs index 3bf0c207..60fc8650 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -1,7 +1,7 @@ import { defineConfig } from "@vscode/test-cli"; export default defineConfig({ - files: "out/test/**/*.test.js", + files: "out/test/integration/**/*.test.js", extensionDevelopmentPath: ".", extensionTestsPath: "./out/test", launchArgs: ["--enable-proposed-api", "coder.coder-remote"], diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..daaef897 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.ts": "explicit", + "source.fixAll.eslint": "explicit" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/.vscodeignore b/.vscodeignore index fe6dbade..d9cdd5e1 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,16 +1,42 @@ -.vscode/** -.vscode-test/** -.nyc_output/** -coverage/** +# Test and coverage output out/** +coverage/** +.nyc_output/** + +# Development files src/** -usage.md -.gitignore -node_modules/** -**/tsconfig.json -**/.eslintrc.json -**/.editorconfig -**/*.map +test/** **/*.ts +**/*.map + +# Configuration files +.vscode/** +.vscode-test/** +.vscode-test.mjs +tsconfig.json +.eslintrc.json +.editorconfig +.prettierignore +.eslintignore +**/.gitignore +**/.git-blame-ignore-revs + +# Package manager files +yarn.lock + +# Nix/flake files +flake.nix +flake.lock +*.nix + +# Dependencies +node_modules/** + +# Development tools and CI +.github/** +.claude/** + +# Documentation and media +usage.md +CLAUDE.md *.gif -fixtures/** diff --git a/package.json b/package.json index 7c7b60ca..23a49a20 100644 --- a/package.json +++ b/package.json @@ -330,11 +330,12 @@ "@types/glob": "^7.1.3", "@types/node": "^22.14.1", "@types/node-forge": "^1.3.14", + "@types/semver": "^7.7.1", "@types/ua-parser-js": "0.7.36", "@types/vscode": "^1.73.0", "@types/ws": "^8.18.1", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^6.21.0", + "@typescript-eslint/eslint-plugin": "^8.44.0", + "@typescript-eslint/parser": "^8.44.0", "@vitest/coverage-v8": "^3.2.4", "@vscode/test-cli": "^0.0.11", "@vscode/test-electron": "^2.5.2", @@ -343,13 +344,15 @@ "coder": "https://github.com/coder/coder#main", "dayjs": "^1.11.13", "eslint": "^8.57.1", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.31.0", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-md": "^1.0.19", "eslint-plugin-package-json": "^0.56.3", "eslint-plugin-prettier": "^5.5.4", "glob": "^10.4.2", "jsonc-eslint-parser": "^2.4.0", + "markdown-eslint-parser": "^1.2.1", "memfs": "^4.47.0", "nyc": "^17.1.0", "prettier": "^3.5.3", diff --git a/src/agentMetadataHelper.ts b/src/agentMetadataHelper.ts index d5e31e5e..0a976411 100644 --- a/src/agentMetadataHelper.ts +++ b/src/agentMetadataHelper.ts @@ -1,11 +1,12 @@ -import { WorkspaceAgent } from "coder/site/src/api/typesGenerated"; +import { type WorkspaceAgent } from "coder/site/src/api/typesGenerated"; import * as vscode from "vscode"; + import { - AgentMetadataEvent, + type AgentMetadataEvent, AgentMetadataEventSchemaArray, errToStr, } from "./api/api-helper"; -import { CoderApi } from "./api/coderApi"; +import { type CoderApi } from "./api/coderApi"; export type AgentMetadataWatcher = { onChange: vscode.EventEmitter["event"]; diff --git a/src/api/api-helper.ts b/src/api/api-helper.ts index 7b41f46c..5b8a5156 100644 --- a/src/api/api-helper.ts +++ b/src/api/api-helper.ts @@ -1,8 +1,8 @@ import { isApiError, isApiErrorResponse } from "coder/site/src/api/errors"; import { - Workspace, - WorkspaceAgent, - WorkspaceResource, + type Workspace, + type WorkspaceAgent, + type WorkspaceResource, } from "coder/site/src/api/typesGenerated"; import { ErrorEvent } from "eventsource"; import { z } from "zod"; diff --git a/src/api/coderApi.ts b/src/api/coderApi.ts index 68592b5c..6c6c0faf 100644 --- a/src/api/coderApi.ts +++ b/src/api/coderApi.ts @@ -1,14 +1,15 @@ -import { AxiosInstance } from "axios"; +import { type AxiosInstance } from "axios"; import { Api } from "coder/site/src/api/api"; import { - GetInboxNotificationResponse, - ProvisionerJobLog, - ServerSentEvent, - Workspace, - WorkspaceAgent, + type GetInboxNotificationResponse, + type ProvisionerJobLog, + type ServerSentEvent, + type Workspace, + type WorkspaceAgent, } from "coder/site/src/api/typesGenerated"; import { type WorkspaceConfiguration } from "vscode"; -import { ClientOptions } from "ws"; +import { type ClientOptions } from "ws"; + import { CertificateError } from "../error"; import { getHeaderCommand, getHeaders } from "../headers"; import { @@ -17,13 +18,17 @@ import { logError, logResponse, } from "../logging/httpLogger"; -import { Logger } from "../logging/logger"; -import { RequestConfigWithMeta, HttpClientLogLevel } from "../logging/types"; +import { type Logger } from "../logging/logger"; +import { + type RequestConfigWithMeta, + HttpClientLogLevel, +} from "../logging/types"; import { WsLogger } from "../logging/wsLogger"; import { OneWayWebSocket, - OneWayWebSocketInit, + type OneWayWebSocketInit, } from "../websocket/oneWayWebSocket"; + import { createHttpAgent } from "./utils"; const coderSessionTokenHeader = "Coder-Session-Token"; diff --git a/src/proxy.ts b/src/api/proxy.ts similarity index 100% rename from src/proxy.ts rename to src/api/proxy.ts diff --git a/src/api/utils.ts b/src/api/utils.ts index 2cb4e91e..91a18885 100644 --- a/src/api/utils.ts +++ b/src/api/utils.ts @@ -1,9 +1,11 @@ import fs from "fs"; import { ProxyAgent } from "proxy-agent"; import { type WorkspaceConfiguration } from "vscode"; -import { getProxyForUrl } from "../proxy"; + import { expandPath } from "../util"; +import { getProxyForUrl } from "./proxy"; + /** * Return whether the API will need a token for authorization. * If mTLS is in use (as specified by the cert or key files being set) then diff --git a/src/api/workspace.ts b/src/api/workspace.ts index 45fa9156..c2e20c0c 100644 --- a/src/api/workspace.ts +++ b/src/api/workspace.ts @@ -1,12 +1,14 @@ import { spawn } from "child_process"; -import { Api } from "coder/site/src/api/api"; -import { Workspace } from "coder/site/src/api/typesGenerated"; +import { type Api } from "coder/site/src/api/api"; +import { type Workspace } from "coder/site/src/api/typesGenerated"; import * as vscode from "vscode"; -import { FeatureSet } from "../featureSet"; + +import { type FeatureSet } from "../featureSet"; import { getGlobalFlags } from "../globalFlags"; import { escapeCommandArg } from "../util"; + import { errToStr, createWorkspaceIdentifier } from "./api-helper"; -import { CoderApi } from "./coderApi"; +import { type CoderApi } from "./coderApi"; /** * Start or update a workspace and return the updated workspace. diff --git a/src/commands.ts b/src/commands.ts index b9dcf10d..462010ba 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,29 +1,37 @@ -import { Api } from "coder/site/src/api/api"; +import { type Api } from "coder/site/src/api/api"; import { getErrorMessage } from "coder/site/src/api/errors"; import { - User, - Workspace, - WorkspaceAgent, + type User, + type Workspace, + type WorkspaceAgent, } from "coder/site/src/api/typesGenerated"; import * as vscode from "vscode"; + import { createWorkspaceIdentifier, extractAgents } from "./api/api-helper"; import { CoderApi } from "./api/coderApi"; import { needToken } from "./api/utils"; -import { CliManager } from "./core/cliManager"; -import { MementoManager } from "./core/mementoManager"; -import { PathResolver } from "./core/pathResolver"; -import { SecretsManager } from "./core/secretsManager"; +import { type CliManager } from "./core/cliManager"; +import { type ServiceContainer } from "./core/container"; +import { type MementoManager } from "./core/mementoManager"; +import { type PathResolver } from "./core/pathResolver"; +import { type SecretsManager } from "./core/secretsManager"; import { CertificateError } from "./error"; import { getGlobalFlags } from "./globalFlags"; -import { Logger } from "./logging/logger"; +import { type Logger } from "./logging/logger"; import { escapeCommandArg, toRemoteAuthority, toSafeHost } from "./util"; import { AgentTreeItem, - OpenableTreeItem, + type OpenableTreeItem, WorkspaceTreeItem, -} from "./workspacesProvider"; +} from "./workspace/workspacesProvider"; export class Commands { + private readonly vscodeProposed: typeof vscode; + private readonly logger: Logger; + private readonly pathResolver: PathResolver; + private readonly mementoManager: MementoManager; + private readonly secretsManager: SecretsManager; + private readonly cliManager: CliManager; // These will only be populated when actively connected to a workspace and are // used in commands. Because commands can be executed by the user, it is not // possible to pass in arguments, so we have to store the current workspace @@ -36,14 +44,16 @@ export class Commands { public workspaceRestClient?: Api; public constructor( - private readonly vscodeProposed: typeof vscode, + serviceContainer: ServiceContainer, private readonly restClient: Api, - private readonly logger: Logger, - private readonly pathResolver: PathResolver, - private readonly mementoManager: MementoManager, - private readonly secretsManager: SecretsManager, - private readonly cliManager: CliManager, - ) {} + ) { + this.vscodeProposed = serviceContainer.getVsCodeProposed(); + this.logger = serviceContainer.getLogger(); + this.pathResolver = serviceContainer.getPathResolver(); + this.mementoManager = serviceContainer.getMementoManager(); + this.secretsManager = serviceContainer.getSecretsManager(); + this.cliManager = serviceContainer.getCliManager(); + } /** * Find the requested agent if specified, otherwise return the agent if there diff --git a/src/core/cliManager.ts b/src/core/cliManager.ts index e8a7ab25..1bb0afa1 100644 --- a/src/core/cliManager.ts +++ b/src/core/cliManager.ts @@ -2,20 +2,21 @@ import globalAxios, { type AxiosInstance, type AxiosRequestConfig, } from "axios"; -import { Api } from "coder/site/src/api/api"; -import { createWriteStream, WriteStream } from "fs"; +import { type Api } from "coder/site/src/api/api"; +import { createWriteStream, type WriteStream } from "fs"; import fs from "fs/promises"; -import { IncomingMessage } from "http"; +import { type IncomingMessage } from "http"; import path from "path"; import prettyBytes from "pretty-bytes"; import * as semver from "semver"; - import * as vscode from "vscode"; + import { errToStr } from "../api/api-helper"; -import * as cli from "../cliUtils"; -import { Logger } from "../logging/logger"; +import { type Logger } from "../logging/logger"; import * as pgp from "../pgp"; -import { PathResolver } from "./pathResolver"; + +import * as cliUtils from "./cliUtils"; +import { type PathResolver } from "./pathResolver"; export class CliManager { constructor( @@ -58,16 +59,16 @@ export class CliManager { // downloads are disabled, we can return early. const binPath = path.join( this.pathResolver.getBinaryCachePath(label), - cli.name(), + cliUtils.name(), ); this.output.info("Using binary path", binPath); - const stat = await cli.stat(binPath); + const stat = await cliUtils.stat(binPath); if (stat === undefined) { this.output.info("No existing binary found, starting download"); } else { this.output.info("Existing binary size is", prettyBytes(stat.size)); try { - const version = await cli.version(binPath); + const version = await cliUtils.version(binPath); this.output.info("Existing binary version is", version); // If we have the right version we can avoid the request entirely. if (version === buildInfo.version) { @@ -97,7 +98,7 @@ export class CliManager { } // Remove any left-over old or temporary binaries and signatures. - const removed = await cli.rmOld(binPath); + const removed = await cliUtils.rmOld(binPath); removed.forEach(({ fileName, error }) => { if (error) { this.output.warn("Failed to remove", fileName, error); @@ -107,7 +108,7 @@ export class CliManager { }); // Figure out where to get the binary. - const binName = cli.name(); + const binName = cliUtils.name(); const configSource = cfg.get("binarySource"); const binSource = configSource && String(configSource).trim().length > 0 @@ -117,7 +118,7 @@ export class CliManager { // Ideally we already caught that this was the right version and returned // early, but just in case set the ETag. - const etag = stat !== undefined ? await cli.eTag(binPath) : ""; + const etag = stat !== undefined ? await cliUtils.eTag(binPath) : ""; this.output.info("Using ETag", etag); // Download the binary to a temporary file. @@ -173,14 +174,14 @@ export class CliManager { await fs.rename(tempFile, binPath); // For debugging, to see if the binary only partially downloaded. - const newStat = await cli.stat(binPath); + const newStat = await cliUtils.stat(binPath); this.output.info( "Downloaded binary size is", prettyBytes(newStat?.size || 0), ); // Make sure we can execute this new binary. - const version = await cli.version(binPath); + const version = await cliUtils.version(binPath); this.output.info("Downloaded binary version is", version); return binPath; @@ -199,8 +200,8 @@ export class CliManager { if (!value) { return; } - const os = cli.goos(); - const arch = cli.goarch(); + const os = cliUtils.goos(); + const arch = cliUtils.goarch(); const params = new URLSearchParams({ title: `Support the \`${os}-${arch}\` platform`, body: `I'd like to use the \`${os}-${arch}\` architecture with the VS Code extension.`, @@ -223,7 +224,7 @@ export class CliManager { return; } const params = new URLSearchParams({ - title: `Failed to download binary on \`${cli.goos()}-${cli.goarch()}\``, + title: `Failed to download binary on \`${cliUtils.goos()}-${cliUtils.goarch()}\``, body: `Received status code \`${status}\` when downloading the binary.`, }); const uri = vscode.Uri.parse( diff --git a/src/cliUtils.ts b/src/core/cliUtils.ts similarity index 100% rename from src/cliUtils.ts rename to src/core/cliUtils.ts diff --git a/src/core/container.ts b/src/core/container.ts new file mode 100644 index 00000000..f820bb0d --- /dev/null +++ b/src/core/container.ts @@ -0,0 +1,69 @@ +import * as vscode from "vscode"; + +import { type Logger } from "../logging/logger"; + +import { CliManager } from "./cliManager"; +import { MementoManager } from "./mementoManager"; +import { PathResolver } from "./pathResolver"; +import { SecretsManager } from "./secretsManager"; + +/** + * Service container for dependency injection. + * Centralizes the creation and management of all core services. + */ +export class ServiceContainer { + private readonly logger: vscode.LogOutputChannel; + private readonly pathResolver: PathResolver; + private readonly mementoManager: MementoManager; + private readonly secretsManager: SecretsManager; + private readonly cliManager: CliManager; + + constructor( + context: vscode.ExtensionContext, + private readonly vscodeProposed: typeof vscode = vscode, + ) { + this.logger = vscode.window.createOutputChannel("Coder", { log: true }); + this.pathResolver = new PathResolver( + context.globalStorageUri.fsPath, + context.logUri.fsPath, + ); + this.mementoManager = new MementoManager(context.globalState); + this.secretsManager = new SecretsManager(context.secrets); + this.cliManager = new CliManager( + this.vscodeProposed, + this.logger, + this.pathResolver, + ); + } + + getVsCodeProposed(): typeof vscode { + return this.vscodeProposed; + } + + getPathResolver(): PathResolver { + return this.pathResolver; + } + + getMementoManager(): MementoManager { + return this.mementoManager; + } + + getSecretsManager(): SecretsManager { + return this.secretsManager; + } + + getLogger(): Logger { + return this.logger; + } + + getCliManager(): CliManager { + return this.cliManager; + } + + /** + * Dispose of all services and clean up resources. + */ + dispose(): void { + this.logger.dispose(); + } +} diff --git a/src/core/secretsManager.ts b/src/core/secretsManager.ts index 7fd98f8f..6a6666da 100644 --- a/src/core/secretsManager.ts +++ b/src/core/secretsManager.ts @@ -20,7 +20,7 @@ export class SecretsManager { public async getSessionToken(): Promise { try { return await this.secrets.get("sessionToken"); - } catch (ex) { + } catch { // The VS Code session store has become corrupt before, and // will fail to get the session token... return undefined; diff --git a/src/error.ts b/src/error.ts index 994b5910..7b93b458 100644 --- a/src/error.ts +++ b/src/error.ts @@ -3,7 +3,8 @@ import { isApiError, isApiErrorResponse } from "coder/site/src/api/errors"; import * as forge from "node-forge"; import * as tls from "tls"; import * as vscode from "vscode"; -import { Logger } from "./logging/logger"; + +import { type Logger } from "./logging/logger"; // X509_ERR_CODE represents error codes as returned from BoringSSL/OpenSSL. export enum X509_ERR_CODE { diff --git a/src/extension.ts b/src/extension.ts index 678ea3b7..f7453cec 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,20 +1,22 @@ "use strict"; + import axios, { isAxiosError } from "axios"; import { getErrorMessage } from "coder/site/src/api/errors"; import * as module from "module"; import * as vscode from "vscode"; + import { errToStr } from "./api/api-helper"; import { CoderApi } from "./api/coderApi"; import { needToken } from "./api/utils"; import { Commands } from "./commands"; -import { CliManager } from "./core/cliManager"; -import { MementoManager } from "./core/mementoManager"; -import { PathResolver } from "./core/pathResolver"; -import { SecretsManager } from "./core/secretsManager"; +import { ServiceContainer } from "./core/container"; import { CertificateError, getErrorDetail } from "./error"; -import { Remote } from "./remote"; +import { Remote } from "./remote/remote"; import { toSafeHost } from "./util"; -import { WorkspaceProvider, WorkspaceQuery } from "./workspacesProvider"; +import { + WorkspaceProvider, + WorkspaceQuery, +} from "./workspace/workspacesProvider"; export async function activate(ctx: vscode.ExtensionContext): Promise { // The Remote SSH extension's proposed APIs are used to override the SSH host @@ -51,14 +53,10 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { ); } - const pathResolver = new PathResolver( - ctx.globalStorageUri.fsPath, - ctx.logUri.fsPath, - ); - const mementoManager = new MementoManager(ctx.globalState); - const secretsManager = new SecretsManager(ctx.secrets); - - const output = vscode.window.createOutputChannel("Coder", { log: true }); + const serviceContainer = new ServiceContainer(ctx, vscodeProposed); + const output = serviceContainer.getLogger(); + const mementoManager = serviceContainer.getMementoManager(); + const secretsManager = serviceContainer.getSecretsManager(); // Try to clear this flag ASAP const isFirstConnect = await mementoManager.getAndClearFirstConnect(); @@ -253,19 +251,11 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { }, }); - const cliManager = new CliManager(vscodeProposed, output, pathResolver); + const cliManager = serviceContainer.getCliManager(); // Register globally available commands. Many of these have visibility // controlled by contexts, see `when` in the package.json. - const commands = new Commands( - vscodeProposed, - client, - output, - pathResolver, - mementoManager, - secretsManager, - cliManager, - ); + const commands = new Commands(serviceContainer, client); vscode.commands.registerCommand("coder.login", commands.login.bind(commands)); vscode.commands.registerCommand( "coder.logout", @@ -319,14 +309,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // (this would require the user to uninstall the Coder extension and // reinstall after installing the remote SSH extension, which is annoying) if (remoteSSHExtension && vscodeProposed.env.remoteAuthority) { - const remote = new Remote( - vscodeProposed, - output, - commands, - ctx.extensionMode, - pathResolver, - cliManager, - ); + const remote = new Remote(serviceContainer, commands, ctx.extensionMode); try { const details = await remote.setup( vscodeProposed.env.remoteAuthority, diff --git a/src/featureSet.ts b/src/featureSet.ts index 67121229..f0b6e95d 100644 --- a/src/featureSet.ts +++ b/src/featureSet.ts @@ -1,4 +1,4 @@ -import * as semver from "semver"; +import type * as semver from "semver"; export type FeatureSet = { vscodessh: boolean; diff --git a/src/globalFlags.ts b/src/globalFlags.ts index 851e41c7..8e75ce8d 100644 --- a/src/globalFlags.ts +++ b/src/globalFlags.ts @@ -1,4 +1,5 @@ -import { WorkspaceConfiguration } from "vscode"; +import { type WorkspaceConfiguration } from "vscode"; + import { getHeaderArgs } from "./headers"; import { escapeCommandArg } from "./util"; diff --git a/src/headers.ts b/src/headers.ts index 1aad4258..f5f45301 100644 --- a/src/headers.ts +++ b/src/headers.ts @@ -1,10 +1,12 @@ import * as cp from "child_process"; import * as os from "os"; import * as util from "util"; -import type { WorkspaceConfiguration } from "vscode"; -import { Logger } from "./logging/logger"; + +import { type Logger } from "./logging/logger"; import { escapeCommandArg } from "./util"; +import type { WorkspaceConfiguration } from "vscode"; + interface ExecException { code?: number; stderr?: string; diff --git a/src/inbox.ts b/src/inbox.ts index e12263bf..61a780bb 100644 --- a/src/inbox.ts +++ b/src/inbox.ts @@ -1,11 +1,13 @@ -import { +import * as vscode from "vscode"; + +import type { Workspace, GetInboxNotificationResponse, } from "coder/site/src/api/typesGenerated"; -import * as vscode from "vscode"; -import { CoderApi } from "./api/coderApi"; -import { Logger } from "./logging/logger"; -import { OneWayWebSocket } from "./websocket/oneWayWebSocket"; + +import type { CoderApi } from "./api/coderApi"; +import type { Logger } from "./logging/logger"; +import type { OneWayWebSocket } from "./websocket/oneWayWebSocket"; // These are the template IDs of our notifications. // Maybe in the future we should avoid hardcoding diff --git a/src/logging/formatters.ts b/src/logging/formatters.ts index 01f55cce..1ad45231 100644 --- a/src/logging/formatters.ts +++ b/src/logging/formatters.ts @@ -1,6 +1,7 @@ -import type { InternalAxiosRequestConfig } from "axios"; import prettyBytes from "pretty-bytes"; +import type { InternalAxiosRequestConfig } from "axios"; + const SENSITIVE_HEADERS = ["Coder-Session-Token", "Proxy-Authorization"]; export function formatTime(ms: number): string { diff --git a/src/logging/httpLogger.ts b/src/logging/httpLogger.ts index 3eed3c56..7e569cad 100644 --- a/src/logging/httpLogger.ts +++ b/src/logging/httpLogger.ts @@ -1,7 +1,8 @@ -import type { AxiosError, AxiosResponse } from "axios"; -import { isAxiosError } from "axios"; +import { isAxiosError, type AxiosError, type AxiosResponse } from "axios"; import { getErrorMessage } from "coder/site/src/api/errors"; + import { getErrorDetail } from "../error"; + import { formatBody, formatContentLength, @@ -10,14 +11,15 @@ import { formatTime, formatUri, } from "./formatters"; -import type { Logger } from "./logger"; import { HttpClientLogLevel, - RequestConfigWithMeta, - RequestMeta, + type RequestConfigWithMeta, + type RequestMeta, } from "./types"; import { createRequestId, shortId } from "./utils"; +import type { Logger } from "./logger"; + /** * Creates metadata for tracking HTTP requests. */ diff --git a/src/logging/wsLogger.ts b/src/logging/wsLogger.ts index 7b922f51..b33118b7 100644 --- a/src/logging/wsLogger.ts +++ b/src/logging/wsLogger.ts @@ -1,9 +1,12 @@ import prettyBytes from "pretty-bytes"; + import { errToStr } from "../api/api-helper"; + import { formatTime } from "./formatters"; -import type { Logger } from "./logger"; import { createRequestId, shortId, sizeOf } from "./utils"; +import type { Logger } from "./logger"; + const numFormatter = new Intl.NumberFormat("en", { notation: "compact", compactDisplay: "short", diff --git a/src/pgp.ts b/src/pgp.ts index 2e82fb79..0e38029f 100644 --- a/src/pgp.ts +++ b/src/pgp.ts @@ -2,8 +2,9 @@ import { createReadStream, promises as fs } from "fs"; import * as openpgp from "openpgp"; import * as path from "path"; import { Readable } from "stream"; + import { errToStr } from "./api/api-helper"; -import { Logger } from "./logging/logger"; +import { type Logger } from "./logging/logger"; export type Key = openpgp.Key; diff --git a/src/remote.ts b/src/remote/remote.ts similarity index 95% rename from src/remote.ts rename to src/remote/remote.ts index c9765fb8..baf7b28c 100644 --- a/src/remote.ts +++ b/src/remote/remote.ts @@ -1,6 +1,9 @@ import { isAxiosError } from "axios"; -import { Api } from "coder/site/src/api/api"; -import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"; +import { type Api } from "coder/site/src/api/api"; +import { + type Workspace, + type WorkspaceAgent, +} from "coder/site/src/api/typesGenerated"; import find from "find-process"; import * as fs from "fs/promises"; import * as jsonc from "jsonc-parser"; @@ -9,34 +12,40 @@ import * as path from "path"; import prettyBytes from "pretty-bytes"; import * as semver from "semver"; import * as vscode from "vscode"; + import { createAgentMetadataWatcher, getEventValue, formatEventLabel, formatMetadataError, -} from "./agentMetadataHelper"; -import { createWorkspaceIdentifier, extractAgents } from "./api/api-helper"; -import { CoderApi } from "./api/coderApi"; -import { needToken } from "./api/utils"; -import { startWorkspaceIfStoppedOrFailed, waitForBuild } from "./api/workspace"; -import * as cliUtils from "./cliUtils"; -import { Commands } from "./commands"; -import { CliManager } from "./core/cliManager"; -import { PathResolver } from "./core/pathResolver"; -import { featureSetForVersion, FeatureSet } from "./featureSet"; -import { getGlobalFlags } from "./globalFlags"; -import { Inbox } from "./inbox"; -import { Logger } from "./logging/logger"; -import { SSHConfig, SSHValues, mergeSSHConfigValues } from "./sshConfig"; -import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport"; +} from "../agentMetadataHelper"; +import { createWorkspaceIdentifier, extractAgents } from "../api/api-helper"; +import { CoderApi } from "../api/coderApi"; +import { needToken } from "../api/utils"; +import { + startWorkspaceIfStoppedOrFailed, + waitForBuild, +} from "../api/workspace"; +import { type Commands } from "../commands"; +import { type CliManager } from "../core/cliManager"; +import * as cliUtils from "../core/cliUtils"; +import { type ServiceContainer } from "../core/container"; +import { type PathResolver } from "../core/pathResolver"; +import { featureSetForVersion, type FeatureSet } from "../featureSet"; +import { getGlobalFlags } from "../globalFlags"; +import { Inbox } from "../inbox"; +import { type Logger } from "../logging/logger"; import { AuthorityPrefix, escapeCommandArg, expandPath, findPort, parseRemoteAuthority, -} from "./util"; -import { WorkspaceMonitor } from "./workspaceMonitor"; +} from "../util"; +import { WorkspaceMonitor } from "../workspace/workspaceMonitor"; + +import { SSHConfig, type SSHValues, mergeSSHConfigValues } from "./sshConfig"; +import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport"; export interface RemoteDetails extends vscode.Disposable { url: string; @@ -44,15 +53,22 @@ export interface RemoteDetails extends vscode.Disposable { } export class Remote { + // We use the proposed API to get access to useCustom in dialogs. + private readonly vscodeProposed: typeof vscode; + private readonly logger: Logger; + private readonly pathResolver: PathResolver; + private readonly cliManager: CliManager; + public constructor( - // We use the proposed API to get access to useCustom in dialogs. - private readonly vscodeProposed: typeof vscode, - private readonly logger: Logger, + serviceContainer: ServiceContainer, private readonly commands: Commands, private readonly mode: vscode.ExtensionMode, - private readonly pathResolver: PathResolver, - private readonly cliManager: CliManager, - ) {} + ) { + this.vscodeProposed = serviceContainer.getVsCodeProposed(); + this.logger = serviceContainer.getLogger(); + this.pathResolver = serviceContainer.getPathResolver(); + this.cliManager = serviceContainer.getCliManager(); + } private async confirmStart(workspaceName: string): Promise { const action = await this.vscodeProposed.window.showInformationMessage( @@ -281,7 +297,7 @@ export class Remote { // This is useful for debugging with a custom bin! binaryPath = path.join(os.tmpdir(), "coder"); await fs.stat(binaryPath); - } catch (ex) { + } catch { binaryPath = await this.cliManager.fetchBinary( workspaceClient, parts.label, @@ -295,7 +311,7 @@ export class Remote { let version: semver.SemVer | null = null; try { version = semver.parse(await cliUtils.version(binaryPath)); - } catch (e) { + } catch { version = semver.parse(buildInfo.version); } @@ -442,7 +458,7 @@ export class Remote { this.pathResolver.getUserSettingsPath(), "utf8", ); - } catch (ex) { + } catch { // Ignore! It's probably because the file doesn't exist. } @@ -932,7 +948,7 @@ export class Remote { .then((parsed) => { try { updateStatus(parsed); - } catch (ex) { + } catch { // Ignore } }) diff --git a/src/sshConfig.ts b/src/remote/sshConfig.ts similarity index 99% rename from src/sshConfig.ts rename to src/remote/sshConfig.ts index 4b184921..f5fea264 100644 --- a/src/sshConfig.ts +++ b/src/remote/sshConfig.ts @@ -1,6 +1,7 @@ import { mkdir, readFile, rename, stat, writeFile } from "fs/promises"; import path from "path"; -import { countSubstring } from "./util"; + +import { countSubstring } from "../util"; class SSHConfigBadFormat extends Error {} @@ -107,7 +108,7 @@ export class SSHConfig { async load() { try { this.raw = await this.fileSystem.readFile(this.filePath, "utf-8"); - } catch (ex) { + } catch { // Probably just doesn't exist! this.raw = ""; } diff --git a/src/sshSupport.ts b/src/remote/sshSupport.ts similarity index 99% rename from src/sshSupport.ts rename to src/remote/sshSupport.ts index 8abcdd24..08860546 100644 --- a/src/sshSupport.ts +++ b/src/remote/sshSupport.ts @@ -6,7 +6,7 @@ export function sshSupportsSetEnv(): boolean { const spawned = childProcess.spawnSync("ssh", ["-V"]); // The version string outputs to stderr. return sshVersionSupportsSetEnv(spawned.stderr.toString().trim()); - } catch (error) { + } catch { return false; } } diff --git a/src/websocket/oneWayWebSocket.ts b/src/websocket/oneWayWebSocket.ts index 3b6a226f..37965596 100644 --- a/src/websocket/oneWayWebSocket.ts +++ b/src/websocket/oneWayWebSocket.ts @@ -7,27 +7,34 @@ * instead of always deriving it from `window.location`. */ -import { WebSocketEventType } from "coder/site/src/utils/OneWayWebSocket"; -import WebSocket, { type ClientOptions } from "ws"; +import { type WebSocketEventType } from "coder/site/src/utils/OneWayWebSocket"; +import Ws, { + type ClientOptions, + type CloseEvent, + type ErrorEvent, + type Event, + type MessageEvent, + type RawData, +} from "ws"; export type OneWayMessageEvent = Readonly< | { - sourceEvent: WebSocket.MessageEvent; + sourceEvent: MessageEvent; parsedMessage: TData; parseError: undefined; } | { - sourceEvent: WebSocket.MessageEvent; + sourceEvent: MessageEvent; parsedMessage: undefined; parseError: Error; } >; type OneWayEventPayloadMap = { - close: WebSocket.CloseEvent; - error: WebSocket.ErrorEvent; + close: CloseEvent; + error: ErrorEvent; message: OneWayMessageEvent; - open: WebSocket.Event; + open: Event; }; type OneWayEventCallback = ( @@ -58,10 +65,10 @@ export type OneWayWebSocketInit = { export class OneWayWebSocket implements OneWayWebSocketApi { - readonly #socket: WebSocket; + readonly #socket: Ws; readonly #messageCallbacks = new Map< OneWayEventCallback, - (data: WebSocket.RawData) => void + (data: RawData) => void >(); constructor(init: OneWayWebSocketInit) { @@ -76,7 +83,7 @@ export class OneWayWebSocket const wsProtocol = location.protocol === "https:" ? "wss:" : "ws:"; const url = `${wsProtocol}//${location.host}${apiRoute}${paramsSuffix}`; - this.#socket = new WebSocket(url, protocols, options); + this.#socket = new Ws(url, protocols, options); } get url(): string { @@ -94,17 +101,17 @@ export class OneWayWebSocket return; } - const wrapped = (data: WebSocket.RawData): void => { + const wrapped = (data: RawData): void => { try { const message = JSON.parse(data.toString()) as TData; messageCallback({ - sourceEvent: { data } as WebSocket.MessageEvent, + sourceEvent: { data } as MessageEvent, parseError: undefined, parsedMessage: message, }); } catch (err) { messageCallback({ - sourceEvent: { data } as WebSocket.MessageEvent, + sourceEvent: { data } as MessageEvent, parseError: err as Error, parsedMessage: undefined, }); diff --git a/src/workspaceMonitor.ts b/src/workspace/workspaceMonitor.ts similarity index 94% rename from src/workspaceMonitor.ts rename to src/workspace/workspaceMonitor.ts index ece765a6..8ff99137 100644 --- a/src/workspaceMonitor.ts +++ b/src/workspace/workspaceMonitor.ts @@ -1,10 +1,14 @@ -import { ServerSentEvent, Workspace } from "coder/site/src/api/typesGenerated"; +import { + type ServerSentEvent, + type Workspace, +} from "coder/site/src/api/typesGenerated"; import { formatDistanceToNowStrict } from "date-fns"; import * as vscode from "vscode"; -import { createWorkspaceIdentifier, errToStr } from "./api/api-helper"; -import { CoderApi } from "./api/coderApi"; -import { Logger } from "./logging/logger"; -import { OneWayWebSocket } from "./websocket/oneWayWebSocket"; + +import { createWorkspaceIdentifier, errToStr } from "../api/api-helper"; +import { type CoderApi } from "../api/coderApi"; +import { type Logger } from "../logging/logger"; +import { type OneWayWebSocket } from "../websocket/oneWayWebSocket"; /** * Monitor a single workspace using a WebSocket for events like shutdown and deletion. diff --git a/src/workspacesProvider.ts b/src/workspace/workspacesProvider.ts similarity index 97% rename from src/workspacesProvider.ts rename to src/workspace/workspacesProvider.ts index 23f5705a..86279401 100644 --- a/src/workspacesProvider.ts +++ b/src/workspace/workspacesProvider.ts @@ -1,23 +1,24 @@ import { - Workspace, - WorkspaceAgent, - WorkspaceApp, + type Workspace, + type WorkspaceAgent, + type WorkspaceApp, } from "coder/site/src/api/typesGenerated"; import * as path from "path"; import * as vscode from "vscode"; + import { - AgentMetadataWatcher, + type AgentMetadataWatcher, createAgentMetadataWatcher, formatEventLabel, formatMetadataError, -} from "./agentMetadataHelper"; +} from "../agentMetadataHelper"; import { - AgentMetadataEvent, + type AgentMetadataEvent, extractAgents, extractAllAgents, -} from "./api/api-helper"; -import { CoderApi } from "./api/coderApi"; -import { Logger } from "./logging/logger"; +} from "../api/api-helper"; +import { type CoderApi } from "../api/coderApi"; +import { type Logger } from "../logging/logger"; export enum WorkspaceQuery { Mine = "owner:me", @@ -71,7 +72,7 @@ export class WorkspaceProvider let hadError = false; try { this.workspaces = await this.fetch(); - } catch (error) { + } catch { hadError = true; this.workspaces = []; } diff --git a/fixtures/bin.bash b/test/fixtures/bin.bash similarity index 100% rename from fixtures/bin.bash rename to test/fixtures/bin.bash diff --git a/fixtures/bin.old.bash b/test/fixtures/bin.old.bash similarity index 100% rename from fixtures/bin.old.bash rename to test/fixtures/bin.old.bash diff --git a/fixtures/pgp/cli b/test/fixtures/pgp/cli similarity index 100% rename from fixtures/pgp/cli rename to test/fixtures/pgp/cli diff --git a/fixtures/pgp/cli.invalid.asc b/test/fixtures/pgp/cli.invalid.asc similarity index 100% rename from fixtures/pgp/cli.invalid.asc rename to test/fixtures/pgp/cli.invalid.asc diff --git a/fixtures/pgp/cli.valid.asc b/test/fixtures/pgp/cli.valid.asc similarity index 100% rename from fixtures/pgp/cli.valid.asc rename to test/fixtures/pgp/cli.valid.asc diff --git a/fixtures/pgp/private.pgp b/test/fixtures/pgp/private.pgp similarity index 100% rename from fixtures/pgp/private.pgp rename to test/fixtures/pgp/private.pgp diff --git a/fixtures/pgp/public.pgp b/test/fixtures/pgp/public.pgp similarity index 100% rename from fixtures/pgp/public.pgp rename to test/fixtures/pgp/public.pgp diff --git a/fixtures/tls/chain-intermediate.crt b/test/fixtures/tls/chain-intermediate.crt similarity index 100% rename from fixtures/tls/chain-intermediate.crt rename to test/fixtures/tls/chain-intermediate.crt diff --git a/fixtures/tls/chain-intermediate.key b/test/fixtures/tls/chain-intermediate.key similarity index 100% rename from fixtures/tls/chain-intermediate.key rename to test/fixtures/tls/chain-intermediate.key diff --git a/fixtures/tls/chain-leaf.crt b/test/fixtures/tls/chain-leaf.crt similarity index 100% rename from fixtures/tls/chain-leaf.crt rename to test/fixtures/tls/chain-leaf.crt diff --git a/fixtures/tls/chain-leaf.key b/test/fixtures/tls/chain-leaf.key similarity index 100% rename from fixtures/tls/chain-leaf.key rename to test/fixtures/tls/chain-leaf.key diff --git a/fixtures/tls/chain-root.crt b/test/fixtures/tls/chain-root.crt similarity index 100% rename from fixtures/tls/chain-root.crt rename to test/fixtures/tls/chain-root.crt diff --git a/fixtures/tls/chain-root.key b/test/fixtures/tls/chain-root.key similarity index 100% rename from fixtures/tls/chain-root.key rename to test/fixtures/tls/chain-root.key diff --git a/fixtures/tls/chain.crt b/test/fixtures/tls/chain.crt similarity index 100% rename from fixtures/tls/chain.crt rename to test/fixtures/tls/chain.crt diff --git a/fixtures/tls/chain.key b/test/fixtures/tls/chain.key similarity index 100% rename from fixtures/tls/chain.key rename to test/fixtures/tls/chain.key diff --git a/fixtures/tls/generate.bash b/test/fixtures/tls/generate.bash similarity index 100% rename from fixtures/tls/generate.bash rename to test/fixtures/tls/generate.bash diff --git a/fixtures/tls/no-signing.crt b/test/fixtures/tls/no-signing.crt similarity index 100% rename from fixtures/tls/no-signing.crt rename to test/fixtures/tls/no-signing.crt diff --git a/fixtures/tls/no-signing.key b/test/fixtures/tls/no-signing.key similarity index 100% rename from fixtures/tls/no-signing.key rename to test/fixtures/tls/no-signing.key diff --git a/fixtures/tls/self-signed.crt b/test/fixtures/tls/self-signed.crt similarity index 100% rename from fixtures/tls/self-signed.crt rename to test/fixtures/tls/self-signed.crt diff --git a/fixtures/tls/self-signed.key b/test/fixtures/tls/self-signed.key similarity index 100% rename from fixtures/tls/self-signed.key rename to test/fixtures/tls/self-signed.key diff --git a/src/test/extension.test.ts b/test/integration/extension.test.ts similarity index 100% rename from src/test/extension.test.ts rename to test/integration/extension.test.ts diff --git a/src/__mocks__/testHelpers.ts b/test/mocks/testHelpers.ts similarity index 90% rename from src/__mocks__/testHelpers.ts rename to test/mocks/testHelpers.ts index 3a4ce407..14eca74b 100644 --- a/src/__mocks__/testHelpers.ts +++ b/test/mocks/testHelpers.ts @@ -181,28 +181,20 @@ export class MockUserInteraction { return this.responses.get(message); }; - vi.mocked(vscode.window.showErrorMessage).mockImplementation( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (message: string): Thenable => { - const response = getResponse(message); - return Promise.resolve(response); - }, - ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleMessage = (message: string): Thenable => { + const response = getResponse(message); + return Promise.resolve(response); + }; + + vi.mocked(vscode.window.showErrorMessage).mockImplementation(handleMessage); vi.mocked(vscode.window.showWarningMessage).mockImplementation( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (message: string): Thenable => { - const response = getResponse(message); - return Promise.resolve(response); - }, + handleMessage, ); vi.mocked(vscode.window.showInformationMessage).mockImplementation( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (message: string): Thenable => { - const response = getResponse(message); - return Promise.resolve(response); - }, + handleMessage, ); vi.mocked(vscode.env.openExternal).mockImplementation( diff --git a/src/__mocks__/vscode.runtime.ts b/test/mocks/vscode.runtime.ts similarity index 100% rename from src/__mocks__/vscode.runtime.ts rename to test/mocks/vscode.runtime.ts diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 00000000..ece5f0b1 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "baseUrl": "..", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["**/*", "../src/**/*"] +} diff --git a/src/core/cliManager.test.ts b/test/unit/core/cliManager.test.ts similarity index 96% rename from src/core/cliManager.test.ts rename to test/unit/core/cliManager.test.ts index 676de44c..2d76e8d4 100644 --- a/src/core/cliManager.test.ts +++ b/test/unit/core/cliManager.test.ts @@ -1,27 +1,29 @@ -import globalAxios, { AxiosInstance } from "axios"; -import { Api } from "coder/site/src/api/api"; +import globalAxios, { type AxiosInstance } from "axios"; +import { type Api } from "coder/site/src/api/api"; import EventEmitter from "events"; import * as fs from "fs"; -import { IncomingMessage } from "http"; +import { type IncomingMessage } from "http"; import { fs as memfs, vol } from "memfs"; import * as os from "os"; import * as path from "path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import * as vscode from "vscode"; + +import { CliManager } from "@/core/cliManager"; +import * as cliUtils from "@/core/cliUtils"; +import { PathResolver } from "@/core/pathResolver"; +import { type Logger } from "@/logging/logger"; +import * as pgp from "@/pgp"; + import { MockConfigurationProvider, MockProgressReporter, MockUserInteraction, -} from "../__mocks__/testHelpers"; -import * as cli from "../cliUtils"; -import { Logger } from "../logging/logger"; -import * as pgp from "../pgp"; -import { CliManager } from "./cliManager"; -import { PathResolver } from "./pathResolver"; +} from "../../mocks/testHelpers"; vi.mock("os"); vi.mock("axios"); -vi.mock("../pgp"); +vi.mock("@/pgp"); vi.mock("fs", async () => { const memfs: { fs: typeof fs } = await vi.importActual("memfs"); @@ -39,10 +41,9 @@ vi.mock("fs/promises", async () => { }; }); -// Only mock the platform detection functions from CLI manager -vi.mock("../cliUtils", async () => { +vi.mock("@/core/cliUtils", async () => { const actual = - await vi.importActual("../cliUtils"); + await vi.importActual("@/core/cliUtils"); return { ...actual, // No need to test script execution here @@ -652,7 +653,7 @@ describe("CliManager", () => { }); // Mock version to return the specified version - vi.mocked(cli.version).mockResolvedValueOnce(version); + vi.mocked(cliUtils.version).mockResolvedValueOnce(version); } function withCorruptedBinary() { @@ -662,7 +663,7 @@ describe("CliManager", () => { }); // Mock version to fail - vi.mocked(cli.version).mockRejectedValueOnce(new Error("corrupted")); + vi.mocked(cliUtils.version).mockRejectedValueOnce(new Error("corrupted")); } function withSuccessfulDownload(opts?: { @@ -676,7 +677,7 @@ describe("CliManager", () => { ); // Mock version to return TEST_VERSION after download - vi.mocked(cli.version).mockResolvedValue(TEST_VERSION); + vi.mocked(cliUtils.version).mockResolvedValue(TEST_VERSION); } function withSignatureResponses(statuses: number[]): void { diff --git a/src/cliUtils.test.ts b/test/unit/core/cliUtils.test.ts similarity index 70% rename from src/cliUtils.test.ts rename to test/unit/core/cliUtils.test.ts index aec78e87..d63ddd87 100644 --- a/src/cliUtils.test.ts +++ b/test/unit/core/cliUtils.test.ts @@ -2,9 +2,12 @@ import fs from "fs/promises"; import os from "os"; import path from "path"; import { beforeAll, describe, expect, it } from "vitest"; -import * as cli from "./cliUtils"; -describe("cliUtils", () => { +import * as cliUtils from "@/core/cliUtils"; + +import { getFixturePath } from "../../utils/fixtures"; + +describe("CliUtils", () => { const tmp = path.join(os.tmpdir(), "vscode-coder-tests"); beforeAll(async () => { @@ -14,34 +17,31 @@ describe("cliUtils", () => { }); it("name", () => { - expect(cli.name().startsWith("coder-")).toBeTruthy(); + expect(cliUtils.name().startsWith("coder-")).toBeTruthy(); }); it("stat", async () => { const binPath = path.join(tmp, "stat"); - expect(await cli.stat(binPath)).toBeUndefined(); + expect(await cliUtils.stat(binPath)).toBeUndefined(); await fs.writeFile(binPath, "test"); - expect((await cli.stat(binPath))?.size).toBe(4); + expect((await cliUtils.stat(binPath))?.size).toBe(4); }); // TODO: CI only runs on Linux but we should run it on Windows too. it("version", async () => { const binPath = path.join(tmp, "version"); - await expect(cli.version(binPath)).rejects.toThrow("ENOENT"); + await expect(cliUtils.version(binPath)).rejects.toThrow("ENOENT"); - const binTmpl = await fs.readFile( - path.join(__dirname, "../fixtures/bin.bash"), - "utf8", - ); + const binTmpl = await fs.readFile(getFixturePath("bin.bash"), "utf8"); await fs.writeFile(binPath, binTmpl.replace("$ECHO", "hello")); - await expect(cli.version(binPath)).rejects.toThrow("EACCES"); + await expect(cliUtils.version(binPath)).rejects.toThrow("EACCES"); await fs.chmod(binPath, "755"); - await expect(cli.version(binPath)).rejects.toThrow("Unexpected token"); + await expect(cliUtils.version(binPath)).rejects.toThrow("Unexpected token"); await fs.writeFile(binPath, binTmpl.replace("$ECHO", "{}")); - await expect(cli.version(binPath)).rejects.toThrow( + await expect(cliUtils.version(binPath)).rejects.toThrow( "No version found in output", ); @@ -54,42 +54,39 @@ describe("cliUtils", () => { }), ), ); - expect(await cli.version(binPath)).toBe("v0.0.0"); + expect(await cliUtils.version(binPath)).toBe("v0.0.0"); - const oldTmpl = await fs.readFile( - path.join(__dirname, "../fixtures/bin.old.bash"), - "utf8", - ); + const oldTmpl = await fs.readFile(getFixturePath("bin.old.bash"), "utf8"); const old = (stderr: string, stdout: string): string => { return oldTmpl.replace("$STDERR", stderr).replace("$STDOUT", stdout); }; // Should fall back only if it says "unknown flag". await fs.writeFile(binPath, old("foobar", "Coder v1.1.1")); - await expect(cli.version(binPath)).rejects.toThrow("foobar"); + await expect(cliUtils.version(binPath)).rejects.toThrow("foobar"); await fs.writeFile(binPath, old("unknown flag: --output", "Coder v1.1.1")); - expect(await cli.version(binPath)).toBe("v1.1.1"); + expect(await cliUtils.version(binPath)).toBe("v1.1.1"); // Should trim off the newline if necessary. await fs.writeFile( binPath, old("unknown flag: --output\n", "Coder v1.1.1\n"), ); - expect(await cli.version(binPath)).toBe("v1.1.1"); + expect(await cliUtils.version(binPath)).toBe("v1.1.1"); // Error with original error if it does not begin with "Coder". await fs.writeFile(binPath, old("unknown flag: --output", "Unrelated")); - await expect(cli.version(binPath)).rejects.toThrow("unknown flag"); + await expect(cliUtils.version(binPath)).rejects.toThrow("unknown flag"); // Error if no version. await fs.writeFile(binPath, old("unknown flag: --output", "Coder")); - await expect(cli.version(binPath)).rejects.toThrow("No version found"); + await expect(cliUtils.version(binPath)).rejects.toThrow("No version found"); }); it("rmOld", async () => { const binDir = path.join(tmp, "bins"); - expect(await cli.rmOld(path.join(binDir, "bin1"))).toStrictEqual([]); + expect(await cliUtils.rmOld(path.join(binDir, "bin1"))).toStrictEqual([]); await fs.mkdir(binDir, { recursive: true }); await fs.writeFile(path.join(binDir, "bin.old-1"), "echo hello"); @@ -102,7 +99,7 @@ describe("cliUtils", () => { await fs.writeFile(path.join(binDir, "bin.old-1.asc"), "echo hello"); await fs.writeFile(path.join(binDir, "bin.temp-2.asc"), "echo hello"); - expect(await cli.rmOld(path.join(binDir, "bin1"))).toStrictEqual([ + expect(await cliUtils.rmOld(path.join(binDir, "bin1"))).toStrictEqual([ { fileName: "bin.asc", error: undefined, @@ -143,12 +140,12 @@ describe("cliUtils", () => { const binPath = path.join(tmp, "hash"); await fs.writeFile(binPath, "foobar"); - expect(await cli.eTag(binPath)).toBe( + expect(await cliUtils.eTag(binPath)).toBe( "8843d7f92416211de9ebb963ff4ce28125932878", ); await fs.writeFile(binPath, "test"); - expect(await cli.eTag(binPath)).toBe( + expect(await cliUtils.eTag(binPath)).toBe( "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", ); }); diff --git a/src/core/mementoManager.test.ts b/test/unit/core/mementoManager.test.ts similarity index 93% rename from src/core/mementoManager.test.ts rename to test/unit/core/mementoManager.test.ts index f1cd6a2d..54289a65 100644 --- a/src/core/mementoManager.test.ts +++ b/test/unit/core/mementoManager.test.ts @@ -1,6 +1,8 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { InMemoryMemento } from "../__mocks__/testHelpers"; -import { MementoManager } from "./mementoManager"; +import { beforeEach, describe, expect, it } from "vitest"; + +import { MementoManager } from "@/core/mementoManager"; + +import { InMemoryMemento } from "../../mocks/testHelpers"; describe("MementoManager", () => { let memento: InMemoryMemento; diff --git a/src/core/pathResolver.test.ts b/test/unit/core/pathResolver.test.ts similarity index 92% rename from src/core/pathResolver.test.ts rename to test/unit/core/pathResolver.test.ts index 3c331a26..e0e3b4d6 100644 --- a/src/core/pathResolver.test.ts +++ b/test/unit/core/pathResolver.test.ts @@ -1,7 +1,9 @@ import * as path from "path"; -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { MockConfigurationProvider } from "../__mocks__/testHelpers"; -import { PathResolver } from "./pathResolver"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { PathResolver } from "@/core/pathResolver"; + +import { MockConfigurationProvider } from "../../mocks/testHelpers"; describe("PathResolver", () => { const basePath = diff --git a/src/core/secretsManager.test.ts b/test/unit/core/secretsManager.test.ts similarity index 87% rename from src/core/secretsManager.test.ts rename to test/unit/core/secretsManager.test.ts index a6487e0f..7100a29b 100644 --- a/src/core/secretsManager.test.ts +++ b/test/unit/core/secretsManager.test.ts @@ -1,6 +1,8 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { InMemorySecretStorage } from "../__mocks__/testHelpers"; -import { SecretsManager } from "./secretsManager"; +import { beforeEach, describe, expect, it } from "vitest"; + +import { SecretsManager } from "@/core/secretsManager"; + +import { InMemorySecretStorage } from "../../mocks/testHelpers"; describe("SecretsManager", () => { let secretStorage: InMemorySecretStorage; diff --git a/src/error.test.ts b/test/unit/error.test.ts similarity index 90% rename from src/error.test.ts rename to test/unit/error.test.ts index 84c1e14b..b606f875 100644 --- a/src/error.test.ts +++ b/test/unit/error.test.ts @@ -1,10 +1,12 @@ import axios from "axios"; import * as fs from "fs/promises"; import https from "https"; -import * as path from "path"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; -import { CertificateError, X509_ERR, X509_ERR_CODE } from "./error"; -import { Logger } from "./logging/logger"; + +import { CertificateError, X509_ERR, X509_ERR_CODE } from "@/error"; +import { type Logger } from "@/logging/logger"; + +import { getFixturePath } from "../utils/fixtures"; describe("Certificate errors", () => { // Before each test we make a request to sanity check that we really get the @@ -45,12 +47,8 @@ describe("Certificate errors", () => { async function startServer(certName: string): Promise { const server = https.createServer( { - key: await fs.readFile( - path.join(__dirname, `../fixtures/tls/${certName}.key`), - ), - cert: await fs.readFile( - path.join(__dirname, `../fixtures/tls/${certName}.crt`), - ), + key: await fs.readFile(getFixturePath("tls", `${certName}.key`)), + cert: await fs.readFile(getFixturePath("tls", `${certName}.crt`)), }, (req, res) => { if (req.url?.endsWith("/error")) { @@ -87,9 +85,7 @@ describe("Certificate errors", () => { const address = await startServer("chain-leaf"); const request = axios.get(address, { httpsAgent: new https.Agent({ - ca: await fs.readFile( - path.join(__dirname, "../fixtures/tls/chain-leaf.crt"), - ), + ca: await fs.readFile(getFixturePath("tls", "chain-leaf.crt")), }), }); await expect(request).rejects.toHaveProperty( @@ -124,9 +120,7 @@ describe("Certificate errors", () => { const address = await startServer("no-signing"); const request = axios.get(address, { httpsAgent: new https.Agent({ - ca: await fs.readFile( - path.join(__dirname, "../fixtures/tls/no-signing.crt"), - ), + ca: await fs.readFile(getFixturePath("tls", "no-signing.crt")), servername: "localhost", }), }); @@ -189,9 +183,7 @@ describe("Certificate errors", () => { const address = await startServer("self-signed"); const request = axios.get(address, { httpsAgent: new https.Agent({ - ca: await fs.readFile( - path.join(__dirname, "../fixtures/tls/self-signed.crt"), - ), + ca: await fs.readFile(getFixturePath("tls", "self-signed.crt")), servername: "localhost", }), }); @@ -234,9 +226,7 @@ describe("Certificate errors", () => { const address = await startServer("chain"); const request = axios.get(address, { httpsAgent: new https.Agent({ - ca: await fs.readFile( - path.join(__dirname, "../fixtures/tls/chain-root.crt"), - ), + ca: await fs.readFile(getFixturePath("tls", "chain-root.crt")), servername: "localhost", }), }); @@ -257,9 +247,7 @@ describe("Certificate errors", () => { const address = await startServer("chain"); const request = axios.get(address + "/error", { httpsAgent: new https.Agent({ - ca: await fs.readFile( - path.join(__dirname, "../fixtures/tls/chain-root.crt"), - ), + ca: await fs.readFile(getFixturePath("tls", "chain-root.crt")), servername: "localhost", }), }); diff --git a/src/featureSet.test.ts b/test/unit/featureSet.test.ts similarity index 94% rename from src/featureSet.test.ts rename to test/unit/featureSet.test.ts index e3c45d3c..919f7089 100644 --- a/src/featureSet.test.ts +++ b/test/unit/featureSet.test.ts @@ -1,6 +1,7 @@ import * as semver from "semver"; import { describe, expect, it } from "vitest"; -import { featureSetForVersion } from "./featureSet"; + +import { featureSetForVersion } from "@/featureSet"; describe("check version support", () => { it("has logs", () => { diff --git a/src/globalFlags.test.ts b/test/unit/globalFlags.test.ts similarity index 95% rename from src/globalFlags.test.ts rename to test/unit/globalFlags.test.ts index 307500e7..d570d609 100644 --- a/src/globalFlags.test.ts +++ b/test/unit/globalFlags.test.ts @@ -1,6 +1,7 @@ import { it, expect, describe } from "vitest"; -import { WorkspaceConfiguration } from "vscode"; -import { getGlobalFlags } from "./globalFlags"; +import { type WorkspaceConfiguration } from "vscode"; + +import { getGlobalFlags } from "@/globalFlags"; describe("Global flags suite", () => { it("should return global-config and header args when no global flags configured", () => { diff --git a/src/headers.test.ts b/test/unit/headers.test.ts similarity index 95% rename from src/headers.test.ts rename to test/unit/headers.test.ts index 6f2933a3..b2c29e22 100644 --- a/src/headers.test.ts +++ b/test/unit/headers.test.ts @@ -1,8 +1,9 @@ import * as os from "os"; -import { it, expect, describe, beforeEach, afterEach, vi } from "vitest"; -import { WorkspaceConfiguration } from "vscode"; -import { getHeaderCommand, getHeaders } from "./headers"; -import { Logger } from "./logging/logger"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { type WorkspaceConfiguration } from "vscode"; + +import { getHeaderCommand, getHeaders } from "@/headers"; +import { type Logger } from "@/logging/logger"; const logger: Logger = { trace: () => {}, diff --git a/src/pgp.test.ts b/test/unit/pgp.test.ts similarity index 85% rename from src/pgp.test.ts rename to test/unit/pgp.test.ts index 6eeff95b..73faa99b 100644 --- a/src/pgp.test.ts +++ b/test/unit/pgp.test.ts @@ -2,22 +2,19 @@ import fs from "fs/promises"; import * as openpgp from "openpgp"; import path from "path"; import { describe, expect, it } from "vitest"; -import * as pgp from "./pgp"; + +import * as pgp from "@/pgp"; + +import { getFixturePath } from "../utils/fixtures"; describe("pgp", () => { // This contains two keys, like Coder's. - const publicKeysPath = path.join(__dirname, "../fixtures/pgp/public.pgp"); + const publicKeysPath = getFixturePath("pgp", "public.pgp"); // Just a text file, not an actual binary. - const cliPath = path.join(__dirname, "../fixtures/pgp/cli"); - const invalidSignaturePath = path.join( - __dirname, - "../fixtures/pgp/cli.invalid.asc", - ); + const cliPath = getFixturePath("pgp", "cli"); + const invalidSignaturePath = getFixturePath("pgp", "cli.invalid.asc"); // This is signed with the second key, like Coder's. - const validSignaturePath = path.join( - __dirname, - "../fixtures/pgp/cli.valid.asc", - ); + const validSignaturePath = getFixturePath("pgp", "cli.valid.asc"); it("reads bundled public keys", async () => { const keys = await pgp.readPublicKeys(); diff --git a/src/sshConfig.test.ts b/test/unit/remote/sshConfig.test.ts similarity index 99% rename from src/sshConfig.test.ts rename to test/unit/remote/sshConfig.test.ts index 1e4cb785..cfc48c74 100644 --- a/src/sshConfig.test.ts +++ b/test/unit/remote/sshConfig.test.ts @@ -1,6 +1,6 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ import { it, afterEach, vi, expect } from "vitest"; -import { SSHConfig } from "./sshConfig"; + +import { SSHConfig } from "@/remote/sshConfig"; // This is not the usual path to ~/.ssh/config, but // setting it to a different path makes it easier to test diff --git a/src/sshSupport.test.ts b/test/unit/remote/sshSupport.test.ts similarity index 99% rename from src/sshSupport.test.ts rename to test/unit/remote/sshSupport.test.ts index 050b7bb2..bb152bd8 100644 --- a/src/sshSupport.test.ts +++ b/test/unit/remote/sshSupport.test.ts @@ -1,9 +1,10 @@ import { it, expect } from "vitest"; + import { computeSSHProperties, sshSupportsSetEnv, sshVersionSupportsSetEnv, -} from "./sshSupport"; +} from "@/remote/sshSupport"; const supports = { "OpenSSH_8.9p1 Ubuntu-3ubuntu0.1, OpenSSL 3.0.2 15 Mar 2022": true, diff --git a/src/util.test.ts b/test/unit/util.test.ts similarity index 99% rename from src/util.test.ts rename to test/unit/util.test.ts index 8f40e656..d508f41c 100644 --- a/src/util.test.ts +++ b/test/unit/util.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from "vitest"; -import { countSubstring, parseRemoteAuthority, toSafeHost } from "./util"; + +import { countSubstring, parseRemoteAuthority, toSafeHost } from "@/util"; it("ignore unrelated authorities", () => { const tests = [ diff --git a/test/utils/fixtures.ts b/test/utils/fixtures.ts new file mode 100644 index 00000000..0b6c66d6 --- /dev/null +++ b/test/utils/fixtures.ts @@ -0,0 +1,5 @@ +import path from "path"; + +const testDir = path.join(__dirname, ".."); +export const getFixturePath = (...parts: string[]) => + path.join(testDir, "fixtures", ...parts); diff --git a/tsconfig.json b/tsconfig.json index 0974a4d1..78cc9654 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, + "baseUrl": ".", "paths": { // axios contains both an index.d.ts and index.d.cts which apparently have // conflicting types. For some reason TypeScript is reading both and @@ -20,5 +21,5 @@ } }, "exclude": ["node_modules"], - "include": ["src/**/*"] + "include": ["src"] } diff --git a/vitest.config.ts b/vitest.config.ts index af067d95..01e3896a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,24 +3,25 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - include: ["src/**/*.test.ts"], + globals: true, + environment: "node", + include: ["test/unit/**/*.test.ts", "test/integration/**/*.test.ts"], exclude: [ + "test/integration/**", "**/node_modules/**", - "**/dist/**", - "**/build/**", "**/out/**", - "**/src/test/**", - "src/test/**", - "./src/test/**", + "**/*.d.ts", ], - environment: "node", + pool: "threads", + fileParallelism: true, coverage: { provider: "v8", }, }, resolve: { alias: { - vscode: path.resolve(__dirname, "src/__mocks__/vscode.runtime.ts"), + "@": path.resolve(__dirname, "src"), + vscode: path.resolve(__dirname, "test/mocks/vscode.runtime.ts"), }, }, }); diff --git a/yarn.lock b/yarn.lock index 581e7d3a..a067635f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -341,6 +341,28 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz#f13c7c205915eb91ae54c557f5e92bddd8be0e83" integrity sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ== +"@emnapi/core@^1.4.3": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.5.0.tgz#85cd84537ec989cebb2343606a1ee663ce4edaf0" + integrity sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg== + dependencies: + "@emnapi/wasi-threads" "1.1.0" + tslib "^2.4.0" + +"@emnapi/runtime@^1.4.3": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.5.0.tgz#9aebfcb9b17195dce3ab53c86787a6b7d058db73" + integrity sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ== + dependencies: + tslib "^2.4.0" + +"@emnapi/wasi-threads@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf" + integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ== + dependencies: + tslib "^2.4.0" + "@esbuild/aix-ppc64@0.25.9": version "0.25.9" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9" @@ -471,17 +493,17 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz#585624dc829cfb6e7c0aa6c3ca7d7e6daa87e34f" integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ== -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" - integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0": + version "4.9.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz#7308df158e064f0dd8b8fdb58aa14fa2a7f913b3" + integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g== dependencies: - eslint-visitor-keys "^3.3.0" + eslint-visitor-keys "^3.4.3" -"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.9.1.tgz#449dfa81a57a1d755b09aa58d826c1262e4283b4" - integrity sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA== +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== "@eslint/eslintrc@^2.1.4": version "2.1.4" @@ -691,6 +713,15 @@ "@jsonjoy.com/buffers" "^1.0.0" "@jsonjoy.com/codegen" "^1.0.0" +"@napi-rs/wasm-runtime@^0.2.11": + version "0.2.12" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" + integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ== + dependencies: + "@emnapi/core" "^1.4.3" + "@emnapi/runtime" "^1.4.3" + "@tybys/wasm-util" "^0.10.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -717,10 +748,10 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@pkgr/core@^0.2.4": - version "0.2.7" - resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.7.tgz#eb5014dfd0b03e7f3ba2eeeff506eed89b028058" - integrity sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg== +"@pkgr/core@^0.2.9": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b" + integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA== "@rollup/rollup-android-arm-eabi@4.50.2": version "4.50.2" @@ -991,6 +1022,13 @@ resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== +"@tybys/wasm-util@^0.10.0": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" + integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg== + dependencies: + tslib "^2.4.0" + "@types/chai@^5.2.2": version "5.2.2" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.2.tgz#6f14cea18180ffc4416bc0fd12be05fdd73bdd6b" @@ -1044,7 +1082,7 @@ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== -"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -1088,10 +1126,10 @@ resolved "https://registry.yarnpkg.com/@types/sarif/-/sarif-2.1.7.tgz#dab4d16ba7568e9846c454a8764f33c5d98e5524" integrity sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ== -"@types/semver@^7.5.0": - version "7.5.3" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.3.tgz#9a726e116beb26c24f1ccd6850201e1246122e04" - integrity sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw== +"@types/semver@^7.7.1": + version "7.7.1" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.1.tgz#3ce3af1a5524ef327d2da9e4fd8b6d95c8d70528" + integrity sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA== "@types/ua-parser-js@0.7.36": version "0.7.36" @@ -1115,126 +1153,103 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.0.tgz#62cda0d35bbf601683c6e58cf5d04f0275caca4e" - integrity sha512-M72SJ0DkcQVmmsbqlzc6EJgb/3Oz2Wdm6AyESB4YkGgCxP8u5jt5jn4/OBMPK3HLOxcttZq5xbBBU7e2By4SZQ== - dependencies: - "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "7.0.0" - "@typescript-eslint/type-utils" "7.0.0" - "@typescript-eslint/utils" "7.0.0" - "@typescript-eslint/visitor-keys" "7.0.0" - debug "^4.3.4" +"@typescript-eslint/eslint-plugin@^8.44.0": + version "8.44.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.1.tgz#011a2b5913d297b3d9d77f64fb78575bab01a1b3" + integrity sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.44.1" + "@typescript-eslint/type-utils" "8.44.1" + "@typescript-eslint/utils" "8.44.1" + "@typescript-eslint/visitor-keys" "8.44.1" graphemer "^1.4.0" - ignore "^5.2.4" + ignore "^7.0.0" natural-compare "^1.4.0" - semver "^7.5.4" - ts-api-utils "^1.0.1" + ts-api-utils "^2.1.0" -"@typescript-eslint/parser@^6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" - integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== +"@typescript-eslint/parser@^8.44.0": + version "8.44.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.44.1.tgz#d4c85791389462823596ad46e2b90d34845e05eb" + integrity sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw== dependencies: - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/scope-manager" "8.44.1" + "@typescript-eslint/types" "8.44.1" + "@typescript-eslint/typescript-estree" "8.44.1" + "@typescript-eslint/visitor-keys" "8.44.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" - integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== +"@typescript-eslint/project-service@8.44.1": + version "8.44.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.44.1.tgz#1bccd9796d25032b190f355f55c5fde061158abb" + integrity sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA== dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - -"@typescript-eslint/scope-manager@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.0.0.tgz#15ea9abad2b56fc8f5c0b516775f41c86c5c8685" - integrity sha512-IxTStwhNDPO07CCrYuAqjuJ3Xf5MrMaNgbAZPxFXAUpAtwqFxiuItxUaVtP/SJQeCdJjwDGh9/lMOluAndkKeg== - dependencies: - "@typescript-eslint/types" "7.0.0" - "@typescript-eslint/visitor-keys" "7.0.0" + "@typescript-eslint/tsconfig-utils" "^8.44.1" + "@typescript-eslint/types" "^8.44.1" + debug "^4.3.4" -"@typescript-eslint/type-utils@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.0.0.tgz#a4c7ae114414e09dbbd3c823b5924793f7483252" - integrity sha512-FIM8HPxj1P2G7qfrpiXvbHeHypgo2mFpFGoh5I73ZlqmJOsloSa1x0ZyXCer43++P1doxCgNqIOLqmZR6SOT8g== +"@typescript-eslint/scope-manager@8.44.1": + version "8.44.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.44.1.tgz#31c27f92e4aed8d0f4d6fe2b9e5187d1d8797bd7" + integrity sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg== dependencies: - "@typescript-eslint/typescript-estree" "7.0.0" - "@typescript-eslint/utils" "7.0.0" - debug "^4.3.4" - ts-api-utils "^1.0.1" + "@typescript-eslint/types" "8.44.1" + "@typescript-eslint/visitor-keys" "8.44.1" -"@typescript-eslint/types@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" - integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== +"@typescript-eslint/tsconfig-utils@8.44.1", "@typescript-eslint/tsconfig-utils@^8.44.1": + version "8.44.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.1.tgz#e1d9d047078fac37d3e638484ab3b56215963342" + integrity sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ== -"@typescript-eslint/types@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.0.0.tgz#2e5889c7fe3c873fc6dc6420aa77775f17cd5dc6" - integrity sha512-9ZIJDqagK1TTs4W9IyeB2sH/s1fFhN9958ycW8NRTg1vXGzzH5PQNzq6KbsbVGMT+oyyfa17DfchHDidcmf5cg== - -"@typescript-eslint/typescript-estree@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" - integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== +"@typescript-eslint/type-utils@8.44.1": + version "8.44.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.44.1.tgz#be9d31e0f911d17ee8ac99921bb74cf1f9df3906" + integrity sha512-KdEerZqHWXsRNKjF9NYswNISnFzXfXNDfPxoTh7tqohU/PRIbwTmsjGK6V9/RTYWau7NZvfo52lgVk+sJh0K3g== dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/types" "8.44.1" + "@typescript-eslint/typescript-estree" "8.44.1" + "@typescript-eslint/utils" "8.44.1" debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/typescript-estree@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.0.tgz#7ce66f2ce068517f034f73fba9029300302fdae9" - integrity sha512-JzsOzhJJm74aQ3c9um/aDryHgSHfaX8SHFIu9x4Gpik/+qxLvxUylhTsO9abcNu39JIdhY2LgYrFxTii3IajLA== - dependencies: - "@typescript-eslint/types" "7.0.0" - "@typescript-eslint/visitor-keys" "7.0.0" + ts-api-utils "^2.1.0" + +"@typescript-eslint/types@8.44.1", "@typescript-eslint/types@^8.44.1": + version "8.44.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.44.1.tgz#85d1cad1290a003ff60420388797e85d1c3f76ff" + integrity sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ== + +"@typescript-eslint/typescript-estree@8.44.1": + version "8.44.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.1.tgz#4f17650e5adabecfcc13cd8c517937a4ef5cd424" + integrity sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A== + dependencies: + "@typescript-eslint/project-service" "8.44.1" + "@typescript-eslint/tsconfig-utils" "8.44.1" + "@typescript-eslint/types" "8.44.1" + "@typescript-eslint/visitor-keys" "8.44.1" debug "^4.3.4" - globby "^11.1.0" + fast-glob "^3.3.2" is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/utils@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.0.0.tgz#e43710af746c6ae08484f7afc68abc0212782c7e" - integrity sha512-kuPZcPAdGcDBAyqDn/JVeJVhySvpkxzfXjJq1X1BFSTYo1TTuo4iyb937u457q4K0In84p6u2VHQGaFnv7VYqg== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "7.0.0" - "@typescript-eslint/types" "7.0.0" - "@typescript-eslint/typescript-estree" "7.0.0" - semver "^7.5.4" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.1.0" -"@typescript-eslint/visitor-keys@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" - integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== +"@typescript-eslint/utils@8.44.1": + version "8.44.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.44.1.tgz#f23d48eb90791a821dc17d4f67bb96faeb75d63d" + integrity sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg== dependencies: - "@typescript-eslint/types" "6.21.0" - eslint-visitor-keys "^3.4.1" + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/scope-manager" "8.44.1" + "@typescript-eslint/types" "8.44.1" + "@typescript-eslint/typescript-estree" "8.44.1" -"@typescript-eslint/visitor-keys@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.0.tgz#83cdadd193ee735fe9ea541f6a2b4d76dfe62081" - integrity sha512-JZP0uw59PRHp7sHQl3aF/lFgwOW2rgNVnXUksj1d932PMita9wFBd3621vHQRDvHwPsSY9FMAAHVc8gTvLYY4w== +"@typescript-eslint/visitor-keys@8.44.1": + version "8.44.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.1.tgz#1d96197a7fcceaba647b3bd6a8594df8dc4deb5a" + integrity sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw== dependencies: - "@typescript-eslint/types" "7.0.0" - eslint-visitor-keys "^3.4.1" + "@typescript-eslint/types" "8.44.1" + eslint-visitor-keys "^4.2.1" "@typespec/ts-http-runtime@^0.3.0": version "0.3.0" @@ -1246,9 +1261,106 @@ tslib "^2.6.2" "@ungap/structured-clone@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" - integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + version "1.3.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + +"@unrs/resolver-binding-android-arm-eabi@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz#9f5b04503088e6a354295e8ea8fe3cb99e43af81" + integrity sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw== + +"@unrs/resolver-binding-android-arm64@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz#7414885431bd7178b989aedc4d25cccb3865bc9f" + integrity sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g== + +"@unrs/resolver-binding-darwin-arm64@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz#b4a8556f42171fb9c9f7bac8235045e82aa0cbdf" + integrity sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g== + +"@unrs/resolver-binding-darwin-x64@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz#fd4d81257b13f4d1a083890a6a17c00de571f0dc" + integrity sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ== + +"@unrs/resolver-binding-freebsd-x64@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz#d2513084d0f37c407757e22f32bd924a78cfd99b" + integrity sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw== + +"@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz#844d2605d057488d77fab09705f2866b86164e0a" + integrity sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw== + +"@unrs/resolver-binding-linux-arm-musleabihf@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz#204892995cefb6bd1d017d52d097193bc61ddad3" + integrity sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw== + +"@unrs/resolver-binding-linux-arm64-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz#023eb0c3aac46066a10be7a3f362e7b34f3bdf9d" + integrity sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ== + +"@unrs/resolver-binding-linux-arm64-musl@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz#9e6f9abb06424e3140a60ac996139786f5d99be0" + integrity sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w== + +"@unrs/resolver-binding-linux-ppc64-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz#b111417f17c9d1b02efbec8e08398f0c5527bb44" + integrity sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA== + +"@unrs/resolver-binding-linux-riscv64-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz#92ffbf02748af3e99873945c9a8a5ead01d508a9" + integrity sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ== + +"@unrs/resolver-binding-linux-riscv64-musl@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz#0bec6f1258fc390e6b305e9ff44256cb207de165" + integrity sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew== + +"@unrs/resolver-binding-linux-s390x-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz#577843a084c5952f5906770633ccfb89dac9bc94" + integrity sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg== + +"@unrs/resolver-binding-linux-x64-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz#36fb318eebdd690f6da32ac5e0499a76fa881935" + integrity sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w== + +"@unrs/resolver-binding-linux-x64-musl@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz#bfb9af75f783f98f6a22c4244214efe4df1853d6" + integrity sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA== + +"@unrs/resolver-binding-wasm32-wasi@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz#752c359dd875684b27429500d88226d7cc72f71d" + integrity sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ== + dependencies: + "@napi-rs/wasm-runtime" "^0.2.11" + +"@unrs/resolver-binding-win32-arm64-msvc@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz#ce5735e600e4c2fbb409cd051b3b7da4a399af35" + integrity sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw== + +"@unrs/resolver-binding-win32-ia32-msvc@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz#72fc57bc7c64ec5c3de0d64ee0d1810317bc60a6" + integrity sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ== + +"@unrs/resolver-binding-win32-x64-msvc@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz#538b1e103bf8d9864e7b85cc96fa8d6fb6c40777" + integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g== "@vitest/coverage-v8@^3.2.4": version "3.2.4" @@ -1776,54 +1888,60 @@ array-buffer-byte-length@^1.0.1: call-bind "^1.0.5" is-array-buffer "^3.0.4" -array-includes@^3.1.8: - version "3.1.8" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" - integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== +array-buffer-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - get-intrinsic "^1.2.4" - is-string "^1.0.7" + call-bound "^1.0.3" + is-array-buffer "^3.0.5" -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +array-includes@^3.1.9: + version "3.1.9" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.9.tgz#1f0ccaa08e90cdbc3eb433210f903ad0f17c3f3a" + integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-abstract "^1.24.0" + es-object-atoms "^1.1.1" + get-intrinsic "^1.3.0" + is-string "^1.1.1" + math-intrinsics "^1.1.0" -array.prototype.findlastindex@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" - integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== +array.prototype.findlastindex@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz#cfa1065c81dcb64e34557c9b81d012f6a421c564" + integrity sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" define-properties "^1.2.1" - es-abstract "^1.23.2" + es-abstract "^1.23.9" es-errors "^1.3.0" - es-object-atoms "^1.0.0" - es-shim-unscopables "^1.0.2" + es-object-atoms "^1.1.1" + es-shim-unscopables "^1.1.0" -array.prototype.flat@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" - integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== +array.prototype.flat@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" + integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" -array.prototype.flatmap@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" - integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== +array.prototype.flatmap@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b" + integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" arraybuffer.prototype.slice@^1.0.2: version "1.0.2" @@ -1852,6 +1970,19 @@ arraybuffer.prototype.slice@^1.0.3: is-array-buffer "^3.0.4" is-shared-array-buffer "^1.0.2" +arraybuffer.prototype.slice@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + is-array-buffer "^3.0.4" + assertion-error@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" @@ -1883,6 +2014,11 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" + integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -2107,7 +2243,7 @@ caching-transform@^4.0.0: package-hash "^4.0.0" write-file-atomic "^3.0.0" -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== @@ -2143,6 +2279,24 @@ call-bind@^1.0.6, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -2471,7 +2625,16 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: +cross-spawn@^7.0.0, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +cross-spawn@^7.0.2, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -2510,6 +2673,15 @@ data-view-buffer@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" +data-view-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" + integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + data-view-byte-length@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" @@ -2519,6 +2691,15 @@ data-view-byte-length@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" +data-view-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735" + integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + data-view-byte-offset@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" @@ -2528,6 +2709,15 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +data-view-byte-offset@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" + integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-data-view "^1.0.1" + date-fns@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" @@ -2694,13 +2884,6 @@ diff@^7.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -2745,7 +2928,7 @@ domutils@^3.0.1: domelementtype "^2.3.0" domhandler "^5.0.1" -dunder-proto@^1.0.1: +dunder-proto@^1.0.0, dunder-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== @@ -2977,6 +3160,66 @@ es-abstract@^1.23.0, es-abstract@^1.23.2: unbox-primitive "^1.0.2" which-typed-array "^1.1.15" +es-abstract@^1.23.5, es-abstract@^1.23.9, es-abstract@^1.24.0: + version "1.24.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.0.tgz#c44732d2beb0acc1ed60df840869e3106e7af328" + integrity sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg== + dependencies: + array-buffer-byte-length "^1.0.2" + arraybuffer.prototype.slice "^1.0.4" + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + data-view-buffer "^1.0.2" + data-view-byte-length "^1.0.2" + data-view-byte-offset "^1.0.1" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + es-set-tostringtag "^2.1.0" + es-to-primitive "^1.3.0" + function.prototype.name "^1.1.8" + get-intrinsic "^1.3.0" + get-proto "^1.0.1" + get-symbol-description "^1.1.0" + globalthis "^1.0.4" + gopd "^1.2.0" + has-property-descriptors "^1.0.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + internal-slot "^1.1.0" + is-array-buffer "^3.0.5" + is-callable "^1.2.7" + is-data-view "^1.0.2" + is-negative-zero "^2.0.3" + is-regex "^1.2.1" + is-set "^2.0.3" + is-shared-array-buffer "^1.0.4" + is-string "^1.1.1" + is-typed-array "^1.1.15" + is-weakref "^1.1.1" + math-intrinsics "^1.1.0" + object-inspect "^1.13.4" + object-keys "^1.1.1" + object.assign "^4.1.7" + own-keys "^1.0.1" + regexp.prototype.flags "^1.5.4" + safe-array-concat "^1.1.3" + safe-push-apply "^1.0.0" + safe-regex-test "^1.1.0" + set-proto "^1.0.0" + stop-iteration-iterator "^1.1.0" + string.prototype.trim "^1.2.10" + string.prototype.trimend "^1.0.9" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.3" + typed-array-byte-length "^1.0.3" + typed-array-byte-offset "^1.0.4" + typed-array-length "^1.0.7" + unbox-primitive "^1.1.0" + which-typed-array "^1.1.19" + es-define-property@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" @@ -3041,13 +3284,6 @@ es-set-tostringtag@^2.1.0: has-tostringtag "^1.0.2" hasown "^2.0.2" -es-shim-unscopables@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" - integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== - dependencies: - has "^1.0.3" - es-shim-unscopables@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" @@ -3055,6 +3291,13 @@ es-shim-unscopables@^1.0.2: dependencies: hasown "^2.0.0" +es-shim-unscopables@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5" + integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw== + dependencies: + hasown "^2.0.2" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -3064,6 +3307,15 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es-to-primitive@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== + dependencies: + is-callable "^1.2.7" + is-date-object "^1.0.5" + is-symbol "^1.0.4" + es6-error@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" @@ -3127,16 +3379,24 @@ escodegen@^2.1.0: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" - integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== +eslint-config-prettier@^10.1.8: + version "10.1.8" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97" + integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w== eslint-fix-utils@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/eslint-fix-utils/-/eslint-fix-utils-0.4.0.tgz#e1085b4f94f41e7448a80b774d8ed5cbbe7f7e31" integrity sha512-nCEciwqByGxsKiWqZjqK7xfL+7dUX9Pi0UL3J0tOwfxVN9e6Y59UxEt1ZYsc3XH0ce6T1WQM/QU2DbKK/6IG7g== +eslint-import-context@^0.1.8: + version "0.1.9" + resolved "https://registry.yarnpkg.com/eslint-import-context/-/eslint-import-context-0.1.9.tgz#967b0b2f0a90ef4b689125e088f790f0b7756dbe" + integrity sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg== + dependencies: + get-tsconfig "^4.10.1" + stable-hash-x "^0.2.0" + eslint-import-resolver-node@^0.3.9: version "0.3.9" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" @@ -3146,36 +3406,49 @@ eslint-import-resolver-node@^0.3.9: is-core-module "^2.13.0" resolve "^1.22.4" -eslint-module-utils@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b" - integrity sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg== +eslint-import-resolver-typescript@^4.4.4: + version "4.4.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz#3e83a9c25f4a053fe20e1b07b47e04e8519a8720" + integrity sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw== + dependencies: + debug "^4.4.1" + eslint-import-context "^0.1.8" + get-tsconfig "^4.10.1" + is-bun-module "^2.0.0" + stable-hash-x "^0.2.0" + tinyglobby "^0.2.14" + unrs-resolver "^1.7.11" + +eslint-module-utils@^2.12.1: + version "2.12.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz#f76d3220bfb83c057651359295ab5854eaad75ff" + integrity sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw== dependencies: debug "^3.2.7" -eslint-plugin-import@^2.31.0: - version "2.31.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7" - integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== +eslint-plugin-import@^2.32.0: + version "2.32.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980" + integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA== dependencies: "@rtsao/scc" "^1.1.0" - array-includes "^3.1.8" - array.prototype.findlastindex "^1.2.5" - array.prototype.flat "^1.3.2" - array.prototype.flatmap "^1.3.2" + array-includes "^3.1.9" + array.prototype.findlastindex "^1.2.6" + array.prototype.flat "^1.3.3" + array.prototype.flatmap "^1.3.3" debug "^3.2.7" doctrine "^2.1.0" eslint-import-resolver-node "^0.3.9" - eslint-module-utils "^2.12.0" + eslint-module-utils "^2.12.1" hasown "^2.0.2" - is-core-module "^2.15.1" + is-core-module "^2.16.1" is-glob "^4.0.3" minimatch "^3.1.2" object.fromentries "^2.0.8" object.groupby "^1.0.3" - object.values "^1.2.0" + object.values "^1.2.1" semver "^6.3.1" - string.prototype.trimend "^1.0.8" + string.prototype.trimend "^1.0.9" tsconfig-paths "^3.15.0" eslint-plugin-md@^1.0.19: @@ -3243,11 +3516,16 @@ eslint-visitor-keys@^1.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== -eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: +eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== +eslint-visitor-keys@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" + integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== + eslint@^6.8.0: version "6.8.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" @@ -3358,13 +3636,20 @@ esprima@^4.0.0, esprima@^4.0.1: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.0.1, esquery@^1.4.2: +esquery@^1.0.1: version "1.5.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== dependencies: estraverse "^5.1.0" +esquery@^1.4.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" @@ -3445,18 +3730,7 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== -fast-glob@^3.2.9: - version "3.2.12" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-glob@^3.3.3: +fast-glob@^3.3.2, fast-glob@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== @@ -3589,11 +3863,12 @@ flat-cache@^2.0.1: write "1.0.3" flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: - flatted "^3.1.0" + flatted "^3.2.9" + keyv "^4.5.3" rimraf "^3.0.2" flat@^5.0.2: @@ -3606,10 +3881,10 @@ flatted@^2.0.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== -flatted@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +flatted@^3.2.9: + version "3.3.3" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== follow-redirects@^1.15.6: version "1.15.6" @@ -3623,6 +3898,13 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== + dependencies: + is-callable "^1.2.7" + foreground-child@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53" @@ -3736,6 +4018,18 @@ function.prototype.name@^1.1.6: es-abstract "^1.22.1" functions-have-names "^1.2.3" +function.prototype.name@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" + integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + functions-have-names "^1.2.3" + hasown "^2.0.2" + is-callable "^1.2.7" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" @@ -3802,7 +4096,7 @@ get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" -get-intrinsic@^1.2.6: +get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -3823,7 +4117,7 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-proto@^1.0.1: +get-proto@^1.0.0, get-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== @@ -3848,6 +4142,22 @@ get-symbol-description@^1.0.2: es-errors "^1.3.0" get-intrinsic "^1.2.4" +get-symbol-description@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + +get-tsconfig@^4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.1.tgz#d34c1c01f47d65a606c37aa7a177bc3e56ab4b2e" + integrity sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ== + dependencies: + resolve-pkg-maps "^1.0.0" + get-uri@^6.0.1: version "6.0.3" resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.3.tgz#0d26697bc13cf91092e519aa63aa60ee5b6f385a" @@ -3941,9 +4251,9 @@ globals@^12.1.0: type-fest "^0.8.1" globals@^13.19.0: - version "13.22.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.22.0.tgz#0c9fcb9c48a2494fbb5edbfee644285543eba9d8" - integrity sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw== + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" @@ -3954,17 +4264,13 @@ globalthis@^1.0.3: dependencies: define-properties "^1.1.3" -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== +globalthis@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" + define-properties "^1.2.1" + gopd "^1.0.1" globby@^14.1.0: version "14.1.0" @@ -4046,6 +4352,13 @@ has-proto@^1.0.3: resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== +has-proto@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== + dependencies: + dunder-proto "^1.0.0" + has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -4181,12 +4494,12 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.2.0, ignore@^5.2.4: +ignore@^5.2.0: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== -ignore@^7.0.3: +ignore@^7.0.0, ignore@^7.0.3: version "7.0.5" resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== @@ -4282,6 +4595,15 @@ internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" +internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.2" + side-channel "^1.1.0" + interpret@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" @@ -4344,6 +4666,26 @@ is-array-buffer@^3.0.4: call-bind "^1.0.2" get-intrinsic "^1.2.1" +is-array-buffer@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + +is-async-function@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523" + integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ== + dependencies: + async-function "^1.0.0" + call-bound "^1.0.3" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" @@ -4351,6 +4693,13 @@ is-bigint@^1.0.1: dependencies: has-bigints "^1.0.1" +is-bigint@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== + dependencies: + has-bigints "^1.0.2" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -4366,23 +4715,45 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-boolean-object@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" + integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + is-buffer@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== +is-bun-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-2.0.0.tgz#4d7859a87c0fcac950c95e666730e745eae8bddd" + integrity sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ== + dependencies: + semver "^7.7.1" + is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.13.0, is-core-module@^2.15.1, is-core-module@^2.9.0: +is-core-module@^2.13.0, is-core-module@^2.9.0: version "2.15.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== dependencies: hasown "^2.0.2" +is-core-module@^2.16.1: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + is-data-view@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" @@ -4390,6 +4761,15 @@ is-data-view@^1.0.1: dependencies: is-typed-array "^1.1.13" +is-data-view@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== + dependencies: + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + is-typed-array "^1.1.13" + is-date-object@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" @@ -4397,6 +4777,14 @@ is-date-object@^1.0.1: dependencies: has-tostringtag "^1.0.0" +is-date-object@^1.0.5, is-date-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + is-decimal@^1.0.0, is-decimal@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" @@ -4412,6 +4800,13 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== +is-finalizationregistry@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== + dependencies: + call-bound "^1.0.3" + is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" @@ -4422,6 +4817,16 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-generator-function@^1.0.10: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.0.tgz#bf3eeda931201394f57b5dba2800f91a238309ca" + integrity sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ== + dependencies: + call-bound "^1.0.3" + get-proto "^1.0.0" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -4446,6 +4851,11 @@ is-interactive@^2.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-2.0.0.tgz#40c57614593826da1100ade6059778d597f16e90" integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ== +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" @@ -4463,6 +4873,14 @@ is-number-object@^1.0.4: dependencies: has-tostringtag "^1.0.0" +is-number-object@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -4498,6 +4916,21 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + is-shared-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" @@ -4512,6 +4945,13 @@ is-shared-array-buffer@^1.0.3: dependencies: call-bind "^1.0.7" +is-shared-array-buffer@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== + dependencies: + call-bound "^1.0.3" + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -4524,6 +4964,14 @@ is-string@^1.0.5, is-string@^1.0.7: dependencies: has-tostringtag "^1.0.0" +is-string@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" @@ -4531,6 +4979,15 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" +is-symbol@^1.0.4, is-symbol@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== + dependencies: + call-bound "^1.0.2" + has-symbols "^1.1.0" + safe-regex-test "^1.1.0" + is-typed-array@^1.1.10, is-typed-array@^1.1.9: version "1.1.10" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" @@ -4556,6 +5013,13 @@ is-typed-array@^1.1.13: dependencies: which-typed-array "^1.1.14" +is-typed-array@^1.1.14, is-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== + dependencies: + which-typed-array "^1.1.16" + is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -4576,6 +5040,11 @@ is-unicode-supported@^2.0.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz#09f0ab0de6d3744d48d265ebb98f65d11f2a9b3a" integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ== +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" @@ -4583,6 +5052,21 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +is-weakref@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293" + integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== + dependencies: + call-bound "^1.0.3" + +is-weakset@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== + dependencies: + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + is-whitespace-character@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7" @@ -4794,6 +5278,11 @@ jsesc@^3.0.2: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -4827,9 +5316,9 @@ json5@^2.2.2, json5@^2.2.3: integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonc-eslint-parser@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.0.tgz#74ded53f9d716e8d0671bd167bf5391f452d5461" - integrity sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg== + version "2.4.1" + resolved "https://registry.yarnpkg.com/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.1.tgz#64a8ed77311d33ac450725c1a438132dd87b2b3b" + integrity sha512-uuPNLJkKN8NXAlZlQ6kmUF9qO+T6Kyd7oV4+/7yy8Jz6+MZNyhPq8EdLpdfnPVzUC8qSf1b4j1azKaGnFsjmsw== dependencies: acorn "^8.5.0" eslint-visitor-keys "^3.0.0" @@ -4901,6 +5390,13 @@ keytar@^7.7.0: node-addon-api "^4.3.0" prebuild-install "^7.0.1" +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + kind-of@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -5120,7 +5616,7 @@ markdown-escapes@^1.0.0: resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== -markdown-eslint-parser@^1.2.0: +markdown-eslint-parser@^1.2.0, markdown-eslint-parser@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/markdown-eslint-parser/-/markdown-eslint-parser-1.2.1.tgz#adea20fd36d08c593a446b39418df0e393eda716" integrity sha512-ImxZH4YUT1BsYrusLPL8tWSZYUN4EZSjaSNL7KC8nsAYWavUgcK/Y1CuufbbkoSlqzv/tjFYLpyxcsaxo97dEA== @@ -5193,12 +5689,12 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0, merge2@^1.4.1: +merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^4.0.0, micromatch@^4.0.4, micromatch@^4.0.8: +micromatch@^4.0.0, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -5238,13 +5734,6 @@ mimic-response@^3.1.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== -minimatch@9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - minimatch@^10.0.3: version "10.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa" @@ -5334,6 +5823,11 @@ napi-build-utils@^1.0.1: resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== +napi-postinstall@^0.3.0: + version "0.3.3" + resolved "https://registry.yarnpkg.com/napi-postinstall/-/napi-postinstall-0.3.3.tgz#93d045c6b576803ead126711d3093995198c6eb9" + integrity sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -5460,6 +5954,11 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== +object-inspect@^1.13.3, object-inspect@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -5485,6 +5984,18 @@ object.assign@^4.1.5: has-symbols "^1.0.3" object-keys "^1.1.1" +object.assign@^4.1.7: + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" + object-keys "^1.1.1" + object.fromentries@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" @@ -5504,12 +6015,13 @@ object.groupby@^1.0.3: define-properties "^1.2.1" es-abstract "^1.23.2" -object.values@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" - integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== +object.values@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" + integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" es-object-atoms "^1.0.0" @@ -5593,6 +6105,15 @@ os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== +own-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" + integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== + dependencies: + get-intrinsic "^1.2.6" + object-keys "^1.1.1" + safe-push-apply "^1.0.0" + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -5780,11 +6301,6 @@ path-scurry@^2.0.0: lru-cache "^11.0.0" minipass "^7.1.2" -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - path-type@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-6.0.0.tgz#2f1bb6791a91ce99194caede5d6c5920ed81eb51" @@ -6065,6 +6581,20 @@ rechoir@^0.8.0: dependencies: resolve "^1.20.0" +reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" + integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.9" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.7" + get-proto "^1.0.1" + which-builtin-type "^1.2.1" + regexp.prototype.flags@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" @@ -6084,6 +6614,18 @@ regexp.prototype.flags@^1.5.2: es-errors "^1.3.0" set-function-name "^2.0.1" +regexp.prototype.flags@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-errors "^1.3.0" + get-proto "^1.0.1" + gopd "^1.2.0" + set-function-name "^2.0.2" + regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" @@ -6692,6 +7234,11 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + resolve@^1.20.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" @@ -6826,6 +7373,17 @@ safe-array-concat@^1.1.2: has-symbols "^1.0.3" isarray "^2.0.5" +safe-array-concat@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" + isarray "^2.0.5" + safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -6836,6 +7394,14 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-push-apply@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" + integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== + dependencies: + es-errors "^1.3.0" + isarray "^2.0.5" + safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" @@ -6854,6 +7420,15 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -6887,7 +7462,7 @@ secretlint@^10.1.1: globby "^14.1.0" read-pkg "^9.0.1" -semver@7.7.1, semver@^5.1.0, semver@^5.5.0, semver@^6.0.0, semver@^6.1.2, semver@^6.3.1, semver@^7.3.4, semver@^7.3.5, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.2, semver@^7.7.1, semver@^7.7.2: +semver@7.7.1, semver@^5.1.0, semver@^5.5.0, semver@^6.0.0, semver@^6.1.2, semver@^6.3.1, semver@^7.3.4, semver@^7.3.5, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2, semver@^7.7.1, semver@^7.7.2: version "7.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== @@ -6915,7 +7490,7 @@ set-function-length@^1.1.1: gopd "^1.0.1" has-property-descriptors "^1.0.1" -set-function-length@^1.2.1: +set-function-length@^1.2.1, set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== @@ -6936,7 +7511,7 @@ set-function-name@^2.0.0: functions-have-names "^1.2.3" has-property-descriptors "^1.0.0" -set-function-name@^2.0.1: +set-function-name@^2.0.1, set-function-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== @@ -6946,6 +7521,15 @@ set-function-name@^2.0.1: functions-have-names "^1.2.3" has-property-descriptors "^1.0.2" +set-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e" + integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== + dependencies: + dunder-proto "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + setimmediate@^1.0.5, setimmediate@~1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -6982,6 +7566,35 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -6991,6 +7604,17 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + siginfo@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" @@ -7020,11 +7644,6 @@ simple-get@^4.0.0: once "^1.3.1" simple-concat "^1.0.0" -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - slash@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" @@ -7164,6 +7783,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +stable-hash-x@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/stable-hash-x/-/stable-hash-x-0.2.0.tgz#dfd76bfa5d839a7470125c6a6b3c8b22061793e9" + integrity sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ== + stackback@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" @@ -7184,6 +7808,14 @@ stdin-discarder@^0.2.2: resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.2.2.tgz#390037f44c4ae1a1ae535c5fe38dc3aba8d997be" integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ== +stop-iteration-iterator@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" + integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== + dependencies: + es-errors "^1.3.0" + internal-slot "^1.1.0" + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -7229,6 +7861,19 @@ string-width@^7.0.0, string-width@^7.2.0: get-east-asian-width "^1.0.0" strip-ansi "^7.1.0" +string.prototype.trim@^1.2.10: + version "1.2.10" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-data-property "^1.1.4" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-object-atoms "^1.0.0" + has-property-descriptors "^1.0.2" + string.prototype.trim@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd" @@ -7257,6 +7902,16 @@ string.prototype.trimend@^1.0.7, string.prototype.trimend@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" +string.prototype.trimend@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + string.prototype.trimstart@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz#d4cdb44b83a4737ffbac2d406e405d43d0184298" @@ -7402,11 +8057,11 @@ supports-preserve-symlinks-flag@^1.0.0: integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== synckit@^0.11.7: - version "0.11.8" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.8.tgz#b2aaae998a4ef47ded60773ad06e7cb821f55457" - integrity sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A== + version "0.11.11" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.11.tgz#c0b619cf258a97faa209155d9cd1699b5c998cb0" + integrity sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw== dependencies: - "@pkgr/core" "^0.2.4" + "@pkgr/core" "^0.2.9" table@^5.2.3: version "5.4.6" @@ -7609,10 +8264,10 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== -ts-api-utils@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" - integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== +ts-api-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" + integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== ts-loader@^9.5.1: version "9.5.1" @@ -7640,7 +8295,7 @@ tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.2.0, tslib@^2.6.2: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.2.0, tslib@^2.4.0, tslib@^2.6.2: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -7709,6 +8364,15 @@ typed-array-buffer@^1.0.2: es-errors "^1.3.0" is-typed-array "^1.1.13" +typed-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-typed-array "^1.1.14" + typed-array-byte-length@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" @@ -7730,6 +8394,17 @@ typed-array-byte-length@^1.0.1: has-proto "^1.0.3" is-typed-array "^1.1.13" +typed-array-byte-length@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== + dependencies: + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.14" + typed-array-byte-offset@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" @@ -7753,6 +8428,19 @@ typed-array-byte-offset@^1.0.2: has-proto "^1.0.3" is-typed-array "^1.1.13" +typed-array-byte-offset@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" + integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.15" + reflect.getprototypeof "^1.0.9" + typed-array-length@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" @@ -7774,6 +8462,18 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" +typed-array-length@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + reflect.getprototypeof "^1.0.6" + typed-rest-client@^1.8.4: version "1.8.9" resolved "https://registry.yarnpkg.com/typed-rest-client/-/typed-rest-client-1.8.9.tgz#e560226bcadfe71b0fb5c416b587f8da3b8f92d8" @@ -7815,6 +8515,16 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +unbox-primitive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== + dependencies: + call-bound "^1.0.3" + has-bigints "^1.0.2" + has-symbols "^1.1.0" + which-boxed-primitive "^1.1.1" + underscore@^1.12.1: version "1.13.6" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" @@ -7918,6 +8628,33 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +unrs-resolver@^1.7.11: + version "1.11.1" + resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz#be9cd8686c99ef53ecb96df2a473c64d304048a9" + integrity sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg== + dependencies: + napi-postinstall "^0.3.0" + optionalDependencies: + "@unrs/resolver-binding-android-arm-eabi" "1.11.1" + "@unrs/resolver-binding-android-arm64" "1.11.1" + "@unrs/resolver-binding-darwin-arm64" "1.11.1" + "@unrs/resolver-binding-darwin-x64" "1.11.1" + "@unrs/resolver-binding-freebsd-x64" "1.11.1" + "@unrs/resolver-binding-linux-arm-gnueabihf" "1.11.1" + "@unrs/resolver-binding-linux-arm-musleabihf" "1.11.1" + "@unrs/resolver-binding-linux-arm64-gnu" "1.11.1" + "@unrs/resolver-binding-linux-arm64-musl" "1.11.1" + "@unrs/resolver-binding-linux-ppc64-gnu" "1.11.1" + "@unrs/resolver-binding-linux-riscv64-gnu" "1.11.1" + "@unrs/resolver-binding-linux-riscv64-musl" "1.11.1" + "@unrs/resolver-binding-linux-s390x-gnu" "1.11.1" + "@unrs/resolver-binding-linux-x64-gnu" "1.11.1" + "@unrs/resolver-binding-linux-x64-musl" "1.11.1" + "@unrs/resolver-binding-wasm32-wasi" "1.11.1" + "@unrs/resolver-binding-win32-arm64-msvc" "1.11.1" + "@unrs/resolver-binding-win32-ia32-msvc" "1.11.1" + "@unrs/resolver-binding-win32-x64-msvc" "1.11.1" + unzipper@^0.10.11: version "0.10.11" resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.11.tgz#0b4991446472cbdb92ee7403909f26c2419c782e" @@ -8173,6 +8910,46 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== + dependencies: + is-bigint "^1.1.0" + is-boolean-object "^1.2.1" + is-number-object "^1.1.1" + is-string "^1.1.1" + is-symbol "^1.1.1" + +which-builtin-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== + dependencies: + call-bound "^1.0.2" + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" + is-async-function "^2.0.0" + is-date-object "^1.1.0" + is-finalizationregistry "^1.1.0" + is-generator-function "^1.0.10" + is-regex "^1.2.1" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.1.0" + which-collection "^1.0.2" + which-typed-array "^1.1.16" + +which-collection@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" @@ -8211,6 +8988,19 @@ which-typed-array@^1.1.14, which-typed-array@^1.1.15: gopd "^1.0.1" has-tostringtag "^1.0.2" +which-typed-array@^1.1.16, which-typed-array@^1.1.19: + version "1.1.19" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" + integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" From 66377d2f2995a99135030e550028056a2679069e Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Thu, 2 Oct 2025 12:28:41 +0300 Subject: [PATCH 17/45] Add search filter button to Coder Workspaces tree views (#603) Fixes #330 --- package.json | 38 ++++++++++++++++++++++++++--- src/extension.ts | 18 ++++++++++++-- src/workspace/workspacesProvider.ts | 8 ++++++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 23a49a20..438ef3c7 100644 --- a/package.json +++ b/package.json @@ -204,6 +204,7 @@ { "command": "coder.createWorkspace", "title": "Create Workspace", + "category": "Coder", "when": "coder.authenticated", "icon": "$(add)" }, @@ -226,7 +227,8 @@ }, { "command": "coder.refreshWorkspaces", - "title": "Coder: Refresh Workspace", + "title": "Refresh Workspace", + "category": "Coder", "icon": "$(refresh)", "when": "coder.authenticated" }, @@ -241,6 +243,18 @@ "title": "Coder: Open App Status", "icon": "$(robot)", "when": "coder.authenticated" + }, + { + "command": "coder.searchMyWorkspaces", + "title": "Search", + "category": "Coder", + "icon": "$(search)" + }, + { + "command": "coder.searchAllWorkspaces", + "title": "Search", + "category": "Coder", + "icon": "$(search)" } ], "menus": { @@ -248,6 +262,14 @@ { "command": "coder.openFromSidebar", "when": "false" + }, + { + "command": "coder.searchMyWorkspaces", + "when": "false" + }, + { + "command": "coder.searchAllWorkspaces", + "when": "false" } ], "view/title": [ @@ -262,12 +284,22 @@ { "command": "coder.createWorkspace", "when": "coder.authenticated && view == myWorkspaces", - "group": "navigation" + "group": "navigation@1" }, { "command": "coder.refreshWorkspaces", "when": "coder.authenticated && view == myWorkspaces", - "group": "navigation" + "group": "navigation@2" + }, + { + "command": "coder.searchMyWorkspaces", + "when": "coder.authenticated && view == myWorkspaces", + "group": "navigation@3" + }, + { + "command": "coder.searchAllWorkspaces", + "when": "coder.authenticated && view == allWorkspaces", + "group": "navigation@3" } ], "view/item/context": [ diff --git a/src/extension.ts b/src/extension.ts index f7453cec..982342eb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,6 +18,9 @@ import { WorkspaceQuery, } from "./workspace/workspacesProvider"; +const MY_WORKSPACES_TREE_ID = "myWorkspaces"; +const ALL_WORKSPACES_TREE_ID = "allWorkspaces"; + export async function activate(ctx: vscode.ExtensionContext): Promise { // The Remote SSH extension's proposed APIs are used to override the SSH host // name in VS Code itself. It's visually unappealing having a lengthy name! @@ -86,7 +89,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // createTreeView, unlike registerTreeDataProvider, gives us the tree view API // (so we can see when it is visible) but otherwise they have the same effect. - const myWsTree = vscode.window.createTreeView("myWorkspaces", { + const myWsTree = vscode.window.createTreeView(MY_WORKSPACES_TREE_ID, { treeDataProvider: myWorkspacesProvider, }); myWorkspacesProvider.setVisibility(myWsTree.visible); @@ -94,7 +97,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { myWorkspacesProvider.setVisibility(event.visible); }); - const allWsTree = vscode.window.createTreeView("allWorkspaces", { + const allWsTree = vscode.window.createTreeView(ALL_WORKSPACES_TREE_ID, { treeDataProvider: allWorkspacesProvider, }); allWorkspacesProvider.setVisibility(allWsTree.visible); @@ -298,6 +301,12 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { "coder.viewLogs", commands.viewLogs.bind(commands), ); + vscode.commands.registerCommand("coder.searchMyWorkspaces", async () => + showTreeViewSearch(MY_WORKSPACES_TREE_ID), + ); + vscode.commands.registerCommand("coder.searchAllWorkspaces", async () => + showTreeViewSearch(ALL_WORKSPACES_TREE_ID), + ); // Since the "onResolveRemoteAuthority:ssh-remote" activation event exists // in package.json we're able to perform actions before the authority is @@ -421,3 +430,8 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { } } } + +async function showTreeViewSearch(id: string): Promise { + await vscode.commands.executeCommand(`${id}.focus`); + await vscode.commands.executeCommand("list.find"); +} diff --git a/src/workspace/workspacesProvider.ts b/src/workspace/workspacesProvider.ts index 86279401..915ef32a 100644 --- a/src/workspace/workspacesProvider.ts +++ b/src/workspace/workspacesProvider.ts @@ -262,6 +262,7 @@ export class WorkspaceProvider // yet. appStatuses.push( new AppStatusTreeItem({ + id: status.id, name: status.message, command: app.command, workspace_name: element.workspace.name, @@ -335,6 +336,7 @@ class AgentMetadataTreeItem extends vscode.TreeItem { metadataEvent.result.collected_at, ).toLocaleString(); + this.id = metadataEvent.description.key; this.tooltip = "Collected at " + collected_at; this.contextValue = "coderAgentMetadata"; } @@ -343,6 +345,7 @@ class AgentMetadataTreeItem extends vscode.TreeItem { class AppStatusTreeItem extends vscode.TreeItem { constructor( public readonly app: { + id: string; name: string; url?: string; command?: string; @@ -350,6 +353,7 @@ class AppStatusTreeItem extends vscode.TreeItem { }, ) { super("", vscode.TreeItemCollapsibleState.None); + this.id = app.id; this.description = app.name; this.contextValue = "coderAppStatus"; @@ -369,6 +373,7 @@ type CoderOpenableTreeItemType = export class OpenableTreeItem extends vscode.TreeItem { constructor( + id: string, label: string, tooltip: string, description: string, @@ -379,6 +384,7 @@ export class OpenableTreeItem extends vscode.TreeItem { contextValue: CoderOpenableTreeItemType, ) { super(label, collapsibleState); + this.id = id; this.contextValue = contextValue; this.tooltip = tooltip; this.description = description; @@ -397,6 +403,7 @@ export class AgentTreeItem extends OpenableTreeItem { watchMetadata = false, ) { super( + agent.id, // id agent.name, // label `Status: ${agent.status}`, // tooltip agent.status, // description @@ -434,6 +441,7 @@ export class WorkspaceTreeItem extends OpenableTreeItem { const detail = `Template: ${workspace.template_display_name || workspace.template_name} • Status: ${status}`; const agents = extractAgents(workspace.latest_build.resources); super( + workspace.id, label, detail, workspace.latest_build.status, // description From 460056787fbb021e6e08d331e38d758c92e5f511 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Fri, 3 Oct 2025 11:48:38 +0300 Subject: [PATCH 18/45] Fix subscriptions not disposed on extension deactivation (#605) Also use `vscode.workspace.getConfiguration` instead of passing a provider --- src/api/coderApi.ts | 42 +-- src/commands.ts | 4 +- src/core/container.ts | 2 +- src/extension.ts | 134 ++++---- src/remote/remote.ts | 453 ++++++++++++++-------------- src/workspace/workspacesProvider.ts | 36 ++- test/tsconfig.json | 2 +- 7 files changed, 344 insertions(+), 329 deletions(-) diff --git a/src/api/coderApi.ts b/src/api/coderApi.ts index 6c6c0faf..1d73ef00 100644 --- a/src/api/coderApi.ts +++ b/src/api/coderApi.ts @@ -7,7 +7,7 @@ import { type Workspace, type WorkspaceAgent, } from "coder/site/src/api/typesGenerated"; -import { type WorkspaceConfiguration } from "vscode"; +import * as vscode from "vscode"; import { type ClientOptions } from "ws"; import { CertificateError } from "../error"; @@ -33,17 +33,12 @@ import { createHttpAgent } from "./utils"; const coderSessionTokenHeader = "Coder-Session-Token"; -type WorkspaceConfigurationProvider = () => WorkspaceConfiguration; - /** * Unified API class that includes both REST API methods from the base Api class * and WebSocket methods for real-time functionality. */ export class CoderApi extends Api { - private constructor( - private readonly output: Logger, - private readonly configProvider: WorkspaceConfigurationProvider, - ) { + private constructor(private readonly output: Logger) { super(); } @@ -55,15 +50,14 @@ export class CoderApi extends Api { baseUrl: string, token: string | undefined, output: Logger, - configProvider: WorkspaceConfigurationProvider, ): CoderApi { - const client = new CoderApi(output, configProvider); + const client = new CoderApi(output); client.setHost(baseUrl); if (token) { client.setSessionToken(token); } - setupInterceptors(client, baseUrl, output, configProvider); + setupInterceptors(client, baseUrl, output); return client; } @@ -127,7 +121,7 @@ export class CoderApi extends Api { coderSessionTokenHeader ] as string | undefined; - const httpAgent = createHttpAgent(this.configProvider()); + const httpAgent = createHttpAgent(vscode.workspace.getConfiguration()); const webSocket = new OneWayWebSocket({ location: baseUrl, ...configs, @@ -174,14 +168,13 @@ function setupInterceptors( client: CoderApi, baseUrl: string, output: Logger, - configProvider: WorkspaceConfigurationProvider, ): void { - addLoggingInterceptors(client.getAxiosInstance(), output, configProvider); + addLoggingInterceptors(client.getAxiosInstance(), output); client.getAxiosInstance().interceptors.request.use(async (config) => { const headers = await getHeaders( baseUrl, - getHeaderCommand(configProvider()), + getHeaderCommand(vscode.workspace.getConfiguration()), output, ); // Add headers from the header command. @@ -192,7 +185,7 @@ function setupInterceptors( // Configure proxy and TLS. // Note that by default VS Code overrides the agent. To prevent this, set // `http.proxySupport` to `on` or `off`. - const agent = createHttpAgent(configProvider()); + const agent = createHttpAgent(vscode.workspace.getConfiguration()); config.httpsAgent = agent; config.httpAgent = agent; config.proxy = false; @@ -209,38 +202,35 @@ function setupInterceptors( ); } -function addLoggingInterceptors( - client: AxiosInstance, - logger: Logger, - configProvider: WorkspaceConfigurationProvider, -) { +function addLoggingInterceptors(client: AxiosInstance, logger: Logger) { client.interceptors.request.use( (config) => { const configWithMeta = config as RequestConfigWithMeta; configWithMeta.metadata = createRequestMeta(); - logRequest(logger, configWithMeta, getLogLevel(configProvider())); + logRequest(logger, configWithMeta, getLogLevel()); return config; }, (error: unknown) => { - logError(logger, error, getLogLevel(configProvider())); + logError(logger, error, getLogLevel()); return Promise.reject(error); }, ); client.interceptors.response.use( (response) => { - logResponse(logger, response, getLogLevel(configProvider())); + logResponse(logger, response, getLogLevel()); return response; }, (error: unknown) => { - logError(logger, error, getLogLevel(configProvider())); + logError(logger, error, getLogLevel()); return Promise.reject(error); }, ); } -function getLogLevel(cfg: WorkspaceConfiguration): HttpClientLogLevel { - const logLevelStr = cfg +function getLogLevel(): HttpClientLogLevel { + const logLevelStr = vscode.workspace + .getConfiguration() .get( "coder.httpClientLogLevel", HttpClientLogLevel[HttpClientLogLevel.BASIC], diff --git a/src/commands.ts b/src/commands.ts index 462010ba..bd4071cc 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -260,9 +260,7 @@ export class Commands { token: string, isAutologin: boolean, ): Promise<{ user: User; token: string } | null> { - const client = CoderApi.create(url, token, this.logger, () => - vscode.workspace.getConfiguration(), - ); + const client = CoderApi.create(url, token, this.logger); if (!needToken(vscode.workspace.getConfiguration())) { try { const user = await client.getAuthenticatedUser(); diff --git a/src/core/container.ts b/src/core/container.ts index f820bb0d..72f28088 100644 --- a/src/core/container.ts +++ b/src/core/container.ts @@ -11,7 +11,7 @@ import { SecretsManager } from "./secretsManager"; * Service container for dependency injection. * Centralizes the creation and management of all core services. */ -export class ServiceContainer { +export class ServiceContainer implements vscode.Disposable { private readonly logger: vscode.LogOutputChannel; private readonly pathResolver: PathResolver; private readonly mementoManager: MementoManager; diff --git a/src/extension.ts b/src/extension.ts index 982342eb..e069c3a3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -57,6 +57,8 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { } const serviceContainer = new ServiceContainer(ctx, vscodeProposed); + ctx.subscriptions.push(serviceContainer); + const output = serviceContainer.getLogger(); const mementoManager = serviceContainer.getMementoManager(); const secretsManager = serviceContainer.getSecretsManager(); @@ -72,7 +74,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { url || "", await secretsManager.getSessionToken(), output, - () => vscode.workspace.getConfiguration(), ); const myWorkspacesProvider = new WorkspaceProvider( @@ -81,33 +82,47 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { output, 5, ); + ctx.subscriptions.push(myWorkspacesProvider); + const allWorkspacesProvider = new WorkspaceProvider( WorkspaceQuery.All, client, output, ); + ctx.subscriptions.push(allWorkspacesProvider); // createTreeView, unlike registerTreeDataProvider, gives us the tree view API // (so we can see when it is visible) but otherwise they have the same effect. const myWsTree = vscode.window.createTreeView(MY_WORKSPACES_TREE_ID, { treeDataProvider: myWorkspacesProvider, }); + ctx.subscriptions.push(myWsTree); myWorkspacesProvider.setVisibility(myWsTree.visible); - myWsTree.onDidChangeVisibility((event) => { - myWorkspacesProvider.setVisibility(event.visible); - }); + myWsTree.onDidChangeVisibility( + (event) => { + myWorkspacesProvider.setVisibility(event.visible); + }, + undefined, + ctx.subscriptions, + ); const allWsTree = vscode.window.createTreeView(ALL_WORKSPACES_TREE_ID, { treeDataProvider: allWorkspacesProvider, }); + ctx.subscriptions.push(allWsTree); allWorkspacesProvider.setVisibility(allWsTree.visible); - allWsTree.onDidChangeVisibility((event) => { - allWorkspacesProvider.setVisibility(event.visible); - }); + allWsTree.onDidChangeVisibility( + (event) => { + allWorkspacesProvider.setVisibility(event.visible); + }, + undefined, + ctx.subscriptions, + ); // Handle vscode:// URIs. - vscode.window.registerUriHandler({ + const uriHandler = vscode.window.registerUriHandler({ handleUri: async (uri) => { + const cliManager = serviceContainer.getCliManager(); const params = new URLSearchParams(uri.query); if (uri.path === "/open") { const owner = params.get("owner"); @@ -253,59 +268,63 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { } }, }); - - const cliManager = serviceContainer.getCliManager(); + ctx.subscriptions.push(uriHandler); // Register globally available commands. Many of these have visibility // controlled by contexts, see `when` in the package.json. const commands = new Commands(serviceContainer, client); - vscode.commands.registerCommand("coder.login", commands.login.bind(commands)); - vscode.commands.registerCommand( - "coder.logout", - commands.logout.bind(commands), - ); - vscode.commands.registerCommand("coder.open", commands.open.bind(commands)); - vscode.commands.registerCommand( - "coder.openDevContainer", - commands.openDevContainer.bind(commands), - ); - vscode.commands.registerCommand( - "coder.openFromSidebar", - commands.openFromSidebar.bind(commands), - ); - vscode.commands.registerCommand( - "coder.openAppStatus", - commands.openAppStatus.bind(commands), - ); - vscode.commands.registerCommand( - "coder.workspace.update", - commands.updateWorkspace.bind(commands), - ); - vscode.commands.registerCommand( - "coder.createWorkspace", - commands.createWorkspace.bind(commands), - ); - vscode.commands.registerCommand( - "coder.navigateToWorkspace", - commands.navigateToWorkspace.bind(commands), - ); - vscode.commands.registerCommand( - "coder.navigateToWorkspaceSettings", - commands.navigateToWorkspaceSettings.bind(commands), - ); - vscode.commands.registerCommand("coder.refreshWorkspaces", () => { - myWorkspacesProvider.fetchAndRefresh(); - allWorkspacesProvider.fetchAndRefresh(); - }); - vscode.commands.registerCommand( - "coder.viewLogs", - commands.viewLogs.bind(commands), - ); - vscode.commands.registerCommand("coder.searchMyWorkspaces", async () => - showTreeViewSearch(MY_WORKSPACES_TREE_ID), - ); - vscode.commands.registerCommand("coder.searchAllWorkspaces", async () => - showTreeViewSearch(ALL_WORKSPACES_TREE_ID), + ctx.subscriptions.push( + vscode.commands.registerCommand( + "coder.login", + commands.login.bind(commands), + ), + vscode.commands.registerCommand( + "coder.logout", + commands.logout.bind(commands), + ), + vscode.commands.registerCommand("coder.open", commands.open.bind(commands)), + vscode.commands.registerCommand( + "coder.openDevContainer", + commands.openDevContainer.bind(commands), + ), + vscode.commands.registerCommand( + "coder.openFromSidebar", + commands.openFromSidebar.bind(commands), + ), + vscode.commands.registerCommand( + "coder.openAppStatus", + commands.openAppStatus.bind(commands), + ), + vscode.commands.registerCommand( + "coder.workspace.update", + commands.updateWorkspace.bind(commands), + ), + vscode.commands.registerCommand( + "coder.createWorkspace", + commands.createWorkspace.bind(commands), + ), + vscode.commands.registerCommand( + "coder.navigateToWorkspace", + commands.navigateToWorkspace.bind(commands), + ), + vscode.commands.registerCommand( + "coder.navigateToWorkspaceSettings", + commands.navigateToWorkspaceSettings.bind(commands), + ), + vscode.commands.registerCommand("coder.refreshWorkspaces", () => { + myWorkspacesProvider.fetchAndRefresh(); + allWorkspacesProvider.fetchAndRefresh(); + }), + vscode.commands.registerCommand( + "coder.viewLogs", + commands.viewLogs.bind(commands), + ), + vscode.commands.registerCommand("coder.searchMyWorkspaces", async () => + showTreeViewSearch(MY_WORKSPACES_TREE_ID), + ), + vscode.commands.registerCommand("coder.searchAllWorkspaces", async () => + showTreeViewSearch(ALL_WORKSPACES_TREE_ID), + ), ); // Since the "onResolveRemoteAuthority:ssh-remote" activation event exists @@ -325,6 +344,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { isFirstConnect, ); if (details) { + ctx.subscriptions.push(details); // Authenticate the plugin client which is used in the sidebar to display // workspaces belonging to this deployment. client.setHost(details.url); diff --git a/src/remote/remote.ts b/src/remote/remote.ts index baf7b28c..2a286ab4 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -276,12 +276,7 @@ export class Remote { // break this connection. We could force close the remote session or // disallow logging out/in altogether, but for now just use a separate // client to remain unaffected by whatever the plugin is doing. - const workspaceClient = CoderApi.create( - baseUrlRaw, - token, - this.logger, - () => vscode.workspace.getConfiguration(), - ); + const workspaceClient = CoderApi.create(baseUrlRaw, token, this.logger); // Store for use in commands. this.commands.workspaceRestClient = workspaceClient; @@ -398,256 +393,260 @@ export class Remote { } const disposables: vscode.Disposable[] = []; - // Register before connection so the label still displays! - disposables.push( - this.registerLabelFormatter( + try { + // Register before connection so the label still displays! + let labelFormatterDisposable = this.registerLabelFormatter( remoteAuthority, workspace.owner_name, workspace.name, - ), - ); - - // If the workspace is not in a running state, try to get it running. - if (workspace.latest_build.status !== "running") { - const updatedWorkspace = await this.maybeWaitForRunning( - workspaceClient, - workspace, - parts.label, - binaryPath, - featureSet, - firstConnect, ); - if (!updatedWorkspace) { - // User declined to start the workspace. + disposables.push({ + dispose: () => labelFormatterDisposable.dispose(), + }); + + // If the workspace is not in a running state, try to get it running. + if (workspace.latest_build.status !== "running") { + const updatedWorkspace = await this.maybeWaitForRunning( + workspaceClient, + workspace, + parts.label, + binaryPath, + featureSet, + firstConnect, + ); + if (!updatedWorkspace) { + // User declined to start the workspace. + await this.closeRemote(); + return; + } + workspace = updatedWorkspace; + } + this.commands.workspace = workspace; + + // Pick an agent. + this.logger.info(`Finding agent for ${workspaceName}...`); + const agents = extractAgents(workspace.latest_build.resources); + const gotAgent = await this.commands.maybeAskAgent(agents, parts.agent); + if (!gotAgent) { + // User declined to pick an agent. await this.closeRemote(); return; } - workspace = updatedWorkspace; - } - this.commands.workspace = workspace; - - // Pick an agent. - this.logger.info(`Finding agent for ${workspaceName}...`); - const agents = extractAgents(workspace.latest_build.resources); - const gotAgent = await this.commands.maybeAskAgent(agents, parts.agent); - if (!gotAgent) { - // User declined to pick an agent. - await this.closeRemote(); - return; - } - let agent = gotAgent; // Reassign so it cannot be undefined in callbacks. - this.logger.info(`Found agent ${agent.name} with status`, agent.status); - - // Do some janky setting manipulation. - this.logger.info("Modifying settings..."); - const remotePlatforms = this.vscodeProposed.workspace - .getConfiguration() - .get>("remote.SSH.remotePlatform", {}); - const connTimeout = this.vscodeProposed.workspace - .getConfiguration() - .get("remote.SSH.connectTimeout"); - - // We have to directly munge the settings file with jsonc because trying to - // update properly through the extension API hangs indefinitely. Possibly - // VS Code is trying to update configuration on the remote, which cannot - // connect until we finish here leading to a deadlock. We need to update it - // locally, anyway, and it does not seem possible to force that via API. - let settingsContent = "{}"; - try { - settingsContent = await fs.readFile( - this.pathResolver.getUserSettingsPath(), - "utf8", - ); - } catch { - // Ignore! It's probably because the file doesn't exist. - } - - // Add the remote platform for this host to bypass a step where VS Code asks - // the user for the platform. - let mungedPlatforms = false; - if ( - !remotePlatforms[parts.host] || - remotePlatforms[parts.host] !== agent.operating_system - ) { - remotePlatforms[parts.host] = agent.operating_system; - settingsContent = jsonc.applyEdits( - settingsContent, - jsonc.modify( - settingsContent, - ["remote.SSH.remotePlatform"], - remotePlatforms, - {}, - ), - ); - mungedPlatforms = true; - } + let agent = gotAgent; // Reassign so it cannot be undefined in callbacks. + this.logger.info(`Found agent ${agent.name} with status`, agent.status); + + // Do some janky setting manipulation. + this.logger.info("Modifying settings..."); + const remotePlatforms = this.vscodeProposed.workspace + .getConfiguration() + .get>("remote.SSH.remotePlatform", {}); + const connTimeout = this.vscodeProposed.workspace + .getConfiguration() + .get("remote.SSH.connectTimeout"); + + // We have to directly munge the settings file with jsonc because trying to + // update properly through the extension API hangs indefinitely. Possibly + // VS Code is trying to update configuration on the remote, which cannot + // connect until we finish here leading to a deadlock. We need to update it + // locally, anyway, and it does not seem possible to force that via API. + let settingsContent = "{}"; + try { + settingsContent = await fs.readFile( + this.pathResolver.getUserSettingsPath(), + "utf8", + ); + } catch { + // Ignore! It's probably because the file doesn't exist. + } - // VS Code ignores the connect timeout in the SSH config and uses a default - // of 15 seconds, which can be too short in the case where we wait for - // startup scripts. For now we hardcode a longer value. Because this is - // potentially overwriting user configuration, it feels a bit sketchy. If - // microsoft/vscode-remote-release#8519 is resolved we can remove this. - const minConnTimeout = 1800; - let mungedConnTimeout = false; - if (!connTimeout || connTimeout < minConnTimeout) { - settingsContent = jsonc.applyEdits( - settingsContent, - jsonc.modify( + // Add the remote platform for this host to bypass a step where VS Code asks + // the user for the platform. + let mungedPlatforms = false; + if ( + !remotePlatforms[parts.host] || + remotePlatforms[parts.host] !== agent.operating_system + ) { + remotePlatforms[parts.host] = agent.operating_system; + settingsContent = jsonc.applyEdits( settingsContent, - ["remote.SSH.connectTimeout"], - minConnTimeout, - {}, - ), - ); - mungedConnTimeout = true; - } + jsonc.modify( + settingsContent, + ["remote.SSH.remotePlatform"], + remotePlatforms, + {}, + ), + ); + mungedPlatforms = true; + } - if (mungedPlatforms || mungedConnTimeout) { - try { - await fs.writeFile( - this.pathResolver.getUserSettingsPath(), + // VS Code ignores the connect timeout in the SSH config and uses a default + // of 15 seconds, which can be too short in the case where we wait for + // startup scripts. For now we hardcode a longer value. Because this is + // potentially overwriting user configuration, it feels a bit sketchy. If + // microsoft/vscode-remote-release#8519 is resolved we can remove this. + const minConnTimeout = 1800; + let mungedConnTimeout = false; + if (!connTimeout || connTimeout < minConnTimeout) { + settingsContent = jsonc.applyEdits( settingsContent, + jsonc.modify( + settingsContent, + ["remote.SSH.connectTimeout"], + minConnTimeout, + {}, + ), ); - } catch (ex) { - // This could be because the user's settings.json is read-only. This is - // the case when using home-manager on NixOS, for example. Failure to - // write here is not necessarily catastrophic since the user will be - // asked for the platform and the default timeout might be sufficient. - mungedPlatforms = mungedConnTimeout = false; - this.logger.warn("Failed to configure settings", ex); + mungedConnTimeout = true; } - } - // Watch the workspace for changes. - const monitor = new WorkspaceMonitor( - workspace, - workspaceClient, - this.logger, - this.vscodeProposed, - ); - disposables.push(monitor); - disposables.push( - monitor.onChange.event((w) => (this.commands.workspace = w)), - ); + if (mungedPlatforms || mungedConnTimeout) { + try { + await fs.writeFile( + this.pathResolver.getUserSettingsPath(), + settingsContent, + ); + } catch (ex) { + // This could be because the user's settings.json is read-only. This is + // the case when using home-manager on NixOS, for example. Failure to + // write here is not necessarily catastrophic since the user will be + // asked for the platform and the default timeout might be sufficient. + mungedPlatforms = mungedConnTimeout = false; + this.logger.warn("Failed to configure settings", ex); + } + } - // Watch coder inbox for messages - const inbox = new Inbox(workspace, workspaceClient, this.logger); - disposables.push(inbox); + // Watch the workspace for changes. + const monitor = new WorkspaceMonitor( + workspace, + workspaceClient, + this.logger, + this.vscodeProposed, + ); + disposables.push(monitor); + disposables.push( + monitor.onChange.event((w) => (this.commands.workspace = w)), + ); - // Wait for the agent to connect. - if (agent.status === "connecting") { - this.logger.info(`Waiting for ${workspaceName}/${agent.name}...`); - await vscode.window.withProgress( - { - title: "Waiting for the agent to connect...", - location: vscode.ProgressLocation.Notification, - }, - async () => { - await new Promise((resolve) => { - const updateEvent = monitor.onChange.event((workspace) => { - if (!agent) { - return; - } - const agents = extractAgents(workspace.latest_build.resources); - const found = agents.find((newAgent) => { - return newAgent.id === agent.id; + // Watch coder inbox for messages + const inbox = new Inbox(workspace, workspaceClient, this.logger); + disposables.push(inbox); + + // Wait for the agent to connect. + if (agent.status === "connecting") { + this.logger.info(`Waiting for ${workspaceName}/${agent.name}...`); + await vscode.window.withProgress( + { + title: "Waiting for the agent to connect...", + location: vscode.ProgressLocation.Notification, + }, + async () => { + await new Promise((resolve) => { + const updateEvent = monitor.onChange.event((workspace) => { + if (!agent) { + return; + } + const agents = extractAgents(workspace.latest_build.resources); + const found = agents.find((newAgent) => { + return newAgent.id === agent.id; + }); + if (!found) { + return; + } + agent = found; + if (agent.status === "connecting") { + return; + } + updateEvent.dispose(); + resolve(); }); - if (!found) { - return; - } - agent = found; - if (agent.status === "connecting") { - return; - } - updateEvent.dispose(); - resolve(); }); - }); - }, - ); - this.logger.info(`Agent ${agent.name} status is now`, agent.status); - } + }, + ); + this.logger.info(`Agent ${agent.name} status is now`, agent.status); + } - // Make sure the agent is connected. - // TODO: Should account for the lifecycle state as well? - if (agent.status !== "connected") { - const result = await this.vscodeProposed.window.showErrorMessage( - `${workspaceName}/${agent.name} ${agent.status}`, - { - useCustom: true, - modal: true, - detail: `The ${agent.name} agent failed to connect. Try restarting your workspace.`, - }, - ); - if (!result) { - await this.closeRemote(); + // Make sure the agent is connected. + // TODO: Should account for the lifecycle state as well? + if (agent.status !== "connected") { + const result = await this.vscodeProposed.window.showErrorMessage( + `${workspaceName}/${agent.name} ${agent.status}`, + { + useCustom: true, + modal: true, + detail: `The ${agent.name} agent failed to connect. Try restarting your workspace.`, + }, + ); + if (!result) { + await this.closeRemote(); + return; + } + await this.reloadWindow(); return; } - await this.reloadWindow(); - return; - } - - const logDir = this.getLogDir(featureSet); - // This ensures the Remote SSH extension resolves the host to execute the - // Coder binary properly. - // - // If we didn't write to the SSH config file, connecting would fail with - // "Host not found". - try { - this.logger.info("Updating SSH config..."); - await this.updateSSHConfig( - workspaceClient, - parts.label, - parts.host, - binaryPath, - logDir, - featureSet, - ); - } catch (error) { - this.logger.warn("Failed to configure SSH", error); - throw error; - } + const logDir = this.getLogDir(featureSet); - // TODO: This needs to be reworked; it fails to pick up reconnects. - this.findSSHProcessID().then(async (pid) => { - if (!pid) { - // TODO: Show an error here! - return; - } - disposables.push(this.showNetworkUpdates(pid)); - if (logDir) { - const logFiles = await fs.readdir(logDir); - const logFileName = logFiles - .reverse() - .find( - (file) => file === `${pid}.log` || file.endsWith(`-${pid}.log`), - ); - this.commands.workspaceLogPath = logFileName - ? path.join(logDir, logFileName) - : undefined; - } else { - this.commands.workspaceLogPath = undefined; + // This ensures the Remote SSH extension resolves the host to execute the + // Coder binary properly. + // + // If we didn't write to the SSH config file, connecting would fail with + // "Host not found". + try { + this.logger.info("Updating SSH config..."); + await this.updateSSHConfig( + workspaceClient, + parts.label, + parts.host, + binaryPath, + logDir, + featureSet, + ); + } catch (error) { + this.logger.warn("Failed to configure SSH", error); + throw error; } - }); - // Register the label formatter again because SSH overrides it! - disposables.push( - vscode.extensions.onDidChange(() => { - disposables.push( - this.registerLabelFormatter( + // TODO: This needs to be reworked; it fails to pick up reconnects. + this.findSSHProcessID().then(async (pid) => { + if (!pid) { + // TODO: Show an error here! + return; + } + disposables.push(this.showNetworkUpdates(pid)); + if (logDir) { + const logFiles = await fs.readdir(logDir); + const logFileName = logFiles + .reverse() + .find( + (file) => file === `${pid}.log` || file.endsWith(`-${pid}.log`), + ); + this.commands.workspaceLogPath = logFileName + ? path.join(logDir, logFileName) + : undefined; + } else { + this.commands.workspaceLogPath = undefined; + } + }); + + // Register the label formatter again because SSH overrides it! + disposables.push( + vscode.extensions.onDidChange(() => { + // Dispose previous label formatter + labelFormatterDisposable.dispose(); + labelFormatterDisposable = this.registerLabelFormatter( remoteAuthority, workspace.owner_name, workspace.name, agent.name, - ), - ); - }), - ); - - disposables.push( - ...this.createAgentMetadataStatusBar(agent, workspaceClient), - ); + ); + }), + ...this.createAgentMetadataStatusBar(agent, workspaceClient), + ); + } catch (ex) { + // Whatever error happens, make sure we clean up the disposables in case of failure + disposables.forEach((d) => d.dispose()); + throw ex; + } this.logger.info("Remote setup complete"); diff --git a/src/workspace/workspacesProvider.ts b/src/workspace/workspacesProvider.ts index 915ef32a..b83e4f84 100644 --- a/src/workspace/workspacesProvider.ts +++ b/src/workspace/workspacesProvider.ts @@ -34,12 +34,12 @@ export enum WorkspaceQuery { * abort polling until fetchAndRefresh() is called again. */ export class WorkspaceProvider - implements vscode.TreeDataProvider + implements vscode.TreeDataProvider, vscode.Disposable { // Undefined if we have never fetched workspaces before. private workspaces: WorkspaceTreeItem[] | undefined; - private agentWatchers: Record = - {}; + private agentWatchers: Map = + new Map(); private timeout: NodeJS.Timeout | undefined; private fetching = false; private visible = false; @@ -121,7 +121,7 @@ export class WorkspaceProvider return this.fetch(); } - const oldWatcherIds = Object.keys(this.agentWatchers); + const oldWatcherIds = [...this.agentWatchers.keys()]; const reusedWatcherIds: string[] = []; // TODO: I think it might make more sense for the tree items to contain @@ -132,23 +132,23 @@ export class WorkspaceProvider const agents = extractAllAgents(resp.workspaces); agents.forEach((agent) => { // If we have an existing watcher, re-use it. - if (this.agentWatchers[agent.id]) { + const oldWatcher = this.agentWatchers.get(agent.id); + if (oldWatcher) { reusedWatcherIds.push(agent.id); - return this.agentWatchers[agent.id]; + } else { + // Otherwise create a new watcher. + const watcher = createAgentMetadataWatcher(agent.id, this.client); + watcher.onChange(() => this.refresh()); + this.agentWatchers.set(agent.id, watcher); } - // Otherwise create a new watcher. - const watcher = createAgentMetadataWatcher(agent.id, this.client); - watcher.onChange(() => this.refresh()); - this.agentWatchers[agent.id] = watcher; - return watcher; }); } // Dispose of watchers we ended up not reusing. oldWatcherIds.forEach((id) => { if (!reusedWatcherIds.includes(id)) { - this.agentWatchers[id].dispose(); - delete this.agentWatchers[id]; + this.agentWatchers.get(id)?.dispose(); + this.agentWatchers.delete(id); } }); @@ -244,7 +244,7 @@ export class WorkspaceProvider return Promise.resolve(agentTreeItems); } else if (element instanceof AgentTreeItem) { - const watcher = this.agentWatchers[element.agent.id]; + const watcher = this.agentWatchers.get(element.agent.id); if (watcher?.error) { return Promise.resolve([new ErrorTreeItem(watcher.error)]); } @@ -305,6 +305,14 @@ export class WorkspaceProvider } return Promise.resolve(this.workspaces || []); } + + dispose() { + this.cancelPendingRefresh(); + for (const watcher of this.agentWatchers.values()) { + watcher.dispose(); + } + this.agentWatchers.clear(); + } } /** diff --git a/test/tsconfig.json b/test/tsconfig.json index ece5f0b1..1be61bbd 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -6,5 +6,5 @@ "@/*": ["src/*"] } }, - "include": ["**/*", "../src/**/*"] + "include": [".", "../src"] } From 648360a5fe3a3140ef0e10bbc487586baa6f2675 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Mon, 6 Oct 2025 14:20:35 +0300 Subject: [PATCH 19/45] Fix login/logout synchronization across multiple VS Code windows (#590) Introduce ContextManager for centralized state management and use secrets to propagate authentication events between windows. Resolves race conditions in session token handling and ensures consistent authentication behavior across all open extension instances. Fixes #498 --- .eslintrc.json | 39 ++++++--- src/commands.ts | 72 ++++++++++------- src/core/container.ts | 8 ++ src/core/contextManager.ts | 33 ++++++++ src/core/secretsManager.ts | 52 +++++++++++- src/error.ts | 3 + src/extension.ts | 53 +++++++----- src/remote/remote.ts | 112 ++++++++++++++++---------- src/workspace/workspaceMonitor.ts | 8 +- test/mocks/testHelpers.ts | 30 ++++++- test/unit/core/secretsManager.test.ts | 48 +++++++++-- 11 files changed, 336 insertions(+), 122 deletions(-) create mode 100644 src/core/contextManager.ts diff --git a/.eslintrc.json b/.eslintrc.json index 91d67601..32fb8e61 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -23,17 +23,6 @@ "import/internal-regex": "^@/" }, "overrides": [ - { - "files": ["test/**/*.{ts,tsx}", "**/*.{test,spec}.ts?(x)"], - "settings": { - "import/resolver": { - "typescript": { - // In tests, resolve using the test tsconfig - "project": "test/tsconfig.json" - } - } - } - }, { "files": ["*.ts"], "rules": { @@ -46,9 +35,30 @@ "prefer": "type-imports", "fixStyle": "inline-type-imports" } + ], + "@typescript-eslint/switch-exhaustiveness-check": [ + "error", + { "considerDefaultExhaustiveForUnions": true } ] } }, + { + "files": ["test/**/*.{ts,tsx}", "**/*.{test,spec}.ts?(x)"], + "settings": { + "import/resolver": { + "typescript": { + // In tests, resolve using the test tsconfig + "project": "test/tsconfig.json" + } + } + } + }, + { + "files": ["src/core/contextManager.ts"], + "rules": { + "no-restricted-syntax": "off" + } + }, { "extends": ["plugin:package-json/legacy-recommended"], "files": ["*.json"], @@ -106,6 +116,13 @@ "sublings_only": true } } + ], + "no-restricted-syntax": [ + "error", + { + "selector": "CallExpression[callee.property.name='executeCommand'][arguments.0.value='setContext'][arguments.length>=3]", + "message": "Do not use executeCommand('setContext', ...) directly. Use the ContextManager class instead." + } ] } } diff --git a/src/commands.ts b/src/commands.ts index bd4071cc..5abeb026 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -12,6 +12,7 @@ import { CoderApi } from "./api/coderApi"; import { needToken } from "./api/utils"; import { type CliManager } from "./core/cliManager"; import { type ServiceContainer } from "./core/container"; +import { type ContextManager } from "./core/contextManager"; import { type MementoManager } from "./core/mementoManager"; import { type PathResolver } from "./core/pathResolver"; import { type SecretsManager } from "./core/secretsManager"; @@ -32,6 +33,7 @@ export class Commands { private readonly mementoManager: MementoManager; private readonly secretsManager: SecretsManager; private readonly cliManager: CliManager; + private readonly contextManager: ContextManager; // These will only be populated when actively connected to a workspace and are // used in commands. Because commands can be executed by the user, it is not // possible to pass in arguments, so we have to store the current workspace @@ -53,6 +55,7 @@ export class Commands { this.mementoManager = serviceContainer.getMementoManager(); this.secretsManager = serviceContainer.getSecretsManager(); this.cliManager = serviceContainer.getCliManager(); + this.contextManager = serviceContainer.getContextManager(); } /** @@ -179,19 +182,22 @@ export class Commands { } /** - * Log into the provided deployment. If the deployment URL is not specified, + * Log into the provided deployment. If the deployment URL is not specified, * ask for it first with a menu showing recent URLs along with the default URL * and CODER_URL, if those are set. */ - public async login(...args: string[]): Promise { - // Destructure would be nice but VS Code can pass undefined which errors. - const inputUrl = args[0]; - const inputToken = args[1]; - const inputLabel = args[2]; - const isAutologin = - typeof args[3] === "undefined" ? false : Boolean(args[3]); - - const url = await this.maybeAskUrl(inputUrl); + public async login(args?: { + url?: string; + token?: string; + label?: string; + autoLogin?: boolean; + }): Promise { + if (this.contextManager.get("coder.authenticated")) { + return; + } + this.logger.info("Logging in"); + + const url = await this.maybeAskUrl(args?.url); if (!url) { return; // The user aborted. } @@ -199,11 +205,11 @@ export class Commands { // It is possible that we are trying to log into an old-style host, in which // case we want to write with the provided blank label instead of generating // a host label. - const label = - typeof inputLabel === "undefined" ? toSafeHost(url) : inputLabel; + const label = args?.label === undefined ? toSafeHost(url) : args.label; // Try to get a token from the user, if we need one, and their user. - const res = await this.maybeAskToken(url, inputToken, isAutologin); + const autoLogin = args?.autoLogin === true; + const res = await this.maybeAskToken(url, args?.token, autoLogin); if (!res) { return; // The user aborted, or unable to auth. } @@ -221,13 +227,9 @@ export class Commands { await this.cliManager.configure(label, url, res.token); // These contexts control various menu items and the sidebar. - await vscode.commands.executeCommand( - "setContext", - "coder.authenticated", - true, - ); + this.contextManager.set("coder.authenticated", true); if (res.user.roles.find((role) => role.name === "owner")) { - await vscode.commands.executeCommand("setContext", "coder.isOwner", true); + this.contextManager.set("coder.isOwner", true); } vscode.window @@ -245,6 +247,7 @@ export class Commands { } }); + await this.secretsManager.triggerLoginStateChange("login"); // Fetch workspaces for the new deployment. vscode.commands.executeCommand("coder.refreshWorkspaces"); } @@ -257,19 +260,21 @@ export class Commands { */ private async maybeAskToken( url: string, - token: string, - isAutologin: boolean, + token: string | undefined, + isAutoLogin: boolean, ): Promise<{ user: User; token: string } | null> { const client = CoderApi.create(url, token, this.logger); - if (!needToken(vscode.workspace.getConfiguration())) { + const needsToken = needToken(vscode.workspace.getConfiguration()); + if (!needsToken || token) { try { const user = await client.getAuthenticatedUser(); // For non-token auth, we write a blank token since the `vscodessh` // command currently always requires a token file. - return { token: "", user }; + // For token auth, we have valid access so we can just return the user here + return { token: needsToken && token ? token : "", user }; } catch (err) { const message = getErrorMessage(err, "no response from the server"); - if (isAutologin) { + if (isAutoLogin) { this.logger.warn("Failed to log in to Coder server:", message); } else { this.vscodeProposed.window.showErrorMessage( @@ -301,6 +306,9 @@ export class Commands { value: token || (await this.secretsManager.getSessionToken()), ignoreFocusOut: true, validateInput: async (value) => { + if (!value) { + return null; + } client.setSessionToken(value); try { user = await client.getAuthenticatedUser(); @@ -369,7 +377,14 @@ export class Commands { // Sanity check; command should not be available if no url. throw new Error("You are not logged in"); } + await this.forceLogout(); + } + public async forceLogout(): Promise { + if (!this.contextManager.get("coder.authenticated")) { + return; + } + this.logger.info("Logging out"); // Clear from the REST client. An empty url will indicate to other parts of // the code that we are logged out. this.restClient.setHost(""); @@ -379,19 +394,16 @@ export class Commands { await this.mementoManager.setUrl(undefined); await this.secretsManager.setSessionToken(undefined); - await vscode.commands.executeCommand( - "setContext", - "coder.authenticated", - false, - ); + this.contextManager.set("coder.authenticated", false); vscode.window .showInformationMessage("You've been logged out of Coder!", "Login") .then((action) => { if (action === "Login") { - vscode.commands.executeCommand("coder.login"); + this.login(); } }); + await this.secretsManager.triggerLoginStateChange("logout"); // This will result in clearing the workspace list. vscode.commands.executeCommand("coder.refreshWorkspaces"); } diff --git a/src/core/container.ts b/src/core/container.ts index 72f28088..a8f938ea 100644 --- a/src/core/container.ts +++ b/src/core/container.ts @@ -3,6 +3,7 @@ import * as vscode from "vscode"; import { type Logger } from "../logging/logger"; import { CliManager } from "./cliManager"; +import { ContextManager } from "./contextManager"; import { MementoManager } from "./mementoManager"; import { PathResolver } from "./pathResolver"; import { SecretsManager } from "./secretsManager"; @@ -17,6 +18,7 @@ export class ServiceContainer implements vscode.Disposable { private readonly mementoManager: MementoManager; private readonly secretsManager: SecretsManager; private readonly cliManager: CliManager; + private readonly contextManager: ContextManager; constructor( context: vscode.ExtensionContext, @@ -34,6 +36,7 @@ export class ServiceContainer implements vscode.Disposable { this.logger, this.pathResolver, ); + this.contextManager = new ContextManager(); } getVsCodeProposed(): typeof vscode { @@ -60,10 +63,15 @@ export class ServiceContainer implements vscode.Disposable { return this.cliManager; } + getContextManager(): ContextManager { + return this.contextManager; + } + /** * Dispose of all services and clean up resources. */ dispose(): void { + this.contextManager.dispose(); this.logger.dispose(); } } diff --git a/src/core/contextManager.ts b/src/core/contextManager.ts new file mode 100644 index 00000000..a5a18397 --- /dev/null +++ b/src/core/contextManager.ts @@ -0,0 +1,33 @@ +import * as vscode from "vscode"; + +const CONTEXT_DEFAULTS = { + "coder.authenticated": false, + "coder.isOwner": false, + "coder.loaded": false, + "coder.workspace.updatable": false, +} as const; + +type CoderContext = keyof typeof CONTEXT_DEFAULTS; + +export class ContextManager implements vscode.Disposable { + private readonly context = new Map(); + + public constructor() { + (Object.keys(CONTEXT_DEFAULTS) as CoderContext[]).forEach((key) => { + this.set(key, CONTEXT_DEFAULTS[key]); + }); + } + + public set(key: CoderContext, value: boolean): void { + this.context.set(key, value); + vscode.commands.executeCommand("setContext", key, value); + } + + public get(key: CoderContext): boolean { + return this.context.get(key) ?? CONTEXT_DEFAULTS[key]; + } + + public dispose() { + this.context.clear(); + } +} diff --git a/src/core/secretsManager.ts b/src/core/secretsManager.ts index 6a6666da..94827b15 100644 --- a/src/core/secretsManager.ts +++ b/src/core/secretsManager.ts @@ -1,4 +1,14 @@ -import type { SecretStorage } from "vscode"; +import type { SecretStorage, Disposable } from "vscode"; + +const SESSION_TOKEN_KEY = "sessionToken"; + +const LOGIN_STATE_KEY = "loginState"; + +export enum AuthAction { + LOGIN, + LOGOUT, + INVALID, +} export class SecretsManager { constructor(private readonly secrets: SecretStorage) {} @@ -8,9 +18,9 @@ export class SecretsManager { */ public async setSessionToken(sessionToken?: string): Promise { if (!sessionToken) { - await this.secrets.delete("sessionToken"); + await this.secrets.delete(SESSION_TOKEN_KEY); } else { - await this.secrets.store("sessionToken", sessionToken); + await this.secrets.store(SESSION_TOKEN_KEY, sessionToken); } } @@ -19,11 +29,45 @@ export class SecretsManager { */ public async getSessionToken(): Promise { try { - return await this.secrets.get("sessionToken"); + return await this.secrets.get(SESSION_TOKEN_KEY); } catch { // The VS Code session store has become corrupt before, and // will fail to get the session token... return undefined; } } + + /** + * Triggers a login/logout event that propagates across all VS Code windows. + * Uses the secrets storage onDidChange event as a cross-window communication mechanism. + * Appends a timestamp to ensure the value always changes, guaranteeing the event fires. + */ + public async triggerLoginStateChange( + action: "login" | "logout", + ): Promise { + const date = new Date().toISOString(); + await this.secrets.store(LOGIN_STATE_KEY, `${action}-${date}`); + } + + /** + * Listens for login/logout events from any VS Code window. + * The secrets storage onDidChange event fires across all windows, enabling cross-window sync. + */ + public onDidChangeLoginState( + listener: (state: AuthAction) => Promise, + ): Disposable { + return this.secrets.onDidChange(async (e) => { + if (e.key === LOGIN_STATE_KEY) { + const state = await this.secrets.get(LOGIN_STATE_KEY); + if (state?.startsWith("login")) { + listener(AuthAction.LOGIN); + } else if (state?.startsWith("logout")) { + listener(AuthAction.LOGOUT); + } else { + // Secret was deleted or is invalid + listener(AuthAction.INVALID); + } + } + }); + } } diff --git a/src/error.ts b/src/error.ts index 7b93b458..70448d76 100644 --- a/src/error.ts +++ b/src/error.ts @@ -64,6 +64,8 @@ export class CertificateError extends Error { return new CertificateError(err.message, X509_ERR.UNTRUSTED_LEAF); case X509_ERR_CODE.SELF_SIGNED_CERT_IN_CHAIN: return new CertificateError(err.message, X509_ERR.UNTRUSTED_CHAIN); + case undefined: + break; } } return err; @@ -154,6 +156,7 @@ export class CertificateError extends Error { ); switch (val) { case CertificateError.ActionOK: + case undefined: return; case CertificateError.ActionAllowInsecure: await this.allowInsecure(); diff --git a/src/extension.ts b/src/extension.ts index e069c3a3..aba94cfe 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,6 +10,7 @@ import { CoderApi } from "./api/coderApi"; import { needToken } from "./api/utils"; import { Commands } from "./commands"; import { ServiceContainer } from "./core/container"; +import { AuthAction } from "./core/secretsManager"; import { CertificateError, getErrorDetail } from "./error"; import { Remote } from "./remote/remote"; import { toSafeHost } from "./util"; @@ -62,6 +63,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { const output = serviceContainer.getLogger(); const mementoManager = serviceContainer.getMementoManager(); const secretsManager = serviceContainer.getSecretsManager(); + const contextManager = serviceContainer.getContextManager(); // Try to clear this flag ASAP const isFirstConnect = await mementoManager.getAndClearFirstConnect(); @@ -167,6 +169,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { const token = needToken(vscode.workspace.getConfiguration()) ? params.get("token") : (params.get("token") ?? ""); + if (token) { client.setSessionToken(token); await secretsManager.setSessionToken(token); @@ -327,6 +330,29 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { ), ); + const remote = new Remote(serviceContainer, commands, ctx.extensionMode); + + ctx.subscriptions.push( + secretsManager.onDidChangeLoginState(async (state) => { + switch (state) { + case AuthAction.LOGIN: { + const token = await secretsManager.getSessionToken(); + const url = mementoManager.getUrl(); + // Should login the user directly if the URL+Token are valid + await commands.login({ url, token }); + // Resolve any pending login detection promises + remote.resolveLoginDetected(); + break; + } + case AuthAction.LOGOUT: + await commands.forceLogout(); + break; + case AuthAction.INVALID: + break; + } + }), + ); + // Since the "onResolveRemoteAuthority:ssh-remote" activation event exists // in package.json we're able to perform actions before the authority is // resolved by the remote SSH extension. @@ -337,7 +363,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // (this would require the user to uninstall the Coder extension and // reinstall after installing the remote SSH extension, which is annoying) if (remoteSSHExtension && vscodeProposed.env.remoteAuthority) { - const remote = new Remote(serviceContainer, commands, ctx.extensionMode); try { const details = await remote.setup( vscodeProposed.env.remoteAuthority, @@ -394,20 +419,12 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { output.info(`Logged in to ${baseUrl}; checking credentials`); client .getAuthenticatedUser() - .then(async (user) => { + .then((user) => { if (user && user.roles) { output.info("Credentials are valid"); - vscode.commands.executeCommand( - "setContext", - "coder.authenticated", - true, - ); + contextManager.set("coder.authenticated", true); if (user.roles.find((role) => role.name === "owner")) { - await vscode.commands.executeCommand( - "setContext", - "coder.isOwner", - true, - ); + contextManager.set("coder.isOwner", true); } // Fetch and monitor workspaces, now that we know the client is good. @@ -426,11 +443,11 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { ); }) .finally(() => { - vscode.commands.executeCommand("setContext", "coder.loaded", true); + contextManager.set("coder.loaded", true); }); } else { output.info("Not currently logged in"); - vscode.commands.executeCommand("setContext", "coder.loaded", true); + contextManager.set("coder.loaded", true); // Handle autologin, if not already logged in. const cfg = vscode.workspace.getConfiguration(); @@ -439,13 +456,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { cfg.get("coder.defaultUrl")?.trim() || process.env.CODER_URL?.trim(); if (defaultUrl) { - vscode.commands.executeCommand( - "coder.login", - defaultUrl, - undefined, - undefined, - "true", - ); + commands.login({ url: defaultUrl, autoLogin: true }); } } } diff --git a/src/remote/remote.ts b/src/remote/remote.ts index 2a286ab4..832a8086 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -30,6 +30,7 @@ import { type Commands } from "../commands"; import { type CliManager } from "../core/cliManager"; import * as cliUtils from "../core/cliUtils"; import { type ServiceContainer } from "../core/container"; +import { type ContextManager } from "../core/contextManager"; import { type PathResolver } from "../core/pathResolver"; import { featureSetForVersion, type FeatureSet } from "../featureSet"; import { getGlobalFlags } from "../globalFlags"; @@ -58,6 +59,12 @@ export class Remote { private readonly logger: Logger; private readonly pathResolver: PathResolver; private readonly cliManager: CliManager; + private readonly contextManager: ContextManager; + + // Used to race between the login dialog and logging in from a different window + private loginDetectedResolver: (() => void) | undefined; + private loginDetectedRejector: ((reason?: Error) => void) | undefined; + private loginDetectedPromise: Promise = Promise.resolve(); public constructor( serviceContainer: ServiceContainer, @@ -68,6 +75,33 @@ export class Remote { this.logger = serviceContainer.getLogger(); this.pathResolver = serviceContainer.getPathResolver(); this.cliManager = serviceContainer.getCliManager(); + this.contextManager = serviceContainer.getContextManager(); + } + + /** + * Creates a new promise that will be resolved when login is detected in another window. + */ + private createLoginDetectionPromise(): void { + if (this.loginDetectedRejector) { + this.loginDetectedRejector( + new Error("Login detection cancelled - new login attempt started"), + ); + } + this.loginDetectedPromise = new Promise((resolve, reject) => { + this.loginDetectedResolver = resolve; + this.loginDetectedRejector = reject; + }); + } + + /** + * Resolves the current login detection promise if one exists. + */ + public resolveLoginDetected(): void { + if (this.loginDetectedResolver) { + this.loginDetectedResolver(); + this.loginDetectedResolver = undefined; + this.loginDetectedRejector = undefined; + } } private async confirmStart(workspaceName: string): Promise { @@ -238,34 +272,48 @@ export class Remote { parts.label, ); - // It could be that the cli config was deleted. If so, ask for the url. - if ( - !baseUrlRaw || - (!token && needToken(vscode.workspace.getConfiguration())) - ) { - const result = await this.vscodeProposed.window.showInformationMessage( - "You are not logged in...", + const showLoginDialog = async (message: string) => { + this.createLoginDetectionPromise(); + const dialogPromise = this.vscodeProposed.window.showInformationMessage( + message, { useCustom: true, modal: true, - detail: `You must log in to access ${workspaceName}.`, + detail: `You must log in to access ${workspaceName}. If you've already logged in, you may close this dialog.`, }, "Log In", ); - if (!result) { - // User declined to log in. - await this.closeRemote(); + + // Race between dialog and login detection + const result = await Promise.race([ + this.loginDetectedPromise.then(() => ({ type: "login" as const })), + dialogPromise.then((userChoice) => ({ + type: "dialog" as const, + userChoice, + })), + ]); + + if (result.type === "login") { + return this.setup(remoteAuthority, firstConnect); } else { - // Log in then try again. - await vscode.commands.executeCommand( - "coder.login", - baseUrlRaw, - undefined, - parts.label, - ); - await this.setup(remoteAuthority, firstConnect); + if (!result.userChoice) { + // User declined to log in. + await this.closeRemote(); + return; + } else { + // Log in then try again. + await this.commands.login({ url: baseUrlRaw, label: parts.label }); + return this.setup(remoteAuthority, firstConnect); + } } - return; + }; + + // It could be that the cli config was deleted. If so, ask for the url. + if ( + !baseUrlRaw || + (!token && needToken(vscode.workspace.getConfiguration())) + ) { + return showLoginDialog("You are not logged in..."); } this.logger.info("Using deployment URL", baseUrlRaw); @@ -364,28 +412,7 @@ export class Remote { return; } case 401: { - const result = - await this.vscodeProposed.window.showInformationMessage( - "Your session expired...", - { - useCustom: true, - modal: true, - detail: `You must log in to access ${workspaceName}.`, - }, - "Log In", - ); - if (!result) { - await this.closeRemote(); - } else { - await vscode.commands.executeCommand( - "coder.login", - baseUrlRaw, - undefined, - parts.label, - ); - await this.setup(remoteAuthority, firstConnect); - } - return; + return showLoginDialog("Your session expired..."); } default: throw error; @@ -521,6 +548,7 @@ export class Remote { workspaceClient, this.logger, this.vscodeProposed, + this.contextManager, ); disposables.push(monitor); disposables.push( diff --git a/src/workspace/workspaceMonitor.ts b/src/workspace/workspaceMonitor.ts index 8ff99137..0b154f75 100644 --- a/src/workspace/workspaceMonitor.ts +++ b/src/workspace/workspaceMonitor.ts @@ -7,6 +7,7 @@ import * as vscode from "vscode"; import { createWorkspaceIdentifier, errToStr } from "../api/api-helper"; import { type CoderApi } from "../api/coderApi"; +import { type ContextManager } from "../core/contextManager"; import { type Logger } from "../logging/logger"; import { type OneWayWebSocket } from "../websocket/oneWayWebSocket"; @@ -41,6 +42,7 @@ export class WorkspaceMonitor implements vscode.Disposable { private readonly logger: Logger, // We use the proposed API to get access to useCustom in dialogs. private readonly vscodeProposed: typeof vscode, + private readonly contextManager: ContextManager, ) { this.name = createWorkspaceIdentifier(workspace); const socket = this.client.watchWorkspace(workspace); @@ -217,11 +219,7 @@ export class WorkspaceMonitor implements vscode.Disposable { } private updateContext(workspace: Workspace) { - vscode.commands.executeCommand( - "setContext", - "coder.workspace.updatable", - workspace.outdated, - ); + this.contextManager.set("coder.workspace.updatable", workspace.outdated); } private updateStatusBar(workspace: Workspace) { diff --git a/test/mocks/testHelpers.ts b/test/mocks/testHelpers.ts index 14eca74b..5cfe44e5 100644 --- a/test/mocks/testHelpers.ts +++ b/test/mocks/testHelpers.ts @@ -234,10 +234,19 @@ export class InMemoryMemento implements vscode.Memento { export class InMemorySecretStorage implements vscode.SecretStorage { private secrets = new Map(); private isCorrupted = false; - - onDidChange: vscode.Event = () => ({ - dispose: () => {}, - }); + private listeners: Array<(e: vscode.SecretStorageChangeEvent) => void> = []; + + onDidChange: vscode.Event = (listener) => { + this.listeners.push(listener); + return { + dispose: () => { + const index = this.listeners.indexOf(listener); + if (index > -1) { + this.listeners.splice(index, 1); + } + }, + }; + }; async get(key: string): Promise { if (this.isCorrupted) { @@ -250,17 +259,30 @@ export class InMemorySecretStorage implements vscode.SecretStorage { if (this.isCorrupted) { return Promise.reject(new Error("Storage corrupted")); } + const oldValue = this.secrets.get(key); this.secrets.set(key, value); + if (oldValue !== value) { + this.fireChangeEvent(key); + } } async delete(key: string): Promise { if (this.isCorrupted) { return Promise.reject(new Error("Storage corrupted")); } + const hadKey = this.secrets.has(key); this.secrets.delete(key); + if (hadKey) { + this.fireChangeEvent(key); + } } corruptStorage(): void { this.isCorrupted = true; } + + private fireChangeEvent(key: string): void { + const event: vscode.SecretStorageChangeEvent = { key }; + this.listeners.forEach((listener) => listener(event)); + } } diff --git a/test/unit/core/secretsManager.test.ts b/test/unit/core/secretsManager.test.ts index 7100a29b..bfe8c713 100644 --- a/test/unit/core/secretsManager.test.ts +++ b/test/unit/core/secretsManager.test.ts @@ -1,6 +1,6 @@ -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; -import { SecretsManager } from "@/core/secretsManager"; +import { AuthAction, SecretsManager } from "@/core/secretsManager"; import { InMemorySecretStorage } from "../../mocks/testHelpers"; @@ -13,7 +13,7 @@ describe("SecretsManager", () => { secretsManager = new SecretsManager(secretStorage); }); - describe("setSessionToken", () => { + describe("session token", () => { it("should store and retrieve tokens", async () => { await secretsManager.setSessionToken("test-token"); expect(await secretsManager.getSessionToken()).toBe("test-token"); @@ -31,9 +31,7 @@ describe("SecretsManager", () => { await secretsManager.setSessionToken(undefined); expect(await secretsManager.getSessionToken()).toBeUndefined(); }); - }); - describe("getSessionToken", () => { it("should return undefined for corrupted storage", async () => { await secretStorage.store("sessionToken", "valid-token"); secretStorage.corruptStorage(); @@ -41,4 +39,44 @@ describe("SecretsManager", () => { expect(await secretsManager.getSessionToken()).toBeUndefined(); }); }); + + describe("login state", () => { + it("should trigger login events", async () => { + const events: Array = []; + secretsManager.onDidChangeLoginState((state) => { + events.push(state); + return Promise.resolve(); + }); + + await secretsManager.triggerLoginStateChange("login"); + expect(events).toEqual([AuthAction.LOGIN]); + }); + + it("should trigger logout events", async () => { + const events: Array = []; + secretsManager.onDidChangeLoginState((state) => { + events.push(state); + return Promise.resolve(); + }); + + await secretsManager.triggerLoginStateChange("logout"); + expect(events).toEqual([AuthAction.LOGOUT]); + }); + + it("should fire same event twice in a row", async () => { + vi.useFakeTimers(); + const events: Array = []; + secretsManager.onDidChangeLoginState((state) => { + events.push(state); + return Promise.resolve(); + }); + + await secretsManager.triggerLoginStateChange("login"); + vi.advanceTimersByTime(5); + await secretsManager.triggerLoginStateChange("login"); + + expect(events).toEqual([AuthAction.LOGIN, AuthAction.LOGIN]); + vi.useRealTimers(); + }); + }); }); From b2ac27bf4d9058cbae0113cbab3745cd437b418b Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Mon, 6 Oct 2025 15:04:20 +0300 Subject: [PATCH 20/45] Add changelog for fixing the login/logout across multiple windows (#608) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9da9987..c09aea12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixed + +- Logging in or out in one VS Code window now properly updates the authentication status in all other open windows. + ### Added - Support for `CODER_BINARY_DESTINATION` environment variable to set CLI download location (overridden by extension setting `coder.binaryDestination` if configured). From c0a2b5f4e794f07e3eb430afcb03c8f4ca6fc917 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 7 Oct 2025 10:49:14 +0300 Subject: [PATCH 21/45] Fix JSON.stringify circular reference error in logging infrastructure (#607) Resolves circular structure errors when serializing request/response bodies for logging. Other changes: * Added test coverage for logging infrastructure * Fixed flaky stream test * Calculate body size directly from strings/buffers instead of stringifying * Use util.inspect instead of JSON.stringify for body serialization Fixes #606 --- CHANGELOG.md | 1 + src/api/coderApi.ts | 80 +++++++++++++++++- src/core/cliManager.ts | 2 +- src/logging/formatters.ts | 35 ++------ src/logging/httpLogger.ts | 18 ++-- src/logging/types.ts | 2 + src/logging/utils.ts | 43 ++++++++-- src/logging/wsLogger.ts | 2 +- test/mocks/testHelpers.ts | 12 +++ test/unit/core/cliManager.test.ts | 26 +++--- test/unit/logging/formatters.test.ts | 122 +++++++++++++++++++++++++++ test/unit/logging/httpLogger.test.ts | 112 ++++++++++++++++++++++++ test/unit/logging/utils.test.ts | 106 +++++++++++++++++++++++ test/unit/logging/wsLogger.test.ts | 71 ++++++++++++++++ 14 files changed, 573 insertions(+), 59 deletions(-) create mode 100644 test/unit/logging/formatters.test.ts create mode 100644 test/unit/logging/httpLogger.test.ts create mode 100644 test/unit/logging/utils.test.ts create mode 100644 test/unit/logging/wsLogger.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c09aea12..9127b22c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Logging in or out in one VS Code window now properly updates the authentication status in all other open windows. +- Fix an issue with JSON stringification errors occurring when logging circular objects. ### Added diff --git a/src/api/coderApi.ts b/src/api/coderApi.ts index 1d73ef00..1d523b60 100644 --- a/src/api/coderApi.ts +++ b/src/api/coderApi.ts @@ -1,4 +1,9 @@ -import { type AxiosInstance } from "axios"; +import { + type AxiosResponseHeaders, + type AxiosInstance, + type AxiosHeaders, + type AxiosResponseTransformer, +} from "axios"; import { Api } from "coder/site/src/api/api"; import { type GetInboxNotificationResponse, @@ -23,6 +28,7 @@ import { type RequestConfigWithMeta, HttpClientLogLevel, } from "../logging/types"; +import { sizeOf } from "../logging/utils"; import { WsLogger } from "../logging/wsLogger"; import { OneWayWebSocket, @@ -207,7 +213,24 @@ function addLoggingInterceptors(client: AxiosInstance, logger: Logger) { (config) => { const configWithMeta = config as RequestConfigWithMeta; configWithMeta.metadata = createRequestMeta(); - logRequest(logger, configWithMeta, getLogLevel()); + + config.transformRequest = [ + ...wrapRequestTransform( + config.transformRequest || client.defaults.transformRequest || [], + configWithMeta, + ), + (data) => { + // Log after setting the raw request size + logRequest(logger, configWithMeta, getLogLevel()); + return data; + }, + ]; + + config.transformResponse = wrapResponseTransform( + config.transformResponse || client.defaults.transformResponse || [], + configWithMeta, + ); + return config; }, (error: unknown) => { @@ -228,6 +251,59 @@ function addLoggingInterceptors(client: AxiosInstance, logger: Logger) { ); } +function wrapRequestTransform( + transformer: AxiosResponseTransformer | AxiosResponseTransformer[], + config: RequestConfigWithMeta, +): AxiosResponseTransformer[] { + return [ + (data: unknown, headers: AxiosHeaders) => { + const transformerArray = Array.isArray(transformer) + ? transformer + : [transformer]; + + // Transform the request first then get the size (measure what's sent over the wire) + const result = transformerArray.reduce( + (d, fn) => fn.call(config, d, headers), + data, + ); + + config.rawRequestSize = getSize(config.headers, result); + + return result; + }, + ]; +} + +function wrapResponseTransform( + transformer: AxiosResponseTransformer | AxiosResponseTransformer[], + config: RequestConfigWithMeta, +): AxiosResponseTransformer[] { + return [ + (data: unknown, headers: AxiosResponseHeaders, status?: number) => { + // Get the size before transforming the response (measure what's sent over the wire) + config.rawResponseSize = getSize(headers, data); + + const transformerArray = Array.isArray(transformer) + ? transformer + : [transformer]; + + return transformerArray.reduce( + (d, fn) => fn.call(config, d, headers, status), + data, + ); + }, + ]; +} + +function getSize(headers: AxiosHeaders, data: unknown): number | undefined { + const contentLength = headers["content-length"]; + if (contentLength !== undefined) { + return parseInt(contentLength, 10); + } + + return sizeOf(data); +} + function getLogLevel(): HttpClientLogLevel { const logLevelStr = vscode.workspace .getConfiguration() diff --git a/src/core/cliManager.ts b/src/core/cliManager.ts index 1bb0afa1..4e8833fe 100644 --- a/src/core/cliManager.ts +++ b/src/core/cliManager.ts @@ -267,7 +267,7 @@ export class CliManager { if (Number.isNaN(contentLength)) { this.output.warn( "Got invalid or missing content length", - rawContentLength, + rawContentLength ?? "", ); } else { this.output.info("Got content length", prettyBytes(contentLength)); diff --git a/src/logging/formatters.ts b/src/logging/formatters.ts index 1ad45231..8247f9b1 100644 --- a/src/logging/formatters.ts +++ b/src/logging/formatters.ts @@ -1,6 +1,8 @@ import prettyBytes from "pretty-bytes"; -import type { InternalAxiosRequestConfig } from "axios"; +import { safeStringify } from "./utils"; + +import type { AxiosRequestConfig } from "axios"; const SENSITIVE_HEADERS = ["Coder-Session-Token", "Proxy-Authorization"]; @@ -18,35 +20,14 @@ export function formatTime(ms: number): string { } export function formatMethod(method: string | undefined): string { - return (method ?? "GET").toUpperCase(); + return method?.toUpperCase() || "GET"; } -/** - * Formats content-length for display. Returns the header value if available, - * otherwise estimates size by serializing the data body (prefixed with ~). - */ -export function formatContentLength( - headers: Record, - data: unknown, -): string { - const len = headers["content-length"]; - if (len && typeof len === "string") { - const bytes = parseInt(len, 10); - return isNaN(bytes) ? "(?b)" : `(${prettyBytes(bytes)})`; - } - - // Estimate from data if no header - if (data !== undefined && data !== null) { - const estimated = Buffer.byteLength(JSON.stringify(data), "utf8"); - return `(~${prettyBytes(estimated)})`; - } - - return `(${prettyBytes(0)})`; +export function formatSize(size: number | undefined): string { + return size === undefined ? "(? B)" : `(${prettyBytes(size)})`; } -export function formatUri( - config: InternalAxiosRequestConfig | undefined, -): string { +export function formatUri(config: AxiosRequestConfig | undefined): string { return config?.url || ""; } @@ -66,7 +47,7 @@ export function formatHeaders(headers: Record): string { export function formatBody(body: unknown): string { if (body) { - return JSON.stringify(body); + return safeStringify(body) ?? ""; } else { return ""; } diff --git a/src/logging/httpLogger.ts b/src/logging/httpLogger.ts index 7e569cad..5634a165 100644 --- a/src/logging/httpLogger.ts +++ b/src/logging/httpLogger.ts @@ -5,9 +5,9 @@ import { getErrorDetail } from "../error"; import { formatBody, - formatContentLength, formatHeaders, formatMethod, + formatSize, formatTime, formatUri, } from "./formatters"; @@ -42,11 +42,10 @@ export function logRequest( return; } - const { requestId, method, url } = parseConfig(config); - const len = formatContentLength(config.headers, config.data); + const { requestId, method, url, requestSize } = parseConfig(config); const msg = [ - `→ ${shortId(requestId)} ${method} ${url} ${len}`, + `→ ${shortId(requestId)} ${method} ${url} ${requestSize}`, ...buildExtraLogs(config.headers, config.data, logLevel), ]; logger.trace(msg.join("\n")); @@ -64,11 +63,12 @@ export function logResponse( return; } - const { requestId, method, url, time } = parseConfig(response.config); - const len = formatContentLength(response.headers, response.data); + const { requestId, method, url, time, responseSize } = parseConfig( + response.config, + ); const msg = [ - `← ${shortId(requestId)} ${response.status} ${method} ${url} ${len} ${time}`, + `← ${shortId(requestId)} ${response.status} ${method} ${url} ${responseSize} ${time}`, ...buildExtraLogs(response.headers, response.data, logLevel), ]; logger.trace(msg.join("\n")); @@ -150,6 +150,8 @@ function parseConfig(config: RequestConfigWithMeta | undefined): { method: string; url: string; time: string; + requestSize: string; + responseSize: string; } { const meta = config?.metadata; return { @@ -157,5 +159,7 @@ function parseConfig(config: RequestConfigWithMeta | undefined): { method: formatMethod(config?.method), url: formatUri(config), time: meta ? formatTime(Date.now() - meta.startedAt) : "?ms", + requestSize: formatSize(config?.rawRequestSize), + responseSize: formatSize(config?.rawResponseSize), }; } diff --git a/src/logging/types.ts b/src/logging/types.ts index d1ee51ca..30837a0d 100644 --- a/src/logging/types.ts +++ b/src/logging/types.ts @@ -14,4 +14,6 @@ export interface RequestMeta { export type RequestConfigWithMeta = InternalAxiosRequestConfig & { metadata?: RequestMeta; + rawRequestSize?: number; + rawResponseSize?: number; }; diff --git a/src/logging/utils.ts b/src/logging/utils.ts index c371f65e..5deadaaf 100644 --- a/src/logging/utils.ts +++ b/src/logging/utils.ts @@ -1,21 +1,37 @@ import { Buffer } from "node:buffer"; import crypto from "node:crypto"; +import util from "node:util"; export function shortId(id: string): string { return id.slice(0, 8); } +export function createRequestId(): string { + return crypto.randomUUID().replace(/-/g, ""); +} + +/** + * Returns the byte size of the data if it can be determined from the data's intrinsic properties, + * otherwise returns undefined (e.g., for plain objects and arrays that would require serialization). + */ export function sizeOf(data: unknown): number | undefined { if (data === null || data === undefined) { return 0; } - if (typeof data === "string") { - return Buffer.byteLength(data); + if (typeof data === "boolean") { + return 4; + } + if (typeof data === "number") { + return 8; } - if (Buffer.isBuffer(data)) { - return data.length; + if (typeof data === "string" || typeof data === "bigint") { + return Buffer.byteLength(data.toString()); } - if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) { + if ( + Buffer.isBuffer(data) || + data instanceof ArrayBuffer || + ArrayBuffer.isView(data) + ) { return data.byteLength; } if ( @@ -28,6 +44,19 @@ export function sizeOf(data: unknown): number | undefined { return undefined; } -export function createRequestId(): string { - return crypto.randomUUID().replace(/-/g, ""); +export function safeStringify(data: unknown): string | null { + try { + return util.inspect(data, { + showHidden: false, + depth: Infinity, + maxArrayLength: Infinity, + maxStringLength: Infinity, + breakLength: Infinity, + compact: true, + getters: false, // avoid side-effects + }); + } catch { + // Should rarely happen but just in case + return null; + } } diff --git a/src/logging/wsLogger.ts b/src/logging/wsLogger.ts index b33118b7..fd6acd00 100644 --- a/src/logging/wsLogger.ts +++ b/src/logging/wsLogger.ts @@ -77,6 +77,6 @@ export class WsLogger { private formatBytes(): string { const bytes = prettyBytes(this.byteCount); - return this.unknownByteCount ? `>=${bytes}` : bytes; + return this.unknownByteCount ? `>= ${bytes}` : bytes; } } diff --git a/test/mocks/testHelpers.ts b/test/mocks/testHelpers.ts index 5cfe44e5..2ef46716 100644 --- a/test/mocks/testHelpers.ts +++ b/test/mocks/testHelpers.ts @@ -1,6 +1,8 @@ import { vi } from "vitest"; import * as vscode from "vscode"; +import { type Logger } from "@/logging/logger"; + /** * Mock configuration provider that integrates with the vscode workspace configuration mock. * Use this to set configuration values that will be returned by vscode.workspace.getConfiguration(). @@ -286,3 +288,13 @@ export class InMemorySecretStorage implements vscode.SecretStorage { this.listeners.forEach((listener) => listener(event)); } } + +export function createMockLogger(): Logger { + return { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; +} diff --git a/test/unit/core/cliManager.test.ts b/test/unit/core/cliManager.test.ts index 2d76e8d4..3e1dfb0d 100644 --- a/test/unit/core/cliManager.test.ts +++ b/test/unit/core/cliManager.test.ts @@ -12,10 +12,10 @@ import * as vscode from "vscode"; import { CliManager } from "@/core/cliManager"; import * as cliUtils from "@/core/cliUtils"; import { PathResolver } from "@/core/pathResolver"; -import { type Logger } from "@/logging/logger"; import * as pgp from "@/pgp"; import { + createMockLogger, MockConfigurationProvider, MockProgressReporter, MockUserInteraction, @@ -625,16 +625,6 @@ describe("CliManager", () => { }); }); - function createMockLogger(): Logger { - return { - trace: vi.fn(), - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }; - } - function createMockApi(version: string, url: string): Api { const axios = { defaults: { baseURL: url }, @@ -740,10 +730,11 @@ describe("CliManager", () => { content: string, options: { chunkSize?: number; delay?: number } = {}, ): IncomingMessage { - const { chunkSize = 8, delay = 0 } = options; + const { chunkSize = 8, delay = 1 } = options; const buffer = Buffer.from(content); let position = 0; + let closeCallback: ((...args: unknown[]) => void) | null = null; return { on: vi.fn((event: string, callback: (...args: unknown[]) => void) => { @@ -759,13 +750,20 @@ describe("CliManager", () => { callback(chunk); if (position < buffer.length) { setTimeout(sendChunk, delay); + } else { + // All chunks sent - use setImmediate to ensure close happens + // after all synchronous operations and I/O callbacks complete + setImmediate(() => { + if (closeCallback) { + closeCallback(); + } + }); } } }; setTimeout(sendChunk, delay); } else if (event === "close") { - // Just close after a delay - setTimeout(() => callback(), 10); + closeCallback = callback; } }), destroy: vi.fn(), diff --git a/test/unit/logging/formatters.test.ts b/test/unit/logging/formatters.test.ts new file mode 100644 index 00000000..1cd4fedf --- /dev/null +++ b/test/unit/logging/formatters.test.ts @@ -0,0 +1,122 @@ +import { describe, expect, it } from "vitest"; + +import { + formatBody, + formatHeaders, + formatMethod, + formatSize, + formatTime, + formatUri, +} from "@/logging/formatters"; + +describe("Logging formatters", () => { + it("formats time in appropriate units", () => { + expect(formatTime(500)).toBe("500ms"); + expect(formatTime(1000)).toBe("1.00s"); + expect(formatTime(5500)).toBe("5.50s"); + expect(formatTime(60000)).toBe("1.00m"); + expect(formatTime(150000)).toBe("2.50m"); + expect(formatTime(3600000)).toBe("1.00h"); + expect(formatTime(7255000)).toBe("2.02h"); + }); + + describe("formatMethod", () => { + it("normalizes HTTP methods to uppercase", () => { + expect(formatMethod("get")).toBe("GET"); + expect(formatMethod("post")).toBe("POST"); + expect(formatMethod("PUT")).toBe("PUT"); + expect(formatMethod("delete")).toBe("DELETE"); + }); + + it("defaults to GET for falsy values", () => { + expect(formatMethod(undefined)).toBe("GET"); + expect(formatMethod("")).toBe("GET"); + }); + }); + + describe("formatSize", () => { + it("formats byte sizes using pretty-bytes", () => { + expect(formatSize(1024)).toContain("1.02 kB"); + expect(formatSize(0)).toBe("(0 B)"); + }); + + it("returns placeholder for undefined", () => { + expect(formatSize(undefined)).toBe("(? B)"); + }); + }); + + describe("formatUri", () => { + it("returns URL when present", () => { + expect(formatUri({ url: "https://example.com/api" })).toBe( + "https://example.com/api", + ); + expect(formatUri({ url: "/relative/path" })).toBe("/relative/path"); + }); + + it("returns placeholder for missing URL", () => { + expect(formatUri(undefined)).toContain("no url"); + expect(formatUri({})).toContain("no url"); + expect(formatUri({ url: "" })).toContain("no url"); + }); + }); + + describe("formatHeaders", () => { + it("formats headers as key-value pairs", () => { + const headers = { + "content-type": "application/json", + accept: "text/html", + }; + const result = formatHeaders(headers); + expect(result).toContain("content-type: application/json"); + expect(result).toContain("accept: text/html"); + }); + + it("redacts sensitive headers", () => { + const sensitiveHeaders = ["Coder-Session-Token", "Proxy-Authorization"]; + + sensitiveHeaders.forEach((header) => { + const result = formatHeaders({ [header]: "secret-value" }); + expect(result).toContain(`${header}: `); + expect(result).not.toContain("secret-value"); + }); + }); + + it("returns placeholder for empty headers", () => { + expect(formatHeaders({})).toBe(""); + }); + }); + + describe("formatBody", () => { + it("formats various body types", () => { + expect(formatBody({ key: "value" })).toContain("key: 'value'"); + expect(formatBody("plain text")).toContain("plain text"); + expect(formatBody([1, 2, 3])).toContain("1"); + expect(formatBody(123)).toContain("123"); + expect(formatBody(true)).toContain("true"); + }); + + it("handles circular references gracefully", () => { + const circular: Record = { a: 1 }; + circular.self = circular; + const result = formatBody(circular); + expect(result).toBeTruthy(); + expect(result).not.toContain("invalid body"); + expect(result).toContain("a: 1"); + }); + + it("handles deep nesting", () => { + const deep = { + level1: { level2: { level3: { level4: { value: "deep" } } } }, + }; + const result = formatBody(deep); + expect(result).toContain("level4: { value: 'deep' }"); + }); + + it("returns placeholder for empty values", () => { + const emptyValues = [null, undefined, "", 0, false]; + emptyValues.forEach((value) => { + expect(formatBody(value)).toContain("no body"); + }); + }); + }); +}); diff --git a/test/unit/logging/httpLogger.test.ts b/test/unit/logging/httpLogger.test.ts new file mode 100644 index 00000000..81cfbed8 --- /dev/null +++ b/test/unit/logging/httpLogger.test.ts @@ -0,0 +1,112 @@ +import { AxiosError, type AxiosHeaders, type AxiosResponse } from "axios"; +import { describe, expect, it, vi } from "vitest"; + +import { + createRequestMeta, + logError, + logRequest, + logResponse, +} from "@/logging/httpLogger"; +import { + HttpClientLogLevel, + type RequestConfigWithMeta, +} from "@/logging/types"; + +import { createMockLogger } from "../../mocks/testHelpers"; + +describe("REST HTTP Logger", () => { + describe("log level behavior", () => { + const config = { + method: "POST", + url: "https://api.example.com/endpoint", + headers: { + "content-type": "application/json", + } as unknown as AxiosHeaders, + data: { key: "value" }, + metadata: createRequestMeta(), + } as RequestConfigWithMeta; + + it("respects NONE level for trace logs", () => { + const logger = createMockLogger(); + + logRequest(logger, config, HttpClientLogLevel.NONE); + logResponse( + logger, + { status: 200 } as AxiosResponse, + HttpClientLogLevel.NONE, + ); + logError(logger, new Error("test"), HttpClientLogLevel.NONE); + + expect(logger.trace).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalled(); // always log errors + }); + + it("includes headers at HEADERS level but not at BASIC", () => { + const logger = createMockLogger(); + + logRequest(logger, config, HttpClientLogLevel.BASIC); + expect(logger.trace).not.toHaveBeenCalledWith( + expect.stringContaining("content-type"), + ); + + vi.clearAllMocks(); + logRequest(logger, config, HttpClientLogLevel.HEADERS); + expect(logger.trace).toHaveBeenCalledWith( + expect.stringContaining("content-type"), + ); + }); + + it("includes body at BODY level but not at HEADERS", () => { + const logger = createMockLogger(); + + logRequest(logger, config, HttpClientLogLevel.HEADERS); + expect(logger.trace).not.toHaveBeenCalledWith( + expect.stringContaining("key: 'value'"), + ); + + vi.clearAllMocks(); + logRequest(logger, config, HttpClientLogLevel.BODY); + expect(logger.trace).toHaveBeenCalledWith( + expect.stringContaining("key: 'value'"), + ); + }); + }); + + describe("error handling", () => { + it("distinguishes between network errors and response errors", () => { + const logger = createMockLogger(); + + const networkError = new AxiosError("Some Network Error", "ECONNREFUSED"); + networkError.config = { + metadata: createRequestMeta(), + } as RequestConfigWithMeta; + + logError(logger, networkError, HttpClientLogLevel.BASIC); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining("Some Network Error"), + ); + + // Response error (4xx/5xx) + vi.clearAllMocks(); + const responseError = new AxiosError("Bad Request"); + responseError.config = { + metadata: createRequestMeta(), + } as RequestConfigWithMeta; + responseError.response = { status: 400 } as AxiosResponse; + + logError(logger, responseError, HttpClientLogLevel.BASIC); + expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("400")); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining("Bad Request"), + ); + }); + + it("handles non-Axios errors", () => { + const logger = createMockLogger(); + const error = new Error("Generic error"); + + logError(logger, error, HttpClientLogLevel.BASIC); + expect(logger.error).toHaveBeenCalledWith("Request error", error); + }); + }); +}); diff --git a/test/unit/logging/utils.test.ts b/test/unit/logging/utils.test.ts new file mode 100644 index 00000000..3adbeecb --- /dev/null +++ b/test/unit/logging/utils.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from "vitest"; + +import { + createRequestId, + serializeValue, + shortId, + sizeOf, +} from "@/logging/utils"; + +describe("Logging utils", () => { + describe("shortId", () => { + it("truncates long strings to 8 characters", () => { + expect(shortId("abcdefghijklmnop")).toBe("abcdefgh"); + expect(shortId("12345678")).toBe("12345678"); + expect(shortId("123456789")).toBe("12345678"); + }); + + it("returns short strings unchanged", () => { + expect(shortId("short")).toBe("short"); + expect(shortId("")).toBe(""); + expect(shortId("1234567")).toBe("1234567"); + }); + }); + + describe("sizeOf", () => { + it.each([ + // Primitives return a fixed value + [null, 0], + [undefined, 0], + [42, 8], + [3.14, 8], + [false, 4], + // Strings + ["hello", 5], + ["✓", 3], + ["unicode: ✓", 12], + // Buffers + [Buffer.from("test"), 4], + [BigInt(12345), 5], + [BigInt(0), 1], + [Buffer.alloc(100), 100], + [Buffer.from([]), 0], + // Typed-arrays + [new ArrayBuffer(50), 50], + [new Uint8Array([1, 2, 3, 4]), 4], + [new Int32Array([1, 2, 3]), 12], + [new Float64Array([1.0, 2.0]), 16], + // Objects/untyped-arrays return undefined + [{ size: 1024 }, 1024], + [{ size: 0 }, 0], + [{ size: "not a number" }, undefined], + [[], undefined], + [[1, 2, 3], undefined], + [["a", "b", "c"], undefined], + [{}, undefined], + [{ foo: "bar" }, undefined], + [{ nested: { value: 123 } }, undefined], + ])("returns size for %s", (data: unknown, bytes: number | undefined) => { + expect(sizeOf(data)).toBe(bytes); + }); + + it("handles circular references safely", () => { + const circular: Record = { a: 1 }; + circular.self = circular; + expect(sizeOf(circular)).toBeUndefined(); + + const arr: unknown[] = [1, 2, 3]; + arr.push(arr); + expect(sizeOf(arr)).toBeUndefined(); + }); + }); + + describe("serializeValue", () => { + it("formats various data types", () => { + expect(serializeValue({ key: "value" })).toContain("key: 'value'"); + expect(serializeValue("plain text")).toContain("plain text"); + expect(serializeValue([1, 2, 3])).toContain("1"); + expect(serializeValue(123)).toContain("123"); + expect(serializeValue(true)).toContain("true"); + }); + + it("handles circular references safely", () => { + const circular: Record = { a: 1 }; + circular.self = circular; + const result = serializeValue(circular); + expect(result).toBeTruthy(); + expect(result).toContain("a: 1"); + }); + + it("handles deep nesting", () => { + const deep = { + level1: { level2: { level3: { level4: { value: "deep" } } } }, + }; + const result = serializeValue(deep); + expect(result).toContain("level4: { value: 'deep' }"); + }); + }); + + describe("createRequestId", () => { + it("generates valid UUID format without dashes", () => { + const id = createRequestId(); + expect(id).toHaveLength(32); + expect(id).not.toContain("-"); + }); + }); +}); diff --git a/test/unit/logging/wsLogger.test.ts b/test/unit/logging/wsLogger.test.ts new file mode 100644 index 00000000..5bf9d5b1 --- /dev/null +++ b/test/unit/logging/wsLogger.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; + +import { WsLogger } from "@/logging/wsLogger"; + +import { createMockLogger } from "../../mocks/testHelpers"; + +describe("WS Logger", () => { + it("tracks message count and byte size", () => { + const logger = createMockLogger(); + const wsLogger = new WsLogger(logger, "wss://example.com"); + + wsLogger.logOpen(); + wsLogger.logMessage("hello"); + wsLogger.logMessage("world"); + wsLogger.logMessage(Buffer.from("test")); + wsLogger.logClose(); + + expect(logger.trace).toHaveBeenCalledWith( + expect.stringContaining("3 msgs"), + ); + expect(logger.trace).toHaveBeenCalledWith(expect.stringContaining("14 B")); + }); + + it("handles unknown byte sizes with >= indicator", () => { + const logger = createMockLogger(); + const wsLogger = new WsLogger(logger, "wss://example.com"); + + wsLogger.logOpen(); + wsLogger.logMessage({ complex: "object" }); // Unknown size - no estimation + wsLogger.logMessage("known"); + wsLogger.logClose(); + + expect(logger.trace).toHaveBeenLastCalledWith( + expect.stringContaining(">= 5 B"), + ); + }); + + it("handles close before open gracefully", () => { + const logger = createMockLogger(); + const wsLogger = new WsLogger(logger, "wss://example.com"); + + // Closing without opening should not throw + expect(() => wsLogger.logClose()).not.toThrow(); + expect(logger.trace).toHaveBeenCalled(); + }); + + it("formats large message counts with compact notation", () => { + const logger = createMockLogger(); + const wsLogger = new WsLogger(logger, "wss://example.com"); + + wsLogger.logOpen(); + for (let i = 0; i < 1100; i++) { + wsLogger.logMessage("x"); + } + wsLogger.logClose(); + + expect(logger.trace).toHaveBeenLastCalledWith( + expect.stringMatching(/1[.,]1K\s*msgs/), + ); + }); + + it("logs errors with error object", () => { + const logger = createMockLogger(); + const wsLogger = new WsLogger(logger, "wss://example.com"); + const error = new Error("Connection failed"); + + wsLogger.logError(error, "Failed to connect"); + + expect(logger.error).toHaveBeenCalledWith(expect.any(String), error); + }); +}); From dc7688e0c630bbe420f36859577e6b464c5ba7c6 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 7 Oct 2025 11:07:59 +0300 Subject: [PATCH 22/45] Fix function rename in logging utils test (#614) --- test/unit/logging/utils.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/unit/logging/utils.test.ts b/test/unit/logging/utils.test.ts index 3adbeecb..4d0f71eb 100644 --- a/test/unit/logging/utils.test.ts +++ b/test/unit/logging/utils.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { createRequestId, - serializeValue, + safeStringify, shortId, sizeOf, } from "@/logging/utils"; @@ -70,19 +70,19 @@ describe("Logging utils", () => { }); }); - describe("serializeValue", () => { + describe("safeStringify", () => { it("formats various data types", () => { - expect(serializeValue({ key: "value" })).toContain("key: 'value'"); - expect(serializeValue("plain text")).toContain("plain text"); - expect(serializeValue([1, 2, 3])).toContain("1"); - expect(serializeValue(123)).toContain("123"); - expect(serializeValue(true)).toContain("true"); + expect(safeStringify({ key: "value" })).toContain("key: 'value'"); + expect(safeStringify("plain text")).toContain("plain text"); + expect(safeStringify([1, 2, 3])).toContain("1"); + expect(safeStringify(123)).toContain("123"); + expect(safeStringify(true)).toContain("true"); }); it("handles circular references safely", () => { const circular: Record = { a: 1 }; circular.self = circular; - const result = serializeValue(circular); + const result = safeStringify(circular); expect(result).toBeTruthy(); expect(result).toContain("a: 1"); }); @@ -91,7 +91,7 @@ describe("Logging utils", () => { const deep = { level1: { level2: { level3: { level4: { value: "deep" } } } }, }; - const result = serializeValue(deep); + const result = safeStringify(deep); expect(result).toContain("level4: { value: 'deep' }"); }); }); From a6cefa25145fce664dbd643eb9263c2dd894b3c2 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 7 Oct 2025 11:36:39 +0300 Subject: [PATCH 23/45] v1.11.1 (#615) --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9127b22c..41b5e7ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,18 @@ ## Unreleased +## [v1.11.1](https://github.com/coder/vscode-coder/releases/tag/v1.11.1) 2025-10-07 + ### Fixed - Logging in or out in one VS Code window now properly updates the authentication status in all other open windows. - Fix an issue with JSON stringification errors occurring when logging circular objects. +- Fix resource cleanup issues that could leave lingering components after extension deactivation. ### Added - Support for `CODER_BINARY_DESTINATION` environment variable to set CLI download location (overridden by extension setting `coder.binaryDestination` if configured). +- Search filter button to Coder Workspaces tree views for easier workspace discovery. ## [v1.11.0](https://github.com/coder/vscode-coder/releases/tag/v1.11.0) 2025-09-24 diff --git a/package.json b/package.json index 438ef3c7..dd8dce12 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coder-remote", "displayName": "Coder", - "version": "1.11.0", + "version": "1.11.1", "description": "Open any workspace with a single click.", "categories": [ "Other" From 2cd05a38931e4c30a42503e3b24ec53425240ff8 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 7 Oct 2025 22:16:10 +0300 Subject: [PATCH 24/45] v1.11.2 (#616) --- .github/workflows/release.yaml | 2 +- CHANGELOG.md | 6 ++++++ README.md | 2 +- package.json | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a6bf5fa4..27214dcc 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -22,7 +22,7 @@ jobs: - run: yarn - - run: npx vsce package + - run: npx @vscode/vsce package - uses: "marvinpinto/action-automatic-releases@latest" with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 41b5e7ff..52a8801a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +## [v1.11.2](https://github.com/coder/vscode-coder/releases/tag/v1.11.2) 2025-10-07 + +### Changed + +- Updated Visual Studio Marketplace badge in README to use img.shields.io service instead of vsmarketplacebadges. + ## [v1.11.1](https://github.com/coder/vscode-coder/releases/tag/v1.11.1) 2025-10-07 ### Fixed diff --git a/README.md b/README.md index b6bd81dd..05c11d2e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Coder Remote -[![Visual Studio Marketplace](https://vsmarketplacebadges.dev/version/coder.coder-remote.svg)](https://marketplace.visualstudio.com/items?itemName=coder.coder-remote) +[![Visual Studio Marketplace](https://img.shields.io/visual-studio-marketplace/v/coder.coder-remote?label=Visual%20Studio%20Marketplace&color=%233fba11)](https://marketplace.visualstudio.com/items?itemName=coder.coder-remote) [![Open VSX Version](https://img.shields.io/open-vsx/v/coder/coder-remote)](https://open-vsx.org/extension/coder/coder-remote) [!["Join us on Discord"](https://badgen.net/discord/online-members/coder)](https://coder.com/chat?utm_source=github.com/coder/vscode-coder&utm_medium=github&utm_campaign=readme.md) diff --git a/package.json b/package.json index dd8dce12..9d2ea2a3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coder-remote", "displayName": "Coder", - "version": "1.11.1", + "version": "1.11.2", "description": "Open any workspace with a single click.", "categories": [ "Other" From a4018052e3c11fc97d1e98d4d3ab006399a8a1f1 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Mon, 13 Oct 2025 23:49:14 +0300 Subject: [PATCH 25/45] Fixed WebSocket connections not receiving headers from the configured header command (#619) Closes #618 --- CHANGELOG.md | 5 ++ src/{ => api}/agentMetadataHelper.ts | 10 ++-- src/api/coderApi.ts | 30 +++++++---- src/api/utils.ts | 18 +++++-- src/api/workspace.ts | 10 ++-- src/headers.ts | 25 +++++----- src/inbox.ts | 48 ++++++++++++------ src/remote/remote.ts | 39 +++++++-------- src/workspace/workspaceMonitor.ts | 74 ++++++++++++++++++---------- src/workspace/workspacesProvider.ts | 15 ++++-- 10 files changed, 167 insertions(+), 107 deletions(-) rename src/{ => api}/agentMetadataHelper.ts (91%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a8801a..ef80cd1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Fixed + +- Fixed WebSocket connections not receiving headers from the configured header command + (`coder.headerCommand`), which could cause authentication failures with remote workspaces. + ## [v1.11.2](https://github.com/coder/vscode-coder/releases/tag/v1.11.2) 2025-10-07 ### Changed diff --git a/src/agentMetadataHelper.ts b/src/api/agentMetadataHelper.ts similarity index 91% rename from src/agentMetadataHelper.ts rename to src/api/agentMetadataHelper.ts index 0a976411..4de804ad 100644 --- a/src/agentMetadataHelper.ts +++ b/src/api/agentMetadataHelper.ts @@ -5,8 +5,8 @@ import { type AgentMetadataEvent, AgentMetadataEventSchemaArray, errToStr, -} from "./api/api-helper"; -import { type CoderApi } from "./api/coderApi"; +} from "./api-helper"; +import { type CoderApi } from "./coderApi"; export type AgentMetadataWatcher = { onChange: vscode.EventEmitter["event"]; @@ -19,11 +19,11 @@ export type AgentMetadataWatcher = { * Opens a websocket connection to watch metadata for a given workspace agent. * Emits onChange when metadata updates or an error occurs. */ -export function createAgentMetadataWatcher( +export async function createAgentMetadataWatcher( agentId: WorkspaceAgent["id"], client: CoderApi, -): AgentMetadataWatcher { - const socket = client.watchAgentMetadata(agentId); +): Promise { + const socket = await client.watchAgentMetadata(agentId); let disposed = false; const onChange = new vscode.EventEmitter(); diff --git a/src/api/coderApi.ts b/src/api/coderApi.ts index 1d523b60..99976ff7 100644 --- a/src/api/coderApi.ts +++ b/src/api/coderApi.ts @@ -67,7 +67,7 @@ export class CoderApi extends Api { return client; } - watchInboxNotifications = ( + watchInboxNotifications = async ( watchTemplates: string[], watchTargets: string[], options?: ClientOptions, @@ -83,14 +83,14 @@ export class CoderApi extends Api { }); }; - watchWorkspace = (workspace: Workspace, options?: ClientOptions) => { + watchWorkspace = async (workspace: Workspace, options?: ClientOptions) => { return this.createWebSocket({ apiRoute: `/api/v2/workspaces/${workspace.id}/watch-ws`, options, }); }; - watchAgentMetadata = ( + watchAgentMetadata = async ( agentId: WorkspaceAgent["id"], options?: ClientOptions, ) => { @@ -100,21 +100,22 @@ export class CoderApi extends Api { }); }; - watchBuildLogsByBuildId = (buildId: string, logs: ProvisionerJobLog[]) => { + watchBuildLogsByBuildId = async ( + buildId: string, + logs: ProvisionerJobLog[], + ) => { const searchParams = new URLSearchParams({ follow: "true" }); if (logs.length) { searchParams.append("after", logs[logs.length - 1].id.toString()); } - const socket = this.createWebSocket({ + return this.createWebSocket({ apiRoute: `/api/v2/workspacebuilds/${buildId}/logs`, searchParams, }); - - return socket; }; - private createWebSocket( + private async createWebSocket( configs: Omit, ) { const baseUrlRaw = this.getAxiosInstance().defaults.baseURL; @@ -127,7 +128,15 @@ export class CoderApi extends Api { coderSessionTokenHeader ] as string | undefined; - const httpAgent = createHttpAgent(vscode.workspace.getConfiguration()); + const headers = await getHeaders( + baseUrlRaw, + getHeaderCommand(vscode.workspace.getConfiguration()), + this.output, + ); + + const httpAgent = await createHttpAgent( + vscode.workspace.getConfiguration(), + ); const webSocket = new OneWayWebSocket({ location: baseUrl, ...configs, @@ -137,6 +146,7 @@ export class CoderApi extends Api { headers: { ...(token ? { [coderSessionTokenHeader]: token } : {}), ...configs.options?.headers, + ...headers, }, ...configs.options, }, @@ -191,7 +201,7 @@ function setupInterceptors( // Configure proxy and TLS. // Note that by default VS Code overrides the agent. To prevent this, set // `http.proxySupport` to `on` or `off`. - const agent = createHttpAgent(vscode.workspace.getConfiguration()); + const agent = await createHttpAgent(vscode.workspace.getConfiguration()); config.httpsAgent = agent; config.httpAgent = agent; config.proxy = false; diff --git a/src/api/utils.ts b/src/api/utils.ts index 91a18885..0f13288e 100644 --- a/src/api/utils.ts +++ b/src/api/utils.ts @@ -1,4 +1,4 @@ -import fs from "fs"; +import fs from "fs/promises"; import { ProxyAgent } from "proxy-agent"; import { type WorkspaceConfiguration } from "vscode"; @@ -23,7 +23,9 @@ export function needToken(cfg: WorkspaceConfiguration): boolean { * Create a new HTTP agent based on the current VS Code settings. * Configures proxy, TLS certificates, and security options. */ -export function createHttpAgent(cfg: WorkspaceConfiguration): ProxyAgent { +export async function createHttpAgent( + cfg: WorkspaceConfiguration, +): Promise { const insecure = Boolean(cfg.get("coder.insecure")); const certFile = expandPath( String(cfg.get("coder.tlsCertFile") ?? "").trim(), @@ -32,6 +34,12 @@ export function createHttpAgent(cfg: WorkspaceConfiguration): ProxyAgent { const caFile = expandPath(String(cfg.get("coder.tlsCaFile") ?? "").trim()); const altHost = expandPath(String(cfg.get("coder.tlsAltHost") ?? "").trim()); + const [cert, key, ca] = await Promise.all([ + certFile === "" ? Promise.resolve(undefined) : fs.readFile(certFile), + keyFile === "" ? Promise.resolve(undefined) : fs.readFile(keyFile), + caFile === "" ? Promise.resolve(undefined) : fs.readFile(caFile), + ]); + return new ProxyAgent({ // Called each time a request is made. getProxyForUrl: (url: string) => { @@ -41,9 +49,9 @@ export function createHttpAgent(cfg: WorkspaceConfiguration): ProxyAgent { cfg.get("coder.proxyBypass"), ); }, - cert: certFile === "" ? undefined : fs.readFileSync(certFile), - key: keyFile === "" ? undefined : fs.readFileSync(keyFile), - ca: caFile === "" ? undefined : fs.readFileSync(caFile), + cert, + key, + ca, servername: altHost === "" ? undefined : altHost, // rejectUnauthorized defaults to true, so we need to explicitly set it to // false if we want to allow self-signed certificates. diff --git a/src/api/workspace.ts b/src/api/workspace.ts index c2e20c0c..cb03d9fc 100644 --- a/src/api/workspace.ts +++ b/src/api/workspace.ts @@ -95,12 +95,12 @@ export async function waitForBuild( const logs = await client.getWorkspaceBuildLogs(workspace.latest_build.id); logs.forEach((log) => writeEmitter.fire(log.output + "\r\n")); - await new Promise((resolve, reject) => { - const socket = client.watchBuildLogsByBuildId( - workspace.latest_build.id, - logs, - ); + const socket = await client.watchBuildLogsByBuildId( + workspace.latest_build.id, + logs, + ); + await new Promise((resolve, reject) => { socket.addEventListener("message", (data) => { if (data.parseError) { writeEmitter.fire( diff --git a/src/headers.ts b/src/headers.ts index f5f45301..6c69258c 100644 --- a/src/headers.ts +++ b/src/headers.ts @@ -24,7 +24,7 @@ export function getHeaderCommand( config.get("coder.headerCommand")?.trim() || process.env.CODER_HEADER_COMMAND?.trim(); - return cmd ? cmd : undefined; + return cmd || undefined; } export function getHeaderArgs(config: WorkspaceConfiguration): string[] { @@ -44,16 +44,13 @@ export function getHeaderArgs(config: WorkspaceConfiguration): string[] { return ["--header-command", escapeSubcommand(command)]; } -// TODO: getHeaders might make more sense to directly implement on Storage -// but it is difficult to test Storage right now since we use vitest instead of -// the standard extension testing framework which would give us access to vscode -// APIs. We should revert the testing framework then consider moving this. - -// getHeaders executes the header command and parses the headers from stdout. -// Both stdout and stderr are logged on error but stderr is otherwise ignored. -// Throws an error if the process exits with non-zero or the JSON is invalid. -// Returns undefined if there is no header command set. No effort is made to -// validate the JSON other than making sure it can be parsed. +/** + * getHeaders executes the header command and parses the headers from stdout. + * Both stdout and stderr are logged on error but stderr is otherwise ignored. + * Throws an error if the process exits with non-zero or the JSON is invalid. + * Returns undefined if there is no header command set. No effort is made to + * validate the JSON other than making sure it can be parsed. + */ export async function getHeaders( url: string | undefined, command: string | undefined, @@ -90,8 +87,8 @@ export async function getHeaders( return headers; } const lines = result.stdout.replace(/\r?\n$/, "").split(/\r?\n/); - for (let i = 0; i < lines.length; ++i) { - const [key, value] = lines[i].split(/=(.*)/); + for (const line of lines) { + const [key, value] = line.split(/=(.*)/); // Header names cannot be blank or contain whitespace and the Coder CLI // requires that there be an equals sign (the value can be blank though). if ( @@ -100,7 +97,7 @@ export async function getHeaders( typeof value === "undefined" ) { throw new Error( - `Malformed line from header command: [${lines[i]}] (out: ${result.stdout})`, + `Malformed line from header command: [${line}] (out: ${result.stdout})`, ); } headers[key] = value; diff --git a/src/inbox.ts b/src/inbox.ts index 61a780bb..8dff573f 100644 --- a/src/inbox.ts +++ b/src/inbox.ts @@ -16,12 +16,21 @@ const TEMPLATE_WORKSPACE_OUT_OF_MEMORY = "a9d027b4-ac49-4fb1-9f6d-45af15f64e7a"; const TEMPLATE_WORKSPACE_OUT_OF_DISK = "f047f6a3-5713-40f7-85aa-0394cce9fa3a"; export class Inbox implements vscode.Disposable { - readonly #logger: Logger; - #disposed = false; - #socket: OneWayWebSocket; + private socket: OneWayWebSocket | undefined; + private disposed = false; - constructor(workspace: Workspace, client: CoderApi, logger: Logger) { - this.#logger = logger; + private constructor(private readonly logger: Logger) {} + + /** + * Factory method to create and initialize an Inbox. + * Use this instead of the constructor to properly handle async websocket initialization. + */ + static async create( + workspace: Workspace, + client: CoderApi, + logger: Logger, + ): Promise { + const inbox = new Inbox(logger); const watchTemplates = [ TEMPLATE_WORKSPACE_OUT_OF_DISK, @@ -30,33 +39,40 @@ export class Inbox implements vscode.Disposable { const watchTargets = [workspace.id]; - this.#socket = client.watchInboxNotifications(watchTemplates, watchTargets); + const socket = await client.watchInboxNotifications( + watchTemplates, + watchTargets, + ); - this.#socket.addEventListener("open", () => { - this.#logger.info("Listening to Coder Inbox"); + socket.addEventListener("open", () => { + logger.info("Listening to Coder Inbox"); }); - this.#socket.addEventListener("error", () => { + socket.addEventListener("error", () => { // Errors are already logged internally - this.dispose(); + inbox.dispose(); }); - this.#socket.addEventListener("message", (data) => { + socket.addEventListener("message", (data) => { if (data.parseError) { - this.#logger.error("Failed to parse inbox message", data.parseError); + logger.error("Failed to parse inbox message", data.parseError); } else { vscode.window.showInformationMessage( data.parsedMessage.notification.title, ); } }); + + inbox.socket = socket; + + return inbox; } dispose() { - if (!this.#disposed) { - this.#logger.info("No longer listening to Coder Inbox"); - this.#socket.close(); - this.#disposed = true; + if (!this.disposed) { + this.logger.info("No longer listening to Coder Inbox"); + this.socket?.close(); + this.disposed = true; } } } diff --git a/src/remote/remote.ts b/src/remote/remote.ts index 832a8086..97cb858e 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -18,7 +18,7 @@ import { getEventValue, formatEventLabel, formatMetadataError, -} from "../agentMetadataHelper"; +} from "../api/agentMetadataHelper"; import { createWorkspaceIdentifier, extractAgents } from "../api/api-helper"; import { CoderApi } from "../api/coderApi"; import { needToken } from "../api/utils"; @@ -135,9 +135,7 @@ export class Remote { let attempts = 0; function initWriteEmitterAndTerminal(): vscode.EventEmitter { - if (!writeEmitter) { - writeEmitter = new vscode.EventEmitter(); - } + writeEmitter ??= new vscode.EventEmitter(); if (!terminal) { terminal = vscode.window.createTerminal({ name: "Build Log", @@ -295,16 +293,14 @@ export class Remote { if (result.type === "login") { return this.setup(remoteAuthority, firstConnect); + } else if (!result.userChoice) { + // User declined to log in. + await this.closeRemote(); + return; } else { - if (!result.userChoice) { - // User declined to log in. - await this.closeRemote(); - return; - } else { - // Log in then try again. - await this.commands.login({ url: baseUrlRaw, label: parts.label }); - return this.setup(remoteAuthority, firstConnect); - } + // Log in then try again. + await this.commands.login({ url: baseUrlRaw, label: parts.label }); + return this.setup(remoteAuthority, firstConnect); } }; @@ -543,7 +539,7 @@ export class Remote { } // Watch the workspace for changes. - const monitor = new WorkspaceMonitor( + const monitor = await WorkspaceMonitor.create( workspace, workspaceClient, this.logger, @@ -556,7 +552,7 @@ export class Remote { ); // Watch coder inbox for messages - const inbox = new Inbox(workspace, workspaceClient, this.logger); + const inbox = await Inbox.create(workspace, workspaceClient, this.logger); disposables.push(inbox); // Wait for the agent to connect. @@ -668,7 +664,7 @@ export class Remote { agent.name, ); }), - ...this.createAgentMetadataStatusBar(agent, workspaceClient), + ...(await this.createAgentMetadataStatusBar(agent, workspaceClient)), ); } catch (ex) { // Whatever error happens, make sure we clean up the disposables in case of failure @@ -858,8 +854,7 @@ export class Remote { "UserKnownHostsFile", "StrictHostKeyChecking", ]; - for (let i = 0; i < keysToMatch.length; i++) { - const key = keysToMatch[i]; + for (const key of keysToMatch) { if (computedProperties[key] === sshValues[key]) { continue; } @@ -1005,7 +1000,7 @@ export class Remote { // this to find the SSH process that is powering this connection. That SSH // process will be logging network information periodically to a file. const text = await fs.readFile(logPath, "utf8"); - const port = await findPort(text); + const port = findPort(text); if (!port) { return; } @@ -1064,16 +1059,16 @@ export class Remote { * The status bar item updates dynamically based on changes to the agent's metadata, * and hides itself if no metadata is available or an error occurs. */ - private createAgentMetadataStatusBar( + private async createAgentMetadataStatusBar( agent: WorkspaceAgent, client: CoderApi, - ): vscode.Disposable[] { + ): Promise { const statusBarItem = vscode.window.createStatusBarItem( "agentMetadata", vscode.StatusBarAlignment.Left, ); - const agentWatcher = createAgentMetadataWatcher(agent.id, client); + const agentWatcher = await createAgentMetadataWatcher(agent.id, client); const onChangeDisposable = agentWatcher.onChange(() => { if (agentWatcher.error) { diff --git a/src/workspace/workspaceMonitor.ts b/src/workspace/workspaceMonitor.ts index 0b154f75..a761249a 100644 --- a/src/workspace/workspaceMonitor.ts +++ b/src/workspace/workspaceMonitor.ts @@ -17,12 +17,12 @@ import { type OneWayWebSocket } from "../websocket/oneWayWebSocket"; * workspace status is also shown in the status bar menu. */ export class WorkspaceMonitor implements vscode.Disposable { - private socket: OneWayWebSocket; + private socket: OneWayWebSocket | undefined; private disposed = false; // How soon in advance to notify about autostop and deletion. - private autostopNotifyTime = 1000 * 60 * 30; // 30 minutes. - private deletionNotifyTime = 1000 * 60 * 60 * 24; // 24 hours. + private readonly autostopNotifyTime = 1000 * 60 * 30; // 30 minutes. + private readonly deletionNotifyTime = 1000 * 60 * 60 * 24; // 24 hours. // Only notify once. private notifiedAutostop = false; @@ -36,7 +36,7 @@ export class WorkspaceMonitor implements vscode.Disposable { // For logging. private readonly name: string; - constructor( + private constructor( workspace: Workspace, private readonly client: CoderApi, private readonly logger: Logger, @@ -45,43 +45,67 @@ export class WorkspaceMonitor implements vscode.Disposable { private readonly contextManager: ContextManager, ) { this.name = createWorkspaceIdentifier(workspace); - const socket = this.client.watchWorkspace(workspace); + + const statusBarItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 999, + ); + statusBarItem.name = "Coder Workspace Update"; + statusBarItem.text = "$(fold-up) Update Workspace"; + statusBarItem.command = "coder.workspace.update"; + + // Store so we can update when the workspace data updates. + this.statusBarItem = statusBarItem; + + this.update(workspace); // Set initial state. + } + + /** + * Factory method to create and initialize a WorkspaceMonitor. + * Use this instead of the constructor to properly handle async websocket initialization. + */ + static async create( + workspace: Workspace, + client: CoderApi, + logger: Logger, + vscodeProposed: typeof vscode, + contextManager: ContextManager, + ): Promise { + const monitor = new WorkspaceMonitor( + workspace, + client, + logger, + vscodeProposed, + contextManager, + ); + + // Initialize websocket connection + const socket = await client.watchWorkspace(workspace); socket.addEventListener("open", () => { - this.logger.info(`Monitoring ${this.name}...`); + logger.info(`Monitoring ${monitor.name}...`); }); socket.addEventListener("message", (event) => { try { if (event.parseError) { - this.notifyError(event.parseError); + monitor.notifyError(event.parseError); return; } // Perhaps we need to parse this and validate it. const newWorkspaceData = event.parsedMessage.data as Workspace; - this.update(newWorkspaceData); - this.maybeNotify(newWorkspaceData); - this.onChange.fire(newWorkspaceData); + monitor.update(newWorkspaceData); + monitor.maybeNotify(newWorkspaceData); + monitor.onChange.fire(newWorkspaceData); } catch (error) { - this.notifyError(error); + monitor.notifyError(error); } }); // Store so we can close in dispose(). - this.socket = socket; - - const statusBarItem = vscode.window.createStatusBarItem( - vscode.StatusBarAlignment.Left, - 999, - ); - statusBarItem.name = "Coder Workspace Update"; - statusBarItem.text = "$(fold-up) Update Workspace"; - statusBarItem.command = "coder.workspace.update"; + monitor.socket = socket; - // Store so we can update when the workspace data updates. - this.statusBarItem = statusBarItem; - - this.update(workspace); // Set initial state. + return monitor; } /** @@ -91,7 +115,7 @@ export class WorkspaceMonitor implements vscode.Disposable { if (!this.disposed) { this.logger.info(`Unmonitoring ${this.name}...`); this.statusBarItem.dispose(); - this.socket.close(); + this.socket?.close(); this.disposed = true; } } diff --git a/src/workspace/workspacesProvider.ts b/src/workspace/workspacesProvider.ts index b83e4f84..2dffec13 100644 --- a/src/workspace/workspacesProvider.ts +++ b/src/workspace/workspacesProvider.ts @@ -11,7 +11,7 @@ import { createAgentMetadataWatcher, formatEventLabel, formatMetadataError, -} from "../agentMetadataHelper"; +} from "../api/agentMetadataHelper"; import { type AgentMetadataEvent, extractAgents, @@ -38,8 +38,10 @@ export class WorkspaceProvider { // Undefined if we have never fetched workspaces before. private workspaces: WorkspaceTreeItem[] | undefined; - private agentWatchers: Map = - new Map(); + private readonly agentWatchers: Map< + WorkspaceAgent["id"], + AgentMetadataWatcher + > = new Map(); private timeout: NodeJS.Timeout | undefined; private fetching = false; private visible = false; @@ -130,14 +132,17 @@ export class WorkspaceProvider const showMetadata = this.getWorkspacesQuery === WorkspaceQuery.Mine; if (showMetadata) { const agents = extractAllAgents(resp.workspaces); - agents.forEach((agent) => { + agents.forEach(async (agent) => { // If we have an existing watcher, re-use it. const oldWatcher = this.agentWatchers.get(agent.id); if (oldWatcher) { reusedWatcherIds.push(agent.id); } else { // Otherwise create a new watcher. - const watcher = createAgentMetadataWatcher(agent.id, this.client); + const watcher = await createAgentMetadataWatcher( + agent.id, + this.client, + ); watcher.onChange(() => this.refresh()); this.agentWatchers.set(agent.id, watcher); } From 5165adeaccfd069069f0532ee44e7f5b3fb69d26 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 14 Oct 2025 11:23:43 +0300 Subject: [PATCH 26/45] Fix unit tests on windows (#617) Closes #604 --- package.json | 2 +- test/fixtures/{ => scripts}/bin.bash | 0 test/fixtures/{ => scripts}/bin.old.bash | 0 test/unit/core/cliManager.test.ts | 34 ++-- test/unit/core/cliUtils.test.ts | 14 +- test/unit/core/pathResolver.test.ts | 28 ++- test/unit/globalFlags.test.ts | 13 +- test/unit/headers.test.ts | 243 ++++++++++++----------- test/utils/platform.test.ts | 86 ++++++++ test/utils/platform.ts | 46 +++++ vitest.config.ts | 9 +- 11 files changed, 315 insertions(+), 160 deletions(-) rename test/fixtures/{ => scripts}/bin.bash (100%) rename test/fixtures/{ => scripts}/bin.old.bash (100%) create mode 100644 test/utils/platform.test.ts create mode 100644 test/utils/platform.ts diff --git a/package.json b/package.json index 9d2ea2a3..02a6ddc3 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "lint:fix": "yarn lint --fix", "package": "webpack --mode production --devtool hidden-source-map", "package:prerelease": "npx vsce package --pre-release", - "pretest": "tsc -p . --outDir out && yarn run build && yarn run lint", + "pretest": "tsc -p . --outDir out && tsc -p test --outDir out && yarn run build && yarn run lint", "test": "vitest", "test:ci": "CI=true yarn test", "test:integration": "vscode-test", diff --git a/test/fixtures/bin.bash b/test/fixtures/scripts/bin.bash similarity index 100% rename from test/fixtures/bin.bash rename to test/fixtures/scripts/bin.bash diff --git a/test/fixtures/bin.old.bash b/test/fixtures/scripts/bin.old.bash similarity index 100% rename from test/fixtures/bin.old.bash rename to test/fixtures/scripts/bin.old.bash diff --git a/test/unit/core/cliManager.test.ts b/test/unit/core/cliManager.test.ts index 3e1dfb0d..f2a2c2e5 100644 --- a/test/unit/core/cliManager.test.ts +++ b/test/unit/core/cliManager.test.ts @@ -20,6 +20,7 @@ import { MockProgressReporter, MockUserInteraction, } from "../../mocks/testHelpers"; +import { expectPathsEqual } from "../../utils/platform"; vi.mock("os"); vi.mock("axios"); @@ -213,7 +214,7 @@ describe("CliManager", () => { it("accepts valid semver versions", async () => { withExistingBinary(TEST_VERSION); const result = await manager.fetchBinary(mockApi, "test"); - expect(result).toBe(BINARY_PATH); + expectPathsEqual(result, BINARY_PATH); }); }); @@ -226,7 +227,7 @@ describe("CliManager", () => { it("reuses matching binary without downloading", async () => { withExistingBinary(TEST_VERSION); const result = await manager.fetchBinary(mockApi, "test"); - expect(result).toBe(BINARY_PATH); + expectPathsEqual(result, BINARY_PATH); expect(mockAxios.get).not.toHaveBeenCalled(); // Verify binary still exists expect(memfs.existsSync(BINARY_PATH)).toBe(true); @@ -236,7 +237,7 @@ describe("CliManager", () => { withExistingBinary("1.0.0"); withSuccessfulDownload(); const result = await manager.fetchBinary(mockApi, "test"); - expect(result).toBe(BINARY_PATH); + expectPathsEqual(result, BINARY_PATH); expect(mockAxios.get).toHaveBeenCalled(); // Verify new binary exists expect(memfs.existsSync(BINARY_PATH)).toBe(true); @@ -249,7 +250,7 @@ describe("CliManager", () => { mockConfig.set("coder.enableDownloads", false); withExistingBinary("1.0.0"); const result = await manager.fetchBinary(mockApi, "test"); - expect(result).toBe(BINARY_PATH); + expectPathsEqual(result, BINARY_PATH); expect(mockAxios.get).not.toHaveBeenCalled(); // Should still have the old version expect(memfs.existsSync(BINARY_PATH)).toBe(true); @@ -262,7 +263,7 @@ describe("CliManager", () => { withCorruptedBinary(); withSuccessfulDownload(); const result = await manager.fetchBinary(mockApi, "test"); - expect(result).toBe(BINARY_PATH); + expectPathsEqual(result, BINARY_PATH); expect(mockAxios.get).toHaveBeenCalled(); expect(memfs.existsSync(BINARY_PATH)).toBe(true); expect(memfs.readFileSync(BINARY_PATH).toString()).toBe( @@ -276,7 +277,7 @@ describe("CliManager", () => { withSuccessfulDownload(); const result = await manager.fetchBinary(mockApi, "test"); - expect(result).toBe(BINARY_PATH); + expectPathsEqual(result, BINARY_PATH); expect(mockAxios.get).toHaveBeenCalled(); // Verify directory was created and binary exists @@ -392,7 +393,7 @@ describe("CliManager", () => { withExistingBinary("1.0.0"); withHttpResponse(304); const result = await manager.fetchBinary(mockApi, "test"); - expect(result).toBe(BINARY_PATH); + expectPathsEqual(result, BINARY_PATH); // No change expect(memfs.readFileSync(BINARY_PATH).toString()).toBe( mockBinaryContent("1.0.0"), @@ -460,7 +461,7 @@ describe("CliManager", () => { it("handles missing content-length", async () => { withSuccessfulDownload({ headers: {} }); const result = await manager.fetchBinary(mockApi, "test"); - expect(result).toBe(BINARY_PATH); + expectPathsEqual(result, BINARY_PATH); expect(memfs.existsSync(BINARY_PATH)).toBe(true); }); }); @@ -494,7 +495,7 @@ describe("CliManager", () => { withSuccessfulDownload(); withSignatureResponses([200]); const result = await manager.fetchBinary(mockApi, "test"); - expect(result).toBe(BINARY_PATH); + expectPathsEqual(result, BINARY_PATH); expect(pgp.verifySignature).toHaveBeenCalled(); const sigFile = expectFileInDir(BINARY_DIR, ".asc"); expect(sigFile).toBeDefined(); @@ -505,7 +506,7 @@ describe("CliManager", () => { withSignatureResponses([404, 200]); mockUI.setResponse("Signature not found", "Download signature"); const result = await manager.fetchBinary(mockApi, "test"); - expect(result).toBe(BINARY_PATH); + expectPathsEqual(result, BINARY_PATH); expect(mockAxios.get).toHaveBeenCalledTimes(3); const sigFile = expectFileInDir(BINARY_DIR, ".asc"); expect(sigFile).toBeDefined(); @@ -519,7 +520,7 @@ describe("CliManager", () => { ); mockUI.setResponse("Signature does not match", "Run anyway"); const result = await manager.fetchBinary(mockApi, "test"); - expect(result).toBe(BINARY_PATH); + expectPathsEqual(result, BINARY_PATH); expect(memfs.existsSync(BINARY_PATH)).toBe(true); }); @@ -539,7 +540,7 @@ describe("CliManager", () => { mockConfig.set("coder.disableSignatureVerification", true); withSuccessfulDownload(); const result = await manager.fetchBinary(mockApi, "test"); - expect(result).toBe(BINARY_PATH); + expectPathsEqual(result, BINARY_PATH); expect(pgp.verifySignature).not.toHaveBeenCalled(); const files = readdir(BINARY_DIR); expect(files.find((file) => file.includes(".asc"))).toBeUndefined(); @@ -553,7 +554,7 @@ describe("CliManager", () => { withHttpResponse(status); mockUI.setResponse(message, "Run without verification"); const result = await manager.fetchBinary(mockApi, "test"); - expect(result).toBe(BINARY_PATH); + expectPathsEqual(result, BINARY_PATH); expect(pgp.verifySignature).not.toHaveBeenCalled(); }); @@ -615,13 +616,16 @@ describe("CliManager", () => { withSuccessfulDownload(); const result = await manager.fetchBinary(mockApi, "test label"); - expect(result).toBe(`${pathWithSpaces}/test label/bin/${BINARY_NAME}`); + expectPathsEqual( + result, + `${pathWithSpaces}/test label/bin/${BINARY_NAME}`, + ); }); it("handles empty deployment label", async () => { withExistingBinary(TEST_VERSION, "/path/base/bin"); const result = await manager.fetchBinary(mockApi, ""); - expect(result).toBe(path.join(BASE_PATH, "bin", BINARY_NAME)); + expectPathsEqual(result, path.join(BASE_PATH, "bin", BINARY_NAME)); }); }); diff --git a/test/unit/core/cliUtils.test.ts b/test/unit/core/cliUtils.test.ts index d63ddd87..dd1c56f0 100644 --- a/test/unit/core/cliUtils.test.ts +++ b/test/unit/core/cliUtils.test.ts @@ -6,6 +6,7 @@ import { beforeAll, describe, expect, it } from "vitest"; import * as cliUtils from "@/core/cliUtils"; import { getFixturePath } from "../../utils/fixtures"; +import { isWindows } from "../../utils/platform"; describe("CliUtils", () => { const tmp = path.join(os.tmpdir(), "vscode-coder-tests"); @@ -28,12 +29,14 @@ describe("CliUtils", () => { expect((await cliUtils.stat(binPath))?.size).toBe(4); }); - // TODO: CI only runs on Linux but we should run it on Windows too. - it("version", async () => { + it.skipIf(isWindows())("version", async () => { const binPath = path.join(tmp, "version"); await expect(cliUtils.version(binPath)).rejects.toThrow("ENOENT"); - const binTmpl = await fs.readFile(getFixturePath("bin.bash"), "utf8"); + const binTmpl = await fs.readFile( + getFixturePath("scripts", "bin.bash"), + "utf8", + ); await fs.writeFile(binPath, binTmpl.replace("$ECHO", "hello")); await expect(cliUtils.version(binPath)).rejects.toThrow("EACCES"); @@ -56,7 +59,10 @@ describe("CliUtils", () => { ); expect(await cliUtils.version(binPath)).toBe("v0.0.0"); - const oldTmpl = await fs.readFile(getFixturePath("bin.old.bash"), "utf8"); + const oldTmpl = await fs.readFile( + getFixturePath("scripts", "bin.old.bash"), + "utf8", + ); const old = (stderr: string, stdout: string): string => { return oldTmpl.replace("$STDERR", stderr).replace("$STDOUT", stdout); }; diff --git a/test/unit/core/pathResolver.test.ts b/test/unit/core/pathResolver.test.ts index e0e3b4d6..2930fb7e 100644 --- a/test/unit/core/pathResolver.test.ts +++ b/test/unit/core/pathResolver.test.ts @@ -1,9 +1,10 @@ import * as path from "path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, it, vi } from "vitest"; import { PathResolver } from "@/core/pathResolver"; import { MockConfigurationProvider } from "../../mocks/testHelpers"; +import { expectPathsEqual } from "../../utils/platform"; describe("PathResolver", () => { const basePath = @@ -19,17 +20,19 @@ describe("PathResolver", () => { }); it("should use base path for empty labels", () => { - expect(pathResolver.getGlobalConfigDir("")).toBe(basePath); - expect(pathResolver.getSessionTokenPath("")).toBe( + expectPathsEqual(pathResolver.getGlobalConfigDir(""), basePath); + expectPathsEqual( + pathResolver.getSessionTokenPath(""), path.join(basePath, "session"), ); - expect(pathResolver.getUrlPath("")).toBe(path.join(basePath, "url")); + expectPathsEqual(pathResolver.getUrlPath(""), path.join(basePath, "url")); }); describe("getBinaryCachePath", () => { it("should use custom binary destination when configured", () => { mockConfig.set("coder.binaryDestination", "/custom/binary/path"); - expect(pathResolver.getBinaryCachePath("deployment")).toBe( + expectPathsEqual( + pathResolver.getBinaryCachePath("deployment"), "/custom/binary/path", ); }); @@ -37,14 +40,16 @@ describe("PathResolver", () => { it("should use default path when custom destination is empty or whitespace", () => { vi.stubEnv("CODER_BINARY_DESTINATION", " "); mockConfig.set("coder.binaryDestination", " "); - expect(pathResolver.getBinaryCachePath("deployment")).toBe( + expectPathsEqual( + pathResolver.getBinaryCachePath("deployment"), path.join(basePath, "deployment", "bin"), ); }); it("should normalize custom paths", () => { mockConfig.set("coder.binaryDestination", "/custom/../binary/./path"); - expect(pathResolver.getBinaryCachePath("deployment")).toBe( + expectPathsEqual( + pathResolver.getBinaryCachePath("deployment"), "/binary/path", ); }); @@ -53,19 +58,22 @@ describe("PathResolver", () => { // Use the global storage when the environment variable and setting are unset/blank vi.stubEnv("CODER_BINARY_DESTINATION", ""); mockConfig.set("coder.binaryDestination", ""); - expect(pathResolver.getBinaryCachePath("deployment")).toBe( + expectPathsEqual( + pathResolver.getBinaryCachePath("deployment"), path.join(basePath, "deployment", "bin"), ); // Test environment variable takes precedence over global storage vi.stubEnv("CODER_BINARY_DESTINATION", " /env/binary/path "); - expect(pathResolver.getBinaryCachePath("deployment")).toBe( + expectPathsEqual( + pathResolver.getBinaryCachePath("deployment"), "/env/binary/path", ); // Test setting takes precedence over environment variable mockConfig.set("coder.binaryDestination", " /setting/path "); - expect(pathResolver.getBinaryCachePath("deployment")).toBe( + expectPathsEqual( + pathResolver.getBinaryCachePath("deployment"), "/setting/path", ); }); diff --git a/test/unit/globalFlags.test.ts b/test/unit/globalFlags.test.ts index d570d609..94c89dba 100644 --- a/test/unit/globalFlags.test.ts +++ b/test/unit/globalFlags.test.ts @@ -3,6 +3,8 @@ import { type WorkspaceConfiguration } from "vscode"; import { getGlobalFlags } from "@/globalFlags"; +import { isWindows } from "../utils/platform"; + describe("Global flags suite", () => { it("should return global-config and header args when no global flags configured", () => { const config = { @@ -53,10 +55,11 @@ describe("Global flags suite", () => { }); it("should not filter header-command flags, header args appended at end", () => { + const headerCommand = "echo test"; const config = { get: (key: string) => { if (key === "coder.headerCommand") { - return "echo test"; + return headerCommand; } if (key === "coder.globalFlags") { return ["-v", "--header-command custom", "--no-feature-warning"]; @@ -73,7 +76,13 @@ describe("Global flags suite", () => { "--global-config", '"/config/dir"', "--header-command", - "'echo test'", + quoteCommand(headerCommand), ]); }); }); + +function quoteCommand(value: string): string { + // Used to escape environment variables in commands. See `getHeaderArgs` in src/headers.ts + const quote = isWindows() ? '"' : "'"; + return `${quote}${value}${quote}`; +} diff --git a/test/unit/headers.test.ts b/test/unit/headers.test.ts index b2c29e22..f5812ec1 100644 --- a/test/unit/headers.test.ts +++ b/test/unit/headers.test.ts @@ -1,10 +1,11 @@ -import * as os from "os"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { type WorkspaceConfiguration } from "vscode"; import { getHeaderCommand, getHeaders } from "@/headers"; import { type Logger } from "@/logging/logger"; +import { printCommand, exitCommand, printEnvCommand } from "../utils/platform"; + const logger: Logger = { trace: () => {}, debug: () => {}, @@ -13,142 +14,142 @@ const logger: Logger = { error: () => {}, }; -it("should return no headers", async () => { - await expect(getHeaders(undefined, undefined, logger)).resolves.toStrictEqual( - {}, - ); - await expect( - getHeaders("localhost", undefined, logger), - ).resolves.toStrictEqual({}); - await expect(getHeaders(undefined, "command", logger)).resolves.toStrictEqual( - {}, - ); - await expect(getHeaders("localhost", "", logger)).resolves.toStrictEqual({}); - await expect(getHeaders("", "command", logger)).resolves.toStrictEqual({}); - await expect(getHeaders("localhost", " ", logger)).resolves.toStrictEqual( - {}, - ); - await expect(getHeaders(" ", "command", logger)).resolves.toStrictEqual({}); - await expect( - getHeaders("localhost", "printf ''", logger), - ).resolves.toStrictEqual({}); -}); - -it("should return headers", async () => { - await expect( - getHeaders("localhost", "printf 'foo=bar\\nbaz=qux'", logger), - ).resolves.toStrictEqual({ - foo: "bar", - baz: "qux", - }); - await expect( - getHeaders("localhost", "printf 'foo=bar\\r\\nbaz=qux'", logger), - ).resolves.toStrictEqual({ - foo: "bar", - baz: "qux", +describe("Headers", () => { + it("should return no headers", async () => { + await expect( + getHeaders(undefined, undefined, logger), + ).resolves.toStrictEqual({}); + await expect( + getHeaders("localhost", undefined, logger), + ).resolves.toStrictEqual({}); + await expect( + getHeaders(undefined, "command", logger), + ).resolves.toStrictEqual({}); + await expect(getHeaders("localhost", "", logger)).resolves.toStrictEqual( + {}, + ); + await expect(getHeaders("", "command", logger)).resolves.toStrictEqual({}); + await expect(getHeaders("localhost", " ", logger)).resolves.toStrictEqual( + {}, + ); + await expect(getHeaders(" ", "command", logger)).resolves.toStrictEqual( + {}, + ); + await expect( + getHeaders("localhost", printCommand(""), logger), + ).resolves.toStrictEqual({}); }); - await expect( - getHeaders("localhost", "printf 'foo=bar\\r\\n'", logger), - ).resolves.toStrictEqual({ foo: "bar" }); - await expect( - getHeaders("localhost", "printf 'foo=bar'", logger), - ).resolves.toStrictEqual({ foo: "bar" }); - await expect( - getHeaders("localhost", "printf 'foo=bar='", logger), - ).resolves.toStrictEqual({ foo: "bar=" }); - await expect( - getHeaders("localhost", "printf 'foo=bar=baz'", logger), - ).resolves.toStrictEqual({ foo: "bar=baz" }); - await expect( - getHeaders("localhost", "printf 'foo='", logger), - ).resolves.toStrictEqual({ foo: "" }); -}); - -it("should error on malformed or empty lines", async () => { - await expect( - getHeaders("localhost", "printf 'foo=bar\\r\\n\\r\\n'", logger), - ).rejects.toThrow(/Malformed/); - await expect( - getHeaders("localhost", "printf '\\r\\nfoo=bar'", logger), - ).rejects.toThrow(/Malformed/); - await expect( - getHeaders("localhost", "printf '=foo'", logger), - ).rejects.toThrow(/Malformed/); - await expect(getHeaders("localhost", "printf 'foo'", logger)).rejects.toThrow( - /Malformed/, - ); - await expect( - getHeaders("localhost", "printf ' =foo'", logger), - ).rejects.toThrow(/Malformed/); - await expect( - getHeaders("localhost", "printf 'foo =bar'", logger), - ).rejects.toThrow(/Malformed/); - await expect( - getHeaders("localhost", "printf 'foo foo=bar'", logger), - ).rejects.toThrow(/Malformed/); -}); -it("should have access to environment variables", async () => { - const coderUrl = "dev.coder.com"; - await expect( - getHeaders( - coderUrl, - os.platform() === "win32" - ? "printf url=%CODER_URL%" - : "printf url=$CODER_URL", - logger, - ), - ).resolves.toStrictEqual({ url: coderUrl }); -}); + it("should return headers", async () => { + await expect( + getHeaders("localhost", printCommand("foo=bar\nbaz=qux"), logger), + ).resolves.toStrictEqual({ + foo: "bar", + baz: "qux", + }); + await expect( + getHeaders("localhost", printCommand("foo=bar\r\nbaz=qux"), logger), + ).resolves.toStrictEqual({ + foo: "bar", + baz: "qux", + }); + await expect( + getHeaders("localhost", printCommand("foo=bar\r\n"), logger), + ).resolves.toStrictEqual({ foo: "bar" }); + await expect( + getHeaders("localhost", printCommand("foo=bar"), logger), + ).resolves.toStrictEqual({ foo: "bar" }); + await expect( + getHeaders("localhost", printCommand("foo=bar="), logger), + ).resolves.toStrictEqual({ foo: "bar=" }); + await expect( + getHeaders("localhost", printCommand("foo=bar=baz"), logger), + ).resolves.toStrictEqual({ foo: "bar=baz" }); + await expect( + getHeaders("localhost", printCommand("foo="), logger), + ).resolves.toStrictEqual({ foo: "" }); + }); -it("should error on non-zero exit", async () => { - await expect(getHeaders("localhost", "exit 10", logger)).rejects.toThrow( - /exited unexpectedly with code 10/, - ); -}); + it("should error on malformed or empty lines", async () => { + await expect( + getHeaders("localhost", printCommand("foo=bar\r\n\r\n"), logger), + ).rejects.toThrow(/Malformed/); + await expect( + getHeaders("localhost", printCommand("\r\nfoo=bar"), logger), + ).rejects.toThrow(/Malformed/); + await expect( + getHeaders("localhost", printCommand("=foo"), logger), + ).rejects.toThrow(/Malformed/); + await expect( + getHeaders("localhost", printCommand("foo"), logger), + ).rejects.toThrow(/Malformed/); + await expect( + getHeaders("localhost", printCommand(" =foo"), logger), + ).rejects.toThrow(/Malformed/); + await expect( + getHeaders("localhost", printCommand("foo =bar"), logger), + ).rejects.toThrow(/Malformed/); + await expect( + getHeaders("localhost", printCommand("foo foo=bar"), logger), + ).rejects.toThrow(/Malformed/); + }); -describe("getHeaderCommand", () => { - beforeEach(() => { - vi.stubEnv("CODER_HEADER_COMMAND", ""); + it("should have access to environment variables", async () => { + const coderUrl = "dev.coder.com"; + await expect( + getHeaders(coderUrl, printEnvCommand("url", "CODER_URL"), logger), + ).resolves.toStrictEqual({ url: coderUrl }); }); - afterEach(() => { - vi.unstubAllEnvs(); + it("should error on non-zero exit", async () => { + await expect( + getHeaders("localhost", exitCommand(10), logger), + ).rejects.toThrow(/exited unexpectedly with code 10/); }); - it("should return undefined if coder.headerCommand is not set in config", () => { - const config = { - get: () => undefined, - } as unknown as WorkspaceConfiguration; + describe("getHeaderCommand", () => { + beforeEach(() => { + vi.stubEnv("CODER_HEADER_COMMAND", ""); + }); - expect(getHeaderCommand(config)).toBeUndefined(); - }); + afterEach(() => { + vi.unstubAllEnvs(); + }); - it("should return undefined if coder.headerCommand is a blank string", () => { - const config = { - get: () => " ", - } as unknown as WorkspaceConfiguration; + it("should return undefined if coder.headerCommand is not set in config", () => { + const config = { + get: () => undefined, + } as unknown as WorkspaceConfiguration; - expect(getHeaderCommand(config)).toBeUndefined(); - }); + expect(getHeaderCommand(config)).toBeUndefined(); + }); - it("should return coder.headerCommand if set in config", () => { - vi.stubEnv("CODER_HEADER_COMMAND", "printf 'x=y'"); + it("should return undefined if coder.headerCommand is a blank string", () => { + const config = { + get: () => " ", + } as unknown as WorkspaceConfiguration; - const config = { - get: () => "printf 'foo=bar'", - } as unknown as WorkspaceConfiguration; + expect(getHeaderCommand(config)).toBeUndefined(); + }); - expect(getHeaderCommand(config)).toBe("printf 'foo=bar'"); - }); + it("should return coder.headerCommand if set in config", () => { + vi.stubEnv("CODER_HEADER_COMMAND", "printf 'x=y'"); + + const config = { + get: () => "printf 'foo=bar'", + } as unknown as WorkspaceConfiguration; + + expect(getHeaderCommand(config)).toBe("printf 'foo=bar'"); + }); - it("should return CODER_HEADER_COMMAND if coder.headerCommand is not set in config and CODER_HEADER_COMMAND is set in environment", () => { - vi.stubEnv("CODER_HEADER_COMMAND", "printf 'x=y'"); + it("should return CODER_HEADER_COMMAND if coder.headerCommand is not set in config and CODER_HEADER_COMMAND is set in environment", () => { + vi.stubEnv("CODER_HEADER_COMMAND", "printf 'x=y'"); - const config = { - get: () => undefined, - } as unknown as WorkspaceConfiguration; + const config = { + get: () => undefined, + } as unknown as WorkspaceConfiguration; - expect(getHeaderCommand(config)).toBe("printf 'x=y'"); + expect(getHeaderCommand(config)).toBe("printf 'x=y'"); + }); }); }); diff --git a/test/utils/platform.test.ts b/test/utils/platform.test.ts new file mode 100644 index 00000000..c04820d6 --- /dev/null +++ b/test/utils/platform.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; + +import { + expectPathsEqual, + exitCommand, + printCommand, + printEnvCommand, + isWindows, +} from "./platform"; + +describe("platform utils", () => { + describe("printCommand", () => { + it("should generate a simple node command", () => { + const result = printCommand("hello world"); + expect(result).toBe("node -e \"process.stdout.write('hello world')\""); + }); + + it("should escape special characters", () => { + const result = printCommand('path\\to\\file\'s "name"\nline2\rcarriage'); + expect(result).toBe( + 'node -e "process.stdout.write(\'path\\\\to\\\\file\\\'s \\"name\\"\\nline2\\rcarriage\')"', + ); + }); + }); + + describe("exitCommand", () => { + it("should generate node commands with various exit codes", () => { + expect(exitCommand(0)).toBe('node -e "process.exit(0)"'); + expect(exitCommand(1)).toBe('node -e "process.exit(1)"'); + expect(exitCommand(42)).toBe('node -e "process.exit(42)"'); + expect(exitCommand(-1)).toBe('node -e "process.exit(-1)"'); + }); + }); + + describe("printEnvCommand", () => { + it("should generate node commands that print env variables", () => { + expect(printEnvCommand("url", "CODER_URL")).toBe( + "node -e \"process.stdout.write('url=' + process.env.CODER_URL)\"", + ); + expect(printEnvCommand("token", "CODER_TOKEN")).toBe( + "node -e \"process.stdout.write('token=' + process.env.CODER_TOKEN)\"", + ); + // Will fail to execute but that's fine + expect(printEnvCommand("", "")).toBe( + "node -e \"process.stdout.write('=' + process.env.)\"", + ); + }); + }); + + describe("expectPathsEqual", () => { + it("should consider identical paths equal", () => { + expectPathsEqual("same/path", "same/path"); + }); + + it("should throw when paths are different", () => { + expect(() => + expectPathsEqual("path/to/file1", "path/to/file2"), + ).toThrow(); + }); + + it("should handle empty paths", () => { + expectPathsEqual("", ""); + }); + + it.runIf(isWindows())( + "should consider paths with different separators equal on Windows", + () => { + expectPathsEqual("path/to/file", "path\\to\\file"); + expectPathsEqual("C:/path/to/file", "C:\\path\\to\\file"); + expectPathsEqual( + "C:/path with spaces/file", + "C:\\path with spaces\\file", + ); + }, + ); + + it.skipIf(isWindows())( + "should consider backslash as literal on non-Windows", + () => { + expect(() => + expectPathsEqual("path/to/file", "path\\to\\file"), + ).toThrow(); + }, + ); + }); +}); diff --git a/test/utils/platform.ts b/test/utils/platform.ts new file mode 100644 index 00000000..b0abc660 --- /dev/null +++ b/test/utils/platform.ts @@ -0,0 +1,46 @@ +import os from "node:os"; +import path from "node:path"; +import { expect } from "vitest"; + +export function isWindows(): boolean { + return os.platform() === "win32"; +} + +/** + * Returns a platform-independent command that outputs the given text. + * Uses Node.js which is guaranteed to be available during tests. + */ +export function printCommand(output: string): string { + const escaped = output + .replace(/\\/g, "\\\\") // Escape backslashes first + .replace(/'/g, "\\'") // Escape single quotes + .replace(/"/g, '\\"') // Escape double quotes + .replace(/\r/g, "\\r") // Preserve carriage returns + .replace(/\n/g, "\\n"); // Preserve newlines + + return `node -e "process.stdout.write('${escaped}')"`; +} + +/** + * Returns a platform-independent command that exits with the given code. + */ +export function exitCommand(code: number): string { + return `node -e "process.exit(${code})"`; +} + +/** + * Returns a platform-independent command that prints an environment variable. + * @param key The key for the header (e.g., "url" to output "url=value") + * @param varName The environment variable name to access + */ +export function printEnvCommand(key: string, varName: string): string { + return `node -e "process.stdout.write('${key}=' + process.env.${varName})"`; +} + +export function expectPathsEqual(actual: string, expected: string) { + expect(normalizePath(actual)).toBe(normalizePath(expected)); +} + +function normalizePath(p: string): string { + return p.replaceAll(path.sep, path.posix.sep); +} diff --git a/vitest.config.ts b/vitest.config.ts index 01e3896a..40c5f958 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,13 +5,8 @@ export default defineConfig({ test: { globals: true, environment: "node", - include: ["test/unit/**/*.test.ts", "test/integration/**/*.test.ts"], - exclude: [ - "test/integration/**", - "**/node_modules/**", - "**/out/**", - "**/*.d.ts", - ], + include: ["test/unit/**/*.test.ts", "test/utils/**/*.test.ts"], + exclude: ["**/node_modules/**", "**/out/**", "**/*.d.ts"], pool: "threads", fileParallelism: true, coverage: { From f9b1f2516638afb466b11a0ccdb6747459900c27 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Fri, 17 Oct 2025 11:34:38 +0300 Subject: [PATCH 27/45] Add SSE fallback to some one way WS connections (#623) Add SSE fallback to some WS connections: * `/api/v2/workspaces/${workspace.id}/watch-ws` -> `/api/v2/workspaceagents/${agentId}/watch-metadata` * `/api/v2/workspaceagents/${agentId}/watch-metadata-ws` -> `/api/v2/workspaceagents/${agentId}/watch-metadata` Restored the previous code regarding `createStreamingFetchAdapter` to stream in SSE events. * Implemented a unified interface for WS and SSE to be similar to the `OneWayWebSocket`. * Added unified logging for WS and SSE. * Fixed issue with headers order precedence * Add tests for `CoderApi` Closes #620 --- src/api/coderApi.ts | 171 +++++-- src/api/streamingFetchAdapter.ts | 71 +++ .../{wsLogger.ts => eventStreamLogger.ts} | 16 +- src/websocket/eventStreamConnection.ts | 51 +++ src/websocket/oneWayWebSocket.ts | 69 +-- src/websocket/sseConnection.ts | 221 +++++++++ src/websocket/utils.ts | 15 + src/workspace/workspaceMonitor.ts | 14 +- test/unit/api/coderApi.test.ts | 431 ++++++++++++++++++ ...gger.test.ts => eventStreamLogger.test.ts} | 62 ++- 10 files changed, 1005 insertions(+), 116 deletions(-) create mode 100644 src/api/streamingFetchAdapter.ts rename src/logging/{wsLogger.ts => eventStreamLogger.ts} (77%) create mode 100644 src/websocket/eventStreamConnection.ts create mode 100644 src/websocket/sseConnection.ts create mode 100644 src/websocket/utils.ts create mode 100644 test/unit/api/coderApi.test.ts rename test/unit/logging/{wsLogger.test.ts => eventStreamLogger.test.ts} (50%) diff --git a/src/api/coderApi.ts b/src/api/coderApi.ts index 99976ff7..6509ac67 100644 --- a/src/api/coderApi.ts +++ b/src/api/coderApi.ts @@ -6,17 +6,18 @@ import { } from "axios"; import { Api } from "coder/site/src/api/api"; import { + type ServerSentEvent, type GetInboxNotificationResponse, type ProvisionerJobLog, - type ServerSentEvent, type Workspace, type WorkspaceAgent, } from "coder/site/src/api/typesGenerated"; import * as vscode from "vscode"; -import { type ClientOptions } from "ws"; +import { type ClientOptions, type CloseEvent, type ErrorEvent } from "ws"; import { CertificateError } from "../error"; import { getHeaderCommand, getHeaders } from "../headers"; +import { EventStreamLogger } from "../logging/eventStreamLogger"; import { createRequestMeta, logRequest, @@ -29,11 +30,12 @@ import { HttpClientLogLevel, } from "../logging/types"; import { sizeOf } from "../logging/utils"; -import { WsLogger } from "../logging/wsLogger"; +import { type UnidirectionalStream } from "../websocket/eventStreamConnection"; import { OneWayWebSocket, type OneWayWebSocketInit, } from "../websocket/oneWayWebSocket"; +import { SseConnection } from "../websocket/sseConnection"; import { createHttpAgent } from "./utils"; @@ -84,8 +86,9 @@ export class CoderApi extends Api { }; watchWorkspace = async (workspace: Workspace, options?: ClientOptions) => { - return this.createWebSocket({ + return this.createWebSocketWithFallback({ apiRoute: `/api/v2/workspaces/${workspace.id}/watch-ws`, + fallbackApiRoute: `/api/v2/workspaces/${workspace.id}/watch`, options, }); }; @@ -94,8 +97,9 @@ export class CoderApi extends Api { agentId: WorkspaceAgent["id"], options?: ClientOptions, ) => { - return this.createWebSocket({ + return this.createWebSocketWithFallback({ apiRoute: `/api/v2/workspaceagents/${agentId}/watch-metadata-ws`, + fallbackApiRoute: `/api/v2/workspaceagents/${agentId}/watch-metadata`, options, }); }; @@ -103,6 +107,7 @@ export class CoderApi extends Api { watchBuildLogsByBuildId = async ( buildId: string, logs: ProvisionerJobLog[], + options?: ClientOptions, ) => { const searchParams = new URLSearchParams({ follow: "true" }); if (logs.length) { @@ -112,6 +117,7 @@ export class CoderApi extends Api { return this.createWebSocket({ apiRoute: `/api/v2/workspacebuilds/${buildId}/logs`, searchParams, + options, }); }; @@ -128,7 +134,7 @@ export class CoderApi extends Api { coderSessionTokenHeader ] as string | undefined; - const headers = await getHeaders( + const headersFromCommand = await getHeaders( baseUrlRaw, getHeaderCommand(vscode.workspace.getConfiguration()), this.output, @@ -137,43 +143,154 @@ export class CoderApi extends Api { const httpAgent = await createHttpAgent( vscode.workspace.getConfiguration(), ); + + /** + * Similar to the REST client, we want to prioritize headers in this order (highest to lowest): + * 1. Headers from the header command + * 2. Any headers passed directly to this function + * 3. Coder session token from the Api client (if set) + */ + const headers = { + ...(token ? { [coderSessionTokenHeader]: token } : {}), + ...configs.options?.headers, + ...headersFromCommand, + }; + const webSocket = new OneWayWebSocket({ location: baseUrl, ...configs, options: { + ...configs.options, agent: httpAgent, followRedirects: true, - headers: { - ...(token ? { [coderSessionTokenHeader]: token } : {}), - ...configs.options?.headers, - ...headers, - }, - ...configs.options, + headers, }, }); - const wsUrl = new URL(webSocket.url); - const pathWithQuery = wsUrl.pathname + wsUrl.search; - const wsLogger = new WsLogger(this.output, pathWithQuery); - wsLogger.logConnecting(); + this.attachStreamLogger(webSocket); + return webSocket; + } - webSocket.addEventListener("open", () => { - wsLogger.logOpen(); - }); + private attachStreamLogger( + connection: UnidirectionalStream, + ): void { + const url = new URL(connection.url); + const logger = new EventStreamLogger( + this.output, + url.pathname + url.search, + url.protocol.startsWith("http") ? "SSE" : "WS", + ); + logger.logConnecting(); - webSocket.addEventListener("message", (event) => { - wsLogger.logMessage(event.sourceEvent.data); - }); + connection.addEventListener("open", () => logger.logOpen()); + connection.addEventListener("close", (event: CloseEvent) => + logger.logClose(event.code, event.reason), + ); + connection.addEventListener("error", (event: ErrorEvent) => + logger.logError(event.error, event.message), + ); + connection.addEventListener("message", (event) => + logger.logMessage(event.sourceEvent.data), + ); + } - webSocket.addEventListener("close", (event) => { - wsLogger.logClose(event.code, event.reason); + /** + * Create a WebSocket connection with SSE fallback on 404. + * + * Note: The fallback on SSE ignores all passed client options except the headers. + */ + private async createWebSocketWithFallback(configs: { + apiRoute: string; + fallbackApiRoute: string; + searchParams?: Record | URLSearchParams; + options?: ClientOptions; + }): Promise> { + let webSocket: OneWayWebSocket; + try { + webSocket = await this.createWebSocket({ + apiRoute: configs.apiRoute, + searchParams: configs.searchParams, + options: configs.options, + }); + } catch { + // Failed to create WebSocket, use SSE fallback + return this.createSseFallback( + configs.fallbackApiRoute, + configs.searchParams, + configs.options?.headers, + ); + } + + return this.waitForConnection(webSocket, () => + this.createSseFallback( + configs.fallbackApiRoute, + configs.searchParams, + configs.options?.headers, + ), + ); + } + + private waitForConnection( + connection: UnidirectionalStream, + onNotFound?: () => Promise>, + ): Promise> { + return new Promise((resolve, reject) => { + const cleanup = () => { + connection.removeEventListener("open", handleOpen); + connection.removeEventListener("error", handleError); + }; + + const handleOpen = () => { + cleanup(); + resolve(connection); + }; + + const handleError = (event: ErrorEvent) => { + cleanup(); + const is404 = + event.message?.includes("404") || + event.error?.message?.includes("404"); + + if (is404 && onNotFound) { + connection.close(); + onNotFound().then(resolve).catch(reject); + } else { + reject(event.error || new Error(event.message)); + } + }; + + connection.addEventListener("open", handleOpen); + connection.addEventListener("error", handleError); }); + } + + /** + * Create SSE fallback connection + */ + private async createSseFallback( + apiRoute: string, + searchParams?: Record | URLSearchParams, + optionsHeaders?: Record, + ): Promise> { + this.output.warn(`WebSocket failed, using SSE fallback: ${apiRoute}`); + + const baseUrlRaw = this.getAxiosInstance().defaults.baseURL; + if (!baseUrlRaw) { + throw new Error("No base URL set on REST client"); + } - webSocket.addEventListener("error", (event) => { - wsLogger.logError(event.error, event.message); + const baseUrl = new URL(baseUrlRaw); + const sseConnection = new SseConnection({ + location: baseUrl, + apiRoute, + searchParams, + axiosInstance: this.getAxiosInstance(), + optionsHeaders: optionsHeaders, + logger: this.output, }); - return webSocket; + this.attachStreamLogger(sseConnection); + return this.waitForConnection(sseConnection); } } diff --git a/src/api/streamingFetchAdapter.ts b/src/api/streamingFetchAdapter.ts new file mode 100644 index 00000000..f0730535 --- /dev/null +++ b/src/api/streamingFetchAdapter.ts @@ -0,0 +1,71 @@ +import { type AxiosInstance } from "axios"; +import { type FetchLikeInit, type FetchLikeResponse } from "eventsource"; +import { type IncomingMessage } from "http"; + +/** + * Creates a fetch adapter using an Axios instance that returns streaming responses. + * This is used by EventSource to make authenticated SSE connections. + */ +export function createStreamingFetchAdapter( + axiosInstance: AxiosInstance, + configHeaders?: Record, +): (url: string | URL, init?: FetchLikeInit) => Promise { + return async ( + url: string | URL, + init?: FetchLikeInit, + ): Promise => { + const urlStr = url.toString(); + + const response = await axiosInstance.request({ + url: urlStr, + signal: init?.signal, + headers: { ...init?.headers, ...configHeaders }, + responseType: "stream", + validateStatus: () => true, // Don't throw on any status code + }); + + const stream = new ReadableStream({ + start(controller) { + response.data.on("data", (chunk: Buffer) => { + try { + controller.enqueue(chunk); + } catch { + // Stream already closed or errored, ignore + } + }); + + response.data.on("end", () => { + try { + controller.close(); + } catch { + // Stream already closed, ignore + } + }); + + response.data.on("error", (err: Error) => { + controller.error(err); + }); + }, + + cancel() { + response.data.destroy(); + return Promise.resolve(); + }, + }); + + return { + body: { + getReader: () => stream.getReader(), + }, + url: urlStr, + status: response.status, + redirected: response.request?.res?.responseUrl !== urlStr, + headers: { + get: (name: string) => { + const value = response.headers[name.toLowerCase()]; + return value === undefined ? null : String(value); + }, + }, + }; + }; +} diff --git a/src/logging/wsLogger.ts b/src/logging/eventStreamLogger.ts similarity index 77% rename from src/logging/wsLogger.ts rename to src/logging/eventStreamLogger.ts index fd6acd00..224f52b7 100644 --- a/src/logging/wsLogger.ts +++ b/src/logging/eventStreamLogger.ts @@ -12,31 +12,35 @@ const numFormatter = new Intl.NumberFormat("en", { compactDisplay: "short", }); -export class WsLogger { +export class EventStreamLogger { private readonly logger: Logger; private readonly url: string; private readonly id: string; + private readonly protocol: string; private readonly startedAt: number; private openedAt?: number; private msgCount = 0; private byteCount = 0; private unknownByteCount = false; - constructor(logger: Logger, url: string) { + constructor(logger: Logger, url: string, protocol: "WS" | "SSE") { this.logger = logger; this.url = url; + this.protocol = protocol; this.id = createRequestId(); this.startedAt = Date.now(); } logConnecting(): void { - this.logger.trace(`→ WS ${shortId(this.id)} ${this.url}`); + this.logger.trace(`→ ${this.protocol} ${shortId(this.id)} ${this.url}`); } logOpen(): void { this.openedAt = Date.now(); const time = formatTime(this.openedAt - this.startedAt); - this.logger.trace(`← WS ${shortId(this.id)} connected ${this.url} ${time}`); + this.logger.trace( + `← ${this.protocol} ${shortId(this.id)} connected ${this.url} ${time}`, + ); } logMessage(data: unknown): void { @@ -62,7 +66,7 @@ export class WsLogger { const statsStr = ` [${stats.join(", ")}]`; this.logger.trace( - `▣ WS ${shortId(this.id)} closed ${this.url}${codeStr}${reasonStr}${statsStr}`, + `▣ ${this.protocol} ${shortId(this.id)} closed ${this.url}${codeStr}${reasonStr}${statsStr}`, ); } @@ -70,7 +74,7 @@ export class WsLogger { const time = formatTime(Date.now() - this.startedAt); const errorMsg = message || errToStr(error, "connection error"); this.logger.error( - `✗ WS ${shortId(this.id)} error ${this.url} ${time} - ${errorMsg}`, + `✗ ${this.protocol} ${shortId(this.id)} error ${this.url} ${time} - ${errorMsg}`, error, ); } diff --git a/src/websocket/eventStreamConnection.ts b/src/websocket/eventStreamConnection.ts new file mode 100644 index 00000000..2dc6514e --- /dev/null +++ b/src/websocket/eventStreamConnection.ts @@ -0,0 +1,51 @@ +import { type WebSocketEventType } from "coder/site/src/utils/OneWayWebSocket"; +import { + type CloseEvent, + type Event as WsEvent, + type ErrorEvent, + type MessageEvent, +} from "ws"; + +// Event payload types matching OneWayWebSocket +export type ParsedMessageEvent = Readonly< + | { + sourceEvent: MessageEvent; + parsedMessage: TData; + parseError: undefined; + } + | { + sourceEvent: MessageEvent; + parsedMessage: undefined; + parseError: Error; + } +>; + +export type EventPayloadMap = { + close: CloseEvent; + error: ErrorEvent; + message: ParsedMessageEvent; + open: WsEvent; +}; + +export type EventHandler = ( + payload: EventPayloadMap[TEvent], +) => void; + +/** + * Common interface for both WebSocket and SSE connections that handle event streams. + * Matches the OneWayWebSocket interface for compatibility. + */ +export interface UnidirectionalStream { + readonly url: string; + addEventListener( + eventType: TEvent, + callback: EventHandler, + ): void; + + removeEventListener( + eventType: TEvent, + callback: EventHandler, + ): void; + + close(code?: number, reason?: string): void; +} diff --git a/src/websocket/oneWayWebSocket.ts b/src/websocket/oneWayWebSocket.ts index 37965596..c27b1fe4 100644 --- a/src/websocket/oneWayWebSocket.ts +++ b/src/websocket/oneWayWebSocket.ts @@ -8,51 +8,13 @@ */ import { type WebSocketEventType } from "coder/site/src/utils/OneWayWebSocket"; -import Ws, { - type ClientOptions, - type CloseEvent, - type ErrorEvent, - type Event, - type MessageEvent, - type RawData, -} from "ws"; +import Ws, { type ClientOptions, type MessageEvent, type RawData } from "ws"; -export type OneWayMessageEvent = Readonly< - | { - sourceEvent: MessageEvent; - parsedMessage: TData; - parseError: undefined; - } - | { - sourceEvent: MessageEvent; - parsedMessage: undefined; - parseError: Error; - } ->; - -type OneWayEventPayloadMap = { - close: CloseEvent; - error: ErrorEvent; - message: OneWayMessageEvent; - open: Event; -}; - -type OneWayEventCallback = ( - payload: OneWayEventPayloadMap[TEvent], -) => void; - -interface OneWayWebSocketApi { - get url(): string; - addEventListener( - eventType: TEvent, - callback: OneWayEventCallback, - ): void; - removeEventListener( - eventType: TEvent, - callback: OneWayEventCallback, - ): void; - close(code?: number, reason?: string): void; -} +import { + type UnidirectionalStream, + type EventHandler, +} from "./eventStreamConnection"; +import { getQueryString } from "./utils"; export type OneWayWebSocketInit = { location: { protocol: string; host: string }; @@ -63,23 +25,18 @@ export type OneWayWebSocketInit = { }; export class OneWayWebSocket - implements OneWayWebSocketApi + implements UnidirectionalStream { readonly #socket: Ws; readonly #messageCallbacks = new Map< - OneWayEventCallback, + EventHandler, (data: RawData) => void >(); constructor(init: OneWayWebSocketInit) { const { location, apiRoute, protocols, options, searchParams } = init; - const formattedParams = - searchParams instanceof URLSearchParams - ? searchParams - : new URLSearchParams(searchParams); - const paramsString = formattedParams.toString(); - const paramsSuffix = paramsString ? `?${paramsString}` : ""; + const paramsSuffix = getQueryString(searchParams); const wsProtocol = location.protocol === "https:" ? "wss:" : "ws:"; const url = `${wsProtocol}//${location.host}${apiRoute}${paramsSuffix}`; @@ -92,10 +49,10 @@ export class OneWayWebSocket addEventListener( event: TEvent, - callback: OneWayEventCallback, + callback: EventHandler, ): void { if (event === "message") { - const messageCallback = callback as OneWayEventCallback; + const messageCallback = callback as EventHandler; if (this.#messageCallbacks.has(messageCallback)) { return; @@ -128,10 +85,10 @@ export class OneWayWebSocket removeEventListener( event: TEvent, - callback: OneWayEventCallback, + callback: EventHandler, ): void { if (event === "message") { - const messageCallback = callback as OneWayEventCallback; + const messageCallback = callback as EventHandler; const wrapper = this.#messageCallbacks.get(messageCallback); if (wrapper) { diff --git a/src/websocket/sseConnection.ts b/src/websocket/sseConnection.ts new file mode 100644 index 00000000..834100aa --- /dev/null +++ b/src/websocket/sseConnection.ts @@ -0,0 +1,221 @@ +import { type AxiosInstance } from "axios"; +import { type ServerSentEvent } from "coder/site/src/api/typesGenerated"; +import { type WebSocketEventType } from "coder/site/src/utils/OneWayWebSocket"; +import { EventSource } from "eventsource"; + +import { createStreamingFetchAdapter } from "../api/streamingFetchAdapter"; +import { type Logger } from "../logging/logger"; + +import { getQueryString } from "./utils"; + +import type { + CloseEvent as WsCloseEvent, + ErrorEvent as WsErrorEvent, + Event as WsEvent, + MessageEvent as WsMessageEvent, +} from "ws"; + +import type { + UnidirectionalStream, + ParsedMessageEvent, + EventHandler, +} from "./eventStreamConnection"; + +export type SseConnectionInit = { + location: { protocol: string; host: string }; + apiRoute: string; + searchParams?: Record | URLSearchParams; + optionsHeaders?: Record; + axiosInstance: AxiosInstance; + logger: Logger; +}; + +export class SseConnection implements UnidirectionalStream { + private readonly eventSource: EventSource; + private readonly logger: Logger; + private readonly callbacks = { + open: new Set>(), + close: new Set>(), + error: new Set>(), + }; + // Original callback -> wrapped callback + private readonly messageWrappers = new Map< + EventHandler, + (event: MessageEvent) => void + >(); + + public readonly url: string; + + public constructor(init: SseConnectionInit) { + this.logger = init.logger; + this.url = this.buildUrl(init); + this.eventSource = new EventSource(this.url, { + fetch: createStreamingFetchAdapter( + init.axiosInstance, + init.optionsHeaders, + ), + }); + this.setupEventHandlers(); + } + + private buildUrl(init: SseConnectionInit): string { + const { location, apiRoute, searchParams } = init; + const queryString = getQueryString(searchParams); + return `${location.protocol}//${location.host}${apiRoute}${queryString}`; + } + + private setupEventHandlers(): void { + this.eventSource.addEventListener("open", () => + this.invokeCallbacks(this.callbacks.open, {} as WsEvent, "open"), + ); + + this.eventSource.addEventListener("data", (event: MessageEvent) => { + this.invokeCallbacks(this.messageWrappers.values(), event, "message"); + }); + + this.eventSource.addEventListener("error", (error: Event | ErrorEvent) => { + this.invokeCallbacks( + this.callbacks.error, + this.createErrorEvent(error), + "error", + ); + + if (this.eventSource.readyState === EventSource.CLOSED) { + this.invokeCallbacks( + this.callbacks.close, + { + code: 1006, + reason: "Connection lost", + wasClean: false, + } as WsCloseEvent, + "close", + ); + } + }); + } + + private invokeCallbacks( + callbacks: Iterable<(event: T) => void>, + event: T, + eventType: string, + ): void { + for (const cb of callbacks) { + try { + cb(event); + } catch (err) { + this.logger.error(`Error in SSE ${eventType} callback:`, err); + } + } + } + + private createErrorEvent(event: Event | ErrorEvent): WsErrorEvent { + const errorMessage = + event instanceof ErrorEvent && event.message + ? event.message + : "SSE connection error"; + const error = event instanceof ErrorEvent ? event.error : undefined; + + return { + error: error, + message: errorMessage, + } as WsErrorEvent; + } + + public addEventListener( + event: TEvent, + callback: EventHandler, + ): void { + switch (event) { + case "close": + this.callbacks.close.add( + callback as EventHandler, + ); + break; + case "error": + this.callbacks.error.add( + callback as EventHandler, + ); + break; + case "message": { + const messageCallback = callback as EventHandler< + ServerSentEvent, + "message" + >; + if (!this.messageWrappers.has(messageCallback)) { + this.messageWrappers.set(messageCallback, (event: MessageEvent) => { + messageCallback(this.parseMessage(event)); + }); + } + break; + } + case "open": + this.callbacks.open.add( + callback as EventHandler, + ); + break; + } + } + + private parseMessage( + event: MessageEvent, + ): ParsedMessageEvent { + const wsEvent = { data: event.data } as WsMessageEvent; + try { + return { + sourceEvent: wsEvent, + parsedMessage: { type: "data", data: JSON.parse(event.data) }, + parseError: undefined, + }; + } catch (err) { + return { + sourceEvent: wsEvent, + parsedMessage: undefined, + parseError: err as Error, + }; + } + } + + public removeEventListener( + event: TEvent, + callback: EventHandler, + ): void { + switch (event) { + case "close": + this.callbacks.close.delete( + callback as EventHandler, + ); + break; + case "error": + this.callbacks.error.delete( + callback as EventHandler, + ); + break; + case "message": + this.messageWrappers.delete( + callback as EventHandler, + ); + break; + case "open": + this.callbacks.open.delete( + callback as EventHandler, + ); + break; + } + } + + public close(code?: number, reason?: string): void { + this.eventSource.close(); + this.invokeCallbacks( + this.callbacks.close, + { + code: code ?? 1000, + reason: reason ?? "Normal closure", + wasClean: true, + } as WsCloseEvent, + "close", + ); + + Object.values(this.callbacks).forEach((callbackSet) => callbackSet.clear()); + this.messageWrappers.clear(); + } +} diff --git a/src/websocket/utils.ts b/src/websocket/utils.ts new file mode 100644 index 00000000..592ce45e --- /dev/null +++ b/src/websocket/utils.ts @@ -0,0 +1,15 @@ +/** + * Converts params to a query string. Returns empty string if no params, + * otherwise returns params prefixed with '?'. + */ +export function getQueryString( + params: Record | URLSearchParams | undefined, +): string { + if (!params) { + return ""; + } + const searchParams = + params instanceof URLSearchParams ? params : new URLSearchParams(params); + const str = searchParams.toString(); + return str ? `?${str}` : ""; +} diff --git a/src/workspace/workspaceMonitor.ts b/src/workspace/workspaceMonitor.ts index a761249a..ceea8a91 100644 --- a/src/workspace/workspaceMonitor.ts +++ b/src/workspace/workspaceMonitor.ts @@ -9,7 +9,7 @@ import { createWorkspaceIdentifier, errToStr } from "../api/api-helper"; import { type CoderApi } from "../api/coderApi"; import { type ContextManager } from "../core/contextManager"; import { type Logger } from "../logging/logger"; -import { type OneWayWebSocket } from "../websocket/oneWayWebSocket"; +import { type UnidirectionalStream } from "../websocket/eventStreamConnection"; /** * Monitor a single workspace using a WebSocket for events like shutdown and deletion. @@ -17,7 +17,7 @@ import { type OneWayWebSocket } from "../websocket/oneWayWebSocket"; * workspace status is also shown in the status bar menu. */ export class WorkspaceMonitor implements vscode.Disposable { - private socket: OneWayWebSocket | undefined; + private socket: UnidirectionalStream | undefined; private disposed = false; // How soon in advance to notify about autostop and deletion. @@ -93,10 +93,12 @@ export class WorkspaceMonitor implements vscode.Disposable { return; } // Perhaps we need to parse this and validate it. - const newWorkspaceData = event.parsedMessage.data as Workspace; - monitor.update(newWorkspaceData); - monitor.maybeNotify(newWorkspaceData); - monitor.onChange.fire(newWorkspaceData); + const newWorkspaceData = event.parsedMessage.data as Workspace | null; + if (newWorkspaceData) { + monitor.update(newWorkspaceData); + monitor.maybeNotify(newWorkspaceData); + monitor.onChange.fire(newWorkspaceData); + } } catch (error) { monitor.notifyError(error); } diff --git a/test/unit/api/coderApi.test.ts b/test/unit/api/coderApi.test.ts new file mode 100644 index 00000000..0336d564 --- /dev/null +++ b/test/unit/api/coderApi.test.ts @@ -0,0 +1,431 @@ +import axios, { AxiosError, AxiosHeaders } from "axios"; +import { type ProvisionerJobLog } from "coder/site/src/api/typesGenerated"; +import { EventSource } from "eventsource"; +import { ProxyAgent } from "proxy-agent"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import Ws from "ws"; + +import { CoderApi } from "@/api/coderApi"; +import { createHttpAgent } from "@/api/utils"; +import { CertificateError } from "@/error"; +import { getHeaders } from "@/headers"; +import { type RequestConfigWithMeta } from "@/logging/types"; +import { OneWayWebSocket } from "@/websocket/oneWayWebSocket"; +import { SseConnection } from "@/websocket/sseConnection"; + +import { + createMockLogger, + MockConfigurationProvider, +} from "../../mocks/testHelpers"; + +const CODER_URL = "https://coder.example.com"; +const AXIOS_TOKEN = "passed-token"; +const BUILD_ID = "build-123"; +const AGENT_ID = "agent-123"; + +vi.mock("ws"); +vi.mock("eventsource"); +vi.mock("proxy-agent"); + +vi.mock("axios", async () => { + const actual = await vi.importActual("axios"); + + const mockAdapter = vi.fn(mockAdapterImpl); + + const mockDefault = { + ...actual.default, + create: vi.fn((config) => { + const instance = actual.default.create({ + ...config, + adapter: mockAdapter, + }); + return instance; + }), + __mockAdapter: mockAdapter, + }; + + return { + ...actual, + default: mockDefault, + }; +}); + +vi.mock("@/headers", () => ({ + getHeaders: vi.fn().mockResolvedValue({}), + getHeaderCommand: vi.fn(), +})); + +vi.mock("@/api/utils", () => ({ + createHttpAgent: vi.fn(), +})); + +vi.mock("@/api/streamingFetchAdapter", () => ({ + createStreamingFetchAdapter: vi.fn(() => fetch), +})); + +describe("CoderApi", () => { + let mockLogger: ReturnType; + let mockConfig: MockConfigurationProvider; + let mockAdapter: ReturnType; + let api: CoderApi; + + const createApi = (url = CODER_URL, token = AXIOS_TOKEN) => { + return CoderApi.create(url, token, mockLogger); + }; + + beforeEach(() => { + vi.resetAllMocks(); + + const axiosMock = axios as typeof axios & { + __mockAdapter: ReturnType; + }; + mockAdapter = axiosMock.__mockAdapter; + mockAdapter.mockImplementation(mockAdapterImpl); + + vi.mocked(getHeaders).mockResolvedValue({}); + mockLogger = createMockLogger(); + mockConfig = new MockConfigurationProvider(); + mockConfig.set("coder.httpClientLogLevel", "BASIC"); + }); + + describe("HTTP Interceptors", () => { + it("adds custom headers and HTTP agent to requests", async () => { + const mockAgent = new ProxyAgent(); + vi.mocked(createHttpAgent).mockResolvedValue(mockAgent); + vi.mocked(getHeaders).mockResolvedValue({ + "X-Custom-Header": "custom-value", + "X-Another-Header": "another-value", + }); + + const api = createApi(); + const response = await api.getAxiosInstance().get("/api/v2/users/me"); + + expect(response.config.headers["X-Custom-Header"]).toBe("custom-value"); + expect(response.config.headers["X-Another-Header"]).toBe("another-value"); + expect(response.config.httpsAgent).toBe(mockAgent); + expect(response.config.httpAgent).toBe(mockAgent); + expect(response.config.proxy).toBe(false); + }); + + it("wraps certificate errors in response interceptor", async () => { + const api = createApi(); + const certError = new AxiosError( + "self signed certificate", + "DEPTH_ZERO_SELF_SIGNED_CERT", + ); + mockAdapter.mockRejectedValueOnce(certError); + + const thrownError = await api + .getAxiosInstance() + .get("/api/v2/users/me") + .catch((e) => e); + + expect(thrownError).toBeInstanceOf(CertificateError); + expect(thrownError.message).toContain("Secure connection"); + expect(thrownError.x509Err).toBeDefined(); + }); + + it("applies headers in correct precedence order (command > config > axios default)", async () => { + const api = createApi(CODER_URL, AXIOS_TOKEN); + + // Test 1: Headers from config, default token from API creation + const response = await api.getAxiosInstance().get("/api/v2/users/me", { + headers: new AxiosHeaders({ + "X-Custom-Header": "from-config", + "X-Extra": "extra-value", + }), + }); + + expect(response.config.headers["X-Custom-Header"]).toBe("from-config"); + expect(response.config.headers["X-Extra"]).toBe("extra-value"); + expect(response.config.headers["Coder-Session-Token"]).toBe(AXIOS_TOKEN); + + // Test 2: Token from request options overrides default + const responseWithToken = await api + .getAxiosInstance() + .get("/api/v2/users/me", { + headers: new AxiosHeaders({ + "Coder-Session-Token": "from-options", + }), + }); + + expect(responseWithToken.config.headers["Coder-Session-Token"]).toBe( + "from-options", + ); + + // Test 3: Header command overrides everything + vi.mocked(getHeaders).mockResolvedValue({ + "Coder-Session-Token": "from-header-command", + }); + + const responseWithHeaderCommand = await api + .getAxiosInstance() + .get("/api/v2/users/me", { + headers: new AxiosHeaders({ + "Coder-Session-Token": "from-options", + }), + }); + + expect( + responseWithHeaderCommand.config.headers["Coder-Session-Token"], + ).toBe("from-header-command"); + }); + + it("logs requests and responses", async () => { + const api = createApi(); + + await api.getWorkspaces({}); + + expect(mockLogger.trace).toHaveBeenCalledWith( + expect.stringContaining("/api/v2/workspaces"), + ); + }); + + it("calculates request and response sizes in transforms", async () => { + const api = createApi(); + const response = await api + .getAxiosInstance() + .post("/api/v2/workspaces", { name: "test" }); + + expect((response.config as RequestConfigWithMeta).rawRequestSize).toBe( + 15, + ); + // We return the same data we sent in the mock adapter + expect((response.config as RequestConfigWithMeta).rawResponseSize).toBe( + 15, + ); + }); + }); + + describe("WebSocket Creation", () => { + const wsUrl = `wss://${CODER_URL.replace("https://", "")}/api/v2/workspacebuilds/${BUILD_ID}/logs?follow=true`; + + beforeEach(() => { + api = createApi(CODER_URL, AXIOS_TOKEN); + const mockWs = createMockWebSocket(wsUrl); + setupWebSocketMock(mockWs); + }); + + it("creates WebSocket with proper headers and configuration", async () => { + const mockAgent = new ProxyAgent(); + vi.mocked(getHeaders).mockResolvedValue({ + "X-Custom-Header": "custom-value", + }); + vi.mocked(createHttpAgent).mockResolvedValue(mockAgent); + + await api.watchBuildLogsByBuildId(BUILD_ID, []); + + expect(Ws).toHaveBeenCalledWith(wsUrl, undefined, { + agent: mockAgent, + followRedirects: true, + headers: { + "X-Custom-Header": "custom-value", + "Coder-Session-Token": AXIOS_TOKEN, + }, + }); + }); + + it("applies headers in correct precedence order (command > config > axios default)", async () => { + // Test 1: Default token from API creation + await api.watchBuildLogsByBuildId(BUILD_ID, []); + + expect(Ws).toHaveBeenCalledWith(wsUrl, undefined, { + agent: undefined, + followRedirects: true, + headers: { + "Coder-Session-Token": AXIOS_TOKEN, + }, + }); + + // Test 2: Token from config options overrides default + await api.watchBuildLogsByBuildId(BUILD_ID, [], { + headers: { + "X-Config-Header": "config-value", + "Coder-Session-Token": "from-config", + }, + }); + + expect(Ws).toHaveBeenCalledWith(wsUrl, undefined, { + agent: undefined, + followRedirects: true, + headers: { + "Coder-Session-Token": "from-config", + "X-Config-Header": "config-value", + }, + }); + + // Test 3: Header command overrides everything + vi.mocked(getHeaders).mockResolvedValue({ + "Coder-Session-Token": "from-header-command", + }); + + await api.watchBuildLogsByBuildId(BUILD_ID, [], { + headers: { + "Coder-Session-Token": "from-config", + }, + }); + + expect(Ws).toHaveBeenCalledWith(wsUrl, undefined, { + agent: undefined, + followRedirects: true, + headers: { + "Coder-Session-Token": "from-header-command", + }, + }); + }); + + it("logs WebSocket connections", async () => { + await api.watchBuildLogsByBuildId(BUILD_ID, []); + + expect(mockLogger.trace).toHaveBeenCalledWith( + expect.stringContaining(BUILD_ID), + ); + }); + + it("'watchBuildLogsByBuildId' includes after parameter for existing logs", async () => { + const jobLog: ProvisionerJobLog = { + created_at: new Date().toISOString(), + id: 1, + output: "log1", + log_source: "provisioner", + log_level: "info", + stage: "stage1", + }; + const existingLogs = [ + jobLog, + { ...jobLog, id: 20 }, + { ...jobLog, id: 5 }, + ]; + + await api.watchBuildLogsByBuildId(BUILD_ID, existingLogs); + + expect(Ws).toHaveBeenCalledWith( + expect.stringContaining("after=5"), + undefined, + expect.any(Object), + ); + }); + }); + + describe("SSE Fallback", () => { + beforeEach(() => { + api = createApi(); + const mockEventSource = createMockEventSource( + `${CODER_URL}/api/v2/workspaces/123/watch`, + ); + setupEventSourceMock(mockEventSource); + }); + + it("uses WebSocket when no errors occur", async () => { + const mockWs = createMockWebSocket( + `wss://${CODER_URL.replace("https://", "")}/api/v2/workspaceagents/${AGENT_ID}/watch-metadata`, + { + on: vi.fn((event, handler) => { + if (event === "open") { + setImmediate(() => handler()); + } + return mockWs as Ws; + }), + }, + ); + setupWebSocketMock(mockWs); + + const connection = await api.watchAgentMetadata(AGENT_ID); + + expect(connection).toBeInstanceOf(OneWayWebSocket); + expect(EventSource).not.toHaveBeenCalled(); + }); + + it("falls back to SSE when WebSocket creation fails", async () => { + vi.mocked(Ws).mockImplementation(() => { + throw new Error("WebSocket creation failed"); + }); + + const connection = await api.watchAgentMetadata(AGENT_ID); + + expect(connection).toBeInstanceOf(SseConnection); + expect(EventSource).toHaveBeenCalled(); + }); + + it("falls back to SSE on 404 error from WebSocket", async () => { + const mockWs = createMockWebSocket( + `wss://${CODER_URL.replace("https://", "")}/api/v2/test`, + { + on: vi.fn((event: string, handler: (e: unknown) => void) => { + if (event === "error") { + setImmediate(() => { + handler({ + error: new Error("404 Not Found"), + message: "404 Not Found", + }); + }); + } + return mockWs as Ws; + }), + }, + ); + setupWebSocketMock(mockWs); + + const connection = await api.watchAgentMetadata(AGENT_ID); + + expect(connection).toBeInstanceOf(SseConnection); + expect(EventSource).toHaveBeenCalled(); + }); + }); + + describe("Error Handling", () => { + it("throws error when no base URL is set", async () => { + const api = createApi(); + api.getAxiosInstance().defaults.baseURL = undefined; + + await expect(api.watchBuildLogsByBuildId(BUILD_ID, [])).rejects.toThrow( + "No base URL set on REST client", + ); + }); + }); +}); + +const mockAdapterImpl = vi.hoisted(() => (config: Record) => { + return Promise.resolve({ + data: config.data || "{}", + status: 200, + statusText: "OK", + headers: {}, + config, + }); +}); + +function createMockWebSocket( + url: string, + overrides?: Partial, +): Partial { + return { + url, + on: vi.fn(), + off: vi.fn(), + close: vi.fn(), + ...overrides, + }; +} + +function createMockEventSource(url: string): Partial { + return { + url, + readyState: EventSource.CONNECTING, + addEventListener: vi.fn((event, handler) => { + if (event === "open") { + setImmediate(() => handler(new Event("open"))); + } + }), + removeEventListener: vi.fn(), + close: vi.fn(), + }; +} + +function setupWebSocketMock(ws: Partial): void { + vi.mocked(Ws).mockImplementation(() => ws as Ws); +} + +function setupEventSourceMock(es: Partial): void { + vi.mocked(EventSource).mockImplementation(() => es as EventSource); +} diff --git a/test/unit/logging/wsLogger.test.ts b/test/unit/logging/eventStreamLogger.test.ts similarity index 50% rename from test/unit/logging/wsLogger.test.ts rename to test/unit/logging/eventStreamLogger.test.ts index 5bf9d5b1..352ccaac 100644 --- a/test/unit/logging/wsLogger.test.ts +++ b/test/unit/logging/eventStreamLogger.test.ts @@ -1,19 +1,23 @@ import { describe, expect, it } from "vitest"; -import { WsLogger } from "@/logging/wsLogger"; +import { EventStreamLogger } from "@/logging/eventStreamLogger"; import { createMockLogger } from "../../mocks/testHelpers"; -describe("WS Logger", () => { +describe("EventStreamLogger", () => { it("tracks message count and byte size", () => { const logger = createMockLogger(); - const wsLogger = new WsLogger(logger, "wss://example.com"); + const eventStreamLogger = new EventStreamLogger( + logger, + "wss://example.com", + "WS", + ); - wsLogger.logOpen(); - wsLogger.logMessage("hello"); - wsLogger.logMessage("world"); - wsLogger.logMessage(Buffer.from("test")); - wsLogger.logClose(); + eventStreamLogger.logOpen(); + eventStreamLogger.logMessage("hello"); + eventStreamLogger.logMessage("world"); + eventStreamLogger.logMessage(Buffer.from("test")); + eventStreamLogger.logClose(); expect(logger.trace).toHaveBeenCalledWith( expect.stringContaining("3 msgs"), @@ -23,12 +27,16 @@ describe("WS Logger", () => { it("handles unknown byte sizes with >= indicator", () => { const logger = createMockLogger(); - const wsLogger = new WsLogger(logger, "wss://example.com"); + const eventStreamLogger = new EventStreamLogger( + logger, + "wss://example.com", + "WS", + ); - wsLogger.logOpen(); - wsLogger.logMessage({ complex: "object" }); // Unknown size - no estimation - wsLogger.logMessage("known"); - wsLogger.logClose(); + eventStreamLogger.logOpen(); + eventStreamLogger.logMessage({ complex: "object" }); // Unknown size - no estimation + eventStreamLogger.logMessage("known"); + eventStreamLogger.logClose(); expect(logger.trace).toHaveBeenLastCalledWith( expect.stringContaining(">= 5 B"), @@ -37,22 +45,30 @@ describe("WS Logger", () => { it("handles close before open gracefully", () => { const logger = createMockLogger(); - const wsLogger = new WsLogger(logger, "wss://example.com"); + const eventStreamLogger = new EventStreamLogger( + logger, + "wss://example.com", + "WS", + ); // Closing without opening should not throw - expect(() => wsLogger.logClose()).not.toThrow(); + expect(() => eventStreamLogger.logClose()).not.toThrow(); expect(logger.trace).toHaveBeenCalled(); }); it("formats large message counts with compact notation", () => { const logger = createMockLogger(); - const wsLogger = new WsLogger(logger, "wss://example.com"); + const eventStreamLogger = new EventStreamLogger( + logger, + "wss://example.com", + "WS", + ); - wsLogger.logOpen(); + eventStreamLogger.logOpen(); for (let i = 0; i < 1100; i++) { - wsLogger.logMessage("x"); + eventStreamLogger.logMessage("x"); } - wsLogger.logClose(); + eventStreamLogger.logClose(); expect(logger.trace).toHaveBeenLastCalledWith( expect.stringMatching(/1[.,]1K\s*msgs/), @@ -61,10 +77,14 @@ describe("WS Logger", () => { it("logs errors with error object", () => { const logger = createMockLogger(); - const wsLogger = new WsLogger(logger, "wss://example.com"); + const eventStreamLogger = new EventStreamLogger( + logger, + "wss://example.com", + "WS", + ); const error = new Error("Connection failed"); - wsLogger.logError(error, "Failed to connect"); + eventStreamLogger.logError(error, "Failed to connect"); expect(logger.error).toHaveBeenCalledWith(expect.any(String), error); }); From 3c499bafbd0a1a278684e45c0262c3b0837c8917 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 21 Oct 2025 12:19:06 +0300 Subject: [PATCH 28/45] Add SSE Connection and streamingFetchAdapter tests (#625) --- src/api/coderApi.ts | 11 +- src/api/streamingFetchAdapter.ts | 2 +- src/websocket/sseConnection.ts | 9 +- test/unit/api/coderApi.test.ts | 4 +- test/unit/api/streamingFetchAdapter.test.ts | 220 ++++++++++++ test/unit/core/cliManager.test.ts | 5 +- test/unit/logging/utils.test.ts | 3 +- test/unit/websocket/sseConnection.test.ts | 356 ++++++++++++++++++++ vitest.config.ts | 2 +- 9 files changed, 595 insertions(+), 17 deletions(-) create mode 100644 test/unit/api/streamingFetchAdapter.test.ts create mode 100644 test/unit/websocket/sseConnection.test.ts diff --git a/src/api/coderApi.ts b/src/api/coderApi.ts index 6509ac67..da624bad 100644 --- a/src/api/coderApi.ts +++ b/src/api/coderApi.ts @@ -110,8 +110,9 @@ export class CoderApi extends Api { options?: ClientOptions, ) => { const searchParams = new URLSearchParams({ follow: "true" }); - if (logs.length) { - searchParams.append("after", logs[logs.length - 1].id.toString()); + const lastLog = logs.at(-1); + if (lastLog) { + searchParams.append("after", lastLog.id.toString()); } return this.createWebSocket({ @@ -311,9 +312,9 @@ function setupInterceptors( output, ); // Add headers from the header command. - Object.entries(headers).forEach(([key, value]) => { + for (const [key, value] of Object.entries(headers)) { config.headers[key] = value; - }); + } // Configure proxy and TLS. // Note that by default VS Code overrides the agent. To prevent this, set @@ -425,7 +426,7 @@ function wrapResponseTransform( function getSize(headers: AxiosHeaders, data: unknown): number | undefined { const contentLength = headers["content-length"]; if (contentLength !== undefined) { - return parseInt(contentLength, 10); + return Number.parseInt(contentLength, 10); } return sizeOf(data); diff --git a/src/api/streamingFetchAdapter.ts b/src/api/streamingFetchAdapter.ts index f0730535..f23ef1a7 100644 --- a/src/api/streamingFetchAdapter.ts +++ b/src/api/streamingFetchAdapter.ts @@ -1,6 +1,6 @@ import { type AxiosInstance } from "axios"; import { type FetchLikeInit, type FetchLikeResponse } from "eventsource"; -import { type IncomingMessage } from "http"; +import { type IncomingMessage } from "node:http"; /** * Creates a fetch adapter using an Axios instance that returns streaming responses. diff --git a/src/websocket/sseConnection.ts b/src/websocket/sseConnection.ts index 834100aa..5a71d303 100644 --- a/src/websocket/sseConnection.ts +++ b/src/websocket/sseConnection.ts @@ -109,11 +109,10 @@ export class SseConnection implements UnidirectionalStream { } private createErrorEvent(event: Event | ErrorEvent): WsErrorEvent { - const errorMessage = - event instanceof ErrorEvent && event.message - ? event.message - : "SSE connection error"; - const error = event instanceof ErrorEvent ? event.error : undefined; + // Check for properties instead of instanceof to avoid browser-only ErrorEvent global + const eventWithMessage = event as { message?: string; error?: unknown }; + const errorMessage = eventWithMessage.message || "SSE connection error"; + const error = eventWithMessage.error; return { error: error, diff --git a/test/unit/api/coderApi.test.ts b/test/unit/api/coderApi.test.ts index 0336d564..f133a72d 100644 --- a/test/unit/api/coderApi.test.ts +++ b/test/unit/api/coderApi.test.ts @@ -125,7 +125,7 @@ describe("CoderApi", () => { expect(thrownError.x509Err).toBeDefined(); }); - it("applies headers in correct precedence order (command > config > axios default)", async () => { + it("applies headers in correct precedence order (command overrides config overrides axios default)", async () => { const api = createApi(CODER_URL, AXIOS_TOKEN); // Test 1: Headers from config, default token from API creation @@ -225,7 +225,7 @@ describe("CoderApi", () => { }); }); - it("applies headers in correct precedence order (command > config > axios default)", async () => { + it("applies headers in correct precedence order (command overrides config overrides axios default)", async () => { // Test 1: Default token from API creation await api.watchBuildLogsByBuildId(BUILD_ID, []); diff --git a/test/unit/api/streamingFetchAdapter.test.ts b/test/unit/api/streamingFetchAdapter.test.ts new file mode 100644 index 00000000..0ba8437b --- /dev/null +++ b/test/unit/api/streamingFetchAdapter.test.ts @@ -0,0 +1,220 @@ +import { type AxiosInstance, type AxiosResponse } from "axios"; +import { type ReaderLike } from "eventsource"; +import { EventEmitter } from "node:events"; +import { type IncomingMessage } from "node:http"; +import { describe, it, expect, vi } from "vitest"; + +import { createStreamingFetchAdapter } from "@/api/streamingFetchAdapter"; + +const TEST_URL = "https://example.com/api"; + +describe("createStreamingFetchAdapter", () => { + describe("Request Handling", () => { + it("passes URL, signal, and responseType to axios", async () => { + const mockAxios = createAxiosMock(); + const mockStream = createMockStream(); + setupAxiosResponse(mockAxios, 200, {}, mockStream); + + const adapter = createStreamingFetchAdapter(mockAxios); + const signal = new AbortController().signal; + + await adapter(TEST_URL, { signal }); + + expect(mockAxios.request).toHaveBeenCalledWith({ + url: TEST_URL, + signal, // correctly passes signal + headers: {}, + responseType: "stream", + validateStatus: expect.any(Function), + }); + }); + + it("applies headers in correct precedence order (config overrides init)", async () => { + const mockAxios = createAxiosMock(); + const mockStream = createMockStream(); + setupAxiosResponse(mockAxios, 200, {}, mockStream); + + // Test 1: No config headers, only init headers + const adapter1 = createStreamingFetchAdapter(mockAxios); + await adapter1(TEST_URL, { + headers: { "X-Init": "init-value" }, + }); + + expect(mockAxios.request).toHaveBeenCalledWith( + expect.objectContaining({ + headers: { "X-Init": "init-value" }, + }), + ); + + // Test 2: Config headers merge with init headers + const adapter2 = createStreamingFetchAdapter(mockAxios, { + "X-Config": "config-value", + }); + await adapter2(TEST_URL, { + headers: { "X-Init": "init-value" }, + }); + + expect(mockAxios.request).toHaveBeenCalledWith( + expect.objectContaining({ + headers: { + "X-Init": "init-value", + "X-Config": "config-value", + }, + }), + ); + + // Test 3: Config headers override init headers + const adapter3 = createStreamingFetchAdapter(mockAxios, { + "X-Header": "config-value", + }); + await adapter3(TEST_URL, { + headers: { "X-Header": "init-value" }, + }); + + expect(mockAxios.request).toHaveBeenCalledWith( + expect.objectContaining({ + headers: { "X-Header": "config-value" }, + }), + ); + }); + }); + + describe("Response Properties", () => { + it("returns response with correct properties", async () => { + const mockAxios = createAxiosMock(); + const mockStream = createMockStream(); + setupAxiosResponse( + mockAxios, + 200, + { "content-type": "text/event-stream" }, + mockStream, + ); + + const adapter = createStreamingFetchAdapter(mockAxios); + const response = await adapter(TEST_URL); + + expect(response.url).toBe(TEST_URL); + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("text/event-stream"); + // Headers are lowercased when we retrieve them + expect(response.headers.get("CoNtEnT-TyPe")).toBe("text/event-stream"); + expect(response.body?.getReader).toBeDefined(); + }); + + it("detects redirected requests", async () => { + const mockAxios = createAxiosMock(); + const mockStream = createMockStream(); + const mockResponse = { + status: 200, + headers: {}, + data: mockStream, + request: { + res: { + responseUrl: "https://redirect.com/api", + }, + }, + } as AxiosResponse; + vi.mocked(mockAxios.request).mockResolvedValue(mockResponse); + + const adapter = createStreamingFetchAdapter(mockAxios); + const response = await adapter(TEST_URL); + + expect(response.redirected).toBe(true); + }); + }); + + describe("Stream Handling", () => { + it("enqueues data chunks from stream", async () => { + const { mockStream, reader } = await setupReaderTest(); + + const chunk1 = Buffer.from("data1"); + const chunk2 = Buffer.from("data2"); + mockStream.emit("data", chunk1); + mockStream.emit("data", chunk2); + mockStream.emit("end"); + + const result1 = await reader.read(); + expect(result1.value).toEqual(chunk1); + expect(result1.done).toBe(false); + + const result2 = await reader.read(); + expect(result2.value).toEqual(chunk2); + expect(result2.done).toBe(false); + + const result3 = await reader.read(); + // Closed after end + expect(result3.done).toBe(true); + }); + + it("propagates stream errors", async () => { + const { mockStream, reader } = await setupReaderTest(); + + const error = new Error("Stream error"); + mockStream.emit("error", error); + + await expect(reader.read()).rejects.toThrow("Stream error"); + }); + + it("handles errors after stream is closed", async () => { + const { mockStream, reader } = await setupReaderTest(); + + mockStream.emit("end"); + await reader.read(); + + // Emit events after stream is closed - should not throw + expect(() => mockStream.emit("data", Buffer.from("late"))).not.toThrow(); + expect(() => mockStream.emit("end")).not.toThrow(); + }); + + it("destroys stream on cancel", async () => { + const { mockStream, reader } = await setupReaderTest(); + + await reader.cancel(); + + expect(mockStream.destroy).toHaveBeenCalled(); + }); + }); +}); + +function createAxiosMock(): AxiosInstance { + return { + request: vi.fn(), + } as unknown as AxiosInstance; +} + +function createMockStream(): IncomingMessage { + const stream = new EventEmitter() as IncomingMessage; + stream.destroy = vi.fn(); + return stream; +} + +function setupAxiosResponse( + axios: AxiosInstance, + status: number, + headers: Record, + stream: IncomingMessage, +): void { + vi.mocked(axios.request).mockResolvedValue({ + status, + headers, + data: stream, + }); +} + +async function setupReaderTest(): Promise<{ + mockStream: IncomingMessage; + reader: ReaderLike | ReadableStreamDefaultReader>; +}> { + const mockAxios = createAxiosMock(); + const mockStream = createMockStream(); + setupAxiosResponse(mockAxios, 200, {}, mockStream); + + const adapter = createStreamingFetchAdapter(mockAxios); + const response = await adapter(TEST_URL); + const reader = response.body?.getReader(); + if (reader === undefined) { + throw new Error("Reader is undefined"); + } + + return { mockStream, reader }; +} diff --git a/test/unit/core/cliManager.test.ts b/test/unit/core/cliManager.test.ts index f2a2c2e5..d4f16c87 100644 --- a/test/unit/core/cliManager.test.ts +++ b/test/unit/core/cliManager.test.ts @@ -546,7 +546,8 @@ describe("CliManager", () => { expect(files.find((file) => file.includes(".asc"))).toBeUndefined(); }); - it.each([ + type SignatureErrorTestCase = [status: number, message: string]; + it.each([ [404, "Signature not found"], [500, "Failed to download signature"], ])("allows skipping verification on %i", async (status, message) => { @@ -558,7 +559,7 @@ describe("CliManager", () => { expect(pgp.verifySignature).not.toHaveBeenCalled(); }); - it.each([ + it.each([ [404, "Signature not found"], [500, "Failed to download signature"], ])( diff --git a/test/unit/logging/utils.test.ts b/test/unit/logging/utils.test.ts index 4d0f71eb..989a23e1 100644 --- a/test/unit/logging/utils.test.ts +++ b/test/unit/logging/utils.test.ts @@ -23,7 +23,8 @@ describe("Logging utils", () => { }); describe("sizeOf", () => { - it.each([ + type SizeOfTestCase = [data: unknown, bytes: number | undefined]; + it.each([ // Primitives return a fixed value [null, 0], [undefined, 0], diff --git a/test/unit/websocket/sseConnection.test.ts b/test/unit/websocket/sseConnection.test.ts new file mode 100644 index 00000000..61cfce4d --- /dev/null +++ b/test/unit/websocket/sseConnection.test.ts @@ -0,0 +1,356 @@ +import axios, { type AxiosInstance } from "axios"; +import { type ServerSentEvent } from "coder/site/src/api/typesGenerated"; +import { type WebSocketEventType } from "coder/site/src/utils/OneWayWebSocket"; +import { EventSource } from "eventsource"; +import { describe, it, expect, vi } from "vitest"; +import { type CloseEvent, type ErrorEvent } from "ws"; + +import { type Logger } from "@/logging/logger"; +import { type ParsedMessageEvent } from "@/websocket/eventStreamConnection"; +import { SseConnection } from "@/websocket/sseConnection"; + +import { createMockLogger } from "../../mocks/testHelpers"; + +const TEST_URL = "https://coder.example.com"; +const API_ROUTE = "/api/v2/workspaces/123/watch"; + +vi.mock("eventsource"); +vi.mock("axios"); + +vi.mock("@/api/streamingFetchAdapter", () => ({ + createStreamingFetchAdapter: vi.fn(() => fetch), +})); + +describe("SseConnection", () => { + describe("URL Building", () => { + type UrlBuildingTestCase = [ + searchParams: Record | URLSearchParams | undefined, + expectedUrl: string, + ]; + it.each([ + [undefined, `${TEST_URL}${API_ROUTE}`], + [ + { follow: "true", after: "123" }, + `${TEST_URL}${API_ROUTE}?follow=true&after=123`, + ], + [new URLSearchParams({ foo: "bar" }), `${TEST_URL}${API_ROUTE}?foo=bar`], + ])("constructs URL with %s search params", (searchParams, expectedUrl) => { + const mockAxios = axios.create(); + const mockLogger = createMockLogger(); + const mockES = createMockEventSource(); + setupEventSourceMock(mockES); + + const connection = new SseConnection({ + location: { protocol: "https:", host: "coder.example.com" }, + apiRoute: API_ROUTE, + searchParams, + axiosInstance: mockAxios, + logger: mockLogger, + }); + expect(connection.url).toBe(expectedUrl); + }); + }); + + describe("Event Handling", () => { + it("fires open event and supports multiple listeners", async () => { + const mockES = createMockEventSource({ + addEventListener: vi.fn((event, handler) => { + if (event === "open") { + setImmediate(() => handler(new Event("open"))); + } + }), + }); + setupEventSourceMock(mockES); + + const mockAxios = axios.create(); + const mockLogger = createMockLogger(); + const connection = createConnection(mockAxios, mockLogger); + const events1: object[] = []; + const events2: object[] = []; + connection.addEventListener("open", (event) => events1.push(event)); + connection.addEventListener("open", (event) => events2.push(event)); + + await waitForNextTick(); + expect(events1).toEqual([{}]); + expect(events2).toEqual([{}]); + }); + + it("fires message event with parsed JSON and handles parse errors", async () => { + const testData = { type: "data", workspace: { status: "running" } }; + const mockES = createMockEventSource({ + addEventListener: vi.fn((event, handler) => { + if (event === "data") { + setImmediate(() => { + // Send valid JSON + handler( + new MessageEvent("data", { data: JSON.stringify(testData) }), + ); + // Send invalid JSON + handler(new MessageEvent("data", { data: "not-valid-json" })); + }); + } + }), + }); + setupEventSourceMock(mockES); + + const mockAxios = axios.create(); + const mockLogger = createMockLogger(); + const connection = createConnection(mockAxios, mockLogger); + const events: ParsedMessageEvent[] = []; + connection.addEventListener("message", (event) => events.push(event)); + + await waitForNextTick(); + expect(events).toEqual([ + { + sourceEvent: { data: JSON.stringify(testData) }, + parsedMessage: { type: "data", data: testData }, + parseError: undefined, + }, + { + sourceEvent: { data: "not-valid-json" }, + parsedMessage: undefined, + parseError: expect.any(Error), + }, + ]); + }); + + it("fires error event when connection fails", async () => { + const mockES = createMockEventSource({ + addEventListener: vi.fn((event, handler) => { + if (event === "error") { + const error = { + message: "Connection failed", + error: new Error("Network error"), + }; + setImmediate(() => handler(error)); + } + }), + }); + setupEventSourceMock(mockES); + + const mockAxios = axios.create(); + const mockLogger = createMockLogger(); + const connection = createConnection(mockAxios, mockLogger); + const events: ErrorEvent[] = []; + connection.addEventListener("error", (event) => events.push(event)); + + await waitForNextTick(); + expect(events).toEqual([ + { + error: expect.any(Error), + message: "Connection failed", + }, + ]); + }); + + it("fires close event when connection closes on error", async () => { + const mockES = createMockEventSource({ + addEventListener: vi.fn((event, handler) => { + if (event === "error") { + setImmediate(() => { + // A bit hacky but readyState is a readonly property so we have to override that here + const esWithReadyState = mockES as { readyState: number }; + // Simulate EventSource behavior: state transitions to CLOSED when error occurs + esWithReadyState.readyState = EventSource.CLOSED; + handler(new Event("error")); + }); + } + }), + }); + setupEventSourceMock(mockES); + + const mockAxios = axios.create(); + const mockLogger = createMockLogger(); + const connection = createConnection(mockAxios, mockLogger); + const events: CloseEvent[] = []; + connection.addEventListener("close", (event) => events.push(event)); + + await waitForNextTick(); + expect(events).toEqual([ + { + code: 1006, + reason: "Connection lost", + wasClean: false, + }, + ]); + }); + }); + + describe("Event Listener Management", () => { + it("removes event listener without affecting others", async () => { + const data = '{"test": true}'; + const mockES = createMockEventSource({ + addEventListener: vi.fn((event, handler) => { + if (event === "data") { + setImmediate(() => handler(new MessageEvent("data", { data }))); + } + }), + }); + setupEventSourceMock(mockES); + + const mockAxios = axios.create(); + const mockLogger = createMockLogger(); + const connection = createConnection(mockAxios, mockLogger); + const events: ParsedMessageEvent[] = []; + + const removedHandler = () => { + throw new Error("Removed handler should not have been called!"); + }; + const keptHandler = (event: ParsedMessageEvent) => + events.push(event); + + connection.addEventListener("message", removedHandler); + connection.addEventListener("message", keptHandler); + connection.removeEventListener("message", removedHandler); + + await waitForNextTick(); + // One message event + expect(events).toEqual([ + { + parseError: undefined, + parsedMessage: { + data: JSON.parse(data), + type: "data", + }, + sourceEvent: { data }, + }, + ]); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); + }); + + describe("Close Handling", () => { + type CloseHandlingTestCase = [ + code: number | undefined, + reason: string | undefined, + closeEvent: Omit, + ]; + it.each([ + [ + undefined, + undefined, + { code: 1000, reason: "Normal closure", wasClean: true }, + ], + [ + 4000, + "Custom close", + { code: 4000, reason: "Custom close", wasClean: true }, + ], + ])( + "closes EventSource with code '%s' and reason '%s'", + (code, reason, closeEvent) => { + const mockES = createMockEventSource(); + setupEventSourceMock(mockES); + + const mockAxios = axios.create(); + const mockLogger = createMockLogger(); + const connection = createConnection(mockAxios, mockLogger); + const events: CloseEvent[] = []; + connection.addEventListener("close", (event) => events.push(event)); + connection.addEventListener("open", () => {}); + + connection.close(code, reason); + expect(mockES.close).toHaveBeenCalled(); + expect(events).toEqual([closeEvent]); + }, + ); + }); + + describe("Callback Error Handling", () => { + type CallbackErrorTestCase = [ + sseEvent: WebSocketEventType, + eventData: Event | MessageEvent, + ]; + it.each([ + ["open", new Event("open")], + ["message", new MessageEvent("data", { data: '{"test": true}' })], + ["error", new Event("error")], + ])( + "logs error and continues when %s callback throws", + async (sseEvent, eventData) => { + const mockES = createMockEventSource({ + addEventListener: vi.fn((event, handler) => { + // All SSE events are streaming data and attach a listener on the "data" type in the EventSource + const esEvent = sseEvent === "message" ? "data" : sseEvent; + if (event === esEvent) { + setImmediate(() => handler(eventData)); + } + }), + }); + setupEventSourceMock(mockES); + + const mockAxios = axios.create(); + const mockLogger = createMockLogger(); + const connection = createConnection(mockAxios, mockLogger); + const events: unknown[] = []; + + connection.addEventListener(sseEvent, () => { + throw new Error("Handler error"); + }); + connection.addEventListener(sseEvent, (event: unknown) => + events.push(event), + ); + + await waitForNextTick(); + expect(events).toHaveLength(1); + expect(mockLogger.error).toHaveBeenCalledWith( + `Error in SSE ${sseEvent} callback:`, + expect.any(Error), + ); + }, + ); + + it("completes cleanup when close callback throws", () => { + const mockES = createMockEventSource(); + setupEventSourceMock(mockES); + + const mockAxios = axios.create(); + const mockLogger = createMockLogger(); + const connection = createConnection(mockAxios, mockLogger); + connection.addEventListener("close", () => { + throw new Error("Handler error"); + }); + + connection.close(); + + expect(mockES.close).toHaveBeenCalled(); + expect(mockLogger.error).toHaveBeenCalledWith( + "Error in SSE close callback:", + expect.any(Error), + ); + }); + }); +}); + +function createConnection( + mockAxios: AxiosInstance, + mockLogger: Logger, +): SseConnection { + return new SseConnection({ + location: { protocol: "https:", host: "coder.example.com" }, + apiRoute: API_ROUTE, + axiosInstance: mockAxios, + logger: mockLogger, + }); +} + +function createMockEventSource( + overrides?: Partial, +): Partial { + return { + url: `${TEST_URL}${API_ROUTE}`, + readyState: EventSource.CONNECTING, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + close: vi.fn(), + ...overrides, + }; +} + +function setupEventSourceMock(es: Partial): void { + vi.mocked(EventSource).mockImplementation(() => es as EventSource); +} + +function waitForNextTick(): Promise { + return new Promise((resolve) => setImmediate(resolve)); +} diff --git a/vitest.config.ts b/vitest.config.ts index 40c5f958..a3fcd089 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,4 +1,4 @@ -import path from "path"; +import path from "node:path"; import { defineConfig } from "vitest/config"; export default defineConfig({ From e64350fd448e63ceb982e1894668816bd091b42e Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 21 Oct 2025 12:33:36 +0300 Subject: [PATCH 29/45] Update CLAUDE.md (#628) --- CLAUDE.md | 51 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 04c75edc..6aa4c61d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,40 @@ # Coder Extension Development Guidelines +## Working Style + +You're an experienced, pragmatic engineer. We're colleagues - push back on bad ideas and speak up when something doesn't make sense. Honesty over agreeableness. + +- Simple solutions over clever ones. Readability is a primary concern. +- YAGNI - don't add features we don't need right now +- Make the smallest reasonable changes to achieve the goal +- Reduce code duplication, even if it takes extra effort +- Match the style of surrounding code - consistency within a file matters +- Fix bugs immediately when you find them + +## Naming and Comments + +Names should describe what code does, not how it's implemented. + +Comments explain what code does or why it exists: + +- Never add comments about what used to be there or how things changed +- Never use temporal terms like "new", "improved", "refactored", "legacy" +- Code should be evergreen - describe it as it is +- Do not add comments when you can instead use proper variable/function naming + +## Testing and Debugging + +- Tests must comprehensively cover functionality +- Never mock behavior in end-to-end tests - use real data +- Mock as little as possible in unit tests - try to use real data +- Find root causes, not symptoms. Read error messages carefully before attempting fixes. + +## Version Control + +- Commit frequently throughout development +- Never skip or disable pre-commit hooks +- Check `git status` before using `git add` + ## Build and Test Commands - Build: `yarn build` @@ -8,20 +43,20 @@ - Lint: `yarn lint` - Lint with auto-fix: `yarn lint:fix` - Run all tests: `yarn test` -- Run specific test: `vitest ./src/filename.test.ts` -- CI test mode: `yarn test:ci` +- Unit tests: `yarn test:ci` - Integration tests: `yarn test:integration` +- Run specific unit test: `yarn test:ci ./test/unit/filename.test.ts` +- Run specific integration test: `yarn test:integration ./test/integration/filename.test.ts` -## Code Style Guidelines +## Code Style - TypeScript with strict typing -- No semicolons (see `.prettierrc`) -- Trailing commas for all multi-line lists -- 120 character line width +- Use Prettier for code formatting and ESLint for code linting - Use ES6 features (arrow functions, destructuring, etc.) - Use `const` by default; `let` only when necessary +- Never use `any`, and use exact types when you can - Prefix unused variables with underscore (e.g., `_unused`) -- Sort imports alphabetically in groups: external → parent → sibling - Error handling: wrap and type errors appropriately - Use async/await for promises, avoid explicit Promise construction where possible -- Test files must be named `*.test.ts` and use Vitest +- Unit test files must be named `*.test.ts` and use Vitest, they should be placed in `./test/unit/` +- Never disable ESLint rules without user approval From ee0a964ba1cc6d353cd27fac453197fec2dcfd01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 10:35:20 +0300 Subject: [PATCH 30/45] chore(deps): bump vite from 7.1.5 to 7.1.11 (#631) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index a067635f..f951b225 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8775,9 +8775,9 @@ vite-node@3.2.4: vite "^5.0.0 || ^6.0.0 || ^7.0.0-0" "vite@^5.0.0 || ^6.0.0 || ^7.0.0-0": - version "7.1.5" - resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.5.tgz#4dbcb48c6313116689be540466fc80faa377be38" - integrity sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ== + version "7.1.11" + resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.11.tgz#4d006746112fee056df64985191e846ebfb6007e" + integrity sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg== dependencies: esbuild "^0.25.0" fdir "^6.5.0" From b9be79b7daf38a2c81e72c785b63451f9f72773c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 10:40:02 +0300 Subject: [PATCH 31/45] chore(deps): bump openpgp from 6.2.0 to 6.2.2 (#611) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 02a6ddc3..95da87d2 100644 --- a/package.json +++ b/package.json @@ -349,7 +349,7 @@ "find-process": "https://github.com/coder/find-process#fix/sequoia-compat", "jsonc-parser": "^3.3.1", "node-forge": "^1.3.1", - "openpgp": "^6.2.0", + "openpgp": "^6.2.2", "pretty-bytes": "^7.0.0", "proxy-agent": "^6.5.0", "semver": "^7.7.1", diff --git a/yarn.lock b/yarn.lock index f951b225..814d8e9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6056,10 +6056,10 @@ open@^10.1.0: is-inside-container "^1.0.0" wsl-utils "^0.1.0" -openpgp@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-6.2.0.tgz#f9ce7b4fa298c9d1c4c51f8d1bd0d6cb00372144" - integrity sha512-zKbgazxMeGrTqUEWicKufbdcjv2E0om3YVxw+I3hRykp8ODp+yQOJIDqIr1UXJjP8vR2fky3bNQwYoQXyFkYMA== +openpgp@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-6.2.2.tgz#329f4fab075f9746a94e584df8cfbda70a0dcaf3" + integrity sha512-P/dyEqQ3gfwOCo+xsqffzXjmUhGn4AZTOJ1LCcN21S23vAk+EAvMJOQTsb/C8krL6GjOSBxqGYckhik7+hneNw== optionator@^0.8.3: version "0.8.3" From f543fa4bb9bad01b9b98733024983f0e56b08b87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 10:42:27 +0300 Subject: [PATCH 32/45] chore(deps): bump ws from 8.18.2 to 8.18.3 (#610) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 95da87d2..218849a5 100644 --- a/package.json +++ b/package.json @@ -354,7 +354,7 @@ "proxy-agent": "^6.5.0", "semver": "^7.7.1", "ua-parser-js": "1.0.40", - "ws": "^8.18.2", + "ws": "^8.18.3", "zod": "^3.25.65" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 814d8e9c..a18ff730 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9113,10 +9113,10 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" -ws@^8.18.2: - version "8.18.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a" - integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ== +ws@^8.18.3: + version "8.18.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== wsl-utils@^0.1.0: version "0.1.0" From 591a74bcc5b0349734673ffee19836d3fc6b8402 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 10:45:22 +0300 Subject: [PATCH 33/45] chore(deps-dev): bump typescript from 5.9.2 to 5.9.3 (#612) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 218849a5..3e8c9b8d 100644 --- a/package.json +++ b/package.json @@ -389,7 +389,7 @@ "nyc": "^17.1.0", "prettier": "^3.5.3", "ts-loader": "^9.5.1", - "typescript": "^5.9.2", + "typescript": "^5.9.3", "utf-8-validate": "^6.0.5", "vitest": "^3.2.4", "vscode-test": "^1.5.0", diff --git a/yarn.lock b/yarn.lock index a18ff730..c8d1ff6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8490,10 +8490,10 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^5.9.2: - version "5.9.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" - integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== +typescript@^5.9.3: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== ua-parser-js@1.0.40: version "1.0.40" From d5c65e852f89f27fc64d0a93db03d5870ae06131 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 10:49:50 +0300 Subject: [PATCH 34/45] chore(deps-dev): bump memfs from 4.47.0 to 4.49.0 (#622) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 3e8c9b8d..1c6abd4e 100644 --- a/package.json +++ b/package.json @@ -385,7 +385,7 @@ "glob": "^10.4.2", "jsonc-eslint-parser": "^2.4.0", "markdown-eslint-parser": "^1.2.1", - "memfs": "^4.47.0", + "memfs": "^4.49.0", "nyc": "^17.1.0", "prettier": "^3.5.3", "ts-loader": "^9.5.1", diff --git a/yarn.lock b/yarn.lock index c8d1ff6a..47f44b96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5672,10 +5672,10 @@ mdurl@^2.0.0: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== -memfs@^4.47.0: - version "4.47.0" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.47.0.tgz#410291da6dcce89a0d6c9cab23b135231a5ed44c" - integrity sha512-Xey8IZA57tfotV/TN4d6BmccQuhFP+CqRiI7TTNdipZdZBzF2WnzUcH//Cudw6X4zJiUbo/LTuU/HPA/iC/pNg== +memfs@^4.49.0: + version "4.49.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.49.0.tgz#bc35069570d41a31c62e31f1a6ec6057a8ea82f0" + integrity sha512-L9uC9vGuc4xFybbdOpRLoOAOq1YEBBsocCs5NVW32DfU+CZWWIn3OVF+lB8Gp4ttBVSMazwrTrjv8ussX/e3VQ== dependencies: "@jsonjoy.com/json-pack" "^1.11.0" "@jsonjoy.com/util" "^1.9.0" From 1bb05c5c7aca8516843c256556f92375bf0a8c32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:17:52 +0300 Subject: [PATCH 35/45] chore(deps): bump semver from 7.7.1 to 7.7.3 (#621) --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 1c6abd4e..363119d5 100644 --- a/package.json +++ b/package.json @@ -338,7 +338,7 @@ "onUri" ], "resolutions": { - "semver": "7.7.1", + "semver": "7.7.3", "trim": "0.0.3", "word-wrap": "1.2.5" }, @@ -352,7 +352,7 @@ "openpgp": "^6.2.2", "pretty-bytes": "^7.0.0", "proxy-agent": "^6.5.0", - "semver": "^7.7.1", + "semver": "^7.7.3", "ua-parser-js": "1.0.40", "ws": "^8.18.3", "zod": "^3.25.65" diff --git a/yarn.lock b/yarn.lock index 47f44b96..3e38b6a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7462,10 +7462,10 @@ secretlint@^10.1.1: globby "^14.1.0" read-pkg "^9.0.1" -semver@7.7.1, semver@^5.1.0, semver@^5.5.0, semver@^6.0.0, semver@^6.1.2, semver@^6.3.1, semver@^7.3.4, semver@^7.3.5, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2, semver@^7.7.1, semver@^7.7.2: - version "7.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" - integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== +semver@7.7.3, semver@^5.1.0, semver@^5.5.0, semver@^6.0.0, semver@^6.1.2, semver@^6.3.1, semver@^7.3.4, semver@^7.3.5, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2, semver@^7.7.1, semver@^7.7.2, semver@^7.7.3: + version "7.7.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== serialize-javascript@^6.0.2: version "6.0.2" From 9c884dfd728b47174b297008740ddd66767b4bb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:20:48 +0300 Subject: [PATCH 36/45] chore(deps): bump actions/setup-node from 5 to 6 (#630) --- .github/workflows/ci.yaml | 4 ++-- .github/workflows/release.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 59a03e0a..87a03723 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: "22" @@ -34,7 +34,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: "22" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 27214dcc..28f8fdf0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: "22" From 930d54329f26ce13793c59f8521d8281be8b55ef Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Thu, 23 Oct 2025 02:35:44 +0300 Subject: [PATCH 37/45] Automatically publish extension and add pre-release version (#624) - Automatically publish the pre-release when pushing a `v*-pre` tag to `main`. - Automatically publish the stable release when pushing a `v*` tag to `main`. - Package VSIX on every PR and merge to main. For PRs, it is retained for 7 days, but for main it's retained indefinitely. - Skips publishing if the secret for that platform is not set. Closes #97 --- .github/workflows/ci.yaml | 59 ++++++++++- .github/workflows/pre-release.yaml | 78 ++++++++++++++ .github/workflows/publish-extension.yaml | 125 +++++++++++++++++++++++ .github/workflows/release.yaml | 67 ++++++++++-- 4 files changed, 317 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/pre-release.yaml create mode 100644 .github/workflows/publish-extension.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 87a03723..a878f9f2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,4 +1,4 @@ -name: ci +name: CI on: push: @@ -11,6 +11,7 @@ on: jobs: lint: + name: Lint runs-on: ubuntu-22.04 steps: @@ -19,6 +20,7 @@ jobs: - uses: actions/setup-node@v6 with: node-version: "22" + cache: "yarn" - run: yarn @@ -29,6 +31,7 @@ jobs: - run: yarn build test: + name: Test runs-on: ubuntu-22.04 steps: @@ -37,7 +40,61 @@ jobs: - uses: actions/setup-node@v6 with: node-version: "22" + cache: "yarn" - run: yarn - run: yarn test:ci + + package: + name: Package + runs-on: ubuntu-22.04 + needs: [lint, test] + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v6 + with: + node-version: "22" + cache: "yarn" + + - name: Install dependencies + run: | + yarn + npm install -g @vscode/vsce + + - name: Get version from package.json + id: version + run: | + VERSION=$(node -e "console.log(require('./package.json').version)") + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Version: $VERSION" + + - name: Setup package path + id: setup + run: | + EXTENSION_NAME=$(node -e "console.log(require('./package.json').name)") + # Add commit SHA for CI builds + SHORT_SHA=$(git rev-parse --short HEAD) + PACKAGE_NAME="${EXTENSION_NAME}-${{ steps.version.outputs.version }}-${SHORT_SHA}.vsix" + echo "packageName=$PACKAGE_NAME" >> $GITHUB_OUTPUT + + - name: Package extension + run: vsce package --out "${{ steps.setup.outputs.packageName }}" + + - name: Upload artifact (PR) + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: extension-pr-${{ github.event.pull_request.number }} + path: ${{ steps.setup.outputs.packageName }} + if-no-files-found: error + retention-days: 7 + + - name: Upload artifact (main) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v4 + with: + name: extension-main-${{ github.sha }} + path: ${{ steps.setup.outputs.packageName }} + if-no-files-found: error diff --git a/.github/workflows/pre-release.yaml b/.github/workflows/pre-release.yaml new file mode 100644 index 00000000..61761700 --- /dev/null +++ b/.github/workflows/pre-release.yaml @@ -0,0 +1,78 @@ +name: Pre-Release +on: + push: + tags: + - "v*-pre" + +permissions: + # Required to publish a release + contents: write + pull-requests: read + +jobs: + package: + name: Package + runs-on: ubuntu-22.04 + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v6 + with: + node-version: "22" + + - name: Extract version from tag + id: version + run: | + # Extract version from tag (remove 'v' prefix and '-pre' suffix) + TAG_NAME=${GITHUB_REF#refs/tags/v} + VERSION=${TAG_NAME%-pre} + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Pre-release version: $VERSION" + + - name: Validate version matches package.json + run: | + TAG_VERSION="${{ steps.version.outputs.version }}" + PACKAGE_VERSION=$(node -e "console.log(require('./package.json').version)") + + if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then + echo "Error: Tag version ($TAG_VERSION) does not match package.json version ($PACKAGE_VERSION)" + echo "Please ensure the tag version matches the version in package.json" + exit 1 + fi + + echo "Version validation successful: $TAG_VERSION" + + - name: Install dependencies + run: | + yarn + npm install -g @vscode/vsce + + - name: Setup package path + id: setup + run: | + EXTENSION_NAME=$(node -e "console.log(require('./package.json').name)") + PACKAGE_NAME="${EXTENSION_NAME}-${{ steps.version.outputs.version }}-pre.vsix" + echo "packageName=$PACKAGE_NAME" >> $GITHUB_OUTPUT + + - name: Package extension + run: vsce package --pre-release --out "${{ steps.setup.outputs.packageName }}" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: extension-${{ steps.version.outputs.version }} + path: ${{ steps.setup.outputs.packageName }} + if-no-files-found: error + + publish: + name: Publish Extension and Create Pre-Release + needs: package + uses: ./.github/workflows/publish-extension.yaml + with: + version: ${{ needs.package.outputs.version }} + isPreRelease: true + secrets: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + OVSX_PAT: ${{ secrets.OVSX_PAT }} diff --git a/.github/workflows/publish-extension.yaml b/.github/workflows/publish-extension.yaml new file mode 100644 index 00000000..cf93d6ba --- /dev/null +++ b/.github/workflows/publish-extension.yaml @@ -0,0 +1,125 @@ +name: Publish Extension + +on: + workflow_call: + inputs: + version: + required: true + type: string + description: "Version to publish" + isPreRelease: + required: false + type: boolean + default: false + description: "Whether this is a pre-release" + secrets: + VSCE_PAT: + required: false + OVSX_PAT: + required: false + +jobs: + setup: + name: Setup + runs-on: ubuntu-22.04 + outputs: + packageName: ${{ steps.package.outputs.packageName }} + hasVscePat: ${{ steps.check-secrets.outputs.hasVscePat }} + hasOvsxPat: ${{ steps.check-secrets.outputs.hasOvsxPat }} + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v6 + with: + node-version: "22" + + - name: Construct package name + id: package + run: | + EXTENSION_NAME=$(node -e "console.log(require('./package.json').name)") + if [ "${{ inputs.isPreRelease }}" = "true" ]; then + PACKAGE_NAME="${EXTENSION_NAME}-${{ inputs.version }}-pre.vsix" + else + PACKAGE_NAME="${EXTENSION_NAME}-${{ inputs.version }}.vsix" + fi + echo "packageName=$PACKAGE_NAME" >> $GITHUB_OUTPUT + echo "Package name: $PACKAGE_NAME" + + - name: Check secrets + id: check-secrets + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + OVSX_PAT: ${{ secrets.OVSX_PAT }} + run: | + echo "hasVscePat=$([ -n "$VSCE_PAT" ] && echo true || echo false)" >> $GITHUB_OUTPUT + echo "hasOvsxPat=$([ -n "$OVSX_PAT" ] && echo true || echo false)" >> $GITHUB_OUTPUT + + publishMS: + name: Publish to VS Marketplace + needs: setup + runs-on: ubuntu-22.04 + if: ${{ needs.setup.outputs.hasVscePat == 'true' }} + steps: + - uses: actions/setup-node@v6 + with: + node-version: "22" + + - name: Install vsce + run: npm install -g @vscode/vsce + + - uses: actions/download-artifact@v5 + with: + name: extension-${{ inputs.version }} + + - name: Publish to VS Marketplace + run: | + echo "Publishing version ${{ inputs.version }} to VS Marketplace" + if [ "${{ inputs.isPreRelease }}" = "true" ]; then + vsce publish --pre-release --packagePath "./${{ needs.setup.outputs.packageName }}" -p ${{ secrets.VSCE_PAT }} + else + vsce publish --packagePath "./${{ needs.setup.outputs.packageName }}" -p ${{ secrets.VSCE_PAT }} + fi + + publishOVSX: + name: Publish to Open VSX + needs: setup + runs-on: ubuntu-22.04 + if: ${{ needs.setup.outputs.hasOvsxPat == 'true' }} + steps: + - uses: actions/setup-node@v6 + with: + node-version: "22" + + - name: Install ovsx + run: npm install -g ovsx + + - uses: actions/download-artifact@v5 + with: + name: extension-${{ inputs.version }} + + - name: Publish to Open VSX + run: | + echo "Publishing version ${{ inputs.version }} to Open VSX" + if [ "${{ inputs.isPreRelease }}" = "true" ]; then + ovsx publish "./${{ needs.setup.outputs.packageName }}" --pre-release -p ${{ secrets.OVSX_PAT }} + else + ovsx publish "./${{ needs.setup.outputs.packageName }}" -p ${{ secrets.OVSX_PAT }} + fi + + publishGH: + name: Create GitHub ${{ inputs.isPreRelease && 'Pre-' || '' }}Release + needs: setup + runs-on: ubuntu-22.04 + steps: + - uses: actions/download-artifact@v5 + with: + name: extension-${{ inputs.version }} + + - name: Create ${{ inputs.isPreRelease && 'Pre-' || '' }}Release + uses: marvinpinto/action-automatic-releases@latest + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + prerelease: ${{ inputs.isPreRelease }} + draft: true + title: "${{ inputs.isPreRelease && 'Pre-' || '' }}Release v${{ inputs.version }}" + files: ${{ needs.setup.outputs.packageName }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 28f8fdf0..51d9ff97 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,18 +1,21 @@ +name: Release on: push: tags: - "v*" - -name: release + - "!v*-pre" permissions: # Required to publish a release contents: write - pull-requests: "read" + pull-requests: read jobs: package: + name: Package runs-on: ubuntu-22.04 + outputs: + version: ${{ steps.version.outputs.version }} steps: - uses: actions/checkout@v5 @@ -20,14 +23,56 @@ jobs: with: node-version: "22" - - run: yarn + - name: Extract version from tag + id: version + run: | + # Extract version from tag (remove 'v' prefix) + VERSION=${GITHUB_REF#refs/tags/v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Release version: $VERSION" + + - name: Validate version matches package.json + run: | + TAG_VERSION="${{ steps.version.outputs.version }}" + PACKAGE_VERSION=$(node -e "console.log(require('./package.json').version)") + + if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then + echo "Error: Tag version ($TAG_VERSION) does not match package.json version ($PACKAGE_VERSION)" + echo "Please ensure the tag version matches the version in package.json" + exit 1 + fi + + echo "Version validation successful: $TAG_VERSION" - - run: npx @vscode/vsce package + - name: Install dependencies + run: | + yarn + npm install -g @vscode/vsce - - uses: "marvinpinto/action-automatic-releases@latest" + - name: Setup package path + id: setup + run: | + EXTENSION_NAME=$(node -e "console.log(require('./package.json').name)") + PACKAGE_NAME="${EXTENSION_NAME}-${{ steps.version.outputs.version }}.vsix" + echo "packageName=$PACKAGE_NAME" >> $GITHUB_OUTPUT + + - name: Package extension + run: vsce package --out "${{ steps.setup.outputs.packageName }}" + + - name: Upload artifact + uses: actions/upload-artifact@v4 with: - repo_token: "${{ secrets.GITHUB_TOKEN }}" - prerelease: false - draft: true - files: | - *.vsix + name: extension-${{ steps.version.outputs.version }} + path: ${{ steps.setup.outputs.packageName }} + if-no-files-found: error + + publish: + name: Publish Extension and Create Release + needs: package + uses: ./.github/workflows/publish-extension.yaml + with: + version: ${{ needs.package.outputs.version }} + isPreRelease: false + secrets: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + OVSX_PAT: ${{ secrets.OVSX_PAT }} From c1206b2497e20cbde7ccde895c948f109c4789fc Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Thu, 23 Oct 2025 13:10:47 +0300 Subject: [PATCH 38/45] v1.11.3 (#632) --- CHANGELOG.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef80cd1a..927d6d12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## [v1.11.3](https://github.com/coder/vscode-coder/releases/tag/v1.11.3) 2025-10-22 + ### Fixed - Fixed WebSocket connections not receiving headers from the configured header command diff --git a/package.json b/package.json index 363119d5..25db26f1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coder-remote", "displayName": "Coder", - "version": "1.11.2", + "version": "1.11.3", "description": "Open any workspace with a single click.", "categories": [ "Other" From ffe0182de549792080c9a54c2a1c72d269ca7e36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:54:19 -0800 Subject: [PATCH 39/45] chore(deps-dev): bump @typescript-eslint/parser from 8.44.1 to 8.46.2 (#634) --- package.json | 2 +- yarn.lock | 86 +++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 25db26f1..56e9e1af 100644 --- a/package.json +++ b/package.json @@ -367,7 +367,7 @@ "@types/vscode": "^1.73.0", "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.44.0", - "@typescript-eslint/parser": "^8.44.0", + "@typescript-eslint/parser": "^8.46.2", "@vitest/coverage-v8": "^3.2.4", "@vscode/test-cli": "^0.0.11", "@vscode/test-electron": "^2.5.2", diff --git a/yarn.lock b/yarn.lock index 3e38b6a6..019cbe01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1168,15 +1168,15 @@ natural-compare "^1.4.0" ts-api-utils "^2.1.0" -"@typescript-eslint/parser@^8.44.0": - version "8.44.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.44.1.tgz#d4c85791389462823596ad46e2b90d34845e05eb" - integrity sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw== - dependencies: - "@typescript-eslint/scope-manager" "8.44.1" - "@typescript-eslint/types" "8.44.1" - "@typescript-eslint/typescript-estree" "8.44.1" - "@typescript-eslint/visitor-keys" "8.44.1" +"@typescript-eslint/parser@^8.46.2": + version "8.46.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.46.2.tgz#dd938d45d581ac8ffa9d8a418a50282b306f7ebf" + integrity sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g== + dependencies: + "@typescript-eslint/scope-manager" "8.46.2" + "@typescript-eslint/types" "8.46.2" + "@typescript-eslint/typescript-estree" "8.46.2" + "@typescript-eslint/visitor-keys" "8.46.2" debug "^4.3.4" "@typescript-eslint/project-service@8.44.1": @@ -1188,6 +1188,15 @@ "@typescript-eslint/types" "^8.44.1" debug "^4.3.4" +"@typescript-eslint/project-service@8.46.2": + version "8.46.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.46.2.tgz#ab2f02a0de4da6a7eeb885af5e059be57819d608" + integrity sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg== + dependencies: + "@typescript-eslint/tsconfig-utils" "^8.46.2" + "@typescript-eslint/types" "^8.46.2" + debug "^4.3.4" + "@typescript-eslint/scope-manager@8.44.1": version "8.44.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.44.1.tgz#31c27f92e4aed8d0f4d6fe2b9e5187d1d8797bd7" @@ -1196,11 +1205,24 @@ "@typescript-eslint/types" "8.44.1" "@typescript-eslint/visitor-keys" "8.44.1" +"@typescript-eslint/scope-manager@8.46.2": + version "8.46.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz#7d37df2493c404450589acb3b5d0c69cc0670a88" + integrity sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA== + dependencies: + "@typescript-eslint/types" "8.46.2" + "@typescript-eslint/visitor-keys" "8.46.2" + "@typescript-eslint/tsconfig-utils@8.44.1", "@typescript-eslint/tsconfig-utils@^8.44.1": version "8.44.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.1.tgz#e1d9d047078fac37d3e638484ab3b56215963342" integrity sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ== +"@typescript-eslint/tsconfig-utils@8.46.2", "@typescript-eslint/tsconfig-utils@^8.46.2": + version "8.46.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz#d110451cb93bbd189865206ea37ef677c196828c" + integrity sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag== + "@typescript-eslint/type-utils@8.44.1": version "8.44.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.44.1.tgz#be9d31e0f911d17ee8ac99921bb74cf1f9df3906" @@ -1212,11 +1234,16 @@ debug "^4.3.4" ts-api-utils "^2.1.0" -"@typescript-eslint/types@8.44.1", "@typescript-eslint/types@^8.44.1": +"@typescript-eslint/types@8.44.1": version "8.44.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.44.1.tgz#85d1cad1290a003ff60420388797e85d1c3f76ff" integrity sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ== +"@typescript-eslint/types@8.46.2", "@typescript-eslint/types@^8.44.1", "@typescript-eslint/types@^8.46.2": + version "8.46.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.46.2.tgz#2bad7348511b31e6e42579820e62b73145635763" + integrity sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ== + "@typescript-eslint/typescript-estree@8.44.1": version "8.44.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.1.tgz#4f17650e5adabecfcc13cd8c517937a4ef5cd424" @@ -1233,6 +1260,22 @@ semver "^7.6.0" ts-api-utils "^2.1.0" +"@typescript-eslint/typescript-estree@8.46.2": + version "8.46.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz#ab547a27e4222bb6a3281cb7e98705272e2c7d08" + integrity sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ== + dependencies: + "@typescript-eslint/project-service" "8.46.2" + "@typescript-eslint/tsconfig-utils" "8.46.2" + "@typescript-eslint/types" "8.46.2" + "@typescript-eslint/visitor-keys" "8.46.2" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.1.0" + "@typescript-eslint/utils@8.44.1": version "8.44.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.44.1.tgz#f23d48eb90791a821dc17d4f67bb96faeb75d63d" @@ -1251,6 +1294,14 @@ "@typescript-eslint/types" "8.44.1" eslint-visitor-keys "^4.2.1" +"@typescript-eslint/visitor-keys@8.46.2": + version "8.46.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz#803fa298948c39acf810af21bdce6f8babfa9738" + integrity sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w== + dependencies: + "@typescript-eslint/types" "8.46.2" + eslint-visitor-keys "^4.2.1" + "@typespec/ts-http-runtime@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.0.tgz#f506ff2170e594a257f8e78aa196088f3a46a22d" @@ -2728,10 +2779,10 @@ dayjs@^1.11.13: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: - version "4.3.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" - integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== +debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== dependencies: ms "^2.1.3" @@ -2742,13 +2793,6 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.5, debug@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" - integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== - dependencies: - ms "^2.1.3" - decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" From 17e2cd164ed2a1a8e0facad95069dfa6899b3500 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:54:48 -0800 Subject: [PATCH 40/45] chore(deps-dev): bump @vscode/vsce from 3.6.0 to 3.6.2 (#639) --- package.json | 2 +- yarn.lock | 171 +++++++++++++++++++++++++-------------------------- 2 files changed, 84 insertions(+), 89 deletions(-) diff --git a/package.json b/package.json index 56e9e1af..b7b63433 100644 --- a/package.json +++ b/package.json @@ -371,7 +371,7 @@ "@vitest/coverage-v8": "^3.2.4", "@vscode/test-cli": "^0.0.11", "@vscode/test-electron": "^2.5.2", - "@vscode/vsce": "^3.6.0", + "@vscode/vsce": "^3.6.2", "bufferutil": "^4.0.9", "coder": "https://github.com/coder/coder#main", "dayjs": "^1.11.13", diff --git a/yarn.lock b/yarn.lock index 019cbe01..f7a7155f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -863,42 +863,42 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@secretlint/config-creator@^10.2.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@secretlint/config-creator/-/config-creator-10.2.1.tgz#867c88741f8cb22988708919e480330e5fa66a44" - integrity sha512-nyuRy8uo2+mXPIRLJ93wizD1HbcdDIsVfgCT01p/zGVFrtvmiL7wqsl4KgZH0QFBM/KRLDLeog3/eaM5ASjtvw== +"@secretlint/config-creator@^10.2.2": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@secretlint/config-creator/-/config-creator-10.2.2.tgz#5d646e83bb2aacfbd5218968ceb358420b4c2cb3" + integrity sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ== dependencies: - "@secretlint/types" "^10.2.1" + "@secretlint/types" "^10.2.2" -"@secretlint/config-loader@^10.2.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@secretlint/config-loader/-/config-loader-10.2.1.tgz#8acff15b4f52a9569e403cef99fee28d330041aa" - integrity sha512-ob1PwhuSw/Hc6Y4TA63NWj6o++rZTRJOwPZG82o6tgEURqkrAN44fXH9GIouLsOxKa8fbCRLMeGmSBtJLdSqtw== +"@secretlint/config-loader@^10.2.2": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@secretlint/config-loader/-/config-loader-10.2.2.tgz#a7790c8d0301db4f6d47e6fb0f0f9482fe652d9a" + integrity sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ== dependencies: - "@secretlint/profiler" "^10.2.1" - "@secretlint/resolver" "^10.2.1" - "@secretlint/types" "^10.2.1" + "@secretlint/profiler" "^10.2.2" + "@secretlint/resolver" "^10.2.2" + "@secretlint/types" "^10.2.2" ajv "^8.17.1" debug "^4.4.1" rc-config-loader "^4.1.3" -"@secretlint/core@^10.2.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@secretlint/core/-/core-10.2.1.tgz#a727174fbfd7b7f5d8f63b46470c1405bbe85cab" - integrity sha512-2sPp5IE7pM5Q+f1/NK6nJ49FKuqh+e3fZq5MVbtVjegiD4NMhjcoML1Cg7atCBgXPufhXRHY1DWhIhkGzOx/cw== +"@secretlint/core@^10.2.2": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@secretlint/core/-/core-10.2.2.tgz#cd41d5c27ba07c217f0af4e0e24dbdfe5ef62042" + integrity sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw== dependencies: - "@secretlint/profiler" "^10.2.1" - "@secretlint/types" "^10.2.1" + "@secretlint/profiler" "^10.2.2" + "@secretlint/types" "^10.2.2" debug "^4.4.1" structured-source "^4.0.0" -"@secretlint/formatter@^10.2.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@secretlint/formatter/-/formatter-10.2.1.tgz#a09ed00dbb91a17476dc3cf885387722b5225881" - integrity sha512-0A7ho3j0Y4ysK0mREB3O6FKQtScD4rQgfzuI4Slv9Cut1ynQOI7JXAoIFm4XVzhNcgtmEPeD3pQB206VFphBgQ== +"@secretlint/formatter@^10.2.2": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@secretlint/formatter/-/formatter-10.2.2.tgz#c8ce35803ad0d841cc9b6e703d6fab68a144e9c0" + integrity sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA== dependencies: - "@secretlint/resolver" "^10.2.1" - "@secretlint/types" "^10.2.1" + "@secretlint/resolver" "^10.2.2" + "@secretlint/types" "^10.2.2" "@textlint/linter-formatter" "^15.2.0" "@textlint/module-interop" "^15.2.0" "@textlint/types" "^15.2.0" @@ -909,61 +909,61 @@ table "^6.9.0" terminal-link "^4.0.0" -"@secretlint/node@^10.1.1", "@secretlint/node@^10.2.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@secretlint/node/-/node-10.2.1.tgz#4ff09a244500ec9c5f9d2a512bd047ebbfa9cb97" - integrity sha512-MQFte7C+5ZHINQGSo6+eUECcUCGvKR9PVgZcTsRj524xsbpeBqF1q1dHsUsdGb9r2jlvf40Q14MRZwMcpmLXWQ== - dependencies: - "@secretlint/config-loader" "^10.2.1" - "@secretlint/core" "^10.2.1" - "@secretlint/formatter" "^10.2.1" - "@secretlint/profiler" "^10.2.1" - "@secretlint/source-creator" "^10.2.1" - "@secretlint/types" "^10.2.1" +"@secretlint/node@^10.1.2", "@secretlint/node@^10.2.2": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@secretlint/node/-/node-10.2.2.tgz#1d8a6ed620170bf4f29829a3a91878682c43c4d9" + integrity sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ== + dependencies: + "@secretlint/config-loader" "^10.2.2" + "@secretlint/core" "^10.2.2" + "@secretlint/formatter" "^10.2.2" + "@secretlint/profiler" "^10.2.2" + "@secretlint/source-creator" "^10.2.2" + "@secretlint/types" "^10.2.2" debug "^4.4.1" p-map "^7.0.3" -"@secretlint/profiler@^10.2.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@secretlint/profiler/-/profiler-10.2.1.tgz#eb532c7549b68c639de399760c654529d8327e51" - integrity sha512-gOlfPZ1ASc5mP5cqsL809uMJGp85t+AJZg1ZPscWvB/m5UFFgeNTZcOawggb1S5ExDvR388sIJxagx5hyDZ34g== +"@secretlint/profiler@^10.2.2": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@secretlint/profiler/-/profiler-10.2.2.tgz#82c085ab1966806763bbf6edb830987f25d4e797" + integrity sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig== -"@secretlint/resolver@^10.2.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@secretlint/resolver/-/resolver-10.2.1.tgz#513e2e4916d09fd96ead8f7020808a5373794cb8" - integrity sha512-AuwehKwnE2uxKaJVv2Z5a8FzGezBmlNhtLKm70Cvsvtwd0oAtenxCSTKXkiPGYC0+S91fAw3lrX7CUkyr9cTCA== +"@secretlint/resolver@^10.2.2": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@secretlint/resolver/-/resolver-10.2.2.tgz#9c3c3e2fef00679fcce99793e76e19e575b75721" + integrity sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w== -"@secretlint/secretlint-formatter-sarif@^10.1.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.1.tgz#65e77f5313914041b353ad221613341a89d5bb80" - integrity sha512-qOZUYBesLkhCBP7YVMv0l1Pypt8e3V2rX2PT2Q5aJhJvKTcMiP9YTHG/3H9Zb7Gq3UIwZLEAGXRqJOu1XlE0Fg== +"@secretlint/secretlint-formatter-sarif@^10.1.2": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz#5c4044a6a6c9d95e2f57270d6184931f0979d649" + integrity sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ== dependencies: node-sarif-builder "^3.2.0" -"@secretlint/secretlint-rule-no-dotenv@^10.1.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.1.tgz#2c272beecd6c262b6d57413c72fe7aae57f1b3eb" - integrity sha512-XwPjc9Wwe2QljerfvGlBmLJAJVATLvoXXw1fnKyCDNgvY33cu1Z561Kxg93xfRB5LSep0S5hQrAfZRJw6x7MBQ== +"@secretlint/secretlint-rule-no-dotenv@^10.1.2": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz#ea43dcc2abd1dac3288b056610361f319f5ce6e9" + integrity sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg== dependencies: - "@secretlint/types" "^10.2.1" + "@secretlint/types" "^10.2.2" -"@secretlint/secretlint-rule-preset-recommend@^10.1.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.1.tgz#c00fbd2257328ec909da43431826cdfb729a2185" - integrity sha512-/kj3UOpFbJt80dqoeEaUVv5nbeW1jPqPExA447FItthiybnaDse5C5HYcfNA2ywEInr399ELdcmpEMRe+ld1iQ== +"@secretlint/secretlint-rule-preset-recommend@^10.1.2": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz#27b17c38b360c6788826d28fcda28ac6e9772d0b" + integrity sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA== -"@secretlint/source-creator@^10.2.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@secretlint/source-creator/-/source-creator-10.2.1.tgz#1b1c1c64db677034e29c1a3db78dccd60da89d32" - integrity sha512-1CgO+hsRx8KdA5R/LEMNTJkujjomwSQQVV0BcuKynpOefV/rRlIDVQJOU0tJOZdqUMC15oAAwQXs9tMwWLu4JQ== +"@secretlint/source-creator@^10.2.2": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@secretlint/source-creator/-/source-creator-10.2.2.tgz#d600b6d4487859cdd39bbb1cf8cf744540b3f7a1" + integrity sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw== dependencies: - "@secretlint/types" "^10.2.1" + "@secretlint/types" "^10.2.2" istextorbinary "^9.5.0" -"@secretlint/types@^10.2.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@secretlint/types/-/types-10.2.1.tgz#018f252a3754a9ff2371b3e132226d281be8515b" - integrity sha512-F5k1qpoMoUe7rrZossOBgJ3jWKv/FGDBZIwepqnefgPmNienBdInxhtZeXiGwjcxXHVhsdgp6I5Fi/M8PMgwcw== +"@secretlint/types@^10.2.2": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@secretlint/types/-/types-10.2.2.tgz#1412d8f699fd900182cbf4c2923a9df9eb321ca7" + integrity sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg== "@sindresorhus/merge-streams@^2.1.0": version "2.3.0" @@ -1579,16 +1579,16 @@ "@vscode/vsce-sign-win32-arm64" "2.0.5" "@vscode/vsce-sign-win32-x64" "2.0.5" -"@vscode/vsce@^3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@vscode/vsce/-/vsce-3.6.0.tgz#7102cb846db83ed70ec7119986af7d7c69cf3538" - integrity sha512-u2ZoMfymRNJb14aHNawnXJtXHLXDVKc1oKZaH4VELKT/9iWKRVgtQOdwxCgtwSxJoqYvuK4hGlBWQJ05wxADhg== +"@vscode/vsce@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@vscode/vsce/-/vsce-3.6.2.tgz#cefd2802f1dec24fca51293ae563e11912f545fd" + integrity sha512-gvBfarWF+Ii20ESqjA3dpnPJpQJ8fFJYtcWtjwbRADommCzGg1emtmb34E+DKKhECYvaVyAl+TF9lWS/3GSPvg== dependencies: "@azure/identity" "^4.1.0" - "@secretlint/node" "^10.1.1" - "@secretlint/secretlint-formatter-sarif" "^10.1.1" - "@secretlint/secretlint-rule-no-dotenv" "^10.1.1" - "@secretlint/secretlint-rule-preset-recommend" "^10.1.1" + "@secretlint/node" "^10.1.2" + "@secretlint/secretlint-formatter-sarif" "^10.1.2" + "@secretlint/secretlint-rule-no-dotenv" "^10.1.2" + "@secretlint/secretlint-rule-preset-recommend" "^10.1.2" "@vscode/vsce-sign" "^2.0.0" azure-devops-node-api "^12.5.0" chalk "^4.1.2" @@ -1605,7 +1605,7 @@ minimatch "^3.0.3" parse-semver "^1.1.1" read "^1.0.7" - secretlint "^10.1.1" + secretlint "^10.1.2" semver "^7.5.2" tmp "^0.2.3" typed-rest-client "^1.8.4" @@ -2408,12 +2408,7 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2, chalk@~4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" - integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== - -chalk@^5.4.1: +chalk@^5.3.0, chalk@^5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== @@ -7493,15 +7488,15 @@ schema-utils@^4.3.0, schema-utils@^4.3.2: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" -secretlint@^10.1.1: - version "10.2.1" - resolved "https://registry.yarnpkg.com/secretlint/-/secretlint-10.2.1.tgz#021ea25bb77f23efba22ce778d1a001b15de77b1" - integrity sha512-3BghQkIGrDz3xJklX/COxgKbxHz2CAsGkXH4oh8MxeYVLlhA3L/TLhAxZiTyqeril+CnDGg8MUEZdX1dZNsxVA== +secretlint@^10.1.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/secretlint/-/secretlint-10.2.2.tgz#c0cf997153a2bef0b653874dc87030daa6a35140" + integrity sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg== dependencies: - "@secretlint/config-creator" "^10.2.1" - "@secretlint/formatter" "^10.2.1" - "@secretlint/node" "^10.2.1" - "@secretlint/profiler" "^10.2.1" + "@secretlint/config-creator" "^10.2.2" + "@secretlint/formatter" "^10.2.2" + "@secretlint/node" "^10.2.2" + "@secretlint/profiler" "^10.2.2" debug "^4.4.1" globby "^14.1.0" read-pkg "^9.0.1" From 159ffcb151974ba0de7a4f6de31923308f770ade Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:55:46 -0800 Subject: [PATCH 41/45] chore(deps-dev): bump eslint-plugin-package-json from 0.56.3 to 0.59.0 (#635) --- package.json | 2 +- yarn.lock | 41 +++++++++++++++++++++++------------------ 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index b7b63433..5db1a2a9 100644 --- a/package.json +++ b/package.json @@ -380,7 +380,7 @@ "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-md": "^1.0.19", - "eslint-plugin-package-json": "^0.56.3", + "eslint-plugin-package-json": "^0.59.0", "eslint-plugin-prettier": "^5.5.4", "glob": "^10.4.2", "jsonc-eslint-parser": "^2.4.0", diff --git a/yarn.lock b/yarn.lock index f7a7155f..ffdcadb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2903,10 +2903,10 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -detect-indent@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-7.0.1.tgz#cbb060a12842b9c4d333f1cac4aa4da1bb66bc25" - integrity sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g== +detect-indent@^7.0.1, detect-indent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-7.0.2.tgz#16c516bf75d4b2f759f68214554996d467c8d648" + integrity sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A== detect-libc@^2.0.0: version "2.0.1" @@ -3503,20 +3503,20 @@ eslint-plugin-md@^1.0.19: remark-preset-lint-markdown-style-guide "^2.1.3" requireindex "~1.1.0" -eslint-plugin-package-json@^0.56.3: - version "0.56.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-package-json/-/eslint-plugin-package-json-0.56.3.tgz#dcf50aaf3a3bc377396d3df72bb63819b02e8d73" - integrity sha512-ArN3wnOAsduM/6a0egB83DQQfF/4KzxE53U8qcvELCXT929TnBy2IeCli4+in3QSHxcVYSIDa2Y5T2vVAXbe6A== +eslint-plugin-package-json@^0.59.0: + version "0.59.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-package-json/-/eslint-plugin-package-json-0.59.0.tgz#fb847e54742a3465de2e6c813608f95c88075c24" + integrity sha512-4xdVhL3b7LqQQh8cvN3hX8HkAVM6cxZoXqyN4ZE4kN9NuJ21sgnj1IGS19/bmIgCdGBhmsWGXbbyD1H9mjZfMA== dependencies: "@altano/repository-tools" "^2.0.1" change-case "^5.4.4" - detect-indent "^7.0.1" + detect-indent "^7.0.2" detect-newline "^4.0.1" eslint-fix-utils "~0.4.0" - package-json-validator "~0.30.0" - semver "^7.5.4" - sort-object-keys "^1.1.3" - sort-package-json "^3.3.0" + package-json-validator "~0.31.0" + semver "^7.7.3" + sort-object-keys "^2.0.0" + sort-package-json "^3.4.0" validate-npm-package-name "^6.0.2" eslint-plugin-prettier@^5.5.4: @@ -6235,10 +6235,10 @@ package-json-from-dist@^1.0.0: resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== -package-json-validator@~0.30.0: - version "0.30.0" - resolved "https://registry.yarnpkg.com/package-json-validator/-/package-json-validator-0.30.0.tgz#31613a3e4a2455599c7ad3a97f134707f13de1e0" - integrity sha512-gOLW+BBye32t+IB2trIALIcL3DZBy3s4G4ZV6dAgDM+qLs/7jUNOV7iO7PwXqyf+3izI12qHBwtS4kOSJp5Tdg== +package-json-validator@~0.31.0: + version "0.31.0" + resolved "https://registry.yarnpkg.com/package-json-validator/-/package-json-validator-0.31.0.tgz#c5a693e6db3ee9ca6dddfd5d07a79807f340dc77" + integrity sha512-kAVO0fNFWI2xpmthogYHnHjCtg0nJvwm9yjd9nnrR5OKIts5fmNMK2OhhjnLD1/ohJNodhCa5tZm8AolOgkfMg== dependencies: semver "^7.7.2" validate-npm-package-license "^3.0.4" @@ -7738,7 +7738,12 @@ sort-object-keys@^1.1.3: resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45" integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg== -sort-package-json@^3.3.0: +sort-object-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-2.0.0.tgz#e5dc3d75d07d4efe73ba6ac55f2f1a4380fdedf8" + integrity sha512-FTUWjmUumK0IGXn1INzkS3lS2Fqw81JuomcExd7LsFvQnNl+9+IZ575fC21F/AwrR/6lMrH7lTX0e7qLBk1wMg== + +sort-package-json@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-3.4.0.tgz#98e42b78848c517736b069f8aa4fa322fae56677" integrity sha512-97oFRRMM2/Js4oEA9LJhjyMlde+2ewpZQf53pgue27UkbEXfHJnDzHlUxQ/DWUkzqmp7DFwJp8D+wi/TYeQhpA== From a2d9d56abbb5b50b44d268e7c3dc8d669e235a1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:32:50 +0300 Subject: [PATCH 42/45] chore(deps): bump actions/upload-artifact from 4 to 5 (#638) --- .github/workflows/ci.yaml | 4 ++-- .github/workflows/pre-release.yaml | 2 +- .github/workflows/release.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a878f9f2..64e85a15 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -84,7 +84,7 @@ jobs: - name: Upload artifact (PR) if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: extension-pr-${{ github.event.pull_request.number }} path: ${{ steps.setup.outputs.packageName }} @@ -93,7 +93,7 @@ jobs: - name: Upload artifact (main) if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: extension-main-${{ github.sha }} path: ${{ steps.setup.outputs.packageName }} diff --git a/.github/workflows/pre-release.yaml b/.github/workflows/pre-release.yaml index 61761700..430aa2a1 100644 --- a/.github/workflows/pre-release.yaml +++ b/.github/workflows/pre-release.yaml @@ -60,7 +60,7 @@ jobs: run: vsce package --pre-release --out "${{ steps.setup.outputs.packageName }}" - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: extension-${{ steps.version.outputs.version }} path: ${{ steps.setup.outputs.packageName }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 51d9ff97..557586ec 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -60,7 +60,7 @@ jobs: run: vsce package --out "${{ steps.setup.outputs.packageName }}" - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: extension-${{ steps.version.outputs.version }} path: ${{ steps.setup.outputs.packageName }} From 8bff063eccac671c9eb0f6fd22c4e1b34f31d36f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:36:41 +0300 Subject: [PATCH 43/45] chore(deps): bump actions/download-artifact from 5 to 6 (#636) --- .github/workflows/publish-extension.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-extension.yaml b/.github/workflows/publish-extension.yaml index cf93d6ba..e7d5dca7 100644 --- a/.github/workflows/publish-extension.yaml +++ b/.github/workflows/publish-extension.yaml @@ -67,7 +67,7 @@ jobs: - name: Install vsce run: npm install -g @vscode/vsce - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: name: extension-${{ inputs.version }} @@ -93,7 +93,7 @@ jobs: - name: Install ovsx run: npm install -g ovsx - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: name: extension-${{ inputs.version }} @@ -111,7 +111,7 @@ jobs: needs: setup runs-on: ubuntu-22.04 steps: - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: name: extension-${{ inputs.version }} From 6ea816a9d4fc5c32cf31e0c55d367f1efec7c7d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:38:59 +0300 Subject: [PATCH 44/45] chore(deps-dev): bump glob from 10.4.5 to 11.0.3 (#637) --- package.json | 2 +- yarn.lock | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 5db1a2a9..dfb6da93 100644 --- a/package.json +++ b/package.json @@ -382,7 +382,7 @@ "eslint-plugin-md": "^1.0.19", "eslint-plugin-package-json": "^0.59.0", "eslint-plugin-prettier": "^5.5.4", - "glob": "^10.4.2", + "glob": "^11.0.3", "jsonc-eslint-parser": "^2.4.0", "markdown-eslint-parser": "^1.2.1", "memfs": "^4.49.0", diff --git a/yarn.lock b/yarn.lock index ffdcadb0..deda75ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3952,15 +3952,7 @@ foreground-child@^2.0.0: cross-spawn "^7.0.0" signal-exit "^3.0.2" -foreground-child@^3.1.0, foreground-child@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" - integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== - dependencies: - cross-spawn "^7.0.0" - signal-exit "^4.0.1" - -foreground-child@^3.1.1, foreground-child@^3.3.1: +foreground-child@^3.1.0, foreground-child@^3.1.1, foreground-child@^3.3.0, foreground-child@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== @@ -4241,7 +4233,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^10.3.10, glob@^10.4.1, glob@^10.4.2, glob@^10.4.5: +glob@^10.3.10, glob@^10.4.1, glob@^10.4.5: version "10.4.5" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== @@ -4253,7 +4245,7 @@ glob@^10.3.10, glob@^10.4.1, glob@^10.4.2, glob@^10.4.5: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^11.0.0: +glob@^11.0.0, glob@^11.0.3: version "11.0.3" resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.3.tgz#9d8087e6d72ddb3c4707b1d2778f80ea3eaefcd6" integrity sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA== From a1ad85e03e1f92116a2771a67a3d34561d9a807c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:43:08 +0300 Subject: [PATCH 45/45] chore(deps): bump zod from 3.25.65 to 4.1.12 (#640) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index dfb6da93..49844183 100644 --- a/package.json +++ b/package.json @@ -355,7 +355,7 @@ "semver": "^7.7.3", "ua-parser-js": "1.0.40", "ws": "^8.18.3", - "zod": "^3.25.65" + "zod": "^4.1.12" }, "devDependencies": { "@types/eventsource": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index deda75ac..cecbf92d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9289,7 +9289,7 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zod@^3.25.65: - version "3.25.65" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.65.tgz#190cb604e1b45e0f789a315f65463953d4d4beee" - integrity sha512-kMyE2qsXK1p+TAvO7zsf5wMFiCejU3obrUDs9bR1q5CBKykfvp7QhhXrycUylMoOow0iEUSyjLlZZdCsHwSldQ== +zod@^4.1.12: + version "4.1.12" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.12.tgz#64f1ea53d00eab91853195653b5af9eee68970f0" + integrity sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==