@urugus/slack-cli 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 (116) hide show
  1. package/.claude/settings.local.json +53 -0
  2. package/.eslintrc.json +25 -0
  3. package/.github/dependabot.yml +18 -0
  4. package/.github/workflows/ci.yml +70 -0
  5. package/.github/workflows/pr-validation.yml +51 -0
  6. package/.prettierignore +11 -0
  7. package/.prettierrc +10 -0
  8. package/CLAUDE.md +16 -0
  9. package/README.md +161 -0
  10. package/dist/commands/channels.d.ts +3 -0
  11. package/dist/commands/channels.d.ts.map +1 -0
  12. package/dist/commands/channels.js +50 -0
  13. package/dist/commands/channels.js.map +1 -0
  14. package/dist/commands/config.d.ts +3 -0
  15. package/dist/commands/config.d.ts.map +1 -0
  16. package/dist/commands/config.js +87 -0
  17. package/dist/commands/config.js.map +1 -0
  18. package/dist/commands/history.d.ts +3 -0
  19. package/dist/commands/history.d.ts.map +1 -0
  20. package/dist/commands/history.js +79 -0
  21. package/dist/commands/history.js.map +1 -0
  22. package/dist/commands/send.d.ts +3 -0
  23. package/dist/commands/send.d.ts.map +1 -0
  24. package/dist/commands/send.js +85 -0
  25. package/dist/commands/send.js.map +1 -0
  26. package/dist/commands/unread.d.ts +3 -0
  27. package/dist/commands/unread.d.ts.map +1 -0
  28. package/dist/commands/unread.js +104 -0
  29. package/dist/commands/unread.js.map +1 -0
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +18 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/types/commands.d.ts +40 -0
  35. package/dist/types/commands.d.ts.map +1 -0
  36. package/dist/types/commands.js +3 -0
  37. package/dist/types/commands.js.map +1 -0
  38. package/dist/types/config.d.ts +18 -0
  39. package/dist/types/config.d.ts.map +1 -0
  40. package/dist/types/config.js +3 -0
  41. package/dist/types/config.js.map +1 -0
  42. package/dist/utils/channel-formatter.d.ts +16 -0
  43. package/dist/utils/channel-formatter.d.ts.map +1 -0
  44. package/dist/utils/channel-formatter.js +77 -0
  45. package/dist/utils/channel-formatter.js.map +1 -0
  46. package/dist/utils/client-factory.d.ts +6 -0
  47. package/dist/utils/client-factory.d.ts.map +1 -0
  48. package/dist/utils/client-factory.js +13 -0
  49. package/dist/utils/client-factory.js.map +1 -0
  50. package/dist/utils/command-wrapper.d.ts +6 -0
  51. package/dist/utils/command-wrapper.d.ts.map +1 -0
  52. package/dist/utils/command-wrapper.js +27 -0
  53. package/dist/utils/command-wrapper.js.map +1 -0
  54. package/dist/utils/config-helper.d.ts +8 -0
  55. package/dist/utils/config-helper.d.ts.map +1 -0
  56. package/dist/utils/config-helper.js +19 -0
  57. package/dist/utils/config-helper.js.map +1 -0
  58. package/dist/utils/config.d.ts +10 -0
  59. package/dist/utils/config.d.ts.map +1 -0
  60. package/dist/utils/config.js +94 -0
  61. package/dist/utils/config.js.map +1 -0
  62. package/dist/utils/constants.d.ts +32 -0
  63. package/dist/utils/constants.d.ts.map +1 -0
  64. package/dist/utils/constants.js +42 -0
  65. package/dist/utils/constants.js.map +1 -0
  66. package/dist/utils/date-utils.d.ts +3 -0
  67. package/dist/utils/date-utils.d.ts.map +1 -0
  68. package/dist/utils/date-utils.js +12 -0
  69. package/dist/utils/date-utils.js.map +1 -0
  70. package/dist/utils/error-utils.d.ts +2 -0
  71. package/dist/utils/error-utils.d.ts.map +1 -0
  72. package/dist/utils/error-utils.js +10 -0
  73. package/dist/utils/error-utils.js.map +1 -0
  74. package/dist/utils/errors.d.ts +17 -0
  75. package/dist/utils/errors.d.ts.map +1 -0
  76. package/dist/utils/errors.js +40 -0
  77. package/dist/utils/errors.js.map +1 -0
  78. package/dist/utils/profile-config.d.ts +21 -0
  79. package/dist/utils/profile-config.d.ts.map +1 -0
  80. package/dist/utils/profile-config.js +173 -0
  81. package/dist/utils/profile-config.js.map +1 -0
  82. package/dist/utils/slack-api-client.d.ts +74 -0
  83. package/dist/utils/slack-api-client.d.ts.map +1 -0
  84. package/dist/utils/slack-api-client.js +132 -0
  85. package/dist/utils/slack-api-client.js.map +1 -0
  86. package/package.json +56 -0
  87. package/src/commands/channels.ts +65 -0
  88. package/src/commands/config.ts +104 -0
  89. package/src/commands/history.ts +96 -0
  90. package/src/commands/send.ts +52 -0
  91. package/src/commands/unread.ts +118 -0
  92. package/src/index.ts +19 -0
  93. package/src/types/commands.ts +46 -0
  94. package/src/types/config.ts +20 -0
  95. package/src/utils/channel-formatter.ts +89 -0
  96. package/src/utils/client-factory.ts +10 -0
  97. package/src/utils/command-wrapper.ts +27 -0
  98. package/src/utils/config-helper.ts +21 -0
  99. package/src/utils/constants.ts +47 -0
  100. package/src/utils/date-utils.ts +8 -0
  101. package/src/utils/error-utils.ts +6 -0
  102. package/src/utils/errors.ts +37 -0
  103. package/src/utils/profile-config.ts +171 -0
  104. package/src/utils/slack-api-client.ts +218 -0
  105. package/tests/commands/channels.test.ts +250 -0
  106. package/tests/commands/config.test.ts +158 -0
  107. package/tests/commands/history.test.ts +250 -0
  108. package/tests/commands/send.test.ts +156 -0
  109. package/tests/commands/unread.test.ts +248 -0
  110. package/tests/test-utils.ts +28 -0
  111. package/tests/utils/config.test.ts +400 -0
  112. package/tests/utils/date-utils.test.ts +30 -0
  113. package/tests/utils/error-utils.test.ts +34 -0
  114. package/tests/utils/slack-api-client.test.ts +170 -0
  115. package/tsconfig.json +22 -0
  116. package/vitest.config.ts +27 -0
