feat(shell-projection): centralize managed hook command policy (#3450)
* feat(shell-projection): route persistent PATH hints through projection seam * refactor(shell-projection): unify managed hook command policy * fix(install): guard malformed settings hooks during uninstall * chore(changeset): add fragment for pr 3450 * fix(install): guard malformed settings hook entries
This commit is contained in:
@@ -9,11 +9,13 @@ const {
|
||||
buildWindowsShimTriple: buildWindowsShimTripleFromProjection,
|
||||
formatSdkPathDiagnostic: formatSdkPathDiagnosticFromProjection,
|
||||
isManagedHookBasename,
|
||||
isManagedHookCommand,
|
||||
projectLocalHookPrefix,
|
||||
projectLegacySettingsHookCommand,
|
||||
projectManagedHookCommand,
|
||||
projectPathActionProjection,
|
||||
projectPortableHookBaseDir,
|
||||
projectPersistentPathExportActions,
|
||||
projectShellCommandText,
|
||||
projectCodexHookTomlCommand,
|
||||
} = require('../get-shit-done/bin/lib/shell-command-projection.cjs');
|
||||
@@ -3276,14 +3278,6 @@ function stripLeakedGsdCodexSections(content) {
|
||||
* Pure function, exported for test coverage. Returns the input unchanged
|
||||
* if no GSD-managed hook section is present.
|
||||
*/
|
||||
// Legacy hook command basenames to detect during strip. Template-literal
|
||||
// form so install-hooks-copy.test.cjs's quoted-literal guard continues to
|
||||
// catch accidental regressions where someone *registers* the inverted
|
||||
// `gsd-update-check.js` filename in a Codex hook command.
|
||||
const STALE_HOOK_BASENAMES = new Set([
|
||||
`gsd-update-check.js`,
|
||||
`gsd-check-update.js`,
|
||||
]);
|
||||
function stripStaleGsdHookBlocks(configContent) {
|
||||
const sections = getTomlTableSections(configContent);
|
||||
const lineRecords = getTomlLineRecords(configContent);
|
||||
@@ -3319,10 +3313,10 @@ function stripStaleGsdHookBlocks(configContent) {
|
||||
continue;
|
||||
}
|
||||
if (typeof parsed.value !== 'string') continue;
|
||||
// Match the basename — Codex configs reference these files by absolute
|
||||
// path under the user's `.codex/hooks/` directory.
|
||||
const basename = parsed.value.split(/[\\/]/).pop() || '';
|
||||
if (STALE_HOOK_BASENAMES.has(basename)) {
|
||||
if (isManagedHookCommand(parsed.value, {
|
||||
surface: 'codex-toml',
|
||||
includeLegacyAliases: true,
|
||||
})) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -6718,22 +6712,19 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
|
||||
// Remove GSD hooks from settings — per-hook granularity to preserve
|
||||
// user hooks that share an entry with a GSD hook (#1755 followup)
|
||||
const isGsdHookCommand = (cmd) =>
|
||||
cmd && (cmd.includes('gsd-check-update') || cmd.includes('gsd-statusline') ||
|
||||
cmd.includes('gsd-session-state') || cmd.includes('gsd-context-monitor') ||
|
||||
cmd.includes('gsd-phase-boundary') || cmd.includes('gsd-prompt-guard') ||
|
||||
cmd.includes('gsd-read-guard') || cmd.includes('gsd-read-injection-scanner') ||
|
||||
cmd.includes('gsd-update-banner') ||
|
||||
cmd.includes('gsd-validate-commit') || cmd.includes('gsd-workflow-guard'));
|
||||
|
||||
for (const eventName of ['SessionStart', 'PostToolUse', 'AfterTool', 'PreToolUse', 'BeforeTool']) {
|
||||
if (settings.hooks && settings.hooks[eventName]) {
|
||||
const before = JSON.stringify(settings.hooks[eventName]);
|
||||
settings.hooks[eventName] = settings.hooks[eventName]
|
||||
.map(entry => {
|
||||
if (!entry.hooks || !Array.isArray(entry.hooks)) return entry;
|
||||
if (!entry || typeof entry !== 'object' || !Array.isArray(entry.hooks)) return entry;
|
||||
// Filter out individual GSD hooks, keep user hooks
|
||||
entry.hooks = entry.hooks.filter(h => !isGsdHookCommand(h.command));
|
||||
entry.hooks = entry.hooks.filter((h) => {
|
||||
if (!h || typeof h.command !== 'string') return true;
|
||||
return !isManagedHookCommand(h.command, {
|
||||
surface: 'settings-json',
|
||||
});
|
||||
});
|
||||
return entry.hooks.length > 0 ? entry : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
@@ -9757,7 +9748,8 @@ function homePathCoveredByRc(globalBin, homeDir, rcFileNames) {
|
||||
* - a diagnostic "already covered via rc file" note, if an rc file has
|
||||
* `export PATH="$HOME/…/bin:$PATH"` (or equivalent) and the user just
|
||||
* needs to reopen their shell
|
||||
* - the absolute `echo 'export PATH="…:$PATH"' >> ~/.zshrc` suggestion,
|
||||
* - projected shell actions that append `export PATH="…:$PATH"` to
|
||||
* `~/.zshrc` / `~/.bashrc` when neither PATH nor rc files cover globalBin
|
||||
* if neither PATH nor any rc file covers globalBin
|
||||
*
|
||||
* Exported for tests; the installer calls this from finishInstall.
|
||||
@@ -9786,14 +9778,13 @@ function maybeSuggestPathExport(globalBin, homeDir) {
|
||||
console.log('');
|
||||
console.log(` ${yellow}⚠${reset} ${bold}${globalBin}${reset} is not on your PATH.`);
|
||||
console.log(` Add it with one of:`);
|
||||
const projected = projectPathActionProjection({
|
||||
mode: 'persist',
|
||||
const projected = projectPersistentPathExportActions({
|
||||
targetDir: globalBin,
|
||||
platform: process.platform,
|
||||
});
|
||||
for (const action of projected.shellActions) {
|
||||
const label = action.label ? `${action.label}: ` : '';
|
||||
console.log(` ${cyan}${label}${action.command}${reset}`);
|
||||
const labelPrefix = action.label ? `${action.label}: ` : '';
|
||||
console.log(` ${cyan}${labelPrefix}${action.command}${reset}`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user