From 153dd26009b4e05999ec412eecb0dda5aad6c8fc Mon Sep 17 00:00:00 2001 From: Yuvraj Sarathe Date: Tue, 26 May 2026 22:41:29 +0530 Subject: [PATCH 1/8] banner --- src/ui/banner.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/ui/banner.ts b/src/ui/banner.ts index 22c8ac6..67f9b2d 100644 --- a/src/ui/banner.ts +++ b/src/ui/banner.ts @@ -1,22 +1,22 @@ import chalk from 'chalk'; export const showWelcomeBanner = (version: string) => { - const banner = ` - ${chalk.cyan('██╗ ██╗██████╗ ███╗ ███╗')} - ${chalk.cyan('██║ ██╔╝██╔══██╗████╗ ████║')} - ${chalk.cyan('█████╔╝ ██║ ██║██╔████╔██║')} - ${chalk.cyan('██╔═██╗ ██║ ██║██║╚██╔╝██║')} - ${chalk.cyan('██║ ██╗██████╔╝██║ ╚═╝ ██║')} - ${chalk.cyan('╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝')} - `; + const banner = + + + + + + + ; - 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\n)); }; From 7374eae6a9cfa3d46761f23623bb19a065169d0c Mon Sep 17 00:00:00 2001 From: Yuvraj Sarathe Date: Wed, 27 May 2026 21:54:38 +0530 Subject: [PATCH 2/8] kdm-config-setup --- src/__tests__/config.test.ts | 61 ++++++++++++++++++++++++++++++++++++ src/commands/config.ts | 29 +++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index b7bf32e..e032838 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -89,6 +89,41 @@ 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('currently set to')); + expect(configUtils.setConfig).toHaveBeenCalledWith('notification_service', 'none'); + }); + 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 +187,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..6814353 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -13,6 +13,26 @@ export const registerConfigCommand = (program: Command) => { .description('Interactively set up notification service') .action(async () => { try { + // Check for existing configuration + 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; + } + } + const choice = await select({ message: 'Select notification service:', options: [ @@ -97,6 +117,15 @@ 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; if (key === 'alert_cooldown' || key === 'email_port') { From bcef684b9e5b6dd8e484f38cf79767a66979545f Mon Sep 17 00:00:00 2001 From: Yuvraj Sarathe Date: Wed, 27 May 2026 22:15:05 +0530 Subject: [PATCH 3/8] Update src/__tests__/config.test.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/__tests__/config.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index e032838..7be9914 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -120,7 +120,10 @@ describe('config command', () => { await program.parseAsync(['node', 'test', 'config', 'setup']); - expect(consoleLogSpy).not.toHaveBeenCalledWith(expect.stringContaining('currently set to')); + 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'); }); From 854c8abf265b039471d74f0accffbd5c91139730 Mon Sep 17 00:00:00 2001 From: Yuvraj Sarathe Date: Wed, 27 May 2026 22:26:38 +0530 Subject: [PATCH 4/8] update --- src/__tests__/config.test.ts | 20 ++++++++++++++++++++ src/ui/banner.ts | 22 ++++++++++------------ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index e032838..96d90ca 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -124,6 +124,26 @@ describe('config command', () => { 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) diff --git a/src/ui/banner.ts b/src/ui/banner.ts index 67f9b2d..995d2e6 100644 --- a/src/ui/banner.ts +++ b/src/ui/banner.ts @@ -1,22 +1,20 @@ import chalk from 'chalk'; export const showWelcomeBanner = (version: string) => { - const banner = - - - - - - - ; + const banner = chalk.white(` + ╔══════════════════════════════════════╗ + ║ _ _____ __ ___ _ __ ║ + ║ / |/ / _ / / / _ ) / | / / ║ + ║ / / __/ _ \\ _ \\/ | / /__ ║ + ║ /_/|_/____/_//_/___/_/|_/____/ ║ + ╚══════════════════════════════════════╝ + `); const signature = chalk.gray( - -------------------------------------------------- - - -------------------------------------------------- + '──────────────────────────────────────────────────' ); console.log(banner); console.log(signature); - console.log(chalk.blue.bold( Kubernetes & Docker Monitor v\n)); + console.log(chalk.blue.bold(` Kubernetes & Docker Monitor v${version}`)); }; From e8afbf4effa3314f2bf2d414d48c7ac8c2aee715 Mon Sep 17 00:00:00 2001 From: Yuvraj Sarathe Date: Thu, 28 May 2026 13:23:24 +0530 Subject: [PATCH 5/8] Revamp welcome banner design in banner.ts Updated the welcome banner design in the showWelcomeBanner function to a new style using cyan color. --- src/ui/banner.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ui/banner.ts b/src/ui/banner.ts index 995d2e6..3e999cc 100644 --- a/src/ui/banner.ts +++ b/src/ui/banner.ts @@ -1,14 +1,14 @@ import chalk from 'chalk'; export const showWelcomeBanner = (version: string) => { - const banner = chalk.white(` - ╔══════════════════════════════════════╗ - ║ _ _____ __ ___ _ __ ║ - ║ / |/ / _ / / / _ ) / | / / ║ - ║ / / __/ _ \\ _ \\/ | / /__ ║ - ║ /_/|_/____/_//_/___/_/|_/____/ ║ - ╚══════════════════════════════════════╝ - `); + const banner = ` + ${chalk.cyan('██╗ ██╗██████╗ ███╗ ███╗')} + ${chalk.cyan('██║ ██╔╝██╔══██╗████╗ ████║')} + ${chalk.cyan('█████╔╝ ██║ ██║██╔████╔██║')} + ${chalk.cyan('██╔═██╗ ██║ ██║██║╚██╔╝██║')} + ${chalk.cyan('██║ ██╗██████╔╝██║ ╚═╝ ██║')} + ${chalk.cyan('╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝')} + `; const signature = chalk.gray( '──────────────────────────────────────────────────' From 4db6ebc9b1d6d8298b49a9784c6950881b3b746e Mon Sep 17 00:00:00 2001 From: Yuvraj Sarathe Date: Thu, 28 May 2026 13:32:47 +0530 Subject: [PATCH 6/8] Refactor notification service setup and prompts --- src/commands/config.ts | 69 ++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/src/commands/config.ts b/src/commands/config.ts index 6814353..56e2ab1 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') @@ -14,24 +39,8 @@ export const registerConfigCommand = (program: Command) => { .action(async () => { try { // Check for existing configuration - 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; - } - } + const shouldProceed = await promptReconfigurationIfNeeded(); + if (!shouldProceed) return; const choice = await select({ message: 'Select notification service:', @@ -59,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'); @@ -102,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}`); } }); @@ -122,8 +131,8 @@ export const registerConfigCommand = (program: Command) => { 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`)); + 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 @@ -131,11 +140,11 @@ export const registerConfigCommand = (program: Command) => { if (key === 'alert_cooldown' || key === 'email_port') { finalValue = parseInt(value, 10); } - + setConfig(key as any, finalValue); - console.log(chalk.green(`✓ Set ${key} to ${finalValue}`)); + console.log(chalk.green`✓ Set ${key} to ${finalValue}`); } catch (error) { - console.error(chalk.red(`✗ Failed to set config: ${(error as Error).message}`)); + console.error(chalk.red`✗ Failed to set config: ${(error as Error).message}`); } }); @@ -146,15 +155,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')); }); From b3441f79f8ab859fac06113e78eac25d44c3f85c Mon Sep 17 00:00:00 2001 From: Yuvraj Sarathe Date: Thu, 28 May 2026 14:33:18 +0530 Subject: [PATCH 7/8] test solution --- src/commands/config.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/config.ts b/src/commands/config.ts index 56e2ab1..987d215 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -9,7 +9,7 @@ const promptReconfigurationIfNeeded = async (): Promise => { const serviceLabel = currentConfig.notification_service === 'discord' ? 'Discord' : 'Email (SMTP)'; console.log( - chalk.yellow`\n⚠ Current notification service is set to: ${chalk.bold(serviceLabel)}` + chalk.yellow(`\n⚠ Current notification service is set to: ${chalk.bold(serviceLabel)}`) ); const shouldReconfigure = await select({ @@ -115,9 +115,9 @@ export const registerConfigCommand = (program: Command) => { console.log(chalk.green('\n✓ Email SMTP configured.')); } - console.log(chalk.green`\n✓ 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`\n✗ Set up cancelled or failed: ${(error as Error).message}`); + console.error(chalk.red(`\n✗ Set up cancelled or failed: ${(error as Error).message}`)); } }); @@ -131,8 +131,8 @@ export const registerConfigCommand = (program: Command) => { 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`); + 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 @@ -142,9 +142,9 @@ export const registerConfigCommand = (program: Command) => { } setConfig(key as any, finalValue); - console.log(chalk.green`✓ Set ${key} to ${finalValue}`); + console.log(chalk.green(`✓ Set ${key} to ${finalValue}`)); } catch (error) { - console.error(chalk.red`✗ Failed to set config: ${(error as Error).message}`); + console.error(chalk.red(`✗ Failed to set config: ${(error as Error).message}`)); } }); From 6471fab5afbfe8faa174591a938802d5f4f9b090 Mon Sep 17 00:00:00 2001 From: Yuvraj Sarathe Date: Thu, 28 May 2026 14:53:56 +0530 Subject: [PATCH 8/8] Update src/commands/config.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/commands/config.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/commands/config.ts b/src/commands/config.ts index 987d215..2d6d0e1 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -137,8 +137,13 @@ export const registerConfigCommand = (program: Command) => { // 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);