@urugus/slack-cli 0.2.12 → 0.3.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 (123) hide show
  1. package/README.md +45 -0
  2. package/dist/commands/history-display.d.ts +5 -1
  3. package/dist/commands/history-display.d.ts.map +1 -1
  4. package/dist/commands/history-display.js +3 -3
  5. package/dist/commands/history-display.js.map +1 -1
  6. package/dist/commands/history.d.ts.map +1 -1
  7. package/dist/commands/history.js +28 -11
  8. package/dist/commands/history.js.map +1 -1
  9. package/dist/commands/search.d.ts +3 -0
  10. package/dist/commands/search.d.ts.map +1 -0
  11. package/dist/commands/search.js +51 -0
  12. package/dist/commands/search.js.map +1 -0
  13. package/dist/index.js +2 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/types/commands.d.ts +10 -0
  16. package/dist/types/commands.d.ts.map +1 -1
  17. package/dist/utils/constants.d.ts +5 -0
  18. package/dist/utils/constants.d.ts.map +1 -1
  19. package/dist/utils/constants.js +5 -0
  20. package/dist/utils/constants.js.map +1 -1
  21. package/dist/utils/formatters/search-formatters.d.ts +10 -0
  22. package/dist/utils/formatters/search-formatters.d.ts.map +1 -0
  23. package/dist/utils/formatters/search-formatters.js +91 -0
  24. package/dist/utils/formatters/search-formatters.js.map +1 -0
  25. package/dist/utils/slack-api-client.d.ts +5 -0
  26. package/dist/utils/slack-api-client.d.ts.map +1 -1
  27. package/dist/utils/slack-api-client.js +8 -0
  28. package/dist/utils/slack-api-client.js.map +1 -1
  29. package/dist/utils/slack-operations/index.d.ts +1 -0
  30. package/dist/utils/slack-operations/index.d.ts.map +1 -1
  31. package/dist/utils/slack-operations/index.js +3 -1
  32. package/dist/utils/slack-operations/index.js.map +1 -1
  33. package/dist/utils/slack-operations/message-operations.d.ts +1 -0
  34. package/dist/utils/slack-operations/message-operations.d.ts.map +1 -1
  35. package/dist/utils/slack-operations/message-operations.js +21 -0
  36. package/dist/utils/slack-operations/message-operations.js.map +1 -1
  37. package/dist/utils/slack-operations/search-operations.d.ts +29 -0
  38. package/dist/utils/slack-operations/search-operations.d.ts.map +1 -0
  39. package/dist/utils/slack-operations/search-operations.js +37 -0
  40. package/dist/utils/slack-operations/search-operations.js.map +1 -0
  41. package/dist/utils/validators.d.ts +16 -0
  42. package/dist/utils/validators.d.ts.map +1 -1
  43. package/dist/utils/validators.js +50 -0
  44. package/dist/utils/validators.js.map +1 -1
  45. package/package.json +5 -2
  46. package/.claude/settings.local.json +0 -75
  47. package/.github/dependabot.yml +0 -18
  48. package/.github/workflows/ci.yml +0 -70
  49. package/.github/workflows/pr-validation.yml +0 -41
  50. package/.prettierignore +0 -11
  51. package/.prettierrc +0 -10
  52. package/CHANGELOG.md +0 -61
  53. package/CLAUDE.md +0 -16
  54. package/eslint.config.js +0 -38
  55. package/src/commands/channels.ts +0 -50
  56. package/src/commands/config-subcommands.ts +0 -63
  57. package/src/commands/config.ts +0 -50
  58. package/src/commands/history-display.ts +0 -19
  59. package/src/commands/history-validators.ts +0 -46
  60. package/src/commands/history.ts +0 -61
  61. package/src/commands/scheduled.ts +0 -71
  62. package/src/commands/send.ts +0 -69
  63. package/src/commands/unread.ts +0 -122
  64. package/src/index.ts +0 -27
  65. package/src/types/commands.ts +0 -58
  66. package/src/types/config.ts +0 -20
  67. package/src/utils/channel-formatter.ts +0 -45
  68. package/src/utils/channel-resolver.ts +0 -82
  69. package/src/utils/client-factory.ts +0 -10
  70. package/src/utils/command-wrapper.ts +0 -27
  71. package/src/utils/config/config-file-manager.ts +0 -56
  72. package/src/utils/config/profile-manager.ts +0 -79
  73. package/src/utils/config/token-crypto-service.ts +0 -80
  74. package/src/utils/config-helper.ts +0 -21
  75. package/src/utils/constants.ts +0 -78
  76. package/src/utils/date-utils.ts +0 -8
  77. package/src/utils/error-utils.ts +0 -6
  78. package/src/utils/errors.ts +0 -33
  79. package/src/utils/format-utils.ts +0 -9
  80. package/src/utils/formatters/base-formatter.ts +0 -34
  81. package/src/utils/formatters/channel-formatters.ts +0 -71
  82. package/src/utils/formatters/channels-list-formatters.ts +0 -55
  83. package/src/utils/formatters/history-formatters.ts +0 -123
  84. package/src/utils/formatters/message-formatters.ts +0 -85
  85. package/src/utils/mention-utils.ts +0 -47
  86. package/src/utils/option-parsers.ts +0 -100
  87. package/src/utils/profile-config.ts +0 -161
  88. package/src/utils/schedule-utils.ts +0 -41
  89. package/src/utils/slack-api-client.ts +0 -135
  90. package/src/utils/slack-operations/base-client.ts +0 -30
  91. package/src/utils/slack-operations/channel-operations.ts +0 -161
  92. package/src/utils/slack-operations/index.ts +0 -3
  93. package/src/utils/slack-operations/message-operations.ts +0 -176
  94. package/src/utils/slack-patterns.ts +0 -9
  95. package/src/utils/token-utils.ts +0 -17
  96. package/src/utils/validators.ts +0 -263
  97. package/tests/commands/channels.test.ts +0 -250
  98. package/tests/commands/config.test.ts +0 -158
  99. package/tests/commands/history.test.ts +0 -403
  100. package/tests/commands/scheduled.test.ts +0 -131
  101. package/tests/commands/send.test.ts +0 -414
  102. package/tests/commands/unread.test.ts +0 -492
  103. package/tests/index.test.ts +0 -40
  104. package/tests/test-utils.ts +0 -28
  105. package/tests/utils/channel-resolver.test.ts +0 -161
  106. package/tests/utils/config/config-file-manager.test.ts +0 -118
  107. package/tests/utils/config/profile-manager.test.ts +0 -266
  108. package/tests/utils/config/token-crypto-service.test.ts +0 -98
  109. package/tests/utils/config.test.ts +0 -400
  110. package/tests/utils/date-utils.test.ts +0 -30
  111. package/tests/utils/error-utils.test.ts +0 -34
  112. package/tests/utils/format-utils.test.ts +0 -61
  113. package/tests/utils/mention-utils.test.ts +0 -100
  114. package/tests/utils/option-parsers.test.ts +0 -173
  115. package/tests/utils/profile-config.test.ts +0 -282
  116. package/tests/utils/schedule-utils.test.ts +0 -63
  117. package/tests/utils/slack-api-client.test.ts +0 -313
  118. package/tests/utils/slack-operations/channel-operations.test.ts +0 -248
  119. package/tests/utils/slack-operations/message-operations.test.ts +0 -163
  120. package/tests/utils/token-utils.test.ts +0 -33
  121. package/tests/utils/validators.test.ts +0 -307
  122. package/tsconfig.json +0 -22
  123. package/vitest.config.ts +0 -27
