diff --git a/.github/workflows/deflake.yml b/.github/workflows/deflake.yml index 3d9e9a3fe8..c9f4c3d59f 100644 --- a/.github/workflows/deflake.yml +++ b/.github/workflows/deflake.yml @@ -24,13 +24,147 @@ concurrency: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }} jobs: - deflake: - name: 'Deflake' + 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: 'Deflake' + - 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: | - ECHO 'DEFLAKE WORKFLOW' + 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 }}'" diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md index b9c09d26fe..23ba764d08 100644 --- a/docs/cli/configuration.md +++ b/docs/cli/configuration.md @@ -275,7 +275,7 @@ contain other project-specific files related to Gemini CLI's operation, such as: - **`telemetry`** (object) - **Description:** Configures logging and metrics collection for Gemini CLI. - For more information, see [Telemetry](../telemetry.md). + For more information, see [Telemetry](./telemetry.md). - **Default:** `{"enabled": false, "target": "local", "otlpEndpoint": "http://localhost:4317", "logPrompts": true}` - **Properties:** diff --git a/docs/cli/enterprise.md b/docs/cli/enterprise.md index 0274e48f00..fb641b82f0 100644 --- a/docs/cli/enterprise.md +++ b/docs/cli/enterprise.md @@ -202,6 +202,26 @@ 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 diff --git a/docs/integration-tests.md b/docs/integration-tests.md index b6b8c1ec51..24377c1934 100644 --- a/docs/integration-tests.md +++ b/docs/integration-tests.md @@ -59,12 +59,20 @@ npm run test:e2e -- --test-name-pattern "reads a file" ### Deflaking a test Before adding a **new** integration test, you should test it at least 5 times -with the deflake script to make sure that it is not flaky. +with the deflake script or workflow to make sure that it is not flaky. + +### Deflake script ```bash npm run deflake -- --runs=5 --command="npm run test:e2e -- -- --test-name-pattern ''" ``` +#### Deflake Workflow + +```bash +gh workflow run deflake.yml --ref -f test_name_pattern="" +``` + ### Running all tests To run the entire suite of integration tests, use the following command: diff --git a/hello/commands/fs/grep-code.toml b/hello/commands/fs/grep-code.toml new file mode 100644 index 0000000000..87d957542a --- /dev/null +++ b/hello/commands/fs/grep-code.toml @@ -0,0 +1,6 @@ +prompt = """ +Please summarize the findings for the pattern `{{args}}`. + +Search Results: +!{grep -r {{args}} .} +""" diff --git a/hello/gemini-extension.json b/hello/gemini-extension.json new file mode 100644 index 0000000000..d973ab8fe4 --- /dev/null +++ b/hello/gemini-extension.json @@ -0,0 +1,4 @@ +{ + "name": "custom-commands", + "version": "1.0.0" +} diff --git a/integration-tests/file-system-interactive.test.ts b/integration-tests/file-system-interactive.test.ts index 2dec6c593c..d7ad73fd0d 100644 --- a/integration-tests/file-system-interactive.test.ts +++ b/integration-tests/file-system-interactive.test.ts @@ -20,7 +20,16 @@ describe('Interactive file system', () => { it('should perform a read-then-write sequence', async () => { const fileName = 'version.txt'; - await rig.setup('interactive-read-then-write'); + await rig.setup('interactive-read-then-write', { + settings: { + security: { + auth: { + selectedType: 'gemini-api-key', + }, + disableYoloMode: false, + }, + }, + }); rig.createFile(fileName, '1.0.0'); const run = await rig.runInteractive(); diff --git a/package-lock.json b/package-lock.json index a41fd70fc7..33e0e61d91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@google/gemini-cli", - "version": "0.11.0-nightly.20251021.e72c00cf", + "version": "0.12.0-nightly.20251022.0542de95", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@google/gemini-cli", - "version": "0.11.0-nightly.20251021.e72c00cf", + "version": "0.12.0-nightly.20251022.0542de95", "workspaces": [ "packages/*" ], @@ -17694,7 +17694,7 @@ }, "packages/a2a-server": { "name": "@google/gemini-cli-a2a-server", - "version": "0.11.0-nightly.20251021.e72c00cf", + "version": "0.12.0-nightly.20251022.0542de95", "dependencies": { "@a2a-js/sdk": "^0.3.2", "@google-cloud/storage": "^7.16.0", @@ -17968,7 +17968,7 @@ }, "packages/cli": { "name": "@google/gemini-cli", - "version": "0.11.0-nightly.20251021.e72c00cf", + "version": "0.12.0-nightly.20251022.0542de95", "dependencies": { "@google/gemini-cli-core": "file:../core", "@google/genai": "1.16.0", @@ -18081,7 +18081,7 @@ }, "packages/core": { "name": "@google/gemini-cli-core", - "version": "0.11.0-nightly.20251021.e72c00cf", + "version": "0.12.0-nightly.20251022.0542de95", "dependencies": { "@google-cloud/logging": "^11.2.1", "@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0", @@ -18222,7 +18222,7 @@ }, "packages/test-utils": { "name": "@google/gemini-cli-test-utils", - "version": "0.11.0-nightly.20251021.e72c00cf", + "version": "0.12.0-nightly.20251022.0542de95", "license": "Apache-2.0", "devDependencies": { "typescript": "^5.3.3" @@ -18233,7 +18233,7 @@ }, "packages/vscode-ide-companion": { "name": "gemini-cli-vscode-ide-companion", - "version": "0.11.0-nightly.20251021.e72c00cf", + "version": "0.12.0-nightly.20251022.0542de95", "license": "LICENSE", "dependencies": { "@modelcontextprotocol/sdk": "^1.15.1", diff --git a/package.json b/package.json index 940139ba24..8d21f14308 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli", - "version": "0.11.0-nightly.20251021.e72c00cf", + "version": "0.12.0-nightly.20251022.0542de95", "engines": { "node": ">=20.0.0" }, @@ -13,15 +13,15 @@ "url": "git+https://github.com/google-gemini/gemini-cli.git" }, "config": { - "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.11.0-nightly.20251021.e72c00cf" + "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.12.0-nightly.20251022.0542de95" }, "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", @@ -41,9 +41,9 @@ "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", - "lint:fix": "eslint . --fix && eslint integration-tests --fix", - "lint:ci": "eslint . --ext .ts,.tsx --max-warnings 0 && eslint integration-tests --max-warnings 0", + "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:all": "node scripts/lint.js", "format": "prettier --experimental-cli --write .", "typecheck": "npm run typecheck --workspaces --if-present", diff --git a/packages/a2a-server/package.json b/packages/a2a-server/package.json index 9594bc8f9d..ec897db298 100644 --- a/packages/a2a-server/package.json +++ b/packages/a2a-server/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli-a2a-server", - "version": "0.11.0-nightly.20251021.e72c00cf", + "version": "0.12.0-nightly.20251022.0542de95", "description": "Gemini CLI A2A Server", "repository": { "type": "git", diff --git a/packages/cli/package.json b/packages/cli/package.json index a562ac22cc..7419223a42 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli", - "version": "0.11.0-nightly.20251021.e72c00cf", + "version": "0.12.0-nightly.20251022.0542de95", "description": "Gemini CLI", "repository": { "type": "git", @@ -25,7 +25,7 @@ "dist" ], "config": { - "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.11.0-nightly.20251021.e72c00cf" + "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.12.0-nightly.20251022.0542de95" }, "dependencies": { "@google/gemini-cli-core": "file:../core", diff --git a/packages/cli/src/commands/extensions.tsx b/packages/cli/src/commands/extensions.tsx index e198a1ac47..be4d7160cc 100644 --- a/packages/cli/src/commands/extensions.tsx +++ b/packages/cli/src/commands/extensions.tsx @@ -16,6 +16,7 @@ import { newCommand } from './extensions/new.js'; export const extensionsCommand: CommandModule = { command: 'extensions ', + aliases: ['extension'], describe: 'Manage Gemini CLI extensions.', builder: (yargs) => yargs diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 5490afb678..b935d4a696 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -1112,6 +1112,23 @@ describe('Approval mode tool exclusion logic', () => { expect(excludedTools).not.toContain(WRITE_FILE_TOOL_NAME); // Should be allowed in auto_edit }); + it('should throw an error if YOLO mode is attempted when disableYoloMode is true', async () => { + process.argv = ['node', 'script.js', '--yolo']; + const argv = await parseArguments({} as Settings); + const settings: Settings = { + security: { + disableYoloMode: true, + }, + }; + const extensions: GeminiCLIExtension[] = []; + + await expect( + loadCliConfig(settings, extensions, 'test-session', argv), + ).rejects.toThrow( + 'Cannot start in YOLO mode when it is disabled by settings', + ); + }); + it('should throw an error for invalid approval mode values in loadCliConfig', async () => { // Create a mock argv with an invalid approval mode that bypasses argument parsing validation const invalidArgv: Partial & { approvalMode: string } = { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 1bb1a3b70d..96ab5c04d1 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -442,6 +442,21 @@ export async function loadCliConfig( argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT; } + // Override approval mode if disableYoloMode is set. + if (settings.security?.disableYoloMode) { + if (approvalMode === ApprovalMode.YOLO) { + debugLogger.error('YOLO mode is disabled by the "disableYolo" setting.'); + throw new FatalConfigError( + 'Cannot start in YOLO mode when it is disabled by settings', + ); + } + approvalMode = ApprovalMode.DEFAULT; + } else if (approvalMode === ApprovalMode.YOLO) { + debugLogger.warn( + 'YOLO mode is enabled. All tool calls will be automatically approved.', + ); + } + // Force approval mode to default if the folder is not trusted. if (!trustedFolder && approvalMode !== ApprovalMode.DEFAULT) { debugLogger.warn( @@ -583,6 +598,7 @@ export async function loadCliConfig( geminiMdFileCount: fileCount, geminiMdFilePaths: filePaths, approvalMode, + disableYoloMode: settings.security?.disableYoloMode, showMemoryUsage: settings.ui?.showMemoryUsage || false, accessibility: { ...settings.ui?.accessibility, diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index 672f42bd5b..c3b7cf748e 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -609,6 +609,40 @@ describe('Settings Loading and Merging', () => { expect(settings.merged.security?.folderTrust?.enabled).toBe(true); // System setting should be used }); + it('should not allow user or workspace to override system disableYoloMode', () => { + (mockFsExistsSync as Mock).mockReturnValue(true); + const userSettingsContent = { + security: { + disableYoloMode: false, + }, + }; + const workspaceSettingsContent = { + security: { + disableYoloMode: false, // This should be ignored + }, + }; + const systemSettingsContent = { + security: { + disableYoloMode: true, + }, + }; + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === getSystemSettingsPath()) + return JSON.stringify(systemSettingsContent); + if (p === USER_SETTINGS_PATH) + return JSON.stringify(userSettingsContent); + if (p === MOCK_WORKSPACE_SETTINGS_PATH) + return JSON.stringify(workspaceSettingsContent); + return '{}'; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + expect(settings.merged.security?.disableYoloMode).toBe(true); // System setting should be used + }); + it('should handle contextFileName correctly when only in user settings', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index c01e691f44..2c3fc21ff4 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -937,6 +937,15 @@ const SETTINGS_SCHEMA = { description: 'Security-related settings.', showInDialog: false, properties: { + disableYoloMode: { + type: 'boolean', + label: 'Disable YOLO Mode', + category: 'Security', + requiresRestart: true, + default: false, + description: 'Disable YOLO mode, even if enabled by a flag.', + showInDialog: true, + }, folderTrust: { type: 'object', label: 'Folder Trust', diff --git a/packages/cli/src/ui/components/shared/BaseSelectionList.test.tsx b/packages/cli/src/ui/components/shared/BaseSelectionList.test.tsx index 8a4e6d24bf..0d383a8641 100644 --- a/packages/cli/src/ui/components/shared/BaseSelectionList.test.tsx +++ b/packages/cli/src/ui/components/shared/BaseSelectionList.test.tsx @@ -254,7 +254,7 @@ describe('BaseSelectionList', () => { }); }); - describe.skip('Scrolling and Pagination (maxItemsToShow)', () => { + describe('Scrolling and Pagination (maxItemsToShow)', () => { const longList = Array.from({ length: 10 }, (_, i) => ({ value: `Item ${i + 1}`, label: `Item ${i + 1}`, @@ -323,11 +323,13 @@ describe('BaseSelectionList', () => { // New visible window should be Items 2, 3, 4 (scroll offset 1). await updateActiveIndex(3); - const output = lastFrame(); - expect(output).not.toContain('Item 1'); - expect(output).toContain('Item 2'); - expect(output).toContain('Item 4'); - expect(output).not.toContain('Item 5'); + await waitFor(() => { + const output = lastFrame(); + expect(output).not.toContain('Item 1'); + expect(output).toContain('Item 2'); + expect(output).toContain('Item 4'); + expect(output).not.toContain('Item 5'); + }); }); it('should scroll up when activeIndex moves before the visible window', async () => { @@ -335,19 +337,23 @@ describe('BaseSelectionList', () => { await updateActiveIndex(4); - let output = lastFrame(); - expect(output).toContain('Item 3'); // Should see items 3, 4, 5 - expect(output).toContain('Item 5'); - expect(output).not.toContain('Item 2'); + await waitFor(() => { + const output = lastFrame(); + expect(output).toContain('Item 3'); // Should see items 3, 4, 5 + expect(output).toContain('Item 5'); + expect(output).not.toContain('Item 2'); + }); // Now test scrolling up: move to index 1 (Item 2) // This should trigger scroll up to show items 2, 3, 4 await updateActiveIndex(1); - output = lastFrame(); - expect(output).toContain('Item 2'); - expect(output).toContain('Item 4'); - expect(output).not.toContain('Item 5'); // Item 5 should no longer be visible + await waitFor(() => { + const output = lastFrame(); + expect(output).toContain('Item 2'); + expect(output).toContain('Item 4'); + expect(output).not.toContain('Item 5'); // Item 5 should no longer be visible + }); }); it('should pin the scroll offset to the end if selection starts near the end', async () => { @@ -375,16 +381,19 @@ describe('BaseSelectionList', () => { expect(lastFrame()).toContain('Item 1'); await updateActiveIndex(3); // Should trigger scroll - let output = lastFrame(); - expect(output).toContain('Item 2'); - expect(output).toContain('Item 4'); - expect(output).not.toContain('Item 1'); - + await waitFor(() => { + const output = lastFrame(); + expect(output).toContain('Item 2'); + expect(output).toContain('Item 4'); + expect(output).not.toContain('Item 1'); + }); await updateActiveIndex(5); // Scroll further - output = lastFrame(); - expect(output).toContain('Item 4'); - expect(output).toContain('Item 6'); - expect(output).not.toContain('Item 3'); + await waitFor(() => { + const output = lastFrame(); + expect(output).toContain('Item 4'); + expect(output).toContain('Item 6'); + expect(output).not.toContain('Item 3'); + }); }); it('should correctly identify the selected item within the visible window', () => { @@ -412,17 +421,16 @@ describe('BaseSelectionList', () => { expect.objectContaining({ value: 'Item 6' }), expect.objectContaining({ isSelected: true }), ); - }); - // Item 4 (index 3) should not be selected - expect(mockRenderItem).toHaveBeenCalledWith( - expect.objectContaining({ value: 'Item 4' }), - expect.objectContaining({ isSelected: false }), - ); + // Item 4 (index 3) should not be selected + expect(mockRenderItem).toHaveBeenCalledWith( + expect.objectContaining({ value: 'Item 4' }), + expect.objectContaining({ isSelected: false }), + ); + }); }); it('should handle maxItemsToShow larger than the list length', () => { - // Test edge case where maxItemsToShow exceeds available items const { lastFrame } = renderComponent( { items: longList, maxItemsToShow: 15 }, 0, diff --git a/packages/cli/src/ui/components/shared/text-buffer.test.ts b/packages/cli/src/ui/components/shared/text-buffer.test.ts index 85555a14cf..9e56856aca 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.test.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.test.ts @@ -953,6 +953,40 @@ describe('useTextBuffer', () => { expect(getBufferState(result).lines).toEqual(['', '']); }); + it('should do nothing for a tab key press', () => { + const { result } = renderHook(() => + useTextBuffer({ viewport, isValidPath: () => false }), + ); + act(() => + result.current.handleInput({ + name: 'tab', + ctrl: false, + meta: false, + shift: false, + paste: false, + sequence: '\t', + }), + ); + expect(getBufferState(result).text).toBe(''); + }); + + it('should do nothing for a shift tab key press', () => { + const { result } = renderHook(() => + useTextBuffer({ viewport, isValidPath: () => false }), + ); + act(() => + result.current.handleInput({ + name: 'tab', + ctrl: false, + meta: false, + shift: true, + paste: false, + sequence: '\u001b[9;2u', + }), + ); + expect(getBufferState(result).text).toBe(''); + }); + it('should handle "Backspace" key', () => { const { result } = renderHook(() => useTextBuffer({ diff --git a/packages/cli/src/ui/components/shared/text-buffer.ts b/packages/cli/src/ui/components/shared/text-buffer.ts index 33548238f6..861017ce03 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.ts @@ -1933,7 +1933,7 @@ export function useTextBuffer({ else if (key.name === 'delete' || (key.ctrl && key.name === 'd')) del(); else if (key.ctrl && !key.shift && key.name === 'z') undo(); else if (key.ctrl && key.shift && key.name === 'z') redo(); - else if (input && !key.ctrl && !key.meta) { + else if (input && !key.ctrl && !key.meta && key.name !== 'tab') { insert(input, { paste: key.paste }); } }, diff --git a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts index 8ad0e7546f..2e103ca234 100644 --- a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts +++ b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts @@ -37,6 +37,7 @@ vi.mock('@google/gemini-cli-core', async () => { interface MockConfigInstanceShape { getApprovalMode: Mock<() => ApprovalMode>; setApprovalMode: Mock<(value: ApprovalMode) => void>; + isYoloModeDisabled: Mock<() => boolean>; isTrustedFolder: Mock<() => boolean>; getCoreTools: Mock<() => string[]>; getToolDiscoveryCommand: Mock<() => string | undefined>; @@ -76,6 +77,7 @@ describe('useAutoAcceptIndicator', () => { setApprovalMode: instanceSetApprovalModeMock as Mock< (value: ApprovalMode) => void >, + isYoloModeDisabled: vi.fn().mockReturnValue(false), isTrustedFolder: vi.fn().mockReturnValue(true) as Mock<() => boolean>, getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>, getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock< @@ -471,6 +473,45 @@ describe('useAutoAcceptIndicator', () => { }); }); + describe('when YOLO mode is disabled by settings', () => { + beforeEach(() => { + // Ensure isYoloModeDisabled returns true for these tests + if (mockConfigInstance && mockConfigInstance.isYoloModeDisabled) { + mockConfigInstance.isYoloModeDisabled.mockReturnValue(true); + } + }); + + it('should not enable YOLO mode when Ctrl+Y is pressed and add an info message', () => { + mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); + const mockAddItem = vi.fn(); + const { result } = renderHook(() => + useAutoAcceptIndicator({ + config: mockConfigInstance as unknown as ActualConfigType, + addItem: mockAddItem, + }), + ); + + expect(result.current).toBe(ApprovalMode.DEFAULT); + + act(() => { + capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); + }); + + // setApprovalMode should not be called because the check should return early + expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled(); + // An info message should be added + expect(mockAddItem).toHaveBeenCalledWith( + { + type: MessageType.WARNING, + text: 'You cannot enter YOLO mode since it is disabled in your settings.', + }, + expect.any(Number), + ); + // The mode should not change + expect(result.current).toBe(ApprovalMode.DEFAULT); + }); + }); + it('should call onApprovalModeChange when switching to YOLO mode', () => { mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); diff --git a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts index ae749a4648..6091420abd 100644 --- a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts +++ b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts @@ -34,6 +34,21 @@ export function useAutoAcceptIndicator({ let nextApprovalMode: ApprovalMode | undefined; if (key.ctrl && key.name === 'y') { + if ( + config.isYoloModeDisabled() && + config.getApprovalMode() !== ApprovalMode.YOLO + ) { + if (addItem) { + addItem( + { + type: MessageType.WARNING, + text: 'You cannot enter YOLO mode since it is disabled in your settings.', + }, + Date.now(), + ); + } + return; + } nextApprovalMode = config.getApprovalMode() === ApprovalMode.YOLO ? ApprovalMode.DEFAULT diff --git a/packages/cli/src/ui/hooks/useToolScheduler.test.ts b/packages/cli/src/ui/hooks/useToolScheduler.test.ts index 9304666246..9fd31b89f9 100644 --- a/packages/cli/src/ui/hooks/useToolScheduler.test.ts +++ b/packages/cli/src/ui/hooks/useToolScheduler.test.ts @@ -32,7 +32,6 @@ import { ApprovalMode, MockTool, } from '@google/gemini-cli-core'; -import type { HistoryItemWithoutId, HistoryItemToolGroup } from '../types.js'; import { ToolCallStatus } from '../types.js'; // Mocks @@ -101,11 +100,9 @@ const mockToolRequiresConfirmation = new MockTool({ describe('useReactToolScheduler in YOLO Mode', () => { let onComplete: Mock; - let setPendingHistoryItem: Mock; beforeEach(() => { onComplete = vi.fn(); - setPendingHistoryItem = vi.fn(); mockToolRegistry.getTool.mockClear(); (mockToolRequiresConfirmation.execute as Mock).mockClear(); (mockToolRequiresConfirmation.shouldConfirmExecute as Mock).mockClear(); @@ -128,7 +125,7 @@ describe('useReactToolScheduler in YOLO Mode', () => { useReactToolScheduler( onComplete, mockConfig as unknown as Config, - setPendingHistoryItem, + () => undefined, () => {}, ), ); @@ -187,26 +184,11 @@ describe('useReactToolScheduler in YOLO Mode', () => { }), }), ]); - - // Ensure no confirmation UI was triggered (setPendingHistoryItem should not have been called with confirmation details) - const setPendingHistoryItemCalls = setPendingHistoryItem.mock.calls; - const confirmationCall = setPendingHistoryItemCalls.find((call) => { - const item = typeof call[0] === 'function' ? call[0]({}) : call[0]; - return item?.tools?.[0]?.confirmationDetails; - }); - expect(confirmationCall).toBeUndefined(); }); }); describe('useReactToolScheduler', () => { - // TODO(ntaylormullen): The following tests are skipped due to difficulties in - // reliably testing the asynchronous state updates and interactions with timers. - // These tests involve complex sequences of events, including confirmations, - // live output updates, and cancellations, which are challenging to assert - // correctly with the current testing setup. Further investigation is needed - // to find a robust way to test these scenarios. let onComplete: Mock; - let setPendingHistoryItem: Mock; let capturedOnConfirmForTest: | ((outcome: ToolConfirmationOutcome) => void | Promise) | undefined; @@ -214,29 +196,6 @@ describe('useReactToolScheduler', () => { beforeEach(() => { onComplete = vi.fn(); capturedOnConfirmForTest = undefined; - setPendingHistoryItem = vi.fn((updaterOrValue) => { - let pendingItem: HistoryItemWithoutId | null = null; - if (typeof updaterOrValue === 'function') { - // Loosen the type for prevState to allow for more flexible updates in tests - const prevState: Partial = { - type: 'tool_group', // Still default to tool_group for most cases - tools: [], - }; - - pendingItem = updaterOrValue(prevState as any); // Allow any for more flexibility - } else { - pendingItem = updaterOrValue; - } - // Capture onConfirm if it exists, regardless of the exact type of pendingItem - // This is a common pattern in these tests. - if ( - (pendingItem as HistoryItemToolGroup)?.tools?.[0]?.confirmationDetails - ?.onConfirm - ) { - capturedOnConfirmForTest = (pendingItem as HistoryItemToolGroup) - .tools[0].confirmationDetails?.onConfirm; - } - }); mockToolRegistry.getTool.mockClear(); (mockTool.execute as Mock).mockClear(); @@ -273,7 +232,7 @@ describe('useReactToolScheduler', () => { useReactToolScheduler( onComplete, mockConfig as unknown as Config, - setPendingHistoryItem, + () => undefined, () => {}, ), ); @@ -448,7 +407,7 @@ describe('useReactToolScheduler', () => { expect(result.current[0]).toEqual([]); }); - it.skip('should handle tool requiring confirmation - approved', async () => { + it('should handle tool requiring confirmation - approved', async () => { mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation); const expectedOutput = 'Confirmed output'; (mockToolRequiresConfirmation.execute as Mock).mockResolvedValue({ @@ -471,7 +430,9 @@ describe('useReactToolScheduler', () => { await vi.runAllTimersAsync(); }); - expect(setPendingHistoryItem).toHaveBeenCalled(); + const waitingCall = result.current[0][0] as any; + expect(waitingCall.status).toBe('awaiting_approval'); + capturedOnConfirmForTest = waitingCall.confirmationDetails?.onConfirm; expect(capturedOnConfirmForTest).toBeDefined(); await act(async () => { @@ -510,7 +471,7 @@ describe('useReactToolScheduler', () => { ]); }); - it.skip('should handle tool requiring confirmation - cancelled by user', async () => { + it('should handle tool requiring confirmation - cancelled by user', async () => { mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation); const { result } = renderScheduler(); const schedule = result.current[1]; @@ -527,7 +488,9 @@ describe('useReactToolScheduler', () => { await vi.runAllTimersAsync(); }); - expect(setPendingHistoryItem).toHaveBeenCalled(); + const waitingCall = result.current[0][0] as any; + expect(waitingCall.status).toBe('awaiting_approval'); + capturedOnConfirmForTest = waitingCall.confirmationDetails?.onConfirm; expect(capturedOnConfirmForTest).toBeDefined(); await act(async () => { @@ -552,7 +515,8 @@ describe('useReactToolScheduler', () => { expect.objectContaining({ functionResponse: expect.objectContaining({ response: expect.objectContaining({ - error: `User did not allow tool call ${request.name}. Reason: User cancelled.`, + error: + '[Operation Cancelled] Reason: User did not allow tool call', }), }), }), @@ -562,7 +526,7 @@ describe('useReactToolScheduler', () => { ]); }); - it.skip('should handle live output updates', async () => { + it('should handle live output updates', async () => { mockToolRegistry.getTool.mockReturnValue(mockToolWithLiveOutput); let liveUpdateFn: ((output: string) => void) | undefined; let resolveExecutePromise: (value: ToolResult) => void; @@ -600,7 +564,7 @@ describe('useReactToolScheduler', () => { }); expect(liveUpdateFn).toBeDefined(); - expect(setPendingHistoryItem).toHaveBeenCalled(); + expect(result.current[0][0].status).toBe('executing'); await act(async () => { liveUpdateFn?.('Live output 1'); @@ -742,7 +706,7 @@ describe('useReactToolScheduler', () => { expect(result.current[0]).toEqual([]); }); - it.skip('should throw error if scheduling while already running', async () => { + it('should queue if scheduling while already running', async () => { mockToolRegistry.getTool.mockReturnValue(mockTool); const longExecutePromise = new Promise((resolve) => setTimeout( @@ -777,9 +741,7 @@ describe('useReactToolScheduler', () => { await vi.runAllTimersAsync(); }); - expect(() => schedule(request2, new AbortController().signal)).toThrow( - 'Cannot schedule tool calls while other tool calls are running', - ); + schedule(request2, new AbortController().signal); await act(async () => { await vi.advanceTimersByTimeAsync(50); @@ -795,6 +757,21 @@ describe('useReactToolScheduler', () => { response: expect.objectContaining({ resultDisplay: 'done display' }), }), ]); + // Wait for request2 to complete + await act(async () => { + await vi.advanceTimersByTimeAsync(50); + await vi.runAllTimersAsync(); + await act(async () => { + await vi.runAllTimersAsync(); + }); + }); + expect(onComplete).toHaveBeenCalledWith([ + expect.objectContaining({ + status: 'success', + request: request2, + response: expect.objectContaining({ resultDisplay: 'done display' }), + }), + ]); expect(result.current[0]).toEqual([]); }); }); diff --git a/packages/core/package.json b/packages/core/package.json index edd640bc43..312b41a099 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli-core", - "version": "0.11.0-nightly.20251021.e72c00cf", + "version": "0.12.0-nightly.20251022.0542de95", "description": "Gemini CLI Core", "repository": { "type": "git", diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 0546f7cba6..d1549c5355 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -995,6 +995,40 @@ describe('setApprovalMode with folder trust', () => { }); }); +describe('isYoloModeDisabled', () => { + const baseParams: ConfigParameters = { + sessionId: 'test', + targetDir: '.', + debugMode: false, + model: 'test-model', + cwd: '.', + }; + + it('should return false when yolo mode is not disabled and folder is trusted', () => { + const config = new Config(baseParams); + vi.spyOn(config, 'isTrustedFolder').mockReturnValue(true); + expect(config.isYoloModeDisabled()).toBe(false); + }); + + it('should return true when yolo mode is disabled by parameter', () => { + const config = new Config({ ...baseParams, disableYoloMode: true }); + vi.spyOn(config, 'isTrustedFolder').mockReturnValue(true); + expect(config.isYoloModeDisabled()).toBe(true); + }); + + it('should return true when folder is untrusted', () => { + const config = new Config(baseParams); + vi.spyOn(config, 'isTrustedFolder').mockReturnValue(false); + expect(config.isYoloModeDisabled()).toBe(true); + }); + + it('should return true when yolo is disabled and folder is untrusted', () => { + const config = new Config({ ...baseParams, disableYoloMode: true }); + vi.spyOn(config, 'isTrustedFolder').mockReturnValue(false); + expect(config.isYoloModeDisabled()).toBe(true); + }); +}); + describe('BaseLlmClient Lifecycle', () => { const MODEL = 'gemini-pro'; const SANDBOX: SandboxConfig = { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index baa8bc6175..e6e859ee00 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -284,6 +284,7 @@ export interface ConfigParameters { retryFetchErrors?: boolean; enableShellOutputEfficiency?: boolean; ptyInfo?: string; + disableYoloMode?: boolean; } export class Config { @@ -380,6 +381,7 @@ export class Config { private readonly continueOnFailedApiCall: boolean; private readonly retryFetchErrors: boolean; private readonly enableShellOutputEfficiency: boolean; + private readonly disableYoloMode: boolean; constructor(params: ConfigParameters) { this.sessionId = params.sessionId; @@ -496,6 +498,7 @@ export class Config { format: params.output?.format ?? OutputFormat.TEXT, }; this.retryFetchErrors = params.retryFetchErrors ?? false; + this.disableYoloMode = params.disableYoloMode ?? false; if (params.contextFileName) { setGeminiMdFilename(params.contextFileName); @@ -761,6 +764,10 @@ export class Config { this.approvalMode = mode; } + isYoloModeDisabled(): boolean { + return this.disableYoloMode || !this.isTrustedFolder(); + } + getShowMemoryUsage(): boolean { return this.showMemoryUsage; } diff --git a/packages/core/src/mcp/oauth-utils.test.ts b/packages/core/src/mcp/oauth-utils.test.ts index 710afe21aa..93aa507e21 100644 --- a/packages/core/src/mcp/oauth-utils.test.ts +++ b/packages/core/src/mcp/oauth-utils.test.ts @@ -297,14 +297,32 @@ describe('OAuthUtils', () => { const result = OAuthUtils.buildResourceParameter( 'https://example.com/oauth/token', ); - expect(result).toBe('https://example.com'); + expect(result).toBe('https://example.com/oauth/token'); }); it('should handle URLs with ports', () => { const result = OAuthUtils.buildResourceParameter( 'https://example.com:8080/oauth/token', ); - expect(result).toBe('https://example.com:8080'); + expect(result).toBe('https://example.com:8080/oauth/token'); + }); + + it('should strip query parameters from the URL', () => { + const result = OAuthUtils.buildResourceParameter( + 'https://example.com/api/v1/data?user=123&scope=read', + ); + expect(result).toBe('https://example.com/api/v1/data'); + }); + + it('should strip URL fragments from the URL', () => { + const result = OAuthUtils.buildResourceParameter( + 'https://example.com/api/v1/data#section-one', + ); + expect(result).toBe('https://example.com/api/v1/data'); + }); + + it('should throw an error for invalid URLs', () => { + expect(() => OAuthUtils.buildResourceParameter('not-a-url')).toThrow(); }); }); }); diff --git a/packages/core/src/mcp/oauth-utils.ts b/packages/core/src/mcp/oauth-utils.ts index 4787422f52..cf6bfc289d 100644 --- a/packages/core/src/mcp/oauth-utils.ts +++ b/packages/core/src/mcp/oauth-utils.ts @@ -360,6 +360,6 @@ export class OAuthUtils { */ static buildResourceParameter(endpointUrl: string): string { const url = new URL(endpointUrl); - return `${url.protocol}//${url.host}`; + return `${url.protocol}//${url.host}${url.pathname}`; } } diff --git a/packages/core/src/services/shellExecutionService.test.ts b/packages/core/src/services/shellExecutionService.test.ts index 45aa9fce97..372077139e 100644 --- a/packages/core/src/services/shellExecutionService.test.ts +++ b/packages/core/src/services/shellExecutionService.test.ts @@ -291,6 +291,54 @@ describe('ShellExecutionService', () => { expect(mockHeadlessTerminal.resize).toHaveBeenCalledWith(100, 40); }); + it('should not resize the pty if it is not active', async () => { + const isPtyActiveSpy = vi + .spyOn(ShellExecutionService, 'isPtyActive') + .mockReturnValue(false); + + await simulateExecution('ls -l', (pty) => { + ShellExecutionService.resizePty(pty.pid!, 100, 40); + pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null }); + }); + + expect(mockPtyProcess.resize).not.toHaveBeenCalled(); + expect(mockHeadlessTerminal.resize).not.toHaveBeenCalled(); + isPtyActiveSpy.mockRestore(); + }); + + it('should ignore errors when resizing an exited pty', async () => { + const resizeError = new Error( + 'Cannot resize a pty that has already exited', + ); + mockPtyProcess.resize.mockImplementation(() => { + throw resizeError; + }); + + // We don't expect this test to throw an error + await expect( + simulateExecution('ls -l', (pty) => { + ShellExecutionService.resizePty(pty.pid!, 100, 40); + pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null }); + }), + ).resolves.not.toThrow(); + + expect(mockPtyProcess.resize).toHaveBeenCalledWith(100, 40); + }); + + it('should re-throw other errors during resize', async () => { + const otherError = new Error('Some other error'); + mockPtyProcess.resize.mockImplementation(() => { + throw otherError; + }); + + await expect( + simulateExecution('ls -l', (pty) => { + ShellExecutionService.resizePty(pty.pid!, 100, 40); + pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null }); + }), + ).rejects.toThrow('Some other error'); + }); + it('should scroll the headless terminal', async () => { await simulateExecution('ls -l', (pty) => { pty.onData.mock.calls[0][0]('file1.txt\n'); diff --git a/packages/core/src/services/shellExecutionService.ts b/packages/core/src/services/shellExecutionService.ts index d5dc17bb14..eeec882ca8 100644 --- a/packages/core/src/services/shellExecutionService.ts +++ b/packages/core/src/services/shellExecutionService.ts @@ -750,7 +750,11 @@ export class ShellExecutionService { } catch (e) { // Ignore errors if the pty has already exited, which can happen // due to a race condition between the exit event and this call. - if (e instanceof Error && 'code' in e && e.code === 'ESRCH') { + if ( + e instanceof Error && + (('code' in e && e.code === 'ESRCH') || + e.message === 'Cannot resize a pty that has already exited') + ) { // ignore } else { throw e; diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index cede1516ce..2ab3cf2441 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -1269,8 +1269,6 @@ export class ClearcutLogger { } getConfigJson() { - const configJson = safeJsonStringifyBooleanValuesOnly(this.config); - debugLogger.debug(configJson); return safeJsonStringifyBooleanValuesOnly(this.config); } diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index ac0c39bc56..7c3e5ba2d2 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli-test-utils", - "version": "0.11.0-nightly.20251021.e72c00cf", + "version": "0.12.0-nightly.20251022.0542de95", "private": true, "main": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index 4a1bc82438..82c54e1601 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -2,7 +2,7 @@ "name": "gemini-cli-vscode-ide-companion", "displayName": "Gemini CLI Companion", "description": "Enable Gemini CLI with direct access to your IDE workspace.", - "version": "0.11.0-nightly.20251021.e72c00cf", + "version": "0.12.0-nightly.20251022.0542de95", "publisher": "google", "icon": "assets/icon.png", "repository": { diff --git a/scripts/get-release-version.js b/scripts/get-release-version.js index 33d955b684..442eb4444d 100644 --- a/scripts/get-release-version.js +++ b/scripts/get-release-version.js @@ -424,7 +424,13 @@ export function getVersion(options = {}) { } break; case 'promote-nightly': - versionData = promoteNightlyVersion(); + versionData = promoteNightlyVersion({ args }); + // A promoted nightly version is still a nightly, so we should check for conflicts. + if (doesVersionExist({ args, version: versionData.releaseVersion })) { + throw new Error( + `Version conflict! Promoted nightly version ${versionData.releaseVersion} already exists.`, + ); + } break; case 'stable': versionData = getStableVersion(args); diff --git a/scripts/lint.js b/scripts/lint.js index 008f7cb209..a9613dfac9 100644 --- a/scripts/lint.js +++ b/scripts/lint.js @@ -142,7 +142,7 @@ export function setupLinters() { export function runESLint() { console.log('\nRunning ESLint...'); - if (!runCommand('npm run lint:ci')) { + if (!runCommand('npm run lint')) { process.exit(1); } }