@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.
- package/.claude/settings.local.json +53 -0
- package/.eslintrc.json +25 -0
- package/.github/dependabot.yml +18 -0
- package/.github/workflows/ci.yml +70 -0
- package/.github/workflows/pr-validation.yml +51 -0
- package/.prettierignore +11 -0
- package/.prettierrc +10 -0
- package/CLAUDE.md +16 -0
- package/README.md +161 -0
- package/dist/commands/channels.d.ts +3 -0
- package/dist/commands/channels.d.ts.map +1 -0
- package/dist/commands/channels.js +50 -0
- package/dist/commands/channels.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +87 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/history.d.ts +3 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +79 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/send.d.ts +3 -0
- package/dist/commands/send.d.ts.map +1 -0
- package/dist/commands/send.js +85 -0
- package/dist/commands/send.js.map +1 -0
- package/dist/commands/unread.d.ts +3 -0
- package/dist/commands/unread.d.ts.map +1 -0
- package/dist/commands/unread.js +104 -0
- package/dist/commands/unread.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/types/commands.d.ts +40 -0
- package/dist/types/commands.d.ts.map +1 -0
- package/dist/types/commands.js +3 -0
- package/dist/types/commands.js.map +1 -0
- package/dist/types/config.d.ts +18 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +3 -0
- package/dist/types/config.js.map +1 -0
- package/dist/utils/channel-formatter.d.ts +16 -0
- package/dist/utils/channel-formatter.d.ts.map +1 -0
- package/dist/utils/channel-formatter.js +77 -0
- package/dist/utils/channel-formatter.js.map +1 -0
- package/dist/utils/client-factory.d.ts +6 -0
- package/dist/utils/client-factory.d.ts.map +1 -0
- package/dist/utils/client-factory.js +13 -0
- package/dist/utils/client-factory.js.map +1 -0
- package/dist/utils/command-wrapper.d.ts +6 -0
- package/dist/utils/command-wrapper.d.ts.map +1 -0
- package/dist/utils/command-wrapper.js +27 -0
- package/dist/utils/command-wrapper.js.map +1 -0
- package/dist/utils/config-helper.d.ts +8 -0
- package/dist/utils/config-helper.d.ts.map +1 -0
- package/dist/utils/config-helper.js +19 -0
- package/dist/utils/config-helper.js.map +1 -0
- package/dist/utils/config.d.ts +10 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +94 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/constants.d.ts +32 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +42 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/date-utils.d.ts +3 -0
- package/dist/utils/date-utils.d.ts.map +1 -0
- package/dist/utils/date-utils.js +12 -0
- package/dist/utils/date-utils.js.map +1 -0
- package/dist/utils/error-utils.d.ts +2 -0
- package/dist/utils/error-utils.d.ts.map +1 -0
- package/dist/utils/error-utils.js +10 -0
- package/dist/utils/error-utils.js.map +1 -0
- package/dist/utils/errors.d.ts +17 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +40 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/profile-config.d.ts +21 -0
- package/dist/utils/profile-config.d.ts.map +1 -0
- package/dist/utils/profile-config.js +173 -0
- package/dist/utils/profile-config.js.map +1 -0
- package/dist/utils/slack-api-client.d.ts +74 -0
- package/dist/utils/slack-api-client.d.ts.map +1 -0
- package/dist/utils/slack-api-client.js +132 -0
- package/dist/utils/slack-api-client.js.map +1 -0
- package/package.json +56 -0
- package/src/commands/channels.ts +65 -0
- package/src/commands/config.ts +104 -0
- package/src/commands/history.ts +96 -0
- package/src/commands/send.ts +52 -0
- package/src/commands/unread.ts +118 -0
- package/src/index.ts +19 -0
- package/src/types/commands.ts +46 -0
- package/src/types/config.ts +20 -0
- package/src/utils/channel-formatter.ts +89 -0
- package/src/utils/client-factory.ts +10 -0
- package/src/utils/command-wrapper.ts +27 -0
- package/src/utils/config-helper.ts +21 -0
- package/src/utils/constants.ts +47 -0
- package/src/utils/date-utils.ts +8 -0
- package/src/utils/error-utils.ts +6 -0
- package/src/utils/errors.ts +37 -0
- package/src/utils/profile-config.ts +171 -0
- package/src/utils/slack-api-client.ts +218 -0
- package/tests/commands/channels.test.ts +250 -0
- package/tests/commands/config.test.ts +158 -0
- package/tests/commands/history.test.ts +250 -0
- package/tests/commands/send.test.ts +156 -0
- package/tests/commands/unread.test.ts +248 -0
- package/tests/test-utils.ts +28 -0
- package/tests/utils/config.test.ts +400 -0
- package/tests/utils/date-utils.test.ts +30 -0
- package/tests/utils/error-utils.test.ts +34 -0
- package/tests/utils/slack-api-client.test.ts +170 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface ConfigSetOptions {
|
|
2
|
+
token: string;
|
|
3
|
+
profile?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ConfigGetOptions {
|
|
7
|
+
profile?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ConfigUseOptions {
|
|
11
|
+
profile: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ConfigClearOptions {
|
|
15
|
+
profile?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SendOptions {
|
|
19
|
+
channel: string;
|
|
20
|
+
message?: string;
|
|
21
|
+
file?: string;
|
|
22
|
+
profile?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ChannelsOptions {
|
|
26
|
+
type: 'public' | 'private' | 'im' | 'mpim' | 'all';
|
|
27
|
+
includeArchived: boolean;
|
|
28
|
+
format: 'table' | 'simple' | 'json';
|
|
29
|
+
limit: string;
|
|
30
|
+
profile?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface HistoryOptions {
|
|
34
|
+
channel: string;
|
|
35
|
+
number?: string;
|
|
36
|
+
since?: string;
|
|
37
|
+
profile?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface UnreadOptions {
|
|
41
|
+
channel?: string;
|
|
42
|
+
format?: 'table' | 'simple' | 'json';
|
|
43
|
+
countOnly?: boolean;
|
|
44
|
+
limit?: string;
|
|
45
|
+
profile?: string;
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
token: string;
|
|
3
|
+
updatedAt: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface Profile {
|
|
7
|
+
name: string;
|
|
8
|
+
config: Config;
|
|
9
|
+
isDefault?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ConfigStore {
|
|
13
|
+
profiles: Record<string, Config>;
|
|
14
|
+
defaultProfile?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ConfigOptions {
|
|
18
|
+
configDir?: string;
|
|
19
|
+
profile?: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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 formatChannelsAsTable(channels: ChannelInfo[]): void {
|
|
31
|
+
// Print table header
|
|
32
|
+
console.log('Name Type Members Created Description');
|
|
33
|
+
console.log('─'.repeat(65));
|
|
34
|
+
|
|
35
|
+
// Print channel rows
|
|
36
|
+
channels.forEach((channel) => {
|
|
37
|
+
const name = channel.name.padEnd(17);
|
|
38
|
+
const type = channel.type.padEnd(9);
|
|
39
|
+
const members = channel.members.toString().padEnd(8);
|
|
40
|
+
const created = channel.created.padEnd(12);
|
|
41
|
+
const purpose =
|
|
42
|
+
channel.purpose.length > 30 ? channel.purpose.substring(0, 27) + '...' : channel.purpose;
|
|
43
|
+
|
|
44
|
+
console.log(`${name} ${type} ${members} ${created} ${purpose}`);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function formatChannelsAsSimple(channels: ChannelInfo[]): void {
|
|
49
|
+
channels.forEach((channel) => console.log(channel.name));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatChannelsAsJson(channels: ChannelInfo[]): void {
|
|
53
|
+
console.log(
|
|
54
|
+
JSON.stringify(
|
|
55
|
+
channels.map((channel) => ({
|
|
56
|
+
id: channel.id,
|
|
57
|
+
name: channel.name,
|
|
58
|
+
type: channel.type,
|
|
59
|
+
members: channel.members,
|
|
60
|
+
created: channel.created + 'T00:00:00Z',
|
|
61
|
+
purpose: channel.purpose,
|
|
62
|
+
})),
|
|
63
|
+
null,
|
|
64
|
+
2
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatChannelName(channelName?: string): string {
|
|
70
|
+
if (!channelName) return '#unknown';
|
|
71
|
+
return channelName.startsWith('#') ? channelName : `#${channelName}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getChannelTypes(type: string): string {
|
|
75
|
+
switch (type) {
|
|
76
|
+
case 'public':
|
|
77
|
+
return 'public_channel';
|
|
78
|
+
case 'private':
|
|
79
|
+
return 'private_channel';
|
|
80
|
+
case 'im':
|
|
81
|
+
return 'im';
|
|
82
|
+
case 'mpim':
|
|
83
|
+
return 'mpim';
|
|
84
|
+
case 'all':
|
|
85
|
+
return 'public_channel,private_channel,mpim,im';
|
|
86
|
+
default:
|
|
87
|
+
return 'public_channel';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
|
|
17
|
+
// API errors
|
|
18
|
+
API_ERROR: (error: string) => `API Error: ${error}`,
|
|
19
|
+
CHANNEL_NOT_FOUND: (channel: string) => `Channel not found: ${channel}`,
|
|
20
|
+
|
|
21
|
+
// File errors
|
|
22
|
+
FILE_READ_ERROR: (file: string, error: string) => `Error reading file ${file}: ${error}`,
|
|
23
|
+
FILE_NOT_FOUND: (file: string) => `File not found: ${file}`,
|
|
24
|
+
|
|
25
|
+
// Channels command errors
|
|
26
|
+
NO_CHANNELS_FOUND: 'No channels found',
|
|
27
|
+
ERROR_LISTING_CHANNELS: (error: string) => `Error listing channels: ${error}`,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export const SUCCESS_MESSAGES = {
|
|
31
|
+
TOKEN_SAVED: (profileName: string) => `Token saved successfully for profile "${profileName}"`,
|
|
32
|
+
PROFILE_SWITCHED: (profileName: string) => `Switched to profile "${profileName}"`,
|
|
33
|
+
PROFILE_CLEARED: (profileName: string) => `Profile "${profileName}" cleared successfully`,
|
|
34
|
+
MESSAGE_SENT: (channel: string) => `Message sent successfully to #${channel}`,
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
// File and system constants
|
|
38
|
+
export const FILE_PERMISSIONS = {
|
|
39
|
+
CONFIG_FILE: 0o600, // Read/write for owner only
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// API limits
|
|
43
|
+
export const API_LIMITS = {
|
|
44
|
+
MAX_MESSAGE_COUNT: 1000,
|
|
45
|
+
MIN_MESSAGE_COUNT: 1,
|
|
46
|
+
DEFAULT_MESSAGE_COUNT: 10,
|
|
47
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export class SlackCliError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
message: string,
|
|
4
|
+
public code?: string
|
|
5
|
+
) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'SlackCliError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ConfigurationError extends SlackCliError {
|
|
12
|
+
constructor(message: string) {
|
|
13
|
+
super(message, 'CONFIGURATION_ERROR');
|
|
14
|
+
this.name = 'ConfigurationError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ValidationError extends SlackCliError {
|
|
19
|
+
constructor(message: string) {
|
|
20
|
+
super(message, 'VALIDATION_ERROR');
|
|
21
|
+
this.name = 'ValidationError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ApiError extends SlackCliError {
|
|
26
|
+
constructor(message: string) {
|
|
27
|
+
super(message, 'API_ERROR');
|
|
28
|
+
this.name = 'ApiError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class FileError extends SlackCliError {
|
|
33
|
+
constructor(message: string) {
|
|
34
|
+
super(message, 'FILE_ERROR');
|
|
35
|
+
this.name = 'FileError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import type { Config, ConfigOptions, ConfigStore, Profile } from '../types/config';
|
|
5
|
+
import {
|
|
6
|
+
TOKEN_MASK_LENGTH,
|
|
7
|
+
TOKEN_MIN_LENGTH,
|
|
8
|
+
DEFAULT_PROFILE_NAME,
|
|
9
|
+
ERROR_MESSAGES,
|
|
10
|
+
FILE_PERMISSIONS,
|
|
11
|
+
} from './constants';
|
|
12
|
+
|
|
13
|
+
export class ProfileConfigManager {
|
|
14
|
+
private configPath: string;
|
|
15
|
+
|
|
16
|
+
constructor(options: ConfigOptions = {}) {
|
|
17
|
+
const configDir = options.configDir || path.join(os.homedir(), '.slack-cli');
|
|
18
|
+
this.configPath = path.join(configDir, 'config.json');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async setToken(token: string, profile?: string): Promise<void> {
|
|
22
|
+
const store = await this.getConfigStore();
|
|
23
|
+
const profileName = profile || store.defaultProfile || DEFAULT_PROFILE_NAME;
|
|
24
|
+
const config: Config = {
|
|
25
|
+
token,
|
|
26
|
+
updatedAt: new Date().toISOString(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
store.profiles[profileName] = config;
|
|
30
|
+
|
|
31
|
+
// Set as default profile if it's the first one or explicitly setting default
|
|
32
|
+
if (!store.defaultProfile || profileName === DEFAULT_PROFILE_NAME) {
|
|
33
|
+
store.defaultProfile = profileName;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await this.saveConfigStore(store);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async getConfig(profile?: string): Promise<Config | null> {
|
|
40
|
+
const store = await this.getConfigStore();
|
|
41
|
+
const profileName = profile || store.defaultProfile || DEFAULT_PROFILE_NAME;
|
|
42
|
+
|
|
43
|
+
return store.profiles[profileName] || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async listProfiles(): Promise<Profile[]> {
|
|
47
|
+
const store = await this.getConfigStore();
|
|
48
|
+
const currentProfile = store.defaultProfile || DEFAULT_PROFILE_NAME;
|
|
49
|
+
|
|
50
|
+
return Object.entries(store.profiles).map(([name, config]) => ({
|
|
51
|
+
name,
|
|
52
|
+
config,
|
|
53
|
+
isDefault: name === currentProfile,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async useProfile(profile: string): Promise<void> {
|
|
58
|
+
const store = await this.getConfigStore();
|
|
59
|
+
|
|
60
|
+
if (!store.profiles[profile]) {
|
|
61
|
+
throw new Error(`Profile "${profile}" does not exist`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
store.defaultProfile = profile;
|
|
65
|
+
await this.saveConfigStore(store);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async getCurrentProfile(): Promise<string> {
|
|
69
|
+
const store = await this.getConfigStore();
|
|
70
|
+
return store.defaultProfile || DEFAULT_PROFILE_NAME;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async clearConfig(profile?: string): Promise<void> {
|
|
74
|
+
const store = await this.getConfigStore();
|
|
75
|
+
const profileName = profile || store.defaultProfile || DEFAULT_PROFILE_NAME;
|
|
76
|
+
|
|
77
|
+
delete store.profiles[profileName];
|
|
78
|
+
|
|
79
|
+
// If we deleted the default profile, set a new default
|
|
80
|
+
if (store.defaultProfile === profileName) {
|
|
81
|
+
const remainingProfiles = Object.keys(store.profiles);
|
|
82
|
+
if (remainingProfiles.length > 0) {
|
|
83
|
+
store.defaultProfile = remainingProfiles[0];
|
|
84
|
+
} else {
|
|
85
|
+
// No profiles left, delete the config file
|
|
86
|
+
try {
|
|
87
|
+
await fs.unlink(this.configPath);
|
|
88
|
+
} catch (error: unknown) {
|
|
89
|
+
if (error && typeof error === 'object' && 'code' in error && error.code !== 'ENOENT') {
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await this.saveConfigStore(store);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
maskToken(token: string): string {
|
|
101
|
+
if (token.length <= TOKEN_MIN_LENGTH) {
|
|
102
|
+
return '****';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const prefix = token.substring(0, TOKEN_MASK_LENGTH);
|
|
106
|
+
const suffix = token.substring(token.length - TOKEN_MASK_LENGTH);
|
|
107
|
+
|
|
108
|
+
return `${prefix}-****-****-${suffix}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async getConfigStore(): Promise<ConfigStore> {
|
|
112
|
+
try {
|
|
113
|
+
const data = await fs.readFile(this.configPath, 'utf-8');
|
|
114
|
+
const parsed = JSON.parse(data);
|
|
115
|
+
|
|
116
|
+
// Handle migration from old format
|
|
117
|
+
if (this.needsMigration(parsed)) {
|
|
118
|
+
return await this.migrateOldConfig(parsed);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return parsed as ConfigStore;
|
|
122
|
+
} catch (error: unknown) {
|
|
123
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
124
|
+
return { profiles: {} };
|
|
125
|
+
}
|
|
126
|
+
if (error instanceof SyntaxError) {
|
|
127
|
+
throw new Error(ERROR_MESSAGES.INVALID_CONFIG_FORMAT);
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private needsMigration(data: any): boolean {
|
|
134
|
+
return data.token && !data.profiles;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async migrateOldConfig(oldData: any): Promise<ConfigStore> {
|
|
138
|
+
const oldConfig: Config = {
|
|
139
|
+
token: oldData.token,
|
|
140
|
+
updatedAt: oldData.updatedAt,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const newStore: ConfigStore = {
|
|
144
|
+
profiles: { [DEFAULT_PROFILE_NAME]: oldConfig },
|
|
145
|
+
defaultProfile: DEFAULT_PROFILE_NAME,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Save migrated config
|
|
149
|
+
await this.saveConfigStore(newStore);
|
|
150
|
+
return newStore;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async saveConfigStore(store: ConfigStore): Promise<void> {
|
|
154
|
+
const configDir = path.dirname(this.configPath);
|
|
155
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
156
|
+
|
|
157
|
+
await fs.writeFile(this.configPath, JSON.stringify(store, null, 2));
|
|
158
|
+
await fs.chmod(this.configPath, FILE_PERMISSIONS.CONFIG_FILE);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const profileConfig = {
|
|
163
|
+
getCurrentProfile: (): string => {
|
|
164
|
+
return DEFAULT_PROFILE_NAME;
|
|
165
|
+
},
|
|
166
|
+
getToken: (_profile?: string): string | undefined => {
|
|
167
|
+
// This is a simplified version for testing
|
|
168
|
+
// In real usage, it would need to be async
|
|
169
|
+
return undefined;
|
|
170
|
+
},
|
|
171
|
+
};
|