55 */
66
77import * as fs from 'node:fs' ;
8+ import * as path from 'node:path' ;
89import { parse , stringify } from 'comment-json' ;
910import { coreEvents } from '@google/gemini-cli-core' ;
1011
@@ -13,25 +14,82 @@ import { coreEvents } from '@google/gemini-cli-core';
1314 */
1415type CommentedRecord = Record < string | symbol , unknown > ;
1516
17+ function isDangerousKey ( key : string ) : boolean {
18+ return key === '__proto__' || key === 'constructor' || key === 'prototype' ;
19+ }
20+
21+ let tempCounter = 0 ;
22+
23+ function writeAtomicSync ( filePath : string , content : string ) : void {
24+ const dir = path . dirname ( filePath ) ;
25+ if ( ! fs . existsSync ( dir ) ) {
26+ fs . mkdirSync ( dir , { recursive : true } ) ;
27+ }
28+ const tempPath = path . join (
29+ dir ,
30+ `.${ path . basename ( filePath ) } .${ process . pid } .${ Date . now ( ) } .${ tempCounter ++ } .tmp` ,
31+ ) ;
32+ try {
33+ fs . writeFileSync ( tempPath , content , 'utf-8' ) ;
34+ fs . renameSync ( tempPath , filePath ) ;
35+ } catch {
36+ try {
37+ if ( fs . existsSync ( tempPath ) ) {
38+ fs . unlinkSync ( tempPath ) ;
39+ }
40+ } catch {
41+ // ignore
42+ }
43+ // Fallback to direct write if rename fails
44+ fs . writeFileSync ( filePath , content , 'utf-8' ) ;
45+ }
46+ }
47+
1648/**
1749 * Updates a JSON file while preserving comments and formatting.
1850 */
1951export function updateSettingsFilePreservingFormat (
2052 filePath : string ,
2153 updates : Record < string , unknown > ,
2254) : void {
55+ const dirPath = path . dirname ( filePath ) ;
56+ if ( ! fs . existsSync ( dirPath ) ) {
57+ fs . mkdirSync ( dirPath , { recursive : true } ) ;
58+ }
59+
2360 if ( ! fs . existsSync ( filePath ) ) {
24- fs . writeFileSync ( filePath , JSON . stringify ( updates , null , 2 ) , 'utf-8' ) ;
61+ writeAtomicSync ( filePath , JSON . stringify ( updates , null , 2 ) ) ;
2562 return ;
2663 }
2764
28- const originalContent = fs . readFileSync ( filePath , 'utf-8' ) ;
29-
3065 let parsed : Record < string , unknown > ;
3166 try {
32- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
33- parsed = parse ( originalContent ) as Record < string , unknown > ;
34- } catch ( error ) {
67+ const originalContent = fs . readFileSync ( filePath , 'utf-8' ) ;
68+ if ( ! originalContent . trim ( ) ) {
69+ parsed = { } ;
70+ } else {
71+ const rawParsed : unknown = parse ( originalContent ) ;
72+ if (
73+ typeof rawParsed !== 'object' ||
74+ rawParsed === null ||
75+ Array . isArray ( rawParsed )
76+ ) {
77+ parsed = { } ;
78+ } else {
79+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
80+ parsed = rawParsed as Record < string , unknown > ;
81+ }
82+ }
83+ } catch ( error : unknown ) {
84+ if (
85+ error &&
86+ typeof error === 'object' &&
87+ 'code' in error &&
88+ error . code === 'ENOENT'
89+ ) {
90+ writeAtomicSync ( filePath , JSON . stringify ( updates , null , 2 ) ) ;
91+ return ;
92+ }
3593 coreEvents . emitFeedback (
3694 'error' ,
3795 'Error parsing settings file. Please check the JSON syntax.' ,
@@ -43,7 +101,7 @@ export function updateSettingsFilePreservingFormat(
43101 const updatedStructure = applyUpdates ( parsed , updates ) ;
44102 const updatedContent = stringify ( updatedStructure , null , 2 ) ;
45103
46- fs . writeFileSync ( filePath , updatedContent , 'utf-8' ) ;
104+ writeAtomicSync ( filePath , updatedContent ) ;
47105}
48106
49107/**
@@ -58,6 +116,9 @@ function preserveCommentsOnPropertyDeletion(
58116 container : Record < string , unknown > ,
59117 propName : string ,
60118) : void {
119+ if ( isDangerousKey ( propName ) ) {
120+ return ;
121+ }
61122 const target = container as CommentedRecord ;
62123 const beforeSym = Symbol . for ( `before:${ propName } ` ) ;
63124 const afterSym = Symbol . for ( `after:${ propName } ` ) ;
@@ -117,13 +178,19 @@ function applyKeyDiff(
117178 desired : Record < string , unknown > ,
118179) : void {
119180 for ( const existingKey of Object . getOwnPropertyNames ( base ) ) {
181+ if ( isDangerousKey ( existingKey ) ) {
182+ continue ;
183+ }
120184 if ( ! Object . prototype . hasOwnProperty . call ( desired , existingKey ) ) {
121185 preserveCommentsOnPropertyDeletion ( base , existingKey ) ;
122186 delete base [ existingKey ] ;
123187 }
124188 }
125189
126190 for ( const nextKey of Object . getOwnPropertyNames ( desired ) ) {
191+ if ( isDangerousKey ( nextKey ) ) {
192+ continue ;
193+ }
127194 const nextVal = desired [ nextKey ] ;
128195 const baseVal = base [ nextKey ] ;
129196
0 commit comments