@@ -1,45 +0,0 @@
1
- import { Channel } from './slack-api-client';
2
- import { formatUnixTimestamp } from './date-utils';
3
-
4
- export interface ChannelInfo {
5
- id: string;
6
- name: string;
7
- type: string;
8
- members: number;
9
- created: string;
10
- purpose: string;
11
- }
12
-
13
- export function mapChannelToInfo(channel: Channel): ChannelInfo {
14
- let type = 'unknown';
15
- if (channel.is_channel && !channel.is_private) type = 'public';
16
- else if (channel.is_group || (channel.is_channel && channel.is_private)) type = 'private';
17
- else if (channel.is_im) type = 'im';
18
- else if (channel.is_mpim) type = 'mpim';
19
-
20
- return {
21
- id: channel.id,
22
- name: channel.name || 'unnamed',
23
- type,
24
- members: channel.num_members || 0,
25
- created: formatUnixTimestamp(channel.created),
26
- purpose: channel.purpose?.value || '',
27
- };
28
- }
29
-
30
- export function formatChannelName(channelName?: string): string {
31
- if (!channelName) return '#unknown';
32
- return channelName.startsWith('#') ? channelName : `#${channelName}`;
33
- }
34
-
35
- export function getChannelTypes(type: string): string {
36
- const channelTypeMap: Record<string, string> = {
37
- public: 'public_channel',
38
- private: 'private_channel',
39
- im: 'im',
40
- mpim: 'mpim',
41
- all: 'public_channel,private_channel,mpim,im',
42
- };
43
-
44
- return channelTypeMap[type] || 'public_channel';
45
- }
@@ -1,82 +0,0 @@
1
- import { Channel } from './slack-api-client';
2
-
3
- export type GetChannelsFunction = () => Promise<Channel[]>;
4
-
5
- export class ChannelResolver {
6
- /**
7
- * Check if the given string is a channel ID
8
- */
9
- isChannelId(channelNameOrId: string): boolean {
10
- return /^[CDG][A-Z0-9]{8,}$/.test(channelNameOrId);
11
- }
12
-
13
- /**
14
- * Find a channel by name from the given list
15
- */
16
- findChannel(channelName: string, channels: Channel[]): Channel | undefined {
17
- return channels.find((c) => {
18
- // Direct name match
19
- if (c.name === channelName) return true;
20
- // Match without # prefix
21
- if (c.name === channelName.replace('#', '')) return true;
22
- // Case-insensitive match
23
- if (c.name?.toLowerCase() === channelName.toLowerCase()) return true;
24
- // Match with normalized name
25
- if (c.name_normalized === channelName) return true;
26
- return false;
27
- });
28
- }
29
-
30
- /**
31
- * Get similar channel names for suggestions
32
- */
33
- getSimilarChannels(channelName: string, channels: Channel[], limit = 5): string[] {
34
- return channels
35
- .filter((c) => c.name?.toLowerCase().includes(channelName.toLowerCase()))
36
- .slice(0, limit)
37
- .map((c) => c.name as string);
38
- }
39
-
40
- /**
41
- * Create an error with channel suggestions
42
- */
43
- resolveChannelError(channelName: string, channels: Channel[]): Error {
44
- const similarChannels = this.getSimilarChannels(channelName, channels);
45
-
46
- if (similarChannels.length > 0) {
47
- return new Error(
48
- `Channel '${channelName}' not found. Did you mean one of these? ${similarChannels.join(', ')}`
49
- );
50
- } else {
51
- return new Error(
52
- `Channel '${channelName}' not found. Make sure you are a member of this channel.`
53
- );
54
- }
55
- }
56
-
57
- /**
58
- * Resolve a channel name or ID to a channel ID
59
- */
60
- async resolveChannelId(
61
- channelNameOrId: string,
62
- getChannels: GetChannelsFunction
63
- ): Promise<string> {
64
- // If it's already an ID, return it
65
- if (this.isChannelId(channelNameOrId)) {
66
- return channelNameOrId;
67
- }
68
-
69
- // Otherwise, fetch channels and resolve the name
70
- const channels = await getChannels();
71
- const channel = this.findChannel(channelNameOrId, channels);
72
-
73
- if (!channel) {
74
- throw this.resolveChannelError(channelNameOrId, channels);
75
- }
76
-
77
- return channel.id;
78
- }
79
- }
80
-
81
- // Export a singleton instance
82
- export const channelResolver = new ChannelResolver();
@@ -1,10 +0,0 @@
1
- import { SlackApiClient } from './slack-api-client';
2
- import { getConfigOrThrow } from './config-helper';
3
-
4
- /**
5
- * Creates a SlackApiClient instance with configuration from the specified profile
6
- */
7
- export async function createSlackClient(profile?: string): Promise<SlackApiClient> {
8
- const config = await getConfigOrThrow(profile);
9
- return new SlackApiClient(config.token);
10
- }
@@ -1,27 +0,0 @@
1
- import chalk from 'chalk';
2
- import { extractErrorMessage } from './error-utils';
3
-
4
- export type CommandAction<T = unknown> = (options: T) => Promise<void> | void;
5
-
6
- export function wrapCommand<T = unknown>(action: CommandAction<T>): CommandAction<T> {
7
- return async (options: T) => {
8
- try {
9
- await action(options);
10
- } catch (error) {
11
- console.error(chalk.red('✗ Error:'), extractErrorMessage(error));
12
-
13
- if (process.env.NODE_ENV === 'development' && error instanceof Error) {
14
- console.error(chalk.gray(error.stack));
15
- }
16
-
17
- process.exit(1);
18
- }
19
- };
20
- }
21
-
22
- export async function getProfileName(
23
- configManager: { getCurrentProfile: () => Promise<string> },
24
- providedProfile?: string
25
- ): Promise<string> {
26
- return providedProfile || (await configManager.getCurrentProfile());
27
- }
@@ -1,56 +0,0 @@
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
- }
@@ -1,79 +0,0 @@
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
- }
@@ -1,80 +0,0 @@
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
- }
@@ -1,21 +0,0 @@
1
- import { ProfileConfigManager } from './profile-config';
2
- import { ConfigurationError } from './errors';
3
- import { ERROR_MESSAGES } from './constants';
4
-
5
- /**
6
- * Helper function to get configuration with proper error handling
7
- */
8
- export async function getConfigOrThrow(
9
- profile?: string,
10
- configManager: ProfileConfigManager = new ProfileConfigManager()
11
- ): Promise<{ token: string }> {
12
- const config = await configManager.getConfig(profile);
13
-
14
- if (!config) {
15
- const profiles = await configManager.listProfiles();
16
- const profileName = profile || profiles.find((p) => p.isDefault)?.name || 'default';
17
- throw new ConfigurationError(ERROR_MESSAGES.NO_CONFIG(profileName));
18
- }
19
-
20
- return config;
21
- }
@@ -1,78 +0,0 @@
1
- export const TOKEN_MASK_LENGTH = 4;
2
- export const TOKEN_MIN_LENGTH = 9;
3
- export const DEFAULT_PROFILE_NAME = 'default';
4
-
5
- export const ERROR_MESSAGES = {
6
- // Configuration errors
7
- NO_CONFIG: (profileName: string) =>
8
- `No configuration found for profile "${profileName}". Use "slack-cli config set --token <token> --profile ${profileName}" to set up.`,
9
- PROFILE_NOT_FOUND: (profileName: string) => `Profile "${profileName}" not found`,
10
- NO_PROFILES_FOUND: 'No profiles found. Use "slack-cli config set --token <token>" to create one.',
11
- INVALID_CONFIG_FORMAT: 'Invalid config file format',
12
-
13
- // Validation errors
14
- NO_MESSAGE_OR_FILE: 'You must specify either --message or --file',
15
- BOTH_MESSAGE_AND_FILE: 'Cannot use both --message and --file',
16
- INVALID_THREAD_TIMESTAMP: 'Invalid thread timestamp format',
17
- INVALID_SCHEDULE_AT:
18
- 'Invalid schedule time format. Use Unix timestamp (seconds) or ISO 8601 date-time',
19
- INVALID_SCHEDULE_AFTER: '--after must be a positive integer (minutes)',
20
- BOTH_SCHEDULE_OPTIONS: 'Cannot use both --at and --after',
21
- SCHEDULE_TIME_IN_PAST: 'Schedule time must be in the future',
22
-
23
- // API errors
24
- API_ERROR: (error: string) => `API Error: ${error}`,
25
- CHANNEL_NOT_FOUND: (channel: string) => `Channel not found: ${channel}`,
26
-
27
- // File errors
28
- FILE_READ_ERROR: (file: string, error: string) => `Error reading file ${file}: ${error}`,
29
- FILE_NOT_FOUND: (file: string) => `File not found: ${file}`,
30
-
31
- // Channels command errors
32
- NO_CHANNELS_FOUND: 'No channels found',
33
- ERROR_LISTING_CHANNELS: (error: string) => `Error listing channels: ${error}`,
34
- } as const;
35
-
36
- export const SUCCESS_MESSAGES = {
37
- TOKEN_SAVED: (profileName: string) => `Token saved successfully for profile "${profileName}"`,
38
- PROFILE_SWITCHED: (profileName: string) => `Switched to profile "${profileName}"`,
39
- PROFILE_CLEARED: (profileName: string) => `Profile "${profileName}" cleared successfully`,
40
- MESSAGE_SENT: (channel: string) => `Message sent successfully to #${channel}`,
41
- MESSAGE_SCHEDULED: (channel: string, postAtIso: string) =>
42
- `Message scheduled to #${channel} at ${postAtIso}`,
43
- } as const;
44
-
45
- // File and system constants
46
- export const FILE_PERMISSIONS = {
47
- CONFIG_FILE: 0o600, // Read/write for owner only
48
- };
49
-
50
- // API limits
51
- export const API_LIMITS = {
52
- MAX_MESSAGE_COUNT: 1000,
53
- MIN_MESSAGE_COUNT: 1,
54
- DEFAULT_MESSAGE_COUNT: 10,
55
- };
56
-
57
- // API Rate Limiting Configuration
58
- export const RATE_LIMIT = {
59
- CONCURRENT_REQUESTS: 3,
60
- BATCH_SIZE: 10,
61
- BATCH_DELAY_MS: 1000,
62
- RETRY_CONFIG: {
63
- retries: 3,
64
- factor: 2,
65
- minTimeout: 1000,
66
- maxTimeout: 30000,
67
- },
68
- };
69
-
70
- // Default values
71
- export const DEFAULTS = {
72
- HISTORY_LIMIT: 20,
73
- CHANNELS_LIMIT: 1000,
74
- UNREAD_DISPLAY_LIMIT: 50,
75
- };
76
-
77
- // Time formats
78
- export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
@@ -1,8 +0,0 @@
1
- export function formatUnixTimestamp(timestamp: number): string {
2
- return new Date(timestamp * 1000).toISOString().split('T')[0];
3
- }
4
-
5
- export function formatSlackTimestamp(slackTimestamp: string): string {
6
- const timestamp = parseFloat(slackTimestamp);
7
- return new Date(timestamp * 1000).toLocaleString();
8
- }
@@ -1,6 +0,0 @@
1
- export function extractErrorMessage(error: unknown): string {
2
- if (error instanceof Error) {
3
- return error.message;
4
- }
5
- return String(error);
6
- }
@@ -1,33 +0,0 @@
1
- export class SlackCliError extends Error {
2
- constructor(
3
- message: string,
4
- public code?: string
5
- ) {
6
- super(message);
7
- this.name = this.constructor.name;
8
- }
9
- }
10
-
11
- export class ConfigurationError extends SlackCliError {
12
- constructor(message: string) {
13
- super(message, 'CONFIGURATION_ERROR');
14
- }
15
- }
16
-
17
- export class ValidationError extends SlackCliError {
18
- constructor(message: string) {
19
- super(message, 'VALIDATION_ERROR');
20
- }
21
- }
22
-
23
- export class ApiError extends SlackCliError {
24
- constructor(message: string) {
25
- super(message, 'API_ERROR');
26
- }
27
- }
28
-
29
- export class FileError extends SlackCliError {
30
- constructor(message: string) {
31
- super(message, 'FILE_ERROR');
32
- }
33
- }
@@ -1,9 +0,0 @@
1
- import { USER_MENTION_PATTERN } from './slack-patterns';
2
-
3
- export function formatMessageWithMentions(message: string, users: Map<string, string>): string {
4
- // Replace <@USERID> mentions with @username
5
- return message.replace(USER_MENTION_PATTERN, (match, userId) => {
6
- const username = users.get(userId) || userId;
7
- return `@${username}`;
8
- });
9
- }
@@ -1,34 +0,0 @@
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,71 +0,0 @@
1
- import chalk from 'chalk';
2
- import { AbstractFormatter, JsonFormatter, createFormatterFactory } from './base-formatter';
3
- import { Channel } from '../slack-api-client';
4
- import { formatChannelName } from '../channel-formatter';
5
- import { formatSlackTimestamp } from '../date-utils';
6
-
7
- export interface ChannelFormatterOptions {
8
- channels: Channel[];
9
- countOnly?: boolean;
10
- }
11
-
12
- class ChannelTableFormatter extends AbstractFormatter<ChannelFormatterOptions> {
13
- format({ channels }: ChannelFormatterOptions): void {
14
- console.log(chalk.bold('Channel Unread Last Message'));
15
- console.log('─'.repeat(50));
16
-
17
- channels.forEach((channel) => {
18
- const channelName = formatChannelName(channel.name);
19
- const paddedName = channelName.padEnd(16);
20
- const count = (channel.unread_count || 0).toString().padEnd(6);
21
- const lastRead = channel.last_read ? formatSlackTimestamp(channel.last_read) : 'Unknown';
22
- console.log(`${paddedName} ${count} ${lastRead}`);
23
- });
24
- }
25
- }
26
-
27
- class ChannelSimpleFormatter extends AbstractFormatter<ChannelFormatterOptions> {
28
- format({ channels }: ChannelFormatterOptions): void {
29
- channels.forEach((channel) => {
30
- const channelName = formatChannelName(channel.name);
31
- console.log(`${channelName} (${channel.unread_count || 0})`);
32
- });
33
- }
34
- }
35
-
36
- class ChannelJsonFormatter extends JsonFormatter<ChannelFormatterOptions> {
37
- protected transform({ channels }: ChannelFormatterOptions) {
38
- return channels.map((channel) => ({
39
- channel: formatChannelName(channel.name),
40
- channelId: channel.id,
41
- unreadCount: channel.unread_count || 0,
42
- }));
43
- }
44
- }
45
-
46
- class ChannelCountFormatter extends AbstractFormatter<ChannelFormatterOptions> {
47
- format({ channels }: ChannelFormatterOptions): void {
48
- let totalUnread = 0;
49
- channels.forEach((channel) => {
50
- const count = channel.unread_count || 0;
51
- totalUnread += count;
52
- const channelName = formatChannelName(channel.name);
53
- console.log(`${channelName}: ${count}`);
54
- });
55
- console.log(chalk.bold(`Total: ${totalUnread} unread messages`));
56
- }
57
- }
58
-
59
- const channelFormatterFactory = createFormatterFactory<ChannelFormatterOptions>({
60
- table: new ChannelTableFormatter(),
61
- simple: new ChannelSimpleFormatter(),
62
- json: new ChannelJsonFormatter(),
63
- count: new ChannelCountFormatter(),
64
- });
65
-
66
- export function createChannelFormatter(format: string, countOnly: boolean) {
67
- if (countOnly) {
68
- return channelFormatterFactory.create('count');
69
- }
70
- return channelFormatterFactory.create(format);
71
- }