From 0111ed3d67fe11107a6050aa3d69aa1b65085cae Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Thu, 14 May 2026 13:09:12 +0530 Subject: [PATCH 1/7] feat: add interactive notification setup using @vr_patel/tui --- package-lock.json | 10 ++++++++++ package.json | 1 + src/__tests__/config.test.ts | 17 +++++++++++++++-- src/commands/config.ts | 28 ++++++++++++++++++++++++++++ src/utils/config.ts | 1 + 5 files changed, 55 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 760acb1..2feb083 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@kubernetes/client-node": "^0.20.0", + "@vr_patel/tui": "^1.0.0", "chalk": "^5.3.0", "cli-table3": "^0.6.3", "commander": "^12.0.0", @@ -1306,6 +1307,15 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vr_patel/tui": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@vr_patel/tui/-/tui-1.0.0.tgz", + "integrity": "sha512-HuPZpOoJJgsiKe5jxGnqKznuAw2S2p/XH6d4aYdP+QRxDGXORzYXdMc2X+iGOk0+OORzhyy9+IdTMjXFzKMlpA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", diff --git a/package.json b/package.json index df60440..a2f8c06 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "license": "MIT", "dependencies": { "@kubernetes/client-node": "^0.20.0", + "@vr_patel/tui": "^1.0.0", "chalk": "^5.3.0", "cli-table3": "^0.6.3", "commander": "^12.0.0", diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index e3b61b3..615f841 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Command } from 'commander'; import { registerConfigCommand } from '../commands/config'; import { setConfig, getConfig, clearConfig } from '../utils/config'; +import { select } from '@vr_patel/tui'; vi.mock('../utils/config', () => ({ setConfig: vi.fn(), @@ -9,6 +10,10 @@ vi.mock('../utils/config', () => ({ clearConfig: vi.fn(), })); +vi.mock('@vr_patel/tui', () => ({ + select: vi.fn(), +})); + describe('config command', () => { let program: Command; let consoleLogSpy: any; @@ -22,10 +27,18 @@ describe('config command', () => { consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); }); - it('should register config set, list, and clear commands', () => { + it('should register config setup, set, list, and clear commands', () => { const configCmd = program.commands.find((c) => c.name() === 'config'); expect(configCmd).toBeDefined(); - expect(configCmd?.commands.map((c) => c.name())).toEqual(['set', 'list', 'clear']); + expect(configCmd?.commands.map((c) => c.name())).toEqual(['setup', 'set', 'list', 'clear']); + }); + + it('should call select and setConfig on config setup', async () => { + (select as any).mockResolvedValue('discord'); + await program.parseAsync(['node', 'test', 'config', 'setup']); + expect(select).toHaveBeenCalled(); + expect(setConfig).toHaveBeenCalledWith('notification_service', 'discord'); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringMatching(/Notification service set to:.*DISCORD/i)); }); it('should call setConfig on config set', async () => { diff --git a/src/commands/config.ts b/src/commands/config.ts index 8f0205b..cdb7f6a 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,12 +1,40 @@ import { Command } from 'commander'; import chalk from 'chalk'; import { setConfig, getConfig, clearConfig } from '../utils/config'; +import { select } from '@vr_patel/tui'; export const registerConfigCommand = (program: Command) => { const config = program .command('config') .description('Manage KDM configuration'); + config + .command('setup') + .description('Interactively setup notification service') + .action(async () => { + try { + const choice = await select({ + message: "Select notification service:", + options: [ + { label: "Discord", value: "discord", description: "Send alerts to a Discord channel via Webhook" }, + { label: "Email (SMTP)", value: "email", description: "Send alerts via Email SMTP" }, + { label: "None", value: "none", description: "Disable notifications" }, + ], + }); + + setConfig('notification_service', choice); + console.log(chalk.green(`\n✓ Notification service set to: ${chalk.bold(choice.toUpperCase())}`)); + + if (choice === 'discord') { + console.log(chalk.yellow('! Remember to set your webhook URL:'), chalk.cyan('kdm config set discord_webhook ')); + } else if (choice === 'email') { + console.log(chalk.yellow('! Remember to set your SMTP details (email_host, email_port, email_user, email_to)')); + } + } catch (error) { + console.error(chalk.red(`✗ Setup cancelled or failed: ${(error as Error).message}`)); + } + }); + config .command('set ') .description('Set a configuration value') diff --git a/src/utils/config.ts b/src/utils/config.ts index 10686cd..0189a74 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,6 +1,7 @@ import Conf from 'conf'; interface KDMConfig { + notification_service?: 'discord' | 'email' | 'none'; discord_webhook?: string; email_host?: string; email_port?: number; From 2b200763ece8fdd300e6ec832c01c1df05eeb605 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Thu, 14 May 2026 13:13:40 +0530 Subject: [PATCH 2/7] feat: add interactive credential input for notification setup --- src/commands/config.ts | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/commands/config.ts b/src/commands/config.ts index cdb7f6a..abb24c4 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import chalk from 'chalk'; import { setConfig, getConfig, clearConfig } from '../utils/config'; -import { select } from '@vr_patel/tui'; +import { select, input } from '@vr_patel/tui'; export const registerConfigCommand = (program: Command) => { const config = program @@ -23,13 +23,44 @@ export const registerConfigCommand = (program: Command) => { }); setConfig('notification_service', choice); - console.log(chalk.green(`\n✓ Notification service set to: ${chalk.bold(choice.toUpperCase())}`)); - + if (choice === 'discord') { - console.log(chalk.yellow('! Remember to set your webhook URL:'), chalk.cyan('kdm config set discord_webhook ')); + const webhook = await input({ + message: "Discord Webhook URL:", + validate: (v) => v.startsWith('https://discord.com/api/webhooks/') || v.startsWith('https://ptb.discord.com/api/webhooks/') || "Must be a valid Discord webhook URL", + }); + setConfig('discord_webhook', webhook); + console.log(chalk.green(`\n✓ Discord Webhook configured.`)); } else if (choice === 'email') { - console.log(chalk.yellow('! Remember to set your SMTP details (email_host, email_port, email_user, email_to)')); + const host = await input({ + message: "SMTP Host:", + placeholder: "smtp.gmail.com", + validate: (v) => v.length > 0 || "Host is required", + }); + const portStr = await input({ + message: "SMTP Port:", + defaultValue: "587", + validate: (v) => !isNaN(parseInt(v)) || "Must be a number", + }); + const user = await input({ + message: "SMTP User:", + validate: (v) => v.includes('@') || "Must be a valid email", + }); + const to = await input({ + message: "Alert Recipient Email:", + validate: (v) => v.includes('@') || "Must be a valid email", + }); + + setConfig('email_host', host); + setConfig('email_port', parseInt(portStr, 10)); + setConfig('email_user', user); + setConfig('email_to', to); + + console.log(chalk.green(`\n✓ Email SMTP configured.`)); + console.log(chalk.yellow('! Note: Please set your SMTP password in the KDM_SMTP_PASSWORD environment variable.')); } + + console.log(chalk.green(`\n✓ Notification service set to: ${chalk.bold(choice.toUpperCase())}`)); } catch (error) { console.error(chalk.red(`✗ Setup cancelled or failed: ${(error as Error).message}`)); } From caa8f84bb815a13249de34adf8229ba43f392de4 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Thu, 14 May 2026 13:16:44 +0530 Subject: [PATCH 3/7] test: add unit tests for interactive credential collection --- src/__tests__/config.test.ts | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 615f841..72d9782 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Command } from 'commander'; import { registerConfigCommand } from '../commands/config'; import { setConfig, getConfig, clearConfig } from '../utils/config'; -import { select } from '@vr_patel/tui'; +import { select, input } from '@vr_patel/tui'; vi.mock('../utils/config', () => ({ setConfig: vi.fn(), @@ -12,6 +12,7 @@ vi.mock('../utils/config', () => ({ vi.mock('@vr_patel/tui', () => ({ select: vi.fn(), + input: vi.fn(), })); describe('config command', () => { @@ -33,12 +34,32 @@ describe('config command', () => { expect(configCmd?.commands.map((c) => c.name())).toEqual(['setup', 'set', 'list', 'clear']); }); - it('should call select and setConfig on config setup', async () => { + it('should call select, input and setConfig on discord setup', async () => { (select as any).mockResolvedValue('discord'); + (input as any).mockResolvedValue('https://discord.com/api/webhooks/123'); await program.parseAsync(['node', 'test', 'config', 'setup']); expect(select).toHaveBeenCalled(); + expect(input).toHaveBeenCalled(); expect(setConfig).toHaveBeenCalledWith('notification_service', 'discord'); - expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringMatching(/Notification service set to:.*DISCORD/i)); + expect(setConfig).toHaveBeenCalledWith('discord_webhook', 'https://discord.com/api/webhooks/123'); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringMatching(/Discord Webhook configured/i)); + }); + + it('should call select, multiple inputs and setConfig on email setup', async () => { + (select as any).mockResolvedValue('email'); + (input as any) + .mockResolvedValueOnce('smtp.gmail.com') // host + .mockResolvedValueOnce('587') // port + .mockResolvedValueOnce('user@test.com') // user + .mockResolvedValueOnce('to@test.com'); // to + + await program.parseAsync(['node', 'test', 'config', 'setup']); + + expect(setConfig).toHaveBeenCalledWith('notification_service', 'email'); + expect(setConfig).toHaveBeenCalledWith('email_host', 'smtp.gmail.com'); + expect(setConfig).toHaveBeenCalledWith('email_port', 587); + expect(setConfig).toHaveBeenCalledWith('email_user', 'user@test.com'); + expect(setConfig).toHaveBeenCalledWith('email_to', 'to@test.com'); }); it('should call setConfig on config set', async () => { From 24b1869e5fd2e4de7cd12cb812fc9ae33fef7383 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Thu, 14 May 2026 13:22:50 +0530 Subject: [PATCH 4/7] refactor: improve interactive setup validation and persistence logic --- package-lock.json | 2 +- package.json | 2 +- src/__tests__/config.test.ts | 18 +++++++++-- src/commands/config.ts | 62 +++++++++++++++++++++++------------- src/utils/config.ts | 8 +++++ 5 files changed, 65 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2feb083..2a8357b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "@types/dockerode": "^3.3.26", "@types/node": "^20.11.24", "@types/nodemailer": "^8.0.0", - "@types/react": "^18.2.63", + "@types/react": "^18.3.28", "tsup": "^8.0.2", "typescript": "^5.3.3", "vitest": "^1.3.1" diff --git a/package.json b/package.json index a2f8c06..ac722fc 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "@types/dockerode": "^3.3.26", "@types/node": "^20.11.24", "@types/nodemailer": "^8.0.0", - "@types/react": "^18.2.63", + "@types/react": "^18.3.28", "tsup": "^8.0.2", "typescript": "^5.3.3", "vitest": "^1.3.1" diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 72d9782..510a03d 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -8,6 +8,7 @@ vi.mock('../utils/config', () => ({ setConfig: vi.fn(), getConfig: vi.fn(() => ({})), clearConfig: vi.fn(), + clearNotificationCredentials: vi.fn(), })); vi.mock('@vr_patel/tui', () => ({ @@ -34,14 +35,26 @@ describe('config command', () => { expect(configCmd?.commands.map((c) => c.name())).toEqual(['setup', 'set', 'list', 'clear']); }); + it('should clear credentials and set service to none', async () => { + (select as any).mockResolvedValue('none'); + await program.parseAsync(['node', 'test', 'config', 'setup']); + + const { setConfig } = await import('../utils/config'); + expect(select).toHaveBeenCalled(); + expect(setConfig).toHaveBeenCalledWith('notification_service', 'none'); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringMatching(/Notifications disabled/i)); + }); + it('should call select, input and setConfig on discord setup', async () => { (select as any).mockResolvedValue('discord'); - (input as any).mockResolvedValue('https://discord.com/api/webhooks/123'); + (input as any).mockResolvedValue('https://discord.com/api/webhooks/123/abc'); await program.parseAsync(['node', 'test', 'config', 'setup']); + + const { setConfig } = await import('../utils/config'); expect(select).toHaveBeenCalled(); expect(input).toHaveBeenCalled(); expect(setConfig).toHaveBeenCalledWith('notification_service', 'discord'); - expect(setConfig).toHaveBeenCalledWith('discord_webhook', 'https://discord.com/api/webhooks/123'); + expect(setConfig).toHaveBeenCalledWith('discord_webhook', 'https://discord.com/api/webhooks/123/abc'); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringMatching(/Discord Webhook configured/i)); }); @@ -55,6 +68,7 @@ describe('config command', () => { await program.parseAsync(['node', 'test', 'config', 'setup']); + const { setConfig } = await import('../utils/config'); expect(setConfig).toHaveBeenCalledWith('notification_service', 'email'); expect(setConfig).toHaveBeenCalledWith('email_host', 'smtp.gmail.com'); expect(setConfig).toHaveBeenCalledWith('email_port', 587); diff --git a/src/commands/config.ts b/src/commands/config.ts index abb24c4..d9446d9 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -10,59 +10,75 @@ export const registerConfigCommand = (program: Command) => { config .command('setup') - .description('Interactively setup notification service') + .description('Interactively set up notification service') .action(async () => { try { const choice = await select({ - message: "Select notification service:", + message: 'Select notification service:', options: [ - { label: "Discord", value: "discord", description: "Send alerts to a Discord channel via Webhook" }, - { label: "Email (SMTP)", value: "email", description: "Send alerts via Email SMTP" }, - { label: "None", value: "none", description: "Disable notifications" }, + { label: 'Discord', value: 'discord', description: 'Send alerts to a Discord channel via Webhook' }, + { label: 'Email (SMTP)', value: 'email', description: 'Send alerts via Email SMTP' }, + { label: 'None', value: 'none', description: 'Disable notifications' }, ], }); - setConfig('notification_service', choice); + if (choice === 'none') { + clearNotificationCredentials(); + setConfig('notification_service', 'none'); + console.log(chalk.green('\n✓ Notifications disabled.')); + return; + } if (choice === 'discord') { const webhook = await input({ - message: "Discord Webhook URL:", - validate: (v) => v.startsWith('https://discord.com/api/webhooks/') || v.startsWith('https://ptb.discord.com/api/webhooks/') || "Must be a valid Discord webhook URL", + message: 'Discord Webhook URL:', + validate: (v) => { + const discordWebhookRegex = /^https:\/\/(?:ptb\.|canary\.)?discord\.com\/api\/webhooks\/\d+\/[\w-]+$/; + return discordWebhookRegex.test(v) || 'Must be a valid Discord webhook URL (including ID and Token)'; + }, }); + + clearNotificationCredentials(); setConfig('discord_webhook', webhook); - console.log(chalk.green(`\n✓ Discord Webhook configured.`)); + setConfig('notification_service', 'discord'); + console.log(chalk.green('\n✓ Discord Webhook configured.')); } else if (choice === 'email') { const host = await input({ - message: "SMTP Host:", - placeholder: "smtp.gmail.com", - validate: (v) => v.length > 0 || "Host is required", + message: 'SMTP Host:', + placeholder: 'smtp.gmail.com', + validate: (v) => v.length > 0 || 'Host is required', }); const portStr = await input({ - message: "SMTP Port:", - defaultValue: "587", - validate: (v) => !isNaN(parseInt(v)) || "Must be a number", + message: 'SMTP Port:', + defaultValue: '587', + validate: (v) => { + const port = parseInt(v, 10); + return (/^\d+$/.test(v) && port > 0 && port <= 65535) || 'Must be a valid port number (1-65535)'; + }, }); const user = await input({ - message: "SMTP User:", - validate: (v) => v.includes('@') || "Must be a valid email", + message: 'SMTP User:', + validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || 'Must be a valid email address', }); const to = await input({ - message: "Alert Recipient Email:", - validate: (v) => v.includes('@') || "Must be a valid email", + message: 'Alert Recipient Email:', + validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || 'Must be a valid email address', }); + clearNotificationCredentials(); setConfig('email_host', host); setConfig('email_port', parseInt(portStr, 10)); setConfig('email_user', user); setConfig('email_to', to); + setConfig('notification_service', 'email'); - console.log(chalk.green(`\n✓ Email SMTP configured.`)); - console.log(chalk.yellow('! Note: Please set your SMTP password in the KDM_SMTP_PASSWORD environment variable.')); + console.log(chalk.green('\n✓ Email SMTP configured.')); + console.log(chalk.yellow('! Important: Set your SMTP password in the KDM_SMTP_PASSWORD environment variable for notifications to work.')); } - console.log(chalk.green(`\n✓ Notification service set to: ${chalk.bold(choice.toUpperCase())}`)); + console.log(chalk.green(`✓ Notification service set to: ${chalk.bold(choice.toUpperCase())}`)); } catch (error) { - console.error(chalk.red(`✗ Setup cancelled or failed: ${(error as Error).message}`)); + console.error(chalk.red(`✗ Set up cancelled or failed: ${(error as Error).message}`)); } }); diff --git a/src/utils/config.ts b/src/utils/config.ts index 0189a74..a7d3036 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -19,6 +19,14 @@ export const setConfig = (key: keyof KDMConfig, value: any) => config.set(key, v export const deleteConfig = (key: keyof KDMConfig) => config.delete(key); export const clearConfig = () => config.clear(); +export const clearNotificationCredentials = () => { + config.delete('discord_webhook'); + config.delete('email_host'); + config.delete('email_port'); + config.delete('email_user'); + config.delete('email_to'); +}; + // Helper for sensitive data - always use environment variables export const getSMTPSettings = () => { return { From 3af02e286f2de51cbc5ae7d8285413b9fb0ee867 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Thu, 14 May 2026 13:25:17 +0530 Subject: [PATCH 5/7] fix: add missing import and fix unit tests for config setup --- src/__tests__/config.test.ts | 61 +++++++++++++++++++----------------- src/commands/config.ts | 2 +- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 510a03d..3b7e917 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -1,9 +1,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Command } from 'commander'; import { registerConfigCommand } from '../commands/config'; -import { setConfig, getConfig, clearConfig } from '../utils/config'; -import { select, input } from '@vr_patel/tui'; +import * as configUtils from '../utils/config'; +import * as tui from '@vr_patel/tui'; +// Mock the modules vi.mock('../utils/config', () => ({ setConfig: vi.fn(), getConfig: vi.fn(() => ({})), @@ -32,35 +33,40 @@ describe('config command', () => { it('should register config setup, set, list, and clear commands', () => { const configCmd = program.commands.find((c) => c.name() === 'config'); expect(configCmd).toBeDefined(); - expect(configCmd?.commands.map((c) => c.name())).toEqual(['setup', 'set', 'list', 'clear']); + const subCommandNames = configCmd?.commands.map((c) => c.name()); + expect(subCommandNames).toContain('setup'); + expect(subCommandNames).toContain('set'); + expect(subCommandNames).toContain('list'); + expect(subCommandNames).toContain('clear'); }); it('should clear credentials and set service to none', async () => { - (select as any).mockResolvedValue('none'); + vi.mocked(tui.select).mockResolvedValue('none'); + await program.parseAsync(['node', 'test', 'config', 'setup']); - const { setConfig } = await import('../utils/config'); - expect(select).toHaveBeenCalled(); - expect(setConfig).toHaveBeenCalledWith('notification_service', 'none'); + expect(tui.select).toHaveBeenCalled(); + expect(configUtils.clearNotificationCredentials).toHaveBeenCalled(); + expect(configUtils.setConfig).toHaveBeenCalledWith('notification_service', 'none'); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringMatching(/Notifications disabled/i)); }); it('should call select, input and setConfig on discord setup', async () => { - (select as any).mockResolvedValue('discord'); - (input as any).mockResolvedValue('https://discord.com/api/webhooks/123/abc'); + vi.mocked(tui.select).mockResolvedValue('discord'); + vi.mocked(tui.input).mockResolvedValue('https://discord.com/api/webhooks/123456789/token-here'); + await program.parseAsync(['node', 'test', 'config', 'setup']); - const { setConfig } = await import('../utils/config'); - expect(select).toHaveBeenCalled(); - expect(input).toHaveBeenCalled(); - expect(setConfig).toHaveBeenCalledWith('notification_service', 'discord'); - expect(setConfig).toHaveBeenCalledWith('discord_webhook', 'https://discord.com/api/webhooks/123/abc'); - expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringMatching(/Discord Webhook configured/i)); + expect(tui.select).toHaveBeenCalled(); + expect(tui.input).toHaveBeenCalled(); + expect(configUtils.clearNotificationCredentials).toHaveBeenCalled(); + expect(configUtils.setConfig).toHaveBeenCalledWith('notification_service', 'discord'); + expect(configUtils.setConfig).toHaveBeenCalledWith('discord_webhook', 'https://discord.com/api/webhooks/123456789/token-here'); }); it('should call select, multiple inputs and setConfig on email setup', async () => { - (select as any).mockResolvedValue('email'); - (input as any) + vi.mocked(tui.select).mockResolvedValue('email'); + vi.mocked(tui.input) .mockResolvedValueOnce('smtp.gmail.com') // host .mockResolvedValueOnce('587') // port .mockResolvedValueOnce('user@test.com') // user @@ -68,36 +74,35 @@ describe('config command', () => { await program.parseAsync(['node', 'test', 'config', 'setup']); - const { setConfig } = await import('../utils/config'); - expect(setConfig).toHaveBeenCalledWith('notification_service', 'email'); - expect(setConfig).toHaveBeenCalledWith('email_host', 'smtp.gmail.com'); - expect(setConfig).toHaveBeenCalledWith('email_port', 587); - expect(setConfig).toHaveBeenCalledWith('email_user', 'user@test.com'); - expect(setConfig).toHaveBeenCalledWith('email_to', 'to@test.com'); + expect(configUtils.setConfig).toHaveBeenCalledWith('notification_service', 'email'); + expect(configUtils.setConfig).toHaveBeenCalledWith('email_host', 'smtp.gmail.com'); + expect(configUtils.setConfig).toHaveBeenCalledWith('email_port', 587); + expect(configUtils.setConfig).toHaveBeenCalledWith('email_user', 'user@test.com'); + expect(configUtils.setConfig).toHaveBeenCalledWith('email_to', 'to@test.com'); }); it('should call setConfig on config set', async () => { await program.parseAsync(['node', 'test', 'config', 'set', 'alert_email', 'test@test.com']); - expect(setConfig).toHaveBeenCalledWith('alert_email', 'test@test.com'); + expect(configUtils.setConfig).toHaveBeenCalledWith('alert_email', 'test@test.com'); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Set alert_email to test@test.com')); }); it('should parse integer for alert_cooldown', async () => { await program.parseAsync(['node', 'test', 'config', 'set', 'alert_cooldown', '123']); - expect(setConfig).toHaveBeenCalledWith('alert_cooldown', 123); + expect(configUtils.setConfig).toHaveBeenCalledWith('alert_cooldown', 123); }); it('should call getConfig on config list', async () => { - (getConfig as any).mockReturnValue({ alert_cooldown: 100 }); + vi.mocked(configUtils.getConfig).mockReturnValue({ alert_cooldown: 100 }); await program.parseAsync(['node', 'test', 'config', 'list']); - expect(getConfig).toHaveBeenCalled(); + expect(configUtils.getConfig).toHaveBeenCalled(); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('alert_cooldown')); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('100')); }); it('should call clearConfig on config clear', async () => { await program.parseAsync(['node', 'test', 'config', 'clear']); - expect(clearConfig).toHaveBeenCalled(); + expect(configUtils.clearConfig).toHaveBeenCalled(); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Configuration cleared')); }); }); diff --git a/src/commands/config.ts b/src/commands/config.ts index d9446d9..50c515f 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,6 +1,6 @@ import { Command } from 'commander'; import chalk from 'chalk'; -import { setConfig, getConfig, clearConfig } from '../utils/config'; +import { setConfig, getConfig, clearConfig, clearNotificationCredentials } from '../utils/config'; import { select, input } from '@vr_patel/tui'; export const registerConfigCommand = (program: Command) => { From 6177220d0a00db9938f770f545f50af11ae531c3 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Thu, 14 May 2026 13:26:43 +0530 Subject: [PATCH 6/7] chore: add .coderabbit.yaml for enhanced PR reviews --- .coderabbit.yaml | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..238a282 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,43 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: "en-US" + +tone_instructions: | + You are a Senior Software Engineer and Security Specialist. + When reviewing, focus on: + 1. **Security**: Ensure no secrets are leaked and input validation is robust (especially for URLs and ports). + 2. **Performance**: Look for inefficient async operations (e.g., Promise.all vs. Promise.allSettled). + 3. **TUI/UX**: Since this is a CLI tool, ensure the interactive flows are intuitive and handle edge cases like cancellations. + 4. **Test Coverage**: Ensure all new logic is covered by vitest unit tests. + +reviews: + profile: "assertive" + high_level_summary: true + auto_review: + enabled: true + drafts: false + + path_filters: + - "!dist/**" + - "!node_modules/**" + - "!package-lock.json" + - "src/**" + - ".github/workflows/**" + + path_instructions: + - path: "src/commands/**/*.ts" + instructions: | + - Verify that all commands follow the modular registration pattern. + - Ensure errors are handled gracefully and logged using the project's logger. + - For TUI interactions, verify that `@vr_patel/tui` tools are used correctly. + - path: "src/utils/config.ts" + instructions: | + - Ensure configuration keys are type-safe. + - Verify that sensitive information is not stored in plain text if possible. + - path: "src/__tests__/**/*.ts" + instructions: | + - Ensure mocks are clean and shared correctly. + - Verify that tests cover both happy paths and error conditions. + + pre_merge_checks: + linked_issue_assessment: + mode: "warning" From 92b927a41b5539abf973fb2d3c0c4c3994d4b030 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Thu, 14 May 2026 13:29:15 +0530 Subject: [PATCH 7/7] refactor: remove redundant inline imports in tests for better mock tracking --- src/__tests__/show.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/show.test.ts b/src/__tests__/show.test.ts index 27c3254..7d5ec82 100644 --- a/src/__tests__/show.test.ts +++ b/src/__tests__/show.test.ts @@ -3,6 +3,7 @@ import { getRunningContainers } from '../docker/containers'; import { getRunningPods } from '../kubernetes/pods'; import { logger } from '../utils/logger'; import { showRunners, showPods } from '../commands/show'; +import * as tableUtils from '../ui/table'; // Mock dependencies vi.mock('../docker/containers', () => ({ @@ -43,8 +44,7 @@ describe('show command runners', () => { // Verify: Warning was logged for Kubernetes, but no crash expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Kubernetes is unreachable')); // Verify: Table still rendered with Docker data - const { renderTable } = await import('../ui/table'); - expect(renderTable).toHaveBeenCalled(); + expect(tableUtils.renderTable).toHaveBeenCalled(); }); it('should handle both services failing gracefully in showRunners', async () => {