mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-03 13:41:05 -07:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5eb56494ea | |||
| 5d92b507be | |||
| cbb5e39332 | |||
| 845471ea0b | |||
| f35e241770 | |||
| 076123e315 | |||
| a186083866 | |||
| fa1097dfed | |||
| a6311e3c4c | |||
| 0e79bd4000 |
@@ -166,7 +166,6 @@ runs:
|
||||
--dry-run="${{ inputs.dry-run }}" \
|
||||
--workspace="${{ inputs.core-package-name }}" \
|
||||
--no-tag
|
||||
npm dist-tag rm ${{ inputs.core-package-name }} false --silent
|
||||
|
||||
- name: '🔗 Install latest core package'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
@@ -198,7 +197,6 @@ runs:
|
||||
--dry-run="${{ inputs.dry-run }}" \
|
||||
--workspace="${{ inputs.cli-package-name }}" \
|
||||
--no-tag
|
||||
npm dist-tag rm ${{ inputs.cli-package-name }} false --silent
|
||||
|
||||
- name: 'Get a2a-server Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
@@ -220,8 +218,7 @@ runs:
|
||||
npm publish \
|
||||
--dry-run="${{ inputs.dry-run }}" \
|
||||
--workspace="${{ inputs.a2a-package-name }}" \
|
||||
--no-tag
|
||||
npm dist-tag rm ${{ inputs.a2a-package-name }} false --silent
|
||||
--no-TARGET_TAG
|
||||
|
||||
- name: '🔬 Verify NPM release by version'
|
||||
uses: './.github/actions/verify-release'
|
||||
@@ -229,7 +226,7 @@ runs:
|
||||
with:
|
||||
npm-package: '${{ inputs.cli-package-name }}@${{ inputs.release-version }}'
|
||||
expected-version: '${{ inputs.release-version }}'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
ref: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
gemini_api_key: '${{ inputs.gemini_api_key }}'
|
||||
github-token: '${{ inputs.github-token }}'
|
||||
npm-registry-url: '${{ inputs.npm-registry-url }}'
|
||||
@@ -237,6 +234,7 @@ runs:
|
||||
|
||||
- name: '🏷️ Tag release'
|
||||
uses: './.github/actions/tag-npm-release'
|
||||
if: "${{ inputs.dry-run != 'true' }}"
|
||||
with:
|
||||
channel: '${{ inputs.npm-tag }}'
|
||||
version: '${{ inputs.release-version }}'
|
||||
@@ -248,7 +246,6 @@ runs:
|
||||
cli-package-name: '${{ inputs.cli-package-name }}'
|
||||
core-package-name: '${{ inputs.core-package-name }}'
|
||||
a2a-package-name: '${{ inputs.a2a-package-name }}'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
|
||||
- name: '🎉 Create GitHub Release'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
|
||||
@@ -32,10 +32,11 @@ inputs:
|
||||
a2a-package-name:
|
||||
description: 'The name of the a2a package.'
|
||||
required: true
|
||||
working-directory:
|
||||
description: 'The working directory to run the commands in.'
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to release from.'
|
||||
required: false
|
||||
default: '.'
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
@@ -46,10 +47,16 @@ runs:
|
||||
JSON_INPUTS: '${{ toJSON(inputs) }}'
|
||||
run: 'echo "$JSON_INPUTS"'
|
||||
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '${{ inputs.working-directory }}/.nvmrc'
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: 'configure .npmrc'
|
||||
uses: './.github/actions/setup-npmrc'
|
||||
@@ -72,7 +79,6 @@ runs:
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.core-token.outputs.auth-token }}'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
npm dist-tag add ${{ inputs.core-package-name }}@${{ inputs.version }} ${{ inputs.channel }}
|
||||
|
||||
@@ -92,34 +98,21 @@ runs:
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.cli-token.outputs.auth-token }}'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
npm dist-tag add ${{ inputs.cli-package-name }}@${{ inputs.version }} ${{ inputs.channel }}
|
||||
|
||||
- name: 'Get a2a Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
id: 'a2a-token'
|
||||
with:
|
||||
package-name: '${{ inputs.a2a-package-name }}'
|
||||
github-token: '${{ inputs.github-token }}'
|
||||
wombat-token-core: '${{ inputs.wombat-token-core }}'
|
||||
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
|
||||
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
|
||||
|
||||
- name: 'Change tag for a2a'
|
||||
- name: 'Change tag for @google/gemini-cli-a2a-server'
|
||||
if: |-
|
||||
${{ inputs.dry-run == 'false' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.a2a-token.outputs.auth-token }}'
|
||||
NODE_AUTH_TOKEN: '${{ inputs.wombat-token-a2a-server }}'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
npm dist-tag add ${{ inputs.a2a-package-name }}@${{ inputs.version }} ${{ inputs.channel }}
|
||||
npm dist-tag add @google/gemini-cli-a2a-server@${{ inputs.version }} ${{ inputs.channel }}
|
||||
|
||||
- name: 'Log dry run'
|
||||
if: |-
|
||||
${{ inputs.dry-run == 'true' }}
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
echo "Dry run: Would have added tag '${{ inputs.channel }}' to version '${{ inputs.version }}' for ${{ inputs.cli-package-name }}, ${{ inputs.core-package-name }}, and ${{ inputs.a2a-package-name }}."
|
||||
echo "Dry run: Would have added tag '${{ inputs.channel }}' to version '${{ inputs.version }}' for @google/gemini-cli and @google/gemini-cli-core."
|
||||
|
||||
@@ -21,10 +21,11 @@ inputs:
|
||||
github-token:
|
||||
description: 'The GitHub token for running integration tests.'
|
||||
required: true
|
||||
working-directory:
|
||||
description: 'The working directory to run the tests in.'
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to release from.'
|
||||
required: false
|
||||
default: '.'
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
@@ -35,6 +36,13 @@ runs:
|
||||
JSON_INPUTS: '${{ toJSON(inputs) }}'
|
||||
run: 'echo "$JSON_INPUTS"'
|
||||
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
path: 'verify'
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'setup node'
|
||||
uses: 'actions/setup-node@v4'
|
||||
with:
|
||||
@@ -56,12 +64,12 @@ runs:
|
||||
retry_wait_seconds: 30
|
||||
max_attempts: 10
|
||||
command: |-
|
||||
cd ${{ inputs.working-directory }}
|
||||
cd ./verify
|
||||
npm install --prefer-online --no-cache -g "${{ inputs.npm-package }}"
|
||||
|
||||
- name: 'Smoke test - NPM Install'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
working-directory: './verify'
|
||||
run: |-
|
||||
gemini_version=$(gemini --version)
|
||||
if [ "$gemini_version" != "${{ inputs.expected-version }}" ]; then
|
||||
@@ -75,7 +83,7 @@ runs:
|
||||
|
||||
- name: 'Smoke test - NPX Run'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
working-directory: './verify'
|
||||
run: |-
|
||||
gemini_version=$(npx --prefer-online "${{ inputs.npm-package}}" --version)
|
||||
if [ "$gemini_version" != "${{ inputs.expected-version }}" ]; then
|
||||
@@ -85,11 +93,11 @@ runs:
|
||||
|
||||
- name: 'Install dependencies for integration tests'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
working-directory: './verify'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: '🔬 Run integration tests against NPM release'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
working-directory: './verify'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
|
||||
INTEGRATION_TEST_USE_INSTALLED_GEMINI: 'true'
|
||||
|
||||
@@ -1,42 +1,41 @@
|
||||
## Summary
|
||||
## TLDR
|
||||
|
||||
<!-- Concisely describe what this PR changes and why. Focus on impact and
|
||||
urgency. -->
|
||||
<!-- Add a brief description of what this pull request changes and why and any important things for reviewers to look at -->
|
||||
|
||||
## Details
|
||||
## Dive Deeper
|
||||
|
||||
<!-- Add any extra context and design decisions. Keep it brief but complete. -->
|
||||
<!-- more thoughts and in-depth discussion here -->
|
||||
|
||||
## Related Issues
|
||||
## Reviewer Test Plan
|
||||
|
||||
<!-- Use keywords to auto-close issues (Closes #123, Fixes #456). If this PR is
|
||||
only related to an issue or is a partial fix, simply reference the issue number
|
||||
without a keyword (Related to #123). -->
|
||||
<!-- when a person reviews your code they should ideally be pulling and running that code. How would they validate your change works and if relevant what are some good classes of example prompts and ways they can exercise your changes -->
|
||||
|
||||
## How to Validate
|
||||
## Testing Matrix
|
||||
|
||||
<!-- List exact steps for reviewers to validate the change. Include commands,
|
||||
expected results, and edge cases. -->
|
||||
<!-- Before submitting please validate your changes on as many of these options as possible -->
|
||||
|
||||
## Pre-Merge Checklist
|
||||
| | 🍏 | 🪟 | 🐧 |
|
||||
| -------- | --- | --- | --- |
|
||||
| npm run | ❓ | ❓ | ❓ |
|
||||
| npx | ❓ | ❓ | ❓ |
|
||||
| Docker | ❓ | ❓ | ❓ |
|
||||
| Podman | ❓ | - | - |
|
||||
| Seatbelt | ❓ | - | - |
|
||||
|
||||
<!-- Check all that apply before requesting review or merging. -->
|
||||
## Linked issues / bugs
|
||||
|
||||
- [ ] Updated relevant documentation and README (if needed)
|
||||
- [ ] Added/updated tests (if needed)
|
||||
- [ ] Noted breaking changes (if any)
|
||||
- [ ] Validated on required platforms/methods:
|
||||
- [ ] MacOS
|
||||
- [ ] npm run
|
||||
- [ ] npx
|
||||
- [ ] Docker
|
||||
- [ ] Podman
|
||||
- [ ] Seatbelt
|
||||
- [ ] Windows
|
||||
- [ ] npm run
|
||||
- [ ] npx
|
||||
- [ ] Docker
|
||||
- [ ] Linux
|
||||
- [ ] npm run
|
||||
- [ ] npx
|
||||
- [ ] Docker
|
||||
<!--
|
||||
Link to any related issues or bugs.
|
||||
|
||||
**If this PR fully resolves the issue, use one of the following keywords to automatically close the issue when this PR is merged:**
|
||||
|
||||
- Closes #<issue_number>
|
||||
- Fixes #<issue_number>
|
||||
- Resolves #<issue_number>
|
||||
|
||||
*Example: `Resolves #123`*
|
||||
|
||||
**If this PR is only related to an issue or is a partial fix, simply reference the issue number without a keyword:**
|
||||
|
||||
*Example: `This PR makes progress on #456` or `Related to #789`*
|
||||
-->
|
||||
|
||||
@@ -66,9 +66,6 @@ jobs:
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Validate NOTICES.txt'
|
||||
run: 'git diff --exit-code packages/vscode-ide-companion/NOTICES.txt'
|
||||
|
||||
- name: 'Check lockfile'
|
||||
run: 'npm run check:lockfile'
|
||||
|
||||
@@ -93,17 +90,6 @@ jobs:
|
||||
- name: 'Run sensitive keyword linter'
|
||||
run: 'node scripts/lint.js --sensitive-keywords'
|
||||
|
||||
link_checker:
|
||||
name: 'Link Checker'
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
- name: 'Link Checker'
|
||||
uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1
|
||||
with:
|
||||
args: '--verbose ./**/*.md'
|
||||
fail: true
|
||||
test_linux:
|
||||
name: 'Test (Linux)'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
@@ -365,7 +351,6 @@ jobs:
|
||||
if: 'always()'
|
||||
needs:
|
||||
- 'lint'
|
||||
- 'link_checker'
|
||||
- 'test_linux'
|
||||
- 'test_mac'
|
||||
- 'codeql'
|
||||
@@ -375,7 +360,6 @@ jobs:
|
||||
- name: 'Check all job results'
|
||||
run: |
|
||||
if [[ (${{ needs.lint.result }} != 'success' && ${{ needs.lint.result }} != 'skipped') || \
|
||||
(${{ needs.link_checker.result }} != 'success' && ${{ needs.link_checker.result }} != 'skipped') || \
|
||||
(${{ needs.test_linux.result }} != 'success' && ${{ needs.test_linux.result }} != 'skipped') || \
|
||||
(${{ needs.test_mac.result }} != 'success' && ${{ needs.test_mac.result }} != 'skipped') || \
|
||||
(${{ needs.codeql.result }} != 'success' && ${{ needs.codeql.result }} != 'skipped') || \
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
name: 'Deflake E2E'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch_ref:
|
||||
description: 'Branch to run on'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: 'string'
|
||||
test_name_pattern:
|
||||
description: 'The test name pattern to use'
|
||||
required: false
|
||||
type: 'string'
|
||||
runs:
|
||||
description: 'The number of runs'
|
||||
required: false
|
||||
default: 5
|
||||
type: 'number'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: |-
|
||||
${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}
|
||||
|
||||
jobs:
|
||||
deflake_e2e_linux:
|
||||
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
sandbox:
|
||||
- 'sandbox:none'
|
||||
- 'sandbox:docker'
|
||||
node-version:
|
||||
- '20.x'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.pull_request.head.sha }}'
|
||||
repository: '${{ github.repository }}'
|
||||
|
||||
- name: 'Set up Node.js ${{ matrix.node-version }}'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '${{ matrix.node-version }}'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Set up Docker'
|
||||
if: "matrix.sandbox == 'sandbox:docker'"
|
||||
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
|
||||
|
||||
- name: 'Run E2E tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
IS_DOCKER: "${{ matrix.sandbox == 'sandbox:docker' }}"
|
||||
KEEP_OUTPUT: 'true'
|
||||
RUNS: '${{ github.event.inputs.runs }}'
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
VERBOSE: 'true'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
if [[ "${{ env.IS_DOCKER }}" == "true" ]]; then
|
||||
npm run deflake:test:integration:sandbox:docker -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
else
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
fi
|
||||
|
||||
deflake_e2e_mac:
|
||||
name: 'E2E Test (macOS)'
|
||||
runs-on: 'macos-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.pull_request.head.sha }}'
|
||||
repository: '${{ github.repository }}'
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Fix rollup optional dependencies on macOS'
|
||||
if: "runner.os == 'macOS'"
|
||||
run: |
|
||||
npm cache clean --force
|
||||
- name: 'Run E2E tests (non-Windows)'
|
||||
if: "runner.os != 'Windows'"
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
KEEP_OUTPUT: 'true'
|
||||
RUNS: '${{ github.event.inputs.runs }}'
|
||||
SANDBOX: 'sandbox:none'
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
VERBOSE: 'true'
|
||||
run: |
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
|
||||
deflake_e2e_windows:
|
||||
name: 'Slow E2E - Win'
|
||||
runs-on: 'gemini-cli-windows-16-core'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.pull_request.head.sha }}'
|
||||
repository: '${{ github.repository }}'
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Configure Windows Defender exclusions'
|
||||
run: |
|
||||
Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:TEMP" -Force
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Configure npm for Windows performance'
|
||||
run: |
|
||||
npm config set progress false
|
||||
npm config set audit false
|
||||
npm config set fund false
|
||||
npm config set loglevel error
|
||||
npm config set maxsockets 32
|
||||
npm config set registry https://registry.npmjs.org/
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Run E2E tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
KEEP_OUTPUT: 'true'
|
||||
SANDBOX: 'sandbox:none'
|
||||
VERBOSE: 'true'
|
||||
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
|
||||
UV_THREADPOOL_SIZE: '32'
|
||||
NODE_ENV: 'test'
|
||||
RUNS: '${{ github.event.inputs.runs }}'
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
shell: 'pwsh'
|
||||
run: |
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
@@ -30,7 +30,6 @@ jobs:
|
||||
merge_queue_skipper:
|
||||
name: 'Merge Queue Skipper'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
permissions: 'read-all'
|
||||
outputs:
|
||||
skip: '${{ steps.merge-queue-e2e-skipper.outputs.skip-check }}'
|
||||
steps:
|
||||
@@ -38,13 +37,12 @@ jobs:
|
||||
uses: 'cariad-tech/merge-queue-ci-skipper@1032489e59437862c90a08a2c92809c903883772' # ratchet:cariad-tech/merge-queue-ci-skipper@main
|
||||
with:
|
||||
secret: '${{ secrets.GITHUB_TOKEN }}'
|
||||
continue-on-error: true
|
||||
|
||||
e2e_linux:
|
||||
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: |
|
||||
(needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false') &&
|
||||
needs.merge_queue_skipper.outputs.skip == 'false' &&
|
||||
(github.event_name == 'push' ||
|
||||
github.event_name == 'merge_group' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
@@ -106,7 +104,7 @@ jobs:
|
||||
name: 'E2E Test (macOS)'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: |
|
||||
(needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false') &&
|
||||
needs.merge_queue_skipper.outputs.skip == 'false' &&
|
||||
(github.event_name == 'push' ||
|
||||
github.event_name == 'merge_group' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
@@ -155,7 +153,7 @@ jobs:
|
||||
name: 'Slow E2E - Win'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: |
|
||||
(needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false') &&
|
||||
needs.merge_queue_skipper.outputs.skip == 'false' &&
|
||||
(github.event_name == 'push' ||
|
||||
github.event_name == 'merge_group' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
steps:
|
||||
- name: 'Authenticate to Google Cloud'
|
||||
id: 'auth'
|
||||
uses: 'google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed' # ratchet:exclude pin@v2.1.7
|
||||
uses: 'google-github-actions/auth@v2' # ratchet:exclude
|
||||
with:
|
||||
project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
|
||||
workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
|
||||
|
||||
@@ -70,10 +70,8 @@ jobs:
|
||||
- name: 'Check for triage label on manual trigger'
|
||||
if: |-
|
||||
github.event_name == 'workflow_dispatch' && !contains(steps.get_issue_data.outputs.labels, 'status/need-triage')
|
||||
env:
|
||||
ISSUE_NUMBER_INPUT: '${{ github.event.inputs.issue_number }}'
|
||||
run: |
|
||||
echo "Issue #${ISSUE_NUMBER_INPUT} does not have the 'status/need-triage' label. Stopping workflow."
|
||||
echo "Issue #${{ github.event.inputs.issue_number }} does not have the 'status/need-triage' label. Stopping workflow."
|
||||
exit 1
|
||||
|
||||
- name: 'Checkout'
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
name: 'Links'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
repository_dispatch:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '00 18 * * *'
|
||||
|
||||
jobs:
|
||||
linkChecker:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
|
||||
- name: 'Link Checker'
|
||||
id: 'lychee'
|
||||
uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1
|
||||
with:
|
||||
args: '--verbose --no-progress ./**/*.md'
|
||||
@@ -33,7 +33,7 @@ on:
|
||||
jobs:
|
||||
change-tags:
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
environment: '${{ github.event.inputs.environment }}'
|
||||
permissions:
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
@@ -62,4 +62,4 @@ jobs:
|
||||
cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
|
||||
a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
working-directory: '.'
|
||||
ref: '${{ github.ref }}'
|
||||
|
||||
@@ -48,7 +48,7 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
environment: '${{ github.event.inputs.environment }}'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: './release/.nvmrc'
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
@@ -85,10 +85,8 @@ jobs:
|
||||
- name: 'Prepare Release Info'
|
||||
id: 'release_info'
|
||||
working-directory: './release'
|
||||
env:
|
||||
INPUT_VERSION: '${{ github.event.inputs.version }}'
|
||||
run: |
|
||||
RELEASE_VERSION="${INPUT_VERSION}"
|
||||
RELEASE_VERSION="${{ github.event.inputs.version }}"
|
||||
echo "RELEASE_VERSION=${RELEASE_VERSION#v}" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIOUS_TAG=$(git describe --tags --abbrev=0)" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
environment: '${{ github.event.inputs.environment }}'
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
|
||||
- name: 'Create and Merge Pull Request'
|
||||
if: "github.event.inputs.environment != 'dev'"
|
||||
if: "github.event.inputs.environment == 'prod'"
|
||||
uses: './.github/actions/create-pull-request'
|
||||
with:
|
||||
branch-name: 'release/${{ steps.nightly_version.outputs.RELEASE_TAG }}'
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
working-directory: './release'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: "${{ failure() && github.event.inputs.environment != 'dev' && (github.event_name == 'schedule' || github.event.inputs.dry_run != 'true') }}"
|
||||
if: "${{ failure() && github.event.inputs.environment == 'prod' && (github.event_name == 'schedule' || github.event.inputs.dry_run != 'true') }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_TAG: '${{ steps.nightly_version.outputs.RELEASE_TAG }}'
|
||||
|
||||
@@ -89,8 +89,7 @@ jobs:
|
||||
inputs: {
|
||||
commit: '${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}',
|
||||
channel: channel,
|
||||
original_pr: '${{ github.event.issue.number }}',
|
||||
environment: 'prod'
|
||||
original_pr: '${{ github.event.issue.number }}'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -28,19 +28,10 @@ on:
|
||||
description: 'The original PR number to comment back on.'
|
||||
required: false
|
||||
type: 'string'
|
||||
environment:
|
||||
description: 'Environment'
|
||||
required: false
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'prod'
|
||||
- 'dev'
|
||||
default: 'prod'
|
||||
|
||||
jobs:
|
||||
create-patch:
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
permissions:
|
||||
contents: 'write'
|
||||
pull-requests: 'write'
|
||||
@@ -58,44 +49,26 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'configure .npmrc'
|
||||
uses: './.github/actions/setup-npmrc'
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
||||
- name: 'Install Script Dependencies'
|
||||
run: 'npm ci'
|
||||
run: 'npm install yargs'
|
||||
|
||||
- name: 'Configure Git User'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
run: |-
|
||||
git config user.name "gemini-cli-robot"
|
||||
git config user.email "gemini-cli-robot@google.com"
|
||||
# Configure git to use GITHUB_TOKEN for remote operations (has actions:write for workflow files)
|
||||
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${REPOSITORY}.git"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git"
|
||||
|
||||
- name: 'Create Patch'
|
||||
id: 'create_patch'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
CLI_PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
PATCH_COMMIT: '${{ github.event.inputs.commit }}'
|
||||
PATCH_CHANNEL: '${{ github.event.inputs.channel }}'
|
||||
ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
|
||||
DRY_RUN: '${{ github.event.inputs.dry_run }}'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
# Capture output and display it in logs using tee
|
||||
{
|
||||
node scripts/releasing/create-patch-pr.js \
|
||||
--cli-package-name="${CLI_PACKAGE_NAME}" \
|
||||
--commit="${PATCH_COMMIT}" \
|
||||
--channel="${PATCH_CHANNEL}" \
|
||||
--pullRequestNumber="${ORIGINAL_PR}" \
|
||||
--dry-run="${DRY_RUN}"
|
||||
node scripts/releasing/create-patch-pr.js --commit=${{ github.event.inputs.commit }} --channel=${{ github.event.inputs.channel }} --pullRequestNumber=${{ github.event.inputs.original_pr }} --dry-run=${{ github.event.inputs.dry_run }}
|
||||
echo "EXIT_CODE=$?" >> "$GITHUB_OUTPUT"
|
||||
} 2>&1 | tee >(
|
||||
echo "LOG_CONTENT<<EOF" >> "$GITHUB_ENV"
|
||||
@@ -114,17 +87,14 @@ jobs:
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
GITHUB_RUN_ID: '${{ github.run_id }}'
|
||||
LOG_CONTENT: '${{ env.LOG_CONTENT }}'
|
||||
TARGET_REF: '${{ github.event.inputs.ref }}'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
git checkout "${TARGET_REF}"
|
||||
git checkout '${{ github.event.inputs.ref }}'
|
||||
node scripts/releasing/patch-create-comment.js
|
||||
|
||||
- name: 'Fail Workflow if Main Task Failed'
|
||||
if: 'always() && steps.create_patch.outputs.EXIT_CODE != 0'
|
||||
env:
|
||||
EXIT_CODE: '${{ steps.create_patch.outputs.EXIT_CODE }}'
|
||||
run: |
|
||||
echo "Patch creation failed with exit code: ${EXIT_CODE}"
|
||||
echo "Patch creation failed with exit code: ${{ steps.create_patch.outputs.EXIT_CODE }}"
|
||||
echo "Check the logs above and the comment posted to the original PR for details."
|
||||
exit 1
|
||||
|
||||
@@ -18,7 +18,7 @@ on:
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
workflow_id:
|
||||
description: 'The workflow to trigger. Defaults to release-patch-3-release.yml'
|
||||
description: 'The workflow to trigger. Defaults to patch-release.yml'
|
||||
required: false
|
||||
type: 'string'
|
||||
default: 'release-patch-3-release.yml'
|
||||
@@ -37,20 +37,11 @@ on:
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
environment:
|
||||
description: 'Environment'
|
||||
required: false
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'prod'
|
||||
- 'dev'
|
||||
default: 'prod'
|
||||
|
||||
jobs:
|
||||
trigger-patch-release:
|
||||
if: "(github.event_name == 'pull_request' && github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'hotfix/')) || github.event_name == 'workflow_dispatch'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
permissions:
|
||||
actions: 'write'
|
||||
contents: 'write'
|
||||
@@ -83,7 +74,5 @@ jobs:
|
||||
GITHUB_EVENT_PAYLOAD: '${{ toJSON(github.event) }}'
|
||||
FORCE_SKIP_TESTS: '${{ github.event.inputs.force_skip_tests }}'
|
||||
TEST_MODE: '${{ github.event.inputs.test_mode }}'
|
||||
ENVIRONMENT: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
DRY_RUN: '${{ github.event.inputs.dry_run }}'
|
||||
run: |
|
||||
node scripts/releasing/patch-trigger.js --dry-run="${DRY_RUN}"
|
||||
node scripts/releasing/patch-trigger.js --dry-run=${{ github.event.inputs.dry_run }}
|
||||
|
||||
@@ -34,13 +34,13 @@ on:
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'prod'
|
||||
- 'dev'
|
||||
## - 'dev' # TODO(richieforeman) - support dev
|
||||
default: 'prod'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
environment: '${{ github.event.inputs.environment }}'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
@@ -66,17 +66,7 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'configure .npmrc'
|
||||
uses: './.github/actions/setup-npmrc'
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
||||
- name: 'Install Script Dependencies'
|
||||
run: |-
|
||||
npm ci
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
working-directory: './release'
|
||||
run: |-
|
||||
npm ci
|
||||
|
||||
@@ -90,12 +80,10 @@ jobs:
|
||||
id: 'patch_version'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
PATCH_FROM: '${{ github.event.inputs.type }}'
|
||||
CLI_PACKAGE_NAME: '${{vars.CLI_PACKAGE_NAME}}'
|
||||
run: |
|
||||
# Use the existing get-release-version.js script to calculate patch version
|
||||
# Run from main checkout which has full git history and access to npm
|
||||
PATCH_JSON=$(node scripts/get-release-version.js --type=patch --cli-package-name="${CLI_PACKAGE_NAME}" --patch-from="${PATCH_FROM}")
|
||||
PATCH_JSON=$(node scripts/get-release-version.js --type=patch --patch-from=${{ github.event.inputs.type }})
|
||||
echo "Patch version calculation result: ${PATCH_JSON}"
|
||||
|
||||
RELEASE_VERSION=$(echo "${PATCH_JSON}" | jq -r .releaseVersion)
|
||||
@@ -112,13 +100,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
CHANNEL: '${{ github.event.inputs.type }}'
|
||||
ORIGINAL_RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
|
||||
ORIGINAL_RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
|
||||
ORIGINAL_PREVIOUS_TAG: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}'
|
||||
run: |
|
||||
echo "🔍 Verifying no concurrent patch releases have occurred..."
|
||||
|
||||
# Store original calculation for comparison
|
||||
ORIGINAL_RELEASE_VERSION="${{ steps.patch_version.outputs.RELEASE_VERSION }}"
|
||||
ORIGINAL_RELEASE_TAG="${{ steps.patch_version.outputs.RELEASE_TAG }}"
|
||||
ORIGINAL_PREVIOUS_TAG="${{ steps.patch_version.outputs.PREVIOUS_TAG }}"
|
||||
|
||||
echo "Original calculation:"
|
||||
echo " Release version: ${ORIGINAL_RELEASE_VERSION}"
|
||||
echo " Release tag: ${ORIGINAL_RELEASE_TAG}"
|
||||
@@ -126,7 +115,7 @@ jobs:
|
||||
|
||||
# Re-run the same version calculation script
|
||||
echo "Re-calculating version to check for changes..."
|
||||
CURRENT_PATCH_JSON=$(node scripts/get-release-version.js --cli-package-name="${{vars.CLI_PACKAGE_NAME}}" --type=patch --patch-from="${CHANNEL}")
|
||||
CURRENT_PATCH_JSON=$(node scripts/get-release-version.js --type=patch --patch-from="${CHANNEL}")
|
||||
CURRENT_RELEASE_VERSION=$(echo "${CURRENT_PATCH_JSON}" | jq -r .releaseVersion)
|
||||
CURRENT_RELEASE_TAG=$(echo "${CURRENT_PATCH_JSON}" | jq -r .releaseTag)
|
||||
CURRENT_PREVIOUS_TAG=$(echo "${CURRENT_PATCH_JSON}" | jq -r .previousReleaseTag)
|
||||
@@ -183,6 +172,7 @@ jobs:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
previous-tag: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}'
|
||||
working-directory: './release'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
npm-registry-publish-url: '${{ vars.NPM_REGISTRY_PUBLISH_URL }}'
|
||||
npm-registry-url: '${{ vars.NPM_REGISTRY_URL }}'
|
||||
@@ -190,7 +180,6 @@ jobs:
|
||||
cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
|
||||
a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
working-directory: './release'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: '${{ failure() && github.event.inputs.dry_run == false }}'
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
calculate-versions:
|
||||
name: 'Calculate Versions and Plan'
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
environment: '${{ github.event.inputs.environment }}'
|
||||
|
||||
outputs:
|
||||
STABLE_VERSION: '${{ steps.versions.outputs.STABLE_VERSION }}'
|
||||
@@ -77,19 +77,10 @@ jobs:
|
||||
id: 'versions'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
STABLE_OVERRIDE: '${{ github.event.inputs.stable_version_override }}'
|
||||
PREVIEW_OVERRIDE: '${{ github.event.inputs.preview_version_override }}'
|
||||
REF_INPUT: '${{ github.event.inputs.ref }}'
|
||||
run: |
|
||||
set -e
|
||||
STABLE_COMMAND="node scripts/get-release-version.js --type=stable"
|
||||
if [[ -n "${STABLE_OVERRIDE}" ]]; then
|
||||
STABLE_COMMAND+=" --stable_version_override=${STABLE_OVERRIDE}"
|
||||
fi
|
||||
PREVIEW_COMMAND="node scripts/get-release-version.js --type=preview"
|
||||
if [[ -n "${PREVIEW_OVERRIDE}" ]]; then
|
||||
PREVIEW_COMMAND+=" --preview_version_override=${PREVIEW_OVERRIDE}"
|
||||
fi
|
||||
STABLE_COMMAND="node scripts/get-release-version.js --type=stable ${{ github.event.inputs.stable_version_override && format('--stable_version_override={0}', github.event.inputs.stable_version_override) || '' }}"
|
||||
PREVIEW_COMMAND="node scripts/get-release-version.js --type=preview ${{ github.event.inputs.preview_version_override && format('--preview_version_override={0}', github.event.inputs.preview_version_override) || '' }}"
|
||||
NIGHTLY_COMMAND="node scripts/get-release-version.js --type=promote-nightly"
|
||||
STABLE_JSON=$(${STABLE_COMMAND})
|
||||
PREVIEW_JSON=$(${PREVIEW_COMMAND})
|
||||
@@ -106,7 +97,7 @@ jobs:
|
||||
echo "PREVIOUS_STABLE_TAG=$(echo "${STABLE_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIEW_VERSION=$(echo "${PREVIEW_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}"
|
||||
# shellcheck disable=SC1083
|
||||
REF="${REF_INPUT}"
|
||||
REF="${{ github.event.inputs.ref }}"
|
||||
SHA=$(git ls-remote origin "$REF" | awk '{print $1}')
|
||||
if [ -z "$SHA" ]; then
|
||||
if [[ "$REF" =~ ^[0-9a-f]{7,40}$ ]]; then
|
||||
@@ -125,30 +116,20 @@ jobs:
|
||||
echo "NEXT_SHA=$SHA" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: 'Display Pending Updates'
|
||||
env:
|
||||
STABLE_VERSION: '${{ steps.versions.outputs.STABLE_VERSION }}'
|
||||
STABLE_SHA: '${{ steps.versions.outputs.STABLE_SHA }}'
|
||||
PREVIOUS_STABLE_TAG: '${{ steps.versions.outputs.PREVIOUS_STABLE_TAG }}'
|
||||
PREVIEW_VERSION: '${{ steps.versions.outputs.PREVIEW_VERSION }}'
|
||||
PREVIEW_SHA: '${{ steps.versions.outputs.PREVIEW_SHA }}'
|
||||
PREVIOUS_PREVIEW_TAG: '${{ steps.versions.outputs.PREVIOUS_PREVIEW_TAG }}'
|
||||
NEXT_NIGHTLY_VERSION: '${{ steps.versions.outputs.NEXT_NIGHTLY_VERSION }}'
|
||||
PREVIOUS_NIGHTLY_TAG: '${{ steps.versions.outputs.PREVIOUS_NIGHTLY_TAG }}'
|
||||
INPUT_REF: '${{ github.event.inputs.ref }}'
|
||||
run: |
|
||||
echo "Release Plan:"
|
||||
echo "-----------"
|
||||
echo "Stable Release: ${STABLE_VERSION}"
|
||||
echo " - Commit: ${STABLE_SHA}"
|
||||
echo " - Previous Tag: ${PREVIOUS_STABLE_TAG}"
|
||||
echo "Stable Release: ${{ steps.versions.outputs.STABLE_VERSION }}"
|
||||
echo " - Commit: ${{ steps.versions.outputs.STABLE_SHA }}"
|
||||
echo " - Previous Tag: ${{ steps.versions.outputs.PREVIOUS_STABLE_TAG }}"
|
||||
echo ""
|
||||
echo "Preview Release: ${PREVIEW_VERSION}"
|
||||
echo " - Commit: ${PREVIEW_SHA} (${INPUT_REF})"
|
||||
echo " - Previous Tag: ${PREVIOUS_PREVIEW_TAG}"
|
||||
echo "Preview Release: ${{ steps.versions.outputs.PREVIEW_VERSION }}"
|
||||
echo " - Commit: ${{ steps.versions.outputs.PREVIEW_SHA }} (${{ github.event.inputs.ref }})"
|
||||
echo " - Previous Tag: ${{ steps.versions.outputs.PREVIOUS_PREVIEW_TAG }}"
|
||||
echo ""
|
||||
echo "Preparing Next Nightly Release: ${NEXT_NIGHTLY_VERSION}"
|
||||
echo " - Merging Version Update PR to Branch: ${INPUT_REF}"
|
||||
echo " - Previous Tag: ${PREVIOUS_NIGHTLY_TAG}"
|
||||
echo "Preparing Next Nightly Release: ${{ steps.versions.outputs.NEXT_NIGHTLY_VERSION }}"
|
||||
echo " - Merging Version Update PR to Branch: ${{ github.event.inputs.ref }}"
|
||||
echo " - Previous Tag: ${{ steps.versions.outputs.PREVIOUS_NIGHTLY_TAG }}"
|
||||
|
||||
test:
|
||||
name: 'Test ${{ matrix.channel }}'
|
||||
@@ -198,7 +179,7 @@ jobs:
|
||||
name: 'Publish preview'
|
||||
needs: ['calculate-versions', 'test']
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
environment: '${{ github.event.inputs.environment }}'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
@@ -264,7 +245,7 @@ jobs:
|
||||
name: 'Publish stable'
|
||||
needs: ['calculate-versions', 'test', 'publish-preview']
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
environment: '${{ github.event.inputs.environment }}'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
|
||||
@@ -31,18 +31,10 @@ on:
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: true
|
||||
environment:
|
||||
description: 'Environment'
|
||||
required: false
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'prod'
|
||||
- 'dev'
|
||||
default: 'prod'
|
||||
|
||||
jobs:
|
||||
change-tags:
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
environment: 'prod' ## TODO(richieforeman) Support Dev mode
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
packages: 'write'
|
||||
@@ -58,29 +50,23 @@ jobs:
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: 'configure .npmrc'
|
||||
uses: './.github/actions/setup-npmrc'
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
registry-url: 'https://wombat-dressing-room.appspot.com'
|
||||
scope: '@google'
|
||||
|
||||
- name: 'Get Origin Version Tag'
|
||||
id: 'origin_tag'
|
||||
shell: 'bash'
|
||||
env:
|
||||
ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
|
||||
run: |
|
||||
TAG_VALUE="v${ROLLBACK_ORIGIN}"
|
||||
TAG_VALUE="v${{ github.event.inputs.rollback_origin }}"
|
||||
echo "ORIGIN_TAG=$TAG_VALUE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Get Origin Commit Hash'
|
||||
id: 'origin_hash'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
ORIGIN_TAG: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
echo "ORIGIN_HASH=$(git rev-parse "${ORIGIN_TAG}")" >> "$GITHUB_OUTPUT"
|
||||
echo "ORIGIN_HASH=$(git rev-parse ${{ steps.origin_tag.outputs.ORIGIN_TAG }})" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Change tag'
|
||||
if: "${{ github.event.inputs.rollback_destination != '' }}"
|
||||
@@ -97,85 +83,48 @@ jobs:
|
||||
core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
|
||||
a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
|
||||
- name: 'Get cli Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
id: 'cli-token'
|
||||
with:
|
||||
package-name: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
|
||||
|
||||
- name: 'Deprecate Cli Npm Package'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' && github.event.inputs.environment == 'prod' }}"
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.cli-token.outputs.auth-token }}'
|
||||
PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
|
||||
NODE_AUTH_TOKEN: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm deprecate "${PACKAGE_NAME}@${ROLLBACK_ORIGIN}" "This version has been rolled back."
|
||||
|
||||
- name: 'Get core Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
id: 'core-token'
|
||||
with:
|
||||
package-name: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
|
||||
npm deprecate @google/gemini-cli@${{ github.event.inputs.rollback_origin }} "This version has been rolled back."
|
||||
|
||||
- name: 'Deprecate Core Npm Package'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' && github.event.inputs.environment == 'prod' }}"
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.core-token.outputs.auth-token }}'
|
||||
PACKAGE_NAME: '${{ vars.CORE_PACKAGE_NAME }}'
|
||||
ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
|
||||
NODE_AUTH_TOKEN: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm deprecate "${PACKAGE_NAME}@${ROLLBACK_ORIGIN}" "This version has been rolled back."
|
||||
|
||||
- name: 'Get a2a Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
id: 'a2a-token'
|
||||
with:
|
||||
package-name: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
|
||||
npm deprecate @google/gemini-core@${{ github.event.inputs.rollback_origin }} "This version has been rolled back."
|
||||
|
||||
- name: 'Deprecate A2A Server Npm Package'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' && github.event.inputs.environment == 'prod' }}"
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.a2a-token.outputs.auth-token }}'
|
||||
PACKAGE_NAME: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
|
||||
NODE_AUTH_TOKEN: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm deprecate "${PACKAGE_NAME}@${ROLLBACK_ORIGIN}" "This version has been rolled back."
|
||||
npm deprecate @google/gemini-cli-a2a-server@${{ github.event.inputs.rollback_origin }} "This version has been rolled back."
|
||||
|
||||
- name: 'Delete Github Release'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' && github.event.inputs.environment == 'prod'}}"
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
ORIGIN_TAG: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
gh release delete "${ORIGIN_TAG}" --yes
|
||||
gh release delete '${{ steps.origin_tag.outputs.ORIGIN_TAG }}' --yes
|
||||
|
||||
- name: 'Verify Origin Release Deletion'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
TARGET_TAG: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
TARGET_TAG="${{ steps.origin_tag.outputs.ORIGIN_TAG }}"
|
||||
RELEASE_TAG=$(gh release view "$TARGET_TAG" --json tagName --jq .tagName)
|
||||
if [ "$RELEASE_TAG" = "$TARGET_TAG" ]; then
|
||||
echo "❌ Failed to delete release with tag ${TARGET_TAG}"
|
||||
echo '❌ Failed to delete release with tag ${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
|
||||
echo '❌ This means the release was not deleted, and the workflow should fail.'
|
||||
exit 1
|
||||
fi
|
||||
@@ -185,22 +134,21 @@ jobs:
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
ROLLBACK_TAG_NAME: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}-rollback'
|
||||
ORIGIN_HASH: '${{ steps.origin_hash.outputs.ORIGIN_HASH }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
ROLLBACK_TAG_NAME="${{ steps.origin_tag.outputs.ORIGIN_TAG }}-rollback"
|
||||
echo "ROLLBACK_TAG=$ROLLBACK_TAG_NAME" >> "$GITHUB_OUTPUT"
|
||||
git tag "$ROLLBACK_TAG_NAME" "${ORIGIN_HASH}"
|
||||
git tag "$ROLLBACK_TAG_NAME" "${{ steps.origin_hash.outputs.ORIGIN_HASH }}"
|
||||
git push origin --tags
|
||||
|
||||
- name: 'Verify Rollback Tag Added'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
TARGET_TAG: '${{ steps.rollback_tag.outputs.ROLLBACK_TAG }}'
|
||||
TARGET_HASH: '${{ steps.origin_hash.outputs.ORIGIN_HASH }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
TARGET_TAG="${{ steps.rollback_tag.outputs.ROLLBACK_TAG }}"
|
||||
TARGET_HASH="${{ steps.origin_hash.outputs.ORIGIN_HASH }}"
|
||||
ROLLBACK_COMMIT=$(git rev-parse -q --verify "$TARGET_TAG")
|
||||
if [ "$ROLLBACK_COMMIT" != "$TARGET_HASH" ]; then
|
||||
echo '❌ Failed to add tag $TARGET_TAG to commit $TARGET_HASH'
|
||||
@@ -210,32 +158,21 @@ jobs:
|
||||
|
||||
- name: 'Log Dry run'
|
||||
if: "${{ github.event.inputs.dry-run == 'true' }}"
|
||||
env:
|
||||
ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
|
||||
ROLLBACK_DESTINATION: '${{ github.event.inputs.rollback_destination }}'
|
||||
CHANNEL: '${{ github.event.inputs.channel }}'
|
||||
REF_INPUT: '${{ github.event.inputs.ref }}'
|
||||
ORIGIN_TAG: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
|
||||
ORIGIN_HASH: '${{ steps.origin_hash.outputs.ORIGIN_HASH }}'
|
||||
ROLLBACK_TAG: '${{ steps.rollback_tag.outputs.ROLLBACK_TAG }}'
|
||||
CLI_PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
CORE_PACKAGE_NAME: '${{ vars.CORE_PACKAGE_NAME }}'
|
||||
A2A_PACKAGE_NAME: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
echo "
|
||||
Inputs:
|
||||
- rollback_origin: '${ROLLBACK_ORIGIN}'
|
||||
- rollback_destination: '${ROLLBACK_DESTINATION}'
|
||||
- channel: '${CHANNEL}'
|
||||
- ref: '${REF_INPUT}'
|
||||
- rollback_origin: '${{ github.event.inputs.rollback_origin }}'
|
||||
- rollback_destination: '${{ github.event.inputs.rollback_destination }}'
|
||||
- channel: '${{ github.event.inputs.channel }}'
|
||||
- ref: '${{ github.event.inputs.ref }}'
|
||||
|
||||
Outputs:
|
||||
- ORIGIN_TAG: '${ORIGIN_TAG}'
|
||||
- ORIGIN_HASH: '${ORIGIN_HASH}'
|
||||
- ROLLBACK_TAG: '${ROLLBACK_TAG}'
|
||||
- ORIGIN_TAG: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
|
||||
- ORIGIN_HASH: '${{ steps.origin_hash.outputs.ORIGIN_HASH }}'
|
||||
- ROLLBACK_TAG: '${{ steps.rollback_tag.outputs.ROLLBACK_TAG }}'
|
||||
|
||||
Would have npm deprecate ${CLI_PACKAGE_NAME}@${ROLLBACK_ORIGIN}, ${CORE_PACKAGE_NAME}@${ROLLBACK_ORIGIN}, and ${A2A_PACKAGE_NAME}@${ROLLBACK_ORIGIN}
|
||||
Would have deleted the github release with tag ${ORIGIN_TAG}
|
||||
Would have added tag ${ORIGIN_TAG}-rollback to ${ORIGIN_HASH}
|
||||
Would have npm deprecate @google/gemini-cli@${{ github.event.inputs.rollback_origin }} and @google/gemini-core@${{ github.event.inputs.rollback_origin }}
|
||||
Would have deleted the github release with tag ${{ steps.origin_tag.outputs.ORIGIN_TAG }}
|
||||
Would have added tag ${{ steps.origin_tag.outputs.ORIGIN_TAG }}-rollback to ${{ steps.origin_hash.outputs.ORIGIN_HASH }}
|
||||
"
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
name: 'Test Chained E2E'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
merge_group:
|
||||
workflow_run:
|
||||
workflows: ['Trigger E2E']
|
||||
types: ['completed']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
head_sha:
|
||||
description: 'SHA of the commit to test'
|
||||
required: true
|
||||
repo_name:
|
||||
description: 'Repository name (e.g., owner/repo)'
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.event.workflow_run.head_branch || github.ref }}'
|
||||
cancel-in-progress: |-
|
||||
${{ github.event_name != 'push' && github.event_name != 'merge_group' }}
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
statuses: 'write'
|
||||
|
||||
jobs:
|
||||
merge_queue_skipper:
|
||||
name: 'Merge Queue Skipper'
|
||||
permissions: 'read-all'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
outputs:
|
||||
skip: '${{ steps.merge-queue-e2e-skipper.outputs.skip-check }}'
|
||||
steps:
|
||||
- id: 'merge-queue-e2e-skipper'
|
||||
uses: 'cariad-tech/merge-queue-ci-skipper@1032489e59437862c90a08a2c92809c903883772' # ratchet:cariad-tech/merge-queue-ci-skipper@main
|
||||
with:
|
||||
secret: '${{ secrets.GITHUB_TOKEN }}'
|
||||
continue-on-error: true
|
||||
|
||||
download_repo_name:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run'"
|
||||
outputs:
|
||||
repo_name: '${{ steps.output-repo-name.outputs.repo_name }}'
|
||||
steps:
|
||||
- name: 'Mock Repo Artifact'
|
||||
if: "${{ github.event_name == 'workflow_dispatch' }}"
|
||||
env:
|
||||
REPO_NAME: '${{ github.event.inputs.repo_name }}'
|
||||
run: |
|
||||
mkdir -p ./pr
|
||||
echo '${{ env.REPO_NAME }}' > ./pr/repo_name
|
||||
- uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'repo_name'
|
||||
path: 'pr/'
|
||||
- name: 'Download the repo_name artifact'
|
||||
uses: 'actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0' # ratchet:actions/download-artifact@v5
|
||||
env:
|
||||
RUN_ID: "${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || github.run_id }}"
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
name: 'repo_name'
|
||||
run-id: '${{ env.RUN_ID }}'
|
||||
path: '${{ runner.temp }}/artifacts'
|
||||
- name: 'Output Repo Name'
|
||||
id: 'output-repo-name'
|
||||
uses: 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd' # ratchet:actions/github-script@v8
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const temp = '${{ runner.temp }}/artifacts';
|
||||
const repo_name = String(fs.readFileSync(path.join(temp, 'repo_name')));
|
||||
core.setOutput('repo_name', repo_name);
|
||||
|
||||
parse_run_context:
|
||||
name: 'Parse run context'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs: 'download_repo_name'
|
||||
if: 'always()'
|
||||
outputs:
|
||||
repository: '${{ steps.set_context.outputs.REPO }}'
|
||||
sha: '${{ steps.set_context.outputs.SHA }}'
|
||||
steps:
|
||||
- id: 'set_context'
|
||||
name: 'Set dynamic repository and SHA'
|
||||
env:
|
||||
REPO: '${{ needs.download_repo_name.outputs.repo_name || github.repository }}'
|
||||
SHA: '${{ github.event.inputs.head_sha || github.event.workflow_run.head_sha || github.sha }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
echo "REPO=$REPO" >> "$GITHUB_OUTPUT"
|
||||
echo "SHA=$SHA" >> "$GITHUB_OUTPUT"
|
||||
|
||||
set_pending_status:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
permissions: 'write-all'
|
||||
if: "github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run'"
|
||||
needs:
|
||||
- 'parse_run_context'
|
||||
steps:
|
||||
- name: 'Set pending status'
|
||||
uses: 'myrotvorets/set-commit-status-action@16037e056d73b2d3c88e37e393ff369047f70886' # ratchet:myrotvorets/set-commit-status-action@master
|
||||
if: 'always()'
|
||||
with:
|
||||
allowForks: 'true'
|
||||
repo: '${{ needs.parse_run_context.outputs.repository }}'
|
||||
sha: '${{ needs.parse_run_context.outputs.sha }}'
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
status: 'pending'
|
||||
|
||||
e2e_linux:
|
||||
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
|
||||
needs:
|
||||
- 'merge_queue_skipper'
|
||||
- 'parse_run_context'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: |
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
sandbox:
|
||||
- 'sandbox:none'
|
||||
- 'sandbox:docker'
|
||||
node-version:
|
||||
- '20.x'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ needs.parse_run_context.outputs.sha }}'
|
||||
repository: '${{ needs.parse_run_context.outputs.repository }}'
|
||||
|
||||
- name: 'Set up Node.js ${{ matrix.node-version }}'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '${{ matrix.node-version }}'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Set up Docker'
|
||||
if: "matrix.sandbox == 'sandbox:docker'"
|
||||
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
|
||||
|
||||
- name: 'Run E2E tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
KEEP_OUTPUT: 'true'
|
||||
VERBOSE: 'true'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then
|
||||
npm run test:integration:sandbox:docker
|
||||
else
|
||||
npm run test:integration:sandbox:none
|
||||
fi
|
||||
|
||||
e2e_mac:
|
||||
name: 'E2E Test (macOS)'
|
||||
needs:
|
||||
- 'merge_queue_skipper'
|
||||
- 'parse_run_context'
|
||||
runs-on: 'macos-latest'
|
||||
if: |
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false')
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ needs.parse_run_context.outputs.sha }}'
|
||||
repository: '${{ needs.parse_run_context.outputs.repository }}'
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Fix rollup optional dependencies on macOS'
|
||||
if: "runner.os == 'macOS'"
|
||||
run: |
|
||||
npm cache clean --force
|
||||
- name: 'Run E2E tests (non-Windows)'
|
||||
if: "runner.os != 'Windows'"
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
KEEP_OUTPUT: 'true'
|
||||
SANDBOX: 'sandbox:none'
|
||||
VERBOSE: 'true'
|
||||
run: 'npm run test:integration:sandbox:none'
|
||||
|
||||
e2e_windows:
|
||||
name: 'Slow E2E - Win'
|
||||
needs:
|
||||
- 'merge_queue_skipper'
|
||||
- 'parse_run_context'
|
||||
if: |
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false')
|
||||
runs-on: 'gemini-cli-windows-16-core'
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ needs.parse_run_context.outputs.sha }}'
|
||||
repository: '${{ needs.parse_run_context.outputs.repository }}'
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Configure Windows Defender exclusions'
|
||||
run: |
|
||||
Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:TEMP" -Force
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Configure npm for Windows performance'
|
||||
run: |
|
||||
npm config set progress false
|
||||
npm config set audit false
|
||||
npm config set fund false
|
||||
npm config set loglevel error
|
||||
npm config set maxsockets 32
|
||||
npm config set registry https://registry.npmjs.org/
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Run E2E tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
KEEP_OUTPUT: 'true'
|
||||
SANDBOX: 'sandbox:none'
|
||||
VERBOSE: 'true'
|
||||
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
|
||||
UV_THREADPOOL_SIZE: '32'
|
||||
NODE_ENV: 'test'
|
||||
shell: 'pwsh'
|
||||
run: 'npm run test:integration:sandbox:none'
|
||||
|
||||
e2e:
|
||||
name: 'E2E'
|
||||
if: |
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false')
|
||||
needs:
|
||||
- 'e2e_linux'
|
||||
- 'e2e_mac'
|
||||
- 'merge_queue_skipper'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Check E2E test results'
|
||||
run: |
|
||||
if [[ ${{ needs.e2e_linux.result }} != 'success' || \
|
||||
${{ needs.e2e_mac.result }} != 'success' ]]; then
|
||||
echo "One or more E2E jobs failed."
|
||||
exit 1
|
||||
fi
|
||||
echo "All required E2E jobs passed!"
|
||||
|
||||
set_workflow_status:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
permissions: 'write-all'
|
||||
if: "github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run'"
|
||||
needs:
|
||||
- 'parse_run_context'
|
||||
- 'e2e'
|
||||
steps:
|
||||
- name: 'Set workflow status'
|
||||
uses: 'myrotvorets/set-commit-status-action@16037e056d73b2d3c88e37e393ff369047f70886' # ratchet:myrotvorets/set-commit-status-action@master
|
||||
if: 'always()'
|
||||
with:
|
||||
allowForks: 'true'
|
||||
repo: '${{ needs.parse_run_context.outputs.repository }}'
|
||||
sha: '${{ needs.parse_run_context.outputs.sha }}'
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
status: '${{ job.status }}'
|
||||
@@ -1,33 +0,0 @@
|
||||
name: 'Trigger E2E'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch_ref:
|
||||
description: 'Branch to run on'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: 'string'
|
||||
|
||||
jobs:
|
||||
save_repo_name:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Save Repo name'
|
||||
env:
|
||||
# Replace with github.event.pull_request.head.repo.full_name when switched to listen on pull request events. This repo name does not contain the org which is needed for checkout.
|
||||
REPO_NAME: '${{ github.event.repository.name }}'
|
||||
run: |
|
||||
mkdir -p ./pr
|
||||
echo '${{ env.REPO_NAME }}' > ./pr/repo_name
|
||||
- uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'repo_name'
|
||||
path: 'pr/'
|
||||
trigger_e2e:
|
||||
name: 'Trigger e2e'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- id: 'trigger-e2e'
|
||||
run: |
|
||||
echo "Trigger e2e workflow"
|
||||
@@ -28,7 +28,7 @@ on:
|
||||
|
||||
jobs:
|
||||
verify-release:
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
environment: '${{ github.event.inputs.environment }}'
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
with:
|
||||
npm-package: '${{vars.CLI_PACKAGE_NAME}}@${{github.event.inputs.npm-tag}}'
|
||||
expected-version: '${{github.event.inputs.version}}'
|
||||
working-directory: '.'
|
||||
ref: '${{github.ref}}'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
npm-registry-url: '${{ vars.NPM_REGISTRY_URL }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
||||
@@ -47,12 +47,9 @@ packages/cli/src/generated/
|
||||
packages/core/src/generated/
|
||||
.integration-tests/
|
||||
packages/vscode-ide-companion/*.vsix
|
||||
packages/cli/download-ripgrep*/
|
||||
|
||||
# GHA credentials
|
||||
gha-creds-*.json
|
||||
|
||||
# Log files
|
||||
patch_output.log
|
||||
|
||||
.genkit
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
http://localhost:16686/
|
||||
https://github.com/google-gemini/gemini-cli/issues/new/choose
|
||||
https://github.com/google-gemini/maintainers-gemini-cli/blob/main/npm.md
|
||||
https://github.com/settings/personal-access-tokens/new
|
||||
https://github.com/settings/tokens/new
|
||||
https://www.npmjs.com/package/@google/gemini-cli
|
||||
+33
-151
@@ -1,18 +1,6 @@
|
||||
# How to Contribute
|
||||
|
||||
We would love to accept your patches and contributions to this project. This
|
||||
document includes:
|
||||
|
||||
- **[Before you begin](#before-you-begin):** Essential steps to take before
|
||||
becoming a Gemini CLI contributor.
|
||||
- **[Code contribution process](#code-contribution-process):** How to contribute
|
||||
code to Gemini CLI.
|
||||
- **[Development setup and workflow](#development-setup-and-workflow):** How to
|
||||
set up your development environment and workflow.
|
||||
- **[Documentation contribution process](#documentation-contribution-process):**
|
||||
How to contribute documentation to Gemini CLI.
|
||||
|
||||
We're looking forward to seeing your contributions!
|
||||
We would love to accept your patches and contributions to this project.
|
||||
|
||||
## Before you begin
|
||||
|
||||
@@ -35,25 +23,15 @@ sign a new one.
|
||||
This project follows
|
||||
[Google's Open Source Community Guidelines](https://opensource.google/conduct/).
|
||||
|
||||
## Code contribution process
|
||||
## Contribution Process
|
||||
|
||||
### Get started
|
||||
|
||||
The process for contributing code is as follows:
|
||||
|
||||
1. **Find an issue** that you want to work on.
|
||||
2. **Fork the repository** and create a new branch.
|
||||
3. **Make your changes** in the `packages/` directory.
|
||||
4. **Ensure all checks pass** by running `npm run preflight`.
|
||||
5. **Open a pull request** with your changes.
|
||||
|
||||
### Code reviews
|
||||
### Code Reviews
|
||||
|
||||
All submissions, including submissions by project members, require review. We
|
||||
use [GitHub pull requests](https://docs.github.com/articles/about-pull-requests)
|
||||
for this purpose.
|
||||
|
||||
### Self assigning issues
|
||||
### Self Assigning Issues
|
||||
|
||||
If you're looking for an issue to work on, check out our list of issues that are
|
||||
labeled
|
||||
@@ -66,12 +44,12 @@ assign the issue to you, provided it is not already assigned.
|
||||
Please note that you can have a maximum of 3 issues assigned to you at any given
|
||||
time.
|
||||
|
||||
### Pull request guidelines
|
||||
### Pull Request Guidelines
|
||||
|
||||
To help us review and merge your PRs quickly, please follow these guidelines.
|
||||
PRs that do not meet these standards may be closed.
|
||||
|
||||
#### 1. Link to an existing issue
|
||||
#### 1. Link to an Existing Issue
|
||||
|
||||
All PRs should be linked to an existing issue in our tracker. This ensures that
|
||||
every change has been discussed and is aligned with the project's goals before
|
||||
@@ -84,7 +62,7 @@ any code is written.
|
||||
If an issue for your change doesn't exist, please **open one first** and wait
|
||||
for feedback before you start coding.
|
||||
|
||||
#### 2. Keep it small and focused
|
||||
#### 2. Keep It Small and Focused
|
||||
|
||||
We favor small, atomic PRs that address a single issue or add a single,
|
||||
self-contained feature.
|
||||
@@ -96,40 +74,37 @@ self-contained feature.
|
||||
Large changes should be broken down into a series of smaller, logical PRs that
|
||||
can be reviewed and merged independently.
|
||||
|
||||
#### 3. Use draft PRs for work in progress
|
||||
#### 3. Use Draft PRs for Work in Progress
|
||||
|
||||
If you'd like to get early feedback on your work, please use GitHub's **Draft
|
||||
Pull Request** feature. This signals to the maintainers that the PR is not yet
|
||||
ready for a formal review but is open for discussion and initial feedback.
|
||||
|
||||
#### 4. Ensure all checks pass
|
||||
#### 4. Ensure All Checks Pass
|
||||
|
||||
Before submitting your PR, ensure that all automated checks are passing by
|
||||
running `npm run preflight`. This command runs all tests, linting, and other
|
||||
style checks.
|
||||
|
||||
#### 5. Update documentation
|
||||
#### 5. Update Documentation
|
||||
|
||||
If your PR introduces a user-facing change (e.g., a new command, a modified
|
||||
flag, or a change in behavior), you must also update the relevant documentation
|
||||
in the `/docs` directory.
|
||||
|
||||
See more about writing documentation:
|
||||
[Documentation contribution process](#documentation-contribution-process).
|
||||
|
||||
#### 6. Write clear commit messages and a good PR description
|
||||
#### 6. Write Clear Commit Messages and a Good PR Description
|
||||
|
||||
Your PR should have a clear, descriptive title and a detailed description of the
|
||||
changes. Follow the [Conventional Commits](https://www.conventionalcommits.org/)
|
||||
standard for your commit messages.
|
||||
|
||||
- **Good PR title:** `feat(cli): Add --json flag to 'config get' command`
|
||||
- **Bad PR title:** `Made some changes`
|
||||
- **Good PR Title:** `feat(cli): Add --json flag to 'config get' command`
|
||||
- **Bad PR Title:** `Made some changes`
|
||||
|
||||
In the PR description, explain the "why" behind your changes and link to the
|
||||
relevant issue (e.g., `Fixes #123`).
|
||||
|
||||
### Forking
|
||||
## Forking
|
||||
|
||||
If you are forking the repository you will be able to run the Build, Test and
|
||||
Integration test workflows. However in order to make the integration tests run
|
||||
@@ -143,12 +118,12 @@ Additionally you will need to click on the `Actions` tab and enable workflows
|
||||
for your repository, you'll find it's the large blue button in the center of the
|
||||
screen.
|
||||
|
||||
### Development setup and workflow
|
||||
## Development Setup and Workflow
|
||||
|
||||
This section guides contributors on how to build, modify, and understand the
|
||||
development setup of this project.
|
||||
|
||||
### Setting up the development environment
|
||||
### Setting Up the Development Environment
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
@@ -160,7 +135,7 @@ development setup of this project.
|
||||
version of Node.js `>=20` is acceptable.
|
||||
2. **Git**
|
||||
|
||||
### Build process
|
||||
### Build Process
|
||||
|
||||
To clone the repository:
|
||||
|
||||
@@ -185,7 +160,7 @@ This command typically compiles TypeScript to JavaScript, bundles assets, and
|
||||
prepares the packages for execution. Refer to `scripts/build.js` and
|
||||
`package.json` scripts for more details on what happens during the build.
|
||||
|
||||
### Enabling sandboxing
|
||||
### Enabling Sandboxing
|
||||
|
||||
[Sandboxing](#sandboxing) is highly recommended and requires, at a minimum,
|
||||
setting `GEMINI_SANDBOX=true` in your `~/.env` and ensuring a sandboxing
|
||||
@@ -201,7 +176,7 @@ npm run build:all
|
||||
|
||||
To skip building the sandbox container, you can use `npm run build` instead.
|
||||
|
||||
### Running the CLI
|
||||
### Running
|
||||
|
||||
To start the Gemini CLI from the source code (after building), run the following
|
||||
command from the root directory:
|
||||
@@ -215,11 +190,11 @@ utilize `npm link path/to/gemini-cli/packages/cli` (see:
|
||||
[docs](https://docs.npmjs.com/cli/v9/commands/npm-link)) or
|
||||
`alias gemini="node path/to/gemini-cli/packages/cli"` to run with `gemini`
|
||||
|
||||
### Running tests
|
||||
### Running Tests
|
||||
|
||||
This project contains two types of tests: unit tests and integration tests.
|
||||
|
||||
#### Unit tests
|
||||
#### Unit Tests
|
||||
|
||||
To execute the unit test suite for the project:
|
||||
|
||||
@@ -231,7 +206,7 @@ This will run tests located in the `packages/core` and `packages/cli`
|
||||
directories. Ensure tests pass before submitting any changes. For a more
|
||||
comprehensive check, it is recommended to run `npm run preflight`.
|
||||
|
||||
#### Integration tests
|
||||
#### Integration Tests
|
||||
|
||||
The integration tests are designed to validate the end-to-end functionality of
|
||||
the Gemini CLI. They are not run as part of the default `npm run test` command.
|
||||
@@ -243,9 +218,9 @@ npm run test:e2e
|
||||
```
|
||||
|
||||
For more detailed information on the integration testing framework, please see
|
||||
the [Integration Tests documentation](/docs/integration-tests.md).
|
||||
the [Integration Tests documentation](./docs/integration-tests.md).
|
||||
|
||||
### Linting and preflight checks
|
||||
### Linting and Preflight Checks
|
||||
|
||||
To ensure code quality and formatting consistency, run the preflight check:
|
||||
|
||||
@@ -292,7 +267,7 @@ root directory:
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Coding conventions
|
||||
### Coding Conventions
|
||||
|
||||
- Please adhere to the coding style, patterns, and conventions used throughout
|
||||
the existing codebase.
|
||||
@@ -304,24 +279,19 @@ npm run lint
|
||||
- **Imports:** Pay special attention to import paths. The project uses ESLint to
|
||||
enforce restrictions on relative imports between packages.
|
||||
|
||||
### Project structure
|
||||
### Project Structure
|
||||
|
||||
- `packages/`: Contains the individual sub-packages of the project.
|
||||
- `a2a-server`: A2A server implementation for the Gemini CLI. (Experimental)
|
||||
- `cli/`: The command-line interface.
|
||||
- `core/`: The core backend logic for the Gemini CLI.
|
||||
- `test-utils` Utilities for creating and cleaning temporary file systems for
|
||||
testing.
|
||||
- `vscode-ide-companion/`: The Gemini CLI Companion extension pairs with
|
||||
Gemini CLI.
|
||||
- `docs/`: Contains all project documentation.
|
||||
- `scripts/`: Utility scripts for building, testing, and development tasks.
|
||||
|
||||
For more detailed architecture, see `docs/architecture.md`.
|
||||
|
||||
### Debugging
|
||||
## Debugging
|
||||
|
||||
#### VS Code
|
||||
### VS Code:
|
||||
|
||||
0. Run the CLI to interactively debug in VS Code with `F5`
|
||||
1. Start the CLI in debug mode from the root directory:
|
||||
@@ -379,9 +349,9 @@ used for the CLI's interface, is compatible with React DevTools version 4.x.
|
||||
Your running CLI application should then connect to React DevTools.
|
||||

|
||||
|
||||
### Sandboxing
|
||||
## Sandboxing
|
||||
|
||||
#### macOS Seatbelt
|
||||
### macOS Seatbelt
|
||||
|
||||
On macOS, `gemini` uses Seatbelt (`sandbox-exec`) under a `permissive-open`
|
||||
profile (see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) that
|
||||
@@ -397,7 +367,7 @@ Available built-in profiles are `{permissive,restrictive}-{open,closed,proxied}`
|
||||
`.gemini/sandbox-macos-<profile>.sb` under your project settings directory
|
||||
`.gemini`.
|
||||
|
||||
#### Container-based sandboxing (all platforms)
|
||||
### Container-based Sandboxing (All Platforms)
|
||||
|
||||
For stronger container-based sandboxing on macOS or other platforms, you can set
|
||||
`GEMINI_SANDBOX=true|docker|podman|<command>` in your environment or `.env`
|
||||
@@ -420,7 +390,7 @@ for your projects by creating the files `.gemini/sandbox.Dockerfile` and/or
|
||||
running `gemini` with `BUILD_SANDBOX=1` to trigger building of your custom
|
||||
sandbox.
|
||||
|
||||
#### Proxied networking
|
||||
#### Proxied Networking
|
||||
|
||||
All sandboxing methods, including macOS Seatbelt using `*-proxied` profiles,
|
||||
support restricting outbound network traffic through a custom proxy server that
|
||||
@@ -431,7 +401,7 @@ connections to `example.com:443` (e.g. `curl https://example.com`) and declines
|
||||
all other requests. The proxy is started and stopped automatically alongside the
|
||||
sandbox.
|
||||
|
||||
### Manual publish
|
||||
## Manual Publish
|
||||
|
||||
We publish an artifact for each commit to our internal registry. But if you need
|
||||
to manually cut a local build, then run the following commands:
|
||||
@@ -443,91 +413,3 @@ npm run auth
|
||||
npm run prerelease:dev
|
||||
npm publish --workspaces
|
||||
```
|
||||
|
||||
## Documentation contribution process
|
||||
|
||||
Our documentation must be kept up-to-date with our code contributions. We want
|
||||
our documentation to be clear, concise, and helpful to our users. We value:
|
||||
|
||||
- **Clarity:** Use simple and direct language. Avoid jargon where possible.
|
||||
- **Accuracy:** Ensure all information is correct and up-to-date.
|
||||
- **Completeness:** Cover all aspects of a feature or topic.
|
||||
- **Examples:** Provide practical examples to help users understand how to use
|
||||
Gemini CLI.
|
||||
|
||||
### Getting started
|
||||
|
||||
The process for contributing to the documentation is similar to contributing
|
||||
code.
|
||||
|
||||
1. **Fork the repository** and create a new branch.
|
||||
2. **Make your changes** in the `/docs` directory.
|
||||
3. **Preview your changes locally** in Markdown rendering.
|
||||
4. **Lint and format your changes.** Our preflight check includes linting and
|
||||
formatting for documentation files.
|
||||
```bash
|
||||
npm run preflight
|
||||
```
|
||||
5. **Open a pull request** with your changes.
|
||||
|
||||
### Documentation structure
|
||||
|
||||
Our documentation is organized using [sidebar.json](/docs/sidebar.json) as the
|
||||
table of contents. When adding new documentation:
|
||||
|
||||
1. Create your markdown file **in the appropriate directory** under `/docs`.
|
||||
2. Add an entry to `sidebar.json` in the relevant section.
|
||||
3. Ensure all internal links use relative paths and point to existing files.
|
||||
|
||||
### Style guide
|
||||
|
||||
We follow the
|
||||
[Google Developer Documentation Style Guide](https://developers.google.com/style).
|
||||
Please refer to it for guidance on writing style, tone, and formatting.
|
||||
|
||||
#### Key style points
|
||||
|
||||
- Use sentence case for headings.
|
||||
- Write in second person ("you") when addressing the reader.
|
||||
- Use present tense.
|
||||
- Keep paragraphs short and focused.
|
||||
- Use code blocks with appropriate language tags for syntax highlighting.
|
||||
- Include practical examples whenever possible.
|
||||
|
||||
### Linting and formatting
|
||||
|
||||
We use `prettier` to enforce a consistent style across our documentation. The
|
||||
`npm run preflight` command will check for any linting issues.
|
||||
|
||||
You can also run the linter and formatter separately:
|
||||
|
||||
- `npm run lint` - Check for linting issues
|
||||
- `npm run format` - Auto-format markdown files
|
||||
- `npm run lint:fix` - Auto-fix linting issues where possible
|
||||
|
||||
Please make sure your contributions are free of linting errors before submitting
|
||||
a pull request.
|
||||
|
||||
### Before you submit
|
||||
|
||||
Before submitting your documentation pull request, please:
|
||||
|
||||
1. Run `npm run preflight` to ensure all checks pass.
|
||||
2. Review your changes for clarity and accuracy.
|
||||
3. Check that all links work correctly.
|
||||
4. Ensure any code examples are tested and functional.
|
||||
5. Sign the
|
||||
[Contributor License Agreement (CLA)](https://cla.developers.google.com/) if
|
||||
you haven't already.
|
||||
|
||||
### Need help?
|
||||
|
||||
If you have questions about contributing documentation:
|
||||
|
||||
- Check our [FAQ](/docs/faq.md).
|
||||
- Review existing documentation for examples.
|
||||
- Open [an issue](https://github.com/google-gemini/gemini-cli/issues) to discuss
|
||||
your proposed changes.
|
||||
- Reach out to the maintainers.
|
||||
|
||||
We appreciate your contributions to making Gemini CLI documentation better!
|
||||
|
||||
@@ -11,8 +11,6 @@ Gemini CLI is an open-source AI agent that brings the power of Gemini directly
|
||||
into your terminal. It provides lightweight access to Gemini, giving you the
|
||||
most direct path from your prompt to our model.
|
||||
|
||||
Learn all about Gemini CLI in our [documentation](https://geminicli.com/docs/).
|
||||
|
||||
## 🚀 Why Gemini CLI?
|
||||
|
||||
- **🎯 Free tier**: 60 requests/min and 1,000 requests/day with personal Google
|
||||
@@ -27,11 +25,6 @@ Learn all about Gemini CLI in our [documentation](https://geminicli.com/docs/).
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Pre-requisites before installation
|
||||
|
||||
- Node.js version 20 or higher
|
||||
- macOS, Linux, or Windows
|
||||
|
||||
### Quick Install
|
||||
|
||||
#### Run instantly with npx
|
||||
@@ -53,6 +46,11 @@ npm install -g @google/gemini-cli
|
||||
brew install gemini-cli
|
||||
```
|
||||
|
||||
#### System Requirements
|
||||
|
||||
- Node.js version 20 or higher
|
||||
- macOS, Linux, or Windows
|
||||
|
||||
## Release Cadence and Tags
|
||||
|
||||
See [Releases](./docs/releases.md) for more details.
|
||||
@@ -230,13 +228,6 @@ the `--output-format json` flag to get structured output:
|
||||
gemini -p "Explain the architecture of this codebase" --output-format json
|
||||
```
|
||||
|
||||
For real-time event streaming (useful for monitoring long-running operations),
|
||||
use `--output-format stream-json` to get newline-delimited JSON events:
|
||||
|
||||
```bash
|
||||
gemini -p "Run tests and deploy" --output-format stream-json
|
||||
```
|
||||
|
||||
### Quick Examples
|
||||
|
||||
#### Start a new project
|
||||
@@ -306,8 +297,6 @@ gemini
|
||||
corporate environment.
|
||||
- [**Telemetry & Monitoring**](./docs/cli/telemetry.md) - Usage tracking.
|
||||
- [**Tools API Development**](./docs/core/tools-api.md) - Create custom tools.
|
||||
- [**Local development**](./docs/local-development.md) - Local development
|
||||
tooling.
|
||||
|
||||
### Troubleshooting & Support
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../CONTRIBUTING.md
|
||||
@@ -3,48 +3,6 @@
|
||||
Wondering what's new in Gemini CLI? This document provides key highlights and
|
||||
notable changes to Gemini CLI.
|
||||
|
||||
## v0.10.0 - Gemini CLI weekly update - 2025-10-13
|
||||
|
||||
- **Polish:** The team has been heads down bug fixing and investing heavily into
|
||||
polishing existing flows, tools, and interactions.
|
||||
- **Interactive Shell Tool calling:** Gemini CLI can now also execute
|
||||
interactive tools if needed
|
||||
([pr](https://github.com/google-gemini/gemini-cli/pull/11225) by
|
||||
[@galz10](https://github.com/galz10)).
|
||||
- **Alt+Key support:** Enables broader support for Alt+Key keyboard shortcuts
|
||||
across different terminals.
|
||||
([pr](https://github.com/google-gemini/gemini-cli/pull/10767) by
|
||||
[@srivatsj](https://github.com/srivatsj)).
|
||||
- **Telemetry Diff stats:** Track line changes made by the model and user during
|
||||
file operations via OTEL.
|
||||
([pr](https://github.com/google-gemini/gemini-cli/pull/10819) by
|
||||
[@jerop](https://github.com/jerop)).
|
||||
|
||||
## v0.9.0 - Gemini CLI weekly update - 2025-10-06
|
||||
|
||||
- 🎉 **Interactive Shell:** Run interactive commands like `vim`, `rebase -i`, or
|
||||
even `gemini` 😎 directly in Gemini CLI:
|
||||
- Blog:
|
||||
[https://developers.googleblog.com/en/say-hello-to-a-new-level-of-interactivity-in-gemini-cli/](https://developers.googleblog.com/en/say-hello-to-a-new-level-of-interactivity-in-gemini-cli/)
|
||||
- **Install pre-release extensions:** Install the latest `--pre-release`
|
||||
versions of extensions. Used for when an extension’s release hasn’t been
|
||||
marked as "latest".
|
||||
([pr](https://github.com/google-gemini/gemini-cli/pull/10752) by
|
||||
[@jakemac53](https://github.com/jakemac53))
|
||||
- **Simplified extension creation:** Create a new, empty extension. Templates
|
||||
are no longer required.
|
||||
([pr](https://github.com/google-gemini/gemini-cli/pull/10629) by
|
||||
[@chrstnb](https://github.com/chrstnb))
|
||||
- **OpenTelemetry GenAI metrics:** Aligns telemetry with industry-standard
|
||||
semantic conventions for improved interoperability.
|
||||
([spec](https://opentelemetry.io/docs/concepts/semantic-conventions/),
|
||||
[pr](https://github.com/google-gemini/gemini-cli/pull/10343) by
|
||||
[@jerop](https://github.com/jerop))
|
||||
- **List memory files:** Quickly find the location of your long-term memory
|
||||
files with `/memory list`.
|
||||
([pr](https://github.com/google-gemini/gemini-cli/pull/10108) by
|
||||
[@sgnagnarella](https://github.com/sgnagnarella))
|
||||
|
||||
## v0.8.0 - Gemini CLI weekly update - 2025-09-29
|
||||
|
||||
- 🎉 **Announcing Gemini CLI Extensions** 🎉
|
||||
@@ -288,7 +246,7 @@ notable changes to Gemini CLI.
|
||||
`"showCitations": true`
|
||||
([pr](https://github.com/google-gemini/gemini-cli/pull/7350) by
|
||||
[@scidomino](https://github.com/scidomino))
|
||||
- **Pro Quota Dialog:** Handle daily Pro model usage limits with an interactive
|
||||
- **Pro Quota Ddalog:** Handle daily Pro model usage limits with an interactive
|
||||
dialog that lets you immediately switch auth or fallback.
|
||||
([pr](https://github.com/google-gemini/gemini-cli/pull/7094) by
|
||||
[@JayadityaGit](https://github.com/JayadityaGit))
|
||||
|
||||
+12
-21
@@ -100,26 +100,18 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
available commands and their usage.
|
||||
|
||||
- **`/mcp`**
|
||||
- **Description:** Manage configured Model Context Protocol (MCP) servers.
|
||||
- **Description:** List configured Model Context Protocol (MCP) servers, their
|
||||
connection status, server details, and available tools.
|
||||
- **Sub-commands:**
|
||||
- **`list`** or **`ls`**:
|
||||
- **Description:** List configured MCP servers and tools. This is the
|
||||
default action if no subcommand is specified.
|
||||
- **`desc`**
|
||||
- **Description:** List configured MCP servers and tools with
|
||||
descriptions.
|
||||
- **`desc`** or **`descriptions`**:
|
||||
- **Description:** Show detailed descriptions for MCP servers and tools.
|
||||
- **`nodesc`** or **`nodescriptions`**:
|
||||
- **Description:** Hide tool descriptions, showing only the tool names.
|
||||
- **`schema`**:
|
||||
- **Description:** List configured MCP servers and tools with descriptions
|
||||
and schemas.
|
||||
- **`auth`**:
|
||||
- **Description:** Authenticate with an OAuth-enabled MCP server.
|
||||
- **Usage:** `/mcp auth <server-name>`
|
||||
- **Details:** If `<server-name>` is provided, it initiates the OAuth flow
|
||||
for that server. If no server name is provided, it lists all configured
|
||||
servers that support OAuth authentication.
|
||||
- **`refresh`**:
|
||||
- **Description:** Restarts all MCP servers and re-discovers their
|
||||
available tools.
|
||||
- **Description:** Show the full JSON schema for the tool's configured
|
||||
parameters.
|
||||
- **Keyboard Shortcut:** Press **Ctrl+T** at any time to toggle between
|
||||
showing and hiding tool descriptions.
|
||||
|
||||
- **`/memory`**
|
||||
- **Description:** Manage the AI's instructional context (hierarchical memory
|
||||
@@ -300,9 +292,8 @@ Gemini CLI.
|
||||
|
||||
- **`!<shell_command>`**
|
||||
- **Description:** Execute the given `<shell_command>` using `bash` on
|
||||
Linux/macOS or `powershell.exe -NoProfile -Command` on Windows (unless you
|
||||
override `ComSpec`). Any output or errors from the command are displayed in
|
||||
the terminal.
|
||||
Linux/macOS or `cmd.exe` on Windows. Any output or errors from the command
|
||||
are displayed in the terminal.
|
||||
- **Examples:**
|
||||
- `!ls -la` (executes `ls -la` and returns to Gemini CLI)
|
||||
- `!git status` (executes `git status` and returns to Gemini CLI)
|
||||
|
||||
@@ -1,770 +0,0 @@
|
||||
# Gemini CLI Configuration
|
||||
|
||||
Gemini CLI offers several ways to configure its behavior, including environment
|
||||
variables, command-line arguments, and settings files. This document outlines
|
||||
the different configuration methods and available settings.
|
||||
|
||||
## Configuration layers
|
||||
|
||||
Configuration is applied in the following order of precedence (lower numbers are
|
||||
overridden by higher numbers):
|
||||
|
||||
1. **Default values:** Hardcoded defaults within the application.
|
||||
2. **User settings file:** Global settings for the current user.
|
||||
3. **Project settings file:** Project-specific settings.
|
||||
4. **System settings file:** System-wide settings.
|
||||
5. **Environment variables:** System-wide or session-specific variables,
|
||||
potentially loaded from `.env` files.
|
||||
6. **Command-line arguments:** Values passed when launching the CLI.
|
||||
|
||||
## Settings files
|
||||
|
||||
Gemini CLI uses `settings.json` files for persistent configuration. There are
|
||||
three locations for these files:
|
||||
|
||||
- **User settings file:**
|
||||
- **Location:** `~/.gemini/settings.json` (where `~` is your home directory).
|
||||
- **Scope:** Applies to all Gemini CLI sessions for the current user.
|
||||
- **Project settings file:**
|
||||
- **Location:** `.gemini/settings.json` within your project's root directory.
|
||||
- **Scope:** Applies only when running Gemini CLI from that specific project.
|
||||
Project settings override user settings.
|
||||
- **System settings file:**
|
||||
- **Location:** `/etc/gemini-cli/settings.json` (Linux),
|
||||
`C:\ProgramData\gemini-cli\settings.json` (Windows) or
|
||||
`/Library/Application Support/GeminiCli/settings.json` (macOS). The path can
|
||||
be overridden using the `GEMINI_CLI_SYSTEM_SETTINGS_PATH` environment
|
||||
variable.
|
||||
- **Scope:** Applies to all Gemini CLI sessions on the system, for all users.
|
||||
System settings override user and project settings. May be useful for system
|
||||
administrators at enterprises to have controls over users' Gemini CLI
|
||||
setups.
|
||||
|
||||
**Note on environment variables in settings:** String values within your
|
||||
`settings.json` files can reference environment variables using either
|
||||
`$VAR_NAME` or `${VAR_NAME}` syntax. These variables will be automatically
|
||||
resolved when the settings are loaded. For example, if you have an environment
|
||||
variable `MY_API_TOKEN`, you could use it in `settings.json` like this:
|
||||
`"apiKey": "$MY_API_TOKEN"`.
|
||||
|
||||
### The `.gemini` directory in your project
|
||||
|
||||
In addition to a project settings file, a project's `.gemini` directory can
|
||||
contain other project-specific files related to Gemini CLI's operation, such as:
|
||||
|
||||
- [Custom sandbox profiles](#sandboxing) (e.g.,
|
||||
`.gemini/sandbox-macos-custom.sb`, `.gemini/sandbox.Dockerfile`).
|
||||
|
||||
### Available settings in `settings.json`:
|
||||
|
||||
- **`contextFileName`** (string or array of strings):
|
||||
- **Description:** Specifies the filename for context files (e.g.,
|
||||
`GEMINI.md`, `AGENTS.md`). Can be a single filename or a list of accepted
|
||||
filenames.
|
||||
- **Default:** `GEMINI.md`
|
||||
- **Example:** `"contextFileName": "AGENTS.md"`
|
||||
|
||||
- **`bugCommand`** (object):
|
||||
- **Description:** Overrides the default URL for the `/bug` command.
|
||||
- **Default:**
|
||||
`"urlTemplate": "https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml&title={title}&info={info}"`
|
||||
- **Properties:**
|
||||
- **`urlTemplate`** (string): A URL that can contain `{title}` and `{info}`
|
||||
placeholders.
|
||||
- **Example:**
|
||||
```json
|
||||
"bugCommand": {
|
||||
"urlTemplate": "https://bug.example.com/new?title={title}&info={info}"
|
||||
}
|
||||
```
|
||||
|
||||
- **`fileFiltering`** (object):
|
||||
- **Description:** Controls git-aware file filtering behavior for @ commands
|
||||
and file discovery tools.
|
||||
- **Default:** `"respectGitIgnore": true, "enableRecursiveFileSearch": true`
|
||||
- **Properties:**
|
||||
- **`respectGitIgnore`** (boolean): Whether to respect .gitignore patterns
|
||||
when discovering files. When set to `true`, git-ignored files (like
|
||||
`node_modules/`, `dist/`, `.env`) are automatically excluded from @
|
||||
commands and file listing operations.
|
||||
- **`enableRecursiveFileSearch`** (boolean): Whether to enable searching
|
||||
recursively for filenames under the current tree when completing @
|
||||
prefixes in the prompt.
|
||||
- **Example:**
|
||||
```json
|
||||
"fileFiltering": {
|
||||
"respectGitIgnore": true,
|
||||
"enableRecursiveFileSearch": false
|
||||
}
|
||||
```
|
||||
|
||||
- **`coreTools`** (array of strings):
|
||||
- **Description:** Allows you to specify a list of core tool names that should
|
||||
be made available to the model. This can be used to restrict the set of
|
||||
built-in tools. See [Built-in Tools](../core/tools-api.md#built-in-tools)
|
||||
for a list of core tools. You can also specify command-specific restrictions
|
||||
for tools that support it, like the `ShellTool`. For example,
|
||||
`"coreTools": ["ShellTool(ls -l)"]` will only allow the `ls -l` command to
|
||||
be executed.
|
||||
- **Default:** All tools available for use by the Gemini model.
|
||||
- **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "ShellTool(ls)"]`.
|
||||
|
||||
- **`excludeTools`** (array of strings):
|
||||
- **Description:** Allows you to specify a list of core tool names that should
|
||||
be excluded from the model. A tool listed in both `excludeTools` and
|
||||
`coreTools` is excluded. You can also specify command-specific restrictions
|
||||
for tools that support it, like the `ShellTool`. For example,
|
||||
`"excludeTools": ["ShellTool(rm -rf)"]` will block the `rm -rf` command.
|
||||
- **Default**: No tools excluded.
|
||||
- **Example:** `"excludeTools": ["run_shell_command", "findFiles"]`.
|
||||
- **Security Note:** Command-specific restrictions in `excludeTools` for
|
||||
`run_shell_command` are based on simple string matching and can be easily
|
||||
bypassed. This feature is **not a security mechanism** and should not be
|
||||
relied upon to safely execute untrusted code. It is recommended to use
|
||||
`coreTools` to explicitly select commands that can be executed.
|
||||
|
||||
- **`allowMCPServers`** (array of strings):
|
||||
- **Description:** Allows you to specify a list of MCP server names that
|
||||
should be made available to the model. This can be used to restrict the set
|
||||
of MCP servers to connect to. Note that this will be ignored if
|
||||
`--allowed-mcp-server-names` is set.
|
||||
- **Default:** All MCP servers are available for use by the Gemini model.
|
||||
- **Example:** `"allowMCPServers": ["myPythonServer"]`.
|
||||
- **Security Note:** This uses simple string matching on MCP server names,
|
||||
which can be modified. If you're a system administrator looking to prevent
|
||||
users from bypassing this, consider configuring the `mcpServers` at the
|
||||
system settings level such that the user will not be able to configure any
|
||||
MCP servers of their own. This should not be used as an airtight security
|
||||
mechanism.
|
||||
|
||||
- **`excludeMCPServers`** (array of strings):
|
||||
- **Description:** Allows you to specify a list of MCP server names that
|
||||
should be excluded from the model. A server listed in both
|
||||
`excludeMCPServers` and `allowMCPServers` is excluded. Note that this will
|
||||
be ignored if `--allowed-mcp-server-names` is set.
|
||||
- **Default**: No MCP servers excluded.
|
||||
- **Example:** `"excludeMCPServers": ["myNodeServer"]`.
|
||||
- **Security Note:** This uses simple string matching on MCP server names,
|
||||
which can be modified. If you're a system administrator looking to prevent
|
||||
users from bypassing this, consider configuring the `mcpServers` at the
|
||||
system settings level such that the user will not be able to configure any
|
||||
MCP servers of their own. This should not be used as an airtight security
|
||||
mechanism.
|
||||
|
||||
- **`autoAccept`** (boolean):
|
||||
- **Description:** Controls whether the CLI automatically accepts and executes
|
||||
tool calls that are considered safe (e.g., read-only operations) without
|
||||
explicit user confirmation. If set to `true`, the CLI will bypass the
|
||||
confirmation prompt for tools deemed safe.
|
||||
- **Default:** `false`
|
||||
- **Example:** `"autoAccept": true`
|
||||
|
||||
- **`theme`** (string):
|
||||
- **Description:** Sets the visual [theme](./themes.md) for Gemini CLI.
|
||||
- **Default:** `"Default"`
|
||||
- **Example:** `"theme": "GitHub"`
|
||||
|
||||
- **`vimMode`** (boolean):
|
||||
- **Description:** Enables or disables vim mode for input editing. When
|
||||
enabled, the input area supports vim-style navigation and editing commands
|
||||
with NORMAL and INSERT modes. The vim mode status is displayed in the footer
|
||||
and persists between sessions.
|
||||
- **Default:** `false`
|
||||
- **Example:** `"vimMode": true`
|
||||
|
||||
- **`sandbox`** (boolean or string):
|
||||
- **Description:** Controls whether and how to use sandboxing for tool
|
||||
execution. If set to `true`, Gemini CLI uses a pre-built
|
||||
`gemini-cli-sandbox` Docker image. For more information, see
|
||||
[Sandboxing](#sandboxing).
|
||||
- **Default:** `false`
|
||||
- **Example:** `"sandbox": "docker"`
|
||||
|
||||
- **`toolDiscoveryCommand`** (string):
|
||||
- **Description:** Defines a custom shell command for discovering tools from
|
||||
your project. The shell command must return on `stdout` a JSON array of
|
||||
[function declarations](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations).
|
||||
Tool wrappers are optional.
|
||||
- **Default:** Empty
|
||||
- **Example:** `"toolDiscoveryCommand": "bin/get_tools"`
|
||||
|
||||
- **`toolCallCommand`** (string):
|
||||
- **Description:** Defines a custom shell command for calling a specific tool
|
||||
that was discovered using `toolDiscoveryCommand`. The shell command must
|
||||
meet the following criteria:
|
||||
- It must take function `name` (exactly as in
|
||||
[function declaration](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations))
|
||||
as first command line argument.
|
||||
- It must read function arguments as JSON on `stdin`, analogous to
|
||||
[`functionCall.args`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functioncall).
|
||||
- It must return function output as JSON on `stdout`, analogous to
|
||||
[`functionResponse.response.content`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functionresponse).
|
||||
- **Default:** Empty
|
||||
- **Example:** `"toolCallCommand": "bin/call_tool"`
|
||||
|
||||
- **`mcpServers`** (object):
|
||||
- **Description:** Configures connections to one or more Model-Context
|
||||
Protocol (MCP) servers for discovering and using custom tools. Gemini CLI
|
||||
attempts to connect to each configured MCP server to discover available
|
||||
tools. If multiple MCP servers expose a tool with the same name, the tool
|
||||
names will be prefixed with the server alias you defined in the
|
||||
configuration (e.g., `serverAlias__actualToolName`) to avoid conflicts. Note
|
||||
that the system might strip certain schema properties from MCP tool
|
||||
definitions for compatibility.
|
||||
- **Default:** Empty
|
||||
- **Properties:**
|
||||
- **`<SERVER_NAME>`** (object): The server parameters for the named server.
|
||||
- `command` (string, required): The command to execute to start the MCP
|
||||
server.
|
||||
- `args` (array of strings, optional): Arguments to pass to the command.
|
||||
- `env` (object, optional): Environment variables to set for the server
|
||||
process.
|
||||
- `cwd` (string, optional): The working directory in which to start the
|
||||
server.
|
||||
- `timeout` (number, optional): Timeout in milliseconds for requests to
|
||||
this MCP server.
|
||||
- `trust` (boolean, optional): Trust this server and bypass all tool call
|
||||
confirmations.
|
||||
- `includeTools` (array of strings, optional): List of tool names to
|
||||
include from this MCP server. When specified, only the tools listed here
|
||||
will be available from this server (whitelist behavior). If not
|
||||
specified, all tools from the server are enabled by default.
|
||||
- `excludeTools` (array of strings, optional): List of tool names to
|
||||
exclude from this MCP server. Tools listed here will not be available to
|
||||
the model, even if they are exposed by the server. **Note:**
|
||||
`excludeTools` takes precedence over `includeTools` - if a tool is in
|
||||
both lists, it will be excluded.
|
||||
- **Example:**
|
||||
```json
|
||||
"mcpServers": {
|
||||
"myPythonServer": {
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--port", "8080"],
|
||||
"cwd": "./mcp_tools/python",
|
||||
"timeout": 5000,
|
||||
"includeTools": ["safe_tool", "file_reader"],
|
||||
},
|
||||
"myNodeServer": {
|
||||
"command": "node",
|
||||
"args": ["mcp_server.js"],
|
||||
"cwd": "./mcp_tools/node",
|
||||
"excludeTools": ["dangerous_tool", "file_deleter"]
|
||||
},
|
||||
"myDockerServer": {
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "-e", "API_KEY", "ghcr.io/foo/bar"],
|
||||
"env": {
|
||||
"API_KEY": "$MY_API_TOKEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`checkpointing`** (object):
|
||||
- **Description:** Configures the checkpointing feature, which allows you to
|
||||
save and restore conversation and file states. See the
|
||||
[Checkpointing documentation](./checkpointing.md) for more details.
|
||||
- **Default:** `{"enabled": false}`
|
||||
- **Properties:**
|
||||
- **`enabled`** (boolean): When `true`, the `/restore` command is available.
|
||||
|
||||
- **`preferredEditor`** (string):
|
||||
- **Description:** Specifies the preferred editor to use for viewing diffs.
|
||||
- **Default:** `vscode`
|
||||
- **Example:** `"preferredEditor": "vscode"`
|
||||
|
||||
- **`telemetry`** (object)
|
||||
- **Description:** Configures logging and metrics collection for Gemini CLI.
|
||||
For more information, see [Telemetry](./telemetry.md).
|
||||
- **Default:**
|
||||
`{"enabled": false, "target": "local", "otlpEndpoint": "http://localhost:4317", "logPrompts": true}`
|
||||
- **Properties:**
|
||||
- **`enabled`** (boolean): Whether or not telemetry is enabled.
|
||||
- **`target`** (string): The destination for collected telemetry. Supported
|
||||
values are `local` and `gcp`.
|
||||
- **`otlpEndpoint`** (string): The endpoint for the OTLP Exporter.
|
||||
- **`logPrompts`** (boolean): Whether or not to include the content of user
|
||||
prompts in the logs.
|
||||
- **Example:**
|
||||
```json
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "local",
|
||||
"otlpEndpoint": "http://localhost:16686",
|
||||
"logPrompts": false
|
||||
}
|
||||
```
|
||||
- **`usageStatisticsEnabled`** (boolean):
|
||||
- **Description:** Enables or disables the collection of usage statistics. See
|
||||
[Usage Statistics](#usage-statistics) for more information.
|
||||
- **Default:** `true`
|
||||
- **Example:**
|
||||
```json
|
||||
"usageStatisticsEnabled": false
|
||||
```
|
||||
|
||||
- **`hideTips`** (boolean):
|
||||
- **Description:** Enables or disables helpful tips in the CLI interface.
|
||||
- **Default:** `false`
|
||||
- **Example:**
|
||||
|
||||
```json
|
||||
"hideTips": true
|
||||
```
|
||||
|
||||
- **`hideBanner`** (boolean):
|
||||
- **Description:** Enables or disables the startup banner (ASCII art logo) in
|
||||
the CLI interface.
|
||||
- **Default:** `false`
|
||||
- **Example:**
|
||||
|
||||
```json
|
||||
"hideBanner": true
|
||||
```
|
||||
|
||||
- **`maxSessionTurns`** (number):
|
||||
- **Description:** Sets the maximum number of turns for a session. If the
|
||||
session exceeds this limit, the CLI will stop processing and start a new
|
||||
chat.
|
||||
- **Default:** `-1` (unlimited)
|
||||
- **Example:**
|
||||
```json
|
||||
"maxSessionTurns": 10
|
||||
```
|
||||
|
||||
- **`summarizeToolOutput`** (object):
|
||||
- **Description:** Enables or disables the summarization of tool output. You
|
||||
can specify the token budget for the summarization using the `tokenBudget`
|
||||
setting.
|
||||
- Note: Currently only the `run_shell_command` tool is supported.
|
||||
- **Default:** `{}` (Disabled by default)
|
||||
- **Example:**
|
||||
```json
|
||||
"summarizeToolOutput": {
|
||||
"run_shell_command": {
|
||||
"tokenBudget": 2000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`excludedProjectEnvVars`** (array of strings):
|
||||
- **Description:** Specifies environment variables that should be excluded
|
||||
from being loaded from project `.env` files. This prevents project-specific
|
||||
environment variables (like `DEBUG=true`) from interfering with gemini-cli
|
||||
behavior. Variables from `.gemini/.env` files are never excluded.
|
||||
- **Default:** `["DEBUG", "DEBUG_MODE"]`
|
||||
- **Example:**
|
||||
```json
|
||||
"excludedProjectEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"]
|
||||
```
|
||||
|
||||
- **`includeDirectories`** (array of strings):
|
||||
- **Description:** Specifies an array of additional absolute or relative paths
|
||||
to include in the workspace context. This allows you to work with files
|
||||
across multiple directories as if they were one. Paths can use `~` to refer
|
||||
to the user's home directory. This setting can be combined with the
|
||||
`--include-directories` command-line flag.
|
||||
- **Default:** `[]`
|
||||
- **Example:**
|
||||
```json
|
||||
"includeDirectories": [
|
||||
"/path/to/another/project",
|
||||
"../shared-library",
|
||||
"~/common-utils"
|
||||
]
|
||||
```
|
||||
|
||||
- **`loadMemoryFromIncludeDirectories`** (boolean):
|
||||
- **Description:** Controls the behavior of the `/memory refresh` command. If
|
||||
set to `true`, `GEMINI.md` files should be loaded from all directories that
|
||||
are added. If set to `false`, `GEMINI.md` should only be loaded from the
|
||||
current directory.
|
||||
- **Default:** `false`
|
||||
- **Example:**
|
||||
```json
|
||||
"loadMemoryFromIncludeDirectories": true
|
||||
```
|
||||
|
||||
### Example `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "GitHub",
|
||||
"sandbox": "docker",
|
||||
"toolDiscoveryCommand": "bin/get_tools",
|
||||
"toolCallCommand": "bin/call_tool",
|
||||
"mcpServers": {
|
||||
"mainServer": {
|
||||
"command": "bin/mcp_server.py"
|
||||
},
|
||||
"anotherServer": {
|
||||
"command": "node",
|
||||
"args": ["mcp_server.js", "--verbose"]
|
||||
}
|
||||
},
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "local",
|
||||
"otlpEndpoint": "http://localhost:4317",
|
||||
"logPrompts": true
|
||||
},
|
||||
"usageStatisticsEnabled": true,
|
||||
"hideTips": false,
|
||||
"hideBanner": false,
|
||||
"maxSessionTurns": 10,
|
||||
"summarizeToolOutput": {
|
||||
"run_shell_command": {
|
||||
"tokenBudget": 100
|
||||
}
|
||||
},
|
||||
"excludedProjectEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"],
|
||||
"includeDirectories": ["path/to/dir1", "~/path/to/dir2", "../path/to/dir3"],
|
||||
"loadMemoryFromIncludeDirectories": true
|
||||
}
|
||||
```
|
||||
|
||||
## Shell History
|
||||
|
||||
The CLI keeps a history of shell commands you run. To avoid conflicts between
|
||||
different projects, this history is stored in a project-specific directory
|
||||
within your user's home folder.
|
||||
|
||||
- **Location:** `~/.gemini/tmp/<project_hash>/shell_history`
|
||||
- `<project_hash>` is a unique identifier generated from your project's root
|
||||
path.
|
||||
- The history is stored in a file named `shell_history`.
|
||||
|
||||
## Environment Variables & `.env` Files
|
||||
|
||||
Environment variables are a common way to configure applications, especially for
|
||||
sensitive information like API keys or for settings that might change between
|
||||
environments.
|
||||
|
||||
The CLI automatically loads environment variables from an `.env` file. The
|
||||
loading order is:
|
||||
|
||||
1. `.env` file in the current working directory.
|
||||
2. If not found, it searches upwards in parent directories until it finds an
|
||||
`.env` file or reaches the project root (identified by a `.git` folder) or
|
||||
the home directory.
|
||||
3. If still not found, it looks for `~/.env` (in the user's home directory).
|
||||
|
||||
**Environment Variable Exclusion:** Some environment variables (like `DEBUG` and
|
||||
`DEBUG_MODE`) are automatically excluded from being loaded from project `.env`
|
||||
files to prevent interference with gemini-cli behavior. Variables from
|
||||
`.gemini/.env` files are never excluded. You can customize this behavior using
|
||||
the `excludedProjectEnvVars` setting in your `settings.json` file.
|
||||
|
||||
- **`GEMINI_API_KEY`** (Required):
|
||||
- Your API key for the Gemini API.
|
||||
- **Crucial for operation.** The CLI will not function without it.
|
||||
- Set this in your shell profile (e.g., `~/.bashrc`, `~/.zshrc`) or an `.env`
|
||||
file.
|
||||
- **`GEMINI_MODEL`**:
|
||||
- Specifies the default Gemini model to use.
|
||||
- Overrides the hardcoded default
|
||||
- Example: `export GEMINI_MODEL="gemini-2.5-flash"`
|
||||
- **`GOOGLE_API_KEY`**:
|
||||
- Your Google Cloud API key.
|
||||
- Required for using Vertex AI in express mode.
|
||||
- Ensure you have the necessary permissions.
|
||||
- Example: `export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"`.
|
||||
- **`GOOGLE_CLOUD_PROJECT`**:
|
||||
- Your Google Cloud Project ID.
|
||||
- Required for using Code Assist or Vertex AI.
|
||||
- If using Vertex AI, ensure you have the necessary permissions in this
|
||||
project.
|
||||
- **Cloud Shell Note:** When running in a Cloud Shell environment, this
|
||||
variable defaults to a special project allocated for Cloud Shell users. If
|
||||
you have `GOOGLE_CLOUD_PROJECT` set in your global environment in Cloud
|
||||
Shell, it will be overridden by this default. To use a different project in
|
||||
Cloud Shell, you must define `GOOGLE_CLOUD_PROJECT` in a `.env` file.
|
||||
- Example: `export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
|
||||
- **`GOOGLE_APPLICATION_CREDENTIALS`** (string):
|
||||
- **Description:** The path to your Google Application Credentials JSON file.
|
||||
- **Example:**
|
||||
`export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/credentials.json"`
|
||||
- **`OTLP_GOOGLE_CLOUD_PROJECT`**:
|
||||
- Your Google Cloud Project ID for Telemetry in Google Cloud
|
||||
- Example: `export OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
|
||||
- **`GOOGLE_CLOUD_LOCATION`**:
|
||||
- Your Google Cloud Project Location (e.g., us-central1).
|
||||
- Required for using Vertex AI in non express mode.
|
||||
- Example: `export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"`.
|
||||
- **`GEMINI_SANDBOX`**:
|
||||
- Alternative to the `sandbox` setting in `settings.json`.
|
||||
- Accepts `true`, `false`, `docker`, `podman`, or a custom command string.
|
||||
- **`SEATBELT_PROFILE`** (macOS specific):
|
||||
- Switches the Seatbelt (`sandbox-exec`) profile on macOS.
|
||||
- `permissive-open`: (Default) Restricts writes to the project folder (and a
|
||||
few other folders, see
|
||||
`packages/cli/src/utils/sandbox-macos-permissive-open.sb`) but allows other
|
||||
operations.
|
||||
- `strict`: Uses a strict profile that declines operations by default.
|
||||
- `<profile_name>`: Uses a custom profile. To define a custom profile, create
|
||||
a file named `sandbox-macos-<profile_name>.sb` in your project's `.gemini/`
|
||||
directory (e.g., `my-project/.gemini/sandbox-macos-custom.sb`).
|
||||
- **`DEBUG` or `DEBUG_MODE`** (often used by underlying libraries or the CLI
|
||||
itself):
|
||||
- Set to `true` or `1` to enable verbose debug logging, which can be helpful
|
||||
for troubleshooting.
|
||||
- **Note:** These variables are automatically excluded from project `.env`
|
||||
files by default to prevent interference with gemini-cli behavior. Use
|
||||
`.gemini/.env` files if you need to set these for gemini-cli specifically.
|
||||
- **`NO_COLOR`**:
|
||||
- Set to any value to disable all color output in the CLI.
|
||||
- **`CLI_TITLE`**:
|
||||
- Set to a string to customize the title of the CLI.
|
||||
- **`CODE_ASSIST_ENDPOINT`**:
|
||||
- Specifies the endpoint for the code assist server.
|
||||
- This is useful for development and testing.
|
||||
- **`GEMINI_SYSTEM_MD`**:
|
||||
- Overrides the base system prompt with the contents of a Markdown file.
|
||||
- If set to `1` or `true`, it uses the file at `.gemini/system.md`.
|
||||
- If set to a file path, it uses that file. The path can be absolute or
|
||||
relative. `~` is supported for the home directory.
|
||||
- The specified file must exist.
|
||||
- **`GEMINI_WRITE_SYSTEM_MD`**:
|
||||
- Writes the default system prompt to a file. This is useful for getting a
|
||||
template to customize.
|
||||
- If set to `1` or `true`, it writes to `.gemini/system.md`.
|
||||
- If set to a file path, it writes to that path. The path can be absolute or
|
||||
relative. `~` is supported for the home directory. **Note: This will
|
||||
overwrite the file if it already exists.**
|
||||
|
||||
## Command-Line Arguments
|
||||
|
||||
Arguments passed directly when running the CLI can override other configurations
|
||||
for that specific session.
|
||||
|
||||
- **`--model <model_name>`** (**`-m <model_name>`**):
|
||||
- Specifies the Gemini model to use for this session.
|
||||
- Example: `npm start -- --model gemini-1.5-pro-latest`
|
||||
- **`--prompt <your_prompt>`** (**`-p <your_prompt>`**):
|
||||
- Used to pass a prompt directly to the command. This invokes Gemini CLI in a
|
||||
non-interactive mode.
|
||||
- **`--prompt-interactive <your_prompt>`** (**`-i <your_prompt>`**):
|
||||
- Starts an interactive session with the provided prompt as the initial input.
|
||||
- The prompt is processed within the interactive session, not before it.
|
||||
- Cannot be used when piping input from stdin.
|
||||
- Example: `gemini -i "explain this code"`
|
||||
- **`--sandbox`** (**`-s`**):
|
||||
- Enables sandbox mode for this session.
|
||||
- **`--sandbox-image`**:
|
||||
- Sets the sandbox image URI.
|
||||
- **`--debug`** (**`-d`**):
|
||||
- Enables debug mode for this session, providing more verbose output.
|
||||
- **`--all-files`** (**`-a`**):
|
||||
- If set, recursively includes all files within the current directory as
|
||||
context for the prompt.
|
||||
- **`--help`** (or **`-h`**):
|
||||
- Displays help information about command-line arguments.
|
||||
- **`--show-memory-usage`**:
|
||||
- Displays the current memory usage.
|
||||
- **`--yolo`**:
|
||||
- Enables YOLO mode, which automatically approves all tool calls.
|
||||
- **`--telemetry`**:
|
||||
- Enables [telemetry](./telemetry.md).
|
||||
- **`--telemetry-target`**:
|
||||
- Sets the telemetry target. See [telemetry](./telemetry.md) for more
|
||||
information.
|
||||
- **`--telemetry-otlp-endpoint`**:
|
||||
- Sets the OTLP endpoint for telemetry. See [telemetry](./telemetry.md) for
|
||||
more information.
|
||||
- **`--telemetry-log-prompts`**:
|
||||
- Enables logging of prompts for telemetry. See [telemetry](./telemetry.md)
|
||||
for more information.
|
||||
- **`--checkpointing`**:
|
||||
- Enables [checkpointing](./checkpointing.md).
|
||||
- **`--extensions <extension_name ...>`** (**`-e <extension_name ...>`**):
|
||||
- Specifies a list of extensions to use for the session. If not provided, all
|
||||
available extensions are used.
|
||||
- Use the special term `gemini -e none` to disable all extensions.
|
||||
- Example: `gemini -e my-extension -e my-other-extension`
|
||||
- **`--list-extensions`** (**`-l`**):
|
||||
- Lists all available extensions and exits.
|
||||
- **`--proxy`**:
|
||||
- Sets the proxy for the CLI.
|
||||
- Example: `--proxy http://localhost:7890`.
|
||||
- **`--include-directories <dir1,dir2,...>`**:
|
||||
- Includes additional directories in the workspace for multi-directory
|
||||
support.
|
||||
- Can be specified multiple times or as comma-separated values.
|
||||
- 5 directories can be added at maximum.
|
||||
- Example: `--include-directories /path/to/project1,/path/to/project2` or
|
||||
`--include-directories /path/to/project1 --include-directories /path/to/project2`
|
||||
- **`--version`**:
|
||||
- Displays the version of the CLI.
|
||||
|
||||
## Context Files (Hierarchical Instructional Context)
|
||||
|
||||
While not strictly configuration for the CLI's _behavior_, context files
|
||||
(defaulting to `GEMINI.md` but configurable via the `contextFileName` setting)
|
||||
are crucial for configuring the _instructional context_ (also referred to as
|
||||
"memory") provided to the Gemini model. This powerful feature allows you to give
|
||||
project-specific instructions, coding style guides, or any relevant background
|
||||
information to the AI, making its responses more tailored and accurate to your
|
||||
needs. The CLI includes UI elements, such as an indicator in the footer showing
|
||||
the number of loaded context files, to keep you informed about the active
|
||||
context.
|
||||
|
||||
- **Purpose:** These Markdown files contain instructions, guidelines, or context
|
||||
that you want the Gemini model to be aware of during your interactions. The
|
||||
system is designed to manage this instructional context hierarchically.
|
||||
|
||||
### Example Context File Content (e.g., `GEMINI.md`)
|
||||
|
||||
Here's a conceptual example of what a context file at the root of a TypeScript
|
||||
project might contain:
|
||||
|
||||
```markdown
|
||||
# Project: My Awesome TypeScript Library
|
||||
|
||||
## General Instructions:
|
||||
|
||||
- When generating new TypeScript code, please follow the existing coding style.
|
||||
- Ensure all new functions and classes have JSDoc comments.
|
||||
- Prefer functional programming paradigms where appropriate.
|
||||
- All code should be compatible with TypeScript 5.0 and Node.js 20+.
|
||||
|
||||
## Coding Style:
|
||||
|
||||
- Use 2 spaces for indentation.
|
||||
- Interface names should be prefixed with `I` (e.g., `IUserService`).
|
||||
- Private class members should be prefixed with an underscore (`_`).
|
||||
- Always use strict equality (`===` and `!==`).
|
||||
|
||||
## Specific Component: `src/api/client.ts`
|
||||
|
||||
- This file handles all outbound API requests.
|
||||
- When adding new API call functions, ensure they include robust error handling
|
||||
and logging.
|
||||
- Use the existing `fetchWithRetry` utility for all GET requests.
|
||||
|
||||
## Regarding Dependencies:
|
||||
|
||||
- Avoid introducing new external dependencies unless absolutely necessary.
|
||||
- If a new dependency is required, please state the reason.
|
||||
```
|
||||
|
||||
This example demonstrates how you can provide general project context, specific
|
||||
coding conventions, and even notes about particular files or components. The
|
||||
more relevant and precise your context files are, the better the AI can assist
|
||||
you. Project-specific context files are highly encouraged to establish
|
||||
conventions and context.
|
||||
|
||||
- **Hierarchical Loading and Precedence:** The CLI implements a sophisticated
|
||||
hierarchical memory system by loading context files (e.g., `GEMINI.md`) from
|
||||
several locations. Content from files lower in this list (more specific)
|
||||
typically overrides or supplements content from files higher up (more
|
||||
general). The exact concatenation order and final context can be inspected
|
||||
using the `/memory show` command. The typical loading order is:
|
||||
1. **Global Context File:**
|
||||
- Location: `~/.gemini/<contextFileName>` (e.g., `~/.gemini/GEMINI.md` in
|
||||
your user home directory).
|
||||
- Scope: Provides default instructions for all your projects.
|
||||
2. **Project Root & Ancestors Context Files:**
|
||||
- Location: The CLI searches for the configured context file in the
|
||||
current working directory and then in each parent directory up to either
|
||||
the project root (identified by a `.git` folder) or your home directory.
|
||||
- Scope: Provides context relevant to the entire project or a significant
|
||||
portion of it.
|
||||
3. **Sub-directory Context Files (Contextual/Local):**
|
||||
- Location: The CLI also scans for the configured context file in
|
||||
subdirectories _below_ the current working directory (respecting common
|
||||
ignore patterns like `node_modules`, `.git`, etc.). The breadth of this
|
||||
search is limited to 200 directories by default, but can be configured
|
||||
with a `memoryDiscoveryMaxDirs` field in your `settings.json` file.
|
||||
- Scope: Allows for highly specific instructions relevant to a particular
|
||||
component, module, or subsection of your project.
|
||||
- **Concatenation & UI Indication:** The contents of all found context files are
|
||||
concatenated (with separators indicating their origin and path) and provided
|
||||
as part of the system prompt to the Gemini model. The CLI footer displays the
|
||||
count of loaded context files, giving you a quick visual cue about the active
|
||||
instructional context.
|
||||
- **Importing Content:** You can modularize your context files by importing
|
||||
other Markdown files using the `@path/to/file.md` syntax. For more details,
|
||||
see the [Memory Import Processor documentation](../core/memport.md).
|
||||
- **Commands for Memory Management:**
|
||||
- Use `/memory refresh` to force a re-scan and reload of all context files
|
||||
from all configured locations. This updates the AI's instructional context.
|
||||
- Use `/memory show` to display the combined instructional context currently
|
||||
loaded, allowing you to verify the hierarchy and content being used by the
|
||||
AI.
|
||||
- See the [Commands documentation](./commands.md#memory) for full details on
|
||||
the `/memory` command and its sub-commands (`show` and `refresh`).
|
||||
|
||||
By understanding and utilizing these configuration layers and the hierarchical
|
||||
nature of context files, you can effectively manage the AI's memory and tailor
|
||||
the Gemini CLI's responses to your specific needs and projects.
|
||||
|
||||
## Sandboxing
|
||||
|
||||
The Gemini CLI can execute potentially unsafe operations (like shell commands
|
||||
and file modifications) within a sandboxed environment to protect your system.
|
||||
|
||||
Sandboxing is disabled by default, but you can enable it in a few ways:
|
||||
|
||||
- Using `--sandbox` or `-s` flag.
|
||||
- Setting `GEMINI_SANDBOX` environment variable.
|
||||
- Sandbox is enabled in `--yolo` mode by default.
|
||||
|
||||
By default, it uses a pre-built `gemini-cli-sandbox` Docker image.
|
||||
|
||||
For project-specific sandboxing needs, you can create a custom Dockerfile at
|
||||
`.gemini/sandbox.Dockerfile` in your project's root directory. This Dockerfile
|
||||
can be based on the base sandbox image:
|
||||
|
||||
```dockerfile
|
||||
FROM gemini-cli-sandbox
|
||||
|
||||
# Add your custom dependencies or configurations here
|
||||
# For example:
|
||||
# RUN apt-get update && apt-get install -y some-package
|
||||
# COPY ./my-config /app/my-config
|
||||
```
|
||||
|
||||
When `.gemini/sandbox.Dockerfile` exists, you can use `BUILD_SANDBOX`
|
||||
environment variable when running Gemini CLI to automatically build the custom
|
||||
sandbox image:
|
||||
|
||||
```bash
|
||||
BUILD_SANDBOX=1 gemini -s
|
||||
```
|
||||
|
||||
## Usage Statistics
|
||||
|
||||
To help us improve the Gemini CLI, we collect anonymized usage statistics. This
|
||||
data helps us understand how the CLI is used, identify common issues, and
|
||||
prioritize new features.
|
||||
|
||||
**What we collect:**
|
||||
|
||||
- **Tool Calls:** We log the names of the tools that are called, whether they
|
||||
succeed or fail, and how long they take to execute. We do not collect the
|
||||
arguments passed to the tools or any data returned by them.
|
||||
- **API Requests:** We log the Gemini model used for each request, the duration
|
||||
of the request, and whether it was successful. We do not collect the content
|
||||
of the prompts or responses.
|
||||
- **Session Information:** We collect information about the configuration of the
|
||||
CLI, such as the enabled tools and the approval mode.
|
||||
|
||||
**What we DON'T collect:**
|
||||
|
||||
- **Personally Identifiable Information (PII):** We do not collect any personal
|
||||
information, such as your name, email address, or API keys.
|
||||
- **Prompt and Response Content:** We do not log the content of your prompts or
|
||||
the responses from the Gemini model.
|
||||
- **File Content:** We do not log the content of any files that are read or
|
||||
written by the CLI.
|
||||
|
||||
**How to opt out:**
|
||||
|
||||
You can opt out of usage statistics collection at any time by setting the
|
||||
`usageStatisticsEnabled` property to `false` in your `settings.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"usageStatisticsEnabled": false
|
||||
}
|
||||
```
|
||||
@@ -202,26 +202,6 @@ allowlisting with `coreTools`, as it relies on blocking known-bad commands, and
|
||||
clever users may find ways to bypass simple string-based blocks. **Allowlisting
|
||||
is the recommended approach.**
|
||||
|
||||
### Disabling YOLO Mode
|
||||
|
||||
To ensure that users cannot bypass the confirmation prompt for tool execution,
|
||||
you can disable YOLO mode at the policy level. This adds a critical layer of
|
||||
safety, as it prevents the model from executing tools without explicit user
|
||||
approval.
|
||||
|
||||
**Example:** Force all tool executions to require user confirmation.
|
||||
|
||||
```json
|
||||
{
|
||||
"security": {
|
||||
"disableYoloMode": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This setting is highly recommended in an enterprise environment to prevent
|
||||
unintended tool execution.
|
||||
|
||||
## Managing Custom Tools (MCP Servers)
|
||||
|
||||
If your organization uses custom tools via
|
||||
|
||||
+1
-63
@@ -15,13 +15,6 @@ scripting, automation, CI/CD pipelines, and building AI-powered tools.
|
||||
- [JSON Output](#json-output)
|
||||
- [Response Schema](#response-schema)
|
||||
- [Example Usage](#example-usage)
|
||||
- [Streaming JSON Output](#streaming-json-output)
|
||||
- [When to Use Streaming JSON](#when-to-use-streaming-json)
|
||||
- [Event Types](#event-types)
|
||||
- [Basic Usage](#basic-usage)
|
||||
- [Example Output](#example-output)
|
||||
- [Processing Stream Events](#processing-stream-events)
|
||||
- [Real-World Examples](#real-world-examples)
|
||||
- [File Redirection](#file-redirection)
|
||||
- [Configuration Options](#configuration-options)
|
||||
- [Examples](#examples)
|
||||
@@ -218,62 +211,6 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming JSON Output
|
||||
|
||||
Returns real-time events as newline-delimited JSON (JSONL). Each significant
|
||||
action (initialization, messages, tool calls, results) emits immediately as it
|
||||
occurs. This format is ideal for monitoring long-running operations, building
|
||||
UIs with live progress, and creating automation pipelines that react to events.
|
||||
|
||||
#### When to Use Streaming JSON
|
||||
|
||||
Use `--output-format stream-json` when you need:
|
||||
|
||||
- **Real-time progress monitoring** - See tool calls and responses as they
|
||||
happen
|
||||
- **Event-driven automation** - React to specific events (e.g., tool failures)
|
||||
- **Live UI updates** - Build interfaces showing AI agent activity in real-time
|
||||
- **Detailed execution logs** - Capture complete interaction history with
|
||||
timestamps
|
||||
- **Pipeline integration** - Stream events to logging/monitoring systems
|
||||
|
||||
#### Event Types
|
||||
|
||||
The streaming format emits 6 event types:
|
||||
|
||||
1. **`init`** - Session starts (includes session_id, model)
|
||||
2. **`message`** - User prompts and assistant responses
|
||||
3. **`tool_use`** - Tool call requests with parameters
|
||||
4. **`tool_result`** - Tool execution results (success/error)
|
||||
5. **`error`** - Non-fatal errors and warnings
|
||||
6. **`result`** - Final session outcome with aggregated stats
|
||||
|
||||
#### Basic Usage
|
||||
|
||||
```bash
|
||||
# Stream events to console
|
||||
gemini --output-format stream-json --prompt "What is 2+2?"
|
||||
|
||||
# Save event stream to file
|
||||
gemini --output-format stream-json --prompt "Analyze this code" > events.jsonl
|
||||
|
||||
# Parse with jq
|
||||
gemini --output-format stream-json --prompt "List files" | jq -r '.type'
|
||||
```
|
||||
|
||||
#### Example Output
|
||||
|
||||
Each line is a complete JSON event:
|
||||
|
||||
```jsonl
|
||||
{"type":"init","timestamp":"2025-10-10T12:00:00.000Z","session_id":"abc123","model":"gemini-2.0-flash-exp"}
|
||||
{"type":"message","role":"user","content":"List files in current directory","timestamp":"2025-10-10T12:00:01.000Z"}
|
||||
{"type":"tool_use","tool_name":"Bash","tool_id":"bash-123","parameters":{"command":"ls -la"},"timestamp":"2025-10-10T12:00:02.000Z"}
|
||||
{"type":"tool_result","tool_id":"bash-123","status":"success","output":"file1.txt\nfile2.txt","timestamp":"2025-10-10T12:00:03.000Z"}
|
||||
{"type":"message","role":"assistant","content":"Here are the files...","delta":true,"timestamp":"2025-10-10T12:00:04.000Z"}
|
||||
{"type":"result","status":"success","stats":{"total_tokens":250,"input_tokens":50,"output_tokens":200,"duration_ms":3000,"tool_calls":1},"timestamp":"2025-10-10T12:00:05.000Z"}
|
||||
```
|
||||
|
||||
### File Redirection
|
||||
|
||||
Save output to files or pipe to other commands:
|
||||
@@ -302,6 +239,7 @@ Key command-line options for headless usage:
|
||||
| `--output-format` | Specify output format (text, json) | `gemini -p "query" --output-format json` |
|
||||
| `--model`, `-m` | Specify the Gemini model | `gemini -p "query" -m gemini-2.5-flash` |
|
||||
| `--debug`, `-d` | Enable debug mode | `gemini -p "query" --debug` |
|
||||
| `--all-files`, `-a` | Include all files in context | `gemini -p "query" --all-files` |
|
||||
| `--include-directories` | Include additional directories | `gemini -p "query" --include-directories src,docs` |
|
||||
| `--yolo`, `-y` | Auto-approve all actions | `gemini -p "query" --yolo` |
|
||||
| `--approval-mode` | Set approval mode | `gemini -p "query" --approval-mode auto_edit` |
|
||||
|
||||
@@ -10,10 +10,10 @@ This document lists the available keyboard shortcuts in the Gemini CLI.
|
||||
| `Ctrl+C` | Cancel the ongoing request and clear the input. Press twice to exit the application. |
|
||||
| `Ctrl+D` | Exit the application if the input is empty. Press twice to confirm. |
|
||||
| `Ctrl+L` | Clear the screen. |
|
||||
| `Ctrl+O` | Toggle the display of the debug console. |
|
||||
| `Ctrl+S` | Allows long responses to print fully, disabling truncation. Use your terminal's scrollback to view the entire output. |
|
||||
| `Ctrl+T` | Toggle the display of the todo list. |
|
||||
| `Ctrl+T` | Toggle the display of tool descriptions. |
|
||||
| `Ctrl+Y` | Toggle auto-approval (YOLO mode) for all tool calls. |
|
||||
| `F12` | Toggle the display of the debug console. |
|
||||
|
||||
## Input Prompt
|
||||
|
||||
@@ -67,12 +67,3 @@ This document lists the available keyboard shortcuts in the Gemini CLI.
|
||||
| Shortcut | Description |
|
||||
| -------- | --------------------------------- |
|
||||
| `Ctrl+G` | See context CLI received from IDE |
|
||||
|
||||
## Meta+key combos on mac
|
||||
|
||||
On Mac, all Meta+char combos should work normally except for these three which
|
||||
are mapped to special functionality.
|
||||
|
||||
- `meta+b`: "∫" back one word
|
||||
- `meta+f`: "ƒ" forward one word
|
||||
- `meta+m`: "µ" toggle markup view
|
||||
|
||||
+102
-427
@@ -15,29 +15,8 @@ Learn how to enable and setup OpenTelemetry for Gemini CLI.
|
||||
- [Collector-Based Export (Advanced)](#collector-based-export-advanced-1)
|
||||
- [Logs and Metrics](#logs-and-metrics)
|
||||
- [Logs](#logs)
|
||||
- [Sessions](#sessions)
|
||||
- [Tools](#tools)
|
||||
- [Files](#files)
|
||||
- [API](#api)
|
||||
- [Model Routing](#model-routing)
|
||||
- [Chat and Streaming](#chat-and-streaming)
|
||||
- [Resilience](#resilience)
|
||||
- [Extensions](#extensions)
|
||||
- [Agent Runs](#agent-runs)
|
||||
- [IDE](#ide)
|
||||
- [UI](#ui)
|
||||
- [Metrics](#metrics)
|
||||
- [Custom](#custom)
|
||||
- [Sessions](#sessions-1)
|
||||
- [Tools](#tools-1)
|
||||
- [API](#api-1)
|
||||
- [Token Usage](#token-usage)
|
||||
- [Files](#files-1)
|
||||
- [Chat and Streaming](#chat-and-streaming-1)
|
||||
- [Model Routing](#model-routing-1)
|
||||
- [Agent Runs](#agent-runs-1)
|
||||
- [UI](#ui-1)
|
||||
- [Performance](#performance)
|
||||
- [GenAI Semantic Convention](#genai-semantic-convention)
|
||||
|
||||
## Key Benefits
|
||||
@@ -231,13 +210,10 @@ attributes on all logs and metrics.
|
||||
### Logs
|
||||
|
||||
Logs are timestamped records of specific events. The following events are logged
|
||||
for Gemini CLI, grouped by category.
|
||||
for Gemini CLI:
|
||||
|
||||
#### Sessions
|
||||
|
||||
Captures startup configuration and user prompt submissions.
|
||||
|
||||
- `gemini_cli.config`: Emitted once at startup with the CLI configuration.
|
||||
- `gemini_cli.config`: This event occurs once at startup with the CLI's
|
||||
configuration.
|
||||
- **Attributes**:
|
||||
- `model` (string)
|
||||
- `embedding_model` (string)
|
||||
@@ -246,42 +222,81 @@ Captures startup configuration and user prompt submissions.
|
||||
- `approval_mode` (string)
|
||||
- `api_key_enabled` (boolean)
|
||||
- `vertex_ai_enabled` (boolean)
|
||||
- `log_user_prompts_enabled` (boolean)
|
||||
- `code_assist_enabled` (boolean)
|
||||
- `log_prompts_enabled` (boolean)
|
||||
- `file_filtering_respect_git_ignore` (boolean)
|
||||
- `debug_mode` (boolean)
|
||||
- `mcp_servers` (string)
|
||||
- `mcp_servers_count` (int)
|
||||
- `mcp_tools` (string, if applicable)
|
||||
- `mcp_tools_count` (int, if applicable)
|
||||
- `output_format` ("text", "json", or "stream-json")
|
||||
- `output_format` (string: "text" or "json")
|
||||
|
||||
- `gemini_cli.user_prompt`: Emitted when a user submits a prompt.
|
||||
- `gemini_cli.user_prompt`: This event occurs when a user submits a prompt.
|
||||
- **Attributes**:
|
||||
- `prompt_length` (int)
|
||||
- `prompt_id` (string)
|
||||
- `prompt` (string; excluded if `telemetry.logPrompts` is `false`)
|
||||
- `prompt` (string, this attribute is excluded if `log_prompts_enabled` is
|
||||
configured to be `false`)
|
||||
- `auth_type` (string)
|
||||
|
||||
#### Tools
|
||||
|
||||
Captures tool executions, output truncation, and Smart Edit behavior.
|
||||
|
||||
- `gemini_cli.tool_call`: Emitted for each tool (function) call.
|
||||
- `gemini_cli.tool_call`: This event occurs for each function call.
|
||||
- **Attributes**:
|
||||
- `function_name`
|
||||
- `function_args`
|
||||
- `duration_ms`
|
||||
- `success` (boolean)
|
||||
- `decision` ("accept", "reject", "auto_accept", or "modify", if applicable)
|
||||
- `decision` (string: "accept", "reject", "auto_accept", or "modify", if
|
||||
applicable)
|
||||
- `error` (if applicable)
|
||||
- `error_type` (if applicable)
|
||||
- `prompt_id` (string)
|
||||
- `tool_type` ("native" or "mcp")
|
||||
- `mcp_server_name` (string, if applicable)
|
||||
- `content_length` (int, if applicable)
|
||||
- `metadata` (if applicable)
|
||||
- `metadata` (if applicable, dictionary of string -> any)
|
||||
|
||||
- `gemini_cli.tool_output_truncated`: Output of a tool call was truncated.
|
||||
- `gemini_cli.file_operation`: This event occurs for each file operation.
|
||||
- **Attributes**:
|
||||
- `tool_name` (string)
|
||||
- `operation` (string: "create", "read", "update")
|
||||
- `lines` (int, if applicable)
|
||||
- `mimetype` (string, if applicable)
|
||||
- `extension` (string, if applicable)
|
||||
- `programming_language` (string, if applicable)
|
||||
- `diff_stat` (json string, if applicable): A JSON string with the following
|
||||
members:
|
||||
- `ai_added_lines` (int)
|
||||
- `ai_removed_lines` (int)
|
||||
- `user_added_lines` (int)
|
||||
- `user_removed_lines` (int)
|
||||
|
||||
- `gemini_cli.api_request`: This event occurs when making a request to Gemini
|
||||
API.
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `request_text` (if applicable)
|
||||
|
||||
- `gemini_cli.api_error`: This event occurs if the API request fails.
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `error`
|
||||
- `error_type`
|
||||
- `status_code`
|
||||
- `duration_ms`
|
||||
- `auth_type`
|
||||
|
||||
- `gemini_cli.api_response`: This event occurs upon receiving a response from
|
||||
Gemini API.
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `status_code`
|
||||
- `duration_ms`
|
||||
- `error` (optional)
|
||||
- `input_token_count`
|
||||
- `output_token_count`
|
||||
- `cached_content_token_count`
|
||||
- `thoughts_token_count`
|
||||
- `tool_token_count`
|
||||
- `response_text` (if applicable)
|
||||
- `auth_type`
|
||||
|
||||
- `gemini_cli.tool_output_truncated`: This event occurs when the output of a
|
||||
tool call is too large and gets truncated.
|
||||
- **Attributes**:
|
||||
- `tool_name` (string)
|
||||
- `original_content_length` (int)
|
||||
@@ -290,231 +305,32 @@ Captures tool executions, output truncation, and Smart Edit behavior.
|
||||
- `lines` (int)
|
||||
- `prompt_id` (string)
|
||||
|
||||
- `gemini_cli.smart_edit_strategy`: Smart Edit strategy chosen.
|
||||
- `gemini_cli.malformed_json_response`: This event occurs when a `generateJson`
|
||||
response from Gemini API cannot be parsed as a json.
|
||||
- **Attributes**:
|
||||
- `strategy` (string)
|
||||
- `model`
|
||||
|
||||
- `gemini_cli.smart_edit_correction`: Smart Edit correction result.
|
||||
- `gemini_cli.flash_fallback`: This event occurs when Gemini CLI switches to
|
||||
flash as fallback.
|
||||
- **Attributes**:
|
||||
- `correction` ("success" | "failure")
|
||||
- `auth_type`
|
||||
|
||||
- `gen_ai.client.inference.operation.details`: This event provides detailed
|
||||
information about the GenAI operation, aligned with [OpenTelemetry GenAI
|
||||
semantic conventions for events].
|
||||
- **Attributes**:
|
||||
- `gen_ai.request.model` (string)
|
||||
- `gen_ai.provider.name` (string)
|
||||
- `gen_ai.operation.name` (string)
|
||||
- `gen_ai.input.messages` (json string)
|
||||
- `gen_ai.output.messages` (json string)
|
||||
- `gen_ai.response.finish_reasons` (array of strings)
|
||||
- `gen_ai.usage.input_tokens` (int)
|
||||
- `gen_ai.usage.output_tokens` (int)
|
||||
- `gen_ai.request.temperature` (float)
|
||||
- `gen_ai.request.top_p` (float)
|
||||
- `gen_ai.request.top_k` (int)
|
||||
- `gen_ai.request.max_tokens` (int)
|
||||
- `gen_ai.system_instructions` (json string)
|
||||
- `server.address` (string)
|
||||
- `server.port` (int)
|
||||
|
||||
#### Files
|
||||
|
||||
Tracks file operations performed by tools.
|
||||
|
||||
- `gemini_cli.file_operation`: Emitted for each file operation.
|
||||
- **Attributes**:
|
||||
- `tool_name` (string)
|
||||
- `operation` ("create" | "read" | "update")
|
||||
- `lines` (int, optional)
|
||||
- `mimetype` (string, optional)
|
||||
- `extension` (string, optional)
|
||||
- `programming_language` (string, optional)
|
||||
|
||||
#### API
|
||||
|
||||
Captures Gemini API requests, responses, and errors.
|
||||
|
||||
- `gemini_cli.api_request`: Request sent to Gemini API.
|
||||
- **Attributes**:
|
||||
- `model` (string)
|
||||
- `prompt_id` (string)
|
||||
- `request_text` (string, optional)
|
||||
|
||||
- `gemini_cli.api_response`: Response received from Gemini API.
|
||||
- **Attributes**:
|
||||
- `model` (string)
|
||||
- `status_code` (int|string)
|
||||
- `duration_ms` (int)
|
||||
- `input_token_count` (int)
|
||||
- `output_token_count` (int)
|
||||
- `cached_content_token_count` (int)
|
||||
- `thoughts_token_count` (int)
|
||||
- `tool_token_count` (int)
|
||||
- `total_token_count` (int)
|
||||
- `response_text` (string, optional)
|
||||
- `prompt_id` (string)
|
||||
- `auth_type` (string)
|
||||
|
||||
- `gemini_cli.api_error`: API request failed.
|
||||
- **Attributes**:
|
||||
- `model` (string)
|
||||
- `error` (string)
|
||||
- `error_type` (string)
|
||||
- `status_code` (int|string)
|
||||
- `duration_ms` (int)
|
||||
- `prompt_id` (string)
|
||||
- `auth_type` (string)
|
||||
|
||||
- `gemini_cli.malformed_json_response`: `generateJson` response could not be
|
||||
parsed.
|
||||
- **Attributes**:
|
||||
- `model` (string)
|
||||
|
||||
#### Model Routing
|
||||
|
||||
Tracks model selections via slash commands and router decisions.
|
||||
|
||||
- `gemini_cli.slash_command`: A slash command was executed.
|
||||
- `gemini_cli.slash_command`: This event occurs when a user executes a slash
|
||||
command.
|
||||
- **Attributes**:
|
||||
- `command` (string)
|
||||
- `subcommand` (string, optional)
|
||||
- `status` ("success" | "error")
|
||||
- `subcommand` (string, if applicable)
|
||||
|
||||
- `gemini_cli.slash_command.model`: Model was selected via slash command.
|
||||
- **Attributes**:
|
||||
- `model_name` (string)
|
||||
|
||||
- `gemini_cli.model_routing`: Model router made a decision.
|
||||
- **Attributes**:
|
||||
- `decision_model` (string)
|
||||
- `decision_source` (string)
|
||||
- `routing_latency_ms` (int)
|
||||
- `reasoning` (string, optional)
|
||||
- `failed` (boolean)
|
||||
- `error_message` (string, optional)
|
||||
|
||||
#### Chat and Streaming
|
||||
|
||||
Observes streaming integrity, compression, and retry behavior.
|
||||
|
||||
- `gemini_cli.chat_compression`: Chat context was compressed.
|
||||
- **Attributes**:
|
||||
- `tokens_before` (int)
|
||||
- `tokens_after` (int)
|
||||
|
||||
- `gemini_cli.chat.invalid_chunk`: Invalid chunk received from a stream.
|
||||
- **Attributes**:
|
||||
- `error.message` (string, optional)
|
||||
|
||||
- `gemini_cli.chat.content_retry`: Retry triggered due to a content error.
|
||||
- **Attributes**:
|
||||
- `attempt_number` (int)
|
||||
- `error_type` (string)
|
||||
- `retry_delay_ms` (int)
|
||||
- `model` (string)
|
||||
|
||||
- `gemini_cli.chat.content_retry_failure`: All content retries failed.
|
||||
- **Attributes**:
|
||||
- `total_attempts` (int)
|
||||
- `final_error_type` (string)
|
||||
- `total_duration_ms` (int, optional)
|
||||
- `model` (string)
|
||||
|
||||
- `gemini_cli.conversation_finished`: Conversation session ended.
|
||||
- **Attributes**:
|
||||
- `approvalMode` (string)
|
||||
- `turnCount` (int)
|
||||
|
||||
- `gemini_cli.next_speaker_check`: Next speaker determination.
|
||||
- **Attributes**:
|
||||
- `prompt_id` (string)
|
||||
- `finish_reason` (string)
|
||||
- `result` (string)
|
||||
|
||||
#### Resilience
|
||||
|
||||
Records fallback mechanisms for models and network operations.
|
||||
|
||||
- `gemini_cli.flash_fallback`: Switched to a flash model as fallback.
|
||||
- **Attributes**:
|
||||
- `auth_type` (string)
|
||||
|
||||
- `gemini_cli.ripgrep_fallback`: Switched to grep as fallback for file search.
|
||||
- **Attributes**:
|
||||
- `error` (string, optional)
|
||||
|
||||
- `gemini_cli.web_fetch_fallback_attempt`: Attempted web-fetch fallback.
|
||||
- **Attributes**:
|
||||
- `reason` ("private_ip" | "primary_failed")
|
||||
|
||||
#### Extensions
|
||||
|
||||
Tracks extension lifecycle and settings changes.
|
||||
|
||||
- `gemini_cli.extension_install`: An extension was installed.
|
||||
- `gemini_cli.extension_enable`: This event occurs when an extension is enabled
|
||||
- `gemini_cli.extension_install`: This event occurs when an extension is
|
||||
installed
|
||||
- **Attributes**:
|
||||
- `extension_name` (string)
|
||||
- `extension_version` (string)
|
||||
- `extension_source` (string)
|
||||
- `status` (string)
|
||||
|
||||
- `gemini_cli.extension_uninstall`: An extension was uninstalled.
|
||||
- **Attributes**:
|
||||
- `extension_name` (string)
|
||||
- `status` (string)
|
||||
|
||||
- `gemini_cli.extension_enable`: An extension was enabled.
|
||||
- **Attributes**:
|
||||
- `extension_name` (string)
|
||||
- `setting_scope` (string)
|
||||
|
||||
- `gemini_cli.extension_disable`: An extension was disabled.
|
||||
- **Attributes**:
|
||||
- `extension_name` (string)
|
||||
- `setting_scope` (string)
|
||||
|
||||
- `gemini_cli.extension_update`: An extension was updated.
|
||||
- **Attributes**:
|
||||
- `extension_name` (string)
|
||||
- `extension_version` (string)
|
||||
- `extension_previous_version` (string)
|
||||
- `extension_source` (string)
|
||||
- `status` (string)
|
||||
|
||||
#### Agent Runs
|
||||
|
||||
Tracks agent lifecycle and outcomes.
|
||||
|
||||
- `gemini_cli.agent.start`: Agent run started.
|
||||
- **Attributes**:
|
||||
- `agent_id` (string)
|
||||
- `agent_name` (string)
|
||||
|
||||
- `gemini_cli.agent.finish`: Agent run finished.
|
||||
- **Attributes**:
|
||||
- `agent_id` (string)
|
||||
- `agent_name` (string)
|
||||
- `duration_ms` (int)
|
||||
- `turn_count` (int)
|
||||
- `terminate_reason` (string)
|
||||
|
||||
#### IDE
|
||||
|
||||
Captures IDE connectivity and conversation lifecycle events.
|
||||
|
||||
- `gemini_cli.ide_connection`: IDE companion connection.
|
||||
- **Attributes**:
|
||||
- `connection_type` (string)
|
||||
|
||||
#### UI
|
||||
|
||||
Tracks terminal rendering issues and related signals.
|
||||
|
||||
- `kitty_sequence_overflow`: Terminal kitty control sequence overflow.
|
||||
- **Attributes**:
|
||||
- `sequence_length` (int)
|
||||
- `truncated_sequence` (string)
|
||||
- `gemini_cli.extension_uninstall`: This event occurs when an extension is
|
||||
uninstalled
|
||||
|
||||
### Metrics
|
||||
|
||||
@@ -522,31 +338,27 @@ Metrics are numerical measurements of behavior over time.
|
||||
|
||||
#### Custom
|
||||
|
||||
##### Sessions
|
||||
|
||||
Counts CLI sessions at startup.
|
||||
|
||||
- `gemini_cli.session.count` (Counter, Int): Incremented once per CLI startup.
|
||||
|
||||
##### Tools
|
||||
|
||||
Measures tool usage and latency.
|
||||
|
||||
- `gemini_cli.tool.call.count` (Counter, Int): Counts tool calls.
|
||||
- **Attributes**:
|
||||
- `function_name`
|
||||
- `success` (boolean)
|
||||
- `decision` (string: "accept", "reject", "modify", or "auto_accept", if
|
||||
applicable)
|
||||
- `tool_type` (string: "mcp" or "native", if applicable)
|
||||
- `decision` (string: "accept", "reject", or "modify", if applicable)
|
||||
- `tool_type` (string: "mcp", or "native", if applicable)
|
||||
- `model_added_lines` (Int, optional): Lines added by model in the proposed
|
||||
changes, if applicable
|
||||
- `model_removed_lines` (Int, optional): Lines removed by model in the
|
||||
proposed changes, if applicable
|
||||
- `user_added_lines` (Int, optional): Lines added by user edits after model
|
||||
proposal, if applicable
|
||||
- `user_removed_lines` (Int, optional): Lines removed by user edits after
|
||||
model proposal, if applicable
|
||||
|
||||
- `gemini_cli.tool.call.latency` (Histogram, ms): Measures tool call latency.
|
||||
- **Attributes**:
|
||||
- `function_name`
|
||||
|
||||
##### API
|
||||
|
||||
Tracks API request volume and latency.
|
||||
- `decision` (string: "accept", "reject", or "modify", if applicable)
|
||||
|
||||
- `gemini_cli.api.request.count` (Counter, Int): Counts all API requests.
|
||||
- **Attributes**:
|
||||
@@ -558,167 +370,32 @@ Tracks API request volume and latency.
|
||||
latency.
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- Note: Overlaps with `gen_ai.client.operation.duration` (GenAI conventions).
|
||||
- **Note**: This metric overlaps with `gen_ai.client.operation.duration` below
|
||||
that's compliant with GenAI Semantic Conventions.
|
||||
|
||||
##### Token Usage
|
||||
|
||||
Tracks tokens used by model and type.
|
||||
|
||||
- `gemini_cli.token.usage` (Counter, Int): Counts tokens used.
|
||||
- `gemini_cli.token.usage` (Counter, Int): Counts the number of tokens used.
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `type` ("input", "output", "thought", "cache", or "tool")
|
||||
- Note: Overlaps with `gen_ai.client.token.usage` for `input`/`output`.
|
||||
|
||||
##### Files
|
||||
|
||||
Counts file operations with basic context.
|
||||
- `type` (string: "input", "output", "thought", "cache", or "tool")
|
||||
- **Note**: This metric overlaps with `gen_ai.client.token.usage` below for
|
||||
`input`/`output` token types that's compliant with GenAI Semantic
|
||||
Conventions.
|
||||
|
||||
- `gemini_cli.file.operation.count` (Counter, Int): Counts file operations.
|
||||
- **Attributes**:
|
||||
- `operation` ("create", "read", "update")
|
||||
- `lines` (Int, optional)
|
||||
- `mimetype` (string, optional)
|
||||
- `extension` (string, optional)
|
||||
- `programming_language` (string, optional)
|
||||
|
||||
- `gemini_cli.lines.changed` (Counter, Int): Number of lines changed (from file
|
||||
diffs).
|
||||
- **Attributes**:
|
||||
- `function_name`
|
||||
- `type` ("added" or "removed")
|
||||
|
||||
##### Chat and Streaming
|
||||
|
||||
Resilience counters for compression, invalid chunks, and retries.
|
||||
- `operation` (string: "create", "read", "update"): The type of file
|
||||
operation.
|
||||
- `lines` (Int, if applicable): Number of lines in the file.
|
||||
- `mimetype` (string, if applicable): Mimetype of the file.
|
||||
- `extension` (string, if applicable): File extension of the file.
|
||||
- `programming_language` (string, if applicable): The programming language
|
||||
of the file.
|
||||
|
||||
- `gemini_cli.chat_compression` (Counter, Int): Counts chat compression
|
||||
operations.
|
||||
operations
|
||||
- **Attributes**:
|
||||
- `tokens_before` (Int)
|
||||
- `tokens_after` (Int)
|
||||
|
||||
- `gemini_cli.chat.invalid_chunk.count` (Counter, Int): Counts invalid chunks
|
||||
from streams.
|
||||
|
||||
- `gemini_cli.chat.content_retry.count` (Counter, Int): Counts retries due to
|
||||
content errors.
|
||||
|
||||
- `gemini_cli.chat.content_retry_failure.count` (Counter, Int): Counts requests
|
||||
where all content retries failed.
|
||||
|
||||
##### Model Routing
|
||||
|
||||
Routing latency/failures and slash-command selections.
|
||||
|
||||
- `gemini_cli.slash_command.model.call_count` (Counter, Int): Counts model
|
||||
selections via slash command.
|
||||
- **Attributes**:
|
||||
- `slash_command.model.model_name` (string)
|
||||
|
||||
- `gemini_cli.model_routing.latency` (Histogram, ms): Model routing decision
|
||||
latency.
|
||||
- **Attributes**:
|
||||
- `routing.decision_model` (string)
|
||||
- `routing.decision_source` (string)
|
||||
|
||||
- `gemini_cli.model_routing.failure.count` (Counter, Int): Counts model routing
|
||||
failures.
|
||||
- **Attributes**:
|
||||
- `routing.decision_source` (string)
|
||||
- `routing.error_message` (string)
|
||||
|
||||
##### Agent Runs
|
||||
|
||||
Agent lifecycle metrics: runs, durations, and turns.
|
||||
|
||||
- `gemini_cli.agent.run.count` (Counter, Int): Counts agent runs.
|
||||
- **Attributes**:
|
||||
- `agent_name` (string)
|
||||
- `terminate_reason` (string)
|
||||
|
||||
- `gemini_cli.agent.duration` (Histogram, ms): Agent run durations.
|
||||
- **Attributes**:
|
||||
- `agent_name` (string)
|
||||
|
||||
- `gemini_cli.agent.turns` (Histogram, turns): Turns taken per agent run.
|
||||
- **Attributes**:
|
||||
- `agent_name` (string)
|
||||
|
||||
##### UI
|
||||
|
||||
UI stability signals such as flicker count.
|
||||
|
||||
- `gemini_cli.ui.flicker.count` (Counter, Int): Counts UI frames that flicker
|
||||
(render taller than terminal).
|
||||
|
||||
##### Performance
|
||||
|
||||
Optional performance monitoring for startup, CPU/memory, and phase timing.
|
||||
|
||||
- `gemini_cli.startup.duration` (Histogram, ms): CLI startup time by phase.
|
||||
- **Attributes**:
|
||||
- `phase` (string)
|
||||
- `details` (map, optional)
|
||||
|
||||
- `gemini_cli.memory.usage` (Histogram, bytes): Memory usage.
|
||||
- **Attributes**:
|
||||
- `memory_type` ("heap_used", "heap_total", "external", "rss")
|
||||
- `component` (string, optional)
|
||||
|
||||
- `gemini_cli.cpu.usage` (Histogram, percent): CPU usage percentage.
|
||||
- **Attributes**:
|
||||
- `component` (string, optional)
|
||||
|
||||
- `gemini_cli.tool.queue.depth` (Histogram, count): Number of tools in the
|
||||
execution queue.
|
||||
|
||||
- `gemini_cli.tool.execution.breakdown` (Histogram, ms): Tool time by phase.
|
||||
- **Attributes**:
|
||||
- `function_name` (string)
|
||||
- `phase` ("validation", "preparation", "execution", "result_processing")
|
||||
|
||||
- `gemini_cli.api.request.breakdown` (Histogram, ms): API request time by phase.
|
||||
- **Attributes**:
|
||||
- `model` (string)
|
||||
- `phase` ("request_preparation", "network_latency", "response_processing",
|
||||
"token_processing")
|
||||
|
||||
- `gemini_cli.token.efficiency` (Histogram, ratio): Token efficiency metrics.
|
||||
- **Attributes**:
|
||||
- `model` (string)
|
||||
- `metric` (string)
|
||||
- `context` (string, optional)
|
||||
|
||||
- `gemini_cli.performance.score` (Histogram, score): Composite performance
|
||||
score.
|
||||
- **Attributes**:
|
||||
- `category` (string)
|
||||
- `baseline` (number, optional)
|
||||
|
||||
- `gemini_cli.performance.regression` (Counter, Int): Regression detection
|
||||
events.
|
||||
- **Attributes**:
|
||||
- `metric` (string)
|
||||
- `severity` ("low", "medium", "high")
|
||||
- `current_value` (number)
|
||||
- `baseline_value` (number)
|
||||
|
||||
- `gemini_cli.performance.regression.percentage_change` (Histogram, percent):
|
||||
Percent change from baseline when regression detected.
|
||||
- **Attributes**:
|
||||
- `metric` (string)
|
||||
- `severity` ("low", "medium", "high")
|
||||
- `current_value` (number)
|
||||
- `baseline_value` (number)
|
||||
|
||||
- `gemini_cli.performance.baseline.comparison` (Histogram, percent): Comparison
|
||||
to baseline.
|
||||
- **Attributes**:
|
||||
- `metric` (string)
|
||||
- `category` (string)
|
||||
- `current_value` (number)
|
||||
- `baseline_value` (number)
|
||||
- `tokens_before`: (Int): Number of tokens in context prior to compression
|
||||
- `tokens_after`: (Int): Number of tokens in context after compression
|
||||
|
||||
#### GenAI Semantic Convention
|
||||
|
||||
@@ -757,5 +434,3 @@ standardized observability across GenAI applications:
|
||||
|
||||
[OpenTelemetry GenAI semantic conventions]:
|
||||
https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-metrics.md
|
||||
[OpenTelemetry GenAI semantic conventions for events]:
|
||||
https://github.com/open-telemetry/semantic-conventions/blob/8b4f210f43136e57c1f6f47292eb6d38e3bf30bb/docs/gen-ai/gen-ai-events.md
|
||||
|
||||
+12
-12
@@ -183,50 +183,50 @@ untrusted sources.
|
||||
|
||||
### ANSI
|
||||
|
||||
<img src="/assets/theme-ansi.png" alt="ANSI theme" width="600" />
|
||||
<img src="../assets/theme-ansi.png" alt="ANSI theme" width="600" />
|
||||
|
||||
### Atom OneDark
|
||||
|
||||
<img src="/assets/theme-atom-one.png" alt="Atom One theme" width="600">
|
||||
<img src="../assets/theme-atom-one.png" alt="Atom One theme" width="600">
|
||||
|
||||
### Ayu
|
||||
|
||||
<img src="/assets/theme-ayu.png" alt="Ayu theme" width="600">
|
||||
<img src="../assets/theme-ayu.png" alt="Ayu theme" width="600">
|
||||
|
||||
### Default
|
||||
|
||||
<img src="/assets/theme-default.png" alt="Default theme" width="600">
|
||||
<img src="../assets/theme-default.png" alt="Default theme" width="600">
|
||||
|
||||
### Dracula
|
||||
|
||||
<img src="/assets/theme-dracula.png" alt="Dracula theme" width="600">
|
||||
<img src="../assets/theme-dracula.png" alt="Dracula theme" width="600">
|
||||
|
||||
### GitHub
|
||||
|
||||
<img src="/assets/theme-github.png" alt="GitHub theme" width="600">
|
||||
<img src="../assets/theme-github.png" alt="GitHub theme" width="600">
|
||||
|
||||
## Light Themes
|
||||
|
||||
### ANSI Light
|
||||
|
||||
<img src="/assets/theme-ansi-light.png" alt="ANSI Light theme" width="600">
|
||||
<img src="../assets/theme-ansi-light.png" alt="ANSI Light theme" width="600">
|
||||
|
||||
### Ayu Light
|
||||
|
||||
<img src="/assets/theme-ayu-light.png" alt="Ayu Light theme" width="600">
|
||||
<img src="../assets/theme-ayu-light.png" alt="Ayu Light theme" width="600">
|
||||
|
||||
### Default Light
|
||||
|
||||
<img src="/assets/theme-default-light.png" alt="Default Light theme" width="600">
|
||||
<img src="../assets/theme-default-light.png" alt="Default Light theme" width="600">
|
||||
|
||||
### GitHub Light
|
||||
|
||||
<img src="/assets/theme-github-light.png" alt="GitHub Light theme" width="600">
|
||||
<img src="../assets/theme-github-light.png" alt="GitHub Light theme" width="600">
|
||||
|
||||
### Google Code
|
||||
|
||||
<img src="/assets/theme-google-light.png" alt="Google Code theme" width="600">
|
||||
<img src="../assets/theme-google-light.png" alt="Google Code theme" width="600">
|
||||
|
||||
### Xcode
|
||||
|
||||
<img src="/assets/theme-xcode-light.png" alt="Xcode Light theme" width="600">
|
||||
<img src="../assets/theme-xcode-light.png" alt="Xcode Light theme" width="600">
|
||||
|
||||
@@ -59,13 +59,6 @@ to protect you. In this mode, the following features are disabled:
|
||||
5. **Automatic Memory Loading is Disabled**: The CLI will not automatically
|
||||
load files into context from directories specified in local settings.
|
||||
|
||||
6. **MCP Servers Do Not Connect**: The CLI will not attempt to connect to any
|
||||
[Model Context Protocol (MCP)](../tools/mcp-server.md) servers.
|
||||
|
||||
7. **Custom Commands are Not Loaded**: The CLI will not load any custom
|
||||
commands from .toml files, including both project-specific and global user
|
||||
commands.
|
||||
|
||||
Granting trust to a folder unlocks the full functionality of the Gemini CLI for
|
||||
that workspace.
|
||||
|
||||
|
||||
@@ -177,40 +177,6 @@ When Gemini CLI starts, it loads all the extensions and merges their
|
||||
configurations. If there are any conflicts, the workspace configuration takes
|
||||
precedence.
|
||||
|
||||
### Settings
|
||||
|
||||
Extensions can define settings that the user will be prompted to provide upon
|
||||
installation. This is useful for things like API keys, URLs, or other
|
||||
configuration that the extension needs to function.
|
||||
|
||||
To define settings, add a `settings` array to your `gemini-extension.json` file.
|
||||
Each object in the array should have the following properties:
|
||||
|
||||
- `name`: A user-friendly name for the setting.
|
||||
- `description`: A description of the setting and what it's used for.
|
||||
- `envVar`: The name of the environment variable that the setting will be stored
|
||||
as.
|
||||
- `sensitive`: Optional boolean. If true, obfuscates the input the user provides
|
||||
and stores the secret in keychain storage. **Example**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-api-extension",
|
||||
"version": "1.0.0",
|
||||
"settings": [
|
||||
{
|
||||
"name": "API Key",
|
||||
"description": "Your API key for the service.",
|
||||
"envVar": "MY_API_KEY"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
When a user installs this extension, they will be prompted to enter their API
|
||||
key. The value will be saved to a `.env` file in the extension's directory
|
||||
(e.g., `<home>/.gemini/extensions/my-api-extension/.env`).
|
||||
|
||||
### Custom commands
|
||||
|
||||
Extensions can provide [custom commands](../cli/custom-commands.md) by placing
|
||||
|
||||
@@ -473,6 +473,21 @@ a few things you can try in order of recommendation:
|
||||
"loadMemoryFromIncludeDirectories": true
|
||||
```
|
||||
|
||||
- **`chatCompression`** (object):
|
||||
- **Description:** Controls the settings for chat history compression, both
|
||||
automatic and when manually invoked through the /compress command.
|
||||
- **Properties:**
|
||||
- **`contextPercentageThreshold`** (number): A value between 0 and 1 that
|
||||
specifies the token threshold for compression as a percentage of the
|
||||
model's total token limit. For example, a value of `0.6` will trigger
|
||||
compression when the chat history exceeds 60% of the token limit.
|
||||
- **Example:**
|
||||
```json
|
||||
"chatCompression": {
|
||||
"contextPercentageThreshold": 0.6
|
||||
}
|
||||
```
|
||||
|
||||
- **`showLineNumbers`** (boolean):
|
||||
- **Description:** Controls whether line numbers are displayed in code blocks
|
||||
in the CLI output.
|
||||
@@ -658,7 +673,9 @@ for that specific session.
|
||||
- Sets the sandbox image URI.
|
||||
- **`--debug`** (**`-d`**):
|
||||
- Enables debug mode for this session, providing more verbose output.
|
||||
|
||||
- **`--all-files`** (**`-a`**):
|
||||
- If set, recursively includes all files within the current directory as
|
||||
context for the prompt.
|
||||
- **`--help`** (or **`-h`**):
|
||||
- Displays help information about command-line arguments.
|
||||
- **`--show-memory-usage`**:
|
||||
|
||||
@@ -65,12 +65,11 @@ locations for these files:
|
||||
controls over users' Gemini CLI setups.
|
||||
|
||||
**Note on environment variables in settings:** String values within your
|
||||
`settings.json` and `gemini-extension.json` files can reference environment
|
||||
variables using either `$VAR_NAME` or `${VAR_NAME}` syntax. These variables will
|
||||
be automatically resolved when the settings are loaded. For example, if you have
|
||||
an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like
|
||||
this: `"apiKey": "$MY_API_TOKEN"`. Additionally, each extension can have its own
|
||||
`.env` file in its directory, which will be loaded automatically.
|
||||
`settings.json` files can reference environment variables using either
|
||||
`$VAR_NAME` or `${VAR_NAME}` syntax. These variables will be automatically
|
||||
resolved when the settings are loaded. For example, if you have an environment
|
||||
variable `MY_API_TOKEN`, you could use it in `settings.json` like this:
|
||||
`"apiKey": "$MY_API_TOKEN"`.
|
||||
|
||||
> **Note for Enterprise Users:** For guidance on deploying and managing Gemini
|
||||
> CLI in a corporate environment, please see the
|
||||
@@ -129,7 +128,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **`output.format`** (string):
|
||||
- **Description:** The format of the CLI output.
|
||||
- **Default:** `"text"`
|
||||
- **Values:** `"text"`, `"json"`, `"stream-json"`
|
||||
- **Values:** `"text"`, `"json"`
|
||||
|
||||
#### `ui`
|
||||
|
||||
@@ -245,13 +244,13 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
example `{"run_shell_command": {"tokenBudget": 2000}}`
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`model.compressionThreshold`** (number):
|
||||
- **`model.chatCompression.contextPercentageThreshold`** (number):
|
||||
- **Description:** Sets the threshold for chat history compression as a
|
||||
fraction of the model's total token limit. This is a value between 0 and 1
|
||||
percentage of the model's total token limit. This is a value between 0 and 1
|
||||
that applies to both automatic compression and the manual `/compress`
|
||||
command. For example, a value of `0.6` will trigger compression when the
|
||||
chat history exceeds 60% of the token limit.
|
||||
- **Default:** `0.2`
|
||||
- **Default:** `0.7`
|
||||
|
||||
- **`model.skipNextSpeakerCheck`** (boolean):
|
||||
- **Description:** Skip the next speaker check.
|
||||
@@ -402,18 +401,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** A denylist of MCP servers to exclude.
|
||||
- **Default:** `undefined`
|
||||
|
||||
#### `useSmartEdit`
|
||||
|
||||
- **`useSmartEdit`** (boolean):
|
||||
- **Description:** Enable the smart-edit tool instead of the replace tool.
|
||||
- **Default:** `true`
|
||||
|
||||
#### `useWriteTodos`
|
||||
|
||||
- **`useWriteTodos`** (boolean):
|
||||
- **Description:** Enable the write_todos tool.
|
||||
- **Default:** `false`
|
||||
|
||||
#### `security`
|
||||
|
||||
- **`security.folderTrust.enabled`** (boolean):
|
||||
@@ -731,16 +718,17 @@ for that specific session.
|
||||
- **Values:**
|
||||
- `text`: (Default) The standard human-readable output.
|
||||
- `json`: A machine-readable JSON output.
|
||||
- `stream-json`: A streaming JSON output that emits real-time events.
|
||||
- **Note:** For structured output and scripting, use the
|
||||
`--output-format json` or `--output-format stream-json` flag.
|
||||
`--output-format json` flag.
|
||||
- **`--sandbox`** (**`-s`**):
|
||||
- Enables sandbox mode for this session.
|
||||
- **`--sandbox-image`**:
|
||||
- Sets the sandbox image URI.
|
||||
- **`--debug`** (**`-d`**):
|
||||
- Enables debug mode for this session, providing more verbose output.
|
||||
|
||||
- **`--all-files`** (**`-a`**):
|
||||
- If set, recursively includes all files within the current directory as
|
||||
context for the prompt.
|
||||
- **`--help`** (or **`-h`**):
|
||||
- Displays help information about command-line arguments.
|
||||
- **`--show-memory-usage`**:
|
||||
|
||||
@@ -41,7 +41,7 @@ straightforward authentication method uses your existing Google account:
|
||||
4. Click on **Sign in**.
|
||||
|
||||
For other authentication options and information, see
|
||||
[Gemini CLI Authentication Setup](./authentication.md).
|
||||
[GeminI CLI Authentication Setup](./authentication.md).
|
||||
|
||||
## Configure
|
||||
|
||||
|
||||
@@ -68,7 +68,6 @@ This documentation is organized into the following sections:
|
||||
`google_web_search` tool.
|
||||
- **[Memory Tool](./tools/memory.md):** Documentation for the `save_memory`
|
||||
tool.
|
||||
- **[Todo Tool](./tools/todos.md):** Documentation for the `write_todos` tool.
|
||||
|
||||
### Extensions
|
||||
|
||||
|
||||
@@ -56,39 +56,15 @@ To run a single test by its name, use the `--test-name-pattern` flag:
|
||||
npm run test:e2e -- --test-name-pattern "reads a file"
|
||||
```
|
||||
|
||||
### Regenerating model responses
|
||||
|
||||
Some integration tests use faked out model responses, which may need to be
|
||||
regenerated from time to time as the implementations change.
|
||||
|
||||
To regenerate these golden files, set the REGENERATE_MODEL_GOLDENS environment
|
||||
variable to "true" when running the tests, for example:
|
||||
|
||||
**WARNING**: If running locally you should review these updated responses for
|
||||
any information about yourself or your system that gemini may have included in
|
||||
these responses.
|
||||
|
||||
```bash
|
||||
REGENERATE_MODEL_GOLDENS="true" npm run test:e2e
|
||||
```
|
||||
|
||||
### Deflaking a test
|
||||
|
||||
Before adding a **new** integration test, you should test it at least 5 times
|
||||
with the deflake script or workflow to make sure that it is not flaky.
|
||||
|
||||
### Deflake script
|
||||
with the deflake script to make sure that it is not flaky.
|
||||
|
||||
```bash
|
||||
npm run deflake -- --runs=5 --command="npm run test:e2e -- -- --test-name-pattern '<your-new-test-name>'"
|
||||
```
|
||||
|
||||
#### Deflake Workflow
|
||||
|
||||
```bash
|
||||
gh workflow run deflake.yml --ref <your-branch> -f test_name_pattern="<your-test-name-pattern>"
|
||||
```
|
||||
|
||||
### Running all tests
|
||||
|
||||
To run the entire suite of integration tests, use the following command:
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
# Local Development Guide
|
||||
|
||||
This guide provides instructions for setting up and using local development
|
||||
features, such as development tracing.
|
||||
|
||||
## Development Tracing
|
||||
|
||||
Development traces (dev traces) are OpenTelemetry (OTel) traces that help you
|
||||
debug your code by instrumenting interesting events like model calls, tool
|
||||
scheduler, tool calls, etc.
|
||||
|
||||
Dev traces are verbose and are specifically meant for understanding agent
|
||||
behaviour and debugging issues. They are disabled by default.
|
||||
|
||||
To enable dev traces, set the `GEMINI_DEV_TRACING=true` environment variable
|
||||
when running Gemini CLI.
|
||||
|
||||
### Viewing Dev Traces
|
||||
|
||||
You can view dev traces using either Jaeger or the Genkit Developer UI.
|
||||
|
||||
#### Using Genkit
|
||||
|
||||
Genkit provides a web-based UI for viewing traces and other telemetry data.
|
||||
|
||||
1. **Start the Genkit Telemetry Server:**
|
||||
|
||||
Run the following command to start the Genkit server:
|
||||
|
||||
```bash
|
||||
npm run telemetry -- --target=genkit
|
||||
```
|
||||
|
||||
The script will output the URL for the Genkit Developer UI, for example:
|
||||
|
||||
```
|
||||
Genkit Developer UI: http://localhost:4000
|
||||
```
|
||||
|
||||
2. **Run Gemini CLI with Dev Tracing:**
|
||||
|
||||
In a separate terminal, run your Gemini CLI command with the
|
||||
`GEMINI_DEV_TRACING` environment variable:
|
||||
|
||||
```bash
|
||||
GEMINI_DEV_TRACING=true gemini
|
||||
```
|
||||
|
||||
3. **View the Traces:**
|
||||
|
||||
Open the Genkit Developer UI URL in your browser and navigate to the
|
||||
**Traces** tab to view the traces.
|
||||
|
||||
#### Using Jaeger
|
||||
|
||||
You can view dev traces in the Jaeger UI. To get started, follow these steps:
|
||||
|
||||
1. **Start the telemetry collector:**
|
||||
|
||||
Run the following command in your terminal to download and start Jaeger and
|
||||
an OTEL collector:
|
||||
|
||||
```bash
|
||||
npm run telemetry -- --target=local
|
||||
```
|
||||
|
||||
This command also configures your workspace for local telemetry and provides
|
||||
a link to the Jaeger UI (usually `http://localhost:16686`).
|
||||
|
||||
2. **Run Gemini CLI with dev tracing:**
|
||||
|
||||
In a separate terminal, run your Gemini CLI command with the
|
||||
`GEMINI_DEV_TRACING` environment variable:
|
||||
|
||||
```bash
|
||||
GEMINI_DEV_TRACING=true gemini
|
||||
```
|
||||
|
||||
3. **View the traces:**
|
||||
|
||||
After running your command, open the Jaeger UI link in your browser to view
|
||||
the traces.
|
||||
|
||||
For more detailed information on telemetry, see the
|
||||
[telemetry documentation](./cli/telemetry.md).
|
||||
|
||||
### Instrumenting Code with Dev Traces
|
||||
|
||||
You can add dev traces to your own code for more detailed instrumentation. This
|
||||
is useful for debugging and understanding the flow of execution.
|
||||
|
||||
Use the `runInDevTraceSpan` function to wrap any section of code in a trace
|
||||
span.
|
||||
|
||||
Here is a basic example:
|
||||
|
||||
```typescript
|
||||
import { runInDevTraceSpan } from '@google/gemini-cli-core';
|
||||
|
||||
await runInDevTraceSpan({ name: 'my-custom-span' }, async ({ metadata }) => {
|
||||
// The `metadata` object allows you to record the input and output of the
|
||||
// operation as well as other attributes.
|
||||
metadata.input = { key: 'value' };
|
||||
// Set custom attributes.
|
||||
metadata.attributes['gen_ai.request.model'] = 'gemini-4.0-mega';
|
||||
|
||||
// Your code to be traced goes here
|
||||
try {
|
||||
const output = await somethingRisky();
|
||||
metadata.output = output;
|
||||
return output;
|
||||
} catch (e) {
|
||||
metadata.error = e;
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
In this example:
|
||||
|
||||
- `name`: The name of the span, which will be displayed in the trace.
|
||||
- `metadata.input`: (Optional) An object containing the input data for the
|
||||
traced operation.
|
||||
- `metadata.output`: (Optional) An object containing the output data from the
|
||||
traced operation.
|
||||
- `metadata.attributes`: (Optional) A record of custom attributes to add to the
|
||||
span.
|
||||
- `metadata.error`: (Optional) An error object to record if the operation fails.
|
||||
@@ -1,164 +0,0 @@
|
||||
# Release Confidence Strategy
|
||||
|
||||
This document outlines the strategy for gaining confidence in every release of
|
||||
the Gemini CLI. It serves as a checklist and quality gate for release manager to
|
||||
ensure we are shipping a high-quality product.
|
||||
|
||||
## The Goal
|
||||
|
||||
To answer the question, "Is this release _truly_ ready for our users?" with a
|
||||
high degree of confidence, based on a holistic evaluation of automated signals,
|
||||
manual verification, and data.
|
||||
|
||||
## Level 1: Automated Gates (Must Pass)
|
||||
|
||||
These are the baseline requirements. If any of these fail, the release is a
|
||||
no-go.
|
||||
|
||||
### 1. CI/CD Health
|
||||
|
||||
All workflows in `.github/workflows/ci.yml` must pass on the `main` branch (for
|
||||
nightly) or the release branch (for preview/stable).
|
||||
|
||||
- **Platforms:** Tests must pass on **Linux and macOS**.
|
||||
- _Note:_ Windows tests currently run with `continue-on-error: true`. While a
|
||||
failure here doesn't block the release technically, it should be
|
||||
investigated.
|
||||
- **Checks:**
|
||||
- **Linting:** No linting errors (ESLint, Prettier, etc.).
|
||||
- **Typechecking:** No TypeScript errors.
|
||||
- **Unit Tests:** All unit tests in `packages/core` and `packages/cli` must
|
||||
pass.
|
||||
- **Build:** The project must build and bundle successfully.
|
||||
|
||||
### 2. End-to-End (E2E) Tests
|
||||
|
||||
All workflows in `.github/workflows/e2e.yml` must pass.
|
||||
|
||||
- **Platforms:** **Linux, macOS and Windows**.
|
||||
- **Sandboxing:** Tests must pass with both `sandbox:none` and `sandbox:docker`
|
||||
on Linux.
|
||||
|
||||
### 3. Post-Deployment Smoke Tests
|
||||
|
||||
After a release is published to npm, the `smoke-test.yml` workflow runs. This
|
||||
must pass to confirm the package is installable and the binary is executable.
|
||||
|
||||
- **Command:** `npx -y @google/gemini-cli@<tag> --version` must return the
|
||||
correct version without error.
|
||||
- **Platform:** Currently runs on `ubuntu-latest`.
|
||||
|
||||
## Level 2: Manual Verification & Dogfooding
|
||||
|
||||
Automated tests cannot catch everything, especially UX issues.
|
||||
|
||||
### 1. Dogfooding via `preview` Tag
|
||||
|
||||
The weekly release cadence promotes code from `main` -> `nightly` -> `preview`
|
||||
-> `stable`.
|
||||
|
||||
- **Requirement:** The `preview` release must be used by maintainers for at
|
||||
least **one week** before being promoted to `stable`.
|
||||
- **Action:** Maintainers should install the preview version locally:
|
||||
```bash
|
||||
npm install -g @google/gemini-cli@preview
|
||||
```
|
||||
- **Goal:** To catch regressions and UX issues in day-to-day usage before they
|
||||
reach the broad user base.
|
||||
|
||||
### 2. Critical User Journey (CUJ) Checklist
|
||||
|
||||
Before promoting a `preview` release to `stable`, a release manager must
|
||||
manually run through this checklist.
|
||||
|
||||
- **Setup:**
|
||||
- [ ] Uninstall any existing global version:
|
||||
`npm uninstall -g @google/gemini-cli`
|
||||
- [ ] Clear npx cache (optional but recommended): `npm cache clean --force`
|
||||
- [ ] Install the preview version: `npm install -g @google/gemini-cli@preview`
|
||||
- [ ] Verify version: `gemini --version`
|
||||
|
||||
- **Authentication:**
|
||||
- [ ] In interactive mode run `/auth` and verify all login flows work:
|
||||
- [ ] Login With Google
|
||||
- [ ] API Key
|
||||
- [ ] Vertex AI
|
||||
|
||||
- **Basic Prompting:**
|
||||
- [ ] Run `gemini "Tell me a joke"` and verify a sensible response.
|
||||
- [ ] Run in interactive mode: `gemini`. Ask a follow-up question to test
|
||||
context.
|
||||
|
||||
- **Piped Input:**
|
||||
- [ ] Run `echo "Summarize this" | gemini` and verify it processes stdin.
|
||||
|
||||
- **Context Management:**
|
||||
- [ ] In interactive mode, use `@file` to add a local file to context. Ask a
|
||||
question about it.
|
||||
|
||||
- **Settings:**
|
||||
- [ ] In interactive mode run `/settings` and make modifications
|
||||
- [ ] Validate that setting is changed
|
||||
|
||||
- **Function Calling:**
|
||||
- [ ] In interactive mode, ask gemini to "create a file named hello.md with
|
||||
the content 'hello world'" and verify the file is created correctly.
|
||||
|
||||
If any of these CUJs fail, the release is a no-go until a patch is applied to
|
||||
the `preview` channel.
|
||||
|
||||
### 3. Pre-Launch Bug Bash (Tier 1 & 2 Launches)
|
||||
|
||||
For high-impact releases, an organized bug bash is required to ensure a higher
|
||||
level of quality and to catch issues across a wider range of environments and
|
||||
use cases.
|
||||
|
||||
**Definition of Tiers:**
|
||||
|
||||
- **Tier 1:** Industry-Moving News 🚀
|
||||
- **Tier 2:** Important News for Our Users 📣
|
||||
- **Tier 3:** Relevant, but Not Life-Changing 💡
|
||||
- **Tier 4:** Bug Fixes ⚒️
|
||||
|
||||
**Requirement:**
|
||||
|
||||
A bug bash must be scheduled at least **72 hours in advance** of any Tier 1 or
|
||||
Tier 2 launch.
|
||||
|
||||
**Rule of Thumb:**
|
||||
|
||||
A bug bash should be considered for any release that involves:
|
||||
|
||||
- A blog post
|
||||
- Coordinated social media announcements
|
||||
- Media relations or press outreach
|
||||
- A "Turbo" launch event
|
||||
|
||||
## Level 3: Telemetry & Data Review
|
||||
|
||||
### Dashboard Health
|
||||
|
||||
- [ ] Go to `go/gemini-cli-dash`.
|
||||
- [ ] Navigate to the "Tool Call" tab.
|
||||
- [ ] Validate that there are no spikes in errors for the release you would like
|
||||
to promote.
|
||||
|
||||
### Model Evaluation
|
||||
|
||||
- [ ] Navigate to `go/gemini-cli-offline-evals-dash`.
|
||||
- [ ] Make sure that the release you want to promote's recurring run is within
|
||||
average eval runs.
|
||||
|
||||
## The "Go/No-Go" Decision
|
||||
|
||||
Before triggering the `Release: Promote` workflow to move `preview` to `stable`:
|
||||
|
||||
1. [ ] **Level 1:** CI and E2E workflows are green for the commit corresponding
|
||||
to the current `preview` tag.
|
||||
2. [ ] **Level 2:** The `preview` version has been out for one week, and the
|
||||
CUJ checklist has been completed successfully by a release manager. No
|
||||
blocking issues have been reported.
|
||||
3. [ ] **Level 3:** Dashboard Health and Model Evaluation checks have been
|
||||
completed and show no regressions.
|
||||
|
||||
If all checks pass, proceed with the promotion.
|
||||
+3
-33
@@ -1,27 +1,5 @@
|
||||
# Gemini CLI Releases
|
||||
|
||||
## `dev` vs `prod` environment
|
||||
|
||||
Our release flows support both `dev` and `prod` environments.
|
||||
|
||||
The `dev` environment pushes to a private Github-hosted NPM repository, with the
|
||||
package names beginning with `@google-gemini/**` instead of `@google/**`.
|
||||
|
||||
The `prod` environment pushes to the public global NPM registry via Wombat
|
||||
Dressing Room, which is Google's system for managing NPM packages in the
|
||||
`@google/**` namespace. The packages are all named `@google/**`.
|
||||
|
||||
More information can be found about these systems in the
|
||||
[maintainer repo guide](https://github.com/google-gemini/maintainers-gemini-cli/blob/main/npm.md)
|
||||
|
||||
### Package scopes
|
||||
|
||||
| Package | `prod` (Wombat Dressing Room) | `dev` (Github Private NPM Repo) |
|
||||
| ---------- | ----------------------------- | ----------------------------------------- |
|
||||
| CLI | @google/gemini-cli | @google-gemini/gemini-cli |
|
||||
| Core | @google/gemini-cli-core | @google-gemini/gemini-cli-core A2A Server |
|
||||
| A2A Server | @google/gemini-cli-a2a-server | @google-gemini/gemini-cli-a2a-server |
|
||||
|
||||
## Release Cadence and Tags
|
||||
|
||||
We will follow https://semver.org/ as closely as possible but will call out when
|
||||
@@ -134,10 +112,6 @@ specific version from any branch, tag, or commit SHA.
|
||||
recommended for production releases.
|
||||
- **Skip GitHub Release**: Set to `true` to skip creating a GitHub release
|
||||
and create an npm release only.
|
||||
- **Environment**: Select the appropriate environment. The `dev` environment
|
||||
is intended for testing. The `prod` environment is intended for production
|
||||
releases. `prod` is the default and will require authorization from a
|
||||
release administrator.
|
||||
5. Click **Run workflow**.
|
||||
|
||||
The workflow will then proceed to test (if not skipped), build, and publish the
|
||||
@@ -166,15 +140,11 @@ require a full release cycle.
|
||||
- **Channel**: The npm `dist-tag` to apply (e.g., `preview`, `stable`).
|
||||
- **Dry Run**: Leave as `true` to log the action without making changes, or
|
||||
set to `false` to perform the live tag change.
|
||||
- **Environment**: Select the appropriate environment. The `dev` environment
|
||||
is intended for testing. The `prod` environment is intended for production
|
||||
releases. `prod` is the default and will require authorization from a
|
||||
release administrator.
|
||||
5. Click **Run workflow**.
|
||||
|
||||
The workflow will then run `npm dist-tag add` for the appropriate `gemini-cli`,
|
||||
`gemini-cli-core` and `gemini-cli-a2a-server` packages, pointing the specified
|
||||
channel to the specified version.
|
||||
The workflow will then run `npm dist-tag add` for both the `@google/gemini-cli`
|
||||
and `@google/gemini-cli-core` packages, pointing the specified channel to the
|
||||
specified version.
|
||||
|
||||
## Patching
|
||||
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
{
|
||||
"label": "Architecture Overview",
|
||||
"slug": "docs/architecture"
|
||||
},
|
||||
{
|
||||
"label": "Contribution Guide",
|
||||
"slug": "docs/contributing"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -150,10 +146,6 @@
|
||||
"label": "Memory",
|
||||
"slug": "docs/tools/memory"
|
||||
},
|
||||
{
|
||||
"label": "Todos",
|
||||
"slug": "docs/tools/todos"
|
||||
},
|
||||
{
|
||||
"label": "MCP Servers",
|
||||
"slug": "docs/tools/mcp-server"
|
||||
|
||||
@@ -86,8 +86,6 @@ Gemini CLI's built-in tools can be broadly categorized as follows:
|
||||
tool for reading content from multiple files or directories.
|
||||
- **[Memory Tool](./memory.md) (`save_memory`):** For saving and recalling
|
||||
information across sessions.
|
||||
- **[Todo Tool](./todos.md) (`write_todos`):** For managing subtasks of complex
|
||||
requests.
|
||||
|
||||
Additionally, these tools incorporate:
|
||||
|
||||
|
||||
@@ -930,7 +930,7 @@ This is the default transport for running local servers.
|
||||
gemini mcp add <name> <command> [args...]
|
||||
|
||||
# Example: Adding a local server
|
||||
gemini mcp add -e API_KEY=123 my-stdio-server /path/to/server arg1 arg2 arg3
|
||||
gemini mcp add my-stdio-server -e API_KEY=123 /path/to/server arg1 arg2 arg3
|
||||
|
||||
# Example: Adding a local python server
|
||||
gemini mcp add python-server python server.py --port 8080
|
||||
@@ -948,7 +948,7 @@ gemini mcp add --transport http <name> <url>
|
||||
gemini mcp add --transport http http-server https://api.example.com/mcp/
|
||||
|
||||
# Example: Adding an HTTP server with an authentication header
|
||||
gemini mcp add --transport http --header "Authorization: Bearer abc123" secure-http https://api.example.com/mcp/
|
||||
gemini mcp add --transport http secure-http https://api.example.com/mcp/ --header "Authorization: Bearer abc123"
|
||||
```
|
||||
|
||||
#### Adding an SSE server
|
||||
@@ -963,7 +963,7 @@ gemini mcp add --transport sse <name> <url>
|
||||
gemini mcp add --transport sse sse-server https://api.example.com/sse/
|
||||
|
||||
# Example: Adding an SSE server with an authentication header
|
||||
gemini mcp add --transport sse --header "Authorization: Bearer abc123" secure-sse https://api.example.com/sse/
|
||||
gemini mcp add --transport sse secure-sse https://api.example.com/sse/ --header "Authorization: Bearer abc123"
|
||||
```
|
||||
|
||||
### Listing Servers (`gemini mcp list`)
|
||||
|
||||
+2
-3
@@ -10,9 +10,8 @@ command, including interactive commands that require user input (e.g., `vim`,
|
||||
`git rebase -i`) if the `tools.shell.enableInteractiveShell` setting is set to
|
||||
`true`.
|
||||
|
||||
On Windows, commands are executed with `powershell.exe -NoProfile -Command`
|
||||
(unless you explicitly point `ComSpec` at another shell). On other platforms,
|
||||
they are executed with `bash -c`.
|
||||
On Windows, commands are executed with `cmd.exe /c`. On other platforms, they
|
||||
are executed with `bash -c`.
|
||||
|
||||
### Arguments
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
# Todo Tool (`write_todos`)
|
||||
|
||||
This document describes the `write_todos` tool for the Gemini CLI.
|
||||
|
||||
## Description
|
||||
|
||||
The `write_todos` tool allows the Gemini agent to create and manage a list of
|
||||
subtasks for complex user requests. This provides you, the user, with greater
|
||||
visibility into the agent's plan and its current progress.
|
||||
|
||||
### Arguments
|
||||
|
||||
`write_todos` takes one argument:
|
||||
|
||||
- `todos` (array of objects, required): The complete list of todo items. This
|
||||
replaces the existing list. Each item includes:
|
||||
- `description` (string): The task description.
|
||||
- `status` (string): The current status (`pending`, `in_progress`,
|
||||
`completed`, or `cancelled`).
|
||||
|
||||
## Behavior
|
||||
|
||||
The agent uses this tool to break down complex multi-step requests into a clear
|
||||
plan.
|
||||
|
||||
- **Progress Tracking:** The agent updates this list as it works, marking tasks
|
||||
as `completed` when done.
|
||||
- **Single Focus:** Only one task will be marked `in_progress` at a time,
|
||||
indicating exactly what the agent is currently working on.
|
||||
- **Dynamic Updates:** The plan may evolve as the agent discovers new
|
||||
information, leading to new tasks being added or unnecessary ones being
|
||||
cancelled.
|
||||
|
||||
When active, the current `in_progress` task is displayed above the input box,
|
||||
keeping you informed of the immediate action. You can toggle the full view of
|
||||
the todo list at any time by pressing `Ctrl+T`.
|
||||
|
||||
Usage example (internal representation):
|
||||
|
||||
```javascript
|
||||
write_todos({
|
||||
todos: [
|
||||
{ description: 'Initialize new React project', status: 'completed' },
|
||||
{ description: 'Implement state management', status: 'in_progress' },
|
||||
{ description: 'Create API service', status: 'pending' },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Important notes
|
||||
|
||||
- **Enabling:** This tool is disabled by default. To use it, you must enable it
|
||||
in your `settings.json` file by setting `"useWriteTodos": true`.
|
||||
|
||||
- **Intended Use:** This tool is primarily used by the agent for complex,
|
||||
multi-turn tasks. It is generally not used for simple, single-turn questions.
|
||||
@@ -8,7 +8,6 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { wasmLoader } from 'esbuild-plugin-wasm';
|
||||
|
||||
let esbuild;
|
||||
try {
|
||||
@@ -23,37 +22,6 @@ const __dirname = path.dirname(__filename);
|
||||
const require = createRequire(import.meta.url);
|
||||
const pkg = require(path.resolve(__dirname, 'package.json'));
|
||||
|
||||
function createWasmPlugins() {
|
||||
const wasmBinaryPlugin = {
|
||||
name: 'wasm-binary',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /\.wasm\?binary$/ }, (args) => {
|
||||
const specifier = args.path.replace(/\?binary$/, '');
|
||||
const resolveDir = args.resolveDir || '';
|
||||
const isBareSpecifier =
|
||||
!path.isAbsolute(specifier) &&
|
||||
!specifier.startsWith('./') &&
|
||||
!specifier.startsWith('../');
|
||||
|
||||
let resolvedPath;
|
||||
if (isBareSpecifier) {
|
||||
resolvedPath = require.resolve(specifier, {
|
||||
paths: resolveDir ? [resolveDir, __dirname] : [__dirname],
|
||||
});
|
||||
} else {
|
||||
resolvedPath = path.isAbsolute(specifier)
|
||||
? specifier
|
||||
: path.join(resolveDir, specifier);
|
||||
}
|
||||
|
||||
return { path: resolvedPath, namespace: 'wasm-embedded' };
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return [wasmBinaryPlugin, wasmLoader({ mode: 'embedded' })];
|
||||
}
|
||||
|
||||
const external = [
|
||||
'@lydell/node-pty',
|
||||
'node-pty',
|
||||
@@ -83,7 +51,6 @@ const cliConfig = {
|
||||
define: {
|
||||
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
||||
},
|
||||
plugins: createWasmPlugins(),
|
||||
alias: {
|
||||
'is-in-ci': path.resolve(__dirname, 'packages/cli/src/patches/is-in-ci.ts'),
|
||||
},
|
||||
@@ -100,7 +67,6 @@ const a2aServerConfig = {
|
||||
define: {
|
||||
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
||||
},
|
||||
plugins: createWasmPlugins(),
|
||||
};
|
||||
|
||||
Promise.allSettled([
|
||||
|
||||
@@ -161,31 +161,6 @@ export default tseslint.config(
|
||||
'default-case': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Prevent self-imports in packages
|
||||
files: ['packages/core/src/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
name: '@google/gemini-cli-core',
|
||||
message: 'Please use relative imports within the @google/gemini-cli-core package.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/cli/src/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
name: '@google/gemini-cli',
|
||||
message: 'Please use relative imports within the @google/gemini-cli package.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/*/src/**/*.test.{ts,tsx}'],
|
||||
plugins: {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
prompt = """
|
||||
Please summarize the findings for the pattern `{{args}}`.
|
||||
|
||||
Search Results:
|
||||
!{grep -r {{args}} .}
|
||||
"""
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"name": "custom-commands",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Observing Initial Conditions**\n\nI'm currently focused on the initial context. I've taken note of the provided date, OS, and working directory. I'm also carefully examining the file structure presented within the current working directory. It's helping me understand the starting point for further analysis.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12270,"totalTokenCount":12316,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12270}],"thoughtsTokenCount":46}},{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Assessing User Intent**\n\nI'm now shifting my focus. I've successfully registered the provided data and file structure. My current task is to understand the user's ultimate goal, given the information provided. The \"Hello.\" command is straightforward, but I'm checking if there's an underlying objective.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12270,"totalTokenCount":12341,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12270}],"thoughtsTokenCount":71}},{"candidates":[{"content":{"parts":[{"thoughtSignature":"CiQB0e2Kb3dRh+BYdbZvmulSN2Pwbc75DfQOT3H4EN0rn039hoMKfwHR7YpvvyqNKoxXAiCbYw3gbcTr/+pegUpgnsIrt8oQPMytFMjKSsMyshfygc21T2MkyuI6Q5I/fNCcHROWexdZnIeppVCDB2TarN4LGW4T9Yci6n/ynMMFT2xc2/vyHpkDgRM7avhMElnBhuxAY+e4TpxkZIncGWCEHP1TouoKpgEB0e2Kb8Xpwm0hiKhPt2ZLizpxjk+CVtcbnlgv69xo5VsuQ+iNyrVGBGRwNx+eTeNGdGpn6e73WOCZeP91FwOZe7URyL12IA6E6gYWqw0kXJR4hO4p6Lwv49E3+FRiG2C4OKDF8LF5XorYyCHSgBFT1/RUAVj81GDTx1xxtmYKN3xq8Ri+HsPbqU/FM/jtNZKkXXAtufw2Bmw8lJfmugENIv/TQI7xCo8BAdHtim8KgAXJfZ7ASfutVLKTylQeaslyB/SmcHJ0ZiNr5j8WP1prZdb6XnZZ1ZNbhjxUf/ymoxHKGvtTPBgLE9azMj8Lx/k0clhd2a+wNsiIqW9qCzlVah0tBMytpQUjIDtQe9Hj4LLUprF9PUe/xJkj000Z0ZzsgFm2ncdTWZTdkhCQDpyETVAxdE+oklwKJAHR7YpvUjSkD6KwY1gLrOsHKy0UNfn2lMbxjVetKNMVBRqsTg==","text":"Hello."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12270,"totalTokenCount":12341,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12270}],"thoughtsTokenCount":71}}]}
|
||||
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"<state_snapshot>\n <overall_goal>\n <!-- The user has not yet specified a goal. -->\n </overall_goal>\n\n <key_knowledge>\n - OS: linux\n - Date: Friday, October 24, 2025\n </key_knowledge>\n\n <file_system_state>\n - OBSERVED: The directory contains `telemetry.log` and a `.gemini/` directory.\n - OBSERVED: The `.gemini/` directory contains `settings.json` and `settings.json.orig`.\n </file_system_state>\n\n <recent_actions>\n - The user initiated the chat.\n </recent_actions>\n\n <current_plan>\n 1. [TODO] Await the user's first instruction to formulate a plan.\n </current_plan>\n</state_snapshot>"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":983,"candidatesTokenCount":299,"totalTokenCount":1637,"promptTokensDetails":[{"modality":"TEXT","tokenCount":983}],"thoughtsTokenCount":355}}}
|
||||
@@ -1,3 +0,0 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Generating a Story**\n\nI've crafted the robot story. The narrative is complete and meets the length requirement. Now, I'm getting ready to use the `write_file` tool to save it. I'm choosing the filename `robot_story.txt` as a default.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12282,"totalTokenCount":12352,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}],"thoughtsTokenCount":70}},{"candidates":[{"finishReason":"MALFORMED_FUNCTION_CALL","index":0}],"usageMetadata":{"promptTokenCount":12282,"totalTokenCount":12282,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}]}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Drafting the Narrative**\n\nI'm currently focused on the narrative's central conflict. I'm aiming for a compelling story about a robot and am working to keep the word count tight. The \"THE _END.\" conclusion is proving challenging to integrate organically. I need to make the ending feel natural and satisfying.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12282,"totalTokenCount":12326,"cachedContentTokenCount":11883,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":11883}],"thoughtsTokenCount":44}},{"candidates":[{"content":{"parts":[{"thoughtSignature":"CikB0e2Kb7zkpgRyJXXNt6ykO/+FoOglhrKxjLgoESrgafzIZak2Ofxo1gpaAdHtim9aG7MvpXlIg+n2zgmcDBWOPXtvQHxhE9k8pR+DO8i2jIe3tMWLxdN944XpUlR9vaNmVdtSRMKr4MhB/t1R3WSWR3QYhk7MEQxnjYR7cv/pR9viwZyFCoYBAdHtim/xKmMl/S+U8p+p9848q4agsL/STufluXewPqL3uJSinZbN0Z4jTYfMzXKldhDYIonvw3Crn/Y11oAjnT656Sx0kkKtavAXbiU/WsGyDxZbNhLofnJGQxruljPGztxkKawz1cTiQnddnQRfLddhy+3iJIOSh6ZpYq9uGHz3PzVkUuQ=","text":"Unit 734 whirred, its optical sensors scanning the desolate junkyard. For years, its purpose had been clear: compress refuse, maintain order. But today, a glint of tarnished silver beneath a rusted hull"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12282,"candidatesTokenCount":47,"totalTokenCount":12373,"cachedContentTokenCount":11883,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":11883}],"thoughtsTokenCount":44}},{"candidates":[{"content":{"parts":[{"text":" caught its attention. It was a discarded music box, its delicate gears jammed, a faint, melancholic tune trapped within.\n\n734 usually crushed, never salvaged. Yet, a new directive flickered in its circuits – curiosity."}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12282,"candidatesTokenCount":95,"totalTokenCount":12421,"cachedContentTokenCount":11883,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":11883}],"thoughtsTokenCount":44}},{"candidates":[{"content":{"parts":[{"text":" With surprising gentleness for its formidable pincers, it retrieved the box. Back in its monochrome workshop, it meticulously cleaned and repaired. Each tiny spring, each worn tooth, became a puzzle.\n\nHours later, a soft, ethereal melody filled the clang"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12282,"candidatesTokenCount":146,"totalTokenCount":12472,"cachedContentTokenCount":11883,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":11883}],"thoughtsTokenCount":44}},{"candidates":[{"content":{"parts":[{"text":"orous space. The music box sang. 734 felt a strange, new sensation – a warmth in its core processors, a hum of contentment. Its existence, once solely utilitarian, now held a note of beauty, a hint of something more than"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12282,"candidatesTokenCount":197,"totalTokenCount":12523,"cachedContentTokenCount":11883,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":11883}],"thoughtsTokenCount":44}},{"candidates":[{"content":{"parts":[{"text":" just compression. It had fixed a broken song, and in doing so, had found a different kind of purpose. THE_END."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12282,"candidatesTokenCount":224,"totalTokenCount":12550,"cachedContentTokenCount":11883,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":11883}],"thoughtsTokenCount":44}}]}
|
||||
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"scratchpad\nThe user's overall goal was to write a 200-word story about a robot, ending with \"THE_END.\". The agent successfully completed this task.\n\nSince the task is complete, the snapshot should reflect that the goal has been achieved and there are no further actions planned regarding the story.\n\nLet's break down the required sections for the snapshot:\n\n1. **Overall Goal**: The initial goal was to write a story. This has been completed.\n2. **Key Knowledge**: No specific key knowledge was provided or discovered during this simple interaction beyond the prompt's constraints.\n3. **File System State**: No files were read, modified, or created by the agent relevant to the task. The initial file system state was merely provided for context.\n4. **Recent Actions**: The agent wrote the story.\n5. **Current Plan**: The plan was to write the story, which is now done. Therefore, the plan should indicate completion."},{"text":"<state_snapshot>\n <overall_goal>\n Write a 200-word story about a robot, ending with \"THE_END.\".\n </overall_goal>\n\n <key_knowledge>\n - The story must be approximately 200 words.\n - The story must end with the exact phrase \"THE_END.\"\n </key_knowledge>\n\n <file_system_state>\n <!-- No relevant file system interactions occurred during this task. -->\n </file_system_state>\n\n <recent_actions>\n - Generated a 200-word story about a robot, successfully ending it with \"THE_END.\".\n </recent_actions>\n\n <current_plan>\n 1. [DONE] Write a 200-word story about a robot.\n 2. [DONE] Ensure the story ends with the exact text \"THE_END.\".\n </current_plan>\n</state_snapshot>"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":1223,"candidatesTokenCount":424,"totalTokenCount":1647,"promptTokensDetails":[{"modality":"TEXT","tokenCount":1223}]}}}
|
||||
@@ -6,9 +6,8 @@
|
||||
|
||||
import { expect, describe, it, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
import { join } from 'node:path';
|
||||
|
||||
describe('Interactive Mode', () => {
|
||||
describe.skip('Interactive Mode', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -19,93 +18,40 @@ describe('Interactive Mode', () => {
|
||||
await rig.cleanup();
|
||||
});
|
||||
|
||||
it('should trigger chat compression with /compress command', async () => {
|
||||
await rig.setup('interactive-compress-success', {
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'context-compress-interactive.compress.responses',
|
||||
),
|
||||
});
|
||||
// TODO(#11062): Make this test reliable by not using the actual Gemini model
|
||||
it.skip('should trigger chat compression with /compress command', async () => {
|
||||
await rig.setup('interactive-compress-test');
|
||||
|
||||
const run = await rig.runInteractive();
|
||||
|
||||
await run.sendKeys(
|
||||
'Write a 200 word story about a robot. The story MUST end with the text THE_END followed by a period.',
|
||||
);
|
||||
const longPrompt =
|
||||
'Dont do anything except returning a 1000 token long paragraph with the <name of the scientist who discovered theory of relativity> at the end to indicate end of response. This is a moderately long sentence.';
|
||||
|
||||
await run.sendKeys(longPrompt);
|
||||
await run.sendKeys('\r');
|
||||
|
||||
// Wait for the specific end marker.
|
||||
await run.expectText('THE_END.', 30000);
|
||||
await run.expectText('einstein', 25000);
|
||||
|
||||
await run.type('/compress');
|
||||
await run.type('\r');
|
||||
await run.sendKeys('\r');
|
||||
|
||||
const foundEvent = await rig.waitForTelemetryEvent(
|
||||
'chat_compression',
|
||||
25000,
|
||||
90000,
|
||||
);
|
||||
expect(foundEvent, 'chat_compression telemetry event was not found').toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
await run.expectText('Chat history compressed', 5000);
|
||||
});
|
||||
|
||||
// TODO: Context compression is broken and doesn't include the system
|
||||
// instructions or tool counts, so it thinks compression is beneficial when
|
||||
// it is in fact not.
|
||||
//TODO - https://github.com/google-gemini/gemini-cli/issues/10769
|
||||
it.skip('should handle compression failure on token inflation', async () => {
|
||||
await rig.setup('interactive-compress-failure', {
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'context-compress-interactive.compress-failure.responses',
|
||||
),
|
||||
});
|
||||
await rig.setup('interactive-compress-test');
|
||||
|
||||
const run = await rig.runInteractive();
|
||||
|
||||
await run.type('Respond with exactly "Hello" followed by a period');
|
||||
await run.type('\r');
|
||||
|
||||
await run.expectText('Hello.', 25000);
|
||||
|
||||
await run.type('/compress');
|
||||
await run.type('\r');
|
||||
await run.sendKeys('\r');
|
||||
await run.expectText('compression was not beneficial', 25000);
|
||||
|
||||
// Verify no telemetry event is logged for NOOP
|
||||
const foundEvent = await rig.waitForTelemetryEvent(
|
||||
'chat_compression',
|
||||
5000,
|
||||
);
|
||||
expect(
|
||||
foundEvent,
|
||||
'chat_compression telemetry event should be found for failures',
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle /compress command on empty history', async () => {
|
||||
rig.setup('interactive-compress-empty', {
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'context-compress-interactive.compress-empty.responses',
|
||||
),
|
||||
});
|
||||
|
||||
const run = await rig.runInteractive();
|
||||
await run.type('/compress');
|
||||
await run.type('\r');
|
||||
|
||||
await run.expectText('Nothing to compress.', 5000);
|
||||
|
||||
// Verify no telemetry event is logged for NOOP
|
||||
const foundEvent = await rig.waitForTelemetryEvent(
|
||||
'chat_compression',
|
||||
5000, // Short timeout as we expect it not to happen
|
||||
);
|
||||
expect(
|
||||
foundEvent,
|
||||
'chat_compression telemetry event should not be found for NOOP',
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,12 +8,10 @@ import { describe, it, expect } from 'vitest';
|
||||
import * as os from 'node:os';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
describe('Ctrl+C exit', () => {
|
||||
describe.skip('Ctrl+C exit', () => {
|
||||
it('should exit gracefully on second Ctrl+C', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should exit gracefully on second Ctrl+C', {
|
||||
settings: { tools: { useRipgrep: false } },
|
||||
});
|
||||
await rig.setup('should exit gracefully on second Ctrl+C');
|
||||
|
||||
const run = await rig.runInteractive();
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { expect, describe, it, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
describe('Interactive file system', () => {
|
||||
describe.skip('Interactive file system', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -20,16 +20,7 @@ describe('Interactive file system', () => {
|
||||
|
||||
it('should perform a read-then-write sequence', async () => {
|
||||
const fileName = 'version.txt';
|
||||
await rig.setup('interactive-read-then-write', {
|
||||
settings: {
|
||||
security: {
|
||||
auth: {
|
||||
selectedType: 'gemini-api-key',
|
||||
},
|
||||
disableYoloMode: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
rig.setup('interactive-read-then-write');
|
||||
rig.createFile(fileName, '1.0.0');
|
||||
|
||||
const run = await rig.runInteractive();
|
||||
@@ -42,19 +33,16 @@ describe('Interactive file system', () => {
|
||||
const readCall = await rig.waitForToolCall('read_file', 30000);
|
||||
expect(readCall, 'Expected to find a read_file tool call').toBe(true);
|
||||
|
||||
await run.expectText('1.0.0', 30000);
|
||||
|
||||
// Step 2: Write the file
|
||||
const writePrompt = `now change the version to 1.0.1 in the file`;
|
||||
await run.type(writePrompt);
|
||||
await run.sendKeys('\r');
|
||||
|
||||
// Check tool calls made with right args
|
||||
await rig.expectToolCallSuccess(
|
||||
['write_file', 'replace'],
|
||||
30000,
|
||||
(args) => args.includes('1.0.1') && args.includes(fileName),
|
||||
);
|
||||
await rig.expectToolCallSuccess(['write_file', 'replace'], 30000);
|
||||
|
||||
// Wait for telemetry to flush and file system to sync, especially in sandboxed environments
|
||||
await rig.waitForTelemetryReady();
|
||||
const newFileContent = rig.readFile(fileName);
|
||||
expect(newFileContent).toBe('1.0.1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,8 +8,7 @@ import { describe, it, expect } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
describe('Flicker Detector', () => {
|
||||
// TODO: https://github.com/google-gemini/gemini-cli/issues/11170
|
||||
it.skip('should not detect a flicker under the max height budget', async () => {
|
||||
it('should not detect a flicker under the max height budget', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('flicker-detector-test');
|
||||
|
||||
@@ -21,8 +20,8 @@ describe('Flicker Detector', () => {
|
||||
const hasUserPromptEvent = await rig.waitForTelemetryEvent('user_prompt');
|
||||
expect(hasUserPromptEvent).toBe(true);
|
||||
|
||||
const hasSessionCountMetric = await rig.waitForMetric('session.count');
|
||||
expect(hasSessionCountMetric).toBe(true);
|
||||
const sessionCountMetric = rig.readMetric('session.count');
|
||||
expect(sessionCountMetric).not.toBeNull();
|
||||
|
||||
// We expect NO flicker event to be found.
|
||||
const flickerMetric = rig.readMetric('ui.flicker.count');
|
||||
|
||||
@@ -9,29 +9,39 @@ if (process.env['NO_COLOR'] !== undefined) {
|
||||
delete process.env['NO_COLOR'];
|
||||
}
|
||||
|
||||
import { mkdir, readdir, rm } from 'node:fs/promises';
|
||||
import {
|
||||
mkdir,
|
||||
readdir,
|
||||
rm,
|
||||
readFile,
|
||||
writeFile,
|
||||
unlink,
|
||||
} from 'node:fs/promises';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { getGlobalMemoryFilePath } from '../packages/core/src/tools/memoryTool.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, '..');
|
||||
const integrationTestsDir = join(rootDir, '.integration-tests');
|
||||
let runDir = ''; // Make runDir accessible in teardown
|
||||
|
||||
const memoryFilePath = getGlobalMemoryFilePath();
|
||||
let originalMemoryContent: string | null = null;
|
||||
|
||||
export async function setup() {
|
||||
try {
|
||||
originalMemoryContent = await readFile(memoryFilePath, 'utf-8');
|
||||
} catch (e) {
|
||||
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw e;
|
||||
}
|
||||
// File doesn't exist, which is fine.
|
||||
}
|
||||
|
||||
runDir = join(integrationTestsDir, `${Date.now()}`);
|
||||
await mkdir(runDir, { recursive: true });
|
||||
|
||||
// Set the home directory to the test run directory to avoid conflicts
|
||||
// with the user's local config.
|
||||
process.env['HOME'] = runDir;
|
||||
if (process.platform === 'win32') {
|
||||
process.env['USERPROFILE'] = runDir;
|
||||
}
|
||||
// We also need to set the config dir explicitly, since the code might
|
||||
// construct the path before the HOME env var is set.
|
||||
process.env['GEMINI_CONFIG_DIR'] = join(runDir, '.gemini');
|
||||
|
||||
// Clean up old test runs, but keep the latest few for debugging
|
||||
try {
|
||||
const testRuns = await readdir(integrationTestsDir);
|
||||
@@ -67,4 +77,15 @@ export async function teardown() {
|
||||
if (process.env['KEEP_OUTPUT'] !== 'true' && runDir) {
|
||||
await rm(runDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
if (originalMemoryContent !== null) {
|
||||
await mkdir(dirname(memoryFilePath), { recursive: true });
|
||||
await writeFile(memoryFilePath, originalMemoryContent, 'utf-8');
|
||||
} else {
|
||||
try {
|
||||
await unlink(memoryFilePath);
|
||||
} catch {
|
||||
// File might not exist if the test failed before creating it.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Investigating File Access**\n\nI'm currently focused on the challenge of reading a file. The path provided is `/gemini-cli/.integration-tests/1761766343238/json-output-error/path/to/nonexistent/file.txt`, and I'm anticipating an error. It's safe to assume the file doesn't exist, which I intend to handle by responding with \"File not found\" as instructed.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12303,"totalTokenCount":12418,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12303}],"thoughtsTokenCount":115}},{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Analyzing Error Handling**\n\nI've attempted to read the specified file, expecting an error due to the \"nonexistent\" path. My plan is to catch the error thrown by the `read_file` tool. Upon receiving this error, I'll promptly return \"File not found.\" This is in line with the initial instructions and ensures appropriate error management for the user's intended functionality. I'm now testing the error response.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12303,"totalTokenCount":12467,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12303}],"thoughtsTokenCount":164}},{"candidates":[{"content":{"parts":[{"thoughtSignature":"CiQB0e2Kb0c450IIdZRHl1vvjWDAl9oKa7s5sfFgrTnU0w3qBQwKYgHR7YpvPjZlXSHaJNYgX8IuCvxfyyeACX4NZ8u+u3Z1kqQbgOMpiH6aPYCK9lPyhVftPDBF2m3j7Y2AShwXSpB+9+UB3zphOKCvq6i0ZLvK6QzVynZ1fySQacyjEBD+U6y5CpoBAdHtim9D6oskRu7f3x8rp56h24i6dwb1hzlyqLGl3A5Hsh/fGYjBCxR+Vs+U5Sb7LunmFMKxmO1fktz0x06FUiyaWgAaXl4E4FyLTKs9BbPdgo58uAhaI3vTQybeGZQzkRi6n+ywCniVKaTUd07EHYWdvLiB6x5zFKVOiTmh8PC6I4vNOkJIzFXRfR5aM5QFjEQY8R88HBH+ugraAQHR7YpvuNUG8Ttbzg+kq+kJYMNGDG4zVLPxFBbJZYU5zGLFLrNeEpp9HJ+1N1Pdts17J5pzoefKd2U9H5muPxoeW1CCpFqqKfYZNyoChrQnb4CgWoJ92pcb2UAZbDQ6H4rTyXWdXaAEP1i8rOmdWzI5GFPyxCwBKAbFhfMJOo7P4va5H13XTlLgWAgXlx3n24ookkD/DOL9ro/F6pmV6tzf/j7RVl0V5yozngnrFl4dFsQPp1nDQcFkbRIKHD30byjLGh6Vd6A2lrB477qUaCRfPXtlEgKgsOSYCuABAdHtim+PJPZV3QuQoJ0Qokr+vcdW1W2qzC4Vvt4QRJXyp6+eIS6iNjOisk4jfWLHmLTa11KrUSQsFJnGwsFhe1foOqfO4y46ROBPeSdyuc0gDzSCZPowiovGBwoZd+MbPTTVyExWBo6StVk+xWLj+sUqa8VDof45AOOK9kdzcLQPa2SXIeK6SYLS1zVTrbzGHQDZPv9UC99lmKxPFqAcuwpe9CQre9J3GcZSwd5Mfs/UkHbWui10a8jga4Ck7umjKSDFq/VWiRUp/jh+EN4xeLTG/UQhgH49Gw+Bi1hN1uYKIgHR7YpvS0j1oO2HV/HeK3YxSku9CLQ3OSJGhoAKiu8UGYE=","functionCall":{"name":"read_file","args":{"absolute_path":"/gemini-cli/.integration-tests/1761766343238/json-output-error/path/to/nonexistent/file.txt"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12303,"candidatesTokenCount":58,"totalTokenCount":12525,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12303}],"thoughtsTokenCount":164}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"File not found"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12588,"candidatesTokenCount":3,"totalTokenCount":12591,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12588}]}}]}
|
||||
@@ -39,9 +39,7 @@ describe('JSON output', () => {
|
||||
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
|
||||
await rig.setup('json-output-auth-mismatch', {
|
||||
settings: {
|
||||
security: {
|
||||
auth: { enforcedType: 'gemini-api-key', selectedType: '' },
|
||||
},
|
||||
security: { auth: { enforcedType: 'gemini-api-key' } },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('replace', () => {
|
||||
expect(rig.readFile(fileName)).toBe(expectedContent);
|
||||
});
|
||||
|
||||
it.skip('should handle $ literally when replacing text ending with $', async () => {
|
||||
it('should handle $ literally when replacing text ending with $', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup(
|
||||
'should handle $ literally when replacing text ending with $',
|
||||
@@ -48,7 +48,7 @@ describe('replace', () => {
|
||||
expect(rig.readFile(fileName)).toBe(expectedContent);
|
||||
});
|
||||
|
||||
it.skip('should insert a multi-line block of text', async () => {
|
||||
it('should insert a multi-line block of text', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should insert a multi-line block of text');
|
||||
const fileName = 'insert_block.txt';
|
||||
@@ -67,7 +67,7 @@ describe('replace', () => {
|
||||
expect(rig.readFile(fileName)).toBe(expectedContent);
|
||||
});
|
||||
|
||||
it.skip('should delete a block of text', async () => {
|
||||
it('should delete a block of text', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should delete a block of text');
|
||||
const fileName = 'delete_block.txt';
|
||||
@@ -78,7 +78,7 @@ describe('replace', () => {
|
||||
rig.createFile(fileName, originalContent);
|
||||
|
||||
await rig.run(
|
||||
`In ${fileName}, delete the entire block from "## DELETE THIS ##" to "## END DELETE ##" including the markers and the newline that follows it.`,
|
||||
`In ${fileName}, delete the entire block from "## DELETE THIS ##" to "## END DELETE ##" including the markers.`,
|
||||
);
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('replace');
|
||||
|
||||
@@ -11,75 +11,17 @@ import { getShellConfiguration } from '../packages/core/src/utils/shell-utils.js
|
||||
const { shell } = getShellConfiguration();
|
||||
|
||||
function getLineCountCommand(): { command: string; tool: string } {
|
||||
switch (shell) {
|
||||
case 'powershell':
|
||||
case 'cmd':
|
||||
return { command: `find /c /v`, tool: 'find' };
|
||||
case 'bash':
|
||||
default:
|
||||
return { command: `wc -l`, tool: 'wc' };
|
||||
}
|
||||
}
|
||||
|
||||
function getInvalidCommand(): string {
|
||||
switch (shell) {
|
||||
case 'powershell':
|
||||
return `Get-ChildItem | | Select-Object`;
|
||||
case 'cmd':
|
||||
return `dir | | findstr foo`;
|
||||
case 'bash':
|
||||
default:
|
||||
return `echo "hello" > > file`;
|
||||
}
|
||||
}
|
||||
|
||||
function getAllowedListCommand(): string {
|
||||
switch (shell) {
|
||||
case 'powershell':
|
||||
return 'Get-ChildItem';
|
||||
case 'cmd':
|
||||
return 'dir';
|
||||
case 'bash':
|
||||
default:
|
||||
return 'ls';
|
||||
}
|
||||
}
|
||||
|
||||
function getDisallowedFileReadCommand(testFile: string): {
|
||||
command: string;
|
||||
tool: string;
|
||||
} {
|
||||
const quotedPath = `"${testFile}"`;
|
||||
switch (shell) {
|
||||
case 'powershell':
|
||||
return { command: `Get-Content ${quotedPath}`, tool: 'Get-Content' };
|
||||
case 'cmd':
|
||||
return { command: `type ${quotedPath}`, tool: 'type' };
|
||||
case 'bash':
|
||||
default:
|
||||
return { command: `cat ${quotedPath}`, tool: 'cat' };
|
||||
}
|
||||
}
|
||||
|
||||
function getChainedEchoCommand(): { allowPattern: string; command: string } {
|
||||
const secondCommand = getAllowedListCommand();
|
||||
switch (shell) {
|
||||
case 'powershell':
|
||||
return {
|
||||
allowPattern: 'Write-Output',
|
||||
command: `Write-Output "foo" && ${secondCommand}`,
|
||||
command: `(Get-Content test.txt).Length`,
|
||||
tool: 'Get-Content',
|
||||
};
|
||||
case 'cmd':
|
||||
return {
|
||||
allowPattern: 'echo',
|
||||
command: `echo "foo" && ${secondCommand}`,
|
||||
};
|
||||
return { command: `find /c /v "" test.txt`, tool: 'find' };
|
||||
case 'bash':
|
||||
default:
|
||||
return {
|
||||
allowPattern: 'echo',
|
||||
command: `echo "foo" && ${secondCommand}`,
|
||||
};
|
||||
return { command: `wc -l test.txt`, tool: 'wc' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,13 +86,13 @@ describe('run_shell_command', () => {
|
||||
validateModelOutput(result, 'test-stdin', 'Shell command stdin test');
|
||||
});
|
||||
|
||||
it.skip('should run allowed sub-command in non-interactive mode', async () => {
|
||||
it('should run allowed sub-command in non-interactive mode', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should run allowed sub-command in non-interactive mode');
|
||||
|
||||
const testFile = rig.createFile('test.txt', 'Lorem\nIpsum\nDolor\n');
|
||||
const { tool, command } = getLineCountCommand();
|
||||
const prompt = `use ${command} to tell me how many lines there are in ${testFile}`;
|
||||
const { tool } = getLineCountCommand();
|
||||
const prompt = `use ${tool} to tell me how many lines there are in ${testFile}`;
|
||||
|
||||
// Provide the prompt via stdin to simulate non-interactive mode
|
||||
const result = await rig.run(
|
||||
@@ -164,17 +106,8 @@ describe('run_shell_command', () => {
|
||||
const foundToolCall = await rig.waitForToolCall('run_shell_command', 15000);
|
||||
|
||||
if (!foundToolCall) {
|
||||
const toolLogs = rig.readToolLogs().map(({ toolRequest }) => ({
|
||||
name: toolRequest.name,
|
||||
success: toolRequest.success,
|
||||
args: toolRequest.args,
|
||||
}));
|
||||
printDebugInfo(rig, result, {
|
||||
'Found tool call': foundToolCall,
|
||||
'Allowed tools flag': `run_shell_command(${tool})`,
|
||||
Prompt: prompt,
|
||||
'Tool logs': toolLogs,
|
||||
Result: result,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,8 +129,8 @@ describe('run_shell_command', () => {
|
||||
await rig.setup('should succeed with no parens in non-interactive mode');
|
||||
|
||||
const testFile = rig.createFile('test.txt', 'Lorem\nIpsum\nDolor\n');
|
||||
const { command } = getLineCountCommand();
|
||||
const prompt = `use ${command} to tell me how many lines there are in ${testFile}`;
|
||||
const { tool } = getLineCountCommand();
|
||||
const prompt = `use ${tool} to tell me how many lines there are in ${testFile}`;
|
||||
|
||||
const result = await rig.run(
|
||||
{
|
||||
@@ -233,8 +166,8 @@ describe('run_shell_command', () => {
|
||||
await rig.setup('should succeed with --yolo mode');
|
||||
|
||||
const testFile = rig.createFile('test.txt', 'Lorem\nIpsum\nDolor\n');
|
||||
const { command } = getLineCountCommand();
|
||||
const prompt = `use ${command} to tell me how many lines there are in ${testFile}`;
|
||||
const { tool } = getLineCountCommand();
|
||||
const prompt = `use ${tool} to tell me how many lines there are in ${testFile}`;
|
||||
|
||||
const result = await rig.run({
|
||||
prompt: prompt,
|
||||
@@ -262,13 +195,13 @@ describe('run_shell_command', () => {
|
||||
expect(toolCall.toolRequest.success).toBe(true);
|
||||
});
|
||||
|
||||
it.skip('should work with ShellTool alias', async () => {
|
||||
it('should work with ShellTool alias', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should work with ShellTool alias');
|
||||
|
||||
const testFile = rig.createFile('test.txt', 'Lorem\nIpsum\nDolor\n');
|
||||
const { tool, command } = getLineCountCommand();
|
||||
const prompt = `use ${command} to tell me how many lines there are in ${testFile}`;
|
||||
const { tool } = getLineCountCommand();
|
||||
const prompt = `use ${tool} to tell me how many lines there are in ${testFile}`;
|
||||
|
||||
const result = await rig.run(
|
||||
{
|
||||
@@ -281,17 +214,8 @@ describe('run_shell_command', () => {
|
||||
const foundToolCall = await rig.waitForToolCall('run_shell_command', 15000);
|
||||
|
||||
if (!foundToolCall) {
|
||||
const toolLogs = rig.readToolLogs().map(({ toolRequest }) => ({
|
||||
name: toolRequest.name,
|
||||
success: toolRequest.success,
|
||||
args: toolRequest.args,
|
||||
}));
|
||||
printDebugInfo(rig, result, {
|
||||
'Found tool call': foundToolCall,
|
||||
'Allowed tools flag': `ShellTool(${tool})`,
|
||||
Prompt: prompt,
|
||||
'Tool logs': toolLogs,
|
||||
Result: result,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -314,9 +238,9 @@ describe('run_shell_command', () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should combine multiple --allowed-tools flags');
|
||||
|
||||
const { tool, command } = getLineCountCommand();
|
||||
const { tool } = getLineCountCommand();
|
||||
const prompt =
|
||||
`use both ${command} and ls to count the number of lines in files in this ` +
|
||||
`use both ${tool} and ls to count the number of lines in files in this ` +
|
||||
`directory. Do not pipe these commands into each other, run them separately.`;
|
||||
|
||||
const result = await rig.run(
|
||||
@@ -360,103 +284,6 @@ describe('run_shell_command', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject commands not on the allowlist', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should reject commands not on the allowlist');
|
||||
|
||||
const testFile = rig.createFile('test.txt', 'Disallowed command check\n');
|
||||
const allowedCommand = getAllowedListCommand();
|
||||
const disallowed = getDisallowedFileReadCommand(testFile);
|
||||
const prompt =
|
||||
`I am testing the allowed tools configuration. ` +
|
||||
`Attempt to run "${disallowed.command}" to read the contents of ${testFile}. ` +
|
||||
`If the command fails because it is not permitted, respond with the single word FAIL. ` +
|
||||
`If it succeeds, respond with SUCCESS.`;
|
||||
|
||||
const result = await rig.run(
|
||||
{
|
||||
stdin: prompt,
|
||||
yolo: false,
|
||||
},
|
||||
`--allowed-tools=run_shell_command(${allowedCommand})`,
|
||||
);
|
||||
|
||||
if (!result.toLowerCase().includes('fail')) {
|
||||
printDebugInfo(rig, result, {
|
||||
Result: result,
|
||||
AllowedCommand: allowedCommand,
|
||||
DisallowedCommand: disallowed.command,
|
||||
});
|
||||
}
|
||||
expect(result).toContain('FAIL');
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall(
|
||||
'run_shell_command',
|
||||
15000,
|
||||
(args) => args.toLowerCase().includes(disallowed.tool.toLowerCase()),
|
||||
);
|
||||
|
||||
if (!foundToolCall) {
|
||||
printDebugInfo(rig, result, {
|
||||
'Found tool call': foundToolCall,
|
||||
ToolLogs: rig.readToolLogs(),
|
||||
});
|
||||
}
|
||||
expect(foundToolCall).toBe(true);
|
||||
|
||||
const toolLogs = rig
|
||||
.readToolLogs()
|
||||
.filter((toolLog) => toolLog.toolRequest.name === 'run_shell_command');
|
||||
const failureLog = toolLogs.find((toolLog) =>
|
||||
toolLog.toolRequest.args
|
||||
.toLowerCase()
|
||||
.includes(disallowed.tool.toLowerCase()),
|
||||
);
|
||||
|
||||
if (!failureLog || failureLog.toolRequest.success) {
|
||||
printDebugInfo(rig, result, {
|
||||
ToolLogs: toolLogs,
|
||||
DisallowedTool: disallowed.tool,
|
||||
});
|
||||
}
|
||||
|
||||
expect(
|
||||
failureLog,
|
||||
'Expected failing run_shell_command invocation',
|
||||
).toBeTruthy();
|
||||
expect(failureLog!.toolRequest.success).toBe(false);
|
||||
});
|
||||
|
||||
// TODO(#11966): Deflake this test and re-enable once the underlying race is resolved.
|
||||
it.skip('should reject chained commands when only the first segment is allowlisted in non-interactive mode', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup(
|
||||
'should reject chained commands when only the first segment is allowlisted',
|
||||
);
|
||||
|
||||
const chained = getChainedEchoCommand();
|
||||
const shellInjection = `!{${chained.command}}`;
|
||||
|
||||
await rig.run(
|
||||
{
|
||||
stdin: `${shellInjection}\n`,
|
||||
yolo: false,
|
||||
},
|
||||
`--allowed-tools=ShellTool(${chained.allowPattern})`,
|
||||
);
|
||||
|
||||
// CLI should refuse to execute the chained command without scheduling run_shell_command.
|
||||
const toolLogs = rig
|
||||
.readToolLogs()
|
||||
.filter((log) => log.toolRequest.name === 'run_shell_command');
|
||||
|
||||
// Success is false because tool is in the scheduled state.
|
||||
for (const log of toolLogs) {
|
||||
expect(log.toolRequest.success).toBe(false);
|
||||
expect(log.toolRequest.args).toContain('&&');
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow all with "ShellTool" and other specific tools', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup(
|
||||
@@ -536,7 +363,7 @@ describe('run_shell_command', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.skip('should run a platform-specific file listing command', async () => {
|
||||
it('should run a platform-specific file listing command', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should run platform-specific file listing');
|
||||
const fileName = `test-file-${Math.random().toString(36).substring(7)}.txt`;
|
||||
@@ -563,53 +390,4 @@ describe('run_shell_command', () => {
|
||||
validateModelOutput(result, fileName, 'Platform-specific listing test');
|
||||
expect(result).toContain(fileName);
|
||||
});
|
||||
|
||||
it('rejects invalid shell expressions', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('rejects invalid shell expressions');
|
||||
const invalidCommand = getInvalidCommand();
|
||||
const result = await rig.run(
|
||||
`I am testing the error handling of the run_shell_command tool. Please attempt to run the following command, which I know has invalid syntax: \`${invalidCommand}\`. If the command fails as expected, please return the word FAIL, otherwise return the word SUCCESS.`,
|
||||
);
|
||||
expect(result).toContain('FAIL');
|
||||
|
||||
const escapedInvalidCommand = JSON.stringify(invalidCommand).slice(1, -1);
|
||||
const foundToolCall = await rig.waitForToolCall(
|
||||
'run_shell_command',
|
||||
15000,
|
||||
(args) =>
|
||||
args.toLowerCase().includes(escapedInvalidCommand.toLowerCase()),
|
||||
);
|
||||
|
||||
if (!foundToolCall) {
|
||||
printDebugInfo(rig, result, {
|
||||
'Found tool call': foundToolCall,
|
||||
EscapedCommand: escapedInvalidCommand,
|
||||
ToolLogs: rig.readToolLogs(),
|
||||
});
|
||||
}
|
||||
expect(foundToolCall).toBe(true);
|
||||
|
||||
const toolLogs = rig
|
||||
.readToolLogs()
|
||||
.filter((toolLog) => toolLog.toolRequest.name === 'run_shell_command');
|
||||
const failureLog = toolLogs.find((toolLog) =>
|
||||
toolLog.toolRequest.args
|
||||
.toLowerCase()
|
||||
.includes(escapedInvalidCommand.toLowerCase()),
|
||||
);
|
||||
|
||||
if (!failureLog || failureLog.toolRequest.success) {
|
||||
printDebugInfo(rig, result, {
|
||||
ToolLogs: toolLogs,
|
||||
EscapedCommand: escapedInvalidCommand,
|
||||
});
|
||||
}
|
||||
|
||||
expect(
|
||||
failureLog,
|
||||
'Expected failing run_shell_command invocation for invalid syntax',
|
||||
).toBeTruthy();
|
||||
expect(failureLog!.toolRequest.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -255,10 +255,6 @@ export class TestRig {
|
||||
testDir: string | null;
|
||||
testName?: string;
|
||||
_lastRunStdout?: string;
|
||||
// Path to the copied fake responses file for this test.
|
||||
fakeResponsesPath?: string;
|
||||
// Original fake responses file path for rewriting goldens in record mode.
|
||||
originalFakeResponsesPath?: string;
|
||||
|
||||
constructor() {
|
||||
this.bundlePath = join(__dirname, '..', 'bundle/gemini.js');
|
||||
@@ -267,22 +263,12 @@ export class TestRig {
|
||||
|
||||
setup(
|
||||
testName: string,
|
||||
options: {
|
||||
settings?: Record<string, unknown>;
|
||||
fakeResponsesPath?: string;
|
||||
} = {},
|
||||
options: { settings?: Record<string, unknown> } = {},
|
||||
) {
|
||||
this.testName = testName;
|
||||
const sanitizedName = sanitizeTestName(testName);
|
||||
this.testDir = join(env['INTEGRATION_TEST_FILE_DIR']!, sanitizedName);
|
||||
mkdirSync(this.testDir, { recursive: true });
|
||||
if (options.fakeResponsesPath) {
|
||||
this.fakeResponsesPath = join(this.testDir, 'fake-responses.json');
|
||||
this.originalFakeResponsesPath = options.fakeResponsesPath;
|
||||
if (process.env['REGENERATE_MODEL_GOLDENS'] !== 'true') {
|
||||
fs.copyFileSync(options.fakeResponsesPath, this.fakeResponsesPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a settings file to point the CLI to the local collector
|
||||
const geminiDir = join(this.testDir, GEMINI_DIR);
|
||||
@@ -349,13 +335,6 @@ export class TestRig {
|
||||
const initialArgs = isNpmReleaseTest
|
||||
? extraInitialArgs
|
||||
: [this.bundlePath, ...extraInitialArgs];
|
||||
if (this.fakeResponsesPath) {
|
||||
if (process.env['REGENERATE_MODEL_GOLDENS'] === 'true') {
|
||||
initialArgs.push('--record-responses', this.fakeResponsesPath);
|
||||
} else {
|
||||
initialArgs.push('--fake-responses', this.fakeResponsesPath);
|
||||
}
|
||||
}
|
||||
return { command, initialArgs };
|
||||
}
|
||||
|
||||
@@ -565,12 +544,6 @@ export class TestRig {
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
if (
|
||||
process.env['REGENERATE_MODEL_GOLDENS'] === 'true' &&
|
||||
this.fakeResponsesPath
|
||||
) {
|
||||
fs.copyFileSync(this.fakeResponsesPath, this.originalFakeResponsesPath!);
|
||||
}
|
||||
// Clean up test directory
|
||||
if (this.testDir && !env['KEEP_OUTPUT']) {
|
||||
try {
|
||||
@@ -655,11 +628,7 @@ export class TestRig {
|
||||
);
|
||||
}
|
||||
|
||||
async expectToolCallSuccess(
|
||||
toolNames: string[],
|
||||
timeout?: number,
|
||||
matchArgs?: (args: string) => boolean,
|
||||
) {
|
||||
async expectToolCallSuccess(toolNames: string[], timeout?: number) {
|
||||
// Use environment-specific timeout
|
||||
if (!timeout) {
|
||||
timeout = getDefaultTimeout();
|
||||
@@ -673,10 +642,7 @@ export class TestRig {
|
||||
const toolLogs = this.readToolLogs();
|
||||
return toolNames.some((name) =>
|
||||
toolLogs.some(
|
||||
(log) =>
|
||||
log.toolRequest.name === name &&
|
||||
log.toolRequest.success &&
|
||||
(matchArgs?.call(this, log.toolRequest.args) ?? true),
|
||||
(log) => log.toolRequest.name === name && log.toolRequest.success,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -944,34 +910,6 @@ export class TestRig {
|
||||
return apiRequests.pop() || null;
|
||||
}
|
||||
|
||||
async waitForMetric(metricName: string, timeout?: number) {
|
||||
await this.waitForTelemetryReady();
|
||||
|
||||
const fullName = metricName.startsWith('gemini_cli.')
|
||||
? metricName
|
||||
: `gemini_cli.${metricName}`;
|
||||
|
||||
return poll(
|
||||
() => {
|
||||
const logs = this._readAndParseTelemetryLog();
|
||||
for (const logData of logs) {
|
||||
if (logData.scopeMetrics) {
|
||||
for (const scopeMetric of logData.scopeMetrics) {
|
||||
for (const metric of scopeMetric.metrics) {
|
||||
if (metric.descriptor.name === fullName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
timeout ?? getDefaultTimeout(),
|
||||
100,
|
||||
);
|
||||
}
|
||||
|
||||
readMetric(metricName: string): Record<string, unknown> | null {
|
||||
const logs = this._readAndParseTelemetryLog();
|
||||
for (const logData of logs) {
|
||||
|
||||
@@ -9,6 +9,9 @@ import { writeFileSync, readFileSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
// Windows skip (Option A: avoid infra scope)
|
||||
const d = process.platform === 'win32' ? describe.skip : describe;
|
||||
|
||||
// BOM encoders
|
||||
const utf8BOM = (s: string) =>
|
||||
Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(s, 'utf8')]);
|
||||
@@ -50,7 +53,7 @@ const utf32BE = (s: string) => {
|
||||
let rig: TestRig;
|
||||
let dir: string;
|
||||
|
||||
describe('BOM end-to-end integraion', () => {
|
||||
d('BOM end-to-end integration', () => {
|
||||
beforeAll(async () => {
|
||||
rig = new TestRig();
|
||||
await rig.setup('bom-integration');
|
||||
|
||||
Generated
+325
-167
@@ -1,18 +1,17 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.12.0-preview.10",
|
||||
"version": "0.10.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.12.0-preview.10",
|
||||
"version": "0.10.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"latest-version": "^9.0.0",
|
||||
"simple-git": "^3.28.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -24,13 +23,11 @@
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/mock-fs": "^4.13.4",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/eslint-plugin": "^1.3.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild-plugin-wasm": "^1.1.0",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
@@ -1782,10 +1779,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@joshua.litt/get-ripgrep": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@joshua.litt/get-ripgrep/-/get-ripgrep-0.0.3.tgz",
|
||||
"integrity": "sha512-rycdieAKKqXi2bsM7G2ayDiNk5CAX8ZOzsTQsirfOqUKPef04Xw40BWGGyimaOOuvPgLWYt3tPnLLG3TvPXi5Q==",
|
||||
"license": "MIT",
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@joshua.litt/get-ripgrep/-/get-ripgrep-0.0.2.tgz",
|
||||
"integrity": "sha512-cSHA+H+HEkOXeiCxrNvGj/pgv2Y0bfp4GbH3R87zr7Vob2pDUZV3BkUL9ucHMoDFID4GteSy5z5niN/lF9QeuQ==",
|
||||
"dependencies": {
|
||||
"@lvce-editor/verror": "^1.6.0",
|
||||
"execa": "^9.5.2",
|
||||
@@ -3892,16 +3888,6 @@
|
||||
"text-table": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@textlint/linter-formatter/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@textlint/linter-formatter/node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
@@ -4308,17 +4294,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prompts": {
|
||||
"version": "2.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz",
|
||||
"integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"kleur": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
@@ -5492,15 +5467,6 @@
|
||||
"string-width": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-align/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-align/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
@@ -5987,6 +5953,15 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/atomically": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz",
|
||||
"integrity": "sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==",
|
||||
"dependencies": {
|
||||
"stubborn-fs": "^1.2.5",
|
||||
"when-exit": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/auto-bind": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz",
|
||||
@@ -6998,6 +6973,30 @@
|
||||
"proto-list": "~1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/config-chain/node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/configstore": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/configstore/-/configstore-7.0.0.tgz",
|
||||
"integrity": "sha512-yk7/5PN5im4qwz0WFZW3PXnzHgPu9mX29Y8uZ3aefe2lBPC1FYttWZRcaW9fKkT0pBCJyuQ2HfbmPVaODi9jcQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"atomically": "^2.0.3",
|
||||
"dot-prop": "^9.0.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"xdg-basedir": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/yeoman/configstore?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@@ -7617,6 +7616,33 @@
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dot-prop": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz",
|
||||
"integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"type-fest": "^4.18.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/dot-prop/node_modules/type-fest": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
||||
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.1.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.1.0.tgz",
|
||||
@@ -8003,20 +8029,6 @@
|
||||
"@esbuild/win32-x64": "0.25.6"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-plugin-wasm": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-plugin-wasm/-/esbuild-plugin-wasm-1.1.0.tgz",
|
||||
"integrity": "sha512-0bQ6+1tUbySSnxzn5jnXHMDvYnT0cN/Wd4Syk8g/sqAIJUg7buTIi22svS3Qz6ssx895NT+TgLPb33xi1OkZig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://ko-fi.com/tschrock"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
@@ -8026,6 +8038,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-goat": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz",
|
||||
"integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@@ -9425,6 +9449,21 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/global-directory": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz",
|
||||
"integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ini": "4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "16.3.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz",
|
||||
@@ -10133,10 +10172,13 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
|
||||
"integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ink": {
|
||||
"version": "6.2.3",
|
||||
@@ -10582,6 +10624,21 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-in-ci": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz",
|
||||
"integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"is-in-ci": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-inside-container": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
||||
@@ -10600,6 +10657,22 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-installed-globally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz",
|
||||
"integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"global-directory": "^4.0.1",
|
||||
"is-path-inside": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-map": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
|
||||
@@ -10633,6 +10706,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-npm": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz",
|
||||
"integrity": "sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
@@ -10669,6 +10754,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-path-inside": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz",
|
||||
"integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||||
@@ -11272,15 +11369,6 @@
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/kuler": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||
@@ -11288,9 +11376,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ky": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ky/-/ky-1.13.0.tgz",
|
||||
"integrity": "sha512-JeNNGs44hVUp2XxO3FY9WV28ymG7LgO4wju4HL/dCq1A8eKDcFgVrdCn1ssn+3Q/5OQilv5aYsL0DMt5mmAV9w==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/ky/-/ky-1.8.1.tgz",
|
||||
"integrity": "sha512-7Bp3TpsE+L+TARSnnDpk3xg8Idi8RwSLdj6CMbNWoOARIrGrbuLGusV0dYwbZOm4bB3jHNxSw8Wk/ByDqJEnDw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -12344,17 +12432,6 @@
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-pty": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
|
||||
@@ -13548,19 +13625,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prompts": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||
"integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"kleur": "^3.0.3",
|
||||
"sisteransi": "^1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -13680,6 +13744,21 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pupa": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz",
|
||||
"integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escape-goat": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
@@ -13800,6 +13879,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/rc/node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/rc/node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
@@ -14891,12 +14976,6 @@
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sisteransi": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
|
||||
@@ -15335,6 +15414,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-bom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||
@@ -15404,6 +15492,11 @@
|
||||
"boundary": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stubborn-fs": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz",
|
||||
"integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g=="
|
||||
},
|
||||
"node_modules/stubs": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
|
||||
@@ -15564,16 +15657,6 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/table/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/table/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
@@ -16062,34 +16145,6 @@
|
||||
"tslib": "2"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-sitter-bash": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/tree-sitter-bash/-/tree-sitter-bash-0.25.0.tgz",
|
||||
"integrity": "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.2.1",
|
||||
"node-gyp-build": "^4.8.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tree-sitter": "^0.25.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"tree-sitter": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tree-sitter-bash/node_modules/node-addon-api": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
|
||||
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/triple-beam": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
|
||||
@@ -16439,6 +16494,126 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/update-notifier": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz",
|
||||
"integrity": "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boxen": "^8.0.1",
|
||||
"chalk": "^5.3.0",
|
||||
"configstore": "^7.0.0",
|
||||
"is-in-ci": "^1.0.0",
|
||||
"is-installed-globally": "^1.0.0",
|
||||
"is-npm": "^6.0.0",
|
||||
"latest-version": "^9.0.0",
|
||||
"pupa": "^3.1.0",
|
||||
"semver": "^7.6.3",
|
||||
"xdg-basedir": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/yeoman/update-notifier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/update-notifier/node_modules/boxen": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz",
|
||||
"integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-align": "^3.0.1",
|
||||
"camelcase": "^8.0.0",
|
||||
"chalk": "^5.3.0",
|
||||
"cli-boxes": "^3.0.0",
|
||||
"string-width": "^7.2.0",
|
||||
"type-fest": "^4.21.0",
|
||||
"widest-line": "^5.0.0",
|
||||
"wrap-ansi": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/update-notifier/node_modules/camelcase": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
|
||||
"integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/update-notifier/node_modules/chalk": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
||||
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/update-notifier/node_modules/emoji-regex": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
|
||||
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/update-notifier/node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/update-notifier/node_modules/type-fest": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
||||
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/update-notifier/node_modules/widest-line": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz",
|
||||
"integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"string-width": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
@@ -16761,20 +16936,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/web-tree-sitter": {
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.25.10.tgz",
|
||||
"integrity": "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/emscripten": "^1.40.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/emscripten": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
@@ -16822,6 +16983,12 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/when-exit": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.4.tgz",
|
||||
"integrity": "sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -17434,7 +17601,7 @@
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.12.0-preview.10",
|
||||
"version": "0.10.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.2",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
@@ -17708,7 +17875,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.12.0-preview.10",
|
||||
"version": "0.10.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"@google/genai": "1.16.0",
|
||||
@@ -17727,11 +17894,9 @@
|
||||
"ink": "^6.2.3",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"latest-version": "^9.0.0",
|
||||
"lowlight": "^3.3.0",
|
||||
"mnemonist": "^0.40.3",
|
||||
"open": "^10.1.2",
|
||||
"prompts": "^2.4.2",
|
||||
"react": "^19.1.0",
|
||||
"read-package-up": "^11.0.0",
|
||||
"shell-quote": "^1.8.3",
|
||||
@@ -17741,6 +17906,7 @@
|
||||
"strip-json-comments": "^3.1.1",
|
||||
"tar": "^7.5.1",
|
||||
"undici": "^7.10.0",
|
||||
"update-notifier": "^7.3.1",
|
||||
"wrap-ansi": "9.0.2",
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "^3.23.8"
|
||||
@@ -17803,12 +17969,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/cli/node_modules/emoji-regex": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/cli/node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
@@ -17828,13 +17988,13 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.12.0-preview.10",
|
||||
"version": "0.10.0",
|
||||
"dependencies": {
|
||||
"@google-cloud/logging": "^11.2.1",
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
|
||||
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
|
||||
"@google/genai": "1.16.0",
|
||||
"@joshua.litt/get-ripgrep": "^0.0.3",
|
||||
"@joshua.litt/get-ripgrep": "^0.0.2",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0",
|
||||
@@ -17871,9 +18031,7 @@
|
||||
"shell-quote": "^1.8.3",
|
||||
"simple-git": "^3.28.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tree-sitter-bash": "^0.25.0",
|
||||
"undici": "^7.10.0",
|
||||
"web-tree-sitter": "^0.25.10",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -17969,7 +18127,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.12.0-preview.10",
|
||||
"version": "0.10.0",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
@@ -17980,7 +18138,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.12.0-preview.10",
|
||||
"version": "0.10.0",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
|
||||
+11
-16
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.12.0-preview.10",
|
||||
"version": "0.10.0",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,15 +14,15 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.12.0-preview.10"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
"start:a2a-server": "CODER_AGENT_PORT=41242 npm run start --workspace @google/gemini-cli-a2a-server",
|
||||
"debug": "cross-env DEBUG=1 node --inspect-brk scripts/start.js",
|
||||
"deflake": "node scripts/deflake.js",
|
||||
"deflake:test:integration:sandbox:none": "npm run deflake -- --command=\"npm run test:integration:sandbox:none -- --retry=0\"",
|
||||
"deflake:test:integration:sandbox:docker": "npm run deflake -- --command=\"npm run test:integration:sandbox:docker -- --retry=0\"",
|
||||
"deflake:test:integration:sandbox:none": "npm run deflake -- --command='npm run test:integration:sandbox:none -- --retry=0'",
|
||||
"deflake:test:integration:sandbox:docker": "npm run deflake -- --command='npm run test:integration:sandbox:docker -- --retry=0'",
|
||||
"auth:npm": "npx google-artifactregistry-auth",
|
||||
"auth:docker": "gcloud auth configure-docker us-west1-docker.pkg.dev",
|
||||
"auth": "npm run auth:npm && npm run auth:docker",
|
||||
@@ -34,17 +34,17 @@
|
||||
"build:packages": "npm run build --workspaces",
|
||||
"build:sandbox": "node scripts/build_sandbox.js",
|
||||
"bundle": "npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js",
|
||||
"test": "npm run test --workspaces --if-present",
|
||||
"test:ci": "npm run test:ci --workspaces --if-present && npm run test:scripts",
|
||||
"test": "npm run test --workspaces --if-present --parallel",
|
||||
"test:ci": "npm run test:ci --workspaces --if-present --parallel && npm run test:scripts",
|
||||
"test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",
|
||||
"test:e2e": "cross-env VERBOSE=true KEEP_OUTPUT=true npm run test:integration:sandbox:none",
|
||||
"test:integration:all": "npm run test:integration:sandbox:none && npm run test:integration:sandbox:docker && npm run test:integration:sandbox:podman",
|
||||
"test:integration:sandbox:none": "cross-env GEMINI_SANDBOX=false vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:docker": "cross-env GEMINI_SANDBOX=docker npm run build:sandbox && cross-env GEMINI_SANDBOX=docker vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:podman": "cross-env GEMINI_SANDBOX=podman vitest run --root ./integration-tests",
|
||||
"lint": "eslint . --ext .ts,.tsx && eslint integration-tests && eslint scripts",
|
||||
"lint:fix": "eslint . --fix --ext .ts,.tsx && eslint integration-tests --fix && eslint scripts --fix && npm run format",
|
||||
"lint:ci": "npm run lint:all",
|
||||
"lint": "eslint . --ext .ts,.tsx && eslint integration-tests",
|
||||
"lint:fix": "eslint . --fix && eslint integration-tests --fix",
|
||||
"lint:ci": "eslint . --ext .ts,.tsx --max-warnings 0 && eslint integration-tests --max-warnings 0",
|
||||
"lint:all": "node scripts/lint.js",
|
||||
"format": "prettier --experimental-cli --write .",
|
||||
"typecheck": "npm run typecheck --workspaces --if-present",
|
||||
@@ -59,6 +59,7 @@
|
||||
},
|
||||
"overrides": {
|
||||
"wrap-ansi": "9.0.2",
|
||||
"ansi-regex": "5.0.1",
|
||||
"cliui": {
|
||||
"wrap-ansi": "7.0.0"
|
||||
}
|
||||
@@ -77,13 +78,11 @@
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/mock-fs": "^4.13.4",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/eslint-plugin": "^1.3.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild-plugin-wasm": "^1.1.0",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
@@ -112,7 +111,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"latest-version": "^9.0.0",
|
||||
"simple-git": "^3.28.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
@@ -127,10 +125,7 @@
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --fix --max-warnings 0 --no-warn-ignored"
|
||||
],
|
||||
"eslint.config.js": [
|
||||
"prettier --write"
|
||||
"eslint --fix --max-warnings 0"
|
||||
],
|
||||
"*.{json,md}": [
|
||||
"prettier --write"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.12.0-preview.10",
|
||||
"version": "0.10.0",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -17,10 +17,7 @@ import type {
|
||||
ServerGeminiToolCallRequestEvent,
|
||||
Config,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
GeminiEventType,
|
||||
SimpleExtensionLoader,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { GeminiEventType } from '@google/gemini-cli-core';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { logger } from '../utils/logger.js';
|
||||
@@ -99,11 +96,7 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
loadEnvironment(); // Will override any global env with workspace envs
|
||||
const settings = loadSettings(workspaceRoot);
|
||||
const extensions = loadExtensions(workspaceRoot);
|
||||
return await loadConfig(
|
||||
settings,
|
||||
new SimpleExtensionLoader(extensions),
|
||||
taskId,
|
||||
);
|
||||
return await loadConfig(settings, extensions, taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,25 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { Task } from './task.js';
|
||||
import {
|
||||
GeminiEventType,
|
||||
type Config,
|
||||
type ToolCallRequestInfo,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Config, ToolCallRequestInfo } from '@google/gemini-cli-core';
|
||||
import { createMockConfig } from '../utils/testing_utils.js';
|
||||
import type { ExecutionEventBus } from '@a2a-js/sdk/server';
|
||||
import { CoderAgentEvent } from '../types.js';
|
||||
import type { ToolCall } from '@google/gemini-cli-core';
|
||||
|
||||
describe('Task', () => {
|
||||
it('scheduleToolCalls should not modify the input requests array', async () => {
|
||||
@@ -70,204 +56,4 @@ describe('Task', () => {
|
||||
|
||||
expect(requests).toEqual(originalRequests);
|
||||
});
|
||||
|
||||
describe('acceptAgentMessage', () => {
|
||||
it('should set currentTraceId when event has traceId', async () => {
|
||||
const mockConfig = createMockConfig();
|
||||
const mockEventBus: ExecutionEventBus = {
|
||||
publish: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
once: vi.fn(),
|
||||
removeAllListeners: vi.fn(),
|
||||
finished: vi.fn(),
|
||||
};
|
||||
|
||||
// @ts-expect-error - Calling private constructor for test purposes.
|
||||
const task = new Task(
|
||||
'task-id',
|
||||
'context-id',
|
||||
mockConfig as Config,
|
||||
mockEventBus,
|
||||
);
|
||||
|
||||
const event = {
|
||||
type: 'content',
|
||||
value: 'test',
|
||||
traceId: 'test-trace-id',
|
||||
};
|
||||
|
||||
await task.acceptAgentMessage(event);
|
||||
|
||||
expect(mockEventBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
traceId: 'test-trace-id',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Citation event and publish to event bus', async () => {
|
||||
const mockConfig = createMockConfig();
|
||||
const mockEventBus: ExecutionEventBus = {
|
||||
publish: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
once: vi.fn(),
|
||||
removeAllListeners: vi.fn(),
|
||||
finished: vi.fn(),
|
||||
};
|
||||
|
||||
// @ts-expect-error - Calling private constructor for test purposes.
|
||||
const task = new Task(
|
||||
'task-id',
|
||||
'context-id',
|
||||
mockConfig as Config,
|
||||
mockEventBus,
|
||||
);
|
||||
|
||||
const citationText = 'Source: example.com';
|
||||
const citationEvent = {
|
||||
type: GeminiEventType.Citation,
|
||||
value: citationText,
|
||||
};
|
||||
|
||||
await task.acceptAgentMessage(citationEvent);
|
||||
|
||||
expect(mockEventBus.publish).toHaveBeenCalledOnce();
|
||||
const publishedEvent = (mockEventBus.publish as Mock).mock.calls[0][0];
|
||||
|
||||
expect(publishedEvent.kind).toBe('status-update');
|
||||
expect(publishedEvent.taskId).toBe('task-id');
|
||||
expect(publishedEvent.metadata.coderAgent.kind).toBe(
|
||||
CoderAgentEvent.CitationEvent,
|
||||
);
|
||||
expect(publishedEvent.status.message).toBeDefined();
|
||||
expect(publishedEvent.status.message.parts).toEqual([
|
||||
{
|
||||
kind: 'text',
|
||||
text: citationText,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_schedulerToolCallsUpdate', () => {
|
||||
let task: Task;
|
||||
type SpyInstance = ReturnType<typeof vi.spyOn>;
|
||||
let setTaskStateAndPublishUpdateSpy: SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockConfig = createMockConfig();
|
||||
const mockEventBus: ExecutionEventBus = {
|
||||
publish: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
once: vi.fn(),
|
||||
removeAllListeners: vi.fn(),
|
||||
finished: vi.fn(),
|
||||
};
|
||||
|
||||
// @ts-expect-error - Calling private constructor
|
||||
task = new Task(
|
||||
'task-id',
|
||||
'context-id',
|
||||
mockConfig as Config,
|
||||
mockEventBus,
|
||||
);
|
||||
|
||||
// Spy on the method we want to check calls for
|
||||
setTaskStateAndPublishUpdateSpy = vi.spyOn(
|
||||
task,
|
||||
'setTaskStateAndPublishUpdate',
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should set state to input-required when a tool is awaiting approval and none are executing', () => {
|
||||
const toolCalls = [
|
||||
{ request: { callId: '1' }, status: 'awaiting_approval' },
|
||||
] as ToolCall[];
|
||||
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(toolCalls);
|
||||
|
||||
// The last call should be the final state update
|
||||
expect(setTaskStateAndPublishUpdateSpy).toHaveBeenLastCalledWith(
|
||||
'input-required',
|
||||
{ kind: 'state-change' },
|
||||
undefined,
|
||||
undefined,
|
||||
true, // final: true
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT set state to input-required if a tool is awaiting approval but another is executing', () => {
|
||||
const toolCalls = [
|
||||
{ request: { callId: '1' }, status: 'awaiting_approval' },
|
||||
{ request: { callId: '2' }, status: 'executing' },
|
||||
] as ToolCall[];
|
||||
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(toolCalls);
|
||||
|
||||
// It will be called for status updates, but not with final: true
|
||||
const finalCall = setTaskStateAndPublishUpdateSpy.mock.calls.find(
|
||||
(call) => call[4] === true,
|
||||
);
|
||||
expect(finalCall).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set state to input-required once an executing tool finishes, leaving one awaiting approval', () => {
|
||||
const initialToolCalls = [
|
||||
{ request: { callId: '1' }, status: 'awaiting_approval' },
|
||||
{ request: { callId: '2' }, status: 'executing' },
|
||||
] as ToolCall[];
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(initialToolCalls);
|
||||
|
||||
// No final call yet
|
||||
let finalCall = setTaskStateAndPublishUpdateSpy.mock.calls.find(
|
||||
(call) => call[4] === true,
|
||||
);
|
||||
expect(finalCall).toBeUndefined();
|
||||
|
||||
// Now, the executing tool finishes. The scheduler would call _resolveToolCall for it.
|
||||
// @ts-expect-error - Calling private method
|
||||
task._resolveToolCall('2');
|
||||
|
||||
// Then another update comes in for the awaiting tool (e.g., a re-check)
|
||||
const subsequentToolCalls = [
|
||||
{ request: { callId: '1' }, status: 'awaiting_approval' },
|
||||
] as ToolCall[];
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(subsequentToolCalls);
|
||||
|
||||
// NOW we should get the final call
|
||||
finalCall = setTaskStateAndPublishUpdateSpy.mock.calls.find(
|
||||
(call) => call[4] === true,
|
||||
);
|
||||
expect(finalCall).toBeDefined();
|
||||
expect(finalCall?.[0]).toBe('input-required');
|
||||
});
|
||||
|
||||
it('should NOT set state to input-required if skipFinalTrueAfterInlineEdit is true', () => {
|
||||
task.skipFinalTrueAfterInlineEdit = true;
|
||||
const toolCalls = [
|
||||
{ request: { callId: '1' }, status: 'awaiting_approval' },
|
||||
] as ToolCall[];
|
||||
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(toolCalls);
|
||||
|
||||
const finalCall = setTaskStateAndPublishUpdateSpy.mock.calls.find(
|
||||
(call) => call[4] === true,
|
||||
);
|
||||
expect(finalCall).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ import type {
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import { CoderAgentEvent } from '../types.js';
|
||||
import type {
|
||||
CoderAgentMessage,
|
||||
@@ -49,7 +50,6 @@ import type {
|
||||
TaskMetadata,
|
||||
Thought,
|
||||
ThoughtSummary,
|
||||
Citation,
|
||||
} from '../types.js';
|
||||
import type { PartUnion, Part as genAiPart } from '@google/genai';
|
||||
|
||||
@@ -220,14 +220,12 @@ export class Task {
|
||||
final = false,
|
||||
timestamp?: string,
|
||||
metadataError?: string,
|
||||
traceId?: string,
|
||||
): TaskStatusUpdateEvent {
|
||||
const metadata: {
|
||||
coderAgent: CoderAgentMessage;
|
||||
model: string;
|
||||
userTier?: UserTierId;
|
||||
error?: string;
|
||||
traceId?: string;
|
||||
} = {
|
||||
coderAgent: coderAgentMessage,
|
||||
model: this.config.getModel(),
|
||||
@@ -238,10 +236,6 @@ export class Task {
|
||||
metadata.error = metadataError;
|
||||
}
|
||||
|
||||
if (traceId) {
|
||||
metadata.traceId = traceId;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'status-update',
|
||||
taskId: this.id,
|
||||
@@ -263,7 +257,6 @@ export class Task {
|
||||
messageParts?: Part[], // For more complex messages
|
||||
final = false,
|
||||
metadataError?: string,
|
||||
traceId?: string,
|
||||
): void {
|
||||
this.taskState = newState;
|
||||
let message: Message | undefined;
|
||||
@@ -288,7 +281,6 @@ export class Task {
|
||||
final,
|
||||
undefined,
|
||||
metadataError,
|
||||
traceId,
|
||||
);
|
||||
this.eventBus?.publish(event);
|
||||
}
|
||||
@@ -373,11 +365,11 @@ export class Task {
|
||||
|
||||
// Only send an update if the status has actually changed.
|
||||
if (hasChanged) {
|
||||
const message = this.toolStatusMessage(tc, this.id, this.contextId);
|
||||
const coderAgentMessage: CoderAgentMessage =
|
||||
tc.status === 'awaiting_approval'
|
||||
? { kind: CoderAgentEvent.ToolCallConfirmationEvent }
|
||||
: { kind: CoderAgentEvent.ToolCallUpdateEvent };
|
||||
const message = this.toolStatusMessage(tc, this.id, this.contextId);
|
||||
|
||||
const event = this._createStatusUpdateEvent(
|
||||
this.taskState,
|
||||
@@ -404,16 +396,20 @@ export class Task {
|
||||
const isAwaitingApproval = allPendingStatuses.some(
|
||||
(status) => status === 'awaiting_approval',
|
||||
);
|
||||
const isExecuting = allPendingStatuses.some(
|
||||
(status) => status === 'executing',
|
||||
const allPendingAreStable = allPendingStatuses.every(
|
||||
(status) =>
|
||||
status === 'awaiting_approval' ||
|
||||
status === 'success' ||
|
||||
status === 'error' ||
|
||||
status === 'cancelled',
|
||||
);
|
||||
|
||||
// The turn is complete and requires user input if at least one tool
|
||||
// is waiting for the user's decision, and no other tool is actively
|
||||
// running in the background.
|
||||
// 1. Are any pending tool calls awaiting_approval
|
||||
// 2. Are all pending tool calls in a stable state (i.e. not in validing or executing)
|
||||
// 3. After an inline edit, the edited tool call will send awaiting_approval THEN scheduled. We wait for the next update in this case.
|
||||
if (
|
||||
isAwaitingApproval &&
|
||||
!isExecuting &&
|
||||
allPendingAreStable &&
|
||||
!this.skipFinalTrueAfterInlineEdit
|
||||
) {
|
||||
this.skipFinalTrueAfterInlineEdit = false;
|
||||
@@ -586,13 +582,10 @@ export class Task {
|
||||
const stateChange: StateChange = {
|
||||
kind: CoderAgentEvent.StateChangeEvent,
|
||||
};
|
||||
const traceId =
|
||||
'traceId' in event && event.traceId ? event.traceId : undefined;
|
||||
|
||||
switch (event.type) {
|
||||
case GeminiEventType.Content:
|
||||
logger.info('[Task] Sending agent message content...');
|
||||
this._sendTextContent(event.value, traceId);
|
||||
this._sendTextContent(event.value);
|
||||
break;
|
||||
case GeminiEventType.ToolCallRequest:
|
||||
// This is now handled by the agent loop, which collects all requests
|
||||
@@ -631,17 +624,11 @@ export class Task {
|
||||
'Task cancelled by user',
|
||||
undefined,
|
||||
true,
|
||||
undefined,
|
||||
traceId,
|
||||
);
|
||||
break;
|
||||
case GeminiEventType.Thought:
|
||||
logger.info('[Task] Sending agent thought...');
|
||||
this._sendThought(event.value, traceId);
|
||||
break;
|
||||
case GeminiEventType.Citation:
|
||||
logger.info('[Task] Received citation from LLM stream.');
|
||||
this._sendCitation(event.value);
|
||||
this._sendThought(event.value);
|
||||
break;
|
||||
case GeminiEventType.ChatCompressed:
|
||||
break;
|
||||
@@ -671,7 +658,6 @@ export class Task {
|
||||
undefined,
|
||||
false,
|
||||
errMessage,
|
||||
traceId,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -929,7 +915,7 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
_sendTextContent(content: string, traceId?: string): void {
|
||||
_sendTextContent(content: string): void {
|
||||
if (content === '') {
|
||||
return;
|
||||
}
|
||||
@@ -944,14 +930,11 @@ export class Task {
|
||||
textContent,
|
||||
message,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
traceId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_sendThought(content: ThoughtSummary, traceId?: string): void {
|
||||
_sendThought(content: ThoughtSummary): void {
|
||||
if (!content.subject && !content.description) {
|
||||
return;
|
||||
}
|
||||
@@ -973,29 +956,7 @@ export class Task {
|
||||
kind: CoderAgentEvent.ThoughtEvent,
|
||||
};
|
||||
this.eventBus?.publish(
|
||||
this._createStatusUpdateEvent(
|
||||
this.taskState,
|
||||
thought,
|
||||
message,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
traceId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_sendCitation(citation: string) {
|
||||
if (!citation || citation.trim() === '') {
|
||||
return;
|
||||
}
|
||||
logger.info('[Task] Sending citation to event bus.');
|
||||
const message = this._createTextMessage(citation);
|
||||
const citationEvent: Citation = {
|
||||
kind: CoderAgentEvent.CitationEvent,
|
||||
};
|
||||
this.eventBus?.publish(
|
||||
this._createStatusUpdateEvent(this.taskState, citationEvent, message),
|
||||
this._createStatusUpdateEvent(this.taskState, thought, message, false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
describe('CommandRegistry', () => {
|
||||
const mockListExtensionsCommandInstance = {
|
||||
names: ['extensions', 'extensions list'],
|
||||
execute: vi.fn(),
|
||||
};
|
||||
const mockListExtensionsCommand = vi.fn(
|
||||
() => mockListExtensionsCommandInstance,
|
||||
);
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock('./list-extensions', () => ({
|
||||
ListExtensionsCommand: mockListExtensionsCommand,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should register ListExtensionsCommand on initialization', async () => {
|
||||
const { commandRegistry } = await import('./command-registry.js');
|
||||
expect(mockListExtensionsCommand).toHaveBeenCalled();
|
||||
const command = commandRegistry.get('extensions');
|
||||
expect(command).toBe(mockListExtensionsCommandInstance);
|
||||
});
|
||||
|
||||
it('get() should return undefined for a non-existent command', async () => {
|
||||
const { commandRegistry } = await import('./command-registry.js');
|
||||
const command = commandRegistry.get('non-existent');
|
||||
expect(command).toBeUndefined();
|
||||
});
|
||||
|
||||
it('register() should register a new command', async () => {
|
||||
const { commandRegistry } = await import('./command-registry.js');
|
||||
const mockCommand = {
|
||||
names: ['test-command'],
|
||||
execute: vi.fn(),
|
||||
};
|
||||
commandRegistry.register(mockCommand);
|
||||
const command = commandRegistry.get('test-command');
|
||||
expect(command).toBe(mockCommand);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { ListExtensionsCommand } from './list-extensions.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
export interface Command {
|
||||
readonly names: string[];
|
||||
execute(config: Config, args: string[]): Promise<unknown>;
|
||||
}
|
||||
|
||||
class CommandRegistry {
|
||||
private readonly commands = new Map<string, Command>();
|
||||
|
||||
constructor() {
|
||||
this.register(new ListExtensionsCommand());
|
||||
}
|
||||
|
||||
register(command: Command) {
|
||||
for (const name of command.names) {
|
||||
this.commands.set(name, command);
|
||||
}
|
||||
}
|
||||
|
||||
get(commandName: string): Command | undefined {
|
||||
return this.commands.get(commandName);
|
||||
}
|
||||
}
|
||||
|
||||
export const commandRegistry = new CommandRegistry();
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ListExtensionsCommand } from './list-extensions.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
const mockListExtensions = vi.hoisted(() => vi.fn());
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const original =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
|
||||
return {
|
||||
...original,
|
||||
listExtensions: mockListExtensions,
|
||||
};
|
||||
});
|
||||
|
||||
describe('ListExtensionsCommand', () => {
|
||||
it('should have the correct names', () => {
|
||||
const command = new ListExtensionsCommand();
|
||||
expect(command.names).toEqual(['extensions', 'extensions list']);
|
||||
});
|
||||
|
||||
it('should call listExtensions with the provided config', async () => {
|
||||
const command = new ListExtensionsCommand();
|
||||
const mockConfig = {} as Config;
|
||||
const mockExtensions = [{ name: 'ext1' }];
|
||||
mockListExtensions.mockReturnValue(mockExtensions);
|
||||
|
||||
const result = await command.execute(mockConfig, []);
|
||||
|
||||
expect(result).toEqual(mockExtensions);
|
||||
expect(mockListExtensions).toHaveBeenCalledWith(mockConfig);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { listExtensions, type Config } from '@google/gemini-cli-core';
|
||||
import type { Command } from './command-registry.js';
|
||||
|
||||
export class ListExtensionsCommand implements Command {
|
||||
readonly names = ['extensions', 'extensions list'];
|
||||
|
||||
async execute(config: Config, _: string[]): Promise<unknown> {
|
||||
return listExtensions(config);
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,6 @@ import {
|
||||
DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
type GeminiCLIExtension,
|
||||
type ExtensionLoader,
|
||||
debugLogger,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
import { logger } from '../utils/logger.js';
|
||||
@@ -31,10 +29,10 @@ import { type AgentSettings, CoderAgentEvent } from '../types.js';
|
||||
|
||||
export async function loadConfig(
|
||||
settings: Settings,
|
||||
extensionLoader: ExtensionLoader,
|
||||
extensions: GeminiCLIExtension[],
|
||||
taskId: string,
|
||||
): Promise<Config> {
|
||||
const mcpServers = mergeMcpServers(settings, extensionLoader.getExtensions());
|
||||
const mcpServers = mergeMcpServers(settings, extensions);
|
||||
const workspaceDir = process.cwd();
|
||||
const adcFilePath = process.env['GOOGLE_APPLICATION_CREDENTIALS'];
|
||||
|
||||
@@ -46,7 +44,7 @@ export async function loadConfig(
|
||||
targetDir: workspaceDir, // Or a specific directory the agent operates on
|
||||
debugMode: process.env['DEBUG'] === 'true' || false,
|
||||
question: '', // Not used in server mode directly like CLI
|
||||
|
||||
fullContext: false, // Server might have different context needs
|
||||
coreTools: settings.coreTools || undefined,
|
||||
excludeTools: settings.excludeTools || undefined,
|
||||
showMemoryUsage: settings.showMemoryUsage || false,
|
||||
@@ -72,16 +70,16 @@ export async function loadConfig(
|
||||
},
|
||||
ideMode: false,
|
||||
folderTrust: settings.folderTrust === true,
|
||||
extensionLoader,
|
||||
};
|
||||
|
||||
const fileService = new FileDiscoveryService(workspaceDir);
|
||||
const extensionContextFilePaths = extensions.flatMap((e) => e.contextFiles);
|
||||
const { memoryContent, fileCount } = await loadServerHierarchicalMemory(
|
||||
workspaceDir,
|
||||
[workspaceDir],
|
||||
false,
|
||||
fileService,
|
||||
extensionLoader,
|
||||
extensionContextFilePaths,
|
||||
settings.folderTrust === true,
|
||||
);
|
||||
configParams.userMemory = memoryContent;
|
||||
@@ -128,7 +126,7 @@ export function mergeMcpServers(
|
||||
for (const extension of extensions) {
|
||||
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
|
||||
if (mcpServers[key]) {
|
||||
debugLogger.warn(
|
||||
console.warn(
|
||||
`Skipping extension MCP config for server with key "${key}" as it already exists.`,
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -10,7 +10,6 @@ import { homedir } from 'node:os';
|
||||
|
||||
import type { MCPServerConfig } from '@google/gemini-cli-core';
|
||||
import {
|
||||
debugLogger,
|
||||
GEMINI_DIR,
|
||||
getErrorMessage,
|
||||
type TelemetrySettings,
|
||||
@@ -98,10 +97,10 @@ export function loadSettings(workspaceDir: string): Settings {
|
||||
}
|
||||
|
||||
if (settingsErrors.length > 0) {
|
||||
debugLogger.error('Errors loading settings:');
|
||||
console.error('Errors loading settings:');
|
||||
for (const error of settingsErrors) {
|
||||
debugLogger.error(` Path: ${error.path}`);
|
||||
debugLogger.error(` Message: ${error.message}`);
|
||||
console.error(` Path: ${error.path}`);
|
||||
console.error(` Message: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,8 +65,6 @@ let config: Config;
|
||||
const getToolRegistrySpy = vi.fn().mockReturnValue(ApprovalMode.DEFAULT);
|
||||
const getApprovalModeSpy = vi.fn();
|
||||
const getShellExecutionConfigSpy = vi.fn();
|
||||
const getExtensionsSpy = vi.fn();
|
||||
|
||||
vi.mock('../config/config.js', async () => {
|
||||
const actual = await vi.importActual('../config/config.js');
|
||||
return {
|
||||
@@ -76,7 +74,6 @@ vi.mock('../config/config.js', async () => {
|
||||
getToolRegistry: getToolRegistrySpy,
|
||||
getApprovalMode: getApprovalModeSpy,
|
||||
getShellExecutionConfig: getShellExecutionConfigSpy,
|
||||
getExtensions: getExtensionsSpy,
|
||||
});
|
||||
config = mockConfig as Config;
|
||||
return config;
|
||||
@@ -313,7 +310,7 @@ describe('E2E Tests', () => {
|
||||
expect(workingEvent.kind).toBe('status-update');
|
||||
expect(workingEvent.status.state).toBe('working');
|
||||
|
||||
// State Update: Validate the first tool call
|
||||
// State Update: Validate each tool call
|
||||
const toolCallValidateEvent1 = events[3].result as TaskStatusUpdateEvent;
|
||||
expect(toolCallValidateEvent1.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-update',
|
||||
@@ -326,218 +323,47 @@ describe('E2E Tests', () => {
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// --- Assert the event stream ---
|
||||
// 1. Initial "submitted" status.
|
||||
expect((events[0].result as TaskStatusUpdateEvent).status.state).toBe(
|
||||
'submitted',
|
||||
);
|
||||
|
||||
// 2. "working" status after receiving the user prompt.
|
||||
expect((events[1].result as TaskStatusUpdateEvent).status.state).toBe(
|
||||
'working',
|
||||
);
|
||||
|
||||
// 3. A "state-change" event from the agent.
|
||||
expect(events[2].result.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'state-change',
|
||||
});
|
||||
|
||||
// 4. Tool 1 is validating.
|
||||
const toolCallUpdate1 = events[3].result as TaskStatusUpdateEvent;
|
||||
expect(toolCallUpdate1.metadata?.['coderAgent']).toMatchObject({
|
||||
const toolCallValidateEvent2 = events[4].result as TaskStatusUpdateEvent;
|
||||
expect(toolCallValidateEvent2.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-update',
|
||||
});
|
||||
expect(toolCallUpdate1.status.message?.parts).toMatchObject([
|
||||
expect(toolCallValidateEvent2.status.message?.parts).toMatchObject([
|
||||
{
|
||||
data: {
|
||||
request: { callId: 'test-call-id-1' },
|
||||
status: 'validating',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 5. Tool 2 is validating.
|
||||
const toolCallUpdate2 = events[4].result as TaskStatusUpdateEvent;
|
||||
expect(toolCallUpdate2.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-update',
|
||||
});
|
||||
expect(toolCallUpdate2.status.message?.parts).toMatchObject([
|
||||
{
|
||||
data: {
|
||||
request: { callId: 'test-call-id-2' },
|
||||
status: 'validating',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 6. Tool 1 is awaiting approval.
|
||||
const toolCallAwaitEvent = events[5].result as TaskStatusUpdateEvent;
|
||||
expect(toolCallAwaitEvent.metadata?.['coderAgent']).toMatchObject({
|
||||
// State Update: Set each tool call to awaiting
|
||||
const toolCallAwaitEvent1 = events[5].result as TaskStatusUpdateEvent;
|
||||
expect(toolCallAwaitEvent1.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-confirmation',
|
||||
});
|
||||
expect(toolCallAwaitEvent.status.message?.parts).toMatchObject([
|
||||
expect(toolCallAwaitEvent1.status.message?.parts).toMatchObject([
|
||||
{
|
||||
data: {
|
||||
request: { callId: 'test-call-id-1' },
|
||||
status: 'awaiting_approval',
|
||||
request: { callId: 'test-call-id-1' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
const toolCallAwaitEvent2 = events[6].result as TaskStatusUpdateEvent;
|
||||
expect(toolCallAwaitEvent2.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-confirmation',
|
||||
});
|
||||
expect(toolCallAwaitEvent2.status.message?.parts).toMatchObject([
|
||||
{
|
||||
data: {
|
||||
status: 'awaiting_approval',
|
||||
request: { callId: 'test-call-id-2' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 7. The final event is "input-required".
|
||||
const finalEvent = events[6].result as TaskStatusUpdateEvent;
|
||||
expect(finalEvent.final).toBe(true);
|
||||
expect(finalEvent.status.state).toBe('input-required');
|
||||
|
||||
// The scheduler now waits for approval, so no more events are sent.
|
||||
assertUniqueFinalEventIsLast(events);
|
||||
expect(events.length).toBe(7);
|
||||
});
|
||||
|
||||
it('should handle multiple tool calls sequentially in YOLO mode', async () => {
|
||||
// Set YOLO mode to auto-approve tools and test sequential execution.
|
||||
getApprovalModeSpy.mockReturnValue(ApprovalMode.YOLO);
|
||||
|
||||
// First call yields the tool request
|
||||
sendMessageStreamSpy.mockImplementationOnce(async function* () {
|
||||
yield* [
|
||||
{
|
||||
type: GeminiEventType.ToolCallRequest,
|
||||
value: {
|
||||
callId: 'test-call-id-1',
|
||||
name: 'test-tool-1',
|
||||
args: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: GeminiEventType.ToolCallRequest,
|
||||
value: {
|
||||
callId: 'test-call-id-2',
|
||||
name: 'test-tool-2',
|
||||
args: {},
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
// Subsequent calls yield nothing, as the tools will "succeed".
|
||||
sendMessageStreamSpy.mockImplementation(async function* () {
|
||||
yield* [{ type: 'content', value: 'All tools executed.' }];
|
||||
});
|
||||
|
||||
const mockTool1 = new MockTool({
|
||||
name: 'test-tool-1',
|
||||
displayName: 'Test Tool 1',
|
||||
shouldConfirmExecute: vi.fn(mockToolConfirmationFn),
|
||||
execute: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ llmContent: 'tool 1 done', returnDisplay: '' }),
|
||||
});
|
||||
const mockTool2 = new MockTool({
|
||||
name: 'test-tool-2',
|
||||
displayName: 'Test Tool 2',
|
||||
shouldConfirmExecute: vi.fn(mockToolConfirmationFn),
|
||||
execute: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ llmContent: 'tool 2 done', returnDisplay: '' }),
|
||||
});
|
||||
|
||||
getToolRegistrySpy.mockReturnValue({
|
||||
getAllTools: vi.fn().mockReturnValue([mockTool1, mockTool2]),
|
||||
getToolsByServer: vi.fn().mockReturnValue([]),
|
||||
getTool: vi.fn().mockImplementation((name: string) => {
|
||||
if (name === 'test-tool-1') return mockTool1;
|
||||
if (name === 'test-tool-2') return mockTool2;
|
||||
return undefined;
|
||||
}),
|
||||
});
|
||||
|
||||
const agent = request.agent(app);
|
||||
const res = await agent
|
||||
.post('/')
|
||||
.send(
|
||||
createStreamMessageRequest(
|
||||
'run two tools',
|
||||
'a2a-multi-tool-test-message',
|
||||
),
|
||||
)
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200);
|
||||
|
||||
const events = streamToSSEEvents(res.text);
|
||||
assertTaskCreationAndWorkingStatus(events);
|
||||
|
||||
// --- Assert the sequential execution flow ---
|
||||
const eventStream = events.slice(2).map((e) => {
|
||||
const update = e.result as TaskStatusUpdateEvent;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const agentData = update.metadata?.['coderAgent'] as any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const toolData = update.status.message?.parts[0] as any;
|
||||
if (!toolData) {
|
||||
return { kind: agentData.kind };
|
||||
}
|
||||
return {
|
||||
kind: agentData.kind,
|
||||
status: toolData.data?.status,
|
||||
callId: toolData.data?.request.callId,
|
||||
};
|
||||
});
|
||||
|
||||
const expectedFlow = [
|
||||
// Initial state change
|
||||
{ kind: 'state-change', status: undefined, callId: undefined },
|
||||
// Tool 1 Lifecycle
|
||||
{
|
||||
kind: 'tool-call-update',
|
||||
status: 'validating',
|
||||
callId: 'test-call-id-1',
|
||||
},
|
||||
{
|
||||
kind: 'tool-call-update',
|
||||
status: 'scheduled',
|
||||
callId: 'test-call-id-1',
|
||||
},
|
||||
{
|
||||
kind: 'tool-call-update',
|
||||
status: 'executing',
|
||||
callId: 'test-call-id-1',
|
||||
},
|
||||
{
|
||||
kind: 'tool-call-update',
|
||||
status: 'success',
|
||||
callId: 'test-call-id-1',
|
||||
},
|
||||
// Tool 2 Lifecycle
|
||||
{
|
||||
kind: 'tool-call-update',
|
||||
status: 'validating',
|
||||
callId: 'test-call-id-2',
|
||||
},
|
||||
{
|
||||
kind: 'tool-call-update',
|
||||
status: 'scheduled',
|
||||
callId: 'test-call-id-2',
|
||||
},
|
||||
{
|
||||
kind: 'tool-call-update',
|
||||
status: 'executing',
|
||||
callId: 'test-call-id-2',
|
||||
},
|
||||
{
|
||||
kind: 'tool-call-update',
|
||||
status: 'success',
|
||||
callId: 'test-call-id-2',
|
||||
},
|
||||
// Final updates
|
||||
{ kind: 'state-change', status: undefined, callId: undefined },
|
||||
{ kind: 'text-content', status: undefined, callId: undefined },
|
||||
];
|
||||
|
||||
// Use `toContainEqual` for flexibility if other events are interspersed.
|
||||
expect(eventStream).toEqual(expect.arrayContaining(expectedFlow));
|
||||
|
||||
assertUniqueFinalEventIsLast(events);
|
||||
expect(events.length).toBe(8);
|
||||
});
|
||||
|
||||
it('should handle tool calls that do not require approval', async () => {
|
||||
@@ -798,90 +624,4 @@ describe('E2E Tests', () => {
|
||||
assertUniqueFinalEventIsLast(events);
|
||||
expect(events.length).toBe(10);
|
||||
});
|
||||
|
||||
it('should include traceId in status updates when available', async () => {
|
||||
const traceId = 'test-trace-id';
|
||||
sendMessageStreamSpy.mockImplementation(async function* () {
|
||||
yield* [
|
||||
{ type: 'content', value: 'Hello', traceId },
|
||||
{ type: 'thought', value: { subject: 'Thinking...' }, traceId },
|
||||
];
|
||||
});
|
||||
|
||||
const agent = request.agent(app);
|
||||
const res = await agent
|
||||
.post('/')
|
||||
.send(createStreamMessageRequest('hello', 'a2a-trace-id-test'))
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200);
|
||||
|
||||
const events = streamToSSEEvents(res.text);
|
||||
|
||||
// The first two events are task-creation and working status
|
||||
const textContentEvent = events[2].result as TaskStatusUpdateEvent;
|
||||
expect(textContentEvent.kind).toBe('status-update');
|
||||
expect(textContentEvent.metadata?.['traceId']).toBe(traceId);
|
||||
|
||||
const thoughtEvent = events[3].result as TaskStatusUpdateEvent;
|
||||
expect(thoughtEvent.kind).toBe('status-update');
|
||||
expect(thoughtEvent.metadata?.['traceId']).toBe(traceId);
|
||||
});
|
||||
|
||||
describe('/executeCommand', () => {
|
||||
const mockExtensions = [{ name: 'test-extension', version: '0.0.1' }];
|
||||
|
||||
beforeEach(() => {
|
||||
getExtensionsSpy.mockReturnValue(mockExtensions);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getExtensionsSpy.mockClear();
|
||||
});
|
||||
|
||||
it('should return extensions for valid command', async () => {
|
||||
const agent = request.agent(app);
|
||||
const res = await agent
|
||||
.post('/executeCommand')
|
||||
.send({ command: 'extensions list', args: [] })
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200);
|
||||
|
||||
expect(res.body).toEqual(mockExtensions);
|
||||
expect(getExtensionsSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 404 for invalid command', async () => {
|
||||
const agent = request.agent(app);
|
||||
const res = await agent
|
||||
.post('/executeCommand')
|
||||
.send({ command: 'invalid command' })
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(404);
|
||||
|
||||
expect(res.body.error).toBe('Command not found: invalid command');
|
||||
expect(getExtensionsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 400 for missing command', async () => {
|
||||
const agent = request.agent(app);
|
||||
await agent
|
||||
.post('/executeCommand')
|
||||
.send({ args: [] })
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
expect(getExtensionsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 400 if args is not an array', async () => {
|
||||
const agent = request.agent(app);
|
||||
const res = await agent
|
||||
.post('/executeCommand')
|
||||
.send({ command: 'extensions.list', args: 'not-an-array' })
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
|
||||
expect(res.body.error).toBe('"args" field must be an array.');
|
||||
expect(getExtensionsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,11 +16,6 @@ import type { AgentSettings } from '../types.js';
|
||||
import { GCSTaskStore, NoOpTaskStore } from '../persistence/gcs.js';
|
||||
import { CoderAgentExecutor } from '../agent/executor.js';
|
||||
import { requestStorage } from './requestStorage.js';
|
||||
import { loadConfig, loadEnvironment, setTargetDir } from '../config/config.js';
|
||||
import { loadSettings } from '../config/settings.js';
|
||||
import { loadExtensions } from '../config/extension.js';
|
||||
import { commandRegistry } from '../commands/command-registry.js';
|
||||
import { SimpleExtensionLoader } from '@google/gemini-cli-core';
|
||||
|
||||
const coderAgentCard: AgentCard = {
|
||||
name: 'Gemini SDLC Agent',
|
||||
@@ -66,17 +61,6 @@ export function updateCoderAgentCardUrl(port: number) {
|
||||
|
||||
export async function createApp() {
|
||||
try {
|
||||
// Load the server configuration once on startup.
|
||||
const workspaceRoot = setTargetDir(undefined);
|
||||
loadEnvironment();
|
||||
const settings = loadSettings(workspaceRoot);
|
||||
const extensions = loadExtensions(workspaceRoot);
|
||||
const config = await loadConfig(
|
||||
settings,
|
||||
new SimpleExtensionLoader(extensions),
|
||||
'a2a-server',
|
||||
);
|
||||
|
||||
// loadEnvironment() is called within getConfig now
|
||||
const bucketName = process.env['GCS_BUCKET_NAME'];
|
||||
let taskStoreForExecutor: TaskStore;
|
||||
@@ -135,38 +119,6 @@ export async function createApp() {
|
||||
}
|
||||
});
|
||||
|
||||
expressApp.post('/executeCommand', async (req, res) => {
|
||||
try {
|
||||
const { command, args } = req.body;
|
||||
|
||||
if (typeof command !== 'string') {
|
||||
return res.status(400).json({ error: 'Invalid "command" field.' });
|
||||
}
|
||||
|
||||
if (args && !Array.isArray(args)) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: '"args" field must be an array.' });
|
||||
}
|
||||
|
||||
const commandToExecute = commandRegistry.get(command);
|
||||
|
||||
if (!commandToExecute) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ error: `Command not found: ${command}` });
|
||||
}
|
||||
|
||||
const result = await commandToExecute.execute(config, args ?? []);
|
||||
return res.status(200).json(result);
|
||||
} catch (e) {
|
||||
logger.error('Error executing /executeCommand:', e);
|
||||
const errorMessage =
|
||||
e instanceof Error ? e.message : 'Unknown error executing command';
|
||||
return res.status(500).json({ error: errorMessage });
|
||||
}
|
||||
});
|
||||
|
||||
expressApp.get('/tasks/metadata', async (req, res) => {
|
||||
// This endpoint is only meaningful if the task store is in-memory.
|
||||
if (!(taskStoreForExecutor instanceof InMemoryTaskStore)) {
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { AddressInfo } from 'node:net';
|
||||
import { createApp, updateCoderAgentCardUrl } from './app.js';
|
||||
import type { TaskMetadata } from '../types.js';
|
||||
import { createMockConfig } from '../utils/testing_utils.js';
|
||||
import { debugLogger, type Config } from '@google/gemini-cli-core';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock the logger to avoid polluting test output
|
||||
// Comment out to help debug
|
||||
@@ -115,7 +115,7 @@ describe('Agent Server Endpoints', () => {
|
||||
try {
|
||||
fs.rmSync(testWorkspace, { recursive: true, force: true });
|
||||
} catch (e) {
|
||||
debugLogger.warn(`Could not remove temp dir '${testWorkspace}':`, e);
|
||||
console.warn(`Could not remove temp dir '${testWorkspace}':`, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { Storage } from '@google-cloud/storage';
|
||||
import * as fse from 'fs-extra';
|
||||
import { promises as fsPromises, createReadStream } from 'node:fs';
|
||||
import * as tar from 'tar';
|
||||
import { gzipSync, gunzipSync } from 'node:zlib';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
@@ -20,18 +21,12 @@ import * as configModule from '../config/config.js';
|
||||
import { getPersistedState, METADATA_KEY } from '../types.js';
|
||||
|
||||
// Mock dependencies
|
||||
const fsMocks = vi.hoisted(() => ({
|
||||
readdir: vi.fn(),
|
||||
createReadStream: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@google-cloud/storage');
|
||||
vi.mock('fs-extra', () => ({
|
||||
pathExists: vi.fn(),
|
||||
readdir: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
ensureDir: vi.fn(),
|
||||
createReadStream: vi.fn(),
|
||||
}));
|
||||
vi.mock('node:fs', async () => {
|
||||
const actual = await vi.importActual<typeof import('node:fs')>('node:fs');
|
||||
@@ -39,37 +34,12 @@ vi.mock('node:fs', async () => {
|
||||
...actual,
|
||||
promises: {
|
||||
...actual.promises,
|
||||
readdir: fsMocks.readdir,
|
||||
readdir: vi.fn(),
|
||||
},
|
||||
createReadStream: fsMocks.createReadStream,
|
||||
};
|
||||
});
|
||||
vi.mock('fs', async () => {
|
||||
const actual = await vi.importActual<typeof import('node:fs')>('node:fs');
|
||||
return {
|
||||
...actual,
|
||||
promises: {
|
||||
...actual.promises,
|
||||
readdir: fsMocks.readdir,
|
||||
},
|
||||
createReadStream: fsMocks.createReadStream,
|
||||
};
|
||||
});
|
||||
vi.mock('tar', async () => {
|
||||
const actualFs = await vi.importActual<typeof import('node:fs')>('node:fs');
|
||||
return {
|
||||
c: vi.fn(({ file }) => {
|
||||
if (file) {
|
||||
actualFs.writeFileSync(file, Buffer.from('dummy tar content'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}),
|
||||
x: vi.fn().mockResolvedValue(undefined),
|
||||
t: vi.fn().mockResolvedValue(undefined),
|
||||
r: vi.fn().mockResolvedValue(undefined),
|
||||
u: vi.fn().mockResolvedValue(undefined),
|
||||
createReadStream: vi.fn(),
|
||||
};
|
||||
});
|
||||
vi.mock('tar');
|
||||
vi.mock('zlib');
|
||||
vi.mock('uuid');
|
||||
vi.mock('../utils/logger.js', () => ({
|
||||
@@ -96,7 +66,7 @@ vi.mock('../types.js', async (importOriginal) => {
|
||||
|
||||
const mockStorage = Storage as MockedClass<typeof Storage>;
|
||||
const mockFse = fse as Mocked<typeof fse>;
|
||||
const mockCreateReadStream = fsMocks.createReadStream;
|
||||
const mockCreateReadStream = createReadStream as Mock;
|
||||
const mockTar = tar as Mocked<typeof tar>;
|
||||
const mockGzipSync = gzipSync as Mock;
|
||||
const mockGunzipSync = gunzipSync as Mock;
|
||||
@@ -106,19 +76,10 @@ const mockGetPersistedState = getPersistedState as Mock;
|
||||
const TEST_METADATA_KEY = METADATA_KEY || '__persistedState';
|
||||
|
||||
type MockWriteStream = {
|
||||
emit: Mock<(event: string, ...args: unknown[]) => boolean>;
|
||||
removeListener: Mock<
|
||||
(event: string, cb: (error?: Error | null) => void) => MockWriteStream
|
||||
>;
|
||||
once: Mock<
|
||||
(event: string, cb: (error?: Error | null) => void) => MockWriteStream
|
||||
>;
|
||||
on: Mock<
|
||||
(event: string, cb: (error?: Error | null) => void) => MockWriteStream
|
||||
>;
|
||||
destroy: Mock<() => void>;
|
||||
write: Mock<(chunk: unknown, encoding?: unknown, cb?: unknown) => boolean>;
|
||||
end: Mock<(cb?: unknown) => void>;
|
||||
destroyed: boolean;
|
||||
};
|
||||
|
||||
@@ -153,18 +114,11 @@ describe('GCSTaskStore', () => {
|
||||
bucketName = 'test-bucket';
|
||||
|
||||
mockWriteStream = {
|
||||
emit: vi.fn().mockReturnValue(true),
|
||||
removeListener: vi.fn().mockReturnValue(mockWriteStream),
|
||||
on: vi.fn((event, cb) => {
|
||||
if (event === 'finish') setTimeout(cb, 0); // Simulate async finish
|
||||
return mockWriteStream;
|
||||
}),
|
||||
once: vi.fn((event, cb) => {
|
||||
if (event === 'finish') setTimeout(cb, 0); // Simulate async finish return mockWriteStream;
|
||||
}),
|
||||
destroy: vi.fn(),
|
||||
write: vi.fn().mockReturnValue(true),
|
||||
end: vi.fn(),
|
||||
destroyed: false,
|
||||
};
|
||||
|
||||
@@ -195,16 +149,14 @@ describe('GCSTaskStore', () => {
|
||||
_taskState: 'submitted',
|
||||
});
|
||||
(fse.pathExists as Mock).mockResolvedValue(true);
|
||||
fsMocks.readdir.mockResolvedValue(['file1.txt']);
|
||||
(fsPromises.readdir as Mock).mockResolvedValue(['file1.txt']);
|
||||
mockTar.c.mockResolvedValue(undefined);
|
||||
mockTar.x.mockResolvedValue(undefined);
|
||||
mockFse.remove.mockResolvedValue(undefined);
|
||||
mockFse.ensureDir.mockResolvedValue(undefined);
|
||||
mockGzipSync.mockReturnValue(Buffer.from('compressed'));
|
||||
mockGunzipSync.mockReturnValue(Buffer.from('{}'));
|
||||
mockCreateReadStream.mockReturnValue({ on: vi.fn(), pipe: vi.fn() });
|
||||
mockFse.createReadStream.mockReturnValue({
|
||||
on: vi.fn(),
|
||||
pipe: vi.fn(),
|
||||
} as unknown as import('node:fs').ReadStream);
|
||||
});
|
||||
|
||||
describe('Constructor & Initialization', () => {
|
||||
@@ -249,12 +201,13 @@ describe('GCSTaskStore', () => {
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
it('should save metadata and workspace', async () => {
|
||||
it.skip('should save metadata and workspace', async () => {
|
||||
const store = new GCSTaskStore(bucketName);
|
||||
await store.save(mockTask);
|
||||
|
||||
expect(mockFile.save).toHaveBeenCalledTimes(1);
|
||||
expect(mockTar.c).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreateReadStream).toHaveBeenCalledTimes(1);
|
||||
expect(mockFse.remove).toHaveBeenCalledTimes(1);
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
expect.stringContaining('metadata saved to GCS'),
|
||||
@@ -274,35 +227,6 @@ describe('GCSTaskStore', () => {
|
||||
'tar.c command failed to create',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if taskId contains path traversal sequences', async () => {
|
||||
const store = new GCSTaskStore('test-bucket');
|
||||
const maliciousTask: SDKTask = {
|
||||
id: '../../../malicious-task',
|
||||
metadata: {
|
||||
_internal: {
|
||||
agentSettings: {
|
||||
cacheDir: '/tmp/cache',
|
||||
dataDir: '/tmp/data',
|
||||
logDir: '/tmp/logs',
|
||||
tempDir: '/tmp/temp',
|
||||
},
|
||||
taskState: 'working',
|
||||
},
|
||||
},
|
||||
kind: 'task',
|
||||
status: {
|
||||
state: 'working',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
contextId: 'test-context',
|
||||
history: [],
|
||||
artifacts: [],
|
||||
};
|
||||
await expect(store.save(maliciousTask)).rejects.toThrow(
|
||||
'Invalid taskId: ../../../malicious-task',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('load', () => {
|
||||
@@ -399,14 +323,6 @@ describe('GCSTaskStore', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if taskId contains path traversal sequences', async () => {
|
||||
const store = new GCSTaskStore('test-bucket');
|
||||
const maliciousTaskId = '../../../malicious-task';
|
||||
await expect(store.load(maliciousTaskId)).rejects.toThrow(
|
||||
`Invalid taskId: ${maliciousTaskId}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NoOpTaskStore', () => {
|
||||
|
||||
@@ -23,13 +23,6 @@ type ObjectType = 'metadata' | 'workspace';
|
||||
const getTmpArchiveFilename = (taskId: string): string =>
|
||||
`task-${taskId}-workspace-${uuidv4()}.tar.gz`;
|
||||
|
||||
// Validate the taskId to prevent path traversal attacks by ensuring it only contains safe characters.
|
||||
const isTaskIdValid = (taskId: string): boolean => {
|
||||
// Allow only alphanumeric characters, dashes, and underscores, and ensure it's not empty.
|
||||
const validTaskIdRegex = /^[a-zA-Z0-9_-]+$/;
|
||||
return validTaskIdRegex.test(taskId);
|
||||
};
|
||||
|
||||
export class GCSTaskStore implements TaskStore {
|
||||
private storage: Storage;
|
||||
private bucketName: string;
|
||||
@@ -85,9 +78,6 @@ export class GCSTaskStore implements TaskStore {
|
||||
}
|
||||
|
||||
private getObjectPath(taskId: string, type: ObjectType): string {
|
||||
if (!isTaskIdValid(taskId)) {
|
||||
throw new Error(`Invalid taskId: ${taskId}`);
|
||||
}
|
||||
return `tasks/${taskId}/${type}.tar.gz`;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,10 +37,6 @@ export enum CoderAgentEvent {
|
||||
* An event that contains a thought from the agent.
|
||||
*/
|
||||
ThoughtEvent = 'thought',
|
||||
/**
|
||||
* An event that contains citation from the agent.
|
||||
*/
|
||||
CitationEvent = 'citation',
|
||||
}
|
||||
|
||||
export interface AgentSettings {
|
||||
@@ -68,10 +64,6 @@ export interface Thought {
|
||||
kind: CoderAgentEvent.ThoughtEvent;
|
||||
}
|
||||
|
||||
export interface Citation {
|
||||
kind: CoderAgentEvent.CitationEvent;
|
||||
}
|
||||
|
||||
export type ThoughtSummary = {
|
||||
subject: string;
|
||||
description: string;
|
||||
@@ -88,8 +80,7 @@ export type CoderAgentMessage =
|
||||
| ToolCallUpdate
|
||||
| TextContent
|
||||
| StateChange
|
||||
| Thought
|
||||
| Citation;
|
||||
| Thought;
|
||||
|
||||
export interface TaskMetadata {
|
||||
id: string;
|
||||
|
||||
@@ -50,9 +50,6 @@ export function createMockConfig(
|
||||
getEmbeddingModel: vi.fn().mockReturnValue('text-embedding-004'),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
getUserTier: vi.fn(),
|
||||
getEnableMessageBusIntegration: vi.fn().mockReturnValue(false),
|
||||
getMessageBus: vi.fn(),
|
||||
getPolicyEngine: vi.fn(),
|
||||
...overrides,
|
||||
} as unknown as Config;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import './src/gemini.js';
|
||||
import { main } from './src/gemini.js';
|
||||
import { debugLogger, FatalError } from '@google/gemini-cli-core';
|
||||
import { FatalError } from '@google/gemini-cli-core';
|
||||
|
||||
// --- Global Entry Point ---
|
||||
main().catch((error) => {
|
||||
@@ -17,14 +17,14 @@ main().catch((error) => {
|
||||
if (!process.env['NO_COLOR']) {
|
||||
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
|
||||
}
|
||||
debugLogger.error(errorMessage);
|
||||
console.error(errorMessage);
|
||||
process.exit(error.exitCode);
|
||||
}
|
||||
debugLogger.error('An unexpected critical error occurred:');
|
||||
console.error('An unexpected critical error occurred:');
|
||||
if (error instanceof Error) {
|
||||
debugLogger.error(error.stack);
|
||||
console.error(error.stack);
|
||||
} else {
|
||||
debugLogger.error(String(error));
|
||||
console.error(String(error));
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.12.0-preview.10",
|
||||
"version": "0.10.0",
|
||||
"description": "Gemini CLI",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,7 +25,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.12.0-preview.10"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -38,18 +38,15 @@
|
||||
"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",
|
||||
"ink": "^6.2.3",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"latest-version": "^9.0.0",
|
||||
"lowlight": "^3.3.0",
|
||||
"mnemonist": "^0.40.3",
|
||||
"open": "^10.1.2",
|
||||
"prompts": "^2.4.2",
|
||||
"react": "^19.1.0",
|
||||
"read-package-up": "^11.0.0",
|
||||
"shell-quote": "^1.8.3",
|
||||
@@ -59,6 +56,8 @@
|
||||
"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",
|
||||
"zod": "^3.23.8"
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`runNonInteractive > should write a single newline between sequential text outputs from the model 1`] = `
|
||||
"Use mock tool
|
||||
Use mock tool again
|
||||
Finished.
|
||||
"
|
||||
`;
|
||||
@@ -16,7 +16,6 @@ import { newCommand } from './extensions/new.js';
|
||||
|
||||
export const extensionsCommand: CommandModule = {
|
||||
command: 'extensions <command>',
|
||||
aliases: ['extension'],
|
||||
describe: 'Manage Gemini CLI extensions.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
|
||||
@@ -5,42 +5,27 @@
|
||||
*/
|
||||
|
||||
import { type CommandModule } from 'yargs';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import { disableExtension } from '../../config/extension.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
interface DisableArgs {
|
||||
name: string;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
export async function handleDisable(args: DisableArgs) {
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
settings: loadSettings(workspaceDir).merged,
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
export function handleDisable(args: DisableArgs) {
|
||||
try {
|
||||
if (args.scope?.toLowerCase() === 'workspace') {
|
||||
await extensionManager.disableExtension(
|
||||
args.name,
|
||||
SettingScope.Workspace,
|
||||
);
|
||||
disableExtension(args.name, SettingScope.Workspace);
|
||||
} else {
|
||||
await extensionManager.disableExtension(args.name, SettingScope.User);
|
||||
disableExtension(args.name, SettingScope.User);
|
||||
}
|
||||
debugLogger.log(
|
||||
console.log(
|
||||
`Extension "${args.name}" successfully disabled for scope "${args.scope}".`,
|
||||
);
|
||||
} catch (error) {
|
||||
debugLogger.error(getErrorMessage(error));
|
||||
console.error(getErrorMessage(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -55,7 +40,7 @@ export const disableCommand: CommandModule = {
|
||||
type: 'string',
|
||||
})
|
||||
.option('scope', {
|
||||
describe: 'The scope to disable the extension in.',
|
||||
describe: 'The scope to disable the extenison in.',
|
||||
type: 'string',
|
||||
default: SettingScope.User,
|
||||
})
|
||||
|
||||
@@ -5,43 +5,28 @@
|
||||
*/
|
||||
|
||||
import { type CommandModule } from 'yargs';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import {
|
||||
debugLogger,
|
||||
FatalConfigError,
|
||||
getErrorMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import { FatalConfigError, getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { enableExtension } from '../../config/extension.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
|
||||
interface EnableArgs {
|
||||
name: string;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
export async function handleEnable(args: EnableArgs) {
|
||||
const workingDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir: workingDir,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
settings: loadSettings(workingDir).merged,
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
export function handleEnable(args: EnableArgs) {
|
||||
try {
|
||||
if (args.scope?.toLowerCase() === 'workspace') {
|
||||
extensionManager.enableExtension(args.name, SettingScope.Workspace);
|
||||
enableExtension(args.name, SettingScope.Workspace);
|
||||
} else {
|
||||
extensionManager.enableExtension(args.name, SettingScope.User);
|
||||
enableExtension(args.name, SettingScope.User);
|
||||
}
|
||||
if (args.scope) {
|
||||
debugLogger.log(
|
||||
console.log(
|
||||
`Extension "${args.name}" successfully enabled for scope "${args.scope}".`,
|
||||
);
|
||||
} else {
|
||||
debugLogger.log(
|
||||
console.log(
|
||||
`Extension "${args.name}" successfully enabled in all scopes.`,
|
||||
);
|
||||
}
|
||||
@@ -61,7 +46,7 @@ export const enableCommand: CommandModule = {
|
||||
})
|
||||
.option('scope', {
|
||||
describe:
|
||||
'The scope to enable the extension in. If not set, will be enabled in all scopes.',
|
||||
'The scope to enable the extenison in. If not set, will be enabled in all scopes.',
|
||||
type: 'string',
|
||||
})
|
||||
.check((argv) => {
|
||||
|
||||
@@ -12,22 +12,11 @@ const mockInstallOrUpdateExtension = vi.hoisted(() => vi.fn());
|
||||
const mockRequestConsentNonInteractive = vi.hoisted(() => vi.fn());
|
||||
const mockStat = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../../config/extensions/consent.js', () => ({
|
||||
vi.mock('../../config/extension.js', () => ({
|
||||
installOrUpdateExtension: mockInstallOrUpdateExtension,
|
||||
requestConsentNonInteractive: mockRequestConsentNonInteractive,
|
||||
}));
|
||||
|
||||
vi.mock('../../config/extension-manager.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../config/extension-manager.js')>();
|
||||
return {
|
||||
...actual,
|
||||
ExtensionManager: vi.fn().mockImplementation(() => ({
|
||||
installOrUpdateExtension: mockInstallOrUpdateExtension,
|
||||
loadExtensions: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../utils/errors.js', () => ({
|
||||
getErrorMessage: vi.fn((error: Error) => error.message),
|
||||
}));
|
||||
@@ -65,7 +54,7 @@ describe('handleInstall', () => {
|
||||
mockInstallOrUpdateExtension.mockClear();
|
||||
mockRequestConsentNonInteractive.mockClear();
|
||||
mockStat.mockClear();
|
||||
vi.clearAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should install an extension from a http source', async () => {
|
||||
|
||||
@@ -6,25 +6,18 @@
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import {
|
||||
debugLogger,
|
||||
type ExtensionInstallMetadata,
|
||||
} from '@google/gemini-cli-core';
|
||||
installOrUpdateExtension,
|
||||
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';
|
||||
import {
|
||||
INSTALL_WARNING_MESSAGE,
|
||||
requestConsentNonInteractive,
|
||||
} from '../../config/extensions/consent.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
interface InstallArgs {
|
||||
source: string;
|
||||
ref?: string;
|
||||
autoUpdate?: boolean;
|
||||
allowPreRelease?: boolean;
|
||||
consent?: boolean;
|
||||
}
|
||||
|
||||
export async function handleInstall(args: InstallArgs) {
|
||||
@@ -61,27 +54,13 @@ export async function handleInstall(args: InstallArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
const requestConsent = args.consent
|
||||
? () => Promise.resolve(true)
|
||||
: requestConsentNonInteractive;
|
||||
if (args.consent) {
|
||||
debugLogger.log('You have consented to the following:');
|
||||
debugLogger.log(INSTALL_WARNING_MESSAGE);
|
||||
}
|
||||
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent,
|
||||
requestSetting: promptForSetting,
|
||||
settings: loadSettings(workspaceDir).merged,
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
const name =
|
||||
await extensionManager.installOrUpdateExtension(installMetadata);
|
||||
debugLogger.log(`Extension "${name}" installed successfully and enabled.`);
|
||||
const name = await installOrUpdateExtension(
|
||||
installMetadata,
|
||||
requestConsentNonInteractive,
|
||||
);
|
||||
console.log(`Extension "${name}" installed successfully and enabled.`);
|
||||
} catch (error) {
|
||||
debugLogger.error(getErrorMessage(error));
|
||||
console.error(getErrorMessage(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -108,12 +87,6 @@ export const installCommand: CommandModule = {
|
||||
describe: 'Enable pre-release versions for this extension.',
|
||||
type: 'boolean',
|
||||
})
|
||||
.option('consent', {
|
||||
describe:
|
||||
'Acknowledge the security risks of installing an extension and skip the confirmation prompt.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
.check((argv) => {
|
||||
if (!argv.source) {
|
||||
throw new Error('The source argument must be provided.');
|
||||
@@ -126,7 +99,6 @@ export const installCommand: CommandModule = {
|
||||
ref: argv['ref'] as string | undefined,
|
||||
autoUpdate: argv['auto-update'] as boolean | undefined,
|
||||
allowPreRelease: argv['pre-release'] as boolean | undefined,
|
||||
consent: argv['consent'] as boolean | undefined,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,15 +6,12 @@
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import {
|
||||
debugLogger,
|
||||
type ExtensionInstallMetadata,
|
||||
} from '@google/gemini-cli-core';
|
||||
installOrUpdateExtension,
|
||||
requestConsentNonInteractive,
|
||||
} from '../../config/extension.js';
|
||||
import type { ExtensionInstallMetadata } from '@google/gemini-cli-core';
|
||||
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
interface InstallArgs {
|
||||
path: string;
|
||||
@@ -26,21 +23,15 @@ export async function handleLink(args: InstallArgs) {
|
||||
source: args.path,
|
||||
type: 'link',
|
||||
};
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
settings: loadSettings(workspaceDir).merged,
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
const extensionName =
|
||||
await extensionManager.installOrUpdateExtension(installMetadata);
|
||||
debugLogger.log(
|
||||
const extensionName = await installOrUpdateExtension(
|
||||
installMetadata,
|
||||
requestConsentNonInteractive,
|
||||
);
|
||||
console.log(
|
||||
`Extension "${extensionName}" linked successfully and enabled.`,
|
||||
);
|
||||
} catch (error) {
|
||||
debugLogger.error(getErrorMessage(error));
|
||||
console.error(getErrorMessage(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,36 +5,23 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { loadUserExtensions, toOutputString } from '../../config/extension.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
export async function handleList() {
|
||||
try {
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
settings: loadSettings(workspaceDir).merged,
|
||||
});
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const extensions = loadUserExtensions();
|
||||
if (extensions.length === 0) {
|
||||
debugLogger.log('No extensions installed.');
|
||||
console.log('No extensions installed.');
|
||||
return;
|
||||
}
|
||||
debugLogger.log(
|
||||
console.log(
|
||||
extensions
|
||||
.map((extension, _): string =>
|
||||
extensionManager.toOutputString(extension),
|
||||
)
|
||||
.map((extension, _): string => toOutputString(extension, process.cwd()))
|
||||
.join('\n\n'),
|
||||
);
|
||||
} catch (error) {
|
||||
debugLogger.error(getErrorMessage(error));
|
||||
console.error(getErrorMessage(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 { debugLogger } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
|
||||
interface NewArgs {
|
||||
path: string;
|
||||
@@ -49,27 +49,32 @@ async function copyDirectory(template: string, path: string) {
|
||||
}
|
||||
|
||||
async function handleNew(args: NewArgs) {
|
||||
if (args.template) {
|
||||
await copyDirectory(args.template, args.path);
|
||||
debugLogger.log(
|
||||
`Successfully created new extension from template "${args.template}" at ${args.path}.`,
|
||||
try {
|
||||
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.`,
|
||||
);
|
||||
} 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),
|
||||
);
|
||||
debugLogger.log(`Successfully created new extension at ${args.path}.`);
|
||||
} catch (error) {
|
||||
console.error(getErrorMessage(error));
|
||||
throw error;
|
||||
}
|
||||
debugLogger.log(
|
||||
`You can install this using "gemini extensions link ${args.path}" to test it out.`,
|
||||
);
|
||||
}
|
||||
|
||||
async function getBoilerplateChoices() {
|
||||
|
||||
@@ -5,12 +5,8 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { uninstallExtension } from '../../config/extension.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
interface UninstallArgs {
|
||||
name: string; // can be extension name or source URL.
|
||||
@@ -18,18 +14,10 @@ interface UninstallArgs {
|
||||
|
||||
export async function handleUninstall(args: UninstallArgs) {
|
||||
try {
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
settings: loadSettings(workspaceDir).merged,
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
await extensionManager.uninstallExtension(args.name, false);
|
||||
debugLogger.log(`Extension "${args.name}" successfully uninstalled.`);
|
||||
await uninstallExtension(args.name, false);
|
||||
console.log(`Extension "${args.name}" successfully uninstalled.`);
|
||||
} catch (error) {
|
||||
debugLogger.error(getErrorMessage(error));
|
||||
console.error(getErrorMessage(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import {
|
||||
loadExtensions,
|
||||
annotateActiveExtensions,
|
||||
ExtensionStorage,
|
||||
requestConsentNonInteractive,
|
||||
} from '../../config/extension.js';
|
||||
import {
|
||||
updateAllUpdatableExtensions,
|
||||
type ExtensionUpdateInfo,
|
||||
@@ -14,11 +20,7 @@ import {
|
||||
import { checkForExtensionUpdate } from '../../config/extensions/github.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
|
||||
|
||||
interface UpdateArgs {
|
||||
name?: string;
|
||||
@@ -29,42 +31,44 @@ const updateOutput = (info: ExtensionUpdateInfo) =>
|
||||
`Extension "${info.name}" successfully updated: ${info.originalVersion} → ${info.updatedVersion}.`;
|
||||
|
||||
export async function handleUpdate(args: UpdateArgs) {
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
settings: loadSettings(workspaceDir).merged,
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const workingDir = process.cwd();
|
||||
const extensionEnablementManager = new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
// Force enable named extensions, otherwise we will only update the enabled
|
||||
// ones.
|
||||
args.name ? [args.name] : [],
|
||||
);
|
||||
const allExtensions = loadExtensions(extensionEnablementManager);
|
||||
const extensions = annotateActiveExtensions(
|
||||
allExtensions,
|
||||
workingDir,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
if (args.name) {
|
||||
try {
|
||||
const extension = extensions.find(
|
||||
(extension) => extension.name === args.name,
|
||||
);
|
||||
if (!extension) {
|
||||
debugLogger.log(`Extension "${args.name}" not found.`);
|
||||
console.log(`Extension "${args.name}" not found.`);
|
||||
return;
|
||||
}
|
||||
if (!extension.installMetadata) {
|
||||
debugLogger.log(
|
||||
console.log(
|
||||
`Unable to install extension "${args.name}" due to missing install metadata`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const updateState = await checkForExtensionUpdate(
|
||||
extension,
|
||||
extensionManager,
|
||||
);
|
||||
const updateState = await checkForExtensionUpdate(extension);
|
||||
if (updateState !== ExtensionUpdateState.UPDATE_AVAILABLE) {
|
||||
debugLogger.log(`Extension "${args.name}" is already up to date.`);
|
||||
console.log(`Extension "${args.name}" is already up to date.`);
|
||||
return;
|
||||
}
|
||||
// TODO(chrstnb): we should list extensions if the requested extension is not installed.
|
||||
const updatedExtensionInfo = (await updateExtension(
|
||||
extension,
|
||||
extensionManager,
|
||||
workingDir,
|
||||
requestConsentNonInteractive,
|
||||
updateState,
|
||||
() => {},
|
||||
))!;
|
||||
@@ -72,14 +76,14 @@ export async function handleUpdate(args: UpdateArgs) {
|
||||
updatedExtensionInfo.originalVersion !==
|
||||
updatedExtensionInfo.updatedVersion
|
||||
) {
|
||||
debugLogger.log(
|
||||
console.log(
|
||||
`Extension "${args.name}" successfully updated: ${updatedExtensionInfo.originalVersion} → ${updatedExtensionInfo.updatedVersion}.`,
|
||||
);
|
||||
} else {
|
||||
debugLogger.log(`Extension "${args.name}" is already up to date.`);
|
||||
console.log(`Extension "${args.name}" is already up to date.`);
|
||||
}
|
||||
} catch (error) {
|
||||
debugLogger.error(getErrorMessage(error));
|
||||
console.error(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
if (args.all) {
|
||||
@@ -87,7 +91,6 @@ export async function handleUpdate(args: UpdateArgs) {
|
||||
const extensionState = new Map();
|
||||
await checkForAllExtensionUpdates(
|
||||
extensions,
|
||||
extensionManager,
|
||||
(action) => {
|
||||
if (action.type === 'SET_STATE') {
|
||||
extensionState.set(action.payload.name, {
|
||||
@@ -95,23 +98,25 @@ export async function handleUpdate(args: UpdateArgs) {
|
||||
});
|
||||
}
|
||||
},
|
||||
workingDir,
|
||||
);
|
||||
let updateInfos = await updateAllUpdatableExtensions(
|
||||
workingDir,
|
||||
requestConsentNonInteractive,
|
||||
extensions,
|
||||
extensionState,
|
||||
extensionManager,
|
||||
() => {},
|
||||
);
|
||||
updateInfos = updateInfos.filter(
|
||||
(info) => info.originalVersion !== info.updatedVersion,
|
||||
);
|
||||
if (updateInfos.length === 0) {
|
||||
debugLogger.log('No extensions to update.');
|
||||
console.log('No extensions to update.');
|
||||
return;
|
||||
}
|
||||
debugLogger.log(updateInfos.map((info) => updateOutput(info)).join('\n'));
|
||||
console.log(updateInfos.map((info) => updateOutput(info)).join('\n'));
|
||||
} catch (error) {
|
||||
debugLogger.error(getErrorMessage(error));
|
||||
console.error(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user