fix: stage legacy team hook deletion

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
neallee 2026-07-10 21:25:58 +08:00
parent 5f1e896f90
commit 7233eb0d60
3 changed files with 36 additions and 9 deletions

View File

@ -220,6 +220,9 @@ HOOK_EOF
# registered successfully. Other project hooks are left untouched.
if [ -e "$HOOKS_DIR/check-gstack.sh" ] || [ -L "$HOOKS_DIR/check-gstack.sh" ]; then
rm "$HOOKS_DIR/check-gstack.sh"
if (cd "$REPO_ROOT" && git ls-files --error-unmatch -- .claude/hooks/check-gstack.sh >/dev/null 2>&1); then
GENERATED+=(".claude/hooks/check-gstack.sh")
fi
echo " - .claude/hooks/check-gstack.sh — removed legacy enforcement hook"
fi
else

View File

@ -18,7 +18,7 @@
<li>The generated hook resolves the user home in <code>HOME</code>, <code>USERPROFILE</code>, then <code>os.homedir()</code> order so POSIX, Git Bash, PowerShell, and cmd runners use the expected global install root.</li>
<li>The hook contract follows <code>docs/spikes/claude-code-hook-mutation.md</code>: pass-through writes <code>{}</code>; denial writes a <code>hookSpecificOutput</code> object for <code>PreToolUse</code> with <code>permissionDecision: "deny"</code>.</li>
<li>Missing or unreadable gstack is an intentional denial with exit code zero. Install instructions are present in stderr and <code>permissionDecisionReason</code>, so the runner does not report a hook execution error.</li>
<li>Rerunning required mode replaces a matching legacy <code>check-gstack.sh</code> registration, removes duplicate matching registrations, and deletes the obsolete Bash hook only after the CommonJS hook is generated and registered successfully. Unrelated project hooks remain unchanged.</li>
<li>Rerunning required mode replaces a matching legacy <code>check-gstack.sh</code> registration, removes duplicate matching registrations, and deletes the obsolete Bash hook only after the CommonJS hook is generated and registered successfully. If Git tracks the deleted hook, the printed <code>git add</code> command includes its path so the suggested migration commit stages the deletion; untracked removed hooks are omitted because they have no index change. Unrelated project hooks remain unchanged.</li>
</ul>
<h2>Deviations</h2>

View File

@ -302,7 +302,7 @@ describe('gstack-team-init', () => {
});
test('required: creates enforcement hook', () => {
run(`${TEAM_INIT} required`, { cwd: tmpDir });
const result = run(`${TEAM_INIT} required`, { cwd: tmpDir });
const hookPath = path.join(tmpDir, '.claude', 'hooks', 'check-gstack.cjs');
expect(fs.existsSync(hookPath)).toBe(true);
expect(
@ -319,6 +319,7 @@ describe('gstack-team-init', () => {
expect(hook).toContain("permissionDecision: 'deny'");
expect(hook).toContain('BLOCKED: gstack is not installed');
expect(hook).not.toContain('#!/bin/bash');
expect(result.stdout).not.toMatch(/git add .*check-gstack\.sh/);
});
test('required: registers one shell-neutral project hook', () => {
@ -534,22 +535,45 @@ describe('gstack-team-init', () => {
execSync('git add .claude', { cwd: tmpDir });
execSync('git commit -m "add legacy hook"', { cwd: tmpDir });
run(`${TEAM_INIT} required`, { cwd: tmpDir });
const migration = run(`${TEAM_INIT} required`, { cwd: tmpDir });
expect(fs.existsSync(legacyHook)).toBe(false);
expect(fs.readFileSync(unrelatedHook, 'utf-8')).toBe(unrelatedHookContents);
const suggestedGitAdd = migration.stdout
.split('\n')
.find(line => line.startsWith(' git add '))
?.trim();
if (!suggestedGitAdd) {
throw new Error('gstack-team-init did not print a git add command');
}
expect(suggestedGitAdd.split(/\s+/)).toContain(
'.claude/hooks/check-gstack.sh',
);
execSync(suggestedGitAdd, { cwd: tmpDir });
execSync('git commit -m "migrate required hook"', { cwd: tmpDir });
expect(
execSync('git status --short', { cwd: tmpDir, encoding: 'utf-8' }),
).toBe('');
run(`${TEAM_INIT} required`, { cwd: tmpDir });
const rerun = run(`${TEAM_INIT} required`, { cwd: tmpDir });
const settings = JSON.parse(
fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf-8'),
);
expect(fs.existsSync(legacyHook)).toBe(false);
expect(fs.readFileSync(unrelatedHook, 'utf-8')).toBe(unrelatedHookContents);
const rerunGitAdd = rerun.stdout
.split('\n')
.find(line => line.startsWith(' git add '))
?.trim();
if (!rerunGitAdd) {
throw new Error('gstack-team-init rerun did not print a git add command');
}
expect(rerunGitAdd.split(/\s+/)).not.toContain(
'.claude/hooks/check-gstack.sh',
);
execSync(rerunGitAdd, { cwd: tmpDir });
expect(
execSync('git status --short -- .claude/hooks/check-gstack.sh', {
cwd: tmpDir,
encoding: 'utf-8',
}),
).toBe(' D .claude/hooks/check-gstack.sh\n');
execSync('git status --short', { cwd: tmpDir, encoding: 'utf-8' }),
).toBe('');
expect(settings.hooks.PreToolUse).toHaveLength(1);
expect(settings.hooks.PreToolUse[0].hooks).toHaveLength(1);
expect(settings.hooks.PreToolUse[0].hooks[0].command).toContain(