Pipeline step 4 dogfood. `/config-audit rollback` could not see a single one of
the four real backups on this machine, and reported "Backup not found" for one
that was sitting right there.
Four defects, one root: nothing agreed on where a backup lives or what its
manifest looks like.
- M-BUG-22 `lib/backup.mjs` resolved `~/.config-audit/backups` (pre-v2.2.0)
while every command, agent and doc uses `~/.claude/config-audit/backups`.
The auto-backup hook and fix-cli wrote to the first, implement to the second,
rollback read only the first. Canonical root now, with the legacy root kept
readable so older backups stay listable and restorable (`legacy: true`).
- M-BUG-25 `parseManifest` understood only the engine's quoted `original_path:`
spelling, but implement hand-builds its manifest with `- backup:`/`original:`/
`sha256:`. Every implement-made backup parsed to zero files and restoreBackup
returned `{restored: [], failed: []}` — a success-shaped no-op. Both formats
parse now, and a manifest with unparseable entries throws instead of
pretending to succeed.
- M-BUG-23 both session hooks watched `~/.config-audit/sessions`, which does not
exist; sessions live under `~/.claude/`. "Check for active sessions" had never
fired once. It fires now.
- M-BUG-24 the suite called createBackup() against the developer's real home —
it had left nine stray backups there, and cleanupOldBackups() deletes past ten.
Root is overridable via CONFIG_AUDIT_BACKUP_ROOT; both test files use it.
Rollback still cannot delete files implement CREATED — no backup can hold a file
that never existed. It no longer does so silently: manifests carry a `created:`
list, restoreBackup returns `createdNotRemoved`, and rollback.md requires the
report. Automatic deletion is a destructive action and needs its own design.
Verified against backup 20260717_032636 on a throwaway copy: all three files
restore byte-exact (sha256 match), zero writes outside the copy, backup dir
unmodified. Suite 1382 -> 1398/0; frozen v5.0.0 snapshots untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SejM9RQAa1Hfuq7Ek2WfFr
70 lines
2.2 KiB
JavaScript
70 lines
2.2 KiB
JavaScript
#!/usr/bin/env node
|
|
// Remind about current config-audit session phase on session end
|
|
// Returns JSON: {} if no active session, systemMessage if active
|
|
|
|
import { readdirSync, readFileSync, statSync, existsSync } from 'fs';
|
|
import { join, basename, dirname } from 'path';
|
|
import { homedir } from 'os';
|
|
|
|
// Canonical location since v2.2.0. The pre-v2.2.0 path is kept as a fallback so
|
|
// sessions created before the move are still detected (see commands/cleanup.md).
|
|
const canonicalSessionsDir = join(homedir(), '.claude', 'config-audit', 'sessions');
|
|
const legacySessionsDir = join(homedir(), '.config-audit', 'sessions');
|
|
const sessionsDir = existsSync(canonicalSessionsDir) ? canonicalSessionsDir : legacySessionsDir;
|
|
|
|
if (!existsSync(sessionsDir)) {
|
|
console.log('{}');
|
|
process.exit(0);
|
|
}
|
|
|
|
function parseYamlValue(content, key) {
|
|
const match = content.match(new RegExp(`${key}:\\s*"?([^"\\n]*)"?`));
|
|
return match ? match[1].trim() : '';
|
|
}
|
|
|
|
let latestState = '';
|
|
let latestTime = 0;
|
|
|
|
try {
|
|
const entries = readdirSync(sessionsDir, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory()) continue;
|
|
|
|
const stateFile = join(sessionsDir, entry.name, 'state.yaml');
|
|
if (!existsSync(stateFile)) continue;
|
|
|
|
const fileTime = statSync(stateFile).mtimeMs;
|
|
if (fileTime > latestTime) {
|
|
latestTime = fileTime;
|
|
latestState = stateFile;
|
|
}
|
|
}
|
|
} catch {
|
|
console.log('{}');
|
|
process.exit(0);
|
|
}
|
|
|
|
if (latestState) {
|
|
// Only remind if session was touched in the last 2 hours (active work)
|
|
const twoHoursMs = 2 * 60 * 60 * 1000;
|
|
if (Date.now() - latestTime > twoHoursMs) {
|
|
console.log('{}');
|
|
process.exit(0);
|
|
}
|
|
|
|
const content = readFileSync(latestState, 'utf-8');
|
|
const currentPhase = parseYamlValue(content, 'current_phase');
|
|
const nextPhase = parseYamlValue(content, 'next_phase');
|
|
|
|
if (currentPhase && currentPhase !== 'verify' && currentPhase !== 'complete') {
|
|
const sessionId = basename(dirname(latestState));
|
|
console.log(JSON.stringify({
|
|
systemMessage: `config-audit: Session ${sessionId} is at phase '${currentPhase}'. Next: /config-audit ${nextPhase}`
|
|
}));
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
console.log('{}');
|
|
process.exit(0);
|