Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/commands/__tests__/hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ afterEach(async () => {
});

function hookPath(): string {
return path.join(tmpDir, '.git', 'hooks', 'pre-commit');
return path.join(tmpDir, '.git', 'hooks', 'pre-push');
}

describe('hook install', () => {
Expand Down
10 changes: 5 additions & 5 deletions src/commands/hook/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ import { uninstallAction } from './uninstall.js';
import { statusAction } from './status.js';

export const hookCommand = new Command('hook')
.description('Manage pre-commit hook for automatic code review');
.description('Manage pre-push hook for automatic code review');

hookCommand
.command('install')
.description('Install pre-commit hook for automatic code review')
.option('--fail-on <severity>', 'Minimum severity to block commit (info, warning, error, critical)', 'critical')
.description('Install pre-push hook for automatic code review')
.option('--fail-on <severity>', 'Minimum severity to block push (info, warning, error, critical)', 'critical')
.option('--fast', 'Use fast mode for review (default: true)', true)
.option('--no-fast', 'Disable fast mode for review')
.option('--force', 'Overwrite existing hook without prompting')
.action(installAction);

hookCommand
.command('uninstall')
.description('Remove pre-commit hook installed by kodus')
.description('Remove pre-push hook installed by kodus')
.action(uninstallAction);

hookCommand
.command('status')
.description('Show pre-commit hook status')
.description('Show pre-push hook status')
.action(statusAction);
44 changes: 36 additions & 8 deletions src/commands/hook/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import { gitService } from '../../services/git.service.js';
const KODUS_MARKER = '# kodus-hook';

function generateHookScript(failOn: string, fast: boolean): string {
const flags = ['--staged'];
const flags: string[] = [];
if (fast) flags.push('--fast');
flags.push('--fail-on', failOn);
flags.push('--format', 'terminal');
flags.push('--quiet');

const reviewFlags = flags.join(' ');

return `#!/bin/sh
${KODUS_MARKER} — installed by kodus CLI
# To uninstall: kodus hook uninstall
Expand All @@ -24,13 +26,39 @@ fi

# Check if kodus is available
if ! command -v kodus >/dev/null 2>&1; then
echo "Warning: kodus CLI not found. Skipping pre-commit review."
echo "Warning: kodus CLI not found. Skipping pre-push review."
echo "Install: npm install -g @kodus/cli"
exit 0
fi

# Run review on staged files
kodus review ${flags.join(' ')}
remote="$1"
current_branch="$(git symbolic-ref --short HEAD 2>/dev/null)"

while read local_ref local_sha remote_ref remote_sha; do
# Skip branch deletions
if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
continue
fi

# New branch — no remote state to compare, skip review
if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
continue
fi

# Extract branch name from ref (refs/heads/my-branch → my-branch)
branch_name="\${local_ref#refs/heads/}"

# Only review if pushing the currently checked-out branch
# (--branch compares against HEAD, so reviewing other refs would produce wrong diffs)
if [ "\$branch_name" != "\$current_branch" ]; then
continue
fi

# Review changes not yet on the remote
if ! kodus review --branch "\${remote}/\${branch_name}" ${reviewFlags}; then
exit 1
fi
done
`;
}

Expand All @@ -50,7 +78,7 @@ export async function installAction(options: {

const gitRoot = await gitService.getGitRoot();
const hooksDir = path.join(gitRoot.trim(), '.git', 'hooks');
const hookPath = path.join(hooksDir, 'pre-commit');
const hookPath = path.join(hooksDir, 'pre-push');

// Check if hook already exists
let existingContent: string | null = null;
Expand All @@ -68,7 +96,7 @@ export async function installAction(options: {
{
type: 'confirm',
name: 'overwrite',
message: 'A pre-commit hook already exists. Overwrite it?',
message: 'A pre-push hook already exists. Overwrite it?',
default: false,
},
]);
Expand All @@ -87,12 +115,12 @@ export async function installAction(options: {
const script = generateHookScript(failOn, fast);
await fs.writeFile(hookPath, script, { mode: 0o755 });

console.log(chalk.green('✓ Pre-commit hook installed successfully!'));
console.log(chalk.green('✓ Pre-push hook installed successfully!'));
console.log(chalk.dim(` Path: ${hookPath}`));
console.log(chalk.dim(` Fail on: ${failOn}`));
console.log(chalk.dim(` Fast mode: ${fast ? 'yes' : 'no'}`));
console.log('');
console.log(chalk.dim('Skip with: KODUS_SKIP_HOOK=1 git commit'));
console.log(chalk.dim('Skip with: KODUS_SKIP_HOOK=1 git push'));
console.log(chalk.dim('Remove with: kodus hook uninstall'));
}

Expand Down
8 changes: 4 additions & 4 deletions src/commands/hook/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ export async function statusAction(): Promise<void> {
}

const gitRoot = await gitService.getGitRoot();
const hookPath = path.join(gitRoot.trim(), '.git', 'hooks', 'pre-commit');
const hookPath = path.join(gitRoot.trim(), '.git', 'hooks', 'pre-push');

let content: string;
try {
content = await fs.readFile(hookPath, 'utf-8');
} catch {
console.log(chalk.yellow('Pre-commit hook: not installed'));
console.log(chalk.yellow('Pre-push hook: not installed'));
return;
}

if (!content.includes(KODUS_MARKER)) {
console.log(chalk.yellow('Pre-commit hook: installed (not by kodus)'));
console.log(chalk.yellow('Pre-push hook: installed (not by kodus)'));
return;
}

console.log(chalk.green('Pre-commit hook: installed'));
console.log(chalk.green('Pre-push hook: installed'));

// Parse config from hook script
const failOnMatch = content.match(/--fail-on\s+(\S+)/);
Expand Down
8 changes: 4 additions & 4 deletions src/commands/hook/uninstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,21 @@ export async function uninstallAction(): Promise<void> {
}

const gitRoot = await gitService.getGitRoot();
const hookPath = path.join(gitRoot.trim(), '.git', 'hooks', 'pre-commit');
const hookPath = path.join(gitRoot.trim(), '.git', 'hooks', 'pre-push');

let content: string;
try {
content = await fs.readFile(hookPath, 'utf-8');
} catch {
console.log(chalk.yellow('No pre-commit hook found.'));
console.log(chalk.yellow('No pre-push hook found.'));
return;
}

if (!content.includes(KODUS_MARKER)) {
console.log(chalk.yellow('The pre-commit hook was not installed by kodus. Skipping.'));
console.log(chalk.yellow('The pre-push hook was not installed by kodus. Skipping.'));
return;
}

await fs.unlink(hookPath);
console.log(chalk.green('✓ Pre-commit hook removed successfully.'));
console.log(chalk.green('✓ Pre-push hook removed successfully.'));
}
6 changes: 3 additions & 3 deletions tests/integration/cli.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,13 @@ describe('auth status integration', () => {
// Hook commands — install, status, uninstall
// ---------------------------------------------------------------------------
describe('hook integration', () => {
it('kodus hook install creates pre-commit hook', async () => {
it('kodus hook install creates pre-push hook', async () => {
const { stdout, stderr, exitCode } = await runCli(['hook', 'install', '--force']);
expect(exitCode).toBe(0);
const output = stdout + stderr;
expect(output).toContain('installed');

const hookPath = path.join(gitRepoDir, '.git', 'hooks', 'pre-commit');
const hookPath = path.join(gitRepoDir, '.git', 'hooks', 'pre-push');
const content = await fs.readFile(hookPath, 'utf-8');
expect(content).toContain('# kodus-hook');
expect(content).toContain('--fail-on critical');
Expand All @@ -263,7 +263,7 @@ describe('hook integration', () => {
const output = stdout + stderr;
expect(output).toContain('removed');

const hookPath = path.join(gitRepoDir, '.git', 'hooks', 'pre-commit');
const hookPath = path.join(gitRepoDir, '.git', 'hooks', 'pre-push');
await expect(fs.access(hookPath)).rejects.toThrow();
});
});
Expand Down