feat: migrate legacy codex hooks cleanup
This commit is contained in:
@@ -777,76 +777,6 @@ function rewriteLegacyCodexHookBlock(content, absoluteRunner) {
|
||||
return { content: updated, changed };
|
||||
}
|
||||
|
||||
function isManagedCodexHookCommand(command, targetDir) {
|
||||
if (typeof command !== 'string') return false;
|
||||
if (typeof targetDir !== 'string' || targetDir.length === 0) return false;
|
||||
const normalizedCommand = command.replace(/\\/g, '/');
|
||||
const managedHooksDir = `${path.join(targetDir, 'hooks').replace(/\\/g, '/')}/`;
|
||||
if (!normalizedCommand.includes(managedHooksDir)) return false;
|
||||
return /(^|[\\/\s"'])(gsd-check-update\.js|gsd-update-check\.js)(?=$|[\s"'])/.test(normalizedCommand);
|
||||
}
|
||||
|
||||
function pruneGsdManagedHooksJsonValue(value, targetDir) {
|
||||
if (Array.isArray(value)) {
|
||||
let changed = false;
|
||||
const next = [];
|
||||
for (const item of value) {
|
||||
const pruned = pruneGsdManagedHooksJsonValue(item, targetDir);
|
||||
if (pruned.changed) changed = true;
|
||||
if (!isStructurallyEmpty(pruned.value)) next.push(pruned.value);
|
||||
else changed = true;
|
||||
}
|
||||
return { value: next, changed };
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
if (isManagedCodexHookCommand(value.command, targetDir)) {
|
||||
return { value: null, changed: true };
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
const next = {};
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
const pruned = pruneGsdManagedHooksJsonValue(child, targetDir);
|
||||
if (pruned.changed) changed = true;
|
||||
if (!isStructurallyEmpty(pruned.value)) next[key] = pruned.value;
|
||||
else changed = true;
|
||||
}
|
||||
return { value: next, changed };
|
||||
}
|
||||
|
||||
return { value, changed: false };
|
||||
}
|
||||
|
||||
function isStructurallyEmpty(value) {
|
||||
if (value === null || value === undefined) return true;
|
||||
if (Array.isArray(value)) return value.length === 0;
|
||||
return typeof value === 'object' && Object.keys(value).length === 0;
|
||||
}
|
||||
|
||||
function cleanupLegacyCodexHooksJson(targetDir) {
|
||||
const hooksPath = path.join(targetDir, 'hooks.json');
|
||||
if (!fs.existsSync(hooksPath)) return { changed: false, removedFile: false };
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
|
||||
} catch {
|
||||
return { changed: false, removedFile: false, skipped: 'invalid_json' };
|
||||
}
|
||||
|
||||
const pruned = pruneGsdManagedHooksJsonValue(parsed, targetDir);
|
||||
if (!pruned.changed) return { changed: false, removedFile: false };
|
||||
|
||||
if (isStructurallyEmpty(pruned.value)) {
|
||||
fs.unlinkSync(hooksPath);
|
||||
return { changed: true, removedFile: true };
|
||||
}
|
||||
|
||||
atomicWriteFileSync(hooksPath, JSON.stringify(pruned.value, null, 2) + '\n', 'utf8');
|
||||
return { changed: true, removedFile: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a hook command path using forward slashes for cross-platform compatibility.
|
||||
* On Windows, $HOME is not expanded by cmd.exe/PowerShell, so we use the actual path.
|
||||
@@ -7620,9 +7550,6 @@ function install(isGlobal, runtime = 'claude') {
|
||||
isGlobal,
|
||||
});
|
||||
|
||||
// Run manifest-backed cleanup migrations before package materialization.
|
||||
runInstallerMigrations({ configDir: targetDir });
|
||||
|
||||
// #3245 — Codex idempotent rollback. Capture pre-install state of ALL
|
||||
// directories and files GSD will mutate so that any post-install validation
|
||||
// failure (config.toml schema check, write failure, etc.) can revert the
|
||||
@@ -7653,6 +7580,13 @@ function install(isGlobal, runtime = 'claude') {
|
||||
// Map<filename, Buffer> — content snapshot of each pre-existing gsd-* agent file.
|
||||
const codexPreInstallAgentContents = new Map();
|
||||
let codexPreInstallVersionBytes = null;
|
||||
let installerMigrationResult = null;
|
||||
const rollbackInstallerMigrations = () => {
|
||||
if (!installerMigrationResult || typeof installerMigrationResult.rollback !== 'function') return;
|
||||
const rollback = installerMigrationResult.rollback;
|
||||
installerMigrationResult = null;
|
||||
rollback();
|
||||
};
|
||||
if (isCodex && !isMinimalMode(installMode)) {
|
||||
const _preSkillsDir = path.join(targetDir, 'skills');
|
||||
if (fs.existsSync(_preSkillsDir)) {
|
||||
@@ -7708,6 +7642,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
// The full restoreCodexSnapshot() (defined inside the config block) additionally
|
||||
// handles config.toml, which is not yet touched at this point in the pipeline.
|
||||
const _codexPreConfigRollback = !isCodex || isMinimalMode(installMode) ? null : () => {
|
||||
rollbackInstallerMigrations();
|
||||
// skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install).
|
||||
const _earlySkillsDir = path.join(targetDir, 'skills');
|
||||
for (const skillName of codexPreInstallSkillNames) {
|
||||
@@ -7784,6 +7719,15 @@ function install(isGlobal, runtime = 'claude') {
|
||||
_earlyCleanTmpFiles(targetDir);
|
||||
};
|
||||
|
||||
// Run manifest-backed cleanup migrations after rollback snapshots exist and
|
||||
// before package materialization. Codex rollback paths invoke the migration
|
||||
// rollback handle if a later install step fails.
|
||||
installerMigrationResult = runInstallerMigrations({
|
||||
configDir: targetDir,
|
||||
runtime,
|
||||
scope: isGlobal ? 'global' : 'local',
|
||||
});
|
||||
|
||||
// #3245 CR finding 2 — wrap the pre-config install operations in a try/catch so
|
||||
// that ANY throw between snapshot capture and the Codex config block triggers rollback.
|
||||
// Non-Codex paths are unaffected (_codexPreConfigRollback is null for them).
|
||||
@@ -8445,6 +8389,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
// existence checks. Safe to call before any snapshots are captured (variables
|
||||
// default to empty Set / null). Does NOT touch non-gsd-* user content.
|
||||
const restoreCodexSnapshot = () => {
|
||||
rollbackInstallerMigrations();
|
||||
// 1. config.toml
|
||||
if (codexConfigPreInstallSnapshot !== null) {
|
||||
try { fs.writeFileSync(codexConfigPathPreInstall, codexConfigPreInstallSnapshot); }
|
||||
@@ -8708,10 +8653,6 @@ function install(isGlobal, runtime = 'claude') {
|
||||
throw wrapped;
|
||||
}
|
||||
console.log(` ${green}✓${reset} Configured Codex hooks (SessionStart)`);
|
||||
const legacyHooksCleanup = cleanupLegacyCodexHooksJson(targetDir);
|
||||
if (legacyHooksCleanup.changed) {
|
||||
console.log(` ${green}✓${reset} Removed legacy GSD hooks.json entries`);
|
||||
}
|
||||
} catch (e) {
|
||||
// #2760 — schema-validation and write failures must be loud and fatal
|
||||
// so the user is never left with a config Codex refuses to load (or no
|
||||
@@ -10804,7 +10745,6 @@ if (process.env.GSD_TEST_MODE) {
|
||||
rewriteLegacyManagedNodeHookCommands,
|
||||
buildCodexHookBlock,
|
||||
rewriteLegacyCodexHookBlock,
|
||||
cleanupLegacyCodexHooksJson,
|
||||
};
|
||||
} else {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user