@@ -0,0 +1,218 @@
1
+ import { WebClient, ChatPostMessageResponse } from '@slack/web-api';
2
+
3
+ export interface Channel {
4
+ id: string;
5
+ name: string;
6
+ is_channel?: boolean;
7
+ is_group?: boolean;
8
+ is_im?: boolean;
9
+ is_mpim?: boolean;
10
+ is_private: boolean;
11
+ created: number;
12
+ is_archived?: boolean;
13
+ is_general?: boolean;
14
+ unlinked?: number;
15
+ name_normalized?: string;
16
+ is_shared?: boolean;
17
+ is_ext_shared?: boolean;
18
+ is_org_shared?: boolean;
19
+ is_member?: boolean;
20
+ num_members?: number;
21
+ unread_count?: number;
22
+ unread_count_display?: number;
23
+ last_read?: string;
24
+ topic?: {
25
+ value: string;
26
+ creator?: string;
27
+ last_set?: number;
28
+ };
29
+ purpose?: {
30
+ value: string;
31
+ creator?: string;
32
+ last_set?: number;
33
+ };
34
+ }
35
+
36
+ export interface ListChannelsOptions {
37
+ types: string;
38
+ exclude_archived: boolean;
39
+ limit: number;
40
+ }
41
+
42
+ export interface HistoryOptions {
43
+ limit: number;
44
+ oldest?: string;
45
+ }
46
+
47
+ export interface Message {
48
+ type: string;
49
+ text?: string;
50
+ user?: string;
51
+ bot_id?: string;
52
+ ts: string;
53
+ thread_ts?: string;
54
+ attachments?: unknown[];
55
+ blocks?: unknown[];
56
+ }
57
+
58
+ export interface HistoryResult {
59
+ messages: Message[];
60
+ users: Map<string, string>;
61
+ }
62
+
63
+ export interface ChannelUnreadResult {
64
+ channel: Channel;
65
+ messages: Message[];
66
+ users: Map<string, string>;
67
+ }
68
+
69
+ export class SlackApiClient {
70
+ private client: WebClient;
71
+
72
+ constructor(token: string) {
73
+ this.client = new WebClient(token);
74
+ }
75
+
76
+ async sendMessage(channel: string, text: string): Promise<ChatPostMessageResponse> {
77
+ return await this.client.chat.postMessage({
78
+ channel,
79
+ text,
80
+ });
81
+ }
82
+
83
+ async listChannels(options: ListChannelsOptions): Promise<Channel[]> {
84
+ const response = await this.client.conversations.list({
85
+ types: options.types,
86
+ exclude_archived: options.exclude_archived,
87
+ limit: options.limit,
88
+ });
89
+
90
+ return response.channels as Channel[];
91
+ }
92
+
93
+ async getHistory(channel: string, options: HistoryOptions): Promise<HistoryResult> {
94
+ const response = await this.client.conversations.history({
95
+ channel,
96
+ limit: options.limit,
97
+ oldest: options.oldest,
98
+ });
99
+
100
+ const messages = response.messages as Message[];
101
+
102
+ // Get unique user IDs
103
+ const userIds = [...new Set(messages.filter((m) => m.user).map((m) => m.user!))];
104
+ const users = new Map<string, string>();
105
+
106
+ // Fetch user information
107
+ if (userIds.length > 0) {
108
+ for (const userId of userIds) {
109
+ try {
110
+ const userInfo = await this.client.users.info({ user: userId });
111
+ if (userInfo.user?.name) {
112
+ users.set(userId, userInfo.user.name);
113
+ }
114
+ } catch (error) {
115
+ // If we can't get user info, we'll use the ID
116
+ users.set(userId, userId);
117
+ }
118
+ }
119
+ }
120
+
121
+ return { messages, users };
122
+ }
123
+
124
+ async listUnreadChannels(): Promise<Channel[]> {
125
+ // Get all conversations the user is a member of
126
+ const response = await this.client.conversations.list({
127
+ types: 'public_channel,private_channel,im,mpim',
128
+ exclude_archived: true,
129
+ limit: 1000,
130
+ });
131
+
132
+ const channels = response.channels as Channel[];
133
+
134
+ // Get unread count for each channel
135
+ const channelsWithUnread = await Promise.all(
136
+ channels.map(async (channel) => {
137
+ try {
138
+ const info = await this.client.conversations.info({
139
+ channel: channel.id,
140
+ include_num_members: false,
141
+ });
142
+ const channelInfo = info.channel as any;
143
+ return {
144
+ ...channel,
145
+ unread_count: channelInfo.unread_count || 0,
146
+ unread_count_display: channelInfo.unread_count_display || 0,
147
+ last_read: channelInfo.last_read,
148
+ };
149
+ } catch {
150
+ return channel;
151
+ }
152
+ })
153
+ );
154
+
155
+ // Filter to only channels with unread messages
156
+ return channelsWithUnread.filter((channel) => (channel.unread_count_display || 0) > 0);
157
+ }
158
+
159
+ async getChannelUnread(channelNameOrId: string): Promise<ChannelUnreadResult> {
160
+ // First, find the channel
161
+ let channelId = channelNameOrId;
162
+ if (
163
+ !channelNameOrId.startsWith('C') &&
164
+ !channelNameOrId.startsWith('D') &&
165
+ !channelNameOrId.startsWith('G')
166
+ ) {
167
+ // It's a name, not an ID - need to find the ID
168
+ const channels = await this.listChannels({
169
+ types: 'public_channel,private_channel,im,mpim',
170
+ exclude_archived: true,
171
+ limit: 1000,
172
+ });
173
+ const channel = channels.find(
174
+ (c) => c.name === channelNameOrId || c.name === channelNameOrId.replace('#', '')
175
+ );
176
+ if (!channel) {
177
+ throw new Error('channel_not_found');
178
+ }
179
+ channelId = channel.id;
180
+ }
181
+
182
+ // Get channel info with unread count
183
+ const info = await this.client.conversations.info({
184
+ channel: channelId,
185
+ });
186
+ const channel = info.channel as any;
187
+
188
+ // Get unread messages
189
+ let messages: Message[] = [];
190
+ let users = new Map<string, string>();
191
+
192
+ if (channel.last_read && channel.unread_count > 0) {
193
+ const historyResult = await this.getHistory(channelId, {
194
+ limit: channel.unread_count,
195
+ oldest: channel.last_read,
196
+ });
197
+ messages = historyResult.messages;
198
+ users = historyResult.users;
199
+ }
200
+
201
+ return {
202
+ channel: {
203
+ ...channel,
204
+ unread_count: channel.unread_count || 0,
205
+ unread_count_display: channel.unread_count_display || 0,
206
+ },
207
+ messages,
208
+ users,
209
+ };
210
+ }
211
+ }
212
+
213
+ export const slackApiClient = {
214
+ listChannels: async (token: string, options: ListChannelsOptions): Promise<Channel[]> => {
215
+ const client = new SlackApiClient(token);
216
+ return client.listChannels(options);
217
+ },
218
+ };
@@ -0,0 +1,250 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { setupChannelsCommand } from '../../src/commands/channels';
3
+ import { SlackApiClient } from '../../src/utils/slack-api-client';
4
+ import { ProfileConfigManager } from '../../src/utils/profile-config';
5
+ import { setupMockConsole, createTestProgram, restoreMocks } from '../test-utils';
6
+ import { ERROR_MESSAGES } from '../../src/utils/constants';
7
+
8
+ vi.mock('../../src/utils/slack-api-client');
9
+ vi.mock('../../src/utils/profile-config');
10
+
11
+ describe('channels command', () => {
12
+ let program: any;
13
+ let mockSlackClient: SlackApiClient;
14
+ let mockConfigManager: ProfileConfigManager;
15
+ let mockConsole: any;
16
+
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+
20
+ mockConfigManager = new ProfileConfigManager();
21
+ vi.mocked(ProfileConfigManager).mockReturnValue(mockConfigManager);
22
+
23
+ mockSlackClient = new SlackApiClient('test-token');
24
+ vi.mocked(SlackApiClient).mockReturnValue(mockSlackClient);
25
+
26
+ mockConsole = setupMockConsole();
27
+ program = createTestProgram();
28
+ program.addCommand(setupChannelsCommand());
29
+ });
30
+
31
+ afterEach(() => {
32
+ restoreMocks();
33
+ });
34
+
35
+ const mockChannels = [
36
+ {
37
+ id: 'C1234567890',
38
+ name: 'general',
39
+ is_channel: true,
40
+ is_private: false,
41
+ num_members: 250,
42
+ created: 1579075200,
43
+ purpose: { value: 'Company announcements' }
44
+ },
45
+ {
46
+ id: 'C0987654321',
47
+ name: 'random',
48
+ is_channel: true,
49
+ is_private: false,
50
+ num_members: 145,
51
+ created: 1579075200,
52
+ purpose: { value: 'Random discussions' }
53
+ }
54
+ ];
55
+
56
+ describe('basic functionality', () => {
57
+ it('should list public channels by default', async () => {
58
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
59
+ token: 'test-token',
60
+ updatedAt: new Date().toISOString()
61
+ });
62
+ vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
63
+
64
+ await program.parseAsync(['node', 'slack-cli', 'channels']);
65
+
66
+ expect(mockSlackClient.listChannels).toHaveBeenCalledWith({
67
+ types: 'public_channel',
68
+ exclude_archived: true,
69
+ limit: 100
70
+ });
71
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('general'));
72
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('random'));
73
+ });
74
+
75
+ it('should show error when no token is configured', async () => {
76
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue(null);
77
+ vi.mocked(mockConfigManager.listProfiles).mockResolvedValue([]);
78
+
79
+ await program.parseAsync(['node', 'slack-cli', 'channels']);
80
+
81
+ expect(mockConsole.errorSpy).toHaveBeenCalledWith('✗ Error:', ERROR_MESSAGES.NO_CONFIG('default'));
82
+ expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
83
+ });
84
+ });
85
+
86
+ describe('channel type filtering', () => {
87
+ it('should list private channels when type is private', async () => {
88
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
89
+ token: 'test-token',
90
+ updatedAt: new Date().toISOString()
91
+ });
92
+ vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
93
+
94
+ await program.parseAsync(['node', 'slack-cli', 'channels', '--type', 'private']);
95
+
96
+ expect(mockSlackClient.listChannels).toHaveBeenCalledWith({
97
+ types: 'private_channel',
98
+ exclude_archived: true,
99
+ limit: 100
100
+ });
101
+ });
102
+
103
+ it('should list all channel types when type is all', async () => {
104
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
105
+ token: 'test-token',
106
+ updatedAt: new Date().toISOString()
107
+ });
108
+ vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
109
+
110
+ await program.parseAsync(['node', 'slack-cli', 'channels', '--type', 'all']);
111
+
112
+ expect(mockSlackClient.listChannels).toHaveBeenCalledWith({
113
+ types: 'public_channel,private_channel,mpim,im',
114
+ exclude_archived: true,
115
+ limit: 100
116
+ });
117
+ });
118
+
119
+ it('should list direct messages when type is im', async () => {
120
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
121
+ token: 'test-token',
122
+ updatedAt: new Date().toISOString()
123
+ });
124
+ vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
125
+
126
+ await program.parseAsync(['node', 'slack-cli', 'channels', '--type', 'im']);
127
+
128
+ expect(mockSlackClient.listChannels).toHaveBeenCalledWith({
129
+ types: 'im',
130
+ exclude_archived: true,
131
+ limit: 100
132
+ });
133
+ });
134
+ });
135
+
136
+ describe('output formatting', () => {
137
+ it('should output in table format by default', async () => {
138
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
139
+ token: 'test-token',
140
+ updatedAt: new Date().toISOString()
141
+ });
142
+ vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
143
+
144
+ await program.parseAsync(['node', 'slack-cli', 'channels']);
145
+
146
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Name'));
147
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Type'));
148
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Members'));
149
+ });
150
+
151
+ it('should output in simple format when specified', async () => {
152
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
153
+ token: 'test-token',
154
+ updatedAt: new Date().toISOString()
155
+ });
156
+ vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
157
+
158
+ await program.parseAsync(['node', 'slack-cli', 'channels', '--format', 'simple']);
159
+
160
+ expect(mockConsole.logSpy).toHaveBeenCalledWith('general');
161
+ expect(mockConsole.logSpy).toHaveBeenCalledWith('random');
162
+ });
163
+
164
+ it('should output in JSON format when specified', async () => {
165
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
166
+ token: 'test-token',
167
+ updatedAt: new Date().toISOString()
168
+ });
169
+ vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
170
+
171
+ await program.parseAsync(['node', 'slack-cli', 'channels', '--format', 'json']);
172
+
173
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('"name": "general"'));
174
+ });
175
+ });
176
+
177
+ describe('additional options', () => {
178
+ it('should include archived channels when flag is set', async () => {
179
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
180
+ token: 'test-token',
181
+ updatedAt: new Date().toISOString()
182
+ });
183
+ vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
184
+
185
+ await program.parseAsync(['node', 'slack-cli', 'channels', '--include-archived']);
186
+
187
+ expect(mockSlackClient.listChannels).toHaveBeenCalledWith({
188
+ types: 'public_channel',
189
+ exclude_archived: false,
190
+ limit: 100
191
+ });
192
+ });
193
+
194
+ it('should respect custom limit', async () => {
195
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
196
+ token: 'test-token',
197
+ updatedAt: new Date().toISOString()
198
+ });
199
+ vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
200
+
201
+ await program.parseAsync(['node', 'slack-cli', 'channels', '--limit', '50']);
202
+
203
+ expect(mockSlackClient.listChannels).toHaveBeenCalledWith({
204
+ types: 'public_channel',
205
+ exclude_archived: true,
206
+ limit: 50
207
+ });
208
+ });
209
+
210
+ it('should use specified profile', async () => {
211
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
212
+ token: 'work-token',
213
+ updatedAt: new Date().toISOString()
214
+ });
215
+ vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
216
+
217
+ await program.parseAsync(['node', 'slack-cli', 'channels', '--profile', 'work']);
218
+
219
+ expect(mockConfigManager.getConfig).toHaveBeenCalledWith('work');
220
+ expect(SlackApiClient).toHaveBeenCalledWith('work-token');
221
+ });
222
+ });
223
+
224
+ describe('error handling', () => {
225
+ it('should handle API errors gracefully', async () => {
226
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
227
+ token: 'test-token',
228
+ updatedAt: new Date().toISOString()
229
+ });
230
+ vi.mocked(mockSlackClient.listChannels).mockRejectedValue(new Error('API Error'));
231
+
232
+ await program.parseAsync(['node', 'slack-cli', 'channels']);
233
+
234
+ expect(mockConsole.errorSpy).toHaveBeenCalledWith('✗ Error:', 'API Error');
235
+ expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
236
+ });
237
+
238
+ it('should show message when no channels found', async () => {
239
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
240
+ token: 'test-token',
241
+ updatedAt: new Date().toISOString()
242
+ });
243
+ vi.mocked(mockSlackClient.listChannels).mockResolvedValue([]);
244
+
245
+ await program.parseAsync(['node', 'slack-cli', 'channels']);
246
+
247
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(ERROR_MESSAGES.NO_CHANNELS_FOUND);
248
+ });
249
+ });
250
+ });
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { setupConfigCommand } from '../../src/commands/config';
3
+ import { ProfileConfigManager } from '../../src/utils/profile-config';
4
+ import type { Config, Profile } from '../../src/types/config';
5
+ import { setupMockConsole, createTestProgram, restoreMocks } from '../test-utils';
6
+ import { SUCCESS_MESSAGES, ERROR_MESSAGES } from '../../src/utils/constants';
7
+
8
+ vi.mock('../../src/utils/profile-config');
9
+
10
+ describe('profile config command', () => {
11
+ let program: any;
12
+ let mockConfigManager: ProfileConfigManager;
13
+ let mockConsole: any;
14
+
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+
18
+ mockConfigManager = new ProfileConfigManager();
19
+ vi.mocked(ProfileConfigManager).mockReturnValue(mockConfigManager);
20
+
21
+ mockConsole = setupMockConsole();
22
+ program = createTestProgram();
23
+ program.addCommand(setupConfigCommand());
24
+ });
25
+
26
+ afterEach(() => {
27
+ restoreMocks();
28
+ });
29
+
30
+ describe('config set with profile', () => {
31
+ it('should set token for specified profile', async () => {
32
+ vi.mocked(mockConfigManager.setToken).mockResolvedValue(undefined);
33
+
34
+ await program.parseAsync(['node', 'slack-cli', 'config', 'set', '--token', 'test-token-123', '--profile', 'work']);
35
+
36
+ expect(mockConfigManager.setToken).toHaveBeenCalledWith('test-token-123', 'work');
37
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining(SUCCESS_MESSAGES.TOKEN_SAVED('work')));
38
+ });
39
+
40
+ it('should set token for default profile when no profile specified', async () => {
41
+ vi.mocked(mockConfigManager.setToken).mockResolvedValue(undefined);
42
+ vi.mocked(mockConfigManager.getCurrentProfile).mockResolvedValue('default');
43
+
44
+ await program.parseAsync(['node', 'slack-cli', 'config', 'set', '--token', 'test-token-123']);
45
+
46
+ expect(mockConfigManager.setToken).toHaveBeenCalledWith('test-token-123', undefined);
47
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Token saved successfully for profile "default"'));
48
+ });
49
+ });
50
+
51
+ describe('config get with profile', () => {
52
+ it('should display config for specified profile', async () => {
53
+ const mockConfig: Config = {
54
+ token: 'test-token-1234567890-abcdefghijklmnop',
55
+ updatedAt: '2025-06-21T10:00:00.000Z'
56
+ };
57
+
58
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue(mockConfig);
59
+ vi.mocked(mockConfigManager.maskToken).mockReturnValue('test-****-****-mnop');
60
+
61
+ await program.parseAsync(['node', 'slack-cli', 'config', 'get', '--profile', 'work']);
62
+
63
+ expect(mockConfigManager.getConfig).toHaveBeenCalledWith('work');
64
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Configuration for profile "work":'));
65
+ });
66
+ });
67
+
68
+ describe('config profiles', () => {
69
+ it('should list all profiles', async () => {
70
+ const mockProfiles: Profile[] = [
71
+ {
72
+ name: 'default',
73
+ config: {
74
+ token: 'default-token',
75
+ updatedAt: '2025-06-21T10:00:00.000Z'
76
+ }
77
+ },
78
+ {
79
+ name: 'work',
80
+ config: {
81
+ token: 'work-token',
82
+ updatedAt: '2025-06-21T11:00:00.000Z'
83
+ }
84
+ }
85
+ ];
86
+
87
+ vi.mocked(mockConfigManager.listProfiles).mockResolvedValue(mockProfiles);
88
+ vi.mocked(mockConfigManager.getCurrentProfile).mockResolvedValue('work');
89
+
90
+ await program.parseAsync(['node', 'slack-cli', 'config', 'profiles']);
91
+
92
+ expect(mockConfigManager.listProfiles).toHaveBeenCalled();
93
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Available profiles:'));
94
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('default'));
95
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('* work')); // current profile marked
96
+ });
97
+
98
+ it('should show message when no profiles exist', async () => {
99
+ vi.mocked(mockConfigManager.listProfiles).mockResolvedValue([]);
100
+
101
+ await program.parseAsync(['node', 'slack-cli', 'config', 'profiles']);
102
+
103
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('No profiles found'));
104
+ });
105
+ });
106
+
107
+ describe('config use', () => {
108
+ it('should switch default profile', async () => {
109
+ vi.mocked(mockConfigManager.useProfile).mockResolvedValue(undefined);
110
+
111
+ await program.parseAsync(['node', 'slack-cli', 'config', 'use', 'work']);
112
+
113
+ expect(mockConfigManager.useProfile).toHaveBeenCalledWith('work');
114
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Switched to profile "work"'));
115
+ });
116
+
117
+ it('should show error when profile does not exist', async () => {
118
+ vi.mocked(mockConfigManager.useProfile).mockRejectedValue(new Error('Profile "nonexistent" does not exist'));
119
+
120
+ await program.parseAsync(['node', 'slack-cli', 'config', 'use', 'nonexistent']);
121
+
122
+ expect(mockConsole.errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error:'), expect.any(String));
123
+ expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
124
+ });
125
+ });
126
+
127
+ describe('config current', () => {
128
+ it('should show current active profile', async () => {
129
+ vi.mocked(mockConfigManager.getCurrentProfile).mockResolvedValue('work');
130
+
131
+ await program.parseAsync(['node', 'slack-cli', 'config', 'current']);
132
+
133
+ expect(mockConfigManager.getCurrentProfile).toHaveBeenCalled();
134
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Current profile: work'));
135
+ });
136
+ });
137
+
138
+ describe('config clear with profile', () => {
139
+ it('should clear specific profile', async () => {
140
+ vi.mocked(mockConfigManager.clearConfig).mockResolvedValue(undefined);
141
+
142
+ await program.parseAsync(['node', 'slack-cli', 'config', 'clear', '--profile', 'work']);
143
+
144
+ expect(mockConfigManager.clearConfig).toHaveBeenCalledWith('work');
145
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Profile "work" cleared successfully'));
146
+ });
147
+
148
+ it('should clear current profile when no profile specified', async () => {
149
+ vi.mocked(mockConfigManager.clearConfig).mockResolvedValue(undefined);
150
+ vi.mocked(mockConfigManager.getCurrentProfile).mockResolvedValue('default');
151
+
152
+ await program.parseAsync(['node', 'slack-cli', 'config', 'clear']);
153
+
154
+ expect(mockConfigManager.clearConfig).toHaveBeenCalledWith(undefined);
155
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Profile "default" cleared successfully'));
156
+ });
157
+ });
158
+ });