slk 0.1.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 +60 -1
- data/README.md +33 -15
- data/bin/ci +15 -0
- data/bin/coverage +225 -0
- data/bin/slk +3 -3
- data/bin/test +7 -0
- data/lib/{slack_cli → slk}/api/activity.rb +10 -11
- data/lib/{slack_cli → slk}/api/bots.rb +5 -4
- data/lib/slk/api/client.rb +51 -0
- data/lib/{slack_cli → slk}/api/conversations.rb +14 -13
- data/lib/slk/api/dnd.rb +41 -0
- data/lib/{slack_cli → slk}/api/emoji.rb +4 -3
- data/lib/slk/api/search.rb +31 -0
- data/lib/{slack_cli → slk}/api/threads.rb +13 -12
- data/lib/{slack_cli → slk}/api/usergroups.rb +2 -1
- data/lib/slk/api/users.rb +105 -0
- data/lib/slk/cli.rb +204 -0
- data/lib/slk/commands/activity.rb +152 -0
- data/lib/{slack_cli → slk}/commands/base.rb +68 -41
- data/lib/slk/commands/cache.rb +141 -0
- data/lib/slk/commands/catchup.rb +412 -0
- data/lib/slk/commands/config.rb +117 -0
- data/lib/slk/commands/dnd.rb +172 -0
- data/lib/slk/commands/emoji.rb +352 -0
- data/lib/slk/commands/help.rb +98 -0
- data/lib/slk/commands/messages.rb +347 -0
- data/lib/slk/commands/presence.rb +109 -0
- data/lib/slk/commands/preset.rb +231 -0
- data/lib/slk/commands/search.rb +223 -0
- data/lib/slk/commands/ssh_key_manager.rb +129 -0
- data/lib/slk/commands/status.rb +223 -0
- data/lib/slk/commands/thread.rb +72 -0
- data/lib/slk/commands/unread.rb +305 -0
- data/lib/slk/commands/workspaces.rb +168 -0
- data/lib/slk/formatters/activity_formatter.rb +148 -0
- data/lib/slk/formatters/attachment_formatter.rb +79 -0
- data/lib/slk/formatters/block_formatter.rb +57 -0
- data/lib/{slack_cli → slk}/formatters/duration_formatter.rb +6 -5
- data/lib/slk/formatters/emoji_replacer.rb +141 -0
- data/lib/slk/formatters/json_message_formatter.rb +95 -0
- data/lib/slk/formatters/mention_replacer.rb +140 -0
- data/lib/slk/formatters/message_formatter.rb +167 -0
- data/lib/{slack_cli → slk}/formatters/output.rb +7 -6
- data/lib/slk/formatters/reaction_formatter.rb +87 -0
- data/lib/slk/formatters/search_formatter.rb +75 -0
- data/lib/{slack_cli → slk}/models/channel.rb +12 -10
- data/lib/slk/models/duration.rb +94 -0
- data/lib/slk/models/message.rb +242 -0
- data/lib/slk/models/preset.rb +78 -0
- data/lib/{slack_cli → slk}/models/reaction.rb +6 -6
- data/lib/slk/models/search_result.rb +115 -0
- data/lib/{slack_cli → slk}/models/status.rb +6 -6
- data/lib/slk/models/user.rb +55 -0
- data/lib/slk/models/workspace.rb +54 -0
- data/lib/{slack_cli → slk}/runner.rb +34 -19
- data/lib/slk/services/activity_enricher.rb +124 -0
- data/lib/slk/services/api_client.rb +194 -0
- data/lib/{slack_cli → slk}/services/cache_store.rb +63 -41
- data/lib/{slack_cli → slk}/services/configuration.rb +9 -8
- data/lib/slk/services/emoji_downloader.rb +103 -0
- data/lib/slk/services/emoji_searcher.rb +72 -0
- data/lib/slk/services/encryption.rb +151 -0
- data/lib/slk/services/gemoji_sync.rb +97 -0
- data/lib/{slack_cli → slk}/services/preset_store.rb +34 -33
- data/lib/slk/services/reaction_enricher.rb +82 -0
- data/lib/slk/services/setup_wizard.rb +131 -0
- data/lib/slk/services/target_resolver.rb +131 -0
- 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 +88 -0
- data/lib/slk/services/unread_marker.rb +101 -0
- data/lib/slk/services/user_lookup.rb +117 -0
- data/lib/slk/support/date_parser.rb +64 -0
- data/lib/{slack_cli → slk}/support/error_logger.rb +2 -1
- data/lib/{slack_cli → slk}/support/help_formatter.rb +36 -44
- data/lib/{slack_cli → slk}/support/inline_images.rb +28 -19
- data/lib/slk/support/interactive_prompt.rb +29 -0
- data/lib/slk/support/platform.rb +34 -0
- data/lib/{slack_cli → slk}/support/slack_url_parser.rb +15 -17
- data/lib/slk/support/text_wrapper.rb +57 -0
- data/lib/slk/support/user_resolver.rb +141 -0
- data/lib/slk/support/xdg_paths.rb +56 -0
- data/lib/slk/version.rb +5 -0
- data/lib/slk.rb +120 -0
- metadata +93 -59
- data/lib/slack_cli/api/client.rb +0 -49
- data/lib/slack_cli/api/dnd.rb +0 -40
- data/lib/slack_cli/api/users.rb +0 -101
- data/lib/slack_cli/cli.rb +0 -118
- data/lib/slack_cli/commands/activity.rb +0 -292
- data/lib/slack_cli/commands/cache.rb +0 -116
- data/lib/slack_cli/commands/catchup.rb +0 -484
- data/lib/slack_cli/commands/config.rb +0 -159
- data/lib/slack_cli/commands/dnd.rb +0 -143
- data/lib/slack_cli/commands/emoji.rb +0 -412
- data/lib/slack_cli/commands/help.rb +0 -76
- data/lib/slack_cli/commands/messages.rb +0 -317
- data/lib/slack_cli/commands/presence.rb +0 -107
- data/lib/slack_cli/commands/preset.rb +0 -239
- data/lib/slack_cli/commands/status.rb +0 -194
- data/lib/slack_cli/commands/thread.rb +0 -62
- data/lib/slack_cli/commands/unread.rb +0 -312
- data/lib/slack_cli/commands/workspaces.rb +0 -151
- data/lib/slack_cli/formatters/emoji_replacer.rb +0 -143
- data/lib/slack_cli/formatters/mention_replacer.rb +0 -154
- data/lib/slack_cli/formatters/message_formatter.rb +0 -429
- data/lib/slack_cli/models/duration.rb +0 -85
- data/lib/slack_cli/models/message.rb +0 -217
- data/lib/slack_cli/models/preset.rb +0 -73
- data/lib/slack_cli/models/user.rb +0 -56
- data/lib/slack_cli/models/workspace.rb +0 -52
- data/lib/slack_cli/services/api_client.rb +0 -149
- data/lib/slack_cli/services/encryption.rb +0 -51
- data/lib/slack_cli/services/reaction_enricher.rb +0 -87
- data/lib/slack_cli/services/token_store.rb +0 -117
- data/lib/slack_cli/support/user_resolver.rb +0 -114
- data/lib/slack_cli/support/xdg_paths.rb +0 -37
- data/lib/slack_cli/version.rb +0 -5
- data/lib/slack_cli.rb +0 -91
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd09692101d4658e57208d39e81d770e6dc36d2218088194ef55ad5fad0adbd0
|
|
4
|
+
data.tar.gz: 88e03a0be601536ffb3fd8f1b986900f8f57144b03a52ec5fa24b0b5dbdfbc57
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 399ca26d5d030704ad553e579fab75376db1578e0814f1ad8a9c2e4115c44e32c8173263fb39c8839eb7a0014ca1758d4e1aaa33b8d65353ba64e6fe83fa8efe
|
|
7
|
+
data.tar.gz: 64bfbe31c44af1961f43cd7f027bd352847af05434b957bbceb03e26c8bdf820ee805c1fb67d2adac80e18e7ddc286fd643572a17695209db1e3e7826c358baa
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,62 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.4.0] - 2026-01-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Windows Support** - slk now runs on Windows
|
|
15
|
+
- Uses `%APPDATA%` and `%LOCALAPPDATA%` for config/cache directories
|
|
16
|
+
- Cross-platform command detection with `Open3.capture3`
|
|
17
|
+
- Proper NTFS permission handling (skips `chmod` on Windows)
|
|
18
|
+
- New `Support::Platform` module for OS-specific behavior
|
|
19
|
+
- CI testing on Windows (Ruby 3.2, 3.3, 3.4, 4.0)
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- New `UserLookup` service consolidates duplicate user name resolution logic
|
|
24
|
+
- Removed ~65 lines of duplicated code from `MentionReplacer` and `MessageFormatter`
|
|
25
|
+
|
|
26
|
+
## [0.3.0] - 2026-01-16
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `-vv`/`--very-verbose` flag for detailed API debugging with timing and response bodies
|
|
31
|
+
- SSH key validation and token migration when keys change
|
|
32
|
+
- Public key validation (ensures it matches private key)
|
|
33
|
+
- `config unset` command for removing configuration values
|
|
34
|
+
- CI infrastructure with GitHub Actions (Ruby 3.2-4.0, macOS, Ubuntu)
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Improved error handling throughout with comprehensive tests
|
|
39
|
+
- Better SSH key error messages with public key prompting
|
|
40
|
+
- Cache user lookups to reduce API calls
|
|
41
|
+
- Improved rate limit error messages
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- Test output no longer leaks to stdout
|
|
46
|
+
- All rubocop offenses resolved
|
|
47
|
+
|
|
48
|
+
## [0.2.0] - 2025-01-15
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- `--workspace-emoji` flag for messages command to display custom workspace emoji as inline images (experimental, requires iTerm2/WezTerm/Mintty)
|
|
53
|
+
- JSON output now includes resolved user and channel names for `messages`, `activity`, and `unread` commands
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- Config/cache directories renamed from `slack-cli` to `slk`
|
|
58
|
+
- Repository renamed from `slack-cli` to `slk`
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- `error()` helper now returns exit code 1 for proper shell exit status
|
|
63
|
+
|
|
8
64
|
## [0.1.0] - 2025-01-14
|
|
9
65
|
|
|
10
66
|
Initial release of the Ruby rewrite. Pure Ruby, no external dependencies.
|
|
@@ -43,4 +99,7 @@ Initial release of the Ruby rewrite. Pure Ruby, no external dependencies.
|
|
|
43
99
|
- Pure Ruby stdlib - no gem dependencies
|
|
44
100
|
- Ruby 3.2+ with modern features (Data.define, pattern matching)
|
|
45
101
|
|
|
46
|
-
[0.
|
|
102
|
+
[0.4.0]: https://github.com/ericboehs/slk/releases/tag/v0.4.0
|
|
103
|
+
[0.3.0]: https://github.com/ericboehs/slk/releases/tag/v0.3.0
|
|
104
|
+
[0.2.0]: https://github.com/ericboehs/slk/releases/tag/v0.2.0
|
|
105
|
+
[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)
|
|
@@ -151,17 +167,17 @@ Optionally encrypt your tokens with [age](https://github.com/FiloSottile/age) us
|
|
|
151
167
|
slk config set ssh_key ~/.ssh/id_ed25519
|
|
152
168
|
```
|
|
153
169
|
|
|
154
|
-
Tokens will be stored encrypted in `~/.config/
|
|
170
|
+
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/
|
|
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/
|
|
180
|
+
- **Cache**: `~/.cache/slk/` (Windows: `%LOCALAPPDATA%\slk\`)
|
|
165
181
|
- `users-{workspace}.json` - User cache
|
|
166
182
|
- `channels-{workspace}.json` - Channel cache
|
|
167
183
|
|
|
@@ -169,21 +185,23 @@ Files are stored in XDG-compliant locations:
|
|
|
169
185
|
|
|
170
186
|
```bash
|
|
171
187
|
# Clone the repo
|
|
172
|
-
git clone https://github.com/ericboehs/
|
|
173
|
-
cd
|
|
188
|
+
git clone https://github.com/ericboehs/slk.git
|
|
189
|
+
cd slk
|
|
174
190
|
|
|
175
191
|
# Run from source
|
|
176
192
|
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/slk
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
$LOAD_PATH.unshift(File.expand_path(
|
|
5
|
-
require
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
5
|
+
require 'slk'
|
|
6
6
|
|
|
7
|
-
exit
|
|
7
|
+
exit Slk::CLI.new(ARGV).run
|
data/bin/test
ADDED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Slk
|
|
4
4
|
module Api
|
|
5
|
+
# Wrapper for the Slack activity.feed API endpoint
|
|
5
6
|
class Activity
|
|
6
7
|
def initialize(api_client, workspace)
|
|
7
8
|
@api = api_client
|
|
@@ -9,20 +10,18 @@ module SlackCli
|
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def feed(limit: 50, types: nil, cursor: nil, mode: 'priority_reads_and_unreads_v1')
|
|
12
|
-
params =
|
|
13
|
-
mode: mode,
|
|
14
|
-
limit: limit.to_s,
|
|
15
|
-
archive_only: 'false',
|
|
16
|
-
snooze_only: 'false',
|
|
17
|
-
unread_only: 'false',
|
|
18
|
-
priority_only: 'false',
|
|
19
|
-
is_activity_inbox: 'false'
|
|
20
|
-
}
|
|
13
|
+
params = build_feed_params(mode, limit)
|
|
21
14
|
params[:types] = types if types
|
|
22
15
|
params[:cursor] = cursor if cursor
|
|
23
|
-
|
|
24
16
|
@api.post_form(@workspace, 'activity.feed', params)
|
|
25
17
|
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def build_feed_params(mode, limit)
|
|
22
|
+
{ mode: mode, limit: limit.to_s, archive_only: 'false', snooze_only: 'false',
|
|
23
|
+
unread_only: 'false', priority_only: 'false', is_activity_inbox: 'false' }
|
|
24
|
+
end
|
|
26
25
|
end
|
|
27
26
|
end
|
|
28
27
|
end
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Slk
|
|
4
4
|
module Api
|
|
5
|
+
# Wrapper for Slack bots.info API endpoint
|
|
5
6
|
class Bots
|
|
6
7
|
def initialize(api_client, workspace, on_debug: nil)
|
|
7
8
|
@api = api_client
|
|
@@ -13,8 +14,8 @@ module SlackCli
|
|
|
13
14
|
# @param bot_id [String] Bot ID starting with "B"
|
|
14
15
|
# @return [Hash, nil] Bot info hash or nil if not found
|
|
15
16
|
def info(bot_id)
|
|
16
|
-
response = @api.post_form(@workspace,
|
|
17
|
-
response[
|
|
17
|
+
response = @api.post_form(@workspace, 'bots.info', { bot: bot_id })
|
|
18
|
+
response['bot'] if response['ok']
|
|
18
19
|
rescue ApiError => e
|
|
19
20
|
@on_debug&.call("Bot lookup failed for #{bot_id}: #{e.message}")
|
|
20
21
|
nil
|
|
@@ -25,7 +26,7 @@ module SlackCli
|
|
|
25
26
|
# @return [String, nil] Bot name or nil if not found
|
|
26
27
|
def get_name(bot_id)
|
|
27
28
|
bot = info(bot_id)
|
|
28
|
-
bot&.dig(
|
|
29
|
+
bot&.dig('name')
|
|
29
30
|
end
|
|
30
31
|
end
|
|
31
32
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Api
|
|
5
|
+
# Wrapper for Slack client.counts and auth.test API endpoints
|
|
6
|
+
class Client
|
|
7
|
+
def initialize(api_client, workspace)
|
|
8
|
+
@api = api_client
|
|
9
|
+
@workspace = workspace
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def counts
|
|
13
|
+
@api.post(@workspace, 'client.counts')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def auth_test
|
|
17
|
+
@api.post(@workspace, 'auth.test')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def team_id
|
|
21
|
+
@team_id ||= auth_test['team_id']
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def unread_channels
|
|
25
|
+
response = counts
|
|
26
|
+
channels = response['channels'] || []
|
|
27
|
+
|
|
28
|
+
channels.select { |ch| (ch['mention_count'] || 0).positive? || ch['has_unreads'] }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def unread_dms
|
|
32
|
+
response = counts
|
|
33
|
+
dms = response['ims'] || []
|
|
34
|
+
mpims = response['mpims'] || []
|
|
35
|
+
|
|
36
|
+
(dms + mpims).select { |dm| (dm['mention_count'] || 0).positive? || dm['has_unreads'] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def total_unread_count
|
|
40
|
+
response = counts
|
|
41
|
+
sum_mentions(response, 'channels') + sum_mentions(response, 'ims') + sum_mentions(response, 'mpims')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def sum_mentions(response, key)
|
|
47
|
+
(response[key] || []).sum { |item| item['mention_count'] || 0 }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Slk
|
|
4
4
|
module Api
|
|
5
|
+
# Wrapper for Slack conversations.* API endpoints
|
|
5
6
|
class Conversations
|
|
6
7
|
def initialize(api_client, workspace)
|
|
7
8
|
@api = api_client
|
|
8
9
|
@workspace = workspace
|
|
9
10
|
end
|
|
10
11
|
|
|
11
|
-
def list(cursor: nil, limit: 1000, types:
|
|
12
|
+
def list(cursor: nil, limit: 1000, types: 'public_channel,private_channel')
|
|
12
13
|
params = { limit: limit, types: types }
|
|
13
14
|
params[:cursor] = cursor if cursor
|
|
14
|
-
@api.post(@workspace,
|
|
15
|
+
@api.post(@workspace, 'conversations.list', params)
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def history(channel:, limit: 20, cursor: nil, oldest: nil, latest: nil)
|
|
@@ -19,33 +20,33 @@ module SlackCli
|
|
|
19
20
|
params[:cursor] = cursor if cursor
|
|
20
21
|
params[:oldest] = oldest if oldest
|
|
21
22
|
params[:latest] = latest if latest
|
|
22
|
-
@api.post(@workspace,
|
|
23
|
+
@api.post(@workspace, 'conversations.history', params)
|
|
23
24
|
end
|
|
24
25
|
|
|
25
|
-
def replies(channel:,
|
|
26
|
-
params = { channel: channel, ts:
|
|
26
|
+
def replies(channel:, timestamp:, limit: 100, cursor: nil)
|
|
27
|
+
params = { channel: channel, ts: timestamp, limit: limit }
|
|
27
28
|
params[:cursor] = cursor if cursor
|
|
28
29
|
# Use form encoding - some workspaces (Enterprise Grid) require it
|
|
29
|
-
@api.post_form(@workspace,
|
|
30
|
+
@api.post_form(@workspace, 'conversations.replies', params)
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def open(users:)
|
|
33
|
-
user_list = Array(users).join(
|
|
34
|
-
@api.post(@workspace,
|
|
34
|
+
user_list = Array(users).join(',')
|
|
35
|
+
@api.post(@workspace, 'conversations.open', { users: user_list })
|
|
35
36
|
end
|
|
36
37
|
|
|
37
|
-
def mark(channel:,
|
|
38
|
-
@api.post(@workspace,
|
|
38
|
+
def mark(channel:, timestamp:)
|
|
39
|
+
@api.post(@workspace, 'conversations.mark', { channel: channel, ts: timestamp })
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
def info(channel:)
|
|
42
|
-
@api.post_form(@workspace,
|
|
43
|
+
@api.post_form(@workspace, 'conversations.info', { channel: channel })
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def members(channel:, cursor: nil, limit: 100)
|
|
46
47
|
params = { channel: channel, limit: limit }
|
|
47
48
|
params[:cursor] = cursor if cursor
|
|
48
|
-
@api.post(@workspace,
|
|
49
|
+
@api.post(@workspace, 'conversations.members', params)
|
|
49
50
|
end
|
|
50
51
|
end
|
|
51
52
|
end
|
data/lib/slk/api/dnd.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Api
|
|
5
|
+
# Wrapper for Slack dnd.* (Do Not Disturb) API endpoints
|
|
6
|
+
class Dnd
|
|
7
|
+
def initialize(api_client, workspace)
|
|
8
|
+
@api = api_client
|
|
9
|
+
@workspace = workspace
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def info
|
|
13
|
+
@api.post(@workspace, 'dnd.info')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def set_snooze(duration) # rubocop:disable Naming/AccessorMethodName
|
|
17
|
+
minutes = duration.to_minutes
|
|
18
|
+
@api.post(@workspace, 'dnd.setSnooze', { num_minutes: minutes })
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def end_snooze
|
|
22
|
+
@api.post(@workspace, 'dnd.endSnooze')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def snoozing?
|
|
26
|
+
info['snooze_enabled'] == true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def snooze_remaining
|
|
30
|
+
data = info
|
|
31
|
+
return nil unless data['snooze_enabled']
|
|
32
|
+
|
|
33
|
+
endtime = data['snooze_endtime']
|
|
34
|
+
return nil unless endtime
|
|
35
|
+
|
|
36
|
+
remaining = endtime - Time.now.to_i
|
|
37
|
+
remaining.positive? ? Models::Duration.new(seconds: remaining) : nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|