mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-28 22:52:00 +08:00
`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:
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user