fix(installer): keep --refresh off the configs of unconfigured agents (#1870) (#1873)

`readJsonFile` copied an unparseable file to `<path>.backup` and warned
that it was about to be overwritten. That is the right thing to do just
before a write, but every target's `detect()` reads its agent's config
through the same helper — and `install --refresh` detects every target
in order to skip the ones codegraph was never installed into.

So an empty `~/.gemini/config/mcp_config.json`, sitting beside an
Antigravity install codegraph does not manage, got a 0-byte
`mcp_config.json.backup` and a warning about an overwrite that never
came. Detection is meant to be read-only.

`readJsonFile` is now a pure read. Preserving an unparseable config
moves to `writeJsonFile`, the one place a target replaces a JSON config
— so the backup still happens wherever it mattered, and only there.
Reads that decide not to write (the legacy-entry cleanups, the uninstall
sweeps) no longer leave one behind either.

Unchanged on purpose: an install that really does overwrite an empty
config still backs it up. Skipping the copy when there is nothing to
preserve is a separate call.
This commit is contained in:
Lukas
2026-09-27 12:25:05 +00:00
committed by GitHub
parent 53fe53deb5
commit 98718cf6db
4 changed files with 126 additions and 11 deletions
+37 -10
View File
@@ -51,9 +51,16 @@ export function getCodeGraphPermissions(): string[] {
/**
* Read a JSON file, returning `{}` when missing or unparseable.
*
* Unparseable files are backed up to `<path>.backup` BEFORE we return
* `{}` — so an idempotent re-run never silently deletes a user's
* existing config that happened to break JSON parse temporarily.
* Read-only, and deliberately silent: every target's `detect()` reads
* its agent's config on every `codegraph install`, and most of those
* agents codegraph was never installed into. A warning about a file we
* are not going to touch is noise, and a `<path>.backup` beside it is a
* bug — `install --refresh` promises unconfigured agents are "skipped
* untouched", and an empty `~/.gemini/config/mcp_config.json` was
* getting backed up by the detection pass alone (issue #1870).
*
* Preserving an unparseable config is `writeJsonFile`'s job — it runs
* only where one is actually about to be overwritten.
*/
export function readJsonFile(filePath: string): Record<string, any> {
if (!fs.existsSync(filePath)) {
@@ -61,13 +68,7 @@ export function readJsonFile(filePath: string): Record<string, any> {
}
try {
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.warn(` Warning: Could not parse ${path.basename(filePath)}: ${msg}`);
console.warn(` A backup will be created before overwriting.`);
try {
fs.copyFileSync(filePath, filePath + '.backup');
} catch { /* ignore backup failure */ }
} catch {
return {};
}
}
@@ -93,11 +94,37 @@ export function atomicWriteFileSync(filePath: string, content: string): void {
}
}
/**
* Copy a file we're about to overwrite to `<path>.backup` when it isn't
* parseable JSON — `readJsonFile` handed the caller `{}` for it, so the
* write is a replacement, not an edit, and without this the user's
* config would be gone. No-op when the file is absent or parses fine.
*/
function backupUnparseableJson(filePath: string): void {
if (!fs.existsSync(filePath)) return;
try {
JSON.parse(fs.readFileSync(filePath, 'utf-8'));
return;
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.warn(` Warning: Could not parse ${path.basename(filePath)}: ${msg}`);
console.warn(` A backup will be created before overwriting.`);
}
try {
fs.copyFileSync(filePath, filePath + '.backup');
} catch { /* ignore backup failure */ }
}
/**
* Atomic JSON write. Trailing newline matches the convention every
* existing target had — preserves diff-friendly file shape.
*
* The single point where a target replaces a JSON config, so it is also
* where an unparseable one gets preserved (issue #1870) — reading a
* config no longer has that side effect.
*/
export function writeJsonFile(filePath: string, data: Record<string, any>): void {
backupUnparseableJson(filePath);
atomicWriteFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
}
+7
View File
@@ -93,6 +93,13 @@ export interface AgentTarget {
* pair lets the orchestrator skip cleanly with a clear message.
*/
supportsLocation(loc: Location): boolean;
/**
* Read-only, and quiet. Every target is detected on every run —
* including agents codegraph was never installed into — and
* `install --refresh` skips the unconfigured ones on the strength of
* this answer, so a target that writes or warns here touches a config
* it has no business touching (issue #1870).
*/
detect(loc: Location): DetectionResult;
install(loc: Location, opts: InstallOptions): WriteResult;
/**