From aaca0bfbd65464929dcab5225949f76f67f8c597 Mon Sep 17 00:00:00 2001 From: christine betts Date: Tue, 7 Oct 2025 13:31:02 -0400 Subject: [PATCH] Patch #10628 and #10514 into v0.8.0 preview (#10646) Co-authored-by: Jacob MacDonald Co-authored-by: Adam Weidman <65992621+adamfweidman@users.noreply.github.com> --- docs/extension.md | 2 +- integration-tests/extensions-install.test.ts | 2 +- package-lock.json | 571 +++++++++++++++++- packages/cli/package.json | 5 + .../src/commands/extensions/install.test.ts | 51 +- .../cli/src/commands/extensions/install.ts | 70 +-- .../cli/src/config/extensions/github.test.ts | 87 ++- packages/cli/src/config/extensions/github.ts | 29 +- .../ui/components/FolderTrustDialog.test.tsx | 5 +- .../cli/src/ui/hooks/useFolderTrust.test.ts | 19 +- 10 files changed, 709 insertions(+), 132 deletions(-) diff --git a/docs/extension.md b/docs/extension.md index a82472bfb9..f7283b75f9 100644 --- a/docs/extension.md +++ b/docs/extension.md @@ -14,7 +14,7 @@ Note that all of these commands will only be reflected in active CLI sessions on ### Installing an extension -You can install an extension using `gemini extensions install` with either a GitHub URL source or `--path=some/local/path`. +You can install an extension using `gemini extensions install` with either a GitHub URL or a local path`. Note that we create a copy of the installed extension, so you will need to run `gemini extensions update` to pull in changes from both locally-defined extensions and those on GitHub. diff --git a/integration-tests/extensions-install.test.ts b/integration-tests/extensions-install.test.ts index 3a94167706..c54f94b12a 100644 --- a/integration-tests/extensions-install.test.ts +++ b/integration-tests/extensions-install.test.ts @@ -31,7 +31,7 @@ test('installs a local extension, verifies a command, and updates it', async () } const result = await rig.runCommand( - ['extensions', 'install', `--path=${rig.testDir!}`], + ['extensions', 'install', `${rig.testDir!}`], { stdin: 'y\n' }, ); expect(result).toContain('test-extension'); diff --git a/package-lock.json b/package-lock.json index 37b0d24c83..d9abf5f1e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3820,6 +3820,16 @@ "node": ">= 10" } }, + "node_modules/@types/archiver": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.3.tgz", + "integrity": "sha512-a6wUll6k3zX6qs5KlxIggs1P1JcYJaTCx2gnlr+f0S1yd2DoaEwoIK10HmBaLnZwWneBz+JBm0dwcZu0zECBcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/readdir-glob": "*" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -4151,6 +4161,16 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/request": { "version": "2.48.13", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", @@ -5354,6 +5374,150 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5670,12 +5834,34 @@ "typed-rest-client": "^1.8.4" } }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz", + "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -6537,6 +6723,65 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -6650,6 +6895,75 @@ "node": ">= 0.10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -8009,6 +8323,26 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -8228,6 +8562,13 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -9558,8 +9899,7 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "5.3.2", @@ -10871,6 +11211,59 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/leac": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", @@ -11589,9 +11982,9 @@ } }, "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -11600,21 +11993,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -11902,6 +12280,16 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.2.tgz", @@ -13029,6 +13417,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -13482,6 +13887,39 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -14534,6 +14972,18 @@ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "license": "MIT" }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", @@ -15077,16 +15527,15 @@ } }, "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", + "minizlib": "^3.1.0", "yallist": "^5.0.0" }, "engines": { @@ -15246,6 +15695,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -16861,6 +17320,63 @@ "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", "license": "MIT" }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", @@ -17164,6 +17680,7 @@ "comment-json": "^4.2.5", "diff": "^7.0.0", "dotenv": "^17.1.0", + "extract-zip": "^2.0.1", "fzf": "^0.5.2", "glob": "^10.4.5", "highlight.js": "^11.11.1", @@ -17179,6 +17696,7 @@ "string-width": "^7.1.0", "strip-ansi": "^7.1.0", "strip-json-comments": "^3.1.1", + "tar": "^7.5.1", "undici": "^7.10.0", "update-notifier": "^7.3.1", "wrap-ansi": "9.0.2", @@ -17192,6 +17710,7 @@ "@babel/runtime": "^7.27.6", "@google/gemini-cli-test-utils": "file:../test-utils", "@testing-library/react": "^16.3.0", + "@types/archiver": "^6.0.3", "@types/command-exists": "^1.2.3", "@types/diff": "^7.0.2", "@types/dotenv": "^6.1.1", @@ -17200,7 +17719,9 @@ "@types/react-dom": "^19.1.6", "@types/semver": "^7.7.0", "@types/shell-quote": "^1.7.5", + "@types/tar": "^6.1.13", "@types/yargs": "^17.0.32", + "archiver": "^7.0.1", "ink-testing-library": "^4.0.0", "jsdom": "^26.1.0", "pretty-format": "^30.0.2", diff --git a/packages/cli/package.json b/packages/cli/package.json index 9f4a48b84c..c387cfb692 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -53,7 +53,9 @@ "string-width": "^7.1.0", "strip-ansi": "^7.1.0", "strip-json-comments": "^3.1.1", + "tar": "^7.5.1", "undici": "^7.10.0", + "extract-zip": "^2.0.1", "update-notifier": "^7.3.1", "wrap-ansi": "9.0.2", "yargs": "^17.7.2", @@ -63,6 +65,7 @@ "@babel/runtime": "^7.27.6", "@google/gemini-cli-test-utils": "file:../test-utils", "@testing-library/react": "^16.3.0", + "@types/archiver": "^6.0.3", "@types/command-exists": "^1.2.3", "@types/diff": "^7.0.2", "@types/dotenv": "^6.1.1", @@ -71,7 +74,9 @@ "@types/react-dom": "^19.1.6", "@types/semver": "^7.7.0", "@types/shell-quote": "^1.7.5", + "@types/tar": "^6.1.13", "@types/yargs": "^17.0.32", + "archiver": "^7.0.1", "ink-testing-library": "^4.0.0", "jsdom": "^26.1.0", "pretty-format": "^30.0.2", diff --git a/packages/cli/src/commands/extensions/install.test.ts b/packages/cli/src/commands/extensions/install.test.ts index 347d074ad6..3e479d7649 100644 --- a/packages/cli/src/commands/extensions/install.test.ts +++ b/packages/cli/src/commands/extensions/install.test.ts @@ -10,6 +10,7 @@ import yargs from 'yargs'; const mockInstallExtension = vi.hoisted(() => vi.fn()); const mockRequestConsentNonInteractive = vi.hoisted(() => vi.fn()); +const mockStat = vi.hoisted(() => vi.fn()); vi.mock('../../config/extension.js', () => ({ installExtension: mockInstallExtension, @@ -20,35 +21,20 @@ vi.mock('../../utils/errors.js', () => ({ getErrorMessage: vi.fn((error: Error) => error.message), })); +vi.mock('node:fs/promises', () => ({ + stat: mockStat, + default: { + stat: mockStat, + }, +})); + describe('extensions install command', () => { it('should fail if no source is provided', () => { const validationParser = yargs([]).command(installCommand).fail(false); expect(() => validationParser.parse('install')).toThrow( - 'Either source or --path must be provided.', + 'Not enough non-option arguments: got 0, need at least 1', ); }); - - it('should fail if both git source and local path are provided', () => { - const validationParser = yargs([]) - .command(installCommand) - .fail(false) - .locale('en'); - expect(() => - validationParser.parse('install some-url --path /some/path'), - ).toThrow('Arguments source and path are mutually exclusive'); - }); - - it('should fail if both auto update and local path are provided', () => { - const validationParser = yargs([]) - .command(installCommand) - .fail(false) - .locale('en'); - expect(() => - validationParser.parse( - 'install some-url --path /some/path --auto-update', - ), - ).toThrow('Arguments path and auto-update are mutually exclusive'); - }); }); describe('handleInstall', () => { @@ -67,6 +53,7 @@ describe('handleInstall', () => { afterEach(() => { mockInstallExtension.mockClear(); mockRequestConsentNonInteractive.mockClear(); + mockStat.mockClear(); vi.resetAllMocks(); }); @@ -107,13 +94,12 @@ describe('handleInstall', () => { }); it('throws an error from an unknown source', async () => { + mockStat.mockRejectedValue(new Error('ENOENT: no such file or directory')); await handleInstall({ source: 'test://google.com', }); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'The source "test://google.com" is not a valid URL format.', - ); + expect(consoleErrorSpy).toHaveBeenCalledWith('Install source not found.'); expect(processSpy).toHaveBeenCalledWith(1); }); @@ -131,9 +117,9 @@ describe('handleInstall', () => { it('should install an extension from a local path', async () => { mockInstallExtension.mockResolvedValue('local-extension'); - + mockStat.mockResolvedValue({}); await handleInstall({ - path: '/some/path', + source: '/some/path', }); expect(consoleLogSpy).toHaveBeenCalledWith( @@ -141,15 +127,6 @@ describe('handleInstall', () => { ); }); - it('should throw an error if no source or path is provided', async () => { - await handleInstall({}); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Either --source or --path must be provided.', - ); - expect(processSpy).toHaveBeenCalledWith(1); - }); - it('should throw an error if install extension fails', async () => { mockInstallExtension.mockRejectedValue( new Error('Install extension failed'), diff --git a/packages/cli/src/commands/extensions/install.ts b/packages/cli/src/commands/extensions/install.ts index 6fef7a0b14..6cdaaec823 100644 --- a/packages/cli/src/commands/extensions/install.ts +++ b/packages/cli/src/commands/extensions/install.ts @@ -10,12 +10,11 @@ import { requestConsentNonInteractive, } from '../../config/extension.js'; import type { ExtensionInstallMetadata } from '@google/gemini-cli-core'; - import { getErrorMessage } from '../../utils/errors.js'; +import { stat } from 'node:fs/promises'; interface InstallArgs { - source?: string; - path?: string; + source: string; ref?: string; autoUpdate?: boolean; } @@ -23,32 +22,34 @@ interface InstallArgs { export async function handleInstall(args: InstallArgs) { try { let installMetadata: ExtensionInstallMetadata; - if (args.source) { - const { source } = args; - if ( - source.startsWith('http://') || - source.startsWith('https://') || - source.startsWith('git@') || - source.startsWith('sso://') - ) { - installMetadata = { - source, - type: 'git', - ref: args.ref, - autoUpdate: args.autoUpdate, - }; - } else { - throw new Error(`The source "${source}" is not a valid URL format.`); - } - } else if (args.path) { + const { source } = args; + if ( + source.startsWith('http://') || + source.startsWith('https://') || + source.startsWith('git@') || + source.startsWith('sso://') + ) { installMetadata = { - source: args.path, - type: 'local', + source, + type: 'git', + ref: args.ref, autoUpdate: args.autoUpdate, }; } else { - // This should not be reached due to the yargs check. - throw new Error('Either --source or --path must be provided.'); + if (args.ref || args.autoUpdate) { + throw new Error( + '--ref and --auto-update are not applicable for local extensions.', + ); + } + try { + await stat(source); + installMetadata = { + source, + type: 'local', + }; + } catch { + throw new Error('Install source not found.'); + } } const name = await installExtension( @@ -63,17 +64,14 @@ export async function handleInstall(args: InstallArgs) { } export const installCommand: CommandModule = { - command: 'install [] [--path] [--ref] [--auto-update]', + command: 'install ', describe: 'Installs an extension from a git repository URL or a local path.', builder: (yargs) => yargs .positional('source', { - describe: 'The github URL of the extension to install.', - type: 'string', - }) - .option('path', { - describe: 'Path to a local extension directory.', + describe: 'The github URL or local path of the extension to install.', type: 'string', + demandOption: true, }) .option('ref', { describe: 'The git ref to install from.', @@ -83,19 +81,15 @@ export const installCommand: CommandModule = { describe: 'Enable auto-update for this extension.', type: 'boolean', }) - .conflicts('source', 'path') - .conflicts('path', 'ref') - .conflicts('path', 'auto-update') .check((argv) => { - if (!argv.source && !argv.path) { - throw new Error('Either source or --path must be provided.'); + if (!argv.source) { + throw new Error('The source argument must be provided.'); } return true; }), handler: async (argv) => { await handleInstall({ - source: argv['source'] as string | undefined, - path: argv['path'] as string | undefined, + source: argv['source'] as string, ref: argv['ref'] as string | undefined, autoUpdate: argv['auto-update'] as boolean | undefined, }); diff --git a/packages/cli/src/config/extensions/github.test.ts b/packages/cli/src/config/extensions/github.test.ts index 6019cfda57..21c2751f97 100644 --- a/packages/cli/src/config/extensions/github.test.ts +++ b/packages/cli/src/config/extensions/github.test.ts @@ -8,12 +8,18 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { checkForExtensionUpdate, cloneFromGit, + extractFile, findReleaseAsset, parseGitHubRepoForReleases, } from './github.js'; import { simpleGit, type SimpleGit } from 'simple-git'; import { ExtensionUpdateState } from '../../ui/state/extensions.js'; -import type * as os from 'node:os'; +import * as os from 'node:os'; +import * as fs from 'node:fs/promises'; +import * as fsSync from 'node:fs'; +import * as path from 'node:path'; +import * as tar from 'tar'; +import * as archiver from 'archiver'; import type { GeminiCLIExtension } from '@google/gemini-cli-core'; const mockPlatform = vi.hoisted(() => vi.fn()); @@ -341,4 +347,83 @@ describe('git extension helpers', () => { ); }); }); + + describe('extractFile', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gemini-test-')); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true, force: true }); + }); + + it('should extract a .tar.gz file', async () => { + const archivePath = path.join(tempDir, 'test.tar.gz'); + const extractionDest = path.join(tempDir, 'extracted'); + await fs.mkdir(extractionDest); + + // Create a dummy file to be archived + const dummyFilePath = path.join(tempDir, 'test.txt'); + await fs.writeFile(dummyFilePath, 'hello tar'); + + // Create the tar.gz file + await tar.c( + { + gzip: true, + file: archivePath, + cwd: tempDir, + }, + ['test.txt'], + ); + + await extractFile(archivePath, extractionDest); + + const extractedFilePath = path.join(extractionDest, 'test.txt'); + const content = await fs.readFile(extractedFilePath, 'utf-8'); + expect(content).toBe('hello tar'); + }); + + it('should extract a .zip file', async () => { + const archivePath = path.join(tempDir, 'test.zip'); + const extractionDest = path.join(tempDir, 'extracted'); + await fs.mkdir(extractionDest); + + // Create a dummy file to be archived + const dummyFilePath = path.join(tempDir, 'test.txt'); + await fs.writeFile(dummyFilePath, 'hello zip'); + + // Create the zip file + const output = fsSync.createWriteStream(archivePath); + const archive = archiver.create('zip'); + + const streamFinished = new Promise((resolve, reject) => { + output.on('close', () => resolve(null)); + archive.on('error', reject); + }); + + archive.pipe(output); + archive.file(dummyFilePath, { name: 'test.txt' }); + await archive.finalize(); + await streamFinished; + + await extractFile(archivePath, extractionDest); + + const extractedFilePath = path.join(extractionDest, 'test.txt'); + const content = await fs.readFile(extractedFilePath, 'utf-8'); + expect(content).toBe('hello zip'); + }); + + it('should throw an error for unsupported file types', async () => { + const unsupportedFilePath = path.join(tempDir, 'test.txt'); + await fs.writeFile(unsupportedFilePath, 'some content'); + const extractionDest = path.join(tempDir, 'extracted'); + await fs.mkdir(extractionDest); + + await expect( + extractFile(unsupportedFilePath, extractionDest), + ).rejects.toThrow('Unsupported file extension for extraction:'); + }); + }); }); diff --git a/packages/cli/src/config/extensions/github.ts b/packages/cli/src/config/extensions/github.ts index 3bc148c82d..cfdb3a36a3 100644 --- a/packages/cli/src/config/extensions/github.ts +++ b/packages/cli/src/config/extensions/github.ts @@ -15,8 +15,9 @@ import * as os from 'node:os'; import * as https from 'node:https'; import * as fs from 'node:fs'; import * as path from 'node:path'; -import { spawnSync } from 'node:child_process'; import { EXTENSIONS_CONFIG_FILENAME, loadExtension } from '../extension.js'; +import * as tar from 'tar'; +import extract from 'extract-zip'; function getGitHubToken(): string | undefined { return process.env['GITHUB_TOKEN']; @@ -270,7 +271,7 @@ export async function downloadFromGitHubRelease( await downloadFile(archiveUrl, downloadedAssetPath); - extractFile(downloadedAssetPath, destination); + await extractFile(downloadedAssetPath, destination); // For regular github releases, the repository is put inside of a top level // directory. In this case we should see exactly two file in the destination @@ -416,27 +417,15 @@ async function downloadFile(url: string, dest: string): Promise { }); } -function extractFile(file: string, dest: string) { - let args: string[]; - let command: 'tar' | 'unzip'; +export async function extractFile(file: string, dest: string): Promise { if (file.endsWith('.tar.gz')) { - args = ['-xzf', file, '-C', dest]; - command = 'tar'; + await tar.x({ + file, + cwd: dest, + }); } else if (file.endsWith('.zip')) { - args = [file, '-d', dest]; - command = 'unzip'; + await extract(file, { dir: dest }); } else { throw new Error(`Unsupported file extension for extraction: ${file}`); } - - const result = spawnSync(command, args, { stdio: 'pipe' }); - - if (result.status !== 0) { - if (result.error) { - throw new Error(`Failed to spawn '${command}': ${result.error.message}`); - } - throw new Error( - `'${command}' command failed with exit code ${result.status}: ${result.stderr.toString()}`, - ); - } } diff --git a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx index c379d4e21e..27405f0181 100644 --- a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx +++ b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx @@ -17,8 +17,9 @@ vi.mock('../../utils/processUtils.js', () => ({ const mockedExit = vi.hoisted(() => vi.fn()); const mockedCwd = vi.hoisted(() => vi.fn()); -vi.mock('process', async () => { - const actual = await vi.importActual('process'); +vi.mock('node:process', async () => { + const actual = + await vi.importActual('node:process'); return { ...actual, exit: mockedExit, diff --git a/packages/cli/src/ui/hooks/useFolderTrust.test.ts b/packages/cli/src/ui/hooks/useFolderTrust.test.ts index cdb66e9f85..211b2d524c 100644 --- a/packages/cli/src/ui/hooks/useFolderTrust.test.ts +++ b/packages/cli/src/ui/hooks/useFolderTrust.test.ts @@ -11,14 +11,19 @@ import type { LoadedSettings } from '../../config/settings.js'; import { FolderTrustChoice } from '../components/FolderTrustDialog.js'; import type { LoadedTrustedFolders } from '../../config/trustedFolders.js'; import { TrustLevel } from '../../config/trustedFolders.js'; -import * as process from 'node:process'; - import * as trustedFolders from '../../config/trustedFolders.js'; -vi.mock('process', () => ({ - cwd: vi.fn(), - platform: 'linux', -})); +const mockedCwd = vi.hoisted(() => vi.fn()); + +vi.mock('node:process', async () => { + const actual = + await vi.importActual('node:process'); + return { + ...actual, + cwd: mockedCwd, + platform: 'linux', + }; +}); describe('useFolderTrust', () => { let mockSettings: LoadedSettings; @@ -47,7 +52,7 @@ describe('useFolderTrust', () => { .spyOn(trustedFolders, 'loadTrustedFolders') .mockReturnValue(mockTrustedFolders); isWorkspaceTrustedSpy = vi.spyOn(trustedFolders, 'isWorkspaceTrusted'); - (process.cwd as vi.Mock).mockReturnValue('/test/path'); + mockedCwd.mockReturnValue('/test/path'); onTrustChange = vi.fn(); });