feat: deepen shell command projection seam (#3445)

* feat: deepen shell command projection seam

* chore: add changeset for shell projection seam

* feat: centralize local and portable hook path projection

* docs: avoid prompt-scan false positive in installer migrations

* docs(adr): fix path-style token punctuation
This commit is contained in:
Tom Boucher
2026-05-12 19:02:18 -04:00
committed by GitHub
parent f8b917d52a
commit 7e91a861ee
12 changed files with 350 additions and 246 deletions

View File

@@ -9,7 +9,14 @@ const path = require('node:path');
const projection = require(path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'shell-command-projection.cjs'));
const install = require(path.join(__dirname, '..', 'bin', 'install.js'));
const { hookCommandNeedsPowerShellCallOperator, formatHookCommandForRuntime } = projection;
const {
hookCommandNeedsPowerShellCallOperator,
formatHookCommandForRuntime,
isManagedHookBasename,
projectLocalHookPrefix,
projectLegacySettingsHookCommand,
projectPortableHookBaseDir,
} = projection;
const { buildHookCommand, rewriteLegacyManagedNodeHookCommands } = install;
describe('bug #3413: Shell Command Projection Module uses runtime-aware hook policy', () => {
@@ -71,3 +78,67 @@ describe('bug #3413: installer hook surfaces consume runtime-aware projection',
);
});
});
describe('bug #3439: shell projection module owns managed-hook policy and legacy rewrite projection', () => {
test('isManagedHookBasename is surface-aware', () => {
assert.equal(isManagedHookBasename('/x/hooks/gsd-check-update.js', { surface: 'settings-json' }), true);
assert.equal(isManagedHookBasename('/x/hooks/gsd-statusline.js', { surface: 'settings-json' }), true);
assert.equal(isManagedHookBasename('/x/hooks/gsd-statusline.js', { surface: 'codex-toml' }), false);
assert.equal(isManagedHookBasename('/x/hooks/custom-hook.js', { surface: 'settings-json' }), false);
});
test('projectLegacySettingsHookCommand preserves non-Windows script token shape', () => {
const command = projectLegacySettingsHookCommand({
absoluteRunner: '"/usr/local/bin/node"',
scriptPath: '/x/hooks/gsd-statusline.js',
scriptToken: "'/x/hooks/gsd-statusline.js'",
platform: 'linux',
runtime: 'claude',
});
assert.equal(command, `"/usr/local/bin/node" '/x/hooks/gsd-statusline.js'`);
});
test('projectLegacySettingsHookCommand normalizes Windows managed paths and runtime wrapper policy', () => {
const command = projectLegacySettingsHookCommand({
absoluteRunner: '"C:/nvm4w/nodejs/node.exe"',
scriptPath: 'C:\\Users\\me\\.gemini\\hooks\\gsd-prompt-guard.js',
scriptToken: "'C:\\Users\\me\\.gemini\\hooks\\gsd-prompt-guard.js'",
platform: 'win32',
runtime: 'gemini',
});
assert.equal(command, '& "C:/nvm4w/nodejs/node.exe" "C:/Users/me/.gemini/hooks/gsd-prompt-guard.js"');
});
test('projectLocalHookPrefix centralizes runtime-specific project-dir interpolation policy', () => {
assert.equal(projectLocalHookPrefix({ runtime: 'gemini', dirName: '.gemini' }), '.gemini');
assert.equal(projectLocalHookPrefix({ runtime: 'antigravity', dirName: '.agent' }), '.agent');
assert.equal(
projectLocalHookPrefix({ runtime: 'claude', dirName: '.claude' }),
'"$CLAUDE_PROJECT_DIR"/.claude',
);
});
test('projectPortableHookBaseDir centralizes $HOME interpolation policy', () => {
assert.equal(
projectPortableHookBaseDir({
configDir: '/Users/me/.claude',
homeDir: '/Users/me',
}),
'$HOME/.claude',
);
assert.equal(
projectPortableHookBaseDir({
configDir: 'C:\\Users\\me\\.claude',
homeDir: 'C:\\Users\\me',
}),
'$HOME/.claude',
);
assert.equal(
projectPortableHookBaseDir({
configDir: '/opt/custom/.claude',
homeDir: '/Users/me',
}),
'/opt/custom/.claude',
);
});
});