slk 0.2.0 → 0.4.1
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 +62 -1
- data/README.md +30 -12
- data/bin/ci +15 -0
- data/bin/coverage +225 -0
- data/bin/test +7 -0
- data/lib/slk/api/saved.rb +26 -0
- data/lib/slk/api/search.rb +31 -0
- data/lib/slk/cli.rb +61 -3
- data/lib/slk/commands/activity.rb +7 -13
- data/lib/slk/commands/base.rb +3 -1
- data/lib/slk/commands/catchup.rb +3 -2
- data/lib/slk/commands/config.rb +48 -45
- data/lib/slk/commands/help.rb +3 -0
- data/lib/slk/commands/later.rb +297 -0
- data/lib/slk/commands/messages.rb +63 -14
- data/lib/slk/commands/search.rb +223 -0
- data/lib/slk/commands/ssh_key_manager.rb +129 -0
- data/lib/slk/formatters/activity_formatter.rb +4 -7
- data/lib/slk/formatters/attachment_formatter.rb +16 -2
- data/lib/slk/formatters/markdown_output.rb +98 -0
- data/lib/slk/formatters/mention_replacer.rb +13 -31
- data/lib/slk/formatters/message_formatter.rb +18 -33
- data/lib/slk/formatters/reaction_formatter.rb +1 -1
- data/lib/slk/formatters/saved_item_formatter.rb +144 -0
- data/lib/slk/formatters/search_formatter.rb +71 -0
- data/lib/slk/formatters/text_processor.rb +48 -0
- data/lib/slk/models/saved_item.rb +128 -0
- data/lib/slk/models/search_result.rb +115 -0
- data/lib/slk/runner.rb +32 -0
- data/lib/slk/services/api_client.rb +60 -11
- data/lib/slk/services/cache_store.rb +55 -36
- data/lib/slk/services/encryption.rb +114 -11
- data/lib/slk/services/message_resolver.rb +38 -0
- data/lib/slk/services/setup_wizard.rb +3 -3
- data/lib/slk/services/target_resolver.rb +27 -4
- data/lib/slk/services/token_loader.rb +83 -0
- data/lib/slk/services/token_saver.rb +87 -0
- data/lib/slk/services/token_store.rb +35 -65
- data/lib/slk/services/user_lookup.rb +117 -0
- data/lib/slk/support/date_parser.rb +64 -0
- data/lib/slk/support/inline_images.rb +94 -10
- data/lib/slk/support/platform.rb +34 -0
- data/lib/slk/support/xdg_paths.rb +27 -9
- data/lib/slk/version.rb +1 -1
- data/lib/slk.rb +15 -0
- metadata +21 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef8284e736f39c42a65ef336e2d8db1a36f998393070433809db242cf6de0861
|
|
4
|
+
data.tar.gz: 6af8a0cdb8cbc5f35aea41e89ba0221cd0eb7ab5e4fbbf360cc60b2eb750c4d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c384f28530877909f82a596d4b306ca342989b5720f7cee47ea8157b91f59bd21202101c85e8878a83cb909a3ff954dec6dcabea8d3de2ae72c5d49e6f55882
|
|
7
|
+
data.tar.gz: 1da065c6704edfd5beaf45b2989a9fec1a183061109d5d167467943d7eecaf3b1f7c707e46ae4b517b2e28c3d1edcb23cf37d0f90fda1882408d362be59fc992
|
data/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Ghostty/Kitty terminal support** - Inline emoji images now work in Ghostty and Kitty terminals
|
|
13
|
+
- Uses Kitty graphics protocol with Unicode placeholders for proper tmux support
|
|
14
|
+
- Images clear correctly with `clear` command (no floating artifacts)
|
|
15
|
+
- Converts GIF/JPEG to PNG automatically (macOS only via `sips`)
|
|
16
|
+
|
|
17
|
+
- **`later` command** - View Slack's "Save for Later" items
|
|
18
|
+
- Lists saved messages with content preview
|
|
19
|
+
- Filter by state: `--completed`, `--in-progress`
|
|
20
|
+
- `--counts` for summary statistics (total, overdue, with due dates)
|
|
21
|
+
- `--no-content` to skip fetching message text
|
|
22
|
+
- `--workspace-emoji` for inline custom emoji images
|
|
23
|
+
- `--width N` to wrap text at N columns
|
|
24
|
+
- `--no-wrap` to truncate messages to single line
|
|
25
|
+
- `--json` output includes message content
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- New `TextProcessor` service centralizes text processing (HTML decode, mentions, emoji)
|
|
30
|
+
- New `MessageResolver` service extracted from activity command for reuse
|
|
31
|
+
- Refactored formatters to use shared TextProcessor
|
|
32
|
+
|
|
33
|
+
## [0.4.0] - 2026-01-30
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **Windows Support** - slk now runs on Windows
|
|
38
|
+
- Uses `%APPDATA%` and `%LOCALAPPDATA%` for config/cache directories
|
|
39
|
+
- Cross-platform command detection with `Open3.capture3`
|
|
40
|
+
- Proper NTFS permission handling (skips `chmod` on Windows)
|
|
41
|
+
- New `Support::Platform` module for OS-specific behavior
|
|
42
|
+
- CI testing on Windows (Ruby 3.2, 3.3, 3.4, 4.0)
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- New `UserLookup` service consolidates duplicate user name resolution logic
|
|
47
|
+
- Removed ~65 lines of duplicated code from `MentionReplacer` and `MessageFormatter`
|
|
48
|
+
|
|
49
|
+
## [0.3.0] - 2026-01-16
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- `-vv`/`--very-verbose` flag for detailed API debugging with timing and response bodies
|
|
54
|
+
- SSH key validation and token migration when keys change
|
|
55
|
+
- Public key validation (ensures it matches private key)
|
|
56
|
+
- `config unset` command for removing configuration values
|
|
57
|
+
- CI infrastructure with GitHub Actions (Ruby 3.2-4.0, macOS, Ubuntu)
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- Improved error handling throughout with comprehensive tests
|
|
62
|
+
- Better SSH key error messages with public key prompting
|
|
63
|
+
- Cache user lookups to reduce API calls
|
|
64
|
+
- Improved rate limit error messages
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- Test output no longer leaks to stdout
|
|
69
|
+
- All rubocop offenses resolved
|
|
11
70
|
|
|
12
71
|
## [0.2.0] - 2025-01-15
|
|
13
72
|
|
|
@@ -63,5 +122,7 @@ Initial release of the Ruby rewrite. Pure Ruby, no external dependencies.
|
|
|
63
122
|
- Pure Ruby stdlib - no gem dependencies
|
|
64
123
|
- Ruby 3.2+ with modern features (Data.define, pattern matching)
|
|
65
124
|
|
|
125
|
+
[0.4.0]: https://github.com/ericboehs/slk/releases/tag/v0.4.0
|
|
126
|
+
[0.3.0]: https://github.com/ericboehs/slk/releases/tag/v0.3.0
|
|
66
127
|
[0.2.0]: https://github.com/ericboehs/slk/releases/tag/v0.2.0
|
|
67
128
|
[0.1.0]: https://github.com/ericboehs/slk/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -12,6 +12,22 @@ gem install slk
|
|
|
12
12
|
|
|
13
13
|
Requires Ruby 3.2+.
|
|
14
14
|
|
|
15
|
+
### Windows
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
# Install Ruby (if needed) via RubyInstaller or Chocolatey
|
|
19
|
+
winget install RubyInstallerTeam.Ruby.3.3
|
|
20
|
+
# or: choco install ruby
|
|
21
|
+
|
|
22
|
+
# Install slk
|
|
23
|
+
gem install slk
|
|
24
|
+
|
|
25
|
+
# (Optional) Install age for encrypted token storage
|
|
26
|
+
choco install age.portable
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Configuration is stored in `%APPDATA%\slk\` on Windows.
|
|
30
|
+
|
|
15
31
|
## Setup
|
|
16
32
|
|
|
17
33
|
Run the setup wizard:
|
|
@@ -56,10 +72,10 @@ slk dnd off # Disable DND
|
|
|
56
72
|
### Messages
|
|
57
73
|
|
|
58
74
|
```bash
|
|
59
|
-
slk messages
|
|
75
|
+
slk messages general # Read channel messages
|
|
60
76
|
slk messages @username # Read DM with user
|
|
61
|
-
slk messages
|
|
62
|
-
slk messages
|
|
77
|
+
slk messages general -n 50 # Show 50 messages
|
|
78
|
+
slk messages general --json # Output as JSON
|
|
63
79
|
```
|
|
64
80
|
|
|
65
81
|
### Activity
|
|
@@ -86,7 +102,7 @@ Use `--show-messages` (or `-m`) to preview the actual message content for each a
|
|
|
86
102
|
```bash
|
|
87
103
|
slk unread # Show unread counts
|
|
88
104
|
slk unread clear # Mark all as read
|
|
89
|
-
slk unread clear
|
|
105
|
+
slk unread clear general # Mark channel as read
|
|
90
106
|
```
|
|
91
107
|
|
|
92
108
|
### Catchup (Interactive Triage)
|
|
@@ -155,13 +171,13 @@ Tokens will be stored encrypted in `~/.config/slk/tokens.age`.
|
|
|
155
171
|
|
|
156
172
|
## Configuration
|
|
157
173
|
|
|
158
|
-
Files are stored in XDG-compliant locations:
|
|
174
|
+
Files are stored in XDG-compliant locations (or `%APPDATA%`/`%LOCALAPPDATA%` on Windows):
|
|
159
175
|
|
|
160
|
-
- **Config**: `~/.config/slk/`
|
|
176
|
+
- **Config**: `~/.config/slk/` (Windows: `%APPDATA%\slk\`)
|
|
161
177
|
- `config.json` - Settings
|
|
162
178
|
- `tokens.json` or `tokens.age` - Workspace tokens
|
|
163
179
|
- `presets.json` - Status presets
|
|
164
|
-
- **Cache**: `~/.cache/slk/`
|
|
180
|
+
- **Cache**: `~/.cache/slk/` (Windows: `%LOCALAPPDATA%\slk\`)
|
|
165
181
|
- `users-{workspace}.json` - User cache
|
|
166
182
|
- `channels-{workspace}.json` - Channel cache
|
|
167
183
|
|
|
@@ -177,13 +193,15 @@ ruby -Ilib bin/slk --version
|
|
|
177
193
|
|
|
178
194
|
# Run tests
|
|
179
195
|
rake test
|
|
196
|
+
```
|
|
180
197
|
|
|
181
|
-
|
|
182
|
-
gem build slk.gemspec
|
|
198
|
+
### Releasing
|
|
183
199
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
200
|
+
1. Update version in `lib/slk/version.rb`
|
|
201
|
+
2. Update `CHANGELOG.md` (move Unreleased to new version, add date)
|
|
202
|
+
3. Commit: `git commit -am "Release vX.Y.Z"`
|
|
203
|
+
4. Release to RubyGems: `rake release`
|
|
204
|
+
5. Create GitHub Release: `gh release create vX.Y.Z --generate-notes`
|
|
187
205
|
|
|
188
206
|
## License
|
|
189
207
|
|
data/bin/ci
ADDED
data/bin/coverage
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'bundler/inline'
|
|
5
|
+
|
|
6
|
+
gemfile do
|
|
7
|
+
source 'https://rubygems.org'
|
|
8
|
+
gem 'nokogiri'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Path to the SimpleCov HTML report
|
|
12
|
+
coverage_file = File.join(Dir.pwd, 'coverage', 'index.html')
|
|
13
|
+
|
|
14
|
+
unless File.exist?(coverage_file)
|
|
15
|
+
puts "No coverage report found at #{coverage_file}"
|
|
16
|
+
puts 'Run tests first: COVERAGE=1 bundle exec rake test'
|
|
17
|
+
exit 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Parse the HTML file
|
|
21
|
+
doc = Nokogiri::HTML(File.read(coverage_file))
|
|
22
|
+
|
|
23
|
+
# Extract overall line coverage
|
|
24
|
+
line_percent = doc.css('.covered_percent span').first&.text&.strip
|
|
25
|
+
total_lines = doc.css('.t-line-summary b').first&.text&.strip
|
|
26
|
+
covered_lines = doc.css('.t-line-summary .green b').first&.text&.strip
|
|
27
|
+
missed_lines = doc.css('.t-line-summary .red b').first&.text&.strip
|
|
28
|
+
|
|
29
|
+
# Extract overall branch coverage (first t-branch-summary is the overall stats)
|
|
30
|
+
overall_branch_summary = doc.css('.t-branch-summary').first
|
|
31
|
+
if overall_branch_summary
|
|
32
|
+
branch_percent = overall_branch_summary.css('span').last.text.strip.gsub(/[()]/, '')
|
|
33
|
+
branch_summary_spans = overall_branch_summary.css('span b')
|
|
34
|
+
overall_total_branches = branch_summary_spans[0]&.text&.strip
|
|
35
|
+
overall_covered_branches = branch_summary_spans[1]&.text&.strip
|
|
36
|
+
overall_missed_branches = branch_summary_spans[2]&.text&.strip
|
|
37
|
+
else
|
|
38
|
+
# No branch coverage available
|
|
39
|
+
branch_percent = nil
|
|
40
|
+
overall_total_branches = nil
|
|
41
|
+
overall_covered_branches = nil
|
|
42
|
+
overall_missed_branches = nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Extract timestamp
|
|
46
|
+
timestamp = doc.css('.timestamp .timeago').first&.attr('title')
|
|
47
|
+
|
|
48
|
+
puts '## SimpleCov Coverage Report'
|
|
49
|
+
puts "Generated: #{timestamp}"
|
|
50
|
+
puts ''
|
|
51
|
+
|
|
52
|
+
puts "### Line Coverage: #{line_percent}"
|
|
53
|
+
puts " #{covered_lines}/#{total_lines} lines covered"
|
|
54
|
+
puts " #{missed_lines} lines missed"
|
|
55
|
+
puts ''
|
|
56
|
+
|
|
57
|
+
if branch_percent
|
|
58
|
+
puts "### Branch Coverage: #{branch_percent}"
|
|
59
|
+
puts " #{overall_covered_branches}/#{overall_total_branches} branches covered"
|
|
60
|
+
puts " #{overall_missed_branches} branches missed"
|
|
61
|
+
puts ''
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Show file-by-file breakdown if there are missed lines or branches
|
|
65
|
+
if missed_lines.to_i.positive? || overall_missed_branches&.to_i&.positive?
|
|
66
|
+
# First, collect all files with missing coverage
|
|
67
|
+
files_with_issues = []
|
|
68
|
+
files_shown = Set.new
|
|
69
|
+
|
|
70
|
+
doc.css('tbody .t-file').each do |row|
|
|
71
|
+
file_name = row.css('.t-file__name a').first&.text&.strip
|
|
72
|
+
line_coverage = row.css('.t-file__coverage').first&.text&.strip
|
|
73
|
+
branch_coverage = row.css('.t-file__branch-coverage').first&.text&.strip
|
|
74
|
+
|
|
75
|
+
# Only include files that aren't 100% covered and haven't been seen yet
|
|
76
|
+
should_skip = files_shown.include?(file_name) ||
|
|
77
|
+
(line_coverage == '100.00 %' && (!branch_coverage || branch_coverage == '100.00 %'))
|
|
78
|
+
next if should_skip
|
|
79
|
+
|
|
80
|
+
files_shown.add(file_name)
|
|
81
|
+
files_with_issues << row
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
total_files_with_issues = files_with_issues.length
|
|
85
|
+
files_to_show = files_with_issues.take(50)
|
|
86
|
+
|
|
87
|
+
puts '### Files with missing coverage:'
|
|
88
|
+
puts ''
|
|
89
|
+
if total_files_with_issues > 50
|
|
90
|
+
puts "Showing top 50 of #{total_files_with_issues} files with missing coverage:"
|
|
91
|
+
puts ''
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# rubocop:disable Metrics/BlockLength
|
|
95
|
+
files_to_show.each do |row|
|
|
96
|
+
file_name = row.css('.t-file__name a').first&.text&.strip
|
|
97
|
+
line_coverage = row.css('.t-file__coverage').first&.text&.strip
|
|
98
|
+
branch_coverage = row.css('.t-file__branch-coverage').first&.text&.strip
|
|
99
|
+
file_link = row.css('.t-file__name a').first&.attr('href')
|
|
100
|
+
|
|
101
|
+
# Extract detailed line information for this file
|
|
102
|
+
missed_lines = []
|
|
103
|
+
missed_branches = []
|
|
104
|
+
total_file_lines = 0
|
|
105
|
+
covered_file_lines = 0
|
|
106
|
+
|
|
107
|
+
if file_link
|
|
108
|
+
file_id = file_link.gsub('#', '')
|
|
109
|
+
file_section = doc.css("##{file_id}")
|
|
110
|
+
|
|
111
|
+
if file_section.any?
|
|
112
|
+
# Get the actual counts from SimpleCov's summary
|
|
113
|
+
line_summary = file_section.css('.t-line-summary')
|
|
114
|
+
if line_summary.any? # rubocop:disable Metrics/BlockNesting
|
|
115
|
+
summary_text = line_summary.text
|
|
116
|
+
# Extract numbers from text like "13 relevant lines. 12 lines covered and 1 lines missed."
|
|
117
|
+
total_file_lines = Regexp.last_match(1).to_i if summary_text.match(/(\d+)\s+relevant\s+lines/) # rubocop:disable Metrics/BlockNesting
|
|
118
|
+
covered_file_lines = Regexp.last_match(1).to_i if summary_text.match(/(\d+)\s+lines\s+covered/) # rubocop:disable Metrics/BlockNesting
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Find missed lines and branches
|
|
122
|
+
file_section.css('li').each do |line_item|
|
|
123
|
+
line_number = line_item.attr('data-linenumber')
|
|
124
|
+
line_class = line_item.attr('class')
|
|
125
|
+
|
|
126
|
+
if line_class&.include?('missed') && !line_class.include?('missed-branch') # rubocop:disable Metrics/BlockNesting
|
|
127
|
+
missed_lines << line_number
|
|
128
|
+
elsif line_class&.include?('missed-branch') # rubocop:disable Metrics/BlockNesting
|
|
129
|
+
missed_branches << line_number
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Format the line ranges more clearly
|
|
136
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
137
|
+
def format_line_ranges(lines)
|
|
138
|
+
return '' if lines.empty?
|
|
139
|
+
|
|
140
|
+
ranges = []
|
|
141
|
+
current_range = [lines.first.to_i]
|
|
142
|
+
|
|
143
|
+
lines.map(&:to_i).sort[1..]&.each do |line|
|
|
144
|
+
if line == current_range.last + 1
|
|
145
|
+
current_range << line
|
|
146
|
+
else
|
|
147
|
+
ranges << format_range(current_range)
|
|
148
|
+
current_range = [line]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
ranges << format_range(current_range)
|
|
152
|
+
|
|
153
|
+
"L#{ranges.join(', L')}"
|
|
154
|
+
end
|
|
155
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
156
|
+
|
|
157
|
+
def format_range(range)
|
|
158
|
+
if range.length == 1
|
|
159
|
+
range.first.to_s
|
|
160
|
+
else
|
|
161
|
+
"#{range.first}-#{range.last}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Get branch counts from the file section
|
|
166
|
+
covered_branches = 0
|
|
167
|
+
total_branches = 0
|
|
168
|
+
|
|
169
|
+
if file_link
|
|
170
|
+
file_id = file_link.gsub('#', '')
|
|
171
|
+
file_section = doc.css("##{file_id}")
|
|
172
|
+
branch_summary = file_section.css('.t-branch-summary')
|
|
173
|
+
|
|
174
|
+
if branch_summary.any?
|
|
175
|
+
branch_spans = branch_summary.css('span b')
|
|
176
|
+
total_branches = branch_spans[0]&.text.to_i
|
|
177
|
+
covered_branches = branch_spans[1]&.text.to_i
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
branch_display = branch_coverage ? "Branch: #{branch_coverage}" : 'Branch: N/A'
|
|
182
|
+
puts " #{file_name} (Line: #{line_coverage}, #{branch_display}):"
|
|
183
|
+
|
|
184
|
+
line_info = "Lines: #{covered_file_lines}/#{total_file_lines}"
|
|
185
|
+
line_info += " (missed: #{format_line_ranges(missed_lines)})" unless missed_lines.empty?
|
|
186
|
+
puts " #{line_info}"
|
|
187
|
+
|
|
188
|
+
if total_branches.positive?
|
|
189
|
+
branch_info = "Branches: #{covered_branches}/#{total_branches}"
|
|
190
|
+
branch_info += " (missed: #{format_line_ranges(missed_branches)})" unless missed_branches.empty?
|
|
191
|
+
puts " #{branch_info}"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
puts ''
|
|
195
|
+
end
|
|
196
|
+
# rubocop:enable Metrics/BlockLength
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Write to GitHub Actions job summary if available
|
|
200
|
+
if ENV['GITHUB_STEP_SUMMARY']
|
|
201
|
+
File.open(ENV['GITHUB_STEP_SUMMARY'], 'a') do |f|
|
|
202
|
+
f.puts ''
|
|
203
|
+
f.puts '## Test Coverage Report'
|
|
204
|
+
f.puts ''
|
|
205
|
+
f.puts "**Line Coverage:** #{line_percent} (#{covered_lines}/#{total_lines} lines)"
|
|
206
|
+
f.puts ''
|
|
207
|
+
|
|
208
|
+
if branch_percent
|
|
209
|
+
f.puts "**Branch Coverage:** #{branch_percent} (#{overall_covered_branches}/#{overall_total_branches} branches)"
|
|
210
|
+
f.puts ''
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Add visual indicators
|
|
214
|
+
line_pct_value = line_percent.gsub('%', '').to_f
|
|
215
|
+
branch_pct_value = branch_percent&.gsub('%', '')&.to_f
|
|
216
|
+
|
|
217
|
+
if line_pct_value >= 95 && (!branch_pct_value || branch_pct_value >= 95)
|
|
218
|
+
f.puts 'Coverage meets quality thresholds'
|
|
219
|
+
elsif line_pct_value >= 80 && (!branch_pct_value || branch_pct_value >= 80)
|
|
220
|
+
f.puts 'Coverage is acceptable but could be improved'
|
|
221
|
+
else
|
|
222
|
+
f.puts 'Coverage is below recommended thresholds'
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
data/bin/test
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Api
|
|
5
|
+
# Thin wrapper for the Slack saved.list API endpoint
|
|
6
|
+
# Used to fetch "Save for Later" items
|
|
7
|
+
class Saved
|
|
8
|
+
def initialize(api_client, workspace)
|
|
9
|
+
@api = api_client
|
|
10
|
+
@workspace = workspace
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# List saved items
|
|
14
|
+
# @param filter [String] Filter type: 'saved', 'in_progress', 'completed', 'archived'
|
|
15
|
+
# @param limit [Integer] Number of items to return (default: 15)
|
|
16
|
+
# @param cursor [String, nil] Pagination cursor
|
|
17
|
+
# @return [Hash] API response with 'ok' and 'saved_items' keys
|
|
18
|
+
# @raise [ApiError] if the API call fails (network error, auth error, etc.)
|
|
19
|
+
def list(filter: 'saved', limit: 15, cursor: nil)
|
|
20
|
+
params = { filter: filter, limit: limit.to_s }
|
|
21
|
+
params[:cursor] = cursor if cursor
|
|
22
|
+
@api.post_form(@workspace, 'saved.list', params)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Api
|
|
5
|
+
# Wrapper for Slack search.* API endpoints
|
|
6
|
+
# Note: search.messages requires user tokens (xoxc/xoxs), NOT bot tokens (xoxb)
|
|
7
|
+
class Search
|
|
8
|
+
def initialize(api_client, workspace)
|
|
9
|
+
@api = api_client
|
|
10
|
+
@workspace = workspace
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Search for messages across channels and DMs
|
|
14
|
+
# @param query [String] Search query with optional modifiers (in:, from:, before:, after:, etc.)
|
|
15
|
+
# @param count [Integer] Number of results per page (max 100)
|
|
16
|
+
# @param page [Integer] Page number (1-indexed)
|
|
17
|
+
# @param sort [String] Sort field: 'score' or 'timestamp'
|
|
18
|
+
# @param sort_dir [String] Sort direction: 'asc' or 'desc'
|
|
19
|
+
# @return [Hash] API response with messages.matches array
|
|
20
|
+
def messages(query:, count: 20, page: 1, sort: 'timestamp', sort_dir: 'desc')
|
|
21
|
+
@api.get(@workspace, 'search.messages', {
|
|
22
|
+
query: query,
|
|
23
|
+
count: [count, 100].min,
|
|
24
|
+
page: page,
|
|
25
|
+
sort: sort,
|
|
26
|
+
sort_dir: sort_dir
|
|
27
|
+
})
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/slk/cli.rb
CHANGED
|
@@ -13,6 +13,8 @@ module Slk
|
|
|
13
13
|
'unread' => Commands::Unread,
|
|
14
14
|
'catchup' => Commands::Catchup,
|
|
15
15
|
'activity' => Commands::Activity,
|
|
16
|
+
'later' => Commands::Later,
|
|
17
|
+
'search' => Commands::Search,
|
|
16
18
|
'preset' => Commands::Preset,
|
|
17
19
|
'workspaces' => Commands::Workspaces,
|
|
18
20
|
'cache' => Commands::Cache,
|
|
@@ -24,6 +26,7 @@ module Slk
|
|
|
24
26
|
def initialize(argv, output: nil)
|
|
25
27
|
@argv = argv.dup
|
|
26
28
|
@output = output || Formatters::Output.new
|
|
29
|
+
@output_injected = !output.nil?
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
def run
|
|
@@ -115,19 +118,73 @@ module Slk
|
|
|
115
118
|
end
|
|
116
119
|
|
|
117
120
|
def build_runner(args)
|
|
118
|
-
verbose =
|
|
119
|
-
|
|
121
|
+
verbose = verbose_mode?(args)
|
|
122
|
+
very_verbose = args.include?('-vv') || args.include?('--very-verbose')
|
|
123
|
+
markdown = args.include?('--markdown')
|
|
124
|
+
output = @output_injected ? @output : build_output(verbose: verbose, markdown: markdown)
|
|
120
125
|
runner = Runner.new(output: output)
|
|
121
126
|
setup_verbose_logging(runner, output) if verbose
|
|
127
|
+
setup_very_verbose_logging(runner, output) if very_verbose
|
|
122
128
|
runner
|
|
123
129
|
end
|
|
124
130
|
|
|
131
|
+
def build_output(verbose:, markdown:)
|
|
132
|
+
if markdown
|
|
133
|
+
Formatters::MarkdownOutput.new(verbose: verbose)
|
|
134
|
+
else
|
|
135
|
+
Formatters::Output.new(verbose: verbose)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
125
139
|
def setup_verbose_logging(runner, output)
|
|
126
140
|
runner.api_client.on_request = lambda { |method, count|
|
|
127
141
|
output.debug("[API ##{count}] #{method}")
|
|
128
142
|
}
|
|
129
143
|
end
|
|
130
144
|
|
|
145
|
+
def setup_very_verbose_logging(runner, output)
|
|
146
|
+
setup_response_header_logging(runner, output)
|
|
147
|
+
setup_request_body_logging(runner, output)
|
|
148
|
+
setup_response_body_logging(runner, output)
|
|
149
|
+
setup_cache_logging(runner, output)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def setup_response_header_logging(runner, output)
|
|
153
|
+
runner.api_client.on_response = lambda { |method, code, headers|
|
|
154
|
+
next if headers.empty?
|
|
155
|
+
|
|
156
|
+
elapsed = headers.delete('elapsed_ms')
|
|
157
|
+
req_id = headers.delete('X-Slack-Req-Id')
|
|
158
|
+
rate_parts = headers.map { |k, v| "#{k.sub('X-RateLimit-', '')}=#{v}" }
|
|
159
|
+
|
|
160
|
+
parts = ["#{elapsed}ms"]
|
|
161
|
+
parts << "req=#{req_id}" if req_id
|
|
162
|
+
parts.concat(rate_parts)
|
|
163
|
+
output.debug(" #{method} #{code}: #{parts.join(', ')}")
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def setup_request_body_logging(runner, output)
|
|
168
|
+
runner.api_client.on_request_body = lambda { |method, body|
|
|
169
|
+
truncated = body.length > 500 ? "#{body[0..500]}..." : body
|
|
170
|
+
output.debug(" #{method} request: #{truncated}")
|
|
171
|
+
}
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def setup_response_body_logging(runner, output)
|
|
175
|
+
runner.api_client.on_response_body = lambda { |method, body|
|
|
176
|
+
truncated = body.length > 500 ? "#{body[0..500]}..." : body
|
|
177
|
+
output.debug(" #{method} response: #{truncated}")
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def setup_cache_logging(runner, output)
|
|
182
|
+
runner.cache_store.on_cache_access = lambda { |type, _workspace, key, hit, value|
|
|
183
|
+
status = hit ? "HIT (#{value})" : 'MISS'
|
|
184
|
+
output.debug(" [Cache] #{type} #{key}: #{status}")
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
131
188
|
def execute_command(command_class, args, runner)
|
|
132
189
|
command = command_class.new(args, runner: runner)
|
|
133
190
|
result = command.execute
|
|
@@ -136,7 +193,8 @@ module Slk
|
|
|
136
193
|
end
|
|
137
194
|
|
|
138
195
|
def verbose_mode?(args)
|
|
139
|
-
args.include?('-v') || args.include?('--verbose')
|
|
196
|
+
args.include?('-v') || args.include?('--verbose') ||
|
|
197
|
+
args.include?('-vv') || args.include?('--very-verbose')
|
|
140
198
|
end
|
|
141
199
|
|
|
142
200
|
def log_api_call_count(runner)
|
|
@@ -118,7 +118,7 @@ module Slk
|
|
|
118
118
|
output: output,
|
|
119
119
|
enricher: enricher(workspace),
|
|
120
120
|
emoji_replacer: runner.emoji_replacer,
|
|
121
|
-
|
|
121
|
+
text_processor: runner.text_processor,
|
|
122
122
|
on_debug: ->(msg) { debug(msg) }
|
|
123
123
|
)
|
|
124
124
|
end
|
|
@@ -131,20 +131,14 @@ module Slk
|
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
def fetch_message(workspace, channel_id, message_ts)
|
|
134
|
-
|
|
135
|
-
return nil unless response['ok'] && response['messages']&.any?
|
|
136
|
-
|
|
137
|
-
response['messages'].find { |msg| msg['ts'] == message_ts }
|
|
138
|
-
rescue ApiError => e
|
|
139
|
-
debug("Could not fetch message #{message_ts} from #{channel_id}: #{e.message}")
|
|
140
|
-
nil
|
|
134
|
+
message_resolver(workspace).fetch_by_ts(channel_id, message_ts)
|
|
141
135
|
end
|
|
142
136
|
|
|
143
|
-
def
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
137
|
+
def message_resolver(workspace)
|
|
138
|
+
Services::MessageResolver.new(
|
|
139
|
+
conversations_api: runner.conversations_api(workspace.name),
|
|
140
|
+
on_debug: ->(msg) { debug(msg) }
|
|
141
|
+
)
|
|
148
142
|
end
|
|
149
143
|
end
|
|
150
144
|
# rubocop:enable Metrics/ClassLength
|
data/lib/slk/commands/base.rb
CHANGED
|
@@ -32,7 +32,7 @@ module Slk
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def base_options
|
|
35
|
-
{ workspace: nil, all: false, verbose: false, quiet: false, json: false, width: default_width }
|
|
35
|
+
{ workspace: nil, all: false, verbose: false, quiet: false, json: false, markdown: false, width: default_width }
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def formatting_options
|
|
@@ -69,8 +69,10 @@ module Slk
|
|
|
69
69
|
when '--no-wrap' then @options[:width] = nil
|
|
70
70
|
when '--all' then @options[:all] = true
|
|
71
71
|
when '-v', '--verbose' then @options[:verbose] = true
|
|
72
|
+
when '-vv', '--very-verbose' then @options[:verbose] = @options[:very_verbose] = true
|
|
72
73
|
when '-q', '--quiet' then @options[:quiet] = true
|
|
73
74
|
when '--json' then @options[:json] = true
|
|
75
|
+
when '--markdown' then @options[:markdown] = true
|
|
74
76
|
when '-h', '--help' then @options[:help] = true
|
|
75
77
|
when '--no-emoji' then @options[:no_emoji] = true
|
|
76
78
|
when '--no-reactions' then @options[:no_reactions] = true
|
data/lib/slk/commands/catchup.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../support/help_formatter'
|
|
4
|
+
require_relative '../support/platform'
|
|
4
5
|
|
|
5
6
|
module Slk
|
|
6
7
|
module Commands
|
|
@@ -293,7 +294,7 @@ module Slk
|
|
|
293
294
|
|
|
294
295
|
def open_channel_in_slack(workspace, channel_id)
|
|
295
296
|
team_id = runner.client_api(workspace.name).team_id
|
|
296
|
-
|
|
297
|
+
Support::Platform.open_url("slack://channel?team=#{team_id}&id=#{channel_id}")
|
|
297
298
|
success('Opened in Slack')
|
|
298
299
|
:next
|
|
299
300
|
end
|
|
@@ -402,7 +403,7 @@ module Slk
|
|
|
402
403
|
first = thread_mark_data.first
|
|
403
404
|
team_id = runner.client_api(workspace.name).team_id
|
|
404
405
|
url = "slack://channel?team=#{team_id}&id=#{first[:channel]}&thread_ts=#{first[:thread_ts]}"
|
|
405
|
-
|
|
406
|
+
Support::Platform.open_url(url)
|
|
406
407
|
success('Opened in Slack')
|
|
407
408
|
end
|
|
408
409
|
end
|