mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-05 02:40:55 -07:00
Co-authored-by: Shreya Keshive <skeshive@gmail.com>
This commit is contained in:
182
packages/cli/src/utils/commentJson.test.ts
Normal file
182
packages/cli/src/utils/commentJson.test.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { updateSettingsFilePreservingFormat } from './commentJson.js';
|
||||
|
||||
describe('commentJson', () => {
|
||||
let tempDir: string;
|
||||
let testFilePath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a temporary directory for test files
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'preserve-format-test-'));
|
||||
testFilePath = path.join(tempDir, 'settings.json');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up temporary directory
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('updateSettingsFilePreservingFormat', () => {
|
||||
it('should preserve comments when updating settings', () => {
|
||||
const originalContent = `{
|
||||
// Model configuration
|
||||
"model": "gemini-2.5-pro",
|
||||
"ui": {
|
||||
// Theme setting
|
||||
"theme": "dark"
|
||||
}
|
||||
}`;
|
||||
|
||||
fs.writeFileSync(testFilePath, originalContent, 'utf-8');
|
||||
|
||||
updateSettingsFilePreservingFormat(testFilePath, {
|
||||
model: 'gemini-3.0-ultra',
|
||||
});
|
||||
|
||||
const updatedContent = fs.readFileSync(testFilePath, 'utf-8');
|
||||
|
||||
expect(updatedContent).toContain('// Model configuration');
|
||||
expect(updatedContent).toContain('// Theme setting');
|
||||
expect(updatedContent).toContain('"model": "gemini-3.0-ultra"');
|
||||
expect(updatedContent).toContain('"theme": "dark"');
|
||||
});
|
||||
|
||||
it('should handle nested object updates', () => {
|
||||
const originalContent = `{
|
||||
"ui": {
|
||||
"theme": "dark",
|
||||
"showLineNumbers": true
|
||||
}
|
||||
}`;
|
||||
|
||||
fs.writeFileSync(testFilePath, originalContent, 'utf-8');
|
||||
|
||||
updateSettingsFilePreservingFormat(testFilePath, {
|
||||
ui: {
|
||||
theme: 'light',
|
||||
showLineNumbers: true,
|
||||
},
|
||||
});
|
||||
|
||||
const updatedContent = fs.readFileSync(testFilePath, 'utf-8');
|
||||
expect(updatedContent).toContain('"theme": "light"');
|
||||
expect(updatedContent).toContain('"showLineNumbers": true');
|
||||
});
|
||||
|
||||
it('should add new fields while preserving existing structure', () => {
|
||||
const originalContent = `{
|
||||
// Existing config
|
||||
"model": "gemini-2.5-pro"
|
||||
}`;
|
||||
|
||||
fs.writeFileSync(testFilePath, originalContent, 'utf-8');
|
||||
|
||||
updateSettingsFilePreservingFormat(testFilePath, {
|
||||
model: 'gemini-2.5-pro',
|
||||
newField: 'newValue',
|
||||
});
|
||||
|
||||
const updatedContent = fs.readFileSync(testFilePath, 'utf-8');
|
||||
expect(updatedContent).toContain('// Existing config');
|
||||
expect(updatedContent).toContain('"newField": "newValue"');
|
||||
});
|
||||
|
||||
it('should create file if it does not exist', () => {
|
||||
updateSettingsFilePreservingFormat(testFilePath, {
|
||||
model: 'gemini-2.5-pro',
|
||||
});
|
||||
|
||||
expect(fs.existsSync(testFilePath)).toBe(true);
|
||||
const content = fs.readFileSync(testFilePath, 'utf-8');
|
||||
expect(content).toContain('"model": "gemini-2.5-pro"');
|
||||
});
|
||||
|
||||
it('should handle complex real-world scenario', () => {
|
||||
const complexContent = `{
|
||||
// Settings
|
||||
"model": "gemini-2.5-pro",
|
||||
"mcpServers": {
|
||||
// Active server
|
||||
"context7": {
|
||||
"headers": {
|
||||
"API_KEY": "test-key" // API key
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
fs.writeFileSync(testFilePath, complexContent, 'utf-8');
|
||||
|
||||
updateSettingsFilePreservingFormat(testFilePath, {
|
||||
model: 'gemini-3.0-ultra',
|
||||
mcpServers: {
|
||||
context7: {
|
||||
headers: {
|
||||
API_KEY: 'new-test-key',
|
||||
},
|
||||
},
|
||||
},
|
||||
newSection: {
|
||||
setting: 'value',
|
||||
},
|
||||
});
|
||||
|
||||
const updatedContent = fs.readFileSync(testFilePath, 'utf-8');
|
||||
|
||||
// Verify comments preserved
|
||||
expect(updatedContent).toContain('// Settings');
|
||||
expect(updatedContent).toContain('// Active server');
|
||||
expect(updatedContent).toContain('// API key');
|
||||
|
||||
// Verify updates applied
|
||||
expect(updatedContent).toContain('"model": "gemini-3.0-ultra"');
|
||||
expect(updatedContent).toContain('"newSection"');
|
||||
expect(updatedContent).toContain('"API_KEY": "new-test-key"');
|
||||
});
|
||||
|
||||
it('should handle corrupted JSON files gracefully', () => {
|
||||
const corruptedContent = `{
|
||||
"model": "gemini-2.5-pro",
|
||||
"ui": {
|
||||
"theme": "dark"
|
||||
// Missing closing brace
|
||||
`;
|
||||
|
||||
fs.writeFileSync(testFilePath, corruptedContent, 'utf-8');
|
||||
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
expect(() => {
|
||||
updateSettingsFilePreservingFormat(testFilePath, {
|
||||
model: 'gemini-3.0-ultra',
|
||||
});
|
||||
}).not.toThrow();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error parsing settings file:',
|
||||
expect.any(Error),
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Settings file may be corrupted. Please check the JSON syntax.',
|
||||
);
|
||||
|
||||
const unchangedContent = fs.readFileSync(testFilePath, 'utf-8');
|
||||
expect(unchangedContent).toBe(corruptedContent);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
67
packages/cli/src/utils/commentJson.ts
Normal file
67
packages/cli/src/utils/commentJson.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { parse, stringify } from 'comment-json';
|
||||
|
||||
/**
|
||||
* Updates a JSON file while preserving comments and formatting.
|
||||
*/
|
||||
export function updateSettingsFilePreservingFormat(
|
||||
filePath: string,
|
||||
updates: Record<string, unknown>,
|
||||
): void {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
fs.writeFileSync(filePath, JSON.stringify(updates, null, 2), 'utf-8');
|
||||
return;
|
||||
}
|
||||
|
||||
const originalContent = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
let parsed: Record<string, unknown>;
|
||||
try {
|
||||
parsed = parse(originalContent) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
console.error('Error parsing settings file:', error);
|
||||
console.error(
|
||||
'Settings file may be corrupted. Please check the JSON syntax.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedStructure = applyUpdates(parsed, updates);
|
||||
const updatedContent = stringify(updatedStructure, null, 2);
|
||||
|
||||
fs.writeFileSync(filePath, updatedContent, 'utf-8');
|
||||
}
|
||||
|
||||
function applyUpdates(
|
||||
current: Record<string, unknown>,
|
||||
updates: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const result = current;
|
||||
|
||||
for (const key of Object.getOwnPropertyNames(updates)) {
|
||||
const value = updates[key];
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
typeof result[key] === 'object' &&
|
||||
result[key] !== null &&
|
||||
!Array.isArray(result[key])
|
||||
) {
|
||||
result[key] = applyUpdates(
|
||||
result[key] as Record<string, unknown>,
|
||||
value as Record<string, unknown>,
|
||||
);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user