@urugus/slack-cli 0.2.1 → 0.2.3

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 (69) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/dist/commands/channels.d.ts.map +1 -1
  3. package/dist/commands/channels.js +7 -4
  4. package/dist/commands/channels.js.map +1 -1
  5. package/dist/commands/history-display.d.ts.map +1 -1
  6. package/dist/commands/history-display.js +3 -1
  7. package/dist/commands/history-display.js.map +1 -1
  8. package/dist/commands/unread.d.ts.map +1 -1
  9. package/dist/commands/unread.js +17 -17
  10. package/dist/commands/unread.js.map +1 -1
  11. package/dist/utils/config/config-file-manager.d.ts +13 -0
  12. package/dist/utils/config/config-file-manager.d.ts.map +1 -0
  13. package/dist/utils/config/config-file-manager.js +85 -0
  14. package/dist/utils/config/config-file-manager.js.map +1 -0
  15. package/dist/utils/config/profile-manager.d.ts +16 -0
  16. package/dist/utils/config/profile-manager.d.ts.map +1 -0
  17. package/dist/utils/config/profile-manager.js +64 -0
  18. package/dist/utils/config/profile-manager.js.map +1 -0
  19. package/dist/utils/config/token-crypto-service.d.ts +11 -0
  20. package/dist/utils/config/token-crypto-service.d.ts.map +1 -0
  21. package/dist/utils/config/token-crypto-service.js +111 -0
  22. package/dist/utils/config/token-crypto-service.js.map +1 -0
  23. package/dist/utils/format-utils.d.ts +2 -0
  24. package/dist/utils/format-utils.d.ts.map +1 -0
  25. package/dist/utils/format-utils.js +11 -0
  26. package/dist/utils/format-utils.js.map +1 -0
  27. package/dist/utils/formatters/base-formatter.d.ts +23 -0
  28. package/dist/utils/formatters/base-formatter.d.ts.map +1 -0
  29. package/dist/utils/formatters/base-formatter.js +26 -0
  30. package/dist/utils/formatters/base-formatter.js.map +1 -0
  31. package/dist/utils/formatters/channel-formatters.d.ts +4 -13
  32. package/dist/utils/formatters/channel-formatters.d.ts.map +1 -1
  33. package/dist/utils/formatters/channel-formatters.js +18 -26
  34. package/dist/utils/formatters/channel-formatters.js.map +1 -1
  35. package/dist/utils/formatters/channels-list-formatters.d.ts +3 -10
  36. package/dist/utils/formatters/channels-list-formatters.d.ts.map +1 -1
  37. package/dist/utils/formatters/channels-list-formatters.js +15 -22
  38. package/dist/utils/formatters/channels-list-formatters.js.map +1 -1
  39. package/dist/utils/formatters/message-formatters.d.ts +9 -0
  40. package/dist/utils/formatters/message-formatters.d.ts.map +1 -0
  41. package/dist/utils/formatters/message-formatters.js +72 -0
  42. package/dist/utils/formatters/message-formatters.js.map +1 -0
  43. package/dist/utils/option-parsers.d.ts +47 -0
  44. package/dist/utils/option-parsers.d.ts.map +1 -0
  45. package/dist/utils/option-parsers.js +75 -0
  46. package/dist/utils/option-parsers.js.map +1 -0
  47. package/dist/utils/profile-config-refactored.d.ts +20 -0
  48. package/dist/utils/profile-config-refactored.d.ts.map +1 -0
  49. package/dist/utils/profile-config-refactored.js +174 -0
  50. package/dist/utils/profile-config-refactored.js.map +1 -0
  51. package/package.json +1 -1
  52. package/src/commands/channels.ts +7 -4
  53. package/src/commands/history-display.ts +3 -1
  54. package/src/commands/unread.ts +18 -17
  55. package/src/utils/config/config-file-manager.ts +56 -0
  56. package/src/utils/config/profile-manager.ts +79 -0
  57. package/src/utils/config/token-crypto-service.ts +80 -0
  58. package/src/utils/format-utils.ts +7 -0
  59. package/src/utils/formatters/base-formatter.ts +34 -0
  60. package/src/utils/formatters/channel-formatters.ts +25 -23
  61. package/src/utils/formatters/channels-list-formatters.ts +27 -31
  62. package/src/utils/formatters/message-formatters.ts +85 -0
  63. package/src/utils/option-parsers.ts +100 -0
  64. package/src/utils/profile-config-refactored.ts +161 -0
  65. package/tests/commands/unread.test.ts +112 -0
  66. package/tests/utils/config/config-file-manager.test.ts +118 -0
  67. package/tests/utils/config/profile-manager.test.ts +266 -0
  68. package/tests/utils/config/token-crypto-service.test.ts +98 -0
  69. package/tests/utils/format-utils.test.ts +61 -0
