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.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -1
  3. data/README.md +33 -15
  4. data/bin/ci +15 -0
  5. data/bin/coverage +225 -0
  6. data/bin/slk +3 -3
  7. data/bin/test +7 -0
  8. data/lib/{slack_cli → slk}/api/activity.rb +10 -11
  9. data/lib/{slack_cli → slk}/api/bots.rb +5 -4
  10. data/lib/slk/api/client.rb +51 -0
  11. data/lib/{slack_cli → slk}/api/conversations.rb +14 -13
  12. data/lib/slk/api/dnd.rb +41 -0
  13. data/lib/{slack_cli → slk}/api/emoji.rb +4 -3
  14. data/lib/slk/api/search.rb +31 -0
  15. data/lib/{slack_cli → slk}/api/threads.rb +13 -12
  16. data/lib/{slack_cli → slk}/api/usergroups.rb +2 -1
  17. data/lib/slk/api/users.rb +105 -0
  18. data/lib/slk/cli.rb +204 -0
  19. data/lib/slk/commands/activity.rb +152 -0
  20. data/lib/{slack_cli → slk}/commands/base.rb +68 -41
  21. data/lib/slk/commands/cache.rb +141 -0
  22. data/lib/slk/commands/catchup.rb +412 -0
  23. data/lib/slk/commands/config.rb +117 -0
  24. data/lib/slk/commands/dnd.rb +172 -0
  25. data/lib/slk/commands/emoji.rb +352 -0
  26. data/lib/slk/commands/help.rb +98 -0
  27. data/lib/slk/commands/messages.rb +347 -0
  28. data/lib/slk/commands/presence.rb +109 -0
  29. data/lib/slk/commands/preset.rb +231 -0
  30. data/lib/slk/commands/search.rb +223 -0
  31. data/lib/slk/commands/ssh_key_manager.rb +129 -0
  32. data/lib/slk/commands/status.rb +223 -0
  33. data/lib/slk/commands/thread.rb +72 -0
  34. data/lib/slk/commands/unread.rb +305 -0
  35. data/lib/slk/commands/workspaces.rb +168 -0
  36. data/lib/slk/formatters/activity_formatter.rb +148 -0
  37. data/lib/slk/formatters/attachment_formatter.rb +79 -0
  38. data/lib/slk/formatters/block_formatter.rb +57 -0
  39. data/lib/{slack_cli → slk}/formatters/duration_formatter.rb +6 -5
  40. data/lib/slk/formatters/emoji_replacer.rb +141 -0
  41. data/lib/slk/formatters/json_message_formatter.rb +95 -0
  42. data/lib/slk/formatters/mention_replacer.rb +140 -0
  43. data/lib/slk/formatters/message_formatter.rb +167 -0
  44. data/lib/{slack_cli → slk}/formatters/output.rb +7 -6
  45. data/lib/slk/formatters/reaction_formatter.rb +87 -0
  46. data/lib/slk/formatters/search_formatter.rb +75 -0
  47. data/lib/{slack_cli → slk}/models/channel.rb +12 -10
  48. data/lib/slk/models/duration.rb +94 -0
  49. data/lib/slk/models/message.rb +242 -0
  50. data/lib/slk/models/preset.rb +78 -0
  51. data/lib/{slack_cli → slk}/models/reaction.rb +6 -6
  52. data/lib/slk/models/search_result.rb +115 -0
  53. data/lib/{slack_cli → slk}/models/status.rb +6 -6
  54. data/lib/slk/models/user.rb +55 -0
  55. data/lib/slk/models/workspace.rb +54 -0
  56. data/lib/{slack_cli → slk}/runner.rb +34 -19
  57. data/lib/slk/services/activity_enricher.rb +124 -0
  58. data/lib/slk/services/api_client.rb +194 -0
  59. data/lib/{slack_cli → slk}/services/cache_store.rb +63 -41
  60. data/lib/{slack_cli → slk}/services/configuration.rb +9 -8
  61. data/lib/slk/services/emoji_downloader.rb +103 -0
  62. data/lib/slk/services/emoji_searcher.rb +72 -0
  63. data/lib/slk/services/encryption.rb +151 -0
  64. data/lib/slk/services/gemoji_sync.rb +97 -0
  65. data/lib/{slack_cli → slk}/services/preset_store.rb +34 -33
  66. data/lib/slk/services/reaction_enricher.rb +82 -0
  67. data/lib/slk/services/setup_wizard.rb +131 -0
  68. data/lib/slk/services/target_resolver.rb +131 -0
  69. data/lib/slk/services/token_loader.rb +83 -0
  70. data/lib/slk/services/token_saver.rb +87 -0
  71. data/lib/slk/services/token_store.rb +88 -0
  72. data/lib/slk/services/unread_marker.rb +101 -0
  73. data/lib/slk/services/user_lookup.rb +117 -0
  74. data/lib/slk/support/date_parser.rb +64 -0
  75. data/lib/{slack_cli → slk}/support/error_logger.rb +2 -1
  76. data/lib/{slack_cli → slk}/support/help_formatter.rb +36 -44
  77. data/lib/{slack_cli → slk}/support/inline_images.rb +28 -19
  78. data/lib/slk/support/interactive_prompt.rb +29 -0
  79. data/lib/slk/support/platform.rb +34 -0
  80. data/lib/{slack_cli → slk}/support/slack_url_parser.rb +15 -17
  81. data/lib/slk/support/text_wrapper.rb +57 -0
  82. data/lib/slk/support/user_resolver.rb +141 -0
  83. data/lib/slk/support/xdg_paths.rb +56 -0
  84. data/lib/slk/version.rb +5 -0
  85. data/lib/slk.rb +120 -0
  86. metadata +93 -59
  87. data/lib/slack_cli/api/client.rb +0 -49
  88. data/lib/slack_cli/api/dnd.rb +0 -40
  89. data/lib/slack_cli/api/users.rb +0 -101
  90. data/lib/slack_cli/cli.rb +0 -118
  91. data/lib/slack_cli/commands/activity.rb +0 -292
  92. data/lib/slack_cli/commands/cache.rb +0 -116
  93. data/lib/slack_cli/commands/catchup.rb +0 -484
  94. data/lib/slack_cli/commands/config.rb +0 -159
  95. data/lib/slack_cli/commands/dnd.rb +0 -143
  96. data/lib/slack_cli/commands/emoji.rb +0 -412
  97. data/lib/slack_cli/commands/help.rb +0 -76
  98. data/lib/slack_cli/commands/messages.rb +0 -317
  99. data/lib/slack_cli/commands/presence.rb +0 -107
  100. data/lib/slack_cli/commands/preset.rb +0 -239
  101. data/lib/slack_cli/commands/status.rb +0 -194
  102. data/lib/slack_cli/commands/thread.rb +0 -62
  103. data/lib/slack_cli/commands/unread.rb +0 -312
  104. data/lib/slack_cli/commands/workspaces.rb +0 -151
  105. data/lib/slack_cli/formatters/emoji_replacer.rb +0 -143
  106. data/lib/slack_cli/formatters/mention_replacer.rb +0 -154
  107. data/lib/slack_cli/formatters/message_formatter.rb +0 -429
  108. data/lib/slack_cli/models/duration.rb +0 -85
  109. data/lib/slack_cli/models/message.rb +0 -217
  110. data/lib/slack_cli/models/preset.rb +0 -73
  111. data/lib/slack_cli/models/user.rb +0 -56
  112. data/lib/slack_cli/models/workspace.rb +0 -52
  113. data/lib/slack_cli/services/api_client.rb +0 -149
  114. data/lib/slack_cli/services/encryption.rb +0 -51
  115. data/lib/slack_cli/services/reaction_enricher.rb +0 -87
  116. data/lib/slack_cli/services/token_store.rb +0 -117
  117. data/lib/slack_cli/support/user_resolver.rb +0 -114
  118. data/lib/slack_cli/support/xdg_paths.rb +0 -37
  119. data/lib/slack_cli/version.rb +0 -5
  120. data/lib/slack_cli.rb +0 -91
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09f740d5f0c6a9290031f4e94edff1bb118c85c80df51ccc4ef98b1dea6800a2'
4
- data.tar.gz: 298d8188fd6bf43a07c8672523d9df253eff44dfe9481a0801d01062b161c8fd
3
+ metadata.gz: fd09692101d4658e57208d39e81d770e6dc36d2218088194ef55ad5fad0adbd0
4
+ data.tar.gz: 88e03a0be601536ffb3fd8f1b986900f8f57144b03a52ec5fa24b0b5dbdfbc57
5
5
  SHA512:
