mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-13 04:48:09 -07:00
feat(cli): disable folder trust in headless mode (#18407)
This commit is contained in:
Generated
+24
-1
@@ -2255,6 +2255,7 @@
|
|||||||
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-token": "^6.0.0",
|
"@octokit/auth-token": "^6.0.0",
|
||||||
"@octokit/graphql": "^9.0.2",
|
"@octokit/graphql": "^9.0.2",
|
||||||
@@ -2435,6 +2436,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
@@ -2468,6 +2470,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
|
||||||
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
|
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
},
|
},
|
||||||
@@ -2836,6 +2839,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
||||||
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
|
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "2.0.1",
|
"@opentelemetry/core": "2.0.1",
|
||||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
@@ -2869,6 +2873,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
|
||||||
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
|
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "2.0.1",
|
"@opentelemetry/core": "2.0.1",
|
||||||
"@opentelemetry/resources": "2.0.1"
|
"@opentelemetry/resources": "2.0.1"
|
||||||
@@ -2921,6 +2926,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
|
||||||
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
|
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "2.0.1",
|
"@opentelemetry/core": "2.0.1",
|
||||||
"@opentelemetry/resources": "2.0.1",
|
"@opentelemetry/resources": "2.0.1",
|
||||||
@@ -4136,6 +4142,7 @@
|
|||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -4430,6 +4437,7 @@
|
|||||||
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.35.0",
|
"@typescript-eslint/scope-manager": "8.35.0",
|
||||||
"@typescript-eslint/types": "8.35.0",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
@@ -5422,6 +5430,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -8431,6 +8440,7 @@
|
|||||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -8971,6 +8981,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "^2.0.0",
|
"accepts": "^2.0.0",
|
||||||
"body-parser": "^2.2.1",
|
"body-parser": "^2.2.1",
|
||||||
@@ -10584,6 +10595,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
|
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
|
||||||
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
|
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||||
"ansi-escapes": "^7.0.0",
|
"ansi-escapes": "^7.0.0",
|
||||||
@@ -14368,6 +14380,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -14378,6 +14391,7 @@
|
|||||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"shell-quote": "^1.6.1",
|
"shell-quote": "^1.6.1",
|
||||||
"ws": "^7"
|
"ws": "^7"
|
||||||
@@ -16614,6 +16628,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -16837,7 +16852,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "0BSD"
|
"license": "0BSD",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/tsx": {
|
"node_modules/tsx": {
|
||||||
"version": "4.20.3",
|
"version": "4.20.3",
|
||||||
@@ -16845,6 +16861,7 @@
|
|||||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "~0.25.0",
|
"esbuild": "~0.25.0",
|
||||||
"get-tsconfig": "^4.7.5"
|
"get-tsconfig": "^4.7.5"
|
||||||
@@ -17017,6 +17034,7 @@
|
|||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -17224,6 +17242,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -17337,6 +17356,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -17349,6 +17369,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/expect": "3.2.4",
|
"@vitest/expect": "3.2.4",
|
||||||
@@ -18053,6 +18074,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -18351,6 +18373,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -141,6 +141,22 @@ vi.mock('@google/gemini-cli-core', async () => {
|
|||||||
defaultDecision: ServerConfig.PolicyDecision.ASK_USER,
|
defaultDecision: ServerConfig.PolicyDecision.ASK_USER,
|
||||||
approvalMode: ServerConfig.ApprovalMode.DEFAULT,
|
approvalMode: ServerConfig.ApprovalMode.DEFAULT,
|
||||||
})),
|
})),
|
||||||
|
isHeadlessMode: vi.fn((opts) => {
|
||||||
|
if (process.env['VITEST'] === 'true') {
|
||||||
|
return (
|
||||||
|
!!opts?.prompt ||
|
||||||
|
(!!process.stdin && !process.stdin.isTTY) ||
|
||||||
|
(!!process.stdout && !process.stdout.isTTY)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
!!opts?.prompt ||
|
||||||
|
process.env['CI'] === 'true' ||
|
||||||
|
process.env['GITHUB_ACTIONS'] === 'true' ||
|
||||||
|
(!!process.stdin && !process.stdin.isTTY) ||
|
||||||
|
(!!process.stdout && !process.stdout.isTTY)
|
||||||
|
);
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,6 +170,8 @@ vi.mock('./extension-manager.js', () => {
|
|||||||
// Global setup to ensure clean environment for all tests in this file
|
// Global setup to ensure clean environment for all tests in this file
|
||||||
const originalArgv = process.argv;
|
const originalArgv = process.argv;
|
||||||
const originalGeminiModel = process.env['GEMINI_MODEL'];
|
const originalGeminiModel = process.env['GEMINI_MODEL'];
|
||||||
|
const originalStdoutIsTTY = process.stdout.isTTY;
|
||||||
|
const originalStdinIsTTY = process.stdin.isTTY;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
delete process.env['GEMINI_MODEL'];
|
delete process.env['GEMINI_MODEL'];
|
||||||
@@ -162,6 +180,18 @@ beforeEach(() => {
|
|||||||
ExtensionManager.prototype.loadExtensions = vi
|
ExtensionManager.prototype.loadExtensions = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue(undefined);
|
.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
// Default to interactive mode for tests unless otherwise specified
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', {
|
||||||
|
value: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', {
|
||||||
|
value: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -171,6 +201,16 @@ afterEach(() => {
|
|||||||
} else {
|
} else {
|
||||||
delete process.env['GEMINI_MODEL'];
|
delete process.env['GEMINI_MODEL'];
|
||||||
}
|
}
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', {
|
||||||
|
value: originalStdoutIsTTY,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', {
|
||||||
|
value: originalStdinIsTTY,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parseArguments', () => {
|
describe('parseArguments', () => {
|
||||||
@@ -249,6 +289,16 @@ describe('parseArguments', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('positional arguments and @commands', () => {
|
describe('positional arguments and @commands', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Default to headless mode for these tests as they mostly expect one-shot behavior
|
||||||
|
process.stdin.isTTY = false;
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', {
|
||||||
|
value: false,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{
|
{
|
||||||
description:
|
description:
|
||||||
@@ -379,8 +429,12 @@ describe('parseArguments', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it('should include a startup message when converting positional query to interactive prompt', async () => {
|
it('should include a startup message when converting positional query to interactive prompt', async () => {
|
||||||
const originalIsTTY = process.stdin.isTTY;
|
|
||||||
process.stdin.isTTY = true;
|
process.stdin.isTTY = true;
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', {
|
||||||
|
value: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
process.argv = ['node', 'script.js', 'hello'];
|
process.argv = ['node', 'script.js', 'hello'];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -389,7 +443,7 @@ describe('parseArguments', () => {
|
|||||||
'Positional arguments now default to interactive mode. To run in non-interactive mode, use the --prompt (-p) flag.',
|
'Positional arguments now default to interactive mode. To run in non-interactive mode, use the --prompt (-p) flag.',
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
process.stdin.isTTY = originalIsTTY;
|
// beforeEach handles resetting
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1732,14 +1786,29 @@ describe('loadCliConfig model selection', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('loadCliConfig folderTrust', () => {
|
describe('loadCliConfig folderTrust', () => {
|
||||||
|
let originalVitest: string | undefined;
|
||||||
|
let originalIntegrationTest: string | undefined;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||||
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
||||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
|
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
|
||||||
|
|
||||||
|
originalVitest = process.env['VITEST'];
|
||||||
|
originalIntegrationTest = process.env['GEMINI_CLI_INTEGRATION_TEST'];
|
||||||
|
delete process.env['VITEST'];
|
||||||
|
delete process.env['GEMINI_CLI_INTEGRATION_TEST'];
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
if (originalVitest !== undefined) {
|
||||||
|
process.env['VITEST'] = originalVitest;
|
||||||
|
}
|
||||||
|
if (originalIntegrationTest !== undefined) {
|
||||||
|
process.env['GEMINI_CLI_INTEGRATION_TEST'] = originalIntegrationTest;
|
||||||
|
}
|
||||||
|
|
||||||
vi.unstubAllEnvs();
|
vi.unstubAllEnvs();
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
@@ -2779,6 +2848,16 @@ describe('Output format', () => {
|
|||||||
describe('parseArguments with positional prompt', () => {
|
describe('parseArguments with positional prompt', () => {
|
||||||
const originalArgv = process.argv;
|
const originalArgv = process.argv;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Default to headless mode for these tests as they mostly expect one-shot behavior
|
||||||
|
process.stdin.isTTY = false;
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', {
|
||||||
|
value: false,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
process.argv = originalArgv;
|
process.argv = originalArgv;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
coreEvents,
|
coreEvents,
|
||||||
GEMINI_MODEL_ALIAS_AUTO,
|
GEMINI_MODEL_ALIAS_AUTO,
|
||||||
getAdminErrorMessage,
|
getAdminErrorMessage,
|
||||||
|
isHeadlessMode,
|
||||||
Config,
|
Config,
|
||||||
applyAdminAllowlist,
|
applyAdminAllowlist,
|
||||||
getAdminBlockedMcpServersMessage,
|
getAdminBlockedMcpServersMessage,
|
||||||
@@ -352,7 +353,7 @@ export async function parseArguments(
|
|||||||
|
|
||||||
// -p/--prompt forces non-interactive mode; positional args default to interactive in TTY
|
// -p/--prompt forces non-interactive mode; positional args default to interactive in TTY
|
||||||
if (q && !result['prompt']) {
|
if (q && !result['prompt']) {
|
||||||
if (process.stdin.isTTY) {
|
if (!isHeadlessMode()) {
|
||||||
startupMessages.push(
|
startupMessages.push(
|
||||||
'Positional arguments now default to interactive mode. To run in non-interactive mode, use the --prompt (-p) flag.',
|
'Positional arguments now default to interactive mode. To run in non-interactive mode, use the --prompt (-p) flag.',
|
||||||
);
|
);
|
||||||
@@ -436,7 +437,11 @@ export async function loadCliConfig(
|
|||||||
|
|
||||||
const ideMode = settings.ide?.enabled ?? false;
|
const ideMode = settings.ide?.enabled ?? false;
|
||||||
|
|
||||||
const folderTrust = settings.security?.folderTrust?.enabled ?? false;
|
const folderTrust =
|
||||||
|
process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true' ||
|
||||||
|
process.env['VITEST'] === 'true'
|
||||||
|
? false
|
||||||
|
: (settings.security?.folderTrust?.enabled ?? false);
|
||||||
const trustedFolder = isWorkspaceTrusted(settings, cwd)?.isTrusted ?? false;
|
const trustedFolder = isWorkspaceTrusted(settings, cwd)?.isTrusted ?? false;
|
||||||
|
|
||||||
// Set the context filename in the server's memoryTool module BEFORE loading memory
|
// Set the context filename in the server's memoryTool module BEFORE loading memory
|
||||||
@@ -592,7 +597,9 @@ export async function loadCliConfig(
|
|||||||
const interactive =
|
const interactive =
|
||||||
!!argv.promptInteractive ||
|
!!argv.promptInteractive ||
|
||||||
!!argv.experimentalAcp ||
|
!!argv.experimentalAcp ||
|
||||||
(process.stdin.isTTY && !argv.query && !argv.prompt && !argv.isCommand);
|
(!isHeadlessMode({ prompt: argv.prompt }) &&
|
||||||
|
!argv.query &&
|
||||||
|
!argv.isCommand);
|
||||||
|
|
||||||
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
|
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
|
||||||
const allowedToolsSet = new Set(allowedTools);
|
const allowedToolsSet = new Set(allowedTools);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
homedir: () => '/mock/home/user',
|
homedir: () => '/mock/home/user',
|
||||||
|
isHeadlessMode: vi.fn(() => false),
|
||||||
coreEvents: {
|
coreEvents: {
|
||||||
emitFeedback: vi.fn(),
|
emitFeedback: vi.fn(),
|
||||||
},
|
},
|
||||||
@@ -280,6 +281,26 @@ describe('Trusted Folders', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return true for a child of a trusted folder', () => {
|
||||||
|
const config = { '/projectA': TrustLevel.TRUST_FOLDER };
|
||||||
|
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||||
|
|
||||||
|
expect(isWorkspaceTrusted(mockSettings, '/projectA/src')).toEqual({
|
||||||
|
isTrusted: true,
|
||||||
|
source: 'file',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for a child of a trusted parent folder', () => {
|
||||||
|
const config = { '/projectB/somefile.txt': TrustLevel.TRUST_PARENT };
|
||||||
|
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||||
|
|
||||||
|
expect(isWorkspaceTrusted(mockSettings, '/projectB')).toEqual({
|
||||||
|
isTrusted: true,
|
||||||
|
source: 'file',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should return false for a directly untrusted folder', () => {
|
it('should return false for a directly untrusted folder', () => {
|
||||||
const config = { '/untrusted': TrustLevel.DO_NOT_TRUST };
|
const config = { '/untrusted': TrustLevel.DO_NOT_TRUST };
|
||||||
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||||
@@ -290,6 +311,15 @@ describe('Trusted Folders', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return false for a child of an untrusted folder', () => {
|
||||||
|
const config = { '/untrusted': TrustLevel.DO_NOT_TRUST };
|
||||||
|
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||||
|
|
||||||
|
expect(isWorkspaceTrusted(mockSettings, '/untrusted/src').isTrusted).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return undefined when no rules match', () => {
|
it('should return undefined when no rules match', () => {
|
||||||
fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8');
|
fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8');
|
||||||
expect(
|
expect(
|
||||||
@@ -297,6 +327,47 @@ describe('Trusted Folders', () => {
|
|||||||
).toBeUndefined();
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should prioritize specific distrust over parent trust', () => {
|
||||||
|
const config = {
|
||||||
|
'/projectA': TrustLevel.TRUST_FOLDER,
|
||||||
|
'/projectA/untrusted': TrustLevel.DO_NOT_TRUST,
|
||||||
|
};
|
||||||
|
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||||
|
|
||||||
|
expect(isWorkspaceTrusted(mockSettings, '/projectA/untrusted')).toEqual({
|
||||||
|
isTrusted: false,
|
||||||
|
source: 'file',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use workspaceDir instead of process.cwd() when provided', () => {
|
||||||
|
const config = {
|
||||||
|
'/projectA': TrustLevel.TRUST_FOLDER,
|
||||||
|
'/untrusted': TrustLevel.DO_NOT_TRUST,
|
||||||
|
};
|
||||||
|
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||||
|
|
||||||
|
vi.spyOn(process, 'cwd').mockImplementation(() => '/untrusted');
|
||||||
|
|
||||||
|
// process.cwd() is untrusted, but workspaceDir is trusted
|
||||||
|
expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
|
||||||
|
isTrusted: true,
|
||||||
|
source: 'file',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle path normalization', () => {
|
||||||
|
const config = { '/home/user/projectA': TrustLevel.TRUST_FOLDER };
|
||||||
|
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isWorkspaceTrusted(mockSettings, '/home/user/../user/projectA'),
|
||||||
|
).toEqual({
|
||||||
|
isTrusted: true,
|
||||||
|
source: 'file',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should prioritize IDE override over file config', () => {
|
it('should prioritize IDE override over file config', () => {
|
||||||
const config = { '/projectA': TrustLevel.DO_NOT_TRUST };
|
const config = { '/projectA': TrustLevel.DO_NOT_TRUST };
|
||||||
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||||
@@ -313,6 +384,30 @@ describe('Trusted Folders', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return false when IDE override is false', () => {
|
||||||
|
const config = { '/projectA': TrustLevel.TRUST_FOLDER };
|
||||||
|
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||||
|
|
||||||
|
ideContextStore.set({ workspaceState: { isTrusted: false } });
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
|
||||||
|
isTrusted: false,
|
||||||
|
source: 'ide',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
ideContextStore.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw FatalConfigError when the config file is invalid', () => {
|
||||||
|
fs.writeFileSync(trustedFoldersPath, 'invalid json', 'utf-8');
|
||||||
|
|
||||||
|
expect(() => isWorkspaceTrusted(mockSettings, '/any')).toThrow(
|
||||||
|
FatalConfigError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should always return true if folderTrust setting is disabled', () => {
|
it('should always return true if folderTrust setting is disabled', () => {
|
||||||
const disabledSettings: Settings = {
|
const disabledSettings: Settings = {
|
||||||
security: { folderTrust: { enabled: false } },
|
security: { folderTrust: { enabled: false } },
|
||||||
@@ -324,7 +419,75 @@ describe('Trusted Folders', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isWorkspaceTrusted headless mode', () => {
|
||||||
|
const mockSettings: Settings = {
|
||||||
|
security: {
|
||||||
|
folderTrust: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should return true when isHeadlessMode is true, ignoring config', async () => {
|
||||||
|
const geminiCore = await import('@google/gemini-cli-core');
|
||||||
|
vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(true);
|
||||||
|
|
||||||
|
expect(isWorkspaceTrusted(mockSettings)).toEqual({
|
||||||
|
isTrusted: true,
|
||||||
|
source: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to config when isHeadlessMode is false', async () => {
|
||||||
|
const geminiCore = await import('@google/gemini-cli-core');
|
||||||
|
vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(false);
|
||||||
|
|
||||||
|
const config = { '/projectA': TrustLevel.DO_NOT_TRUST };
|
||||||
|
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||||
|
|
||||||
|
expect(isWorkspaceTrusted(mockSettings, '/projectA').isTrusted).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Trusted Folders Caching', () => {
|
||||||
|
it('should cache the loaded folders object', () => {
|
||||||
|
// First call should load and cache
|
||||||
|
const folders1 = loadTrustedFolders();
|
||||||
|
|
||||||
|
// Second call should return the same instance from cache
|
||||||
|
const folders2 = loadTrustedFolders();
|
||||||
|
expect(folders1).toBe(folders2);
|
||||||
|
|
||||||
|
// Resetting should clear the cache
|
||||||
|
resetTrustedFoldersForTesting();
|
||||||
|
|
||||||
|
// Third call should return a new instance
|
||||||
|
const folders3 = loadTrustedFolders();
|
||||||
|
expect(folders3).not.toBe(folders1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('invalid trust levels', () => {
|
||||||
|
it('should create a comprehensive error message for invalid trust level', () => {
|
||||||
|
const config = { '/user/folder': 'INVALID_TRUST_LEVEL' };
|
||||||
|
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||||
|
|
||||||
|
const { errors } = loadTrustedFolders();
|
||||||
|
const possibleValues = Object.values(TrustLevel).join(', ');
|
||||||
|
expect(errors.length).toBe(1);
|
||||||
|
expect(errors[0].message).toBe(
|
||||||
|
`Invalid trust level "INVALID_TRUST_LEVEL" for path "/user/folder". Possible values are: ${possibleValues}.`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Symlinks Support', () => {
|
describe('Symlinks Support', () => {
|
||||||
|
const mockSettings: Settings = {
|
||||||
|
security: { folderTrust: { enabled: true } },
|
||||||
|
};
|
||||||
|
|
||||||
it('should trust a folder if the rule matches the realpath', () => {
|
it('should trust a folder if the rule matches the realpath', () => {
|
||||||
// Create a real directory and a symlink
|
// Create a real directory and a symlink
|
||||||
const realDir = path.join(tempDir, 'real');
|
const realDir = path.join(tempDir, 'real');
|
||||||
@@ -339,10 +502,6 @@ describe('Trusted Folders', () => {
|
|||||||
// Check against symlink path
|
// Check against symlink path
|
||||||
expect(isWorkspaceTrusted(mockSettings, symlinkDir).isTrusted).toBe(true);
|
expect(isWorkspaceTrusted(mockSettings, symlinkDir).isTrusted).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockSettings: Settings = {
|
|
||||||
security: { folderTrust: { enabled: true } },
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Verification: Auth and Trust Interaction', () => {
|
describe('Verification: Auth and Trust Interaction', () => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
ideContextStore,
|
ideContextStore,
|
||||||
GEMINI_DIR,
|
GEMINI_DIR,
|
||||||
homedir,
|
homedir,
|
||||||
|
isHeadlessMode,
|
||||||
coreEvents,
|
coreEvents,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { Settings } from './settings.js';
|
import type { Settings } from './settings.js';
|
||||||
@@ -354,6 +355,10 @@ export function isWorkspaceTrusted(
|
|||||||
workspaceDir: string = process.cwd(),
|
workspaceDir: string = process.cwd(),
|
||||||
trustConfig?: Record<string, TrustLevel>,
|
trustConfig?: Record<string, TrustLevel>,
|
||||||
): TrustResult {
|
): TrustResult {
|
||||||
|
if (isHeadlessMode()) {
|
||||||
|
return { isTrusted: true, source: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
if (!isFolderTrustEnabled(settings)) {
|
if (!isFolderTrustEnabled(settings)) {
|
||||||
return { isTrusted: true, source: undefined };
|
return { isTrusted: true, source: undefined };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,22 @@ import { FolderTrustChoice } from '../components/FolderTrustDialog.js';
|
|||||||
import type { LoadedTrustedFolders } from '../../config/trustedFolders.js';
|
import type { LoadedTrustedFolders } from '../../config/trustedFolders.js';
|
||||||
import { TrustLevel } from '../../config/trustedFolders.js';
|
import { TrustLevel } from '../../config/trustedFolders.js';
|
||||||
import * as trustedFolders from '../../config/trustedFolders.js';
|
import * as trustedFolders from '../../config/trustedFolders.js';
|
||||||
import { coreEvents, ExitCodes } from '@google/gemini-cli-core';
|
import { coreEvents, ExitCodes, isHeadlessMode } from '@google/gemini-cli-core';
|
||||||
|
import { MessageType } from '../types.js';
|
||||||
|
|
||||||
const mockedCwd = vi.hoisted(() => vi.fn());
|
const mockedCwd = vi.hoisted(() => vi.fn());
|
||||||
const mockedExit = vi.hoisted(() => vi.fn());
|
const mockedExit = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
|
vi.mock('@google/gemini-cli-core', async () => {
|
||||||
|
const actual = await vi.importActual<
|
||||||
|
typeof import('@google/gemini-cli-core')
|
||||||
|
>('@google/gemini-cli-core');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
isHeadlessMode: vi.fn().mockReturnValue(false),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock('node:process', async () => {
|
vi.mock('node:process', async () => {
|
||||||
const actual =
|
const actual =
|
||||||
await vi.importActual<typeof import('node:process')>('node:process');
|
await vi.importActual<typeof import('node:process')>('node:process');
|
||||||
@@ -46,8 +57,24 @@ describe('useFolderTrust', () => {
|
|||||||
let onTrustChange: (isTrusted: boolean | undefined) => void;
|
let onTrustChange: (isTrusted: boolean | undefined) => void;
|
||||||
let addItem: Mock;
|
let addItem: Mock;
|
||||||
|
|
||||||
|
const originalStdoutIsTTY = process.stdout.isTTY;
|
||||||
|
const originalStdinIsTTY = process.stdin.isTTY;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
// Default to interactive mode for tests
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', {
|
||||||
|
value: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', {
|
||||||
|
value: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
mockSettings = {
|
mockSettings = {
|
||||||
merged: {
|
merged: {
|
||||||
security: {
|
security: {
|
||||||
@@ -75,6 +102,16 @@ describe('useFolderTrust', () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', {
|
||||||
|
value: originalStdoutIsTTY,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', {
|
||||||
|
value: originalStdinIsTTY,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not open dialog when folder is already trusted', () => {
|
it('should not open dialog when folder is already trusted', () => {
|
||||||
@@ -318,4 +355,28 @@ describe('useFolderTrust', () => {
|
|||||||
);
|
);
|
||||||
expect(mockedExit).toHaveBeenCalledWith(ExitCodes.FATAL_CONFIG_ERROR);
|
expect(mockedExit).toHaveBeenCalledWith(ExitCodes.FATAL_CONFIG_ERROR);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('headless mode', () => {
|
||||||
|
it('should force trust and hide dialog in headless mode', () => {
|
||||||
|
vi.mocked(isHeadlessMode).mockReturnValue(true);
|
||||||
|
isWorkspaceTrustedSpy.mockReturnValue({
|
||||||
|
isTrusted: false,
|
||||||
|
source: 'file',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useFolderTrust(mockSettings, onTrustChange, addItem),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.isFolderTrustDialogOpen).toBe(false);
|
||||||
|
expect(onTrustChange).toHaveBeenCalledWith(true);
|
||||||
|
expect(addItem).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: MessageType.INFO,
|
||||||
|
text: expect.stringContaining('This folder is untrusted'),
|
||||||
|
}),
|
||||||
|
expect.any(Number),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from '../../config/trustedFolders.js';
|
} from '../../config/trustedFolders.js';
|
||||||
import * as process from 'node:process';
|
import * as process from 'node:process';
|
||||||
import { type HistoryItemWithoutId, MessageType } from '../types.js';
|
import { type HistoryItemWithoutId, MessageType } from '../types.js';
|
||||||
import { coreEvents, ExitCodes } from '@google/gemini-cli-core';
|
import { coreEvents, ExitCodes, isHeadlessMode } from '@google/gemini-cli-core';
|
||||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||||
|
|
||||||
export const useFolderTrust = (
|
export const useFolderTrust = (
|
||||||
@@ -30,21 +30,39 @@ export const useFolderTrust = (
|
|||||||
const folderTrust = settings.merged.security.folderTrust.enabled ?? true;
|
const folderTrust = settings.merged.security.folderTrust.enabled ?? true;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
const { isTrusted: trusted } = isWorkspaceTrusted(settings.merged);
|
const { isTrusted: trusted } = isWorkspaceTrusted(settings.merged);
|
||||||
setIsTrusted(trusted);
|
|
||||||
setIsFolderTrustDialogOpen(trusted === undefined);
|
|
||||||
onTrustChange(trusted);
|
|
||||||
|
|
||||||
if (trusted === false && !startupMessageSent.current) {
|
const showUntrustedMessage = () => {
|
||||||
addItem(
|
if (trusted === false && !startupMessageSent.current) {
|
||||||
{
|
addItem(
|
||||||
type: MessageType.INFO,
|
{
|
||||||
text: 'This folder is untrusted, project settings, hooks, MCPs, and GEMINI.md files will not be applied for this folder.\nUse the `/permissions` command to change the trust level.',
|
type: MessageType.INFO,
|
||||||
},
|
text: 'This folder is untrusted, project settings, hooks, MCPs, and GEMINI.md files will not be applied for this folder.\nUse the `/permissions` command to change the trust level.',
|
||||||
Date.now(),
|
},
|
||||||
);
|
Date.now(),
|
||||||
startupMessageSent.current = true;
|
);
|
||||||
|
startupMessageSent.current = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isHeadlessMode()) {
|
||||||
|
if (isMounted) {
|
||||||
|
setIsTrusted(trusted);
|
||||||
|
setIsFolderTrustDialogOpen(false);
|
||||||
|
onTrustChange(true);
|
||||||
|
showUntrustedMessage();
|
||||||
|
}
|
||||||
|
} else if (isMounted) {
|
||||||
|
setIsTrusted(trusted);
|
||||||
|
setIsFolderTrustDialogOpen(trusted === undefined);
|
||||||
|
onTrustChange(trusted);
|
||||||
|
showUntrustedMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
}, [folderTrust, onTrustChange, settings.merged, addItem]);
|
}, [folderTrust, onTrustChange, settings.merged, addItem]);
|
||||||
|
|
||||||
const handleFolderTrustSelect = useCallback(
|
const handleFolderTrustSelect = useCallback(
|
||||||
|
|||||||
@@ -316,10 +316,14 @@ describe('Server Config (config.ts)', () => {
|
|||||||
'../tools/mcp-client-manager.js'
|
'../tools/mcp-client-manager.js'
|
||||||
);
|
);
|
||||||
let mcpStarted = false;
|
let mcpStarted = false;
|
||||||
|
let resolveMcp: (value: unknown) => void;
|
||||||
|
const mcpPromise = new Promise((resolve) => {
|
||||||
|
resolveMcp = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
(McpClientManager as unknown as Mock).mockImplementation(() => ({
|
(McpClientManager as unknown as Mock).mockImplementation(() => ({
|
||||||
startConfiguredMcpServers: vi.fn().mockImplementation(async () => {
|
startConfiguredMcpServers: vi.fn().mockImplementation(async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
await mcpPromise;
|
||||||
mcpStarted = true;
|
mcpStarted = true;
|
||||||
}),
|
}),
|
||||||
getMcpInstructions: vi.fn(),
|
getMcpInstructions: vi.fn(),
|
||||||
@@ -330,8 +334,9 @@ describe('Server Config (config.ts)', () => {
|
|||||||
// Should return immediately, before MCP finishes
|
// Should return immediately, before MCP finishes
|
||||||
expect(mcpStarted).toBe(false);
|
expect(mcpStarted).toBe(false);
|
||||||
|
|
||||||
// Wait for it to eventually finish to avoid open handles
|
// Now let it finish
|
||||||
await new Promise((resolve) => setTimeout(resolve, 60));
|
resolveMcp!(undefined);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
expect(mcpStarted).toBe(true);
|
expect(mcpStarted).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export * from './utils/fetch.js';
|
|||||||
export { homedir, tmpdir } from './utils/paths.js';
|
export { homedir, tmpdir } from './utils/paths.js';
|
||||||
export * from './utils/paths.js';
|
export * from './utils/paths.js';
|
||||||
export * from './utils/checks.js';
|
export * from './utils/checks.js';
|
||||||
|
export * from './utils/headless.js';
|
||||||
export * from './utils/schemaValidator.js';
|
export * from './utils/schemaValidator.js';
|
||||||
export * from './utils/errors.js';
|
export * from './utils/errors.js';
|
||||||
export * from './utils/exitCodes.js';
|
export * from './utils/exitCodes.js';
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ import { coreEvents } from './events.js';
|
|||||||
import { getConsentForOauth } from './authConsent.js';
|
import { getConsentForOauth } from './authConsent.js';
|
||||||
import { FatalAuthenticationError } from './errors.js';
|
import { FatalAuthenticationError } from './errors.js';
|
||||||
import { writeToStdout } from './stdio.js';
|
import { writeToStdout } from './stdio.js';
|
||||||
|
import { isHeadlessMode } from './headless.js';
|
||||||
|
|
||||||
vi.mock('node:readline');
|
vi.mock('node:readline');
|
||||||
|
vi.mock('./headless.js', () => ({
|
||||||
|
isHeadlessMode: vi.fn(),
|
||||||
|
}));
|
||||||
vi.mock('./stdio.js', () => ({
|
vi.mock('./stdio.js', () => ({
|
||||||
writeToStdout: vi.fn(),
|
writeToStdout: vi.fn(),
|
||||||
createWorkingStdio: vi.fn(() => ({
|
createWorkingStdio: vi.fn(() => ({
|
||||||
@@ -49,16 +53,12 @@ describe('getConsentForOauth', () => {
|
|||||||
mockEmitConsentRequest.mockRestore();
|
mockEmitConsentRequest.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use readline when no listeners are present and stdin is a TTY', async () => {
|
it('should use readline when no listeners are present and not headless', async () => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
const mockListenerCount = vi
|
const mockListenerCount = vi
|
||||||
.spyOn(coreEvents, 'listenerCount')
|
.spyOn(coreEvents, 'listenerCount')
|
||||||
.mockReturnValue(0);
|
.mockReturnValue(0);
|
||||||
const originalIsTTY = process.stdin.isTTY;
|
(isHeadlessMode as Mock).mockReturnValue(false);
|
||||||
Object.defineProperty(process.stdin, 'isTTY', {
|
|
||||||
value: true,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockReadline = {
|
const mockReadline = {
|
||||||
on: vi.fn((event, callback) => {
|
on: vi.fn((event, callback) => {
|
||||||
@@ -81,31 +81,19 @@ describe('getConsentForOauth', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
mockListenerCount.mockRestore();
|
mockListenerCount.mockRestore();
|
||||||
Object.defineProperty(process.stdin, 'isTTY', {
|
|
||||||
value: originalIsTTY,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw FatalAuthenticationError when no listeners and not a TTY', async () => {
|
it('should throw FatalAuthenticationError when no listeners and headless', async () => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
const mockListenerCount = vi
|
const mockListenerCount = vi
|
||||||
.spyOn(coreEvents, 'listenerCount')
|
.spyOn(coreEvents, 'listenerCount')
|
||||||
.mockReturnValue(0);
|
.mockReturnValue(0);
|
||||||
const originalIsTTY = process.stdin.isTTY;
|
(isHeadlessMode as Mock).mockReturnValue(true);
|
||||||
Object.defineProperty(process.stdin, 'isTTY', {
|
|
||||||
value: false,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(getConsentForOauth('Login required.')).rejects.toThrow(
|
await expect(getConsentForOauth('Login required.')).rejects.toThrow(
|
||||||
FatalAuthenticationError,
|
FatalAuthenticationError,
|
||||||
);
|
);
|
||||||
|
|
||||||
mockListenerCount.mockRestore();
|
mockListenerCount.mockRestore();
|
||||||
Object.defineProperty(process.stdin, 'isTTY', {
|
|
||||||
value: originalIsTTY,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import readline from 'node:readline';
|
|||||||
import { CoreEvent, coreEvents } from './events.js';
|
import { CoreEvent, coreEvents } from './events.js';
|
||||||
import { FatalAuthenticationError } from './errors.js';
|
import { FatalAuthenticationError } from './errors.js';
|
||||||
import { createWorkingStdio, writeToStdout } from './stdio.js';
|
import { createWorkingStdio, writeToStdout } from './stdio.js';
|
||||||
|
import { isHeadlessMode } from './headless.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests consent from the user for OAuth login.
|
* Requests consent from the user for OAuth login.
|
||||||
@@ -17,7 +18,7 @@ export async function getConsentForOauth(prompt: string): Promise<boolean> {
|
|||||||
const finalPrompt = prompt + ' Opening authentication page in your browser. ';
|
const finalPrompt = prompt + ' Opening authentication page in your browser. ';
|
||||||
|
|
||||||
if (coreEvents.listenerCount(CoreEvent.ConsentRequest) === 0) {
|
if (coreEvents.listenerCount(CoreEvent.ConsentRequest) === 0) {
|
||||||
if (!process.stdin.isTTY) {
|
if (isHeadlessMode()) {
|
||||||
throw new FatalAuthenticationError(
|
throw new FatalAuthenticationError(
|
||||||
'Interactive consent could not be obtained.\n' +
|
'Interactive consent could not be obtained.\n' +
|
||||||
'Please run Gemini CLI in an interactive terminal to authenticate, or use NO_BROWSER=true for manual authentication.',
|
'Please run Gemini CLI in an interactive terminal to authenticate, or use NO_BROWSER=true for manual authentication.',
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { isHeadlessMode } from './headless.js';
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
|
describe('isHeadlessMode', () => {
|
||||||
|
const originalStdoutIsTTY = process.stdout.isTTY;
|
||||||
|
const originalStdinIsTTY = process.stdin.isTTY;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubEnv('CI', '');
|
||||||
|
vi.stubEnv('GITHUB_ACTIONS', '');
|
||||||
|
// We can't easily stub process.stdout.isTTY with vi.stubEnv
|
||||||
|
// So we'll use Object.defineProperty
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', {
|
||||||
|
value: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', {
|
||||||
|
value: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', {
|
||||||
|
value: originalStdoutIsTTY,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', {
|
||||||
|
value: originalStdinIsTTY,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false in a normal TTY environment', () => {
|
||||||
|
expect(isHeadlessMode()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if CI environment variable is "true"', () => {
|
||||||
|
vi.stubEnv('CI', 'true');
|
||||||
|
expect(isHeadlessMode()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if GITHUB_ACTIONS environment variable is "true"', () => {
|
||||||
|
vi.stubEnv('GITHUB_ACTIONS', 'true');
|
||||||
|
expect(isHeadlessMode()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if stdout is not a TTY', () => {
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', {
|
||||||
|
value: false,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
expect(isHeadlessMode()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if stdin is not a TTY', () => {
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', {
|
||||||
|
value: false,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
expect(isHeadlessMode()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if stdin is a TTY but stdout is not', () => {
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', {
|
||||||
|
value: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', {
|
||||||
|
value: false,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
expect(isHeadlessMode()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if stdout is a TTY but stdin is not', () => {
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', {
|
||||||
|
value: false,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', {
|
||||||
|
value: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
expect(isHeadlessMode()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if a prompt option is provided', () => {
|
||||||
|
expect(isHeadlessMode({ prompt: 'test prompt' })).toBe(true);
|
||||||
|
expect(isHeadlessMode({ prompt: true })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if query is provided but it is still a TTY', () => {
|
||||||
|
// Note: per current logic, query alone doesn't force headless if TTY
|
||||||
|
// This matches the existing behavior in packages/cli/src/config/config.ts
|
||||||
|
expect(isHeadlessMode({ query: 'test query' })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined process.stdout gracefully', () => {
|
||||||
|
const originalStdout = process.stdout;
|
||||||
|
// @ts-expect-error - testing edge case
|
||||||
|
delete process.stdout;
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(isHeadlessMode()).toBe(false);
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(process, 'stdout', {
|
||||||
|
value: originalStdout,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined process.stdin gracefully', () => {
|
||||||
|
const originalStdin = process.stdin;
|
||||||
|
// @ts-expect-error - testing edge case
|
||||||
|
delete process.stdin;
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(isHeadlessMode()).toBe(false);
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(process, 'stdin', {
|
||||||
|
value: originalStdin,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if multiple headless indicators are set', () => {
|
||||||
|
vi.stubEnv('CI', 'true');
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', {
|
||||||
|
value: false,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
expect(isHeadlessMode({ prompt: true })).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for headless mode detection.
|
||||||
|
*/
|
||||||
|
export interface HeadlessModeOptions {
|
||||||
|
/** Explicit prompt string or flag. */
|
||||||
|
prompt?: string | boolean;
|
||||||
|
/** Initial query positional argument. */
|
||||||
|
query?: string | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects if the CLI is running in a "headless" (non-interactive) mode.
|
||||||
|
*
|
||||||
|
* Headless mode is triggered by:
|
||||||
|
* 1. process.env.CI being set to 'true'.
|
||||||
|
* 2. process.stdout not being a TTY.
|
||||||
|
* 3. Presence of an explicit prompt flag.
|
||||||
|
*
|
||||||
|
* @param options - Optional flags and arguments from the CLI.
|
||||||
|
* @returns true if the environment is considered headless.
|
||||||
|
*/
|
||||||
|
export function isHeadlessMode(options?: HeadlessModeOptions): boolean {
|
||||||
|
if (process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true') {
|
||||||
|
return (
|
||||||
|
!!options?.prompt ||
|
||||||
|
(!!process.stdin && !process.stdin.isTTY) ||
|
||||||
|
(!!process.stdout && !process.stdout.isTTY)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
process.env['CI'] === 'true' ||
|
||||||
|
process.env['GITHUB_ACTIONS'] === 'true' ||
|
||||||
|
!!options?.prompt ||
|
||||||
|
(!!process.stdin && !process.stdin.isTTY) ||
|
||||||
|
(!!process.stdout && !process.stdout.isTTY)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -485,6 +485,7 @@ export class TestRig {
|
|||||||
key !== 'GEMINI_MODEL' &&
|
key !== 'GEMINI_MODEL' &&
|
||||||
key !== 'GEMINI_DEBUG' &&
|
key !== 'GEMINI_DEBUG' &&
|
||||||
key !== 'GEMINI_CLI_TEST_VAR' &&
|
key !== 'GEMINI_CLI_TEST_VAR' &&
|
||||||
|
key !== 'GEMINI_CLI_INTEGRATION_TEST' &&
|
||||||
!key.startsWith('GEMINI_CLI_ACTIVITY_LOG')
|
!key.startsWith('GEMINI_CLI_ACTIVITY_LOG')
|
||||||
) {
|
) {
|
||||||
delete cleanEnv[key];
|
delete cleanEnv[key];
|
||||||
|
|||||||
Reference in New Issue
Block a user