fix(installer): log CLAUDE.md as already up to date when install leaves it unchanged (#4377)

executeClaudeMdTransaction drops write operations whose planned bytes equal
the existing file, so a repeated install over an up-to-date CLAUDE.md returns
no CLAUDE.md write. install() chose 'Created CLAUDE.md' whenever no write with
existedBefore was present, so the second run logged 'Created CLAUDE.md' for a
file that already existed and was not modified.

Look up the CLAUDE.md write operation first. No operation logs
'CLAUDE.md already up to date'; an operation with existedBefore keeps
'Updated CLAUDE.md (merged with existing content)'; otherwise
'Created CLAUDE.md' is unchanged.

Adds a test that installs twice into one temp config dir and checks the
second run logs the new message, not 'Created CLAUDE.md', and leaves the file
byte-identical.

Co-authored-by: gaebal-gajae <clawdbot@users.noreply.github.com>
Co-authored-by: gaebal-gajae <gaebal-gajae@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-14 06:29:21 +01:00
1 parent 410f424c7e
commit 51bec397e7
3 files changed
+46 -8

No files matched your search

+5 -5
View File
@@ -5,15 +5,15 @@
"provenance": {
"base": "05c800f40d1ad53b42a78609d2667ef4f726808b",
"planningHead": "0a91273e61dbbd47eb0af4c02844409251e08398",
"head": "fa0d0b1357fd6a053dd54640d66f1780272a5839",
"sourceSha256": "0d81f6b34855bc712fbadff4ff960cdc2217040c2d880a08b331bc01cbf270a6",
"head": "36dd164b7200d34d31840f9522e3758d2372916b",
"sourceSha256": "29c9bef99d25ae952beb32f0b2665eef0290f836d5bfdb15e81e778100eb5b58",
"generatedAt": null,
"generator": "scripts/generate-inventory-graph.mjs"
},
"base": "05c800f40d1ad53b42a78609d2667ef4f726808b",
"planningHead": "0a91273e61dbbd47eb0af4c02844409251e08398",
"head": "fa0d0b1357fd6a053dd54640d66f1780272a5839",
"sourceSha256": "0d81f6b34855bc712fbadff4ff960cdc2217040c2d880a08b331bc01cbf270a6",
"head": "36dd164b7200d34d31840f9522e3758d2372916b",
"sourceSha256": "29c9bef99d25ae952beb32f0b2665eef0290f836d5bfdb15e81e778100eb5b58",
"counts": {
"public": {
"skills": 48,
@@ -88363,5 +88363,5 @@
}
},
"inventorySha256": "d11a3ba23d8ffd964115b1f162f7d2e98bcce5564b14699355479249007ebecd",
"manifestSha256": "14757239a259fe1756ffdca667b2c4c9c94f94aeb3212a11ecd97f729b0ff608"
"manifestSha256": "89811521915ee6ac40a8c3315eb97a0dee0c1f187255125c4b23db6488374ce8"
}
@@ -291,4 +291,37 @@ describe('installer bundled + standalone skill sync', () => {
logSpy.mockRestore();
}
});
it('reports an unchanged CLAUDE.md as already up to date on a repeated install', async () => {
const claudeMdPath = join(claudeConfigDir, 'CLAUDE.md');
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
try {
const firstInstaller = await loadInstallerWithEnv(claudeConfigDir, homeDir);
const firstResult = firstInstaller.install({
skipClaudeCheck: true,
skipHud: true,
verbose: true,
});
expect(firstResult.success).toBe(true);
expect(logSpy).toHaveBeenCalledWith('Created CLAUDE.md');
const firstContent = readFileSync(claudeMdPath, 'utf-8');
logSpy.mockClear();
const secondInstaller = await loadInstallerWithEnv(claudeConfigDir, homeDir);
const secondResult = secondInstaller.install({
skipClaudeCheck: true,
skipHud: true,
verbose: true,
});
expect(secondResult.success).toBe(true);
expect(logSpy).toHaveBeenCalledWith('CLAUDE.md already up to date');
expect(logSpy).not.toHaveBeenCalledWith('Created CLAUDE.md');
expect(logSpy).not.toHaveBeenCalledWith('Updated CLAUDE.md (merged with existing content)');
expect(readFileSync(claudeMdPath, 'utf-8')).toBe(firstContent);
} finally {
logSpy.mockRestore();
}
});
});
+8 -3
View File
@@ -2733,9 +2733,14 @@ export function install(options: InstallOptions = {}): InstallResult {
});
if (!transaction.ok) throw new Error(transaction.error ?? 'CLAUDE.md transaction failed');
for (const backupPath of transaction.backups) log(`Backed up existing CLAUDE.md to ${backupPath}`);
log(transaction.operations.some(operation => operation.type === 'write' && operation.existedBefore && basename(operation.path) === 'CLAUDE.md')
? 'Updated CLAUDE.md (merged with existing content)'
: 'Created CLAUDE.md');
// The transaction drops writes whose bytes already match, so a missing
// CLAUDE.md write op means the existing file was left untouched.
const claudeMdWrite = transaction.operations.find(operation => operation.type === 'write' && basename(operation.path) === 'CLAUDE.md');
log(!claudeMdWrite
? 'CLAUDE.md already up to date'
: claudeMdWrite.existedBefore
? 'Updated CLAUDE.md (merged with existing content)'
: 'Created CLAUDE.md');
}
// Install HUD statusline (skip for project-scoped plugins, skipHud option, or hudEnabled config)