ghagga 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +41 -8
  2. package/dist/commands/hooks/index.d.ts +9 -0
  3. package/dist/commands/hooks/index.d.ts.map +1 -0
  4. package/dist/commands/hooks/index.js +16 -0
  5. package/dist/commands/hooks/index.js.map +1 -0
  6. package/dist/commands/hooks/install.d.ts +13 -0
  7. package/dist/commands/hooks/install.d.ts.map +1 -0
  8. package/dist/commands/hooks/install.js +64 -0
  9. package/dist/commands/hooks/install.js.map +1 -0
  10. package/dist/commands/hooks/install.test.d.ts +11 -0
  11. package/dist/commands/hooks/install.test.d.ts.map +1 -0
  12. package/dist/commands/hooks/install.test.js +157 -0
  13. package/dist/commands/hooks/install.test.js.map +1 -0
  14. package/dist/commands/hooks/status.d.ts +12 -0
  15. package/dist/commands/hooks/status.d.ts.map +1 -0
  16. package/dist/commands/hooks/status.js +38 -0
  17. package/dist/commands/hooks/status.js.map +1 -0
  18. package/dist/commands/hooks/status.test.d.ts +11 -0
  19. package/dist/commands/hooks/status.test.d.ts.map +1 -0
  20. package/dist/commands/hooks/status.test.js +123 -0
  21. package/dist/commands/hooks/status.test.js.map +1 -0
  22. package/dist/commands/hooks/uninstall.d.ts +12 -0
  23. package/dist/commands/hooks/uninstall.d.ts.map +1 -0
  24. package/dist/commands/hooks/uninstall.js +44 -0
  25. package/dist/commands/hooks/uninstall.js.map +1 -0
  26. package/dist/commands/hooks/uninstall.test.d.ts +10 -0
  27. package/dist/commands/hooks/uninstall.test.d.ts.map +1 -0
  28. package/dist/commands/hooks/uninstall.test.js +120 -0
  29. package/dist/commands/hooks/uninstall.test.js.map +1 -0
  30. package/dist/commands/review-commit-msg.d.ts +28 -0
  31. package/dist/commands/review-commit-msg.d.ts.map +1 -0
  32. package/dist/commands/review-commit-msg.js +126 -0
  33. package/dist/commands/review-commit-msg.js.map +1 -0
  34. package/dist/commands/review-commit-msg.test.d.ts +11 -0
  35. package/dist/commands/review-commit-msg.test.d.ts.map +1 -0
  36. package/dist/commands/review-commit-msg.test.js +126 -0
  37. package/dist/commands/review-commit-msg.test.js.map +1 -0
  38. package/dist/commands/review.d.ts +6 -0
  39. package/dist/commands/review.d.ts.map +1 -1
  40. package/dist/commands/review.js +105 -10
  41. package/dist/commands/review.js.map +1 -1
  42. package/dist/index.d.ts +5 -4
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +21 -7
  45. package/dist/index.js.map +1 -1
  46. package/dist/lib/git-hooks.d.ts +19 -0
  47. package/dist/lib/git-hooks.d.ts.map +1 -0
  48. package/dist/lib/git-hooks.js +129 -0
  49. package/dist/lib/git-hooks.js.map +1 -0
  50. package/dist/lib/git-hooks.test.d.ts +11 -0
  51. package/dist/lib/git-hooks.test.d.ts.map +1 -0
  52. package/dist/lib/git-hooks.test.js +178 -0
  53. package/dist/lib/git-hooks.test.js.map +1 -0
  54. package/dist/lib/git.d.ts +10 -0
  55. package/dist/lib/git.d.ts.map +1 -1
  56. package/dist/lib/git.js +32 -0
  57. package/dist/lib/git.js.map +1 -1
  58. package/dist/lib/hook-templates.d.ts +12 -0
  59. package/dist/lib/hook-templates.d.ts.map +1 -0
  60. package/dist/lib/hook-templates.js +52 -0
  61. package/dist/lib/hook-templates.js.map +1 -0
  62. package/dist/lib/hook-templates.test.d.ts +11 -0
  63. package/dist/lib/hook-templates.test.d.ts.map +1 -0
  64. package/dist/lib/hook-templates.test.js +76 -0
  65. package/dist/lib/hook-templates.test.js.map +1 -0
  66. package/dist/lib/hooks-types.d.ts +30 -0
  67. package/dist/lib/hooks-types.d.ts.map +1 -0
  68. package/dist/lib/hooks-types.js +9 -0
  69. package/dist/lib/hooks-types.js.map +1 -0
  70. package/package.json +2 -2
package/README.md CHANGED
@@ -52,7 +52,15 @@ npx ghagga status # Show auth & config
52
52
  npx ghagga logout # Clear credentials
53
53
  ```
54
54
 
55
- ### 4. Manage review memory
55
+ ### 4. Install git hooks (optional)
56
+
57
+ ```bash
58
+ npx ghagga hooks install # Auto-review on every commit
59
+ npx ghagga hooks status # Check hook status
60
+ npx ghagga hooks uninstall # Remove hooks
61
+ ```
62
+
63
+ ### 5. Manage review memory
56
64
 
57
65
  ```bash
58
66
  npx ghagga memory list # List stored observations
@@ -98,9 +106,29 @@ Options:
98
106
  --no-trivy Disable Trivy vulnerability scanning
