slk 0.1.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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +46 -0
  3. data/LICENSE +21 -0
  4. data/README.md +190 -0
  5. data/bin/slk +7 -0
  6. data/lib/slack_cli/api/activity.rb +28 -0
  7. data/lib/slack_cli/api/bots.rb +32 -0
  8. data/lib/slack_cli/api/client.rb +49 -0
  9. data/lib/slack_cli/api/conversations.rb +52 -0
  10. data/lib/slack_cli/api/dnd.rb +40 -0
  11. data/lib/slack_cli/api/emoji.rb +21 -0
  12. data/lib/slack_cli/api/threads.rb +44 -0
  13. data/lib/slack_cli/api/usergroups.rb +25 -0
  14. data/lib/slack_cli/api/users.rb +101 -0
  15. data/lib/slack_cli/cli.rb +118 -0
  16. data/lib/slack_cli/commands/activity.rb +292 -0
  17. data/lib/slack_cli/commands/base.rb +175 -0
  18. data/lib/slack_cli/commands/cache.rb +116 -0
  19. data/lib/slack_cli/commands/catchup.rb +484 -0
  20. data/lib/slack_cli/commands/config.rb +159 -0
  21. data/lib/slack_cli/commands/dnd.rb +143 -0
  22. data/lib/slack_cli/commands/emoji.rb +412 -0
  23. data/lib/slack_cli/commands/help.rb +76 -0
  24. data/lib/slack_cli/commands/messages.rb +317 -0
  25. data/lib/slack_cli/commands/presence.rb +107 -0
  26. data/lib/slack_cli/commands/preset.rb +239 -0
  27. data/lib/slack_cli/commands/status.rb +194 -0
  28. data/lib/slack_cli/commands/thread.rb +62 -0
  29. data/lib/slack_cli/commands/unread.rb +312 -0
  30. data/lib/slack_cli/commands/workspaces.rb +151 -0
  31. data/lib/slack_cli/formatters/duration_formatter.rb +28 -0
  32. data/lib/slack_cli/formatters/emoji_replacer.rb +143 -0
  33. data/lib/slack_cli/formatters/mention_replacer.rb +154 -0
  34. data/lib/slack_cli/formatters/message_formatter.rb +429 -0
  35. data/lib/slack_cli/formatters/output.rb +89 -0
  36. data/lib/slack_cli/models/channel.rb +52 -0
  37. data/lib/slack_cli/models/duration.rb +85 -0
  38. data/lib/slack_cli/models/message.rb +217 -0
  39. data/lib/slack_cli/models/preset.rb +73 -0
  40. data/lib/slack_cli/models/reaction.rb +54 -0
  41. data/lib/slack_cli/models/status.rb +57 -0
  42. data/lib/slack_cli/models/user.rb +56 -0
  43. data/lib/slack_cli/models/workspace.rb +52 -0
  44. data/lib/slack_cli/runner.rb +123 -0
  45. data/lib/slack_cli/services/api_client.rb +149 -0
  46. data/lib/slack_cli/services/cache_store.rb +198 -0
  47. data/lib/slack_cli/services/configuration.rb +74 -0
  48. data/lib/slack_cli/services/encryption.rb +51 -0
  49. data/lib/slack_cli/services/preset_store.rb +112 -0
  50. data/lib/slack_cli/services/reaction_enricher.rb +87 -0
  51. data/lib/slack_cli/services/token_store.rb +117 -0
  52. data/lib/slack_cli/support/error_logger.rb +28 -0
  53. data/lib/slack_cli/support/help_formatter.rb +139 -0
  54. data/lib/slack_cli/support/inline_images.rb +62 -0
  55. data/lib/slack_cli/support/slack_url_parser.rb +78 -0
  56. data/lib/slack_cli/support/user_resolver.rb +114 -0
  57. data/lib/slack_cli/support/xdg_paths.rb +37 -0
  58. data/lib/slack_cli/version.rb +5 -0
  59. data/lib/slack_cli.rb +91 -0
  60. metadata +103 -0
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ class CLI
5
+ COMMANDS = {
6
+ "status" => Commands::Status,
7
+ "presence" => Commands::Presence,
8
+ "dnd" => Commands::Dnd,
9
+ "messages" => Commands::Messages,
10
+ "thread" => Commands::Thread,
11
+ "unread" => Commands::Unread,
12
+ "catchup" => Commands::Catchup,
13
+ "activity" => Commands::Activity,
14
+ "preset" => Commands::Preset,
15
+ "workspaces" => Commands::Workspaces,
16
+ "cache" => Commands::Cache,
17
+ "emoji" => Commands::Emoji,
18
+ "config" => Commands::Config,
19
+ "help" => Commands::Help
20
+ }.freeze
21
+
22
+ def initialize(argv, output: nil)
23
+ @argv = argv.dup
24
+ @output = output || Formatters::Output.new
25
+ end
26
+
27
+ def run
28
+ command_name, *args = @argv
29
+
30
+ # Handle version flags
31
+ if command_name.nil? || command_name == "--help" || command_name == "-h"
32
+ return run_command("help", [])
33
+ end
34
+
35
+ if command_name == "--version" || command_name == "-V" || command_name == "version"
36
+ @output.puts "slk v#{VERSION}"
37
+ return 0
38
+ end
39
+
40
+ # Look up command
41
+ if (command_class = COMMANDS[command_name])
42
+ run_command(command_name, args)
43
+ elsif preset_exists?(command_name)
44
+ # Treat as preset shortcut
45
+ run_command("preset", [command_name] + args)
46
+ else
47
+ @output.error("Unknown command: #{command_name}")
48
+ @output.puts
49
+ @output.puts "Run 'slk help' for available commands."
50
+ 1
51
+ end
52
+ rescue ConfigError => e
53
+ @output.error(e.message)
54
+ log_error(e)
55
+ 1
56
+ rescue EncryptionError => e
57
+ @output.error("Encryption error: #{e.message}")
58
+ log_error(e)
59
+ 1
60
+ rescue ApiError => e
61
+ @output.error("API error: #{e.message}")
62
+ log_error(e)
63
+ 1
64
+ rescue Interrupt
65
+ @output.puts
66
+ @output.puts "Interrupted."
67
+ 130
68
+ rescue StandardError => e
69
+ @output.error("Unexpected error: #{e.message}")
70
+ log_path = log_error(e)
71
+ @output.puts "Details logged to: #{log_path}" if log_path
72
+ 1
73
+ end
74
+
75
+ private
76
+
77
+ def run_command(name, args)
78
+ command_class = COMMANDS[name]
79
+ return 1 unless command_class
80
+
81
+ verbose = args.include?("-v") || args.include?("--verbose")
82
+
83
+ # Create output with verbose flag
84
+ output = Formatters::Output.new(verbose: verbose)
85
+ runner = Runner.new(output: output)
86
+
87
+ # Set up API call logging if verbose
88
+ if verbose
89
+ runner.api_client.on_request = ->(method, count) {
90
+ output.debug("[API ##{count}] #{method}")
91
+ }
92
+ end
93
+
94
+ command = command_class.new(args, runner: runner)
95
+ result = command.execute
96
+
97
+ # Show API call count if verbose
98
+ if verbose && runner.api_client.call_count > 0
99
+ output.debug("Total API calls: #{runner.api_client.call_count}")
100
+ end
101
+
102
+ result
103
+ ensure
104
+ # Clean up HTTP connections
105
+ runner&.api_client&.close
106
+ end
107
+
108
+ def preset_exists?(name)
109
+ # PresetStore handles JSON parse errors internally via on_warning callback
110
+ # ConfigError should propagate as it indicates a real configuration problem
111
+ Services::PresetStore.new.exists?(name)
112
+ end
113
+
114
+ def log_error(error)
115
+ Support::ErrorLogger.log(error)
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../support/help_formatter'
4
+
5
+ module SlackCli
6
+ module Commands
7
+ class Activity < Base
8
+ def execute
9
+ result = validate_options
10
+ return result if result
11
+
12
+ workspace = target_workspaces.first
13
+ api = runner.activity_api(workspace.name)
14
+
15
+ response = api.feed(limit: @options[:limit], types: activity_types)
16
+
17
+ unless response['ok']
18
+ error("Failed to fetch activity: #{response['error']}")
19
+ return 1
20
+ end
21
+
22
+ items = response['items'] || []
23
+
24
+ if @options[:json]
25
+ output_json(items)
26
+ else
27
+ display_activity(items, workspace)
28
+ end
29
+
30
+ 0
31
+ rescue ApiError => e
32
+ error("Failed to fetch activity: #{e.message}")
33
+ 1
34
+ end
35
+
36
+ protected
37
+
38
+ def default_options
39
+ super.merge(
40
+ limit: 20,
41
+ filter: :all,
42
+ show_messages: false
43
+ )
44
+ end
45
+
46
+ def handle_option(arg, args, remaining)
47
+ case arg
48
+ when '-n', '--limit'
49
+ @options[:limit] = args.shift.to_i
50
+ when '--reactions'
51
+ @options[:filter] = :reactions
52
+ when '--mentions'
53
+ @options[:filter] = :mentions
54
+ when '--threads'
55
+ @options[:filter] = :threads
56
+ when '--show-messages', '-m'
57
+ @options[:show_messages] = true
58
+ else
59
+ super
60
+ end
61
+ end
62
+
63
+ def help_text
64
+ help = Support::HelpFormatter.new('slk activity [options]')
65
+ help.description('Show recent activity from the activity feed.')
66
+
67
+ help.section('OPTIONS') do |s|
68
+ s.option('-n, --limit N', 'Number of items (default: 20, max: 50)')
69
+ s.option('--reactions', 'Show only reaction activity')
70
+ s.option('--mentions', 'Show only mentions')
71
+ s.option('--threads', 'Show only thread replies')
72
+ s.option('-m, --show-messages', 'Show the message content for each activity')
73
+ s.option('--json', 'Output as JSON')
74
+ s.option('-w, --workspace', 'Specify workspace')
75
+ s.option('-v, --verbose', 'Show debug information')
76
+ s.option('-q, --quiet', 'Suppress output')
77
+ end
78
+
79
+ help.render
80
+ end
81
+
82
+ private
83
+
84
+ def activity_types
85
+ case @options[:filter]
86
+ when :reactions
87
+ 'message_reaction'
88
+ when :mentions
89
+ 'at_user,at_user_group,at_channel,at_everyone'
90
+ when :threads
91
+ 'thread_v2'
92
+ else
93
+ # All activity types that the Slack web UI uses
94
+ 'thread_v2,message_reaction,bot_dm_bundle,at_user,at_user_group,at_channel,at_everyone'
95
+ end
96
+ end
97
+
98
+ def display_activity(items, workspace)
99
+ return puts 'No activity found.' if items.empty?
100
+
101
+ items.each do |item|
102
+ display_activity_item(item, workspace)
103
+ end
104
+ end
105
+
106
+ def display_activity_item(item, workspace)
107
+ type = item.dig('item', 'type')
108
+ timestamp = format_activity_time(item['feed_ts'])
109
+
110
+ case type
111
+ when 'message_reaction'
112
+ display_reaction_activity(item, workspace, timestamp)
113
+ when 'at_user', 'at_user_group', 'at_channel', 'at_everyone'
114
+ display_mention_activity(item, workspace, timestamp)
115
+ when 'thread_v2'
116
+ display_thread_v2_activity(item, workspace, timestamp)
117
+ when 'bot_dm_bundle'
118
+ display_bot_dm_activity(item, workspace, timestamp)
119
+ else
120
+ # Unknown activity type - skip silently
121
+ end
122
+ end
123
+
124
+ def display_reaction_activity(item, workspace, timestamp)
125
+ reaction_data = item.dig('item', 'reaction')
126
+ message_data = item.dig('item', 'message')
127
+ return unless reaction_data && message_data
128
+
129
+ user_id = reaction_data['user']
130
+ username = resolve_user(workspace, user_id)
131
+ emoji_name = reaction_data['name']
132
+ emoji = runner.emoji_replacer.lookup_emoji(emoji_name) || ":#{emoji_name}:"
133
+ channel_id = message_data['channel']
134
+ channel = resolve_channel(workspace, channel_id)
135
+
136
+ puts "#{output.blue(timestamp)} #{output.bold(username)} reacted #{emoji} in #{channel}"
137
+
138
+ if @options[:show_messages]
139
+ message = fetch_message(workspace, channel_id, message_data['ts'])
140
+ display_message_preview(message, workspace)
141
+ end
142
+ end
143
+
144
+ def display_mention_activity(item, workspace, timestamp)
145
+ message_data = item.dig('item', 'message')
146
+ return unless message_data
147
+
148
+ user_id = message_data['author_user_id'] || message_data['user']
149
+ username = resolve_user(workspace, user_id)
150
+ channel_id = message_data['channel']
151
+ channel = resolve_channel(workspace, channel_id)
152
+
153
+ puts "#{output.blue(timestamp)} #{output.bold(username)} mentioned you in #{channel}"
154
+
155
+ if @options[:show_messages]
156
+ message = fetch_message(workspace, channel_id, message_data['ts'])
157
+ display_message_preview(message, workspace)
158
+ end
159
+ end
160
+
161
+ def display_thread_v2_activity(item, workspace, timestamp)
162
+ thread_entry = item.dig('item', 'bundle_info', 'payload', 'thread_entry')
163
+ return unless thread_entry
164
+
165
+ channel_id = thread_entry['channel_id']
166
+ channel = resolve_channel(workspace, channel_id)
167
+
168
+ puts "#{output.blue(timestamp)} Thread activity in #{channel}"
169
+
170
+ if @options[:show_messages] && thread_entry['thread_ts']
171
+ # Fetch the thread parent message
172
+ message = fetch_message(workspace, channel_id, thread_entry['thread_ts'])
173
+ display_message_preview(message, workspace)
174
+ end
175
+ end
176
+
177
+ def display_bot_dm_activity(item, workspace, timestamp)
178
+ message_data = item.dig('item', 'bundle_info', 'payload', 'message')
179
+ return unless message_data
180
+
181
+ channel_id = message_data['channel']
182
+ message_ts = message_data['ts']
183
+ channel = resolve_channel(workspace, channel_id)
184
+
185
+ puts "#{output.blue(timestamp)} Bot message in #{channel}"
186
+
187
+ # Always try to fetch and show the message content (or when --show-messages is enabled)
188
+ if @options[:show_messages]
189
+ message = fetch_message(workspace, channel_id, message_ts)
190
+ display_message_preview(message, workspace) if message
191
+ end
192
+ end
193
+
194
+ def resolve_user(workspace, user_id)
195
+ # Try cache first
196
+ cached = cache_store.get_user(workspace.name, user_id)
197
+ return cached if cached
198
+
199
+ # Fall back to user ID
200
+ user_id
201
+ end
202
+
203
+ def resolve_channel(workspace, channel_id)
204
+ # DMs and Group DMs - don't try to resolve
205
+ return 'DM' if channel_id.start_with?('D')
206
+ return 'Group DM' if channel_id.start_with?('G')
207
+
208
+ # Try cache first
209
+ cached = cache_store.get_channel_name(workspace.name, channel_id)
210
+ return "##{cached}" if cached
211
+
212
+ # Try to fetch from API
213
+ begin
214
+ api = runner.conversations_api(workspace.name)
215
+ response = api.info(channel: channel_id)
216
+ if response['ok'] && response['channel']
217
+ name = response['channel']['name']
218
+ cache_store.set_channel(workspace.name, name, channel_id)
219
+ return "##{name}"
220
+ end
221
+ rescue ApiError
222
+ # Fall back to channel ID if API call fails
223
+ end
224
+
225
+ # Fall back to channel ID
226
+ channel_id
227
+ end
228
+
229
+ def fetch_message(workspace, channel_id, message_ts)
230
+ api = runner.conversations_api(workspace.name)
231
+ # Fetch a window of messages around the target timestamp
232
+ # Use oldest (exclusive) and latest (inclusive) to create a window
233
+ oldest_ts = (message_ts.to_f - 1).to_s # 1 second before
234
+ latest_ts = (message_ts.to_f + 1).to_s # 1 second after
235
+
236
+ response = api.history(
237
+ channel: channel_id,
238
+ limit: 10,
239
+ oldest: oldest_ts,
240
+ latest: latest_ts
241
+ )
242
+
243
+ return nil unless response['ok'] && response['messages']&.any?
244
+
245
+ # Find the exact message by timestamp
246
+ response['messages'].find { |msg| msg['ts'] == message_ts }
247
+ rescue ApiError
248
+ nil
249
+ end
250
+
251
+ def display_message_preview(message, workspace)
252
+ return unless message
253
+
254
+ # Get username
255
+ username = if message['user']
256
+ resolve_user(workspace, message['user'])
257
+ elsif message['bot_id']
258
+ 'Bot'
259
+ else
260
+ 'Unknown'
261
+ end
262
+
263
+ # Get text and replace mentions
264
+ text = message['text'] || ''
265
+ text = '[No text]' if text.empty?
266
+ text = runner.mention_replacer.replace(text, workspace) unless text == '[No text]'
267
+
268
+ # Format as indented preview
269
+ lines = text.lines
270
+ first_line = lines.first&.strip || text
271
+ first_line = "#{first_line[0..100]}..." if first_line.length > 100
272
+
273
+ puts " └─ #{username}: #{first_line}"
274
+
275
+ # Show additional lines if any
276
+ if lines.length > 1
277
+ remaining = lines[1..2].map(&:strip).reject(&:empty?)
278
+ remaining.each do |line|
279
+ line = "#{line[0..100]}..." if line.length > 100
280
+ puts " #{line}"
281
+ end
282
+ puts " [#{lines.length - 3} more lines...]" if lines.length > 3
283
+ end
284
+ end
285
+
286
+ def format_activity_time(slack_timestamp)
287
+ time = Time.at(slack_timestamp.to_f)
288
+ time.strftime('%b %d %-I:%M %p') # e.g., "Jan 13 2:45 PM"
289
+ end
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Commands
5
+ class Base
6
+ attr_reader :runner, :options, :positional_args
7
+
8
+ def initialize(args, runner:)
9
+ @runner = runner
10
+ @options = default_options
11
+ @positional_args = parse_options(args)
12
+ end
13
+
14
+ def execute
15
+ raise NotImplementedError, "Subclass must implement #execute"
16
+ end
17
+
18
+ protected
19
+
20
+ # Convenience accessors
21
+ def output = runner.output
22
+ def config = runner.config
23
+ def cache_store = runner.cache_store
24
+ def preset_store = runner.preset_store
25
+ def token_store = runner.token_store
26
+ def api_client = runner.api_client
27
+
28
+ def default_options
29
+ {
30
+ workspace: nil,
31
+ all: false,
32
+ verbose: false,
33
+ quiet: false,
34
+ json: false,
35
+ width: default_width
36
+ }
37
+ end
38
+
39
+ # Default wrap width: 72 for interactive terminals, nil (no wrap) otherwise
40
+ def default_width
41
+ $stdout.tty? ? 72 : nil
42
+ end
43
+
44
+ def parse_options(args)
45
+ remaining = []
46
+ args = args.dup
47
+ @unknown_options = []
48
+
49
+ while args.any?
50
+ arg = args.shift
51
+
52
+ case arg
53
+ when "-w", "--workspace"
54
+ @options[:workspace] = args.shift
55
+ when "--width"
56
+ value = args.shift
57
+ @options[:width] = value == '0' ? nil : value.to_i
58
+ when "--no-wrap"
59
+ @options[:width] = nil
60
+ when "--all"
61
+ @options[:all] = true
62
+ when "-v", "--verbose"
63
+ @options[:verbose] = true
64
+ when "-q", "--quiet"
65
+ @options[:quiet] = true
66
+ when "--json"
67
+ @options[:json] = true
68
+ when "-h", "--help"
69
+ @options[:help] = true
70
+ when /^-/
71
+ # Let subclass handle unknown options
72
+ handle_option(arg, args, remaining)
73
+ else
74
+ remaining << arg
75
+ end
76
+ end
77
+
78
+ remaining
79
+ end
80
+
81
+ # Override in subclass to handle command-specific options
82
+ # Return true if option was handled, false to raise unknown option error
83
+ def handle_option(arg, args, remaining)
84
+ # By default, unknown options are errors
85
+ # Subclasses can override and return true to accept the option,
86
+ # or call super to get this error behavior
87
+ @unknown_options ||= []
88
+ @unknown_options << arg
89
+ false
90
+ end
91
+
92
+ # Check for unknown options and return error code if any were passed
93
+ def check_unknown_options
94
+ return nil if @unknown_options.nil? || @unknown_options.empty?
95
+
96
+ error("Unknown option: #{@unknown_options.first}")
97
+ error("Run with --help for available options.")
98
+ 1
99
+ end
100
+
101
+ # Returns true if there are unknown options
102
+ def has_unknown_options?
103
+ @unknown_options && @unknown_options.any?
104
+ end
105
+
106
+ # Get workspaces to operate on based on options
107
+ def target_workspaces
108
+ if @options[:all]
109
+ runner.all_workspaces
110
+ elsif @options[:workspace]
111
+ [runner.workspace(@options[:workspace])]
112
+ else
113
+ [runner.workspace]
114
+ end
115
+ end
116
+
117
+ # Show help if requested
118
+ def show_help?
119
+ @options[:help]
120
+ end
121
+
122
+ def show_help
123
+ output.puts help_text
124
+ 0
125
+ end
126
+
127
+ # Call at start of execute to check for help flag and unknown options
128
+ # Returns exit code if should return early, nil otherwise
129
+ def validate_options
130
+ return show_help if show_help?
131
+ return check_unknown_options if has_unknown_options?
132
+
133
+ nil
134
+ end
135
+
136
+ def help_text
137
+ "No help available for this command."
138
+ end
139
+
140
+ # Output helpers
141
+ def success(message)
142
+ output.success(message) unless @options[:quiet]
143
+ end
144
+
145
+ def info(message)
146
+ output.info(message) unless @options[:quiet]
147
+ end
148
+
149
+ def warn(message)
150
+ output.warn(message)
151
+ end
152
+
153
+ def error(message)
154
+ output.error(message)
155
+ end
156
+
157
+ def debug(message)
158
+ output.debug(message) if @options[:verbose]
159
+ end
160
+
161
+ def puts(message = "")
162
+ output.puts(message) unless @options[:quiet]
163
+ end
164
+
165
+ def print(message)
166
+ output.print(message) unless @options[:quiet]
167
+ end
168
+
169
+ # JSON output helper
170
+ def output_json(data)
171
+ output.puts(JSON.pretty_generate(data))
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../support/help_formatter"
4
+
5
+ module SlackCli
6
+ module Commands
7
+ class Cache < Base
8
+ def execute
9
+ result = validate_options
10
+ return result if result
11
+
12
+ case positional_args
13
+ in ["status" | "info"] | []
14
+ show_status
15
+ in ["clear", *rest]
16
+ clear_cache(rest.first)
17
+ in ["populate" | "refresh", *rest]
18
+ populate_cache(rest.first)
19
+ else
20
+ error("Unknown action: #{positional_args.first}")
21
+ 1
22
+ end
23
+ rescue ApiError => e
24
+ error("Failed: #{e.message}")
25
+ 1
26
+ end
27
+
28
+ protected
29
+
30
+ def help_text
31
+ help = Support::HelpFormatter.new("slk cache <action> [workspace]")
32
+ help.description("Manage user and channel cache.")
33
+
34
+ help.section("ACTIONS") do |s|
35
+ s.action("status", "Show cache status")
36
+ s.action("clear [ws]", "Clear cache (all or specific workspace)")
37
+ s.action("populate [ws]", "Populate user cache from API")
38
+ end
39
+
40
+ help.section("OPTIONS") do |s|
41
+ s.option("-w, --workspace", "Specify workspace")
42
+ s.option("-q, --quiet", "Suppress output")
43
+ end
44
+
45
+ help.render
46
+ end
47
+
48
+ private
49
+
50
+ def show_status
51
+ target_workspaces.each do |workspace|
52
+ if target_workspaces.size > 1
53
+ puts output.bold(workspace.name)
54
+ end
55
+
56
+ user_count = cache_store.user_cache_size(workspace.name)
57
+ channel_count = cache_store.channel_cache_size(workspace.name)
58
+
59
+ puts " Users cached: #{user_count}"
60
+ puts " Channels cached: #{channel_count}"
61
+
62
+ if cache_store.user_cache_file_exists?(workspace.name)
63
+ puts " User cache: #{output.green("present")}"
64
+ else
65
+ puts " User cache: #{output.yellow("not populated")}"
66
+ end
67
+ end
68
+
69
+ 0
70
+ end
71
+
72
+ def clear_cache(workspace_name)
73
+ if workspace_name
74
+ cache_store.clear_user_cache(workspace_name)
75
+ cache_store.clear_channel_cache(workspace_name)
76
+ success("Cleared cache for #{workspace_name}")
77
+ else
78
+ cache_store.clear_user_cache
79
+ cache_store.clear_channel_cache
80
+ success("Cleared all caches")
81
+ end
82
+
83
+ 0
84
+ end
85
+
86
+ def populate_cache(workspace_name)
87
+ workspaces = workspace_name ? [runner.workspace(workspace_name)] : target_workspaces
88
+
89
+ workspaces.each do |workspace|
90
+ puts "Populating user cache for #{workspace.name}..."
91
+
92
+ api = runner.users_api(workspace.name)
93
+ all_users = []
94
+ cursor = nil
95
+
96
+ loop do
97
+ response = api.list(cursor: cursor)
98
+ members = response["members"] || []
99
+ all_users.concat(members.map { |m| Models::User.from_api(m) })
100
+
101
+ cursor = response.dig("response_metadata", "next_cursor")
102
+ break if cursor.nil? || cursor.empty?
103
+
104
+ print "."
105
+ end
106
+
107
+ count = cache_store.populate_user_cache(workspace.name, all_users)
108
+ puts
109
+ success("Cached #{count} users for #{workspace.name}")
110
+ end
111
+
112
+ 0
113
+ end
114
+ end
115
+ end
116
+ end