diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index b7bf32e..c9a4dc7 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -89,6 +89,64 @@ describe('config command', () => { expect(webhookPrompt.validate('not-a-webhook')).toBe('Must be a valid Discord webhook URL (including ID and Token)'); }); + it('should detect existing config and prompt for reconfiguration — cancel keeps current config', async () => { + vi.mocked(configUtils.getConfig).mockReturnValue({ notification_service: 'discord' }); + vi.mocked(tui.select).mockResolvedValueOnce('no'); // decline reconfiguration + + await program.parseAsync(['node', 'test', 'config', 'setup']); + + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Current notification service is set to')); + expect(tui.select).toHaveBeenCalledTimes(1); // only reconfiguration prompt + expect(configUtils.setConfig).not.toHaveBeenCalled(); + }); + + it('should proceed with setup when user confirms reconfiguration', async () => { + vi.mocked(configUtils.getConfig).mockReturnValue({ notification_service: 'discord' }); + vi.mocked(tui.select) + .mockResolvedValueOnce('yes') // confirm reconfiguration + .mockResolvedValueOnce('discord'); // select discord service + vi.mocked(tui.input).mockResolvedValue('https://discord.com/api/webhooks/123456789/token-here'); + + await program.parseAsync(['node', 'test', 'config', 'setup']); + + expect(tui.select).toHaveBeenCalledTimes(2); // reconfiguration + service selection + expect(configUtils.setConfig).toHaveBeenCalledWith('notification_service', 'discord'); + expect(configUtils.setConfig).toHaveBeenCalledWith('discord_webhook', 'https://discord.com/api/webhooks/123456789/token-here'); + }); + + it('should skip reconfiguration prompt when no existing config', async () => { + vi.mocked(configUtils.getConfig).mockReturnValue({}); + vi.mocked(tui.select).mockResolvedValue('none'); + + await program.parseAsync(['node', 'test', 'config', 'setup']); + + expect(consoleLogSpy).not.toHaveBeenCalledWith( + expect.stringContaining('Current notification service is set to'), + ); + expect(tui.select).toHaveBeenCalledTimes(1); // only service selection + expect(configUtils.setConfig).toHaveBeenCalledWith('notification_service', 'none'); + }); + + it('should handle select rejection gracefully during setup', async () => { + vi.mocked(tui.select).mockRejectedValueOnce(new Error('select failed')); + + await program.parseAsync(['node', 'test', 'config', 'setup']); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('select failed')); + expect(configUtils.setConfig).not.toHaveBeenCalled(); + }); + + it('should handle setConfig failure gracefully during setup', async () => { + vi.mocked(tui.select).mockResolvedValue('discord'); + vi.mocked(tui.input).mockResolvedValue('https://discord.com/api/webhooks/123456789/token-here'); + // Use mockImplementationOnce to avoid polluting subsequent tests + vi.mocked(configUtils.setConfig).mockImplementationOnce(() => { throw new Error('write failed'); }); + + await program.parseAsync(['node', 'test', 'config', 'setup']); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('write failed')); + }); + it('should call select, multiple inputs and setConfig on email setup without password', async () => { vi.mocked(tui.select).mockResolvedValue('email'); vi.mocked(tui.input) @@ -152,6 +210,32 @@ describe('config command', () => { expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Set alert_email to test@test.com')); }); + it('should show deprecation warning when setting credential key via config set', async () => { + await program.parseAsync(['node', 'test', 'config', 'set', 'discord_webhook', 'https://discord.com/api/webhooks/test']); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Deprecation warning')); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('kdm config setup')); + expect(configUtils.setConfig).toHaveBeenCalled(); // still executes (soft deprecation) + }); + + it('should show deprecation warning for all credential keys', async () => { + const credentialKeys = ['notification_service', 'discord_webhook', 'email_host', 'email_port', 'email_user', 'email_to']; + for (const key of credentialKeys) { + vi.clearAllMocks(); + // email_port gets parsed to int by the existing handler logic + const testValue = key === 'email_port' ? '587' : 'test-value'; + const expectedValue = key === 'email_port' ? 587 : testValue; + await program.parseAsync(['node', 'test', 'config', 'set', key, testValue]); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Deprecation warning')); + expect(configUtils.setConfig).toHaveBeenCalledWith(key, expectedValue); + } + }); + + it('should not show deprecation warning for non-credential keys', async () => { + await program.parseAsync(['node', 'test', 'config', 'set', 'alert_cooldown', '300']); + expect(consoleLogSpy).not.toHaveBeenCalledWith(expect.stringContaining('Deprecation warning')); + expect(configUtils.setConfig).toHaveBeenCalledWith('alert_cooldown', 300); + }); + it('should parse integer for alert_cooldown', async () => { await program.parseAsync(['node', 'test', 'config', 'set', 'alert_cooldown', '123']); expect(configUtils.setConfig).toHaveBeenCalledWith('alert_cooldown', 123); diff --git a/src/commands/config.ts b/src/commands/config.ts index 1d42758..2d6d0e1 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -3,6 +3,31 @@ import chalk from 'chalk'; import { setConfig, getConfig, clearConfig, clearNotificationCredentials } from '../utils/config'; import { select, input } from '@vr_patel/tui'; +const promptReconfigurationIfNeeded = async (): Promise => { + const currentConfig = getConfig(); + if (currentConfig.notification_service && currentConfig.notification_service !== 'none') { + const serviceLabel = + currentConfig.notification_service === 'discord' ? 'Discord' : 'Email (SMTP)'; + console.log( + chalk.yellow(`\n⚠ Current notification service is set to: ${chalk.bold(serviceLabel)}`) + ); + + const shouldReconfigure = await select({ + message: 'Would you like to reconfigure?', + options: [ + { label: 'Yes', value: 'yes' }, + { label: 'No', value: 'no' }, + ], + }); + + if (shouldReconfigure === 'no') { + console.log(chalk.dim('Setup cancelled. Current configuration unchanged.')); + return false; + } + } + return true; +}; + export const registerConfigCommand = (program: Command) => { const config = program .command('config') @@ -13,6 +38,10 @@ export const registerConfigCommand = (program: Command) => { .description('Interactively set up notification service') .action(async () => { try { + // Check for existing configuration + const shouldProceed = await promptReconfigurationIfNeeded(); + if (!shouldProceed) return; + const choice = await select({ message: 'Select notification service:', options: [ @@ -39,7 +68,7 @@ export const registerConfigCommand = (program: Command) => { return discordWebhookRegex.test(v) || 'Must be a valid Discord webhook URL (including ID and Token)'; }, }); - + clearNotificationCredentials(); setConfig('discord_webhook', webhook); setConfig('notification_service', 'discord'); @@ -82,13 +111,13 @@ export const registerConfigCommand = (program: Command) => { setConfig('email_password', password); } setConfig('notification_service', 'email'); - + console.log(chalk.green('\n✓ Email SMTP configured.')); } - console.log(chalk.green(`✓ Notification service set to: ${chalk.bold(choice.toUpperCase())}`)); + console.log(chalk.green(`\n✓ Notification service set to: ${chalk.bold(choice.toUpperCase())}`)); } catch (error) { - console.error(chalk.red(`✗ Set up cancelled or failed: ${(error as Error).message}`)); + console.error(chalk.red(`\n✗ Set up cancelled or failed: ${(error as Error).message}`)); } }); @@ -97,12 +126,26 @@ export const registerConfigCommand = (program: Command) => { .description('Set a configuration value') .action((key, value) => { try { + // Credential keys that should be configured via the interactive setup + const credentialKeys = ['notification_service', 'discord_webhook', 'email_host', 'email_port', 'email_user', 'email_to']; + const credentialKeyPattern = new RegExp(`^(${credentialKeys.join('|')})$`); + + if (credentialKeyPattern.test(key)) { + console.log(chalk.yellow(`\n⚠ Deprecation warning: Setting "${key}" via "kdm config set" is deprecated.`)); + console.log(chalk.yellow(` Use ${chalk.bold('kdm config setup')} for guided configuration.\n`)); + } + // Convert value to number if key is alert_cooldown or email_port let finalValue = value; + let finalValue = value; if (key === 'alert_cooldown' || key === 'email_port') { - finalValue = parseInt(value, 10); + const parsed = Number.parseInt(value, 10); + if (Number.isNaN(parsed)) { + throw new Error(`Invalid numeric value for "${key}"`); + } + finalValue = parsed; } - + setConfig(key as any, finalValue); console.log(chalk.green(`✓ Set ${key} to ${finalValue}`)); } catch (error) { @@ -117,15 +160,15 @@ export const registerConfigCommand = (program: Command) => { const current = getConfig(); console.log(chalk.bold('\nCurrent KDM Configuration:')); console.log(chalk.gray('──────────────────────────────────────────────────')); - + if (Object.keys(current).length === 0) { console.log(chalk.yellow(' No configuration found. Use "kdm config set "')); } else { Object.entries(current).forEach(([key, value]) => { - console.log(` ${chalk.cyan(key.padEnd(20))} : ${chalk.white(value)}`); + console.log(`${chalk.cyan(key.padEnd(20))} : ${chalk.white(value)}`); }); } - + console.log(chalk.gray('──────────────────────────────────────────────────')); console.log(chalk.dim('\n Note: SMTP password can be set either in config or via the KDM_SMTP_PASSWORD environment variable, which takes precedence if both are set.\n')); }); diff --git a/src/ui/banner.ts b/src/ui/banner.ts index 22c8ac6..3e999cc 100644 --- a/src/ui/banner.ts +++ b/src/ui/banner.ts @@ -10,13 +10,11 @@ export const showWelcomeBanner = (version: string) => { ${chalk.cyan('╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝')} `; - const signature = chalk.gray(` - ────────────────────────────────────────────────── - ${chalk.white.bold('devloped by')} ${chalk.yellow.bold('utkarshpatrikar')} - ────────────────────────────────────────────────── - `); + const signature = chalk.gray( + '──────────────────────────────────────────────────' + ); console.log(banner); console.log(signature); - console.log(chalk.blue.bold(` Kubernetes & Docker Monitor v${version}\n`)); + console.log(chalk.blue.bold(` Kubernetes & Docker Monitor v${version}`)); };