@@ -5,6 +5,7 @@ import { ERROR_MESSAGES } from '../utils/constants';
5
5
  import { ChannelsOptions } from '../types/commands';
6
6
  import { mapChannelToInfo, getChannelTypes } from '../utils/channel-formatter';
7
7
  import { createChannelsListFormatter } from '../utils/formatters/channels-list-formatters';
8
+ import { parseFormat, parseLimit, parseBoolean } from '../utils/option-parsers';
8
9
 
9
10
  export function setupChannelsCommand(): Command {
10
11
  const channelsCommand = new Command('channels');
@@ -25,10 +26,11 @@ export function setupChannelsCommand(): Command {
25
26
  const types = getChannelTypes(options.type);
26
27
 
27
28
  // List channels
29
+ const limit = parseLimit(options.limit, 100);
28
30
  const channels = await client.listChannels({
29
31
  types,
30
- exclude_archived: !options.includeArchived,
31
- limit: parseInt(options.limit, 10),
32
+ exclude_archived: !parseBoolean(options.includeArchived),
33
+ limit: limit,
32
34
  });
33
35
 
34
36
  if (channels.length === 0) {
@@ -38,8 +40,9 @@ export function setupChannelsCommand(): Command {
38
40
 
39
41
  // Format and display channels
40
42
  const channelInfos = channels.map(mapChannelToInfo);
41
- const formatter = createChannelsListFormatter(options.format);
42
- formatter.format(channelInfos);
43
+ const format = parseFormat(options.format);
44
+ const formatter = createChannelsListFormatter(format);
45
+ formatter.format({ channels: channelInfos });
43
46
  })
44
47
  );
45
48
 
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { Message } from '../utils/slack-api-client';
3
3
  import { formatSlackTimestamp } from '../utils/date-utils';
4
+ import { formatMessageWithMentions } from '../utils/format-utils';
4
5
 
5
6
  export function displayHistoryResults(
6
7
  messages: Message[],
@@ -27,7 +28,8 @@ export function displayHistoryResults(
27
28
 
28
29
  console.log(chalk.gray(`[${timestamp}]`) + ' ' + chalk.cyan(author));
29
30
  if (message.text) {
30
- console.log(message.text);
31
+ const formattedText = formatMessageWithMentions(message.text, users);
32
+ console.log(formattedText);
31
33
  }
32
34
  console.log(''); // Empty line between messages
33
35
  });
@@ -4,30 +4,28 @@ import { createSlackClient } from '../utils/client-factory';
4
4
  import { SlackApiClient } from '../utils/slack-api-client';
5
5
  import { UnreadOptions } from '../types/commands';
6
6
  import chalk from 'chalk';
7
- import { formatSlackTimestamp } from '../utils/date-utils';
8
- import { formatChannelName } from '../utils/channel-formatter';
9
7
  import { createChannelFormatter } from '../utils/formatters/channel-formatters';
8
+ import { createMessageFormatter } from '../utils/formatters/message-formatters';
10
9
  import { DEFAULTS } from '../utils/constants';
10
+ import { parseLimit, parseFormat, parseBoolean } from '../utils/option-parsers';
11
11
 
12
12
  async function handleSpecificChannelUnread(
13
13
  client: SlackApiClient,
14
14
  options: UnreadOptions
15
15
  ): Promise<void> {
16
16
  const result = await client.getChannelUnread(options.channel!);
17
- const channelName = formatChannelName(result.channel.name);
18
17
 
19
- console.log(chalk.bold(`${channelName}: ${result.channel.unread_count || 0} unread messages`));
18
+ const format = parseFormat(options.format);
19
+ const countOnly = parseBoolean(options.countOnly);
20
20
 
21
- if (!options.countOnly && result.messages.length > 0) {
22
- console.log('');
23
- result.messages.forEach((message) => {
24
- const timestamp = formatSlackTimestamp(message.ts);
25
- const author = message.user ? result.users.get(message.user) || message.user : 'unknown';
26
- console.log(`${chalk.gray(timestamp)} ${chalk.cyan(author)}`);
27
- console.log(message.text || '(no text)');
28
- console.log('');
29
- });
30
- }
21
+ const formatter = createMessageFormatter(format);
22
+ formatter.format({
23
+ channel: result.channel,
24
+ messages: result.messages,
25
+ users: result.users,
26
+ countOnly: countOnly,
27
+ format: format,
28
+ });
31
29
  }
32
30
 
33
31
  async function handleAllChannelsUnread(
@@ -42,11 +40,14 @@ async function handleAllChannelsUnread(
42
40
  }
43
41
 
44
42
  // Apply limit
45
- const limit = parseInt(options.limit || DEFAULTS.UNREAD_DISPLAY_LIMIT.toString(), 10);
43
+ const limit = parseLimit(options.limit, DEFAULTS.UNREAD_DISPLAY_LIMIT);
46
44
  const displayChannels = channels.slice(0, limit);
47
45
 
48
- const formatter = createChannelFormatter(options.format || 'table', options.countOnly || false);
49
- formatter.format(displayChannels);
46
+ const format = parseFormat(options.format);
47
+ const countOnly = parseBoolean(options.countOnly);
48
+
49
+ const formatter = createChannelFormatter(format, countOnly);
50
+ formatter.format({ channels: displayChannels, countOnly: countOnly });
50
51
  }
51
52
 
52
53
  export function setupUnreadCommand(): Command {
@@ -0,0 +1,56 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+
5
+ export interface ConfigData {
6
+ profiles: Record<string, any>;
7
+ currentProfile: string;
8
+ }
9
+
10
+ export class ConfigFileManager {
11
+ private readonly configPath: string;
12
+
13
+ constructor() {
14
+ this.configPath = path.join(os.homedir(), '.slack-cli', 'config.json');
15
+ }
16
+
17
+ async read(): Promise<ConfigData> {
18
+ try {
19
+ await fs.access(this.configPath);
20
+ const data = await fs.readFile(this.configPath, 'utf-8');
21
+ try {
22
+ return JSON.parse(data);
23
+ } catch {
24
+ throw new Error('Invalid configuration file');
25
+ }
26
+ } catch (error: any) {
27
+ if (error.message === 'Invalid configuration file') {
28
+ throw error;
29
+ }
30
+ // File doesn't exist, return default config
31
+ return {
32
+ profiles: {},
33
+ currentProfile: 'default',
34
+ };
35
+ }
36
+ }
37
+
38
+ async write(data: ConfigData): Promise<void> {
39
+ const dir = path.dirname(this.configPath);
40
+ await fs.mkdir(dir, { recursive: true });
41
+ await fs.writeFile(this.configPath, JSON.stringify(data, null, 2), 'utf-8');
42
+ }
43
+
44
+ async exists(): Promise<boolean> {
45
+ try {
46
+ await fs.access(this.configPath);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ getConfigPath(): string {
54
+ return this.configPath;
55
+ }
56
+ }
@@ -0,0 +1,79 @@
1
+ import { Config } from '../../types/config';
2
+ import { ConfigFileManager, ConfigData } from './config-file-manager';
3
+ import { TokenCryptoService } from './token-crypto-service';
4
+
5
+ export class ProfileManager {
6
+ constructor(
7
+ private fileManager: ConfigFileManager,
8
+ private cryptoService: TokenCryptoService
9
+ ) {}
10
+
11
+ async getProfile(profileName: string): Promise<Config> {
12
+ const data = await this.fileManager.read();
13
+ const profile = data.profiles[profileName];
14
+
15
+ if (!profile) {
16
+ throw new Error(`Profile "${profileName}" not found`);
17
+ }
18
+
19
+ // Decrypt token if encrypted
20
+ const token = this.cryptoService.isEncrypted(profile.token)
21
+ ? this.cryptoService.decrypt(profile.token)
22
+ : profile.token;
23
+
24
+ return {
25
+ ...profile,
26
+ token,
27
+ };
28
+ }
29
+
30
+ async setProfile(profileName: string, config: Config): Promise<void> {
31
+ const data = await this.fileManager.read();
32
+
33
+ // Encrypt the token before saving
34
+ const encryptedConfig = {
35
+ ...config,
36
+ token: this.cryptoService.encrypt(config.token),
37
+ };
38
+
39
+ data.profiles[profileName] = encryptedConfig;
40
+ await this.fileManager.write(data);
41
+ }
42
+
43
+ async deleteProfile(profileName: string): Promise<void> {
44
+ const data = await this.fileManager.read();
45
+
46
+ if (!data.profiles[profileName]) {
47
+ throw new Error(`Profile "${profileName}" not found`);
48
+ }
49
+
50
+ delete data.profiles[profileName];
51
+ await this.fileManager.write(data);
52
+ }
53
+
54
+ async listProfiles(): Promise<string[]> {
55
+ const data = await this.fileManager.read();
56
+ return Object.keys(data.profiles);
57
+ }
58
+
59
+ async getCurrentProfile(): Promise<string> {
60
+ const data = await this.fileManager.read();
61
+ return data.currentProfile || 'default';
62
+ }
63
+
64
+ async setCurrentProfile(profileName: string): Promise<void> {
65
+ const data = await this.fileManager.read();
66
+
67
+ if (!data.profiles[profileName]) {
68
+ throw new Error(`Profile "${profileName}" not found`);
69
+ }
70
+
71
+ data.currentProfile = profileName;
72
+ await this.fileManager.write(data);
73
+ }
74
+
75
+ async profileExists(profileName: string): Promise<boolean> {
76
+ const data = await this.fileManager.read();
77
+ return profileName in data.profiles;
78
+ }
79
+ }
@@ -0,0 +1,80 @@
1
+ import * as crypto from 'crypto';
2
+
3
+ export class TokenCryptoService {
4
+ private readonly algorithm = 'aes-256-cbc';
5
+ private readonly keyLength = 32;
6
+ private readonly ivLength = 16;
7
+ private readonly separator = ':';
8
+
9
+ private deriveKey(): Buffer {
10
+ // Derive a consistent key from a fixed string
11
+ // In production, this should use a more secure method
12
+ const fixedSalt = 'slack-cli-salt-v1';
13
+ return crypto.pbkdf2Sync('slack-cli-key', fixedSalt, 100000, this.keyLength, 'sha256');
14
+ }
15
+
16
+ encrypt(token: string): string {
17
+ try {
18
+ const key = this.deriveKey();
19
+ const iv = crypto.randomBytes(this.ivLength);
20
+ const cipher = crypto.createCipheriv(this.algorithm, key, iv);
21
+
22
+ let encrypted = cipher.update(token, 'utf8', 'hex');
23
+ encrypted += cipher.final('hex');
24
+
25
+ // Combine IV and encrypted data
26
+ return iv.toString('hex') + this.separator + encrypted;
27
+ } catch (error) {
28
+ throw new Error('Failed to encrypt token');
29
+ }
30
+ }
31
+
32
+ decrypt(encryptedData: string): string {
33
+ try {
34
+ if (!encryptedData || !encryptedData.includes(this.separator)) {
35
+ throw new Error('Invalid encrypted data format');
36
+ }
37
+
38
+ const parts = encryptedData.split(this.separator);
39
+ if (parts.length !== 2) {
40
+ throw new Error('Invalid encrypted data format');
41
+ }
42
+
43
+ const iv = Buffer.from(parts[0], 'hex');
44
+ const encrypted = parts[1];
45
+
46
+ if (iv.length !== this.ivLength) {
47
+ throw new Error('Invalid IV length');
48
+ }
49
+
50
+ const key = this.deriveKey();
51
+ const decipher = crypto.createDecipheriv(this.algorithm, key, iv);
52
+
53
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
54
+ decrypted += decipher.final('utf8');
55
+
56
+ return decrypted;
57
+ } catch (error) {
58
+ throw new Error('Failed to decrypt token');
59
+ }
60
+ }
61
+
62
+ isEncrypted(value: string): boolean {
63
+ if (!value) return false;
64
+
65
+ // Check if the value has the expected format
66
+ const parts = value.split(this.separator);
67
+ if (parts.length !== 2) return false;
68
+
69
+ // Check if the IV part is valid hex and has correct length
70
+ try {
71
+ const ivHex = parts[0];
72
+ if (!/^[0-9a-fA-F]+$/.test(ivHex)) return false;
73
+ if (ivHex.length !== this.ivLength * 2) return false;
74
+
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,7 @@
1
+ export function formatMessageWithMentions(message: string, users: Map<string, string>): string {
2
+ // Replace <@USERID> mentions with @username
3
+ return message.replace(/<@([A-Z0-9]+)>/g, (match, userId) => {
4
+ const username = users.get(userId) || userId;
5
+ return `@${username}`;
6
+ });
7
+ }
@@ -0,0 +1,34 @@
1
+ export interface BaseFormatter<T> {
2
+ format(data: T): void;
3
+ }
4
+
5
+ export abstract class AbstractFormatter<T> implements BaseFormatter<T> {
6
+ abstract format(data: T): void;
7
+ }
8
+
9
+ export abstract class JsonFormatter<TInput, TOutput = any> extends AbstractFormatter<TInput> {
10
+ protected abstract transform(data: TInput): TOutput;
11
+
12
+ format(data: TInput): void {
13
+ console.log(JSON.stringify(this.transform(data), null, 2));
14
+ }
15
+ }
16
+
17
+ export interface FormatterMap<T> {
18
+ table: BaseFormatter<T>;
19
+ simple: BaseFormatter<T>;
20
+ json: BaseFormatter<T>;
21
+ [key: string]: BaseFormatter<T>;
22
+ }
23
+
24
+ export class FormatterFactory<T> {
25
+ constructor(private formatters: FormatterMap<T>) {}
26
+
27
+ create(format: string = 'table'): BaseFormatter<T> {
28
+ return this.formatters[format] || this.formatters.table;
29
+ }
30
+ }
31
+
32
+ export function createFormatterFactory<T>(formatters: FormatterMap<T>): FormatterFactory<T> {
33
+ return new FormatterFactory(formatters);
34
+ }
@@ -1,11 +1,16 @@
1
1
  import chalk from 'chalk';
2
- import { BaseFormatter } from './output-formatter';
2
+ import { AbstractFormatter, JsonFormatter, createFormatterFactory } from './base-formatter';
3
3
  import { Channel } from '../slack-api-client';
4
4
  import { formatChannelName } from '../channel-formatter';
5
5
  import { formatSlackTimestamp } from '../date-utils';
6
6
 
7
- export class ChannelTableFormatter extends BaseFormatter<Channel> {
8
- format(channels: Channel[]): void {
7
+ export interface ChannelFormatterOptions {
8
+ channels: Channel[];
9
+ countOnly?: boolean;
10
+ }
11
+
12
+ class ChannelTableFormatter extends AbstractFormatter<ChannelFormatterOptions> {
13
+ format({ channels }: ChannelFormatterOptions): void {
9
14
  console.log(chalk.bold('Channel Unread Last Message'));
10
15
  console.log('─'.repeat(50));
11
16
 
@@ -19,8 +24,8 @@ export class ChannelTableFormatter extends BaseFormatter<Channel> {
19
24
  }
20
25
  }
21
26
 
22
- export class ChannelSimpleFormatter extends BaseFormatter<Channel> {
23
- format(channels: Channel[]): void {
27
+ class ChannelSimpleFormatter extends AbstractFormatter<ChannelFormatterOptions> {
28
+ format({ channels }: ChannelFormatterOptions): void {
24
29
  channels.forEach((channel) => {
25
30
  const channelName = formatChannelName(channel.name);
26
31
  console.log(`${channelName} (${channel.unread_count || 0})`);
@@ -28,19 +33,18 @@ export class ChannelSimpleFormatter extends BaseFormatter<Channel> {
28
33
  }
29
34
  }
30
35
 
31
- export class ChannelJsonFormatter extends BaseFormatter<Channel> {
32
- format(channels: Channel[]): void {
33
- const output = channels.map((channel) => ({
36
+ class ChannelJsonFormatter extends JsonFormatter<ChannelFormatterOptions> {
37
+ protected transform({ channels }: ChannelFormatterOptions) {
38
+ return channels.map((channel) => ({
34
39
  channel: formatChannelName(channel.name),
35
40
  channelId: channel.id,
36
41
  unreadCount: channel.unread_count || 0,
37
42
  }));
38
- console.log(JSON.stringify(output, null, 2));
39
43
  }
40
44
  }
41
45
 
42
- export class ChannelCountFormatter extends BaseFormatter<Channel> {
43
- format(channels: Channel[]): void {
46
+ class ChannelCountFormatter extends AbstractFormatter<ChannelFormatterOptions> {
47
+ format({ channels }: ChannelFormatterOptions): void {
44
48
  let totalUnread = 0;
45
49
  channels.forEach((channel) => {
46
50
  const count = channel.unread_count || 0;
@@ -52,18 +56,16 @@ export class ChannelCountFormatter extends BaseFormatter<Channel> {
52
56
  }
53
57
  }
54
58
 
55
- export function createChannelFormatter(format: string, countOnly: boolean): BaseFormatter<Channel> {
56
- if (countOnly) {
57
- return new ChannelCountFormatter();
58
- }
59
+ const channelFormatterFactory = createFormatterFactory<ChannelFormatterOptions>({
60
+ table: new ChannelTableFormatter(),
61
+ simple: new ChannelSimpleFormatter(),
62
+ json: new ChannelJsonFormatter(),
63
+ count: new ChannelCountFormatter(),
64
+ });
59
65
 
60
- switch (format) {
61
- case 'json':
62
- return new ChannelJsonFormatter();
63
- case 'simple':
64
- return new ChannelSimpleFormatter();
65
- case 'table':
66
- default:
67
- return new ChannelTableFormatter();
66
+ export function createChannelFormatter(format: string, countOnly: boolean) {
67
+ if (countOnly) {
68
+ return channelFormatterFactory.create('count');
68
69
  }
70
+ return channelFormatterFactory.create(format);
69
71
  }
@@ -1,8 +1,12 @@
1
- import { BaseFormatter } from './output-formatter';
1
+ import { AbstractFormatter, JsonFormatter, createFormatterFactory } from './base-formatter';
2
2
  import { ChannelInfo } from '../channel-formatter';
3
3
 
4
- export class ChannelsTableFormatter extends BaseFormatter<ChannelInfo> {
5
- format(channels: ChannelInfo[]): void {
4
+ export interface ChannelsListFormatterOptions {
5
+ channels: ChannelInfo[];
6
+ }
7
+
8
+ class ChannelsTableFormatter extends AbstractFormatter<ChannelsListFormatterOptions> {
9
+ format({ channels }: ChannelsListFormatterOptions): void {
6
10
  // Print table header
7
11
  console.log('Name Type Members Created Description');
8
12
  console.log('─'.repeat(65));
@@ -21,39 +25,31 @@ export class ChannelsTableFormatter extends BaseFormatter<ChannelInfo> {
21
25
  }
22
26
  }
23
27
 
24
- export class ChannelsSimpleFormatter extends BaseFormatter<ChannelInfo> {
25
- format(channels: ChannelInfo[]): void {
28
+ class ChannelsSimpleFormatter extends AbstractFormatter<ChannelsListFormatterOptions> {
29
+ format({ channels }: ChannelsListFormatterOptions): void {
26
30
  channels.forEach((channel) => console.log(channel.name));
27
31
  }
28
32
  }
29
33
 
30
- export class ChannelsJsonFormatter extends BaseFormatter<ChannelInfo> {
31
- format(channels: ChannelInfo[]): void {
32
- console.log(
33
- JSON.stringify(
34
- channels.map((channel) => ({
35
- id: channel.id,
36
- name: channel.name,
37
- type: channel.type,
38
- members: channel.members,
39
- created: channel.created + 'T00:00:00Z',
40
- purpose: channel.purpose,
41
- })),
42
- null,
43
- 2
44
- )
45
- );
34
+ class ChannelsJsonFormatter extends JsonFormatter<ChannelsListFormatterOptions> {
35
+ protected transform({ channels }: ChannelsListFormatterOptions) {
36
+ return channels.map((channel) => ({
37
+ id: channel.id,
38
+ name: channel.name,
39
+ type: channel.type,
40
+ members: channel.members,
41
+ created: channel.created + 'T00:00:00Z',
42
+ purpose: channel.purpose,
43
+ }));
46
44
  }
47
45
  }
48
46
 
49
- export function createChannelsListFormatter(format: string): BaseFormatter<ChannelInfo> {
50
- switch (format) {
51
- case 'json':
52
- return new ChannelsJsonFormatter();
53
- case 'simple':
54
- return new ChannelsSimpleFormatter();
55
- case 'table':
56
- default:
57
- return new ChannelsTableFormatter();
58
- }
47
+ const channelsListFormatterFactory = createFormatterFactory<ChannelsListFormatterOptions>({
48
+ table: new ChannelsTableFormatter(),
49
+ simple: new ChannelsSimpleFormatter(),
50
+ json: new ChannelsJsonFormatter(),
51
+ });
52
+
53
+ export function createChannelsListFormatter(format: string) {
54
+ return channelsListFormatterFactory.create(format);
59
55
  }
@@ -0,0 +1,85 @@
1
+ import chalk from 'chalk';
2
+ import { AbstractFormatter, JsonFormatter, createFormatterFactory } from './base-formatter';
3
+ import { formatSlackTimestamp } from '../date-utils';
4
+ import { formatMessageWithMentions } from '../format-utils';
5
+ import { formatChannelName } from '../channel-formatter';
6
+
7
+ export interface MessageFormatterOptions {
8
+ channel: any;
9
+ messages: any[];
10
+ users: Map<string, string>;
11
+ countOnly: boolean;
12
+ format: string;
13
+ }
14
+
15
+ class TableMessageFormatter extends AbstractFormatter<MessageFormatterOptions> {
16
+ format(options: MessageFormatterOptions): void {
17
+ const { channel, messages, users, countOnly } = options;
18
+ const channelName = formatChannelName(channel.name);
19
+
20
+ console.log(chalk.bold(`${channelName}: ${channel.unread_count || 0} unread messages`));
21
+
22
+ if (!countOnly && messages.length > 0) {
23
+ console.log('');
24
+ messages.forEach((message) => {
25
+ const timestamp = formatSlackTimestamp(message.ts);
26
+ const author = message.user ? users.get(message.user) || message.user : 'unknown';
27
+ console.log(`${chalk.gray(timestamp)} ${chalk.cyan(author)}`);
28
+ const text = message.text ? formatMessageWithMentions(message.text, users) : '(no text)';
29
+ console.log(text);
30
+ console.log('');
31
+ });
32
+ }
33
+ }
34
+ }
35
+
36
+ class SimpleMessageFormatter extends AbstractFormatter<MessageFormatterOptions> {
37
+ format(options: MessageFormatterOptions): void {
38
+ const { channel, messages, users, countOnly } = options;
39
+ const channelName = formatChannelName(channel.name);
40
+
41
+ console.log(`${channelName} (${channel.unread_count || 0})`);
42
+
43
+ if (!countOnly && messages.length > 0) {
44
+ messages.forEach((message) => {
45
+ const timestamp = formatSlackTimestamp(message.ts);
46
+ const author = message.user ? users.get(message.user) || message.user : 'unknown';
47
+ const text = message.text ? formatMessageWithMentions(message.text, users) : '(no text)';
48
+ console.log(`[${timestamp}] ${author}: ${text}`);
49
+ });
50
+ }
51
+ }
52
+ }
53
+
54
+ class JsonMessageFormatter extends JsonFormatter<MessageFormatterOptions> {
55
+ protected transform(options: MessageFormatterOptions) {
56
+ const { channel, messages, users, countOnly } = options;
57
+ const channelName = formatChannelName(channel.name);
58
+
59
+ const output: any = {
60
+ channel: channelName,
61
+ channelId: channel.id,
62
+ unreadCount: channel.unread_count || 0,
63
+ };
64
+
65
+ if (!countOnly && messages.length > 0) {
66
+ output.messages = messages.map((message) => ({
67
+ timestamp: formatSlackTimestamp(message.ts),
68
+ author: message.user ? users.get(message.user) || message.user : 'unknown',
69
+ text: message.text || '(no text)',
70
+ }));
71
+ }
72
+
73
+ return output;
74
+ }
75
+ }
76
+
77
+ const messageFormatterFactory = createFormatterFactory<MessageFormatterOptions>({
78
+ table: new TableMessageFormatter(),
79
+ simple: new SimpleMessageFormatter(),
80
+ json: new JsonMessageFormatter(),
81
+ });
82
+
83
+ export function createMessageFormatter(format: string) {
84
+ return messageFormatterFactory.create(format);
85
+ }