* fix(upgrade): refresh installer-written agent surfaces after a binary upgrade codegraph upgrade swapped the binary but never revisited what earlier installs wrote into CLAUDE.md / AGENTS.md / GEMINI.md and the agent configs, so sections written by a pre-1.0 installer kept teaching agents a multi-tool surface (including tools that no longer exist) months of releases later. The install path already self-heals everything it owns, but nothing ever called it on upgrade. - codegraph install --refresh: non-interactive sweep that re-runs install() for already-configured targets only — never a first install; permissions and prompt-hook choices are preserved. - codegraph upgrade spawns it via the freshly-installed binary after a successful swap (the still-running old process would only rewrite its own stale template). Gated on PATH resolution and the CODEGRAPH_NO_INSTALL_REFRESH=1 kill-switch; never fatal to the upgrade. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(installer): clarify refresh change reporting --------- Co-authored-by: xuing <np2v9bvbbs@privaterelay.appleid.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Colby McHenry <me@colbymchenry.com>
This commit is contained in:
co-authored by
Claude Fable 5
xuing
Colby McHenry
parent
63eb488ed4
commit
386bff0f84
@@ -19,7 +19,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { ALL_TARGETS, getTarget, resolveTargetFlag } from '../src/installer/targets/registry';
|
||||
import { uninstallTargets } from '../src/installer';
|
||||
import { uninstallTargets, refreshTargets } from '../src/installer';
|
||||
import { upsertTomlTable, removeTomlTable, buildTomlTable } from '../src/installer/targets/toml';
|
||||
import { cleanupLegacyHooks, writePromptHookEntry, removePromptHookEntry } from '../src/installer/targets/claude';
|
||||
|
||||
@@ -1396,6 +1396,89 @@ describe('Installer — uninstallTargets sweep (codegraph uninstall)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Installer — refreshTargets sweep (codegraph install --refresh)', () => {
|
||||
let tmpHome: string;
|
||||
let tmpCwd: string;
|
||||
let origCwd: string;
|
||||
let homeRestore: { restore: () => void };
|
||||
|
||||
beforeEach(() => {
|
||||
tmpHome = mkTmpDir('rf-home');
|
||||
tmpCwd = mkTmpDir('rf-cwd');
|
||||
origCwd = process.cwd();
|
||||
process.chdir(tmpCwd);
|
||||
homeRestore = setHome(tmpHome);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
homeRestore.restore();
|
||||
process.chdir(origCwd);
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
fs.rmSync(tmpCwd, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('rewrites a stale instructions block a previous version left, and reports refreshed', () => {
|
||||
const claude = getTarget('claude')!;
|
||||
claude.install('global', { autoAllow: true });
|
||||
|
||||
// Simulate the file as an old install left it: same markers, the old
|
||||
// multi-tool wording.
|
||||
const claudeMd = path.join(tmpHome, '.claude', 'CLAUDE.md');
|
||||
fs.writeFileSync(claudeMd, LEGACY_BLOCK + '\n');
|
||||
|
||||
const reports = refreshTargets([claude], 'global');
|
||||
expect(reports[0].status).toBe('refreshed');
|
||||
expect(reports[0].changedPaths).toContain(claudeMd);
|
||||
|
||||
const md = fs.readFileSync(claudeMd, 'utf-8');
|
||||
expect(md).not.toContain('codegraph_search');
|
||||
expect(md).toContain('codegraph_explore');
|
||||
});
|
||||
|
||||
it('never performs a first install — unconfigured agents stay untouched', () => {
|
||||
const reports = refreshTargets(ALL_TARGETS, 'global');
|
||||
for (const t of ALL_TARGETS) {
|
||||
const r = reports.find((x) => x.id === t.id)!;
|
||||
expect(r.status).toBe(t.supportsLocation('global') ? 'not-configured' : 'unsupported');
|
||||
expect(r.changedPaths).toEqual([]);
|
||||
expect(t.detect('global').alreadyConfigured).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('preserves the user\'s permission choices (refresh never writes permissions)', () => {
|
||||
const claude = getTarget('claude')!;
|
||||
claude.install('global', { autoAllow: true });
|
||||
|
||||
// The user has since trimmed the allowlist by hand.
|
||||
const settingsPath = path.join(tmpHome, '.claude', 'settings.json');
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
||||
settings.permissions.allow = [];
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
|
||||
refreshTargets([claude], 'global');
|
||||
|
||||
const after = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
||||
expect(after.permissions.allow).toEqual([]);
|
||||
});
|
||||
|
||||
it('is idempotent — a second sweep on a current machine reports unchanged everywhere', () => {
|
||||
for (const t of ALL_TARGETS) {
|
||||
if (t.supportsLocation('global')) t.install('global', { autoAllow: true });
|
||||
}
|
||||
const first = refreshTargets(ALL_TARGETS, 'global');
|
||||
// Fresh installs are already current, so even the first sweep may be
|
||||
// all-unchanged; what matters is the second definitely is.
|
||||
const second = refreshTargets(ALL_TARGETS, 'global');
|
||||
for (const r of [...first, ...second]) {
|
||||
expect(['unchanged', 'refreshed']).toContain(r.status);
|
||||
}
|
||||
for (const r of second) {
|
||||
expect(r.status).toBe('unchanged');
|
||||
expect(r.changedPaths).toEqual([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Installer — Cursor rules file cleanup on uninstall', () => {
|
||||
let tmpHome: string;
|
||||
let tmpCwd: string;
|
||||
|
||||
Reference in New Issue
Block a user