99
107
  --no-cpd Disable CPD duplicate detection
100
108
  --no-memory Disable review memory (skip search and persist)
109
+ --memory-backend <type> Memory backend: sqlite (default) or engram
110
+ --staged Review only staged files (for pre-commit hook)
111
+ --quick Static analysis only, skip AI review (~5-10s)
112
+ --commit-msg <file> Validate commit message from file
113
+ --exit-on-issues Exit with code 1 if critical/high issues found
101
114
  -c, --config <path> Path to .ghagga.json config file
102
115
  ```
103
116
 
117
+ ## Git Hooks
118
+
119
+ Install git hooks for automatic code review on every commit:
120
+
121
+ ```bash
122
+ ghagga hooks install # Install pre-commit + commit-msg hooks
123
+ ghagga hooks install --force # Overwrite existing hooks (backs up originals)
124
+ ghagga hooks install --pre-commit # Only pre-commit hook
125
+ ghagga hooks install --commit-msg # Only commit-msg hook
126
+ ghagga hooks uninstall # Remove GHAGGA-managed hooks
127
+ ghagga hooks status # Show hook status
128
+ ```
129
+
130
+ Hooks auto-detect `ghagga` in PATH and fail gracefully if not found. Installed hooks use `--plain --exit-on-issues` automatically.
131
+
104
132
  ## Memory Subcommands
105
133
 
106
134
  ```bash
@@ -160,10 +188,13 @@ ghagga review --provider ollama
160
188
  ## Environment Variables
161
189
 
162
190
  ```bash
163
- GHAGGA_API_KEY=<key> # API key for the LLM provider
164
- GHAGGA_PROVIDER=<provider> # LLM provider override
165
- GHAGGA_MODEL=<model> # Model identifier override
166
- GITHUB_TOKEN=<token> # GitHub token (fallback for github provider)
191
+ GHAGGA_API_KEY=<key> # API key for the LLM provider
192
+ GHAGGA_PROVIDER=<provider> # LLM provider override
193
+ GHAGGA_MODEL=<model> # Model identifier override
194
+ GHAGGA_MEMORY_BACKEND=<type> # Memory backend: sqlite (default) or engram
195
+ GHAGGA_ENGRAM_HOST=<url> # Engram server URL (default: http://localhost:7437)
196
+ GHAGGA_ENGRAM_TIMEOUT=<seconds> # Engram connection timeout (default: 5)
197
+ GITHUB_TOKEN=<token> # GitHub token (fallback for github provider)
167
198
  ```
168
199
 
169
200
  ## Config File
@@ -183,14 +214,16 @@ Create a `.ghagga.json` in your project root:
183
214
 
184
215
  ## How It Works
185
216
 
186
- 1. Gets your `git diff` (staged or uncommitted changes)
217
+ 1. Gets your `git diff` (staged or uncommitted changes; `--staged` uses `git diff --cached`)
187
218
  2. Parses the diff and detects tech stacks
188
219
  3. Runs static analysis (Semgrep, Trivy, CPD) if available
189
- 4. Searches local memory for relevant past observations (SQLite + FTS5 at `~/.config/ghagga/memory.db`)
190
- 5. Sends the diff + static findings + memory context to the AI review agent
220
+ 4. Searches memory for relevant past observations (SQLite + FTS5 at `~/.config/ghagga/memory.db`, or Engram if `--memory-backend engram`)
221
+ 5. Sends the diff + static findings + memory context to the AI review agent (skipped with `--quick`)
191
222
  6. Returns findings with severity, file, line, and suggestions
192
223
  7. Persists new observations (decisions, patterns, bug fixes) to local memory for future reviews
193
224
 
225
+ With `ghagga hooks install`, steps 1-7 run automatically on every commit via pre-commit and commit-msg hooks.
226
+
194
227
  ## Requirements
195
228
 
196
229
  - Node.js >= 20
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Hooks command group barrel.
3
+ *
4
+ * Registers the `ghagga hooks` subcommands (install, uninstall, status)
5
+ * and exports the parent Command for registration in the CLI entry point.
6
+ */
7
+ import { Command } from 'commander';
8
+ export declare const hooksCommand: Command;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,YAAY,SACmC,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Hooks command group barrel.
3
+ *
4
+ * Registers the `ghagga hooks` subcommands (install, uninstall, status)
5
+ * and exports the parent Command for registration in the CLI entry point.
6
+ */
7
+ import { Command } from 'commander';
8
+ import { registerInstallCommand } from './install.js';
9
+ import { registerUninstallCommand } from './uninstall.js';
10
+ import { registerStatusCommand } from './status.js';
11
+ export const hooksCommand = new Command('hooks')
12
+ .description('Manage git hooks for automated code review');
13
+ registerInstallCommand(hooksCommand);
14
+ registerUninstallCommand(hooksCommand);
15
+ registerStatusCommand(hooksCommand);
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/commands/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC;KAC7C,WAAW,CAAC,4CAA4C,CAAC,CAAC;AAE7D,sBAAsB,CAAC,YAAY,CAAC,CAAC;AACrC,wBAAwB,CAAC,YAAY,CAAC,CAAC;AACvC,qBAAqB,CAAC,YAAY,CAAC,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `ghagga hooks install` subcommand.
3
+ *
4
+ * Installs GHAGGA-managed pre-commit and/or commit-msg hooks into the
5
+ * current git repository. Handles backup of existing non-GHAGGA hooks,
6
+ * idempotent reinstall of GHAGGA hooks, and the --force flag for
7
+ * overwriting external hooks.
8
+ *
9
+ * @see Phase 3, Task 3.2
10
+ */
11
+ import { Command } from 'commander';
12
+ export declare function registerInstallCommand(parent: Command): void;
13
+ //# sourceMappingURL=install.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../../src/commands/hooks/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAwD5D"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `ghagga hooks install` subcommand.
3
+ *
4
+ * Installs GHAGGA-managed pre-commit and/or commit-msg hooks into the
5
+ * current git repository. Handles backup of existing non-GHAGGA hooks,
6
+ * idempotent reinstall of GHAGGA hooks, and the --force flag for
7
+ * overwriting external hooks.
8
+ *
9
+ * @see Phase 3, Task 3.2
10
+ */
11
+ import { isGitRepo, getHooksDir, installHook } from '../../lib/git-hooks.js';
12
+ import { generatePreCommitHook, generateCommitMsgHook } from '../../lib/hook-templates.js';
13
+ import * as tui from '../../ui/tui.js';
14
+ export function registerInstallCommand(parent) {
15
+ parent
16
+ .command('install')
17
+ .description('Install git hooks for automated code review')
18
+ .option('--force', 'Overwrite existing non-GHAGGA hooks (with backup)')
19
+ .option('--pre-commit', 'Install only the pre-commit hook')
20
+ .option('--commit-msg', 'Install only the commit-msg hook')
21
+ .action((opts) => {
22
+ if (!isGitRepo()) {
23
+ tui.log.error('Not a git repository. Run this command from inside a git repo.');
24
+ process.exit(1);
25
+ }
26
+ const hooksDir = getHooksDir();
27
+ const force = opts.force ?? false;
28
+ // Determine which hooks to install.
29
+ // If neither flag is set, install both. If one or both are set, install only those.
30
+ const installPreCommit = opts.preCommit || (!opts.preCommit && !opts.commitMsg);
31
+ const installCommitMsg = opts.commitMsg || (!opts.preCommit && !opts.commitMsg);
32
+ const hooks = [];
33
+ if (installPreCommit) {
34
+ hooks.push({
35
+ type: 'pre-commit',
36
+ content: generatePreCommitHook(),
37
+ });
38
+ }
39
+ if (installCommitMsg) {
40
+ hooks.push({
41
+ type: 'commit-msg',
42
+ content: generateCommitMsgHook(),
43
+ });
44
+ }
45
+ let installed = 0;
46
+ for (const hook of hooks) {
47
+ const result = installHook(hooksDir, hook.type, hook.content, force);
48
+ if (result.success) {
49
+ tui.log.success(result.message);
50
+ installed++;
51
+ }
52
+ else {
53
+ tui.log.error(result.message);
54
+ }
55
+ }
56
+ if (installed > 0) {
57
+ tui.log.info(`\nInstalled ${installed} hook(s) to ${hooksDir}`);
58
+ }
59
+ else {
60
+ tui.log.warn('No hooks were installed.');
61
+ }
62
+ });
63
+ }
64
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../../../src/commands/hooks/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAE3F,OAAO,KAAK,GAAG,MAAM,iBAAiB,CAAC;AAQvC,MAAM,UAAU,sBAAsB,CAAC,MAAe;IACpD,MAAM;SACH,OAAO,CAAC,SAAS,CAAC;SAClB,WAAW,CAAC,6CAA6C,CAAC;SAC1D,MAAM,CAAC,SAAS,EAAE,mDAAmD,CAAC;SACtE,MAAM,CAAC,cAAc,EAAE,kCAAkC,CAAC;SAC1D,MAAM,CAAC,cAAc,EAAE,kCAAkC,CAAC;SAC1D,MAAM,CAAC,CAAC,IAAoB,EAAE,EAAE;QAC/B,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;YACjB,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,gEAAgE,CAAC,CAAC;YAChF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC;QAElC,oCAAoC;QACpC,oFAAoF;QACpF,MAAM,gBAAgB,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAChF,MAAM,gBAAgB,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAEhF,MAAM,KAAK,GAA+C,EAAE,CAAC;QAE7D,IAAI,gBAAgB,EAAE,CAAC;YACrB,KAAK,CAAC,IAAI,CAAC;gBACT,IAAI,EAAE,YAAY;gBAClB,OAAO,EAAE,qBAAqB,EAAE;aACjC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,gBAAgB,EAAE,CAAC;YACrB,KAAK,CAAC,IAAI,CAAC;gBACT,IAAI,EAAE,YAAY;gBAClB,OAAO,EAAE,qBAAqB,EAAE;aACjC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,SAAS,GAAG,CAAC,CAAC;QAElB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YAErE,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnB,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAChC,SAAS,EAAE,CAAC;YACd,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAChC,CAAC;QACH,CAAC;QAED,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YAClB,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,SAAS,eAAe,QAAQ,EAAE,CAAC,CAAC;QAClE,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Tests for `ghagga hooks install` subcommand.
3
+ *
4
+ * Mocks git-hooks utilities, hook-templates, and the TUI layer.
5
+ * Tests hook selection (both, --pre-commit, --commit-msg),
6
+ * --force flag, non-git-repo error, and success/failure messages.
7
+ *
8
+ * @see Phase 4, Test 4
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=install.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.test.d.ts","sourceRoot":"","sources":["../../../src/commands/hooks/install.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Tests for `ghagga hooks install` subcommand.
3
+ *
4
+ * Mocks git-hooks utilities, hook-templates, and the TUI layer.
5
+ * Tests hook selection (both, --pre-commit, --commit-msg),
6
+ * --force flag, non-git-repo error, and success/failure messages.
7
+ *
8
+ * @see Phase 4, Test 4
9
+ */
10
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
+ import { Command } from 'commander';
12
+ // ─── Mocks ──────────────────────────────────────────────────────
13
+ const { mockIsGitRepo, mockGetHooksDir, mockInstallHook, mockGenPreCommit, mockGenCommitMsg } = vi.hoisted(() => ({
14
+ mockIsGitRepo: vi.fn(),
15
+ mockGetHooksDir: vi.fn(),
16
+ mockInstallHook: vi.fn(),
17
+ mockGenPreCommit: vi.fn(),
18
+ mockGenCommitMsg: vi.fn(),
19
+ }));
20
+ vi.mock('../../lib/git-hooks.js', () => ({
21
+ isGitRepo: (...args) => mockIsGitRepo(...args),
22
+ getHooksDir: (...args) => mockGetHooksDir(...args),
23
+ installHook: (...args) => mockInstallHook(...args),
24
+ }));
25
+ vi.mock('../../lib/hook-templates.js', () => ({
26
+ generatePreCommitHook: (...args) => mockGenPreCommit(...args),
27
+ generateCommitMsgHook: (...args) => mockGenCommitMsg(...args),
28
+ }));
29
+ vi.mock('../../ui/tui.js', () => ({
30
+ log: {
31
+ success: vi.fn(),
32
+ error: vi.fn(),
33
+ info: vi.fn(),
34
+ warn: vi.fn(),
35
+ },
36
+ }));
37
+ import { registerInstallCommand } from './install.js';
38
+ import * as tui from '../../ui/tui.js';
39
+ // ─── Helpers ────────────────────────────────────────────────────
40
+ class ProcessExitError extends Error {
41
+ code;
42
+ constructor(code) {
43
+ super(`process.exit(${code})`);
44
+ this.code = code;
45
+ }
46
+ }
47
+ async function runInstallCommand(args = []) {
48
+ const parent = new Command('hooks');
49
+ registerInstallCommand(parent);
50
+ try {
51
+ await parent.parseAsync(['install', ...args], { from: 'user' });
52
+ }
53
+ catch (err) {
54
+ if (!(err instanceof ProcessExitError))
55
+ throw err;
56
+ }
57
+ }
58
+ // ─── Setup ──────────────────────────────────────────────────────
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ let exitSpy;
61
+ beforeEach(() => {
62
+ vi.clearAllMocks();
63
+ exitSpy = vi
64
+ .spyOn(process, 'exit')
65
+ .mockImplementation(((code) => {
66
+ throw new ProcessExitError(code);
67
+ }));
68
+ // Defaults: in a git repo, hooks dir exists
69
+ mockIsGitRepo.mockReturnValue(true);
70
+ mockGetHooksDir.mockReturnValue('/repo/.git/hooks');
71
+ mockGenPreCommit.mockReturnValue('pre-commit-content');
72
+ mockGenCommitMsg.mockReturnValue('commit-msg-content');
73
+ mockInstallHook.mockReturnValue({
74
+ type: 'pre-commit',
75
+ success: true,
76
+ message: 'Installed pre-commit hook',
77
+ });
78
+ });
79
+ afterEach(() => {
80
+ exitSpy.mockRestore();
81
+ });
82
+ // ─── Tests ──────────────────────────────────────────────────────
83
+ describe('ghagga hooks install', () => {
84
+ it('installs both hooks by default', async () => {
85
+ mockInstallHook
86
+ .mockReturnValueOnce({ type: 'pre-commit', success: true, message: 'Installed pre-commit hook' })
87
+ .mockReturnValueOnce({ type: 'commit-msg', success: true, message: 'Installed commit-msg hook' });
88
+ await runInstallCommand();
89
+ expect(mockInstallHook).toHaveBeenCalledTimes(2);
90
+ expect(mockInstallHook).toHaveBeenCalledWith('/repo/.git/hooks', 'pre-commit', 'pre-commit-content', false);
91
+ expect(mockInstallHook).toHaveBeenCalledWith('/repo/.git/hooks', 'commit-msg', 'commit-msg-content', false);
92
+ expect(tui.log.success).toHaveBeenCalledTimes(2);
93
+ });
94
+ it('installs only pre-commit when --pre-commit is passed', async () => {
95
+ mockInstallHook.mockReturnValue({
96
+ type: 'pre-commit', success: true, message: 'Installed pre-commit hook',
97
+ });
98
+ await runInstallCommand(['--pre-commit']);
99
+ expect(mockInstallHook).toHaveBeenCalledTimes(1);
100
+ expect(mockInstallHook).toHaveBeenCalledWith('/repo/.git/hooks', 'pre-commit', 'pre-commit-content', false);
101
+ });
102
+ it('installs only commit-msg when --commit-msg is passed', async () => {
103
+ mockInstallHook.mockReturnValue({
104
+ type: 'commit-msg', success: true, message: 'Installed commit-msg hook',
105
+ });
106
+ await runInstallCommand(['--commit-msg']);
107
+ expect(mockInstallHook).toHaveBeenCalledTimes(1);
108
+ expect(mockInstallHook).toHaveBeenCalledWith('/repo/.git/hooks', 'commit-msg', 'commit-msg-content', false);
109
+ });
110
+ it('passes force flag when --force is used', async () => {
111
+ mockInstallHook.mockReturnValue({
112
+ type: 'pre-commit', success: true, message: 'Installed',
113
+ });
114
+ await runInstallCommand(['--force', '--pre-commit']);
115
+ expect(mockInstallHook).toHaveBeenCalledWith('/repo/.git/hooks', 'pre-commit', 'pre-commit-content', true);
116
+ });
117
+ it('exits with error when not in a git repo', async () => {
118
+ mockIsGitRepo.mockReturnValue(false);
119
+ await runInstallCommand();
120
+ expect(exitSpy).toHaveBeenCalledWith(1);
121
+ expect(tui.log.error).toHaveBeenCalledWith(expect.stringContaining('Not a git repository'));
122
+ expect(mockInstallHook).not.toHaveBeenCalled();
123
+ });
124
+ it('shows error message when install fails', async () => {
125
+ mockInstallHook
126
+ .mockReturnValueOnce({
127
+ type: 'pre-commit',
128
+ success: false,
129
+ message: 'Hook pre-commit already exists (not managed by GHAGGA). Use --force to overwrite.',
130
+ })
131
+ .mockReturnValueOnce({
132
+ type: 'commit-msg',
133
+ success: true,
134
+ message: 'Installed commit-msg hook',
135
+ });
136
+ await runInstallCommand();
137
+ expect(tui.log.error).toHaveBeenCalledWith(expect.stringContaining('--force'));
138
+ expect(tui.log.success).toHaveBeenCalledWith(expect.stringContaining('Installed commit-msg'));
139
+ });
140
+ it('shows "no hooks installed" warning when all installs fail', async () => {
141
+ mockInstallHook.mockReturnValue({
142
+ type: 'pre-commit',
143
+ success: false,
144
+ message: 'Failed',
145
+ });
146
+ await runInstallCommand();
147
+ expect(tui.log.warn).toHaveBeenCalledWith(expect.stringContaining('No hooks were installed'));
148
+ });
149
+ it('shows install count summary on success', async () => {
150
+ mockInstallHook
151
+ .mockReturnValueOnce({ type: 'pre-commit', success: true, message: 'ok' })
152
+ .mockReturnValueOnce({ type: 'commit-msg', success: true, message: 'ok' });
153
+ await runInstallCommand();
154
+ expect(tui.log.info).toHaveBeenCalledWith(expect.stringContaining('Installed 2 hook(s)'));
155
+ });
156
+ });
157
+ //# sourceMappingURL=install.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.test.js","sourceRoot":"","sources":["../../../src/commands/hooks/install.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,mEAAmE;AAEnE,MAAM,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,GAC3F,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAChB,aAAa,EAAE,EAAE,CAAC,EAAE,EAAE;IACtB,eAAe,EAAE,EAAE,CAAC,EAAE,EAAE;IACxB,eAAe,EAAE,EAAE,CAAC,EAAE,EAAE;IACxB,gBAAgB,EAAE,EAAE,CAAC,EAAE,EAAE;IACzB,gBAAgB,EAAE,EAAE,CAAC,EAAE,EAAE;CAC1B,CAAC,CAAC,CAAC;AAEN,EAAE,CAAC,IAAI,CAAC,wBAAwB,EAAE,GAAG,EAAE,CAAC,CAAC;IACvC,SAAS,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC;IACzD,WAAW,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,eAAe,CAAC,GAAG,IAAI,CAAC;IAC7D,WAAW,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,eAAe,CAAC,GAAG,IAAI,CAAC;CAC9D,CAAC,CAAC,CAAC;AAEJ,EAAE,CAAC,IAAI,CAAC,6BAA6B,EAAE,GAAG,EAAE,CAAC,CAAC;IAC5C,qBAAqB,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAC;IACxE,qBAAqB,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAC;CACzE,CAAC,CAAC,CAAC;AAEJ,EAAE,CAAC,IAAI,CAAC,iBAAiB,EAAE,GAAG,EAAE,CAAC,CAAC;IAChC,GAAG,EAAE;QACH,OAAO,EAAE,EAAE,CAAC,EAAE,EAAE;QAChB,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE;QACd,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE;QACb,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE;KACd;CACF,CAAC,CAAC,CAAC;AAEJ,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,GAAG,MAAM,iBAAiB,CAAC;AAEvC,mEAAmE;AAEnE,MAAM,gBAAiB,SAAQ,KAAK;IACf;IAAnB,YAAmB,IAAwB;QACzC,KAAK,CAAC,gBAAgB,IAAI,GAAG,CAAC,CAAC;QADd,SAAI,GAAJ,IAAI,CAAoB;IAE3C,CAAC;CACF;AAED,KAAK,UAAU,iBAAiB,CAAC,OAAiB,EAAE;IAClD,MAAM,MAAM,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACpC,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,EAAE,GAAG,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IAClE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,CAAC,GAAG,YAAY,gBAAgB,CAAC;YAAE,MAAM,GAAG,CAAC;IACpD,CAAC;AACH,CAAC;AAED,mEAAmE;AAEnE,8DAA8D;AAC9D,IAAI,OAAY,CAAC;AAEjB,UAAU,CAAC,GAAG,EAAE;IACd,EAAE,CAAC,aAAa,EAAE,CAAC;IACnB,OAAO,GAAG,EAAE;SACT,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC;SACtB,kBAAkB,CAAC,CAAC,CAAC,IAAa,EAAE,EAAE;QACrC,MAAM,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC,CAAU,CAAC,CAAC;IAEf,4CAA4C;IAC5C,aAAa,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACpC,eAAe,CAAC,eAAe,CAAC,kBAAkB,CAAC,CAAC;IACpD,gBAAgB,CAAC,eAAe,CAAC,oBAAoB,CAAC,CAAC;IACvD,gBAAgB,CAAC,eAAe,CAAC,oBAAoB,CAAC,CAAC;IACvD,eAAe,CAAC,eAAe,CAAC;QAC9B,IAAI,EAAE,YAAY;QAClB,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,2BAA2B;KACrC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,SAAS,CAAC,GAAG,EAAE;IACb,OAAO,CAAC,WAAW,EAAE,CAAC;AACxB,CAAC,CAAC,CAAC;AAEH,mEAAmE;AAEnE,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,gCAAgC,EAAE,KAAK,IAAI,EAAE;QAC9C,eAAe;aACZ,mBAAmB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,2BAA2B,EAAE,CAAC;aAChG,mBAAmB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,2BAA2B,EAAE,CAAC,CAAC;QAEpG,MAAM,iBAAiB,EAAE,CAAC;QAE1B,MAAM,CAAC,eAAe,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QACjD,MAAM,CAAC,eAAe,CAAC,CAAC,oBAAoB,CAC1C,kBAAkB,EAAE,YAAY,EAAE,oBAAoB,EAAE,KAAK,CAC9D,CAAC;QACF,MAAM,CAAC,eAAe,CAAC,CAAC,oBAAoB,CAC1C,kBAAkB,EAAE,YAAY,EAAE,oBAAoB,EAAE,KAAK,CAC9D,CAAC;QACF,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACpE,eAAe,CAAC,eAAe,CAAC;YAC9B,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,2BAA2B;SACxE,CAAC,CAAC;QAEH,MAAM,iBAAiB,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC;QAE1C,MAAM,CAAC,eAAe,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QACjD,MAAM,CAAC,eAAe,CAAC,CAAC,oBAAoB,CAC1C,kBAAkB,EAAE,YAAY,EAAE,oBAAoB,EAAE,KAAK,CAC9D,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACpE,eAAe,CAAC,eAAe,CAAC;YAC9B,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,2BAA2B;SACxE,CAAC,CAAC;QAEH,MAAM,iBAAiB,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC;QAE1C,MAAM,CAAC,eAAe,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QACjD,MAAM,CAAC,eAAe,CAAC,CAAC,oBAAoB,CAC1C,kBAAkB,EAAE,YAAY,EAAE,oBAAoB,EAAE,KAAK,CAC9D,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACtD,eAAe,CAAC,eAAe,CAAC;YAC9B,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW;SACxD,CAAC,CAAC;QAEH,MAAM,iBAAiB,CAAC,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC;QAErD,MAAM,CAAC,eAAe,CAAC,CAAC,oBAAoB,CAC1C,kBAAkB,EAAE,YAAY,EAAE,oBAAoB,EAAE,IAAI,CAC7D,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,aAAa,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;QAErC,MAAM,iBAAiB,EAAE,CAAC;QAE1B,MAAM,CAAC,OAAO,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC;QACxC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,oBAAoB,CACxC,MAAM,CAAC,gBAAgB,CAAC,sBAAsB,CAAC,CAChD,CAAC;QACF,MAAM,CAAC,eAAe,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACtD,eAAe;aACZ,mBAAmB,CAAC;YACnB,IAAI,EAAE,YAAY;YAClB,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,mFAAmF;SAC7F,CAAC;aACD,mBAAmB,CAAC;YACnB,IAAI,EAAE,YAAY;YAClB,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,2BAA2B;SACrC,CAAC,CAAC;QAEL,MAAM,iBAAiB,EAAE,CAAC;QAE1B,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,oBAAoB,CACxC,MAAM,CAAC,gBAAgB,CAAC,SAAS,CAAC,CACnC,CAAC;QACF,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,oBAAoB,CAC1C,MAAM,CAAC,gBAAgB,CAAC,sBAAsB,CAAC,CAChD,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACzE,eAAe,CAAC,eAAe,CAAC;YAC9B,IAAI,EAAE,YAAY;YAClB,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,QAAQ;SAClB,CAAC,CAAC;QAEH,MAAM,iBAAiB,EAAE,CAAC;QAE1B,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,oBAAoB,CACvC,MAAM,CAAC,gBAAgB,CAAC,yBAAyB,CAAC,CACnD,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACtD,eAAe;aACZ,mBAAmB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;aACzE,mBAAmB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAE7E,MAAM,iBAAiB,EAAE,CAAC;QAE1B,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,oBAAoB,CACvC,MAAM,CAAC,gBAAgB,CAAC,qBAAqB,CAAC,CAC/C,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `ghagga hooks status` subcommand.
3
+ *
4
+ * Shows the status of pre-commit and commit-msg hooks in the
5
+ * current git repository: not installed, installed (GHAGGA-managed),
6
+ * or installed (external).
7
+ *
8
+ * @see Phase 3, Task 3.4
9
+ */
10
+ import { Command } from 'commander';
11
+ export declare function registerStatusCommand(parent: Command): void;
12
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/commands/hooks/status.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CA0B3D"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `ghagga hooks status` subcommand.
3
+ *
4
+ * Shows the status of pre-commit and commit-msg hooks in the
5
+ * current git repository: not installed, installed (GHAGGA-managed),
6
+ * or installed (external).
7
+ *
8
+ * @see Phase 3, Task 3.4
9
+ */
10
+ import { isGitRepo, getHooksDir, getHookStatus } from '../../lib/git-hooks.js';
11
+ import * as tui from '../../ui/tui.js';
12
+ const HOOK_TYPES = ['pre-commit', 'commit-msg'];
13
+ export function registerStatusCommand(parent) {
14
+ parent
15
+ .command('status')
16
+ .description('Show status of git hooks')
17
+ .action(() => {
18
+ if (!isGitRepo()) {
19
+ tui.log.error('Not a git repository. Run this command from inside a git repo.');
20
+ process.exit(1);
21
+ }
22
+ const hooksDir = getHooksDir();
23
+ tui.log.info(`Hooks directory: ${hooksDir}\n`);
24
+ for (const hookType of HOOK_TYPES) {
25
+ const status = getHookStatus(hooksDir, hookType);
26
+ if (!status.installed) {
27
+ tui.log.info(` ${hookType}: not installed`);
28
+ }
29
+ else if (status.managedByGhagga) {
30
+ tui.log.success(` ${hookType}: installed (GHAGGA-managed)`);
31
+ }
32
+ else {
33
+ tui.log.warn(` ${hookType}: installed (external — not managed by GHAGGA)`);
34
+ }
35
+ }
36
+ });
37
+ }
38
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/commands/hooks/status.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE/E,OAAO,KAAK,GAAG,MAAM,iBAAiB,CAAC;AAEvC,MAAM,UAAU,GAAe,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;AAE5D,MAAM,UAAU,qBAAqB,CAAC,MAAe;IACnD,MAAM;SACH,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,0BAA0B,CAAC;SACvC,MAAM,CAAC,GAAG,EAAE;QACX,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;YACjB,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,gEAAgE,CAAC,CAAC;YAChF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;QAE/B,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,oBAAoB,QAAQ,IAAI,CAAC,CAAC;QAE/C,KAAK,MAAM,QAAQ,IAAI,UAAU,EAAE,CAAC;YAClC,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;YAEjD,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;gBACtB,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,QAAQ,iBAAiB,CAAC,CAAC;YAC/C,CAAC;iBAAM,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;gBAClC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,QAAQ,8BAA8B,CAAC,CAAC;YAC/D,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,QAAQ,gDAAgD,CAAC,CAAC;YAC9E,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Tests for `ghagga hooks status` subcommand.
3
+ *
4
+ * Mocks git-hooks utilities and the TUI layer.
5
+ * Tests display of hook status for both hooks, non-git-repo error,
6
+ * and correct labels for not-installed / GHAGGA / external hooks.
7
+ *
8
+ * @see Phase 4, Test 6
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=status.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.test.d.ts","sourceRoot":"","sources":["../../../src/commands/hooks/status.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Tests for `ghagga hooks status` subcommand.
3
+ *
4
+ * Mocks git-hooks utilities and the TUI layer.
5
+ * Tests display of hook status for both hooks, non-git-repo error,
6
+ * and correct labels for not-installed / GHAGGA / external hooks.
7
+ *
8
+ * @see Phase 4, Test 6
9
+ */
10
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
+ import { Command } from 'commander';
12
+ // ─── Mocks ──────────────────────────────────────────────────────
13
+ const { mockIsGitRepo, mockGetHooksDir, mockGetHookStatus } = vi.hoisted(() => ({
14
+ mockIsGitRepo: vi.fn(),
15
+ mockGetHooksDir: vi.fn(),
16
+ mockGetHookStatus: vi.fn(),
17
+ }));
18
+ vi.mock('../../lib/git-hooks.js', () => ({
19
+ isGitRepo: (...args) => mockIsGitRepo(...args),
20
+ getHooksDir: (...args) => mockGetHooksDir(...args),
21
+ getHookStatus: (...args) => mockGetHookStatus(...args),
22
+ }));
23
+ vi.mock('../../ui/tui.js', () => ({
24
+ log: {
25
+ success: vi.fn(),
26
+ error: vi.fn(),
27
+ info: vi.fn(),
28
+ warn: vi.fn(),
29
+ },
30
+ }));
31
+ import { registerStatusCommand } from './status.js';
32
+ import * as tui from '../../ui/tui.js';
33
+ // ─── Helpers ────────────────────────────────────────────────────
34
+ class ProcessExitError extends Error {
35
+ code;
36
+ constructor(code) {
37
+ super(`process.exit(${code})`);
38
+ this.code = code;
39
+ }
40
+ }
41
+ async function runStatusCommand(args = []) {
42
+ const parent = new Command('hooks');
43
+ registerStatusCommand(parent);
44
+ try {
45
+ await parent.parseAsync(['status', ...args], { from: 'user' });
46
+ }
47
+ catch (err) {
48
+ if (!(err instanceof ProcessExitError))
49
+ throw err;
50
+ }
51
+ }
52
+ // ─── Setup ──────────────────────────────────────────────────────
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ let exitSpy;
55
+ beforeEach(() => {
56
+ vi.clearAllMocks();
57
+ exitSpy = vi
58
+ .spyOn(process, 'exit')
59
+ .mockImplementation(((code) => {
60
+ throw new ProcessExitError(code);
61
+ }));
62
+ mockIsGitRepo.mockReturnValue(true);
63
+ mockGetHooksDir.mockReturnValue('/repo/.git/hooks');
64
+ });
65
+ afterEach(() => {
66
+ exitSpy.mockRestore();
67
+ });
68
+ // ─── Tests ──────────────────────────────────────────────────────
69
+ describe('ghagga hooks status', () => {
70
+ it('shows status for both pre-commit and commit-msg hooks', async () => {
71
+ mockGetHookStatus
72
+ .mockReturnValueOnce({ type: 'pre-commit', installed: true, managedByGhagga: true, path: '/repo/.git/hooks/pre-commit' })
73
+ .mockReturnValueOnce({ type: 'commit-msg', installed: false, managedByGhagga: false, path: '/repo/.git/hooks/commit-msg' });
74
+ await runStatusCommand();
75
+ expect(mockGetHookStatus).toHaveBeenCalledTimes(2);
76
+ expect(mockGetHookStatus).toHaveBeenCalledWith('/repo/.git/hooks', 'pre-commit');
77
+ expect(mockGetHookStatus).toHaveBeenCalledWith('/repo/.git/hooks', 'commit-msg');
78
+ });
79
+ it('exits with error when not in a git repo', async () => {
80
+ mockIsGitRepo.mockReturnValue(false);
81
+ await runStatusCommand();
82
+ expect(exitSpy).toHaveBeenCalledWith(1);
83
+ expect(tui.log.error).toHaveBeenCalledWith(expect.stringContaining('Not a git repository'));
84
+ expect(mockGetHookStatus).not.toHaveBeenCalled();
85
+ });
86
+ it('shows "not installed" for hooks that do not exist', async () => {
87
+ mockGetHookStatus.mockReturnValue({
88
+ type: 'pre-commit', installed: false, managedByGhagga: false, path: '/repo/.git/hooks/pre-commit',
89
+ });
90
+ await runStatusCommand();
91
+ expect(tui.log.info).toHaveBeenCalledWith(expect.stringContaining('not installed'));
92
+ });
93
+ it('shows "GHAGGA-managed" for installed GHAGGA hooks', async () => {
94
+ mockGetHookStatus.mockReturnValue({
95
+ type: 'pre-commit', installed: true, managedByGhagga: true, path: '/repo/.git/hooks/pre-commit',
96
+ });
97
+ await runStatusCommand();
98
+ expect(tui.log.success).toHaveBeenCalledWith(expect.stringContaining('GHAGGA-managed'));
99
+ });
100
+ it('shows "external" for installed non-GHAGGA hooks', async () => {
101
+ mockGetHookStatus.mockReturnValue({
102
+ type: 'pre-commit', installed: true, managedByGhagga: false, path: '/repo/.git/hooks/pre-commit',
103
+ });
104
+ await runStatusCommand();
105
+ expect(tui.log.warn).toHaveBeenCalledWith(expect.stringContaining('external'));
106
+ });
107
+ it('shows hooks directory path', async () => {
108
+ mockGetHookStatus.mockReturnValue({
109
+ type: 'pre-commit', installed: false, managedByGhagga: false, path: '/repo/.git/hooks/pre-commit',
110
+ });
111
+ await runStatusCommand();
112
+ expect(tui.log.info).toHaveBeenCalledWith(expect.stringContaining('/repo/.git/hooks'));
113
+ });
114
+ it('shows mixed status for different hooks', async () => {
115
+ mockGetHookStatus
116
+ .mockReturnValueOnce({ type: 'pre-commit', installed: true, managedByGhagga: true, path: '/repo/.git/hooks/pre-commit' })
117
+ .mockReturnValueOnce({ type: 'commit-msg', installed: true, managedByGhagga: false, path: '/repo/.git/hooks/commit-msg' });
118
+ await runStatusCommand();
119
+ expect(tui.log.success).toHaveBeenCalledWith(expect.stringContaining('pre-commit'));
120
+ expect(tui.log.warn).toHaveBeenCalledWith(expect.stringContaining('commit-msg'));
121
+ });
122
+ });
123
+ //# sourceMappingURL=status.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.test.js","sourceRoot":"","sources":["../../../src/commands/hooks/status.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `ghagga hooks uninstall` subcommand.
3
+ *
4
+ * Removes GHAGGA-managed hooks from the current git repository.
5
+ * Only removes hooks that contain the GHAGGA marker comment.
6
+ * Restores backed-up hooks if they exist.
7
+ *
8
+ * @see Phase 3, Task 3.3
9
+ */
10
+ import { Command } from 'commander';
11
+ export declare function registerUninstallCommand(parent: Command): void;
12
+ //# sourceMappingURL=uninstall.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uninstall.d.ts","sourceRoot":"","sources":["../../../src/commands/hooks/uninstall.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAgC9D"}