@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.
- package/README.md +45 -0
- package/dist/commands/history-display.d.ts +5 -1
- package/dist/commands/history-display.d.ts.map +1 -1
- package/dist/commands/history-display.js +3 -3
- package/dist/commands/history-display.js.map +1 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +28 -11
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/search.d.ts +3 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +51 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/types/commands.d.ts +10 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/utils/constants.d.ts +5 -0
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +5 -0
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/formatters/search-formatters.d.ts +10 -0
- package/dist/utils/formatters/search-formatters.d.ts.map +1 -0
- package/dist/utils/formatters/search-formatters.js +91 -0
- package/dist/utils/formatters/search-formatters.js.map +1 -0
- package/dist/utils/slack-api-client.d.ts +5 -0
- package/dist/utils/slack-api-client.d.ts.map +1 -1
- package/dist/utils/slack-api-client.js +8 -0
- package/dist/utils/slack-api-client.js.map +1 -1
- package/dist/utils/slack-operations/index.d.ts +1 -0
- package/dist/utils/slack-operations/index.d.ts.map +1 -1
- package/dist/utils/slack-operations/index.js +3 -1
- package/dist/utils/slack-operations/index.js.map +1 -1
- package/dist/utils/slack-operations/message-operations.d.ts +1 -0
- package/dist/utils/slack-operations/message-operations.d.ts.map +1 -1
- package/dist/utils/slack-operations/message-operations.js +21 -0
- package/dist/utils/slack-operations/message-operations.js.map +1 -1
- package/dist/utils/slack-operations/search-operations.d.ts +29 -0
- package/dist/utils/slack-operations/search-operations.d.ts.map +1 -0
- package/dist/utils/slack-operations/search-operations.js +37 -0
- package/dist/utils/slack-operations/search-operations.js.map +1 -0
- package/dist/utils/validators.d.ts +16 -0
- package/dist/utils/validators.d.ts.map +1 -1
- package/dist/utils/validators.js +50 -0
- package/dist/utils/validators.js.map +1 -1
- package/package.json +5 -2
- package/.claude/settings.local.json +0 -75
- package/.github/dependabot.yml +0 -18
- package/.github/workflows/ci.yml +0 -70
- package/.github/workflows/pr-validation.yml +0 -41
- package/.prettierignore +0 -11
- package/.prettierrc +0 -10
- package/CHANGELOG.md +0 -61
- package/CLAUDE.md +0 -16
- package/eslint.config.js +0 -38
- package/src/commands/channels.ts +0 -50
- package/src/commands/config-subcommands.ts +0 -63
- package/src/commands/config.ts +0 -50
- package/src/commands/history-display.ts +0 -19
- package/src/commands/history-validators.ts +0 -46
- package/src/commands/history.ts +0 -61
- package/src/commands/scheduled.ts +0 -71
- package/src/commands/send.ts +0 -69
- package/src/commands/unread.ts +0 -122
- package/src/index.ts +0 -27
- package/src/types/commands.ts +0 -58
- package/src/types/config.ts +0 -20
- package/src/utils/channel-formatter.ts +0 -45
- package/src/utils/channel-resolver.ts +0 -82
- package/src/utils/client-factory.ts +0 -10
- package/src/utils/command-wrapper.ts +0 -27
- package/src/utils/config/config-file-manager.ts +0 -56
- package/src/utils/config/profile-manager.ts +0 -79
- package/src/utils/config/token-crypto-service.ts +0 -80
- package/src/utils/config-helper.ts +0 -21
- package/src/utils/constants.ts +0 -78
- package/src/utils/date-utils.ts +0 -8
- package/src/utils/error-utils.ts +0 -6
- package/src/utils/errors.ts +0 -33
- package/src/utils/format-utils.ts +0 -9
- package/src/utils/formatters/base-formatter.ts +0 -34
- package/src/utils/formatters/channel-formatters.ts +0 -71
- package/src/utils/formatters/channels-list-formatters.ts +0 -55
- package/src/utils/formatters/history-formatters.ts +0 -123
- package/src/utils/formatters/message-formatters.ts +0 -85
- package/src/utils/mention-utils.ts +0 -47
- package/src/utils/option-parsers.ts +0 -100
- package/src/utils/profile-config.ts +0 -161
- package/src/utils/schedule-utils.ts +0 -41
- package/src/utils/slack-api-client.ts +0 -135
- package/src/utils/slack-operations/base-client.ts +0 -30
- package/src/utils/slack-operations/channel-operations.ts +0 -161
- package/src/utils/slack-operations/index.ts +0 -3
- package/src/utils/slack-operations/message-operations.ts +0 -176
- package/src/utils/slack-patterns.ts +0 -9
- package/src/utils/token-utils.ts +0 -17
- package/src/utils/validators.ts +0 -263
- package/tests/commands/channels.test.ts +0 -250
- package/tests/commands/config.test.ts +0 -158
- package/tests/commands/history.test.ts +0 -403
- package/tests/commands/scheduled.test.ts +0 -131
- package/tests/commands/send.test.ts +0 -414
- package/tests/commands/unread.test.ts +0 -492
- package/tests/index.test.ts +0 -40
- package/tests/test-utils.ts +0 -28
- package/tests/utils/channel-resolver.test.ts +0 -161
- package/tests/utils/config/config-file-manager.test.ts +0 -118
- package/tests/utils/config/profile-manager.test.ts +0 -266
- package/tests/utils/config/token-crypto-service.test.ts +0 -98
- package/tests/utils/config.test.ts +0 -400
- package/tests/utils/date-utils.test.ts +0 -30
- package/tests/utils/error-utils.test.ts +0 -34
- package/tests/utils/format-utils.test.ts +0 -61
- package/tests/utils/mention-utils.test.ts +0 -100
- package/tests/utils/option-parsers.test.ts +0 -173
- package/tests/utils/profile-config.test.ts +0 -282
- package/tests/utils/schedule-utils.test.ts +0 -63
- package/tests/utils/slack-api-client.test.ts +0 -313
- package/tests/utils/slack-operations/channel-operations.test.ts +0 -248
- package/tests/utils/slack-operations/message-operations.test.ts +0 -163
- package/tests/utils/token-utils.test.ts +0 -33
- package/tests/utils/validators.test.ts +0 -307
- package/tsconfig.json +0 -22
- 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
|
-
}
|
package/src/utils/constants.ts
DELETED
|
@@ -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';
|
package/src/utils/date-utils.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/error-utils.ts
DELETED
package/src/utils/errors.ts
DELETED
|
@@ -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
|
-
}
|