6
- metadata.gz: dca5ad190d8155a4312bbafc39def037d1dfc9401170a58cc9adc2f22d6bbb4645efc7bcfd383e2be5d29b31c4f794da518f182df62182c47445fe3deec5d625
7
- data.tar.gz: cba53cbbf73ed98c220bc984150792bd44e832992be70e62e62d425e2f0a6885f1414a1fe027d1d07ad449c1bef374f12aa2cc12814fda4cfe5bde9844df9f46
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.1.0]: https://github.com/ericboehs/slack-cli/releases/tag/v0.1.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 #general # Read channel messages
75
+ slk messages general # Read channel messages
60
76
  slk messages @username # Read DM with user
61
- slk messages #general -n 50 # Show 50 messages
62
- slk messages #general --json # Output as JSON
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 #general # Mark channel as read
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/slack-cli/tokens.age`.
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/slack-cli/`
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/slack-cli/`
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/slack-cli.git
173
- cd slack-cli
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
- # Build gem
182
- gem build slk.gemspec
198
+ ### Releasing
183
199
 
184
- # Install locally
185
- gem install ./slk-0.1.0.gem
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
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ # Run the full CI suite (tests + linting)
3
+ set -e
4
+
5
+ cd "$(dirname "$0")/.."
6
+
7
+ echo "==> Running tests..."
8
+ rake test
9
+
10
+ echo ""
11
+ echo "==> Running rubocop..."
12
+ rubocop
13
+
14
+ echo ""
15
+ echo "==> All checks passed!"
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("../lib", __dir__))
5
- require "slack_cli"
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+ require 'slk'
6
6
 
7
- exit SlackCli::CLI.new(ARGV).run
7
+ exit Slk::CLI.new(ARGV).run
data/bin/test ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ # Run the test suite
3
+ set -e
4
+
5
+ cd "$(dirname "$0")/.."
6
+
7
+ rake test "$@"
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SlackCli
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 SlackCli
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, "bots.info", { bot: bot_id })
17
- response["bot"] if response["ok"]
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("name")
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 SlackCli
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: "public_channel,private_channel")
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, "conversations.list", params)
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, "conversations.history", params)
23
+ @api.post(@workspace, 'conversations.history', params)
23
24
  end
24
25
 
25
- def replies(channel:, ts:, limit: 100, cursor: nil)
26
- params = { channel: channel, ts: ts, limit: limit }
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, "conversations.replies", params)
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, "conversations.open", { users: user_list })
34
+ user_list = Array(users).join(',')
35
+ @api.post(@workspace, 'conversations.open', { users: user_list })
35
36
  end
36
37
 
37
- def mark(channel:, ts:)
38
- @api.post(@workspace, "conversations.mark", { channel: channel, ts: ts })
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, "conversations.info", { channel: channel })
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, "conversations.members", params)
49
+ @api.post(@workspace, 'conversations.members', params)
49
50
  end
50
51
  end
51
52
  end
@@ -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