|
9 | 9 | */ |
10 | 10 |
|
11 | 11 | import * as p from "@clack/prompts"; |
12 | | -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; |
| 12 | +import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs"; |
13 | 13 | import { join } from "node:path"; |
14 | 14 | import { execSync } from "node:child_process"; |
15 | 15 | import { createApiClient, type WorkspaceInfo } from "../api-client.js"; |
16 | 16 |
|
| 17 | +// --------------------------------------------------------------------------- |
| 18 | +// Git hook installer |
| 19 | +// --------------------------------------------------------------------------- |
| 20 | + |
| 21 | +function findGitDir(cwd: string): string | null { |
| 22 | + try { |
| 23 | + return execSync("git rev-parse --git-dir", { cwd, stdio: "pipe" }).toString().trim(); |
| 24 | + } catch { |
| 25 | + return null; |
| 26 | + } |
| 27 | +} |
| 28 | + |
| 29 | +function installPostPushHook(cwd: string, apiUrl: string, apiKey: string): boolean { |
| 30 | + const gitDir = findGitDir(cwd); |
| 31 | + if (!gitDir) return false; |
| 32 | + |
| 33 | + const hooksDir = join(gitDir, "hooks"); |
| 34 | + mkdirSync(hooksDir, { recursive: true }); |
| 35 | + |
| 36 | + // post-push is not a native git hook, so we use post-commit + push detection |
| 37 | + // Instead, use pre-push which fires before push completes |
| 38 | + const hookPath = join(hooksDir, "pre-push"); |
| 39 | + |
| 40 | + // Don't overwrite existing hooks |
| 41 | + if (existsSync(hookPath)) { |
| 42 | + const existing = readFileSync(hookPath, "utf-8"); |
| 43 | + if (existing.includes("toban review")) return true; // already installed |
| 44 | + return false; // user has a custom hook |
| 45 | + } |
| 46 | + |
| 47 | + const hookScript = `#!/bin/sh |
| 48 | +# Auto-review on push — installed by toban init |
| 49 | +# Runs toban review asynchronously after push completes |
| 50 | +# Only reviews if the push succeeds (pre-push runs before push, so we background it) |
| 51 | +
|
| 52 | +# Get the commit range being pushed |
| 53 | +while read local_ref local_sha remote_ref remote_sha; do |
| 54 | + if [ "$local_sha" != "0000000000000000000000000000000000000000" ]; then |
| 55 | + # Background: wait for push to finish, then review |
| 56 | + ( |
| 57 | + sleep 2 |
| 58 | + npx toban review --api-url "${apiUrl}" --api-key "${apiKey}" --diff "$remote_sha..$local_sha" 2>/dev/null & |
| 59 | + ) & |
| 60 | + fi |
| 61 | +done |
| 62 | +
|
| 63 | +exit 0 |
| 64 | +`; |
| 65 | + |
| 66 | + writeFileSync(hookPath, hookScript); |
| 67 | + chmodSync(hookPath, 0o755); |
| 68 | + return true; |
| 69 | +} |
| 70 | + |
| 71 | + |
17 | 72 | // --------------------------------------------------------------------------- |
18 | 73 | // Config types |
19 | 74 | // --------------------------------------------------------------------------- |
@@ -270,7 +325,20 @@ export async function handleInit(): Promise<void> { |
270 | 325 |
|
271 | 326 | p.log.success(`Config saved to ${CONFIG_DIR}/${CONFIG_FILE}`); |
272 | 327 |
|
273 | | - // 7. Optionally create first sprint |
| 328 | + // 7. Install git pre-push hook for auto-review |
| 329 | + const hookInstalled = installPostPushHook(cwd, apiUrl, apiKey); |
| 330 | + if (hookInstalled) { |
| 331 | + p.log.success("Git pre-push hook installed (auto-review on push)"); |
| 332 | + } else { |
| 333 | + const gitDir = findGitDir(cwd); |
| 334 | + if (!gitDir) { |
| 335 | + p.log.warning("Not a git repository — skipping auto-review hook"); |
| 336 | + } else { |
| 337 | + p.log.warning("Existing pre-push hook found — auto-review not installed"); |
| 338 | + } |
| 339 | + } |
| 340 | + |
| 341 | + // 8. Optionally create first sprint |
274 | 342 | const createSprint = await p.confirm({ |
275 | 343 | message: "Create and start the first sprint now?", |
276 | 344 | initialValue: false, |
|
0 commit comments