sight 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/CLAUDE.md +10 -2
- data/README.md +20 -0
- data/lib/sight/app.rb +67 -6
- data/lib/sight/claude_hook_installer.rb +64 -0
- data/lib/sight/cli.rb +93 -5
- data/lib/sight/cursor_hook_installer.rb +62 -0
- data/lib/sight/diff_parser.rb +14 -2
- data/lib/sight/git.rb +29 -0
- data/lib/sight/version.rb +1 -1
- data/lib/sight.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff89f4bccb463b21835b299946bcf7a54b43f9792eb0b262954812c79633da19
|
|
4
|
+
data.tar.gz: 7dab4e4911a9bf145a5b0542ac993c6b5d3ce25af54b345e537291c5567db681
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c591ba467ae6d6a64d1d54c188513a2afe743c40e767bca6892f9f30e6ff45ecfb1f134fcef62c420b4e7fb8c13a77fcd9c69e8f830c6cf1a7c79f8d4d61b82f
|
|
7
|
+
data.tar.gz: 131fd548a595de6b14288b2ae536b2275eb66fb50f6e08182875866bdee44c0acfbb8194a0ee515e510472bb72f38eeae34fa7c64f64e0a97b2e4e4433867775
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-03-06
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Untracked file support with file status badges (new, modified, deleted)
|
|
8
|
+
- Color-coded status badges in file header
|
|
9
|
+
- Ctrl-f/b/d/u scroll navigation
|
|
10
|
+
- Scroll percentage indicator replacing line counter
|
|
11
|
+
- Hook system for AI agent integration (`sight install-hook <agent>`)
|
|
12
|
+
- Claude Code hook via `UserPromptSubmit`
|
|
13
|
+
- Cursor hook via `beforeSubmitPrompt`
|
|
14
|
+
- Highlighted commented hunks in gutter and status bar
|
|
15
|
+
|
|
3
16
|
## [0.3.0] - 2026-03-04
|
|
4
17
|
|
|
5
18
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -15,12 +15,20 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
15
15
|
|
|
16
16
|
TUI for closing the loop on AI-generated code changes — browse diffs, jump between hunks, and annotate them. Entry point: `exe/sight` → `CLI.run` → `App.new(files).run`.
|
|
17
17
|
|
|
18
|
-
**Data flow**: `Git.diff` (raw string) → `DiffParser.parse` (returns `DiffFile[]`) → `App` (curses TUI)
|
|
18
|
+
**Data flow**: `Git.diff` (raw string) → `DiffParser.parse` (returns `DiffFile[]`) → `App` (curses TUI).
|
|
19
|
+
Untracked files are added via `Git.untracked_files` → `DiffParser.build_untracked`.
|
|
19
20
|
|
|
20
|
-
**Key structs** (all in `diff_parser.rb`): `DiffFile(path, hunks)`, `Hunk(context, lines)`, `DiffLine(type, content, lineno, old_lineno)`.
|
|
21
|
+
**Key structs** (all in `diff_parser.rb`): `DiffFile(path, hunks, status)`, `Hunk(context, lines)`, `DiffLine(type, content, lineno, old_lineno)`.
|
|
22
|
+
`DisplayLine(type, text, lineno)` is the render-side equivalent in `display_line.rb`.
|
|
23
|
+
`Annotation(file_path, type, hunk, comment)` in `annotation.rb` stores per-hunk comments; `AnnotationFormatter` serializes them for output.
|
|
21
24
|
|
|
22
25
|
**App** renders per-file views with hunk-based navigation (j/k). Active hunk is highlighted; inactive hunks render in dark gray (color pair 5, color 240).
|
|
23
26
|
|
|
27
|
+
**Hook system**: Unified via `sight install-hook <agent>` / `sight uninstall-hook <agent>` (agent: `claude` or `cursor`).
|
|
28
|
+
`ClaudeHookInstaller` manages a Claude Code `UserPromptSubmit` hook in `~/.config/claude/settings.json`.
|
|
29
|
+
`CursorHookInstaller` manages a Cursor `beforeSubmitPrompt` hook in `~/.cursor/hooks.json`.
|
|
30
|
+
Both read `.git/sight/pending-review`, output annotations, and delete the file. Hidden subcommands: `hook-run`, `cursor-hook-run`.
|
|
31
|
+
|
|
24
32
|
## Conventions
|
|
25
33
|
|
|
26
34
|
- Ruby >= 3.2, uses `frozen_string_literal` in all files
|
data/README.md
CHANGED
|
@@ -24,10 +24,30 @@ sight
|
|
|
24
24
|
| `k` | Previous hunk |
|
|
25
25
|
| `n` | Next file |
|
|
26
26
|
| `p` | Previous file |
|
|
27
|
+
| `Ctrl-F` / `Ctrl-B` | Scroll full page down / up |
|
|
28
|
+
| `Ctrl-D` / `Ctrl-U` | Scroll half page down / up |
|
|
27
29
|
| `?` | Toggle help |
|
|
28
30
|
| `c` | Comment on hunk |
|
|
29
31
|
| `q` / `Esc` | Quit |
|
|
30
32
|
|
|
33
|
+
### Agent Integration
|
|
34
|
+
|
|
35
|
+
Install a hook so annotations are automatically fed as context in your next message:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
sight install-hook claude # Claude Code (~/.config/claude/settings.json)
|
|
39
|
+
sight install-hook cursor # Cursor (~/.cursor/hooks.json)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
When you quit sight after annotating, the next message you send will include your annotations.
|
|
43
|
+
|
|
44
|
+
To remove:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
sight uninstall-hook claude
|
|
48
|
+
sight uninstall-hook cursor
|
|
49
|
+
```
|
|
50
|
+
|
|
31
51
|
## Development
|
|
32
52
|
|
|
33
53
|
```bash
|
data/lib/sight/app.rb
CHANGED
|
@@ -53,6 +53,7 @@ module Sight
|
|
|
53
53
|
Curses.init_pair(3, Curses::COLOR_CYAN, -1)
|
|
54
54
|
Curses.init_pair(4, Curses::COLOR_YELLOW, -1)
|
|
55
55
|
Curses.init_pair(5, 240, -1)
|
|
56
|
+
Curses.init_pair(6, Curses::COLOR_MAGENTA, -1)
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
def lines
|
|
@@ -70,11 +71,19 @@ module Sight
|
|
|
70
71
|
end
|
|
71
72
|
|
|
72
73
|
def render_header(win, width)
|
|
73
|
-
|
|
74
|
+
file = files[file_idx]
|
|
75
|
+
badge = "[#{file.status || :modified}]"
|
|
76
|
+
path = file.path
|
|
77
|
+
gap = width - path.length - badge.length
|
|
74
78
|
win.setpos(0, 0)
|
|
75
|
-
|
|
79
|
+
if gap >= 1
|
|
80
|
+
win.attron(color_for(:header)) { win.addstr("#{path}#{" " * gap}") }
|
|
81
|
+
win.attron(badge_color(file.status)) { win.addstr(badge) }
|
|
82
|
+
else
|
|
83
|
+
win.attron(color_for(:header)) { win.addstr(path[0, width]) }
|
|
84
|
+
end
|
|
76
85
|
win.setpos(1, 0)
|
|
77
|
-
win.attron(
|
|
86
|
+
win.attron(Curses.color_pair(0) | Curses::A_BOLD) { win.addstr("\u2500" * width) }
|
|
78
87
|
end
|
|
79
88
|
|
|
80
89
|
def render_content(win, width)
|
|
@@ -89,13 +98,21 @@ module Sight
|
|
|
89
98
|
lines.size
|
|
90
99
|
end
|
|
91
100
|
|
|
101
|
+
commented_lines = commented_hunk_lines
|
|
102
|
+
|
|
92
103
|
scroll_height.times do |row|
|
|
93
104
|
idx = offset + row
|
|
94
105
|
break if idx >= lines.size
|
|
95
106
|
line = lines[idx]
|
|
96
107
|
win.setpos(row + 2, 0)
|
|
97
108
|
active = idx >= selected_start && idx < selected_end
|
|
98
|
-
|
|
109
|
+
gutter_str = format_gutter(line.type, line.lineno, gutter)
|
|
110
|
+
commented = commented_lines.include?(idx)
|
|
111
|
+
separator = commented ? "\u2503" : "\u2502"
|
|
112
|
+
sep_attr = commented ? Curses.color_pair(4) : dim
|
|
113
|
+
win.attron(dim) { win.addstr("#{gutter_str} ") }
|
|
114
|
+
win.attron(sep_attr) { win.addstr(separator) }
|
|
115
|
+
win.attron(dim) { win.addstr(" ") }
|
|
99
116
|
attr = active ? color_for(line.type) : Curses.color_pair(5)
|
|
100
117
|
win.attron(attr) { win.addstr(line.text[0, content_width]) }
|
|
101
118
|
end
|
|
@@ -104,7 +121,13 @@ module Sight
|
|
|
104
121
|
def render_status_bar(win, width)
|
|
105
122
|
win.setpos(Curses.lines - 1, 0)
|
|
106
123
|
win.attron(Curses.color_pair(4) | Curses::A_REVERSE) do
|
|
107
|
-
|
|
124
|
+
percent = if lines.size <= scroll_height
|
|
125
|
+
100
|
|
126
|
+
else
|
|
127
|
+
((offset + scroll_height) * 100.0 / lines.size).ceil.clamp(0, 100)
|
|
128
|
+
end
|
|
129
|
+
commented = hunk_commented?(file_idx, hunk_idx) ? " [commented]" : ""
|
|
130
|
+
status = " File #{file_idx + 1}/#{files.size} | Hunk #{hunk_idx + 1}/#{hunk_offsets.size}#{commented} | #{percent}% "
|
|
108
131
|
win.addstr(status.ljust(width))
|
|
109
132
|
end
|
|
110
133
|
end
|
|
@@ -123,11 +146,20 @@ module Sight
|
|
|
123
146
|
case type
|
|
124
147
|
when :add then Curses.color_pair(1)
|
|
125
148
|
when :del then Curses.color_pair(2)
|
|
126
|
-
when :header then Curses.color_pair(
|
|
149
|
+
when :header then Curses.color_pair(0) | Curses::A_BOLD
|
|
127
150
|
else Curses.color_pair(0)
|
|
128
151
|
end
|
|
129
152
|
end
|
|
130
153
|
|
|
154
|
+
def badge_color(status)
|
|
155
|
+
case status
|
|
156
|
+
when :added then Curses.color_pair(1) | Curses::A_BOLD
|
|
157
|
+
when :deleted then Curses.color_pair(2) | Curses::A_BOLD
|
|
158
|
+
when :untracked then Curses.color_pair(6) | Curses::A_BOLD
|
|
159
|
+
else Curses.color_pair(4) | Curses::A_BOLD
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
131
163
|
def scroll_height
|
|
132
164
|
Curses.lines - 3
|
|
133
165
|
end
|
|
@@ -147,6 +179,10 @@ module Sight
|
|
|
147
179
|
when "k" then jump_hunk(-1)
|
|
148
180
|
when "n" then jump_file(1)
|
|
149
181
|
when "p" then jump_file(-1)
|
|
182
|
+
when 6 then scroll(scroll_height)
|
|
183
|
+
when 2 then scroll(-scroll_height)
|
|
184
|
+
when 4 then scroll(scroll_height / 2)
|
|
185
|
+
when 21 then scroll(-scroll_height / 2)
|
|
150
186
|
when "c" then annotate_hunk
|
|
151
187
|
when "?" then show_help
|
|
152
188
|
end
|
|
@@ -156,6 +192,10 @@ module Sight
|
|
|
156
192
|
HELP_KEYS = [
|
|
157
193
|
["j", "Next hunk"],
|
|
158
194
|
["k", "Previous hunk"],
|
|
195
|
+
["C-f", "Page down"],
|
|
196
|
+
["C-b", "Page up"],
|
|
197
|
+
["C-d", "Half page down"],
|
|
198
|
+
["C-u", "Half page up"],
|
|
159
199
|
["n", "Next file"],
|
|
160
200
|
["p", "Previous file"],
|
|
161
201
|
["q / Esc", "Quit"],
|
|
@@ -271,6 +311,27 @@ module Sight
|
|
|
271
311
|
end
|
|
272
312
|
end
|
|
273
313
|
|
|
314
|
+
def hunk_commented?(file_index, hunk_index)
|
|
315
|
+
path = files[file_index].path
|
|
316
|
+
hunk = files[file_index].hunks[hunk_index]
|
|
317
|
+
annotations.any? { |a| a.file_path == path && a.hunk.equal?(hunk) }
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def commented_hunk_lines
|
|
321
|
+
result = []
|
|
322
|
+
hunk_offsets.each_with_index do |offset, hunk_index|
|
|
323
|
+
next unless hunk_commented?(file_idx, hunk_index)
|
|
324
|
+
hunk_end = (hunk_index + 1 < hunk_offsets.size) ? hunk_offsets[hunk_index + 1] : lines.size
|
|
325
|
+
(offset...hunk_end).each { |i| result << i }
|
|
326
|
+
end
|
|
327
|
+
result
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def scroll(delta)
|
|
331
|
+
max = [0, lines.size - scroll_height].max
|
|
332
|
+
self.offset = (offset + delta).clamp(0, max)
|
|
333
|
+
end
|
|
334
|
+
|
|
274
335
|
def jump_hunk(delta)
|
|
275
336
|
return if hunk_offsets.empty?
|
|
276
337
|
self.hunk_idx = (hunk_idx + delta).clamp(0, hunk_offsets.size - 1)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Sight
|
|
7
|
+
module ClaudeHookInstaller
|
|
8
|
+
HOOK_COMMAND = "sight hook-run"
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def settings_path
|
|
13
|
+
File.join(Dir.home, ".config", "claude", "settings.json")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def install(path: settings_path)
|
|
17
|
+
settings = if File.exist?(path)
|
|
18
|
+
JSON.parse(File.read(path))
|
|
19
|
+
else
|
|
20
|
+
{}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
hooks = settings["hooks"] ||= {}
|
|
24
|
+
prompt_hooks = hooks["UserPromptSubmit"] ||= []
|
|
25
|
+
|
|
26
|
+
if prompt_hooks.any? { |h| hook_is_sight?(h) }
|
|
27
|
+
puts "sight hook already installed"
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
prompt_hooks << {
|
|
32
|
+
"matcher" => "*",
|
|
33
|
+
"hooks" => [{"type" => "command", "command" => HOOK_COMMAND}]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
37
|
+
File.write(path, JSON.pretty_generate(settings) + "\n")
|
|
38
|
+
puts "Installed sight hook into #{path}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def uninstall(path: settings_path)
|
|
42
|
+
unless File.exist?(path)
|
|
43
|
+
puts "No settings file found"
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
settings = JSON.parse(File.read(path))
|
|
48
|
+
prompt_hooks = settings.dig("hooks", "UserPromptSubmit")
|
|
49
|
+
|
|
50
|
+
unless prompt_hooks&.any? { |h| hook_is_sight?(h) }
|
|
51
|
+
puts "No sight hook found"
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
prompt_hooks.reject! { |h| hook_is_sight?(h) }
|
|
56
|
+
File.write(path, JSON.pretty_generate(settings) + "\n")
|
|
57
|
+
puts "Uninstalled sight hook"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def hook_is_sight?(entry)
|
|
61
|
+
Array(entry["hooks"]).any? { |h| h["command"]&.include?("sight") }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/sight/cli.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
module Sight
|
|
4
6
|
module CLI
|
|
5
7
|
module_function
|
|
@@ -11,7 +13,9 @@ module Sight
|
|
|
11
13
|
puts
|
|
12
14
|
puts "Keys: j/k hunks, n/p files, c comment, ? help, q quit"
|
|
13
15
|
puts
|
|
14
|
-
puts "
|
|
16
|
+
puts "Subcommands:"
|
|
17
|
+
puts " install-hook <agent> Install hook (claude, cursor)"
|
|
18
|
+
puts " uninstall-hook <agent> Remove hook (claude, cursor)"
|
|
15
19
|
return
|
|
16
20
|
end
|
|
17
21
|
|
|
@@ -20,19 +24,103 @@ module Sight
|
|
|
20
24
|
return
|
|
21
25
|
end
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
if argv[0] == "install-hook"
|
|
28
|
+
install_hook(argv[1])
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if argv[0] == "uninstall-hook"
|
|
33
|
+
uninstall_hook(argv[1])
|
|
26
34
|
return
|
|
27
35
|
end
|
|
28
36
|
|
|
37
|
+
if argv.include?("hook-run")
|
|
38
|
+
run_hook
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if argv.include?("cursor-hook-run")
|
|
43
|
+
run_cursor_hook
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Git.clear_pending_review
|
|
48
|
+
|
|
49
|
+
raw = Git.diff
|
|
29
50
|
files = DiffParser.parse(raw)
|
|
51
|
+
|
|
52
|
+
Git.untracked_files.each do |path|
|
|
53
|
+
content = Git.file_content(path)
|
|
54
|
+
next unless content.valid_encoding? && !content.include?("\x00")
|
|
55
|
+
files << DiffParser.build_untracked(path, content)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if files.empty?
|
|
59
|
+
warn "No changes"
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
|
|
30
63
|
app = App.new(files)
|
|
31
64
|
app.run
|
|
32
65
|
|
|
33
66
|
unless app.annotations.empty?
|
|
34
|
-
|
|
67
|
+
formatted = AnnotationFormatter.format(app.annotations)
|
|
68
|
+
puts formatted
|
|
69
|
+
Git.save_pending_review(formatted)
|
|
35
70
|
end
|
|
36
71
|
end
|
|
72
|
+
|
|
73
|
+
AGENTS = {
|
|
74
|
+
"claude" => ClaudeHookInstaller,
|
|
75
|
+
"cursor" => CursorHookInstaller
|
|
76
|
+
}.freeze
|
|
77
|
+
|
|
78
|
+
def install_hook(agent)
|
|
79
|
+
installer = AGENTS[agent]
|
|
80
|
+
unless installer
|
|
81
|
+
warn "Unknown agent: #{agent.inspect}. Use: claude, cursor"
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
installer.install
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def uninstall_hook(agent)
|
|
88
|
+
installer = AGENTS[agent]
|
|
89
|
+
unless installer
|
|
90
|
+
warn "Unknown agent: #{agent.inspect}. Use: claude, cursor"
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
installer.uninstall
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def run_hook
|
|
97
|
+
git_dir = Git.repo_dir
|
|
98
|
+
rescue Error
|
|
99
|
+
nil
|
|
100
|
+
else
|
|
101
|
+
file = File.join(git_dir, "sight", "pending-review")
|
|
102
|
+
return unless File.exist?(file)
|
|
103
|
+
|
|
104
|
+
content = File.read(file)
|
|
105
|
+
puts "The user has just finished reviewing your code changes in sight. Here are their annotations:"
|
|
106
|
+
puts
|
|
107
|
+
puts content
|
|
108
|
+
File.delete(file)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def run_cursor_hook
|
|
112
|
+
$stdin.read
|
|
113
|
+
git_dir = Git.repo_dir
|
|
114
|
+
rescue Error
|
|
115
|
+
nil
|
|
116
|
+
else
|
|
117
|
+
file = File.join(git_dir, "sight", "pending-review")
|
|
118
|
+
return unless File.exist?(file)
|
|
119
|
+
|
|
120
|
+
content = File.read(file)
|
|
121
|
+
message = "The user has just finished reviewing your code changes in sight. Here are their annotations:\n\n#{content}"
|
|
122
|
+
puts JSON.generate(user_message: message)
|
|
123
|
+
File.delete(file)
|
|
124
|
+
end
|
|
37
125
|
end
|
|
38
126
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Sight
|
|
7
|
+
module CursorHookInstaller
|
|
8
|
+
HOOK_COMMAND = "sight cursor-hook-run"
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def hooks_path
|
|
13
|
+
File.join(Dir.home, ".cursor", "hooks.json")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def install(path: hooks_path)
|
|
17
|
+
config = if File.exist?(path)
|
|
18
|
+
JSON.parse(File.read(path))
|
|
19
|
+
else
|
|
20
|
+
{"version" => 1, "hooks" => {}}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
config["version"] ||= 1
|
|
24
|
+
hooks = config["hooks"] ||= {}
|
|
25
|
+
prompt_hooks = hooks["beforeSubmitPrompt"] ||= []
|
|
26
|
+
|
|
27
|
+
if prompt_hooks.any? { |h| hook_is_sight?(h) }
|
|
28
|
+
puts "sight hook already installed"
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
prompt_hooks << {"command" => HOOK_COMMAND}
|
|
33
|
+
|
|
34
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
35
|
+
File.write(path, JSON.pretty_generate(config) + "\n")
|
|
36
|
+
puts "Installed sight hook into #{path}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def uninstall(path: hooks_path)
|
|
40
|
+
unless File.exist?(path)
|
|
41
|
+
puts "No hooks file found"
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
config = JSON.parse(File.read(path))
|
|
46
|
+
prompt_hooks = config.dig("hooks", "beforeSubmitPrompt")
|
|
47
|
+
|
|
48
|
+
unless prompt_hooks&.any? { |h| hook_is_sight?(h) }
|
|
49
|
+
puts "No sight hook found"
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
prompt_hooks.reject! { |h| hook_is_sight?(h) }
|
|
54
|
+
File.write(path, JSON.pretty_generate(config) + "\n")
|
|
55
|
+
puts "Uninstalled sight hook"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def hook_is_sight?(entry)
|
|
59
|
+
entry["command"]&.include?("sight")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/sight/diff_parser.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Sight
|
|
4
4
|
DiffLine = Struct.new(:type, :content, :lineno, :old_lineno, keyword_init: true)
|
|
5
5
|
Hunk = Struct.new(:context, :lines, keyword_init: true)
|
|
6
|
-
DiffFile = Struct.new(:path, :hunks, keyword_init: true)
|
|
6
|
+
DiffFile = Struct.new(:path, :hunks, :status, keyword_init: true)
|
|
7
7
|
|
|
8
8
|
module DiffParser
|
|
9
9
|
module_function
|
|
@@ -19,10 +19,14 @@ module Sight
|
|
|
19
19
|
if line.start_with?("diff --git ")
|
|
20
20
|
current_hunk = nil
|
|
21
21
|
path = line.split(" b/", 2).last
|
|
22
|
-
current_file = DiffFile.new(path: path, hunks: [])
|
|
22
|
+
current_file = DiffFile.new(path: path, hunks: [], status: :modified)
|
|
23
23
|
files << current_file
|
|
24
24
|
elsif current_file.nil?
|
|
25
25
|
next
|
|
26
|
+
elsif line.start_with?("new file mode")
|
|
27
|
+
current_file.status = :added
|
|
28
|
+
elsif line.start_with?("deleted file mode")
|
|
29
|
+
current_file.status = :deleted
|
|
26
30
|
elsif line.start_with?("@@ ")
|
|
27
31
|
context, new_start, old_start = parse_hunk_header(line)
|
|
28
32
|
new_lineno = new_start
|
|
@@ -48,6 +52,14 @@ module Sight
|
|
|
48
52
|
files
|
|
49
53
|
end
|
|
50
54
|
|
|
55
|
+
def build_untracked(path, content)
|
|
56
|
+
lines = content.lines(chomp: true).each_with_index.map do |text, i|
|
|
57
|
+
DiffLine.new(type: :add, content: "+#{text}", lineno: i + 1, old_lineno: nil)
|
|
58
|
+
end
|
|
59
|
+
hunk = Hunk.new(context: nil, lines: lines)
|
|
60
|
+
DiffFile.new(path: path, hunks: [hunk], status: :untracked)
|
|
61
|
+
end
|
|
62
|
+
|
|
51
63
|
def parse_hunk_header(line)
|
|
52
64
|
match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)/)
|
|
53
65
|
return [nil, 1, 1] unless match
|
data/lib/sight/git.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
3
5
|
module Sight
|
|
4
6
|
module Git
|
|
5
7
|
module_function
|
|
@@ -10,6 +12,33 @@ module Sight
|
|
|
10
12
|
output
|
|
11
13
|
end
|
|
12
14
|
|
|
15
|
+
def untracked_files
|
|
16
|
+
output, success = run_cmd(["git", "ls-files", "--others", "--exclude-standard"])
|
|
17
|
+
return [] unless success
|
|
18
|
+
output.lines(chomp: true).reject(&:empty?)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def file_content(path)
|
|
22
|
+
File.read(path, mode: "rb")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def repo_dir
|
|
26
|
+
output, success = run_cmd(["git", "rev-parse", "--git-dir"])
|
|
27
|
+
raise Error, "not a git repository" unless success
|
|
28
|
+
output.strip
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def save_pending_review(content)
|
|
32
|
+
dir = File.join(repo_dir, "sight")
|
|
33
|
+
FileUtils.mkdir_p(dir)
|
|
34
|
+
File.write(File.join(dir, "pending-review"), content)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def clear_pending_review
|
|
38
|
+
path = File.join(repo_dir, "sight", "pending-review")
|
|
39
|
+
File.delete(path) if File.exist?(path)
|
|
40
|
+
end
|
|
41
|
+
|
|
13
42
|
def run_cmd(cmd, err: IO::NULL)
|
|
14
43
|
output = IO.popen(cmd, err: err, &:read)
|
|
15
44
|
[output, $?.success?]
|
data/lib/sight/version.rb
CHANGED
data/lib/sight.rb
CHANGED
|
@@ -6,6 +6,8 @@ require_relative "sight/diff_parser"
|
|
|
6
6
|
require_relative "sight/git"
|
|
7
7
|
require_relative "sight/annotation"
|
|
8
8
|
require_relative "sight/annotation_formatter"
|
|
9
|
+
require_relative "sight/claude_hook_installer"
|
|
10
|
+
require_relative "sight/cursor_hook_installer"
|
|
9
11
|
require_relative "sight/cli"
|
|
10
12
|
require_relative "sight/app"
|
|
11
13
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sight
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ariel Rzezak
|
|
@@ -43,7 +43,9 @@ files:
|
|
|
43
43
|
- lib/sight/annotation.rb
|
|
44
44
|
- lib/sight/annotation_formatter.rb
|
|
45
45
|
- lib/sight/app.rb
|
|
46
|
+
- lib/sight/claude_hook_installer.rb
|
|
46
47
|
- lib/sight/cli.rb
|
|
48
|
+
- lib/sight/cursor_hook_installer.rb
|
|
47
49
|
- lib/sight/diff_parser.rb
|
|
48
50
|
- lib/sight/display_line.rb
|
|
49
51
|
- lib/sight/git.rb
|