slk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -1
  3. data/README.md +5 -5
  4. data/bin/slk +3 -3
  5. data/lib/{slack_cli → slk}/api/activity.rb +10 -11
  6. data/lib/{slack_cli → slk}/api/bots.rb +5 -4
  7. data/lib/slk/api/client.rb +51 -0
  8. data/lib/{slack_cli → slk}/api/conversations.rb +14 -13
  9. data/lib/slk/api/dnd.rb +41 -0
  10. data/lib/{slack_cli → slk}/api/emoji.rb +4 -3
  11. data/lib/{slack_cli → slk}/api/threads.rb +13 -12
  12. data/lib/{slack_cli → slk}/api/usergroups.rb +2 -1
  13. data/lib/slk/api/users.rb +105 -0
  14. data/lib/slk/cli.rb +157 -0
  15. data/lib/slk/commands/activity.rb +152 -0
  16. data/lib/{slack_cli → slk}/commands/base.rb +67 -41
  17. data/lib/slk/commands/cache.rb +141 -0
  18. data/lib/slk/commands/catchup.rb +411 -0
  19. data/lib/slk/commands/config.rb +114 -0
  20. data/lib/slk/commands/dnd.rb +172 -0
  21. data/lib/slk/commands/emoji.rb +352 -0
  22. data/lib/slk/commands/help.rb +97 -0
  23. data/lib/slk/commands/messages.rb +299 -0
  24. data/lib/slk/commands/presence.rb +109 -0
  25. data/lib/slk/commands/preset.rb +231 -0
  26. data/lib/slk/commands/status.rb +223 -0
  27. data/lib/slk/commands/thread.rb +72 -0
  28. data/lib/slk/commands/unread.rb +305 -0
  29. data/lib/slk/commands/workspaces.rb +168 -0
  30. data/lib/slk/formatters/activity_formatter.rb +148 -0
  31. data/lib/slk/formatters/attachment_formatter.rb +65 -0
  32. data/lib/slk/formatters/block_formatter.rb +57 -0
  33. data/lib/{slack_cli → slk}/formatters/duration_formatter.rb +6 -5
  34. data/lib/slk/formatters/emoji_replacer.rb +141 -0
  35. data/lib/slk/formatters/json_message_formatter.rb +95 -0
  36. data/lib/slk/formatters/mention_replacer.rb +158 -0
  37. data/lib/slk/formatters/message_formatter.rb +174 -0
  38. data/lib/{slack_cli → slk}/formatters/output.rb +7 -6
  39. data/lib/slk/formatters/reaction_formatter.rb +87 -0
  40. data/lib/{slack_cli → slk}/models/channel.rb +12 -10
  41. data/lib/slk/models/duration.rb +94 -0
  42. data/lib/slk/models/message.rb +242 -0
  43. data/lib/slk/models/preset.rb +78 -0
  44. data/lib/{slack_cli → slk}/models/reaction.rb +6 -6
  45. data/lib/{slack_cli → slk}/models/status.rb +6 -6
  46. data/lib/slk/models/user.rb +55 -0
  47. data/lib/slk/models/workspace.rb +54 -0
  48. data/lib/{slack_cli → slk}/runner.rb +22 -19
  49. data/lib/slk/services/activity_enricher.rb +124 -0
  50. data/lib/slk/services/api_client.rb +145 -0
  51. data/lib/{slack_cli → slk}/services/cache_store.rb +20 -17
  52. data/lib/{slack_cli → slk}/services/configuration.rb +9 -8
  53. data/lib/slk/services/emoji_downloader.rb +103 -0
  54. data/lib/slk/services/emoji_searcher.rb +72 -0
  55. data/lib/{slack_cli → slk}/services/encryption.rb +11 -14
  56. data/lib/slk/services/gemoji_sync.rb +97 -0
  57. data/lib/{slack_cli → slk}/services/preset_store.rb +34 -33
  58. data/lib/slk/services/reaction_enricher.rb +82 -0
  59. data/lib/slk/services/setup_wizard.rb +131 -0
  60. data/lib/slk/services/target_resolver.rb +108 -0
  61. data/lib/{slack_cli → slk}/services/token_store.rb +11 -10
  62. data/lib/slk/services/unread_marker.rb +101 -0
  63. data/lib/{slack_cli → slk}/support/error_logger.rb +2 -1
  64. data/lib/{slack_cli → slk}/support/help_formatter.rb +36 -44
  65. data/lib/{slack_cli → slk}/support/inline_images.rb +28 -19
  66. data/lib/slk/support/interactive_prompt.rb +29 -0
  67. data/lib/{slack_cli → slk}/support/slack_url_parser.rb +15 -17
  68. data/lib/slk/support/text_wrapper.rb +57 -0
  69. data/lib/slk/support/user_resolver.rb +141 -0
  70. data/lib/{slack_cli → slk}/support/xdg_paths.rb +6 -5
  71. data/lib/slk/version.rb +5 -0
  72. data/lib/slk.rb +112 -0
  73. metadata +80 -59
  74. data/lib/slack_cli/api/client.rb +0 -49
  75. data/lib/slack_cli/api/dnd.rb +0 -40
  76. data/lib/slack_cli/api/users.rb +0 -101
  77. data/lib/slack_cli/cli.rb +0 -118
  78. data/lib/slack_cli/commands/activity.rb +0 -292
  79. data/lib/slack_cli/commands/cache.rb +0 -116
  80. data/lib/slack_cli/commands/catchup.rb +0 -484
  81. data/lib/slack_cli/commands/config.rb +0 -159
  82. data/lib/slack_cli/commands/dnd.rb +0 -143
  83. data/lib/slack_cli/commands/emoji.rb +0 -412
  84. data/lib/slack_cli/commands/help.rb +0 -76
  85. data/lib/slack_cli/commands/messages.rb +0 -317
  86. data/lib/slack_cli/commands/presence.rb +0 -107
  87. data/lib/slack_cli/commands/preset.rb +0 -239
  88. data/lib/slack_cli/commands/status.rb +0 -194
  89. data/lib/slack_cli/commands/thread.rb +0 -62
  90. data/lib/slack_cli/commands/unread.rb +0 -312
  91. data/lib/slack_cli/commands/workspaces.rb +0 -151
  92. data/lib/slack_cli/formatters/emoji_replacer.rb +0 -143
  93. data/lib/slack_cli/formatters/mention_replacer.rb +0 -154
  94. data/lib/slack_cli/formatters/message_formatter.rb +0 -429
  95. data/lib/slack_cli/models/duration.rb +0 -85
  96. data/lib/slack_cli/models/message.rb +0 -217
  97. data/lib/slack_cli/models/preset.rb +0 -73
  98. data/lib/slack_cli/models/user.rb +0 -56
  99. data/lib/slack_cli/models/workspace.rb +0 -52
  100. data/lib/slack_cli/services/api_client.rb +0 -149
  101. data/lib/slack_cli/services/reaction_enricher.rb +0 -87
  102. data/lib/slack_cli/support/user_resolver.rb +0 -114
  103. data/lib/slack_cli/version.rb +0 -5
  104. data/lib/slack_cli.rb +0 -91
@@ -0,0 +1,352 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../support/inline_images'
4
+ require_relative '../support/help_formatter'
5
+
6
+ module Slk
7
+ module Commands
8
+ # Downloads and manages workspace custom emoji
9
+ # rubocop:disable Metrics/ClassLength
10
+ class Emoji < Base
11
+ include Support::InlineImages
12
+
13
+ def execute
14
+ result = validate_options
15
+ return result if result
16
+
17
+ dispatch_action
18
+ rescue ApiError => e
19
+ error("Failed: #{e.message}")
20
+ 1
21
+ end
22
+
23
+ private
24
+
25
+ def dispatch_action
26
+ case positional_args
27
+ in ['status' | 'list'] | [] then show_status
28
+ in ['sync-standard'] then sync_standard
29
+ in ['download', *rest] then download_emoji(rest.first)
30
+ in ['clear', *rest] then clear_emoji(rest.first)
31
+ in ['search', query, *_] then search_emoji(query)
32
+ in ['search'] then missing_search_query
33
+ else unknown_action
34
+ end
35
+ end
36
+
37
+ def missing_search_query
38
+ error('Usage: slk emoji search <query>')
39
+ 1
40
+ end
41
+
42
+ def unknown_action
43
+ error("Unknown action: #{positional_args.first}")
44
+ 1
45
+ end
46
+
47
+ protected
48
+
49
+ def default_options
50
+ super.merge(force: false)
51
+ end
52
+
53
+ def handle_option(arg, args, remaining)
54
+ case arg
55
+ when '-f', '--force'
56
+ @options[:force] = true
57
+ else
58
+ super
59
+ end
60
+ end
61
+
62
+ def help_text
63
+ help = Support::HelpFormatter.new('slk emoji <action> [workspace]')
64
+ help.description('Manage emoji cache.')
65
+ add_actions_section(help)
66
+ add_options_section(help)
67
+ help.render
68
+ end
69
+
70
+ def add_actions_section(help)
71
+ help.section('ACTIONS') do |s|
72
+ s.action('status', 'Show emoji cache status')
73
+ s.action('search <query>', 'Search emoji by name (all workspaces by default)')
74
+ s.action('sync-standard', 'Download standard emoji database (gemoji)')
75
+ s.action('download [ws]', 'Download workspace custom emoji')
76
+ s.action('clear [ws]', 'Clear emoji cache')
77
+ end
78
+ end
79
+
80
+ def add_options_section(help)
81
+ help.section('OPTIONS') do |s|
82
+ s.option('-w, --workspace', 'Limit to specific workspace')
83
+ s.option('-f, --force', 'Skip confirmation for clear')
84
+ s.option('-q, --quiet', 'Suppress output')
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def show_status
91
+ paths = Support::XdgPaths.new
92
+ emoji_dir = config.emoji_dir || paths.cache_dir
93
+
94
+ show_standard_emoji_status(paths.cache_dir)
95
+ puts
96
+ show_workspace_emoji_status(emoji_dir)
97
+
98
+ 0
99
+ end
100
+
101
+ def show_standard_emoji_status(cache_dir)
102
+ gemoji_path = File.join(cache_dir, 'gemoji.json')
103
+
104
+ if File.exist?(gemoji_path)
105
+ display_gemoji_status(gemoji_path)
106
+ else
107
+ puts "Standard emoji database: #{output.yellow('not downloaded')}"
108
+ puts " Run 'slk emoji sync-standard' to download"
109
+ end
110
+ end
111
+
112
+ def display_gemoji_status(gemoji_path)
113
+ gemoji = JSON.parse(File.read(gemoji_path))
114
+ puts "Standard emoji database: #{gemoji.size} emojis"
115
+ rescue JSON::ParserError
116
+ puts "Standard emoji database: #{output.yellow('corrupted')}"
117
+ puts " Run 'slk emoji sync-standard' to re-download"
118
+ end
119
+
120
+ def show_workspace_emoji_status(emoji_dir)
121
+ puts "Workspace emojis: (#{emoji_dir})"
122
+ target_workspaces.each { |workspace| display_workspace_status(emoji_dir, workspace) }
123
+ end
124
+
125
+ def display_workspace_status(emoji_dir, workspace)
126
+ workspace_dir = File.join(emoji_dir, workspace.name)
127
+
128
+ if Dir.exist?(workspace_dir)
129
+ display_workspace_emoji_count(workspace.name, workspace_dir)
130
+ else
131
+ puts " #{workspace.name}: #{output.yellow('not downloaded')}"
132
+ end
133
+ end
134
+
135
+ def display_workspace_emoji_count(name, workspace_dir)
136
+ files = Dir.glob(File.join(workspace_dir, '*'))
137
+ count = files.count
138
+ size = files.sum { |f| safe_file_size(f) }
139
+ puts " #{name}: #{count} emojis (#{format_size(size)})"
140
+ end
141
+
142
+ def search_emoji(query)
143
+ searcher = build_emoji_searcher
144
+ workspaces = @options[:workspace] ? [runner.workspace(@options[:workspace])] : runner.all_workspaces
145
+ by_source = searcher.search(query, workspaces: workspaces)
146
+
147
+ if by_source.empty?
148
+ puts "No emoji matching '#{query}'"
149
+ else
150
+ display_search_results(by_source)
151
+ puts "Found #{by_source.values.flatten.size} emoji matching '#{query}'"
152
+ end
153
+ 0
154
+ end
155
+
156
+ def build_emoji_searcher
157
+ paths = Support::XdgPaths.new
158
+ emoji_dir = config.emoji_dir || paths.cache_dir
159
+
160
+ Services::EmojiSearcher.new(
161
+ cache_dir: paths.cache_dir,
162
+ emoji_dir: emoji_dir,
163
+ on_debug: ->(msg) { debug(msg) }
164
+ )
165
+ end
166
+
167
+ def display_search_results(by_source)
168
+ by_source.each do |source, items|
169
+ puts output.bold(source == 'standard' ? 'Standard emoji:' : "#{source}:")
170
+ items.sort_by { |r| r[:name] }.each do |item|
171
+ display_emoji_item(item)
172
+ end
173
+ puts
174
+ end
175
+ end
176
+
177
+ def display_emoji_item(item)
178
+ if item[:char]
179
+ puts "#{item[:char]} :#{item[:name]}:"
180
+ elsif item[:path]
181
+ puts ":#{item[:name]}:" unless print_inline_image_with_text(item[:path], ":#{item[:name]}:")
182
+ else
183
+ puts ":#{item[:name]}:"
184
+ end
185
+ end
186
+
187
+ def print_progress(current, total, downloaded, _skipped)
188
+ # Only update every 1% or when downloading (to show new count)
189
+ pct = ((current.to_f / total) * 100).round
190
+ @last_pct ||= -1
191
+ return if pct == @last_pct && downloaded == (@last_downloaded || 0)
192
+
193
+ @last_pct = pct
194
+ @last_downloaded = downloaded
195
+
196
+ bar_width = 20
197
+ filled = (pct * bar_width / 100).round
198
+ bar = ('=' * filled) + ('-' * (bar_width - filled))
199
+ print "\r [#{bar}] #{pct}% (#{current}/#{total}) +#{downloaded} new"
200
+ $stdout.flush
201
+ end
202
+
203
+ def format_size(bytes)
204
+ if bytes >= 1024 * 1024
205
+ "#{(bytes / (1024.0 * 1024)).round}M"
206
+ elsif bytes >= 1024
207
+ "#{(bytes / 1024.0).round}K"
208
+ else
209
+ "#{bytes}B"
210
+ end
211
+ end
212
+
213
+ # Get file size, returning 0 if file doesn't exist or is inaccessible
214
+ def safe_file_size(path)
215
+ File.size(path)
216
+ rescue Errno::ENOENT, Errno::EACCES
217
+ 0
218
+ end
219
+
220
+ def sync_standard
221
+ paths = Support::XdgPaths.new
222
+ syncer = Services::GemojiSync.new(cache_dir: paths.cache_dir, on_progress: ->(msg) { puts msg })
223
+ result = syncer.sync
224
+
225
+ return sync_error(result[:error]) if result[:error]
226
+
227
+ success("Downloaded #{result[:count]} standard emoji mappings")
228
+ puts " Location: #{result[:path]}"
229
+ 0
230
+ end
231
+
232
+ def sync_error(message)
233
+ error(message)
234
+ 1
235
+ end
236
+
237
+ def download_emoji(workspace_name)
238
+ workspaces = workspace_name ? [runner.workspace(workspace_name)] : target_workspaces
239
+ downloader = build_emoji_downloader
240
+
241
+ workspaces.each { |workspace| download_workspace_emoji(workspace, downloader) }
242
+ 0
243
+ end
244
+
245
+ def build_emoji_downloader
246
+ paths = Support::XdgPaths.new
247
+ emoji_dir = config.emoji_dir || paths.cache_dir
248
+
249
+ Services::EmojiDownloader.new(
250
+ emoji_dir: emoji_dir,
251
+ on_progress: ->(current, total, downloaded, skipped) { print_progress(current, total, downloaded, skipped) },
252
+ on_debug: ->(msg) { debug(msg) }
253
+ )
254
+ end
255
+
256
+ def download_workspace_emoji(workspace, downloader)
257
+ puts "Fetching emoji list for #{workspace.name}..."
258
+
259
+ api = runner.emoji_api(workspace.name)
260
+ emoji_map = api.custom_emoji
261
+ stats = downloader.download(workspace.name, emoji_map)
262
+
263
+ display_download_results(workspace.name, stats)
264
+ end
265
+
266
+ def display_download_results(workspace_name, stats)
267
+ puts "\r#{' ' * 60}\r" # Clear progress line
268
+ success("Downloaded #{stats[:downloaded]} new emoji for #{workspace_name}")
269
+ return unless stats[:skipped].positive? || stats[:failed].positive?
270
+
271
+ puts " Skipped: #{stats[:skipped]} (already exist), #{stats[:aliases]} aliases, #{stats[:failed]} failed"
272
+ end
273
+
274
+ def clear_emoji(workspace_name)
275
+ paths = Support::XdgPaths.new
276
+ emoji_dir = config.emoji_dir || paths.cache_dir
277
+
278
+ to_clear = gather_dirs_to_clear(emoji_dir, workspace_name)
279
+ return 0 if to_clear.nil?
280
+
281
+ stats = display_clear_preview(to_clear)
282
+ return 0 unless confirm_clear?
283
+
284
+ perform_clear(to_clear, stats[:total_count])
285
+ end
286
+
287
+ def gather_dirs_to_clear(emoji_dir, workspace_name)
288
+ workspace_name ? gather_single_workspace_dir(emoji_dir, workspace_name) : gather_all_workspace_dirs(emoji_dir)
289
+ end
290
+
291
+ def gather_single_workspace_dir(emoji_dir, workspace_name)
292
+ workspace_dir = File.join(emoji_dir, workspace_name)
293
+ return [{ name: workspace_name, dir: workspace_dir }] if Dir.exist?(workspace_dir)
294
+
295
+ puts "No emoji cache for #{workspace_name}"
296
+ nil
297
+ end
298
+
299
+ def gather_all_workspace_dirs(emoji_dir)
300
+ dirs = target_workspaces.filter_map do |workspace|
301
+ workspace_dir = File.join(emoji_dir, workspace.name)
302
+ { name: workspace.name, dir: workspace_dir } if Dir.exist?(workspace_dir)
303
+ end
304
+
305
+ return dirs if dirs.any?
306
+
307
+ puts 'No emoji caches to clear'
308
+ nil
309
+ end
310
+
311
+ def display_clear_preview(to_clear)
312
+ puts 'Will delete:'
313
+ totals = { count: 0, size: 0 }
314
+
315
+ to_clear.each { |entry| display_clear_entry(entry, totals) }
316
+
317
+ puts " Total: #{totals[:count]} files (#{format_size(totals[:size])})"
318
+ { total_count: totals[:count], total_size: totals[:size] }
319
+ end
320
+
321
+ def display_clear_entry(entry, totals)
322
+ files = Dir.glob(File.join(entry[:dir], '*'))
323
+ count = files.count
324
+ size = files.sum { |f| safe_file_size(f) }
325
+ totals[:count] += count
326
+ totals[:size] += size
327
+ puts " #{entry[:name]}: #{count} files (#{format_size(size)})"
328
+ end
329
+
330
+ def confirm_clear?
331
+ return true if @options[:force]
332
+
333
+ print "\nAre you sure? [y/N] "
334
+ response = $stdin.gets&.chomp&.downcase
335
+ return true if %w[y yes].include?(response)
336
+
337
+ puts 'Cancelled'
338
+ false
339
+ end
340
+
341
+ def perform_clear(to_clear, total_count)
342
+ to_clear.each do |entry|
343
+ FileUtils.rm_rf(entry[:dir])
344
+ end
345
+
346
+ success("Cleared #{total_count} emoji files")
347
+ 0
348
+ end
349
+ end
350
+ # rubocop:enable Metrics/ClassLength
351
+ end
352
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Commands
5
+ # Displays help information for commands
6
+ class Help < Base
7
+ def execute
8
+ topic = positional_args.first
9
+
10
+ if topic
11
+ show_command_help(topic)
12
+ else
13
+ show_general_help
14
+ end
15
+
16
+ 0
17
+ end
18
+
19
+ private
20
+
21
+ def show_general_help
22
+ puts build_header
23
+ puts build_commands_section
24
+ puts build_options_section
25
+ puts build_examples_section
26
+ puts "Run #{output.cyan('slk <command> --help')} for command-specific help."
27
+ end
28
+
29
+ def build_header
30
+ <<~HEADER
31
+ #{output.bold('slk')} - Slack CLI v#{VERSION}
32
+
33
+ #{output.bold('USAGE:')}
34
+ slk <command> [options]
35
+ HEADER
36
+ end
37
+
38
+ # rubocop:disable Metrics/AbcSize
39
+ def build_commands_section
40
+ <<~COMMANDS
41
+ #{output.bold('COMMANDS:')}
42
+ #{output.cyan('status')} Get or set your status
43
+ #{output.cyan('presence')} Get or set your presence (away/active)
44
+ #{output.cyan('dnd')} Manage Do Not Disturb
45
+ #{output.cyan('messages')} Read channel or DM messages
46
+ #{output.cyan('unread')} View and clear unread messages
47
+ #{output.cyan('preset')} Manage and apply status presets
48
+ #{output.cyan('workspaces')} Manage Slack workspaces
49
+ #{output.cyan('cache')} Manage user/channel cache
50
+ #{output.cyan('emoji')} Download workspace custom emoji
51
+ #{output.cyan('config')} Configuration and setup
52
+ COMMANDS
53
+ end
54
+ # rubocop:enable Metrics/AbcSize
55
+
56
+ def build_options_section
57
+ <<~OPTIONS
58
+ #{output.bold('GLOBAL OPTIONS:')}
59
+ -w, --workspace NAME Use specific workspace
60
+ --all Apply to all workspaces
61
+ -v, --verbose Show debug output
62
+ -q, --quiet Suppress output
63
+ --json Output as JSON (where supported)
64
+ -h, --help Show help
65
+ OPTIONS
66
+ end
67
+
68
+ def build_examples_section
69
+ <<~EXAMPLES
70
+ #{output.bold('EXAMPLES:')}
71
+ slk status Show current status
72
+ slk status "Working" :laptop: Set status
73
+ slk status clear Clear status
74
+ slk dnd 1h Enable DND for 1 hour
75
+ slk messages #general Read channel messages
76
+ slk preset meeting Apply preset
77
+ EXAMPLES
78
+ end
79
+
80
+ def show_command_help(topic)
81
+ command_class = CLI::COMMANDS[topic]
82
+
83
+ if command_class
84
+ # Create instance just to get help text
85
+ # Call --help directly since help_text is protected
86
+ runner_stub = Runner.new(output: output)
87
+ cmd = command_class.new(['--help'], runner: runner_stub)
88
+ cmd.execute
89
+ else
90
+ error("Unknown command: #{topic}")
91
+ puts
92
+ puts "Available commands: #{CLI::COMMANDS.keys.join(', ')}"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end