mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 00:01:24 -07:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0612839ab6 | |||
| 9b2d4c6180 | |||
| a1cc5ac2b3 | |||
| a0987602b6 | |||
| 024aaf95ec | |||
| a07f40a753 | |||
| eac33bf9e7 | |||
| 589f037b2d | |||
| 11f7a6a2d6 | |||
| aaca0bfbd6 | |||
| 97f8268998 | |||
| 17f9d949ff |
+1
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
Generated
+553
-32
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.8.0-nightly.20250925.b1da8c21",
|
||||
"version": "0.8.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.8.0-nightly.20250925.b1da8c21",
|
||||
"version": "0.8.2",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -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",
|
||||
@@ -16881,7 +17397,7 @@
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.8.0-nightly.20250925.b1da8c21",
|
||||
"version": "0.8.2",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.2",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
@@ -17152,7 +17668,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.8.0-nightly.20250925.b1da8c21",
|
||||
"version": "0.8.2",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"@google/genai": "1.16.0",
|
||||
@@ -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",
|
||||
@@ -17259,7 +17780,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.8.0-nightly.20250925.b1da8c21",
|
||||
"version": "0.8.2",
|
||||
"dependencies": {
|
||||
"@google-cloud/logging": "^11.2.1",
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
|
||||
@@ -17398,7 +17919,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.8.0-nightly.20250925.b1da8c21",
|
||||
"version": "0.8.2",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
@@ -17409,7 +17930,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.8.0-nightly.20250925.b1da8c21",
|
||||
"version": "0.8.2",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.8.0-nightly.20250925.b1da8c21",
|
||||
"version": "0.8.2",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.8.0-nightly.20250925.b1da8c21"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.8.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.8.0-nightly.20250925.b1da8c21",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.8.0-nightly.20250925.b1da8c21",
|
||||
"version": "0.8.2",
|
||||
"description": "Gemini CLI",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,7 +25,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.8.0-nightly.20250925.b1da8c21"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.8.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -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",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "context-example",
|
||||
"version": "1.0.0",
|
||||
"contextFileName": "GEMINI.md"
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 [<source>] [--path] [--ref] [--auto-update]',
|
||||
command: 'install <source>',
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -30,15 +30,22 @@ describe('extensions new command', () => {
|
||||
it('should fail if no path is provided', async () => {
|
||||
const parser = yargs([]).command(newCommand).fail(false).locale('en');
|
||||
await expect(parser.parseAsync('new')).rejects.toThrow(
|
||||
'Not enough non-option arguments: got 0, need at least 2',
|
||||
'Not enough non-option arguments: got 0, need at least 1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail if no template is provided', async () => {
|
||||
const parser = yargs([]).command(newCommand).fail(false).locale('en');
|
||||
await expect(parser.parseAsync('new /some/path')).rejects.toThrow(
|
||||
'Not enough non-option arguments: got 1, need at least 2',
|
||||
);
|
||||
it('should create directory when no template is provided', async () => {
|
||||
mockedFs.access.mockRejectedValue(new Error('ENOENT'));
|
||||
mockedFs.mkdir.mockResolvedValue(undefined);
|
||||
|
||||
const parser = yargs([]).command(newCommand).fail(false);
|
||||
|
||||
await parser.parseAsync('new /some/path');
|
||||
|
||||
expect(mockedFs.mkdir).toHaveBeenCalledWith('/some/path', {
|
||||
recursive: true,
|
||||
});
|
||||
expect(mockedFs.cp).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create directory and copy files when path does not exist', async () => {
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { access, cp, mkdir, readdir } from 'node:fs/promises';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { access, cp, mkdir, readdir, writeFile } from 'node:fs/promises';
|
||||
import { join, dirname, basename } from 'node:path';
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
|
||||
interface NewArgs {
|
||||
path: string;
|
||||
template: string;
|
||||
template?: string;
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -29,13 +29,17 @@ async function pathExists(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyDirectory(template: string, path: string) {
|
||||
async function createDirectory(path: string) {
|
||||
if (await pathExists(path)) {
|
||||
throw new Error(`Path already exists: ${path}`);
|
||||
}
|
||||
await mkdir(path, { recursive: true });
|
||||
}
|
||||
|
||||
async function copyDirectory(template: string, path: string) {
|
||||
await createDirectory(path);
|
||||
|
||||
const examplePath = join(EXAMPLES_PATH, template);
|
||||
await mkdir(path, { recursive: true });
|
||||
const entries = await readdir(examplePath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const srcPath = join(examplePath, entry.name);
|
||||
@@ -46,10 +50,24 @@ async function copyDirectory(template: string, path: string) {
|
||||
|
||||
async function handleNew(args: NewArgs) {
|
||||
try {
|
||||
await copyDirectory(args.template, args.path);
|
||||
console.log(
|
||||
`Successfully created new extension from template "${args.template}" at ${args.path}.`,
|
||||
);
|
||||
if (args.template) {
|
||||
await copyDirectory(args.template, args.path);
|
||||
console.log(
|
||||
`Successfully created new extension from template "${args.template}" at ${args.path}.`,
|
||||
);
|
||||
} else {
|
||||
await createDirectory(args.path);
|
||||
const extensionName = basename(args.path);
|
||||
const manifest = {
|
||||
name: extensionName,
|
||||
version: '1.0.0',
|
||||
};
|
||||
await writeFile(
|
||||
join(args.path, 'gemini-extension.json'),
|
||||
JSON.stringify(manifest, null, 2),
|
||||
);
|
||||
console.log(`Successfully created new extension at ${args.path}.`);
|
||||
}
|
||||
console.log(
|
||||
`You can install this using "gemini extensions link ${args.path}" to test it out.`,
|
||||
);
|
||||
@@ -67,7 +85,7 @@ async function getBoilerplateChoices() {
|
||||
}
|
||||
|
||||
export const newCommand: CommandModule = {
|
||||
command: 'new <path> <template>',
|
||||
command: 'new <path> [template]',
|
||||
describe: 'Create a new extension from a boilerplate example.',
|
||||
builder: async (yargs) => {
|
||||
const choices = await getBoilerplateChoices();
|
||||
@@ -85,7 +103,7 @@ export const newCommand: CommandModule = {
|
||||
handler: async (args) => {
|
||||
await handleNew({
|
||||
path: args['path'] as string,
|
||||
template: args['template'] as string,
|
||||
template: args['template'] as string | undefined,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -91,11 +91,20 @@ export async function handleUpdate(args: UpdateArgs) {
|
||||
}
|
||||
if (args.all) {
|
||||
try {
|
||||
const extensionState = new Map();
|
||||
await checkForAllExtensionUpdates(extensions, (action) => {
|
||||
if (action.type === 'SET_STATE') {
|
||||
extensionState.set(action.payload.name, {
|
||||
status: action.payload.state,
|
||||
processed: true, // No need to process as we will force the update.
|
||||
});
|
||||
}
|
||||
});
|
||||
let updateInfos = await updateAllUpdatableExtensions(
|
||||
workingDir,
|
||||
requestConsentNonInteractive,
|
||||
extensions,
|
||||
await checkForAllExtensionUpdates(extensions, new Map(), (_) => {}),
|
||||
extensionState,
|
||||
() => {},
|
||||
);
|
||||
updateInfos = updateInfos.filter(
|
||||
|
||||
@@ -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:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<void> {
|
||||
});
|
||||
}
|
||||
|
||||
function extractFile(file: string, dest: string) {
|
||||
let args: string[];
|
||||
let command: 'tar' | 'unzip';
|
||||
export async function extractFile(file: string, dest: string): Promise<void> {
|
||||
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()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,8 +185,7 @@ describe('update tests', () => {
|
||||
});
|
||||
mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]);
|
||||
|
||||
const setExtensionUpdateState = vi.fn();
|
||||
|
||||
const dispatch = vi.fn();
|
||||
const extension = annotateActiveExtensions(
|
||||
[
|
||||
loadExtension({
|
||||
@@ -202,15 +201,23 @@ describe('update tests', () => {
|
||||
tempHomeDir,
|
||||
async (_) => true,
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
setExtensionUpdateState,
|
||||
dispatch,
|
||||
);
|
||||
|
||||
expect(setExtensionUpdateState).toHaveBeenCalledWith(
|
||||
ExtensionUpdateState.UPDATING,
|
||||
);
|
||||
expect(setExtensionUpdateState).toHaveBeenCalledWith(
|
||||
ExtensionUpdateState.UPDATED_NEEDS_RESTART,
|
||||
);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: extensionName,
|
||||
state: ExtensionUpdateState.UPDATING,
|
||||
},
|
||||
});
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: extensionName,
|
||||
state: ExtensionUpdateState.UPDATED_NEEDS_RESTART,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call setExtensionUpdateState with ERROR on failure', async () => {
|
||||
@@ -228,7 +235,7 @@ describe('update tests', () => {
|
||||
mockGit.clone.mockRejectedValue(new Error('Git clone failed'));
|
||||
mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]);
|
||||
|
||||
const setExtensionUpdateState = vi.fn();
|
||||
const dispatch = vi.fn();
|
||||
const extension = annotateActiveExtensions(
|
||||
[
|
||||
loadExtension({
|
||||
@@ -245,16 +252,24 @@ describe('update tests', () => {
|
||||
tempHomeDir,
|
||||
async (_) => true,
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
setExtensionUpdateState,
|
||||
dispatch,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(setExtensionUpdateState).toHaveBeenCalledWith(
|
||||
ExtensionUpdateState.UPDATING,
|
||||
);
|
||||
expect(setExtensionUpdateState).toHaveBeenCalledWith(
|
||||
ExtensionUpdateState.ERROR,
|
||||
);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: extensionName,
|
||||
state: ExtensionUpdateState.UPDATING,
|
||||
},
|
||||
});
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: extensionName,
|
||||
state: ExtensionUpdateState.ERROR,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -286,20 +301,15 @@ describe('update tests', () => {
|
||||
mockGit.listRemote.mockResolvedValue('remoteHash HEAD');
|
||||
mockGit.revparse.mockResolvedValue('localHash');
|
||||
|
||||
let extensionState = new Map();
|
||||
const results = await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionState,
|
||||
(newState) => {
|
||||
if (typeof newState === 'function') {
|
||||
newState(extensionState);
|
||||
} else {
|
||||
extensionState = newState;
|
||||
}
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates([extension], dispatch);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: 'test-extension',
|
||||
state: ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
},
|
||||
);
|
||||
const result = results.get('test-extension');
|
||||
expect(result).toBe(ExtensionUpdateState.UPDATE_AVAILABLE);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return UpToDate for a git extension with no updates', async () => {
|
||||
@@ -329,20 +339,15 @@ describe('update tests', () => {
|
||||
mockGit.listRemote.mockResolvedValue('sameHash HEAD');
|
||||
mockGit.revparse.mockResolvedValue('sameHash');
|
||||
|
||||
let extensionState = new Map();
|
||||
const results = await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionState,
|
||||
(newState) => {
|
||||
if (typeof newState === 'function') {
|
||||
newState(extensionState);
|
||||
} else {
|
||||
extensionState = newState;
|
||||
}
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates([extension], dispatch);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: 'test-extension',
|
||||
state: ExtensionUpdateState.UP_TO_DATE,
|
||||
},
|
||||
);
|
||||
const result = results.get('test-extension');
|
||||
expect(result).toBe(ExtensionUpdateState.UP_TO_DATE);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return UpToDate for a local extension with no updates', async () => {
|
||||
@@ -369,21 +374,15 @@ describe('update tests', () => {
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
)[0];
|
||||
let extensionState = new Map();
|
||||
const results = await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionState,
|
||||
(newState) => {
|
||||
if (typeof newState === 'function') {
|
||||
newState(extensionState);
|
||||
} else {
|
||||
extensionState = newState;
|
||||
}
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates([extension], dispatch);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: 'local-extension',
|
||||
state: ExtensionUpdateState.UP_TO_DATE,
|
||||
},
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
const result = results.get('local-extension');
|
||||
expect(result).toBe(ExtensionUpdateState.UP_TO_DATE);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return UpdateAvailable for a local extension with updates', async () => {
|
||||
@@ -410,21 +409,15 @@ describe('update tests', () => {
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
)[0];
|
||||
let extensionState = new Map();
|
||||
const results = await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionState,
|
||||
(newState) => {
|
||||
if (typeof newState === 'function') {
|
||||
newState(extensionState);
|
||||
} else {
|
||||
extensionState = newState;
|
||||
}
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates([extension], dispatch);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: 'local-extension',
|
||||
state: ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
},
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
const result = results.get('local-extension');
|
||||
expect(result).toBe(ExtensionUpdateState.UPDATE_AVAILABLE);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Error when git check fails', async () => {
|
||||
@@ -450,20 +443,15 @@ describe('update tests', () => {
|
||||
|
||||
mockGit.getRemotes.mockRejectedValue(new Error('Git error'));
|
||||
|
||||
let extensionState = new Map();
|
||||
const results = await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionState,
|
||||
(newState) => {
|
||||
if (typeof newState === 'function') {
|
||||
newState(extensionState);
|
||||
} else {
|
||||
extensionState = newState;
|
||||
}
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates([extension], dispatch);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: 'error-extension',
|
||||
state: ExtensionUpdateState.ERROR,
|
||||
},
|
||||
);
|
||||
const result = results.get('error-extension');
|
||||
expect(result).toBe(ExtensionUpdateState.ERROR);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
|
||||
import { type Dispatch, type SetStateAction } from 'react';
|
||||
import {
|
||||
type ExtensionUpdateAction,
|
||||
ExtensionUpdateState,
|
||||
type ExtensionUpdateStatus,
|
||||
} from '../../ui/state/extensions.js';
|
||||
import {
|
||||
copyExtension,
|
||||
installExtension,
|
||||
@@ -19,6 +19,9 @@ import {
|
||||
loadExtensionConfig,
|
||||
} from '../extension.js';
|
||||
import { checkForExtensionUpdate } from './github.js';
|
||||
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
|
||||
export interface ExtensionUpdateInfo {
|
||||
name: string;
|
||||
@@ -31,22 +34,31 @@ export async function updateExtension(
|
||||
cwd: string = process.cwd(),
|
||||
requestConsent: (consent: string) => Promise<boolean>,
|
||||
currentState: ExtensionUpdateState,
|
||||
setExtensionUpdateState: (updateState: ExtensionUpdateState) => void,
|
||||
dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void,
|
||||
): Promise<ExtensionUpdateInfo | undefined> {
|
||||
if (currentState === ExtensionUpdateState.UPDATING) {
|
||||
return undefined;
|
||||
}
|
||||
setExtensionUpdateState(ExtensionUpdateState.UPDATING);
|
||||
dispatchExtensionStateUpdate({
|
||||
type: 'SET_STATE',
|
||||
payload: { name: extension.name, state: ExtensionUpdateState.UPDATING },
|
||||
});
|
||||
const installMetadata = loadInstallMetadata(extension.path);
|
||||
|
||||
if (!installMetadata?.type) {
|
||||
setExtensionUpdateState(ExtensionUpdateState.ERROR);
|
||||
dispatchExtensionStateUpdate({
|
||||
type: 'SET_STATE',
|
||||
payload: { name: extension.name, state: ExtensionUpdateState.ERROR },
|
||||
});
|
||||
throw new Error(
|
||||
`Extension ${extension.name} cannot be updated, type is unknown.`,
|
||||
);
|
||||
}
|
||||
if (installMetadata?.type === 'link') {
|
||||
setExtensionUpdateState(ExtensionUpdateState.UP_TO_DATE);
|
||||
dispatchExtensionStateUpdate({
|
||||
type: 'SET_STATE',
|
||||
payload: { name: extension.name, state: ExtensionUpdateState.UP_TO_DATE },
|
||||
});
|
||||
throw new Error(`Extension is linked so does not need to be updated`);
|
||||
}
|
||||
const originalVersion = extension.version;
|
||||
@@ -72,11 +84,20 @@ export async function updateExtension(
|
||||
workspaceDir: cwd,
|
||||
});
|
||||
if (!updatedExtension) {
|
||||
setExtensionUpdateState(ExtensionUpdateState.ERROR);
|
||||
dispatchExtensionStateUpdate({
|
||||
type: 'SET_STATE',
|
||||
payload: { name: extension.name, state: ExtensionUpdateState.ERROR },
|
||||
});
|
||||
throw new Error('Updated extension not found after installation.');
|
||||
}
|
||||
const updatedVersion = updatedExtension.config.version;
|
||||
setExtensionUpdateState(ExtensionUpdateState.UPDATED_NEEDS_RESTART);
|
||||
dispatchExtensionStateUpdate({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: extension.name,
|
||||
state: ExtensionUpdateState.UPDATED_NEEDS_RESTART,
|
||||
},
|
||||
});
|
||||
return {
|
||||
name: extension.name,
|
||||
originalVersion,
|
||||
@@ -86,7 +107,10 @@ export async function updateExtension(
|
||||
console.error(
|
||||
`Error updating extension, rolling back. ${getErrorMessage(e)}`,
|
||||
);
|
||||
setExtensionUpdateState(ExtensionUpdateState.ERROR);
|
||||
dispatchExtensionStateUpdate({
|
||||
type: 'SET_STATE',
|
||||
payload: { name: extension.name, state: ExtensionUpdateState.ERROR },
|
||||
});
|
||||
await copyExtension(tempDir, extension.path);
|
||||
throw e;
|
||||
} finally {
|
||||
@@ -98,17 +122,15 @@ export async function updateAllUpdatableExtensions(
|
||||
cwd: string = process.cwd(),
|
||||
requestConsent: (consent: string) => Promise<boolean>,
|
||||
extensions: GeminiCLIExtension[],
|
||||
extensionsState: Map<string, ExtensionUpdateState>,
|
||||
setExtensionsUpdateState: Dispatch<
|
||||
SetStateAction<Map<string, ExtensionUpdateState>>
|
||||
>,
|
||||
extensionsState: Map<string, ExtensionUpdateStatus>,
|
||||
dispatch: (action: ExtensionUpdateAction) => void,
|
||||
): Promise<ExtensionUpdateInfo[]> {
|
||||
return (
|
||||
await Promise.all(
|
||||
extensions
|
||||
.filter(
|
||||
(extension) =>
|
||||
extensionsState.get(extension.name) ===
|
||||
extensionsState.get(extension.name)?.status ===
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
)
|
||||
.map((extension) =>
|
||||
@@ -116,14 +138,8 @@ export async function updateAllUpdatableExtensions(
|
||||
extension,
|
||||
cwd,
|
||||
requestConsent,
|
||||
extensionsState.get(extension.name)!,
|
||||
(updateState) => {
|
||||
setExtensionsUpdateState((prev) => {
|
||||
const finalState = new Map(prev);
|
||||
finalState.set(extension.name, updateState);
|
||||
return finalState;
|
||||
});
|
||||
},
|
||||
extensionsState.get(extension.name)!.status,
|
||||
dispatch,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -137,38 +153,30 @@ export interface ExtensionUpdateCheckResult {
|
||||
|
||||
export async function checkForAllExtensionUpdates(
|
||||
extensions: GeminiCLIExtension[],
|
||||
extensionsUpdateState: Map<string, ExtensionUpdateState>,
|
||||
setExtensionsUpdateState: Dispatch<
|
||||
SetStateAction<Map<string, ExtensionUpdateState>>
|
||||
>,
|
||||
cwd: string = process.cwd(),
|
||||
): Promise<Map<string, ExtensionUpdateState>> {
|
||||
let newStates: Map<string, ExtensionUpdateState> = new Map(
|
||||
extensionsUpdateState,
|
||||
);
|
||||
dispatch: (action: ExtensionUpdateAction) => void,
|
||||
): Promise<void> {
|
||||
dispatch({ type: 'BATCH_CHECK_START' });
|
||||
const promises: Array<Promise<void>> = [];
|
||||
for (const extension of extensions) {
|
||||
const initialState = extensionsUpdateState.get(extension.name);
|
||||
if (initialState === undefined) {
|
||||
if (!extension.installMetadata) {
|
||||
setExtensionsUpdateState((prev) => {
|
||||
newStates = new Map(prev);
|
||||
newStates.set(extension.name, ExtensionUpdateState.NOT_UPDATABLE);
|
||||
return newStates;
|
||||
});
|
||||
continue;
|
||||
}
|
||||
await checkForExtensionUpdate(
|
||||
extension,
|
||||
(updatedState) => {
|
||||
setExtensionsUpdateState((prev) => {
|
||||
newStates = new Map(prev);
|
||||
newStates.set(extension.name, updatedState);
|
||||
return newStates;
|
||||
});
|
||||
if (!extension.installMetadata) {
|
||||
dispatch({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: extension.name,
|
||||
state: ExtensionUpdateState.NOT_UPDATABLE,
|
||||
},
|
||||
cwd,
|
||||
);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
promises.push(
|
||||
checkForExtensionUpdate(extension, (updatedState) => {
|
||||
dispatch({
|
||||
type: 'SET_STATE',
|
||||
payload: { name: extension.name, state: updatedState },
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
return newStates;
|
||||
await Promise.all(promises);
|
||||
dispatch({ type: 'BATCH_CHECK_END' });
|
||||
}
|
||||
|
||||
@@ -155,7 +155,8 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
const extensions = config.getExtensions();
|
||||
const {
|
||||
extensionsUpdateState,
|
||||
setExtensionsUpdateState,
|
||||
extensionsUpdateStateInternal,
|
||||
dispatchExtensionStateUpdate,
|
||||
confirmUpdateExtensionRequests,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
} = useExtensionUpdates(
|
||||
@@ -459,7 +460,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
},
|
||||
setDebugMessage,
|
||||
toggleCorgiMode: () => setCorgiMode((prev) => !prev),
|
||||
setExtensionsUpdateState,
|
||||
dispatchExtensionStateUpdate,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
}),
|
||||
[
|
||||
@@ -472,7 +473,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
setDebugMessage,
|
||||
setShowPrivacyNotice,
|
||||
setCorgiMode,
|
||||
setExtensionsUpdateState,
|
||||
dispatchExtensionStateUpdate,
|
||||
openPermissionsDialog,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
],
|
||||
@@ -496,7 +497,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
setIsProcessing,
|
||||
setGeminiMdFileCount,
|
||||
slashCommandActions,
|
||||
extensionsUpdateState,
|
||||
extensionsUpdateStateInternal,
|
||||
isConfigInitialized,
|
||||
);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import { ExtensionUpdateState } from '../state/extensions.js';
|
||||
vi.mock('../../config/extensions/update.js', () => ({
|
||||
updateExtension: vi.fn(),
|
||||
updateAllUpdatableExtensions: vi.fn(),
|
||||
checkForAllExtensionUpdates: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockUpdateExtension = updateExtension as MockedFunction<
|
||||
@@ -51,6 +52,9 @@ describe('extensionsCommand', () => {
|
||||
getWorkingDir: () => '/test/dir',
|
||||
},
|
||||
},
|
||||
ui: {
|
||||
dispatchExtensionStateUpdate: vi.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,10 +172,10 @@ describe('extensionsCommand', () => {
|
||||
updatedVersion: '1.0.1',
|
||||
});
|
||||
mockGetExtensions.mockReturnValue([extension]);
|
||||
mockContext.ui.extensionsUpdateState.set(
|
||||
extension.name,
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
);
|
||||
mockContext.ui.extensionsUpdateState.set(extension.name, {
|
||||
status: ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
processed: false,
|
||||
});
|
||||
await updateAction(mockContext, 'ext-one');
|
||||
expect(mockUpdateExtension).toHaveBeenCalledWith(
|
||||
extension,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
updateAllUpdatableExtensions,
|
||||
type ExtensionUpdateInfo,
|
||||
updateExtension,
|
||||
checkForAllExtensionUpdates,
|
||||
} from '../../config/extensions/update.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ExtensionUpdateState } from '../state/extensions.js';
|
||||
@@ -46,6 +47,10 @@ async function updateAction(context: CommandContext, args: string) {
|
||||
}
|
||||
|
||||
try {
|
||||
await checkForAllExtensionUpdates(
|
||||
context.services.config!.getExtensions(),
|
||||
context.ui.dispatchExtensionStateUpdate,
|
||||
);
|
||||
context.ui.setPendingItem({
|
||||
type: MessageType.EXTENSIONS_LIST,
|
||||
});
|
||||
@@ -60,7 +65,7 @@ async function updateAction(context: CommandContext, args: string) {
|
||||
),
|
||||
context.services.config!.getExtensions(),
|
||||
context.ui.extensionsUpdateState,
|
||||
context.ui.setExtensionsUpdateState,
|
||||
context.ui.dispatchExtensionStateUpdate,
|
||||
);
|
||||
} else if (names?.length) {
|
||||
const workingDir = context.services.config!.getWorkingDir();
|
||||
@@ -87,15 +92,9 @@ async function updateAction(context: CommandContext, args: string) {
|
||||
description,
|
||||
context.ui.addConfirmUpdateExtensionRequest,
|
||||
),
|
||||
context.ui.extensionsUpdateState.get(extension.name) ??
|
||||
context.ui.extensionsUpdateState.get(extension.name)?.status ??
|
||||
ExtensionUpdateState.UNKNOWN,
|
||||
(updateState) => {
|
||||
context.ui.setExtensionsUpdateState((prev) => {
|
||||
const newState = new Map(prev);
|
||||
newState.set(name, updateState);
|
||||
return newState;
|
||||
});
|
||||
},
|
||||
context.ui.dispatchExtensionStateUpdate,
|
||||
);
|
||||
if (updateInfo) updateInfos.push(updateInfo);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Dispatch, ReactNode, SetStateAction } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { Content, PartListUnion } from '@google/genai';
|
||||
import type {
|
||||
HistoryItemWithoutId,
|
||||
@@ -15,7 +15,10 @@ import type { Config, GitService, Logger } from '@google/gemini-cli-core';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import type { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js';
|
||||
import type { SessionStatsState } from '../contexts/SessionContext.js';
|
||||
import type { ExtensionUpdateState } from '../state/extensions.js';
|
||||
import type {
|
||||
ExtensionUpdateAction,
|
||||
ExtensionUpdateStatus,
|
||||
} from '../state/extensions.js';
|
||||
|
||||
// Grouped dependencies for clarity and easier mocking
|
||||
export interface CommandContext {
|
||||
@@ -66,10 +69,8 @@ export interface CommandContext {
|
||||
toggleVimEnabled: () => Promise<boolean>;
|
||||
setGeminiMdFileCount: (count: number) => void;
|
||||
reloadCommands: () => void;
|
||||
extensionsUpdateState: Map<string, ExtensionUpdateState>;
|
||||
setExtensionsUpdateState: Dispatch<
|
||||
SetStateAction<Map<string, ExtensionUpdateState>>
|
||||
>;
|
||||
extensionsUpdateState: Map<string, ExtensionUpdateStatus>;
|
||||
dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void;
|
||||
addConfirmUpdateExtensionRequest: (value: ConfirmationRequest) => void;
|
||||
};
|
||||
// Session-specific data
|
||||
|
||||
@@ -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<typeof import('node:process')>('node:process');
|
||||
return {
|
||||
...actual,
|
||||
exit: mockedExit,
|
||||
|
||||
@@ -26,8 +26,8 @@ import type {
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { DOMElement } from 'ink';
|
||||
import type { SessionStatsState } from '../contexts/SessionContext.js';
|
||||
import type { UpdateObject } from '../utils/updateCheck.js';
|
||||
import type { ExtensionUpdateState } from '../state/extensions.js';
|
||||
import type { UpdateObject } from '../utils/updateCheck.js';
|
||||
|
||||
export interface ProQuotaDialogRequest {
|
||||
failedModel: string;
|
||||
|
||||
@@ -4,14 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useEffect,
|
||||
useState,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
} from 'react';
|
||||
import { useCallback, useMemo, useEffect, useState } from 'react';
|
||||
import { type PartListUnion } from '@google/genai';
|
||||
import process from 'node:process';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
@@ -42,7 +35,10 @@ import { BuiltinCommandLoader } from '../../services/BuiltinCommandLoader.js';
|
||||
import { FileCommandLoader } from '../../services/FileCommandLoader.js';
|
||||
import { McpPromptLoader } from '../../services/McpPromptLoader.js';
|
||||
import { parseSlashCommand } from '../../utils/commands.js';
|
||||
import type { ExtensionUpdateState } from '../state/extensions.js';
|
||||
import {
|
||||
type ExtensionUpdateAction,
|
||||
type ExtensionUpdateStatus,
|
||||
} from '../state/extensions.js';
|
||||
|
||||
interface SlashCommandProcessorActions {
|
||||
openAuthDialog: () => void;
|
||||
@@ -55,9 +51,7 @@ interface SlashCommandProcessorActions {
|
||||
quit: (messages: HistoryItem[]) => void;
|
||||
setDebugMessage: (message: string) => void;
|
||||
toggleCorgiMode: () => void;
|
||||
setExtensionsUpdateState: Dispatch<
|
||||
SetStateAction<Map<string, ExtensionUpdateState>>
|
||||
>;
|
||||
dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void;
|
||||
addConfirmUpdateExtensionRequest: (request: ConfirmationRequest) => void;
|
||||
}
|
||||
|
||||
@@ -75,7 +69,7 @@ export const useSlashCommandProcessor = (
|
||||
setIsProcessing: (isProcessing: boolean) => void,
|
||||
setGeminiMdFileCount: (count: number) => void,
|
||||
actions: SlashCommandProcessorActions,
|
||||
extensionsUpdateState: Map<string, ExtensionUpdateState>,
|
||||
extensionsUpdateState: Map<string, ExtensionUpdateStatus>,
|
||||
isConfigInitialized: boolean,
|
||||
) => {
|
||||
const session = useSessionStats();
|
||||
@@ -207,7 +201,7 @@ export const useSlashCommandProcessor = (
|
||||
setGeminiMdFileCount,
|
||||
reloadCommands,
|
||||
extensionsUpdateState,
|
||||
setExtensionsUpdateState: actions.setExtensionsUpdateState,
|
||||
dispatchExtensionStateUpdate: actions.dispatchExtensionStateUpdate,
|
||||
addConfirmUpdateExtensionRequest:
|
||||
actions.addConfirmUpdateExtensionRequest,
|
||||
},
|
||||
|
||||
@@ -9,7 +9,6 @@ import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import {
|
||||
EXTENSIONS_CONFIG_FILENAME,
|
||||
ExtensionStorage,
|
||||
annotateActiveExtensions,
|
||||
loadExtension,
|
||||
@@ -17,29 +16,14 @@ import {
|
||||
import { createExtension } from '../../test-utils/createExtension.js';
|
||||
import { useExtensionUpdates } from './useExtensionUpdates.js';
|
||||
import { GEMINI_DIR, type GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import { isWorkspaceTrusted } from '../../config/trustedFolders.js';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { MessageType } from '../types.js';
|
||||
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
|
||||
|
||||
const mockGit = {
|
||||
clone: vi.fn(),
|
||||
getRemotes: vi.fn(),
|
||||
fetch: vi.fn(),
|
||||
checkout: vi.fn(),
|
||||
listRemote: vi.fn(),
|
||||
revparse: vi.fn(),
|
||||
// Not a part of the actual API, but we need to use this to do the correct
|
||||
// file system interactions.
|
||||
path: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('simple-git', () => ({
|
||||
simpleGit: vi.fn((path: string) => {
|
||||
mockGit.path.mockReturnValue(path);
|
||||
return mockGit;
|
||||
}),
|
||||
}));
|
||||
import {
|
||||
checkForAllExtensionUpdates,
|
||||
updateExtension,
|
||||
} from '../../config/extensions/update.js';
|
||||
import { ExtensionUpdateState } from '../state/extensions.js';
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const mockedOs = await importOriginal<typeof os>();
|
||||
@@ -49,45 +33,9 @@ vi.mock('os', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../config/trustedFolders.js', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../config/trustedFolders.js')>();
|
||||
return {
|
||||
...actual,
|
||||
isWorkspaceTrusted: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockLogExtensionInstallEvent = vi.hoisted(() => vi.fn());
|
||||
const mockLogExtensionUninstall = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
logExtensionInstallEvent: mockLogExtensionInstallEvent,
|
||||
logExtensionUninstall: mockLogExtensionUninstall,
|
||||
ExtensionInstallEvent: vi.fn(),
|
||||
ExtensionUninstallEvent: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('child_process', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('child_process')>();
|
||||
return {
|
||||
...actual,
|
||||
execSync: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockQuestion = vi.hoisted(() => vi.fn());
|
||||
const mockClose = vi.hoisted(() => vi.fn());
|
||||
vi.mock('node:readline', () => ({
|
||||
createInterface: vi.fn(() => ({
|
||||
question: mockQuestion,
|
||||
close: mockClose,
|
||||
})),
|
||||
vi.mock('../../config/extensions/update.js', () => ({
|
||||
checkForAllExtensionUpdates: vi.fn(),
|
||||
updateExtension: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('useExtensionUpdates', () => {
|
||||
@@ -101,7 +49,8 @@ describe('useExtensionUpdates', () => {
|
||||
vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
|
||||
userExtensionsDir = path.join(tempHomeDir, GEMINI_DIR, 'extensions');
|
||||
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
||||
Object.values(mockGit).forEach((fn) => fn.mockReset());
|
||||
vi.mocked(checkForAllExtensionUpdates).mockReset();
|
||||
vi.mocked(updateExtension).mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -126,16 +75,17 @@ describe('useExtensionUpdates', () => {
|
||||
const addItem = vi.fn();
|
||||
const cwd = '/test/cwd';
|
||||
|
||||
mockGit.getRemotes.mockResolvedValue([
|
||||
{
|
||||
name: 'origin',
|
||||
refs: {
|
||||
fetch: 'https://github.com/google/gemini-cli.git',
|
||||
},
|
||||
vi.mocked(checkForAllExtensionUpdates).mockImplementation(
|
||||
async (extensions, dispatch) => {
|
||||
dispatch({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: 'test-extension',
|
||||
state: ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
},
|
||||
});
|
||||
},
|
||||
]);
|
||||
mockGit.revparse.mockResolvedValue('local-hash');
|
||||
mockGit.listRemote.mockResolvedValue('remote-hash\tHEAD');
|
||||
);
|
||||
|
||||
renderHook(() =>
|
||||
useExtensionUpdates(extensions as GeminiCLIExtension[], addItem, cwd),
|
||||
@@ -170,28 +120,23 @@ describe('useExtensionUpdates', () => {
|
||||
)[0];
|
||||
|
||||
const addItem = vi.fn();
|
||||
mockGit.getRemotes.mockResolvedValue([
|
||||
{
|
||||
name: 'origin',
|
||||
refs: {
|
||||
fetch: 'https://github.com/google/gemini-cli.git',
|
||||
},
|
||||
|
||||
vi.mocked(checkForAllExtensionUpdates).mockImplementation(
|
||||
async (extensions, dispatch) => {
|
||||
dispatch({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: 'test-extension',
|
||||
state: ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
},
|
||||
});
|
||||
},
|
||||
]);
|
||||
mockGit.revparse.mockResolvedValue('local-hash');
|
||||
mockGit.listRemote.mockResolvedValue('remote-hash\tHEAD');
|
||||
mockGit.clone.mockImplementation(async (_, destination) => {
|
||||
fs.mkdirSync(path.join(mockGit.path(), destination), {
|
||||
recursive: true,
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(mockGit.path(), destination, EXTENSIONS_CONFIG_FILENAME),
|
||||
JSON.stringify({ name: 'test-extension', version: '1.1.0' }),
|
||||
);
|
||||
});
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
);
|
||||
|
||||
vi.mocked(updateExtension).mockResolvedValue({
|
||||
originalVersion: '1.0.0',
|
||||
updatedVersion: '1.1.0',
|
||||
name: '',
|
||||
});
|
||||
|
||||
renderHook(() => useExtensionUpdates([extension], addItem, tempHomeDir));
|
||||
@@ -206,7 +151,169 @@ describe('useExtensionUpdates', () => {
|
||||
expect.any(Number),
|
||||
);
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
{ timeout: 4000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('should batch update notifications for multiple extensions', async () => {
|
||||
const extensionDir1 = createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'test-extension-1',
|
||||
version: '1.0.0',
|
||||
installMetadata: {
|
||||
source: 'https://some.git/repo1',
|
||||
type: 'git',
|
||||
autoUpdate: true,
|
||||
},
|
||||
});
|
||||
const extensionDir2 = createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'test-extension-2',
|
||||
version: '2.0.0',
|
||||
installMetadata: {
|
||||
source: 'https://some.git/repo2',
|
||||
type: 'git',
|
||||
autoUpdate: true,
|
||||
},
|
||||
});
|
||||
|
||||
const extensions = annotateActiveExtensions(
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir: extensionDir1,
|
||||
workspaceDir: tempHomeDir,
|
||||
})!,
|
||||
loadExtension({
|
||||
extensionDir: extensionDir2,
|
||||
workspaceDir: tempHomeDir,
|
||||
})!,
|
||||
],
|
||||
tempHomeDir,
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
);
|
||||
|
||||
const addItem = vi.fn();
|
||||
|
||||
vi.mocked(checkForAllExtensionUpdates).mockImplementation(
|
||||
async (extensions, dispatch) => {
|
||||
dispatch({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: 'test-extension-1',
|
||||
state: ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
},
|
||||
});
|
||||
dispatch({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: 'test-extension-2',
|
||||
state: ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(updateExtension)
|
||||
.mockResolvedValueOnce({
|
||||
originalVersion: '1.0.0',
|
||||
updatedVersion: '1.1.0',
|
||||
name: '',
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
originalVersion: '2.0.0',
|
||||
updatedVersion: '2.1.0',
|
||||
name: '',
|
||||
});
|
||||
|
||||
renderHook(() => useExtensionUpdates(extensions, addItem, tempHomeDir));
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(addItem).toHaveBeenCalledTimes(2);
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Extension "test-extension-1" successfully updated: 1.0.0 → 1.1.0.',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Extension "test-extension-2" successfully updated: 2.0.0 → 2.1.0.',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
},
|
||||
{ timeout: 4000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('should batch update notifications for multiple extensions with autoUpdate: false', async () => {
|
||||
const extensions = [
|
||||
{
|
||||
name: 'test-extension-1',
|
||||
type: 'git',
|
||||
version: '1.0.0',
|
||||
path: '/some/path1',
|
||||
isActive: true,
|
||||
installMetadata: {
|
||||
type: 'git',
|
||||
source: 'https://some/repo1',
|
||||
autoUpdate: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'test-extension-2',
|
||||
type: 'git',
|
||||
version: '2.0.0',
|
||||
path: '/some/path2',
|
||||
isActive: true,
|
||||
installMetadata: {
|
||||
type: 'git',
|
||||
source: 'https://some/repo2',
|
||||
autoUpdate: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
const addItem = vi.fn();
|
||||
const cwd = '/test/cwd';
|
||||
|
||||
vi.mocked(checkForAllExtensionUpdates).mockImplementation(
|
||||
async (extensions, dispatch) => {
|
||||
dispatch({ type: 'BATCH_CHECK_START' });
|
||||
dispatch({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: 'test-extension-1',
|
||||
state: ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
},
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
dispatch({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
name: 'test-extension-2',
|
||||
state: ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
},
|
||||
});
|
||||
dispatch({ type: 'BATCH_CHECK_END' });
|
||||
},
|
||||
);
|
||||
|
||||
renderHook(() =>
|
||||
useExtensionUpdates(extensions as GeminiCLIExtension[], addItem, cwd),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addItem).toHaveBeenCalledTimes(1);
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'You have 2 extensions with an update available, run "/extensions list" for more information.',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,12 @@
|
||||
|
||||
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ExtensionUpdateState } from '../state/extensions.js';
|
||||
import { useCallback, useState } from 'react';
|
||||
import {
|
||||
ExtensionUpdateState,
|
||||
extensionUpdatesReducer,
|
||||
initialExtensionUpdatesState,
|
||||
} from '../state/extensions.js';
|
||||
import { useCallback, useEffect, useMemo, useReducer } from 'react';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import { MessageType, type ConfirmationRequest } from '../types.js';
|
||||
import {
|
||||
@@ -15,118 +19,167 @@ import {
|
||||
updateExtension,
|
||||
} from '../../config/extensions/update.js';
|
||||
import { requestConsentInteractive } from '../../config/extension.js';
|
||||
import { checkExhaustive } from '../../utils/checks.js';
|
||||
|
||||
type ConfirmationRequestWrapper = {
|
||||
prompt: React.ReactNode;
|
||||
onConfirm: (confirmed: boolean) => void;
|
||||
};
|
||||
|
||||
type ConfirmationRequestAction =
|
||||
| { type: 'add'; request: ConfirmationRequestWrapper }
|
||||
| { type: 'remove'; request: ConfirmationRequestWrapper };
|
||||
|
||||
function confirmationRequestsReducer(
|
||||
state: ConfirmationRequestWrapper[],
|
||||
action: ConfirmationRequestAction,
|
||||
): ConfirmationRequestWrapper[] {
|
||||
switch (action.type) {
|
||||
case 'add':
|
||||
return [...state, action.request];
|
||||
case 'remove':
|
||||
return state.filter((r) => r !== action.request);
|
||||
default:
|
||||
checkExhaustive(action);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const useExtensionUpdates = (
|
||||
extensions: GeminiCLIExtension[],
|
||||
addItem: UseHistoryManagerReturn['addItem'],
|
||||
cwd: string,
|
||||
) => {
|
||||
const [extensionsUpdateState, setExtensionsUpdateState] = useState(
|
||||
new Map<string, ExtensionUpdateState>(),
|
||||
const [extensionsUpdateState, dispatchExtensionStateUpdate] = useReducer(
|
||||
extensionUpdatesReducer,
|
||||
initialExtensionUpdatesState,
|
||||
);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const [confirmUpdateExtensionRequests, setConfirmUpdateExtensionRequests] =
|
||||
useState<
|
||||
Array<{
|
||||
prompt: React.ReactNode;
|
||||
onConfirm: (confirmed: boolean) => void;
|
||||
}>
|
||||
>([]);
|
||||
const [
|
||||
confirmUpdateExtensionRequests,
|
||||
dispatchConfirmUpdateExtensionRequests,
|
||||
] = useReducer(confirmationRequestsReducer, []);
|
||||
const addConfirmUpdateExtensionRequest = useCallback(
|
||||
(original: ConfirmationRequest) => {
|
||||
const wrappedRequest = {
|
||||
prompt: original.prompt,
|
||||
onConfirm: (confirmed: boolean) => {
|
||||
// Remove it from the outstanding list of requests by identity.
|
||||
setConfirmUpdateExtensionRequests((prev) =>
|
||||
prev.filter((r) => r !== wrappedRequest),
|
||||
);
|
||||
dispatchConfirmUpdateExtensionRequests({
|
||||
type: 'remove',
|
||||
request: wrappedRequest,
|
||||
});
|
||||
original.onConfirm(confirmed);
|
||||
},
|
||||
};
|
||||
setConfirmUpdateExtensionRequests((prev) => [...prev, wrappedRequest]);
|
||||
dispatchConfirmUpdateExtensionRequests({
|
||||
type: 'add',
|
||||
request: wrappedRequest,
|
||||
});
|
||||
},
|
||||
[setConfirmUpdateExtensionRequests],
|
||||
[dispatchConfirmUpdateExtensionRequests],
|
||||
);
|
||||
|
||||
(async () => {
|
||||
if (isChecking) return;
|
||||
setIsChecking(true);
|
||||
try {
|
||||
const updateState = await checkForAllExtensionUpdates(
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await checkForAllExtensionUpdates(
|
||||
extensions,
|
||||
extensionsUpdateState,
|
||||
setExtensionsUpdateState,
|
||||
dispatchExtensionStateUpdate,
|
||||
);
|
||||
let extensionsWithUpdatesCount = 0;
|
||||
for (const extension of extensions) {
|
||||
const prevState = extensionsUpdateState.get(extension.name);
|
||||
const currentState = updateState.get(extension.name);
|
||||
if (
|
||||
prevState === currentState ||
|
||||
currentState !== ExtensionUpdateState.UPDATE_AVAILABLE
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (extension.installMetadata?.autoUpdate) {
|
||||
updateExtension(
|
||||
extension,
|
||||
cwd,
|
||||
(description) =>
|
||||
requestConsentInteractive(
|
||||
description,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
),
|
||||
currentState,
|
||||
(newState) => {
|
||||
setExtensionsUpdateState((prev) => {
|
||||
const finalState = new Map(prev);
|
||||
finalState.set(extension.name, newState);
|
||||
return finalState;
|
||||
});
|
||||
},
|
||||
)
|
||||
.then((result) => {
|
||||
if (!result) return;
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Extension "${extension.name}" successfully updated: ${result.originalVersion} → ${result.updatedVersion}.`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: getErrorMessage(error),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
extensionsWithUpdatesCount++;
|
||||
}
|
||||
}
|
||||
if (extensionsWithUpdatesCount > 0) {
|
||||
const s = extensionsWithUpdatesCount > 1 ? 's' : '';
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `You have ${extensionsWithUpdatesCount} extension${s} with an update available, run "/extensions list" for more information.`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
})();
|
||||
}, [extensions, extensions.length, dispatchExtensionStateUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (extensionsUpdateState.batchChecksInProgress > 0) {
|
||||
return;
|
||||
}
|
||||
})();
|
||||
|
||||
let extensionsWithUpdatesCount = 0;
|
||||
for (const extension of extensions) {
|
||||
const currentState = extensionsUpdateState.extensionStatuses.get(
|
||||
extension.name,
|
||||
);
|
||||
if (
|
||||
!currentState ||
|
||||
currentState.processed ||
|
||||
currentState.status !== ExtensionUpdateState.UPDATE_AVAILABLE
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mark as processed immediately to avoid re-triggering.
|
||||
dispatchExtensionStateUpdate({
|
||||
type: 'SET_PROCESSED',
|
||||
payload: { name: extension.name, processed: true },
|
||||
});
|
||||
|
||||
if (extension.installMetadata?.autoUpdate) {
|
||||
updateExtension(
|
||||
extension,
|
||||
cwd,
|
||||
(description) =>
|
||||
requestConsentInteractive(
|
||||
description,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
),
|
||||
currentState.status,
|
||||
dispatchExtensionStateUpdate,
|
||||
)
|
||||
.then((result) => {
|
||||
if (!result) return;
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Extension "${extension.name}" successfully updated: ${result.originalVersion} → ${result.updatedVersion}.`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: getErrorMessage(error),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
extensionsWithUpdatesCount++;
|
||||
}
|
||||
}
|
||||
if (extensionsWithUpdatesCount > 0) {
|
||||
const s = extensionsWithUpdatesCount > 1 ? 's' : '';
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `You have ${extensionsWithUpdatesCount} extension${s} with an update available, run "/extensions list" for more information.`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
}, [
|
||||
extensions,
|
||||
extensionsUpdateState,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
addItem,
|
||||
cwd,
|
||||
]);
|
||||
|
||||
const extensionsUpdateStateComputed = useMemo(() => {
|
||||
const result = new Map<string, ExtensionUpdateState>();
|
||||
for (const [
|
||||
key,
|
||||
value,
|
||||
] of extensionsUpdateState.extensionStatuses.entries()) {
|
||||
result.set(key, value.status);
|
||||
}
|
||||
return result;
|
||||
}, [extensionsUpdateState]);
|
||||
|
||||
return {
|
||||
extensionsUpdateState,
|
||||
setExtensionsUpdateState,
|
||||
extensionsUpdateState: extensionsUpdateStateComputed,
|
||||
extensionsUpdateStateInternal: extensionsUpdateState.extensionStatuses,
|
||||
dispatchExtensionStateUpdate,
|
||||
confirmUpdateExtensionRequests,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
};
|
||||
|
||||
@@ -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<typeof import('node:process')>('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();
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { CommandContext } from '../commands/types.js';
|
||||
import type { ExtensionUpdateAction } from '../state/extensions.js';
|
||||
|
||||
/**
|
||||
* Creates a UI context object with no-op functions.
|
||||
@@ -24,7 +25,7 @@ export function createNonInteractiveUI(): CommandContext['ui'] {
|
||||
setGeminiMdFileCount: (_count) => {},
|
||||
reloadCommands: () => {},
|
||||
extensionsUpdateState: new Map(),
|
||||
setExtensionsUpdateState: (_updateState) => {},
|
||||
dispatchExtensionStateUpdate: (_action: ExtensionUpdateAction) => {},
|
||||
addConfirmUpdateExtensionRequest: (_request) => {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { checkExhaustive } from '../../utils/checks.js';
|
||||
|
||||
export enum ExtensionUpdateState {
|
||||
CHECKING_FOR_UPDATES = 'checking for updates',
|
||||
UPDATED_NEEDS_RESTART = 'updated, needs restart',
|
||||
@@ -14,3 +16,74 @@ export enum ExtensionUpdateState {
|
||||
NOT_UPDATABLE = 'not updatable',
|
||||
UNKNOWN = 'unknown',
|
||||
}
|
||||
|
||||
export interface ExtensionUpdateStatus {
|
||||
status: ExtensionUpdateState;
|
||||
processed: boolean;
|
||||
}
|
||||
|
||||
export interface ExtensionUpdatesState {
|
||||
extensionStatuses: Map<string, ExtensionUpdateStatus>;
|
||||
batchChecksInProgress: number;
|
||||
}
|
||||
|
||||
export const initialExtensionUpdatesState: ExtensionUpdatesState = {
|
||||
extensionStatuses: new Map(),
|
||||
batchChecksInProgress: 0,
|
||||
};
|
||||
|
||||
export type ExtensionUpdateAction =
|
||||
| {
|
||||
type: 'SET_STATE';
|
||||
payload: { name: string; state: ExtensionUpdateState };
|
||||
}
|
||||
| {
|
||||
type: 'SET_PROCESSED';
|
||||
payload: { name: string; processed: boolean };
|
||||
}
|
||||
| { type: 'BATCH_CHECK_START' }
|
||||
| { type: 'BATCH_CHECK_END' };
|
||||
|
||||
export function extensionUpdatesReducer(
|
||||
state: ExtensionUpdatesState,
|
||||
action: ExtensionUpdateAction,
|
||||
): ExtensionUpdatesState {
|
||||
switch (action.type) {
|
||||
case 'SET_STATE': {
|
||||
const existing = state.extensionStatuses.get(action.payload.name);
|
||||
if (existing?.status === action.payload.state) {
|
||||
return state;
|
||||
}
|
||||
const newStatuses = new Map(state.extensionStatuses);
|
||||
newStatuses.set(action.payload.name, {
|
||||
status: action.payload.state,
|
||||
processed: false,
|
||||
});
|
||||
return { ...state, extensionStatuses: newStatuses };
|
||||
}
|
||||
case 'SET_PROCESSED': {
|
||||
const existing = state.extensionStatuses.get(action.payload.name);
|
||||
if (!existing || existing.processed === action.payload.processed) {
|
||||
return state;
|
||||
}
|
||||
const newStatuses = new Map(state.extensionStatuses);
|
||||
newStatuses.set(action.payload.name, {
|
||||
...existing,
|
||||
processed: action.payload.processed,
|
||||
});
|
||||
return { ...state, extensionStatuses: newStatuses };
|
||||
}
|
||||
case 'BATCH_CHECK_START':
|
||||
return {
|
||||
...state,
|
||||
batchChecksInProgress: state.batchChecksInProgress + 1,
|
||||
};
|
||||
case 'BATCH_CHECK_END':
|
||||
return {
|
||||
...state,
|
||||
batchChecksInProgress: state.batchChecksInProgress - 1,
|
||||
};
|
||||
default:
|
||||
checkExhaustive(action);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.8.0-nightly.20250925.b1da8c21",
|
||||
"version": "0.8.2",
|
||||
"description": "Gemini CLI Core",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -899,9 +899,13 @@ describe('GeminiChat', () => {
|
||||
expect(mockLogContentRetry).toHaveBeenCalledTimes(1);
|
||||
expect(mockLogContentRetryFailure).toHaveBeenCalledTimes(1);
|
||||
|
||||
// History should be clean, as if the failed turn never happened.
|
||||
// History should still contain the user message.
|
||||
const history = chat.getHistory();
|
||||
expect(history.length).toBe(0);
|
||||
expect(history.length).toBe(1);
|
||||
expect(history[0]).toEqual({
|
||||
role: 'user',
|
||||
parts: [{ text: 'test' }],
|
||||
});
|
||||
});
|
||||
|
||||
describe('API error retry behavior', () => {
|
||||
|
||||
@@ -327,10 +327,6 @@ export class GeminiChat {
|
||||
),
|
||||
);
|
||||
}
|
||||
// If the stream fails, remove the user message that was added.
|
||||
if (self.history[self.history.length - 1] === userContent) {
|
||||
self.history.pop();
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -902,7 +902,9 @@ export class ClearcutLogger {
|
||||
this.enqueueLogEvent(
|
||||
this.createLogEvent(EventNames.EXTENSION_INSTALL, data),
|
||||
);
|
||||
this.flushIfNeeded();
|
||||
this.flushToClearcut().catch((error) => {
|
||||
console.debug('Error flushing to Clearcut:', error);
|
||||
});
|
||||
}
|
||||
|
||||
logExtensionUninstallEvent(event: ExtensionUninstallEvent): void {
|
||||
@@ -920,7 +922,9 @@ export class ClearcutLogger {
|
||||
this.enqueueLogEvent(
|
||||
this.createLogEvent(EventNames.EXTENSION_UNINSTALL, data),
|
||||
);
|
||||
this.flushIfNeeded();
|
||||
this.flushToClearcut().catch((error) => {
|
||||
console.debug('Error flushing to Clearcut:', error);
|
||||
});
|
||||
}
|
||||
|
||||
logToolOutputTruncatedEvent(event: ToolOutputTruncatedEvent): void {
|
||||
@@ -1003,7 +1007,9 @@ export class ClearcutLogger {
|
||||
this.enqueueLogEvent(
|
||||
this.createLogEvent(EventNames.EXTENSION_ENABLE, data),
|
||||
);
|
||||
this.flushIfNeeded();
|
||||
this.flushToClearcut().catch((error) => {
|
||||
console.debug('Error flushing to Clearcut:', error);
|
||||
});
|
||||
}
|
||||
|
||||
logModelSlashCommandEvent(event: ModelSlashCommandEvent): void {
|
||||
@@ -1036,7 +1042,9 @@ export class ClearcutLogger {
|
||||
this.enqueueLogEvent(
|
||||
this.createLogEvent(EventNames.EXTENSION_DISABLE, data),
|
||||
);
|
||||
this.flushIfNeeded();
|
||||
this.flushToClearcut().catch((error) => {
|
||||
console.debug('Error flushing to Clearcut:', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.8.0-nightly.20250925.b1da8c21",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"displayName": "Gemini CLI Companion",
|
||||
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
||||
"version": "0.8.0-nightly.20250925.b1da8c21",
|
||||
"version": "0.8.2",
|
||||
"publisher": "google",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
|
||||
Reference in New Issue
Block a user