From 8857fde9e9719a9a635dafa9fc8350021b55d6ba Mon Sep 17 00:00:00 2001 From: phukon Date: Wed, 15 Jan 2025 01:43:39 +0530 Subject: [PATCH 1/5] refactor(error): implement consistent error handling system BREAKING CHANGE: GitKeyKitCodes enum changed to string literals - Replace numeric error codes with string literals for better debugging - Implement consistent error handling across all modules - Add proper error propagation chain - Improve error messages and debugging information - Add detailed error logging - Centralize error handling in CLI layer --- bin/index.ts | 103 +++++++++++++++++---------------- src/commands/reset.ts | 78 +++++++++++++------------ src/commands/start.ts | 57 ++++++------------ src/gitkeykitCodes.ts | 103 +++++++++++++++++---------------- src/systemCheck.ts | 70 ++++++++++------------ src/utils/checkDependencies.ts | 21 ------- src/utils/checkSecretKeys.ts | 44 +++++++++----- src/utils/createKey.ts | 37 +++++++----- src/utils/linuxConfig.ts | 68 +++++++++++----------- src/utils/setGitConfig.ts | 90 ++++++++++++++-------------- 10 files changed, 323 insertions(+), 348 deletions(-) delete mode 100644 src/utils/checkDependencies.ts diff --git a/bin/index.ts b/bin/index.ts index c1555e0..065709e 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -1,31 +1,29 @@ #!/usr/bin/env node -import arg from "arg"; +import arg, { ArgError } from "arg"; import chalk from "chalk"; -import { fileURLToPath } from 'url'; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import { readFileSync } from "fs"; +import boxen from "boxen"; import { start } from "../src/commands/start"; import { reset } from "../src/commands/reset"; import { importKey } from "../src/commands/import"; +import { GitKeyKitError, GitKeyKitCodes } from "../src/gitkeykitCodes"; import createLogger from "../src/utils/logger"; -import boxen from 'boxen'; -import { GitKeyKitCodes } from "../src/gitkeykitCodes"; -import { dirname, join } from "path"; -import { readFileSync } from "fs"; -process.on("SIGINT", () => process.exit(GitKeyKitCodes.SUCCESS)); -process.on("SIGTERM", () => process.exit(GitKeyKitCodes.SUCCESS)); +const logger = createLogger("bin"); + +process.on("SIGINT", () => process.exit(0)); +process.on("SIGTERM", () => process.exit(0)); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const packageJson = JSON.parse( - readFileSync(join(__dirname, '../package.json'), 'utf8') -); +const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8")); const { version } = packageJson; -const logger = createLogger("bin"); - function usage() { console.log("\n"); - console.log(chalk.blueBright(boxen('GitKeyKit - Simplify PGP key🔑 setup and signing commits on Linux and Windows machines.', {padding: 1, borderStyle: 'round'}))); + console.log(chalk.blueBright(boxen("GitKeyKit - Simplify PGP key🔑 setup and signing commits on Linux and Windows machines.", { padding: 1, borderStyle: "round" }))); console.log(chalk.whiteBright("Usage: gitkeykit\n")); console.log(chalk.whiteBright("Options:")); console.log(chalk.blueBright("--reset\t\t\tReset Git and GPG configurations")); @@ -43,85 +41,90 @@ function usage() { console.log("\n"); } -async function handleImport(keyPath: string): Promise { +async function handleImport(keyPath: string): Promise { try { - await importKey(keyPath); + importKey(keyPath); logger.log(`Imported key from ${keyPath}`); await start(); - return GitKeyKitCodes.SUCCESS; } catch (error) { - console.error(`Error importing key from ${keyPath}:`, error); - return GitKeyKitCodes.ERR_KEY_IMPORT; + if (error instanceof GitKeyKitError) { + throw error; + } + throw new GitKeyKitError(`Failed to import key from ${keyPath}`, GitKeyKitCodes.KEY_IMPORT_ERROR, error); } } -async function handleReset(): Promise { +async function handleReset(): Promise { try { - reset(); - return GitKeyKitCodes.SUCCESS; - } catch (error: any) { - logger.warning((error as Error).message); - console.log(); - usage(); - return GitKeyKitCodes.ERR_GIT_CONFIG_RESET; + await reset(); + } catch (error) { + if (error instanceof GitKeyKitError) { + throw error; + } + throw new GitKeyKitError("Failed to reset configurations", GitKeyKitCodes.GIT_CONFIG_RESET_ERROR, error); } } -async function main(): Promise { +async function main(): Promise { try { const args = arg({ "--reset": Boolean, "--help": Boolean, "--import": String, - "--version": Boolean + "--version": Boolean, }); logger.debug("Received args", args); if (Object.keys(args).length === 1) { await start(); - return GitKeyKitCodes.SUCCESS; + return; } if (args["--reset"]) { - return handleReset(); + await handleReset(); + return; } if (args["--help"]) { usage(); - return GitKeyKitCodes.SUCCESS; + return; } if (args["--import"]) { const keyPath = args["--import"]; - return handleImport(keyPath); + await handleImport(keyPath); + return; } if (args["--version"]) { console.log(`v${version}`); - return GitKeyKitCodes.SUCCESS; + return; } usage(); - return GitKeyKitCodes.ERR_INVALID_ARGS; - } catch (error: any) { - if (error?.code === 'ARG_UNKNOWN_OPTION') { + } catch (error) { + if (error instanceof ArgError && error?.code === "ARG_UNKNOWN_OPTION") { logger.error(`Invalid argument: ${error.message}`); - console.log('------'); + console.log("------"); usage(); - return GitKeyKitCodes.ERR_INVALID_ARGS; + process.exit(1); } - - // Handle any other unexpected errors - logger.error('An unexpected error occurred:', error); - return GitKeyKitCodes.ERR_INVALID_ARGS; + + if (error instanceof GitKeyKitError) { + logger.error(`Error: ${error.message} (${error.code})`); + if (error.details) { + logger.debug("Error details:", error.details); + } + process.exit(1); + } + + logger.error("An unexpected error occurred:", error); + process.exit(1); } } -// Execute and handle exit codes -main() - .then(exitCode => process.exit(exitCode)) - .catch(error => { - console.error('Unexpected error:', error); - process.exit(1); - }); \ No newline at end of file +main().catch((error) => { + logger.error("Fatal error:", error); + process.exit(1); +}); diff --git a/src/commands/reset.ts b/src/commands/reset.ts index d3a098f..6eb596f 100644 --- a/src/commands/reset.ts +++ b/src/commands/reset.ts @@ -1,66 +1,64 @@ import { execSync } from "child_process"; import { existsSync, readFileSync, writeFileSync } from "fs"; +import { GitKeyKitCodes, GitKeyKitError } from "../gitkeykitCodes"; +import { platform, homedir } from "os"; +import { join } from "path"; import createLogger from "../utils/logger"; -import os from "os"; -import path from "path"; -import { GitKeyKitCodes } from "../gitkeykitCodes"; -const logger = createLogger("commands: reset"); +const logger = createLogger("commands:reset"); -function restoreGPGConfig(): GitKeyKitCodes { - const homeDir = os.homedir(); +async function restoreGpgConfig(): Promise { + const homeDir = homedir(); if (!homeDir) { - logger.error("Error: Could not get the home directory"); - return GitKeyKitCodes.ERR_HOME_DIRECTORY_NOT_FOUND; + throw new GitKeyKitError("Could not get home directory", GitKeyKitCodes.HOME_DIR_NOT_FOUND); } - const gnupgDir = path.join(homeDir, ".gnupg"); - const gpgConfPath = path.join(gnupgDir, "gpg.conf"); + const gnupgDir = join(homeDir, ".gnupg"); + const gpgConfPath = join(gnupgDir, "gpg.conf"); const backupPath = `${gpgConfPath}.backup`; if (existsSync(backupPath)) { try { - const backupContent = readFileSync(backupPath, 'utf-8'); + const backupContent = readFileSync(backupPath, "utf-8"); writeFileSync(gpgConfPath, backupContent); - writeFileSync(backupPath, ''); + writeFileSync(backupPath, ""); logger.log("GPG configuration restored from backup."); - return GitKeyKitCodes.SUCCESS; + return; } catch (error) { - logger.error("Error: Could not restore GPG configuration from backup"); - return GitKeyKitCodes.ERR_GPG_CONFIG_RESET; + throw new GitKeyKitError("Could not restore GPG configuration from backup", GitKeyKitCodes.GPG_CONFIG_RESET_ERROR, error); } } - return clearGPGConfig(); + await clearGpgConfig(); } -function clearGPGConfig(): GitKeyKitCodes { - const homeDir = os.homedir(); +async function clearGpgConfig(): Promise { + const homeDir = homedir(); if (!homeDir) { - logger.error("Error: Could not get the home directory"); - return GitKeyKitCodes.ERR_HOME_DIRECTORY_NOT_FOUND; + throw new GitKeyKitError("Could not get home directory", GitKeyKitCodes.HOME_DIR_NOT_FOUND); } - const gnupgDir = path.join(homeDir, ".gnupg"); - const gpgConfPath = path.join(gnupgDir, "gpg.conf"); + const gnupgDir = join(homeDir, ".gnupg"); + const gpgConfPath = join(gnupgDir, "gpg.conf"); // If .gnupg directory doesn't exist, nothing to clear if (!existsSync(gnupgDir)) { - return GitKeyKitCodes.SUCCESS; + return; } try { - // Write empty content to gpg.conf writeFileSync(gpgConfPath, ""); logger.log("GPG configuration cleared."); - return GitKeyKitCodes.SUCCESS; } catch (error) { - logger.error("Error: Could not open gpg.conf for clearing"); - return GitKeyKitCodes.ERR_GPG_CONFIG_RESET; + throw new GitKeyKitError("Could not open gpg.conf for clearing", GitKeyKitCodes.GPG_CONFIG_RESET_ERROR, error); } } -export function reset(): GitKeyKitCodes { +/** + * Resets Git and GPG configurations + * @throws {GitKeyKitError} If reset operation fails + */ +export async function reset(): Promise { try { const gitCommands = [ "git config --global --unset user.name", @@ -68,22 +66,28 @@ export function reset(): GitKeyKitCodes { "git config --global --unset user.signingkey", "git config --global --unset commit.gpgsign", "git config --global --unset tag.gpgsign", - "git config --global --unset gpg.program" + "git config --global --unset gpg.program", ]; for (const cmd of gitCommands) { - execSync(cmd); + try { + execSync(cmd); + } catch (error) { + if (!(error as any)?.message?.includes("key does not exist")) { + throw error; + } + } } logger.log("Git configuration reset successfully."); - if (os.platform() === "linux") { - return restoreGPGConfig(); + if (platform() === "linux") { + await restoreGpgConfig(); } - - return GitKeyKitCodes.SUCCESS; } catch (error) { - logger.error("Error: Failed to reset git configuration."); - return GitKeyKitCodes.ERR_GIT_CONFIG_RESET; + if (error instanceof GitKeyKitError) { + throw error; + } + throw new GitKeyKitError("Failed to reset git configuration", GitKeyKitCodes.GIT_CONFIG_RESET_ERROR, error); } -} \ No newline at end of file +} diff --git a/src/commands/start.ts b/src/commands/start.ts index 779a90a..429e49e 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -1,59 +1,40 @@ -import { checkRequiredDependencies } from '../utils/checkDependencies'; +import { checkRequiredDependencies } from '../systemCheck'; import { checkSecretKeys } from '../utils/checkSecretKeys'; import { createPgpKey } from '../utils/createKey'; import { setGitConfig } from '../utils/setGitConfig' import { addExtraConfig } from '../utils/linuxConfig'; import { platform } from 'os'; -// import chalk from 'chalk'; -import { GitKeyKitCodes } from '../gitkeykitCodes'; +import { GitKeyKitError, GitKeyKitCodes } from '../gitkeykitCodes'; import createLogger from '../utils/logger'; const logger = createLogger('commands:start'); -export async function start(): Promise { +export async function start(): Promise { try { - // Check dependencies - const { code, gpgPath } = await checkRequiredDependencies(); - if (code !== GitKeyKitCodes.SUCCESS) { - return code; - } + const gpgPath = await checkRequiredDependencies(); - if (!gpgPath) { - logger.error('GPG path not found'); - return GitKeyKitCodes.ERR_GPG_NOT_FOUND; - } + await checkSecretKeys(); + + await createPgpKey(); - // Check for existing GPG keys - const secretKeyResult = await checkSecretKeys(); - if (secretKeyResult !== GitKeyKitCodes.SUCCESS) { - // Create new key if none exists - const keyResult = await createPgpKey(); - if (keyResult !== GitKeyKitCodes.SUCCESS) { - return keyResult; - } - } + await setGitConfig(gpgPath); - // Configure git - const gitConfigResult = await setGitConfig(gpgPath); - if (gitConfigResult !== GitKeyKitCodes.SUCCESS) { - return gitConfigResult; - } - - // Add extra configuration for non-Windows platforms if (platform() !== 'win32') { - const configResult = await addExtraConfig(); - if (configResult !== GitKeyKitCodes.SUCCESS) { - return configResult; - } + await addExtraConfig(); } logger.green('Setup complete. Happy coding! 🎉'); - return GitKeyKitCodes.SUCCESS; - } catch (error) { - if (error instanceof Error) { - logger.error(`Unexpected error: ${error.message}`); + if (error instanceof GitKeyKitError) { + logger.error(`Setup failed: ${error.message}`); + logger.debug('Error details:', error.details); + throw error; // Re-throwing... to be handled by the CLI } - return GitKeyKitCodes.ERR_INVALID_INPUT; + + throw new GitKeyKitError( + 'Unexpected error during setup', + GitKeyKitCodes.INVALID_INPUT, + error + ); } } \ No newline at end of file diff --git a/src/gitkeykitCodes.ts b/src/gitkeykitCodes.ts index 75e397d..76691bf 100644 --- a/src/gitkeykitCodes.ts +++ b/src/gitkeykitCodes.ts @@ -1,62 +1,63 @@ +export class GitKeyKitError extends Error { + constructor( + message: string, + public code: string, + public details?: any + // NOTE: no need to manually assign 'this.code = code' + // because the 'public' keyword does it automatically, + // it's a Typescript feature! + ) { + super(message); + this.name = 'GitKeyKitError'; + Object.setPrototypeOf(this, GitKeyKitError.prototype); + } +} + /** * Error codes for GitKeyKit operations */ -export enum GitKeyKitCodes { +export const GitKeyKitCodes = { // Success - SUCCESS = 0, + SUCCESS: 'SUCCESS', - // Dependency Errors (1-2) - ERR_GPG_NOT_FOUND = 1, - ERR_GIT_NOT_FOUND = 2, + // Dependency Errors + GPG_NOT_FOUND: 'GPG_NOT_FOUND', + GIT_NOT_FOUND: 'GIT_NOT_FOUND', - // Input/Argument Errors (3-5) - ERR_INVALID_ARGS = 3, - ERR_NO_SECRET_KEYS = 4, - ERR_INVALID_INPUT = 5, + // Input/Argument Errors + INVALID_ARGS: 'INVALID_ARGS', + NO_SECRET_KEYS: 'NO_SECRET_KEYS', + INVALID_INPUT: 'INVALID_INPUT', - // Configuration Errors (6, 9-10) - ERR_GIT_CONFIG = 6, - ERR_GIT_CONFIG_RESET = 9, - ERR_GPG_CONFIG_RESET = 10, + // Configuration Errors + GIT_CONFIG_ERROR: 'GIT_CONFIG_ERROR', + GIT_CONFIG_RESET_ERROR: 'GIT_CONFIG_RESET_ERROR', + GPG_CONFIG_RESET_ERROR: 'GPG_CONFIG_RESET_ERROR', - // Key Operation Errors (7-8) - ERR_KEY_GENERATION = 7, - ERR_KEY_IMPORT = 8, + // Key Operation Errors + KEY_GENERATION_ERROR: 'KEY_GENERATION_ERROR', + KEY_IMPORT_ERROR: 'KEY_IMPORT_ERROR', - // System Errors (11-12) - ERR_HOME_DIRECTORY_NOT_FOUND = 11, - ERR_BUFFER_OVERFLOW = 12, -} + // System Errors + HOME_DIR_NOT_FOUND: 'HOME_DIR_NOT_FOUND' +} as const; -export function getCodeMessage(code: GitKeyKitCodes): string { - switch (code) { - case GitKeyKitCodes.SUCCESS: - return 'Operation completed successfully'; - case GitKeyKitCodes.ERR_GPG_NOT_FOUND: - return 'GPG installation not found'; - case GitKeyKitCodes.ERR_GIT_NOT_FOUND: - return 'Git installation not found'; - case GitKeyKitCodes.ERR_INVALID_ARGS: - return 'Invalid arguments provided'; - case GitKeyKitCodes.ERR_NO_SECRET_KEYS: - return 'No GPG secret keys found'; - case GitKeyKitCodes.ERR_INVALID_INPUT: - return 'Invalid input provided'; - case GitKeyKitCodes.ERR_GIT_CONFIG: - return 'Git configuration error'; - case GitKeyKitCodes.ERR_KEY_GENERATION: - return 'Error generating GPG key'; - case GitKeyKitCodes.ERR_KEY_IMPORT: - return 'Error importing GPG key'; - case GitKeyKitCodes.ERR_GIT_CONFIG_RESET: - return 'Error resetting Git configuration'; - case GitKeyKitCodes.ERR_GPG_CONFIG_RESET: - return 'Error resetting GPG configuration'; - case GitKeyKitCodes.ERR_HOME_DIRECTORY_NOT_FOUND: - return 'Home directory not found'; - case GitKeyKitCodes.ERR_BUFFER_OVERFLOW: - return 'Buffer overflow error'; - default: - return 'Unknown error'; - } +export type GitKeyKitCodeType = typeof GitKeyKitCodes[keyof typeof GitKeyKitCodes]; + +export function getErrorMessage(code: GitKeyKitCodeType): string { + const messages = { + [GitKeyKitCodes.SUCCESS]: 'Operation completed successfully', + [GitKeyKitCodes.GPG_NOT_FOUND]: 'GPG installation not found', + [GitKeyKitCodes.GIT_NOT_FOUND]: 'Git installation not found', + [GitKeyKitCodes.INVALID_ARGS]: 'Invalid arguments provided', + [GitKeyKitCodes.NO_SECRET_KEYS]: 'No secret GPG keys found', + [GitKeyKitCodes.INVALID_INPUT]: 'Invalid input provided', + [GitKeyKitCodes.GIT_CONFIG_ERROR]: 'Error configuring Git settings', + [GitKeyKitCodes.GIT_CONFIG_RESET_ERROR]: 'Error resetting Git configuration', + [GitKeyKitCodes.GPG_CONFIG_RESET_ERROR]: 'Error resetting GPG configuration', + [GitKeyKitCodes.KEY_GENERATION_ERROR]: 'Error generating GPG key', + [GitKeyKitCodes.KEY_IMPORT_ERROR]: 'Error importing GPG key', + [GitKeyKitCodes.HOME_DIR_NOT_FOUND]: 'Home directory not found', + }; + return messages[code] || 'Unknown error'; } diff --git a/src/systemCheck.ts b/src/systemCheck.ts index 7160154..0079727 100644 --- a/src/systemCheck.ts +++ b/src/systemCheck.ts @@ -1,69 +1,63 @@ -import { exec } from 'child_process'; -import { promisify } from 'util'; -import { GitKeyKitCodes } from './gitkeykitCodes'; -import { platform } from 'os'; +import { exec } from "child_process"; +import { promisify } from "util"; +import { GitKeyKitCodes, GitKeyKitError } from "./gitkeykitCodes"; +import { platform } from "os"; const execAsync = promisify(exec); const COMMANDS = { - GPG_CHECK: platform() === 'win32' ? 'where gpg' : 'which gpg', - GIT_CHECK: platform() === 'win32' ? 'where git' : 'which git' + GPG_CHECK: platform() === "win32" ? "where gpg" : "which gpg", + GIT_CHECK: platform() === "win32" ? "where git" : "which git", }; /** * Checks if GPG is installed and returns its path - * @returns Promise resolving to GPG path or error code + * @throws {GitKeyKitError} If GPG is not found or check fails */ -export async function checkGpgInstallation(): Promise<{ code: GitKeyKitCodes, path?: string }> { +export async function checkGpgInstallation(): Promise { try { const { stdout } = await execAsync(COMMANDS.GPG_CHECK); const gpgPath = stdout.trim(); - - if (gpgPath) { - return { - code: GitKeyKitCodes.SUCCESS, - path: gpgPath - }; + + if (!gpgPath) { + throw new GitKeyKitError("GPG installation not found", GitKeyKitCodes.GPG_NOT_FOUND); } - - return { - code: GitKeyKitCodes.ERR_GPG_NOT_FOUND - }; + + return gpgPath; } catch (error) { - return { - code: GitKeyKitCodes.ERR_GPG_NOT_FOUND - }; + throw new GitKeyKitError("Failed to check GPG installation", GitKeyKitCodes.GPG_NOT_FOUND, error); } } /** * Checks if Git is installed - * @returns Promise resolving to success or error code + * @throws {GitKeyKitError} If Git is not found or check fails */ -export async function checkGitInstallation(): Promise { +export async function checkGitInstallation(): Promise { try { await execAsync(COMMANDS.GIT_CHECK); - return GitKeyKitCodes.SUCCESS; } catch (error) { - return GitKeyKitCodes.ERR_GIT_NOT_FOUND; + throw new GitKeyKitError("Git installation not found", GitKeyKitCodes.GIT_NOT_FOUND, error); } } /** * Checks all required dependencies - * @returns Promise resolving to GPG path or error code + * @returns Promise resolving to GPG path + * @throws {GitKeyKitError} If any dependency check fails */ -export async function checkRequiredDependencies(): Promise<{ code: GitKeyKitCodes, gpgPath?: string }> { - // Check Git first - const gitCheck = await checkGitInstallation(); - if (gitCheck !== GitKeyKitCodes.SUCCESS) { - return { code: gitCheck }; - } +export async function checkRequiredDependencies(): Promise { + try { + + await checkGitInstallation(); + const gpgPath = await checkGpgInstallation(); + return gpgPath; - // Then check GPG - const gpgCheck = await checkGpgInstallation(); - return { - code: gpgCheck.code, - gpgPath: gpgCheck.path - }; + } catch (error) { + if (error instanceof GitKeyKitError) { + throw error; + } + + throw new GitKeyKitError("Failed to check dependencies", GitKeyKitCodes.INVALID_INPUT, error); + } } diff --git a/src/utils/checkDependencies.ts b/src/utils/checkDependencies.ts deleted file mode 100644 index c9fc6c4..0000000 --- a/src/utils/checkDependencies.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { GitKeyKitCodes } from "../gitkeykitCodes"; -import { checkGitInstallation, checkGpgInstallation } from "../systemCheck"; - -/** - * Checks all required dependencies - * @returns Promise resolving to GPG path or error code - */ -export async function checkRequiredDependencies(): Promise<{ code: GitKeyKitCodes, gpgPath?: string }> { - // Check Git first - const gitCheck = await checkGitInstallation(); - if (gitCheck !== GitKeyKitCodes.SUCCESS) { - return { code: gitCheck }; - } - - // Then check GPG - const gpgCheck = await checkGpgInstallation(); - return { - code: gpgCheck.code, - gpgPath: gpgCheck.path - }; -} \ No newline at end of file diff --git a/src/utils/checkSecretKeys.ts b/src/utils/checkSecretKeys.ts index e4995c3..565c4a8 100644 --- a/src/utils/checkSecretKeys.ts +++ b/src/utils/checkSecretKeys.ts @@ -1,28 +1,40 @@ -import { spawn } from 'child_process'; -import { GitKeyKitCodes } from "../gitkeykitCodes"; +import { spawn } from "child_process"; +import { GitKeyKitCodes, GitKeyKitError } from "../gitkeykitCodes"; + +import createLogger from "./logger"; + +const logger = createLogger("utils:checkSecretKeys"); /** * Checks if GPG secret keys exist on the system - * @returns Promise that resolves to a GitKeyKitCodes value + * @throws {GitKeyKitError} If no secret keys found or check fails */ -export async function checkSecretKeys(): Promise { - return new Promise((resolve) => { - const gpgProcess = spawn('gpg', ['--list-secret-keys']); - let output: string = ''; +export async function checkSecretKeys(): Promise { + return new Promise((resolve, reject) => { + const gpgProcess = spawn("gpg", ["--list-secret-keys"]); + let output: string = ""; - gpgProcess.stdout.on('data', (data: Buffer) => { + gpgProcess.stdout.on("data", (data: Buffer) => { output += data.toString(); }); - gpgProcess.on('error', () => { - resolve(GitKeyKitCodes.ERR_NO_SECRET_KEYS); + gpgProcess.on("error", (error) => { + reject(new GitKeyKitError("Failed to check GPG secret keys", GitKeyKitCodes.NO_SECRET_KEYS, error)); }); - gpgProcess.on('close', () => { - if (output.includes('sec')) { - resolve(GitKeyKitCodes.SUCCESS); - } else { - resolve(GitKeyKitCodes.ERR_NO_SECRET_KEYS); + + gpgProcess.on("close", (code) => { + if (code !== 0) { + reject(new GitKeyKitError("GPG process exited with non-zero code", GitKeyKitCodes.NO_SECRET_KEYS, { exitCode: code })); + return; } + + if (!output.includes("sec")) { + reject(new GitKeyKitError("No GPG secret keys found", GitKeyKitCodes.NO_SECRET_KEYS)); + return; + } + + logger.debug("Found existing GPG secret keys"); + resolve(); }); }); -} \ No newline at end of file +} diff --git a/src/utils/createKey.ts b/src/utils/createKey.ts index b8bd966..2a3300e 100644 --- a/src/utils/createKey.ts +++ b/src/utils/createKey.ts @@ -1,9 +1,15 @@ import { spawn } from "child_process"; import confirm from "@inquirer/confirm"; -import chalk from "chalk"; -import { GitKeyKitCodes } from "../gitkeykitCodes"; +import { GitKeyKitCodes, GitKeyKitError } from "../gitkeykitCodes"; +import createLogger from "./logger"; -export async function createPgpKey(): Promise { +const logger = createLogger("utils:createKey"); + +/** + * Creates a new PGP key + * @throws {GitKeyKitError} If key creation fails or is aborted + */ +export async function createPgpKey(): Promise { try { const shouldCreate = await confirm({ message: "Do you want to create a new PGP key?", @@ -11,36 +17,35 @@ export async function createPgpKey(): Promise { }); if (!shouldCreate) { - console.log(chalk.yellow("Aborting key creation.")); - return GitKeyKitCodes.SUCCESS; + logger.highlight("User aborted key creation"); + return; } - console.log(chalk.blue("Creating new PGP key...")); + logger.blue("Creating new PGP key..."); - return new Promise((resolve) => { + await new Promise((resolve, reject) => { const gpg = spawn("gpg", ["--full-generate-key"], { stdio: "inherit", }); gpg.on("error", (error) => { - console.error(chalk.red(`Failed to start GPG process: ${error.message}`)); - resolve(GitKeyKitCodes.ERR_KEY_GENERATION); + reject(new GitKeyKitError("Failed to start GPG process", GitKeyKitCodes.KEY_GENERATION_ERROR, error)); }); gpg.on("close", (code) => { if (code === 0) { - console.log(chalk.green("GPG key has been generated successfully.")); - resolve(GitKeyKitCodes.SUCCESS); + logger.green("GPG key has been generated successfully."); + resolve(); } else { - console.error(chalk.red("Error: Failed to generate GPG key.")); - resolve(GitKeyKitCodes.ERR_KEY_GENERATION); + reject(new GitKeyKitError("Failed to generate GPG key", GitKeyKitCodes.KEY_GENERATION_ERROR, { exitCode: code })); } }); }); } catch (error) { - if (error instanceof Error) { - console.error(chalk.red(`Error: ${error.message}`)); + if (error instanceof GitKeyKitError) { + throw error; } - return GitKeyKitCodes.ERR_KEY_GENERATION; + + throw new GitKeyKitError("Unexpected error during key creation", GitKeyKitCodes.KEY_GENERATION_ERROR, error); } } diff --git a/src/utils/linuxConfig.ts b/src/utils/linuxConfig.ts index 36330e3..ceb501f 100644 --- a/src/utils/linuxConfig.ts +++ b/src/utils/linuxConfig.ts @@ -5,86 +5,86 @@ import { promisify } from "util"; import { homedir } from "os"; import chalk from "chalk"; import boxen from "boxen"; -import { GitKeyKitCodes } from "../gitkeykitCodes"; +import { GitKeyKitCodes, GitKeyKitError } from "../gitkeykitCodes"; +import createLogger from "./logger"; +const logger = createLogger("utils:linuxConfig"); const execAsync = promisify(exec); async function backupGpgConfig(gpgConfPath: string): Promise { try { await access(gpgConfPath); - const existingConfig = await readFile(gpgConfPath, 'utf-8'); + const existingConfig = await readFile(gpgConfPath, "utf-8"); const backupPath = `${gpgConfPath}.backup`; await writeFile(backupPath, existingConfig); + logger.debug(`Backed up GPG config to ${backupPath}`); } catch (error) { // File doesn't exist, no backup needed - if ((error as { code?: string }).code === 'ENOENT') { + if ((error as { code?: string }).code === "ENOENT") { + logger.debug("No existing GPG config to backup"); return; } - throw error; + throw new GitKeyKitError("Failed to backup GPG config", GitKeyKitCodes.GPG_CONFIG_RESET_ERROR, error); } } async function createDirectory(path: string): Promise { try { await mkdir(path, { mode: 0o700, recursive: true }); + logger.debug(`Created directory: ${path}`); } catch (error) { if ((error as { code?: string }).code !== "EEXIST") { - throw error; + throw new GitKeyKitError(`Failed to create directory: ${path}`, GitKeyKitCodes.HOME_DIR_NOT_FOUND, error); } } } async function appendToFile(filepath: string, content: string): Promise { - await appendFile(filepath, content + "\n"); + try { + await appendFile(filepath, content + "\n"); + logger.debug(`Appended to file ${filepath}: ${content}`); + } catch (error) { + throw new GitKeyKitError(`Failed to write to file: ${filepath}`, GitKeyKitCodes.GPG_CONFIG_RESET_ERROR, error); + } } -export async function addExtraConfig(): Promise { +export async function addExtraConfig(): Promise { try { const homeDir = homedir(); if (!homeDir) { - console.error(chalk.red("Error: Could not get home directory")); - return GitKeyKitCodes.ERR_INVALID_INPUT; + throw new GitKeyKitError("Could not get home directory", GitKeyKitCodes.HOME_DIR_NOT_FOUND); } const gnupgDir = join(homeDir, ".gnupg"); const gpgConfPath = join(gnupgDir, "gpg.conf"); - await backupGpgConfig(gpgConfPath); - try { - await createDirectory(gnupgDir); - } catch (error) { - console.error(chalk.red("Error: Could not create .gnupg directory")); - return GitKeyKitCodes.ERR_INVALID_INPUT; - } + await backupGpgConfig(gpgConfPath); + await createDirectory(gnupgDir); try { - await writeFile(gpgConfPath, "", {flag: 'a'}); + await writeFile(gpgConfPath, "", { flag: "a" }); } catch (error) { - console.error(chalk.red("Error: Could not open gpg.conf")); - return GitKeyKitCodes.ERR_INVALID_INPUT; + throw new GitKeyKitError("Could not open gpg.conf", GitKeyKitCodes.GPG_CONFIG_RESET_ERROR, error); } - try { - await appendToFile(gpgConfPath, "use-agent"); - await appendToFile(gpgConfPath, "pinentry-mode loopback"); - } catch (error) { - console.error(chalk.red("Error: Could not write to gpg.conf")); - return GitKeyKitCodes.ERR_INVALID_INPUT; - } + await appendToFile(gpgConfPath, "use-agent"); + await appendToFile(gpgConfPath, "pinentry-mode loopback"); // kill and restart gpg-agent try { await execAsync("gpgconf --kill gpg-agent"); - console.log(chalk.green("gpg-agent killed.")); + logger.green("gpg-agent killed."); } catch (error) { - console.warn(chalk.yellow("Warning: Could not kill gpg-agent")); + logger.warning("Warning: Could not kill gpg-agent"); + logger.debug("Kill gpg-agent error:", error); } try { await execAsync("gpg-agent --daemon"); - console.log(chalk.green("gpg-agent restarted.")); + logger.green("gpg-agent restarted."); } catch (error) { - console.warn(chalk.yellow("Warning: Could not start gpg-agent")); + logger.warning("Warning: Could not start gpg-agent"); + logger.debug("Start gpg-agent error:", error); } console.log( @@ -94,10 +94,10 @@ export async function addExtraConfig(): Promise { borderStyle: "round", }) ); - - return GitKeyKitCodes.SUCCESS; } catch (error) { - console.error(chalk.red(`Unexpected error: ${error}`)); - return GitKeyKitCodes.ERR_INVALID_INPUT; + if (error instanceof GitKeyKitError) { + throw error; + } + throw new GitKeyKitError("Failed to configure GPG", GitKeyKitCodes.GPG_CONFIG_RESET_ERROR, error); } } diff --git a/src/utils/setGitConfig.ts b/src/utils/setGitConfig.ts index 39b3d7c..2fbcdab 100644 --- a/src/utils/setGitConfig.ts +++ b/src/utils/setGitConfig.ts @@ -1,9 +1,10 @@ -import input from '@inquirer/input'; -import { execFile } from 'child_process'; -import { promisify } from 'util'; -import chalk from 'chalk'; -import { GitKeyKitCodes } from '../gitkeykitCodes'; +import input from "@inquirer/input"; +import { execFile } from "child_process"; +import { promisify } from "util"; +import { GitKeyKitCodes, GitKeyKitError } from "../gitkeykitCodes"; +import createLogger from "./logger"; +const logger = createLogger("utils:setGitConfig"); const execFileAsync = promisify(execFile); async function getGpgKeyFingerprint(): Promise { @@ -24,98 +25,93 @@ async function getGpgKeyFingerprint(): Promise { │ grp:::::::::WXYZ7890ABCD1234EFGH5678IJKL9012MNOP3456: │ └────────────────────────────────────────────────────────────────────────────┘ */ - const { stdout } = await execFileAsync('gpg', ['--list-secret-keys', '--with-colons']); - - const lines = stdout.split('\n'); + const { stdout } = await execFileAsync("gpg", ["--list-secret-keys", "--with-colons"]); + + const lines = stdout.split("\n"); let isPrimaryKey = false; - let keyFingerprint = ''; - + let keyFingerprint = ""; + for (const line of lines) { - const parts = line.split(':'); + const parts = line.split(":"); // Mark when we find a primary key (sec) - if (parts[0] === 'sec') { + if (parts[0] === "sec") { isPrimaryKey = true; continue; } // Get the fingerprint only if it's for the primary key - if (isPrimaryKey && parts[0] === 'fpr') { + if (isPrimaryKey && parts[0] === "fpr") { keyFingerprint = parts[9]; break; } - - if (parts[0] === 'ssb') { + + if (parts[0] === "ssb") { isPrimaryKey = false; } } if (!keyFingerprint) { - throw new Error('No GPG key found'); + throw new GitKeyKitError("No GPG key found", GitKeyKitCodes.NO_SECRET_KEYS); } - console.log(chalk.green(`Found GPG key: ${keyFingerprint}`)); + logger.debug(`Found GPG key: ${keyFingerprint}`); return keyFingerprint; - } catch (error) { - throw new Error('Failed to get GPG key fingerprint'); + if (error instanceof GitKeyKitError) { + throw error; + } + throw new GitKeyKitError("Failed to get GPG key fingerprint", GitKeyKitCodes.NO_SECRET_KEYS, error); } } async function setGitConfigValue(key: string, value: string): Promise { try { - await execFileAsync('git', ['config', '--global', key, value]); + await execFileAsync("git", ["config", "--global", key, value]); } catch (error) { - throw new Error(`Error setting git config ${key}`); + throw new GitKeyKitError(`Error setting git config ${key}`, GitKeyKitCodes.GIT_CONFIG_ERROR, error); } } -export async function setGitConfig(gpgPath: string): Promise { +export async function setGitConfig(gpgPath: string): Promise { try { const username = await input({ - message: 'Enter your name:', - validate: (value) => value.length > 0 || 'Name cannot be empty' + message: "Enter your name:", + validate: (value) => value.length > 0 || "Name cannot be empty", }); const email = await input({ - message: 'Enter your email:', + message: "Enter your email:", validate: (value) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(value) || 'Please enter a valid email'; - } + return emailRegex.test(value) || "Please enter a valid email"; + }, }); - console.log(chalk.blue('Setting git config...')); + logger.blue("Setting git config..."); const keyFingerprint = await getGpgKeyFingerprint(); const configs = [ - ['user.name', username], - ['user.email', email], - ['user.signingkey', keyFingerprint], - ['commit.gpgsign', 'true'], - ['tag.gpgsign', 'true'], - ['gpg.program', gpgPath] + ["user.name", username], + ["user.email", email], + ["user.signingkey", keyFingerprint], + ["commit.gpgsign", "true"], + ["tag.gpgsign", "true"], + ["gpg.program", gpgPath], ]; for (const [key, value] of configs) { try { await setGitConfigValue(key, value); } catch (error) { - console.error(chalk.red(`Error setting ${key}: ${error}`)); - return GitKeyKitCodes.ERR_GIT_CONFIG; + throw new GitKeyKitError(`Failed to set git config: ${key}`, GitKeyKitCodes.GIT_CONFIG_ERROR, error); } } - console.log(chalk.green('Git configurations applied successfully')); - return GitKeyKitCodes.SUCCESS; - + logger.green("Git configurations applied successfully"); } catch (error) { - if (error instanceof Error) { - if (error.message.includes('No GPG key found')) { - console.error(chalk.red('No GPG key found')); - return GitKeyKitCodes.ERR_NO_SECRET_KEYS; - } - console.error(chalk.red(`Error: ${error.message}`)); + if (error instanceof GitKeyKitError) { + throw error; } - return GitKeyKitCodes.ERR_INVALID_INPUT; + throw new GitKeyKitError("Unexpected error during git configuration", GitKeyKitCodes.GIT_CONFIG_ERROR, error); } -} \ No newline at end of file +} From e6aa7a9d1f5dbc2b77f55b6c8496077bbacfb389 Mon Sep 17 00:00:00 2001 From: phukon Date: Wed, 15 Jan 2025 01:43:59 +0530 Subject: [PATCH 2/5] feat: add input validation for key import --- bin/index.ts | 7 ++----- src/commands/import.ts | 44 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/bin/index.ts b/bin/index.ts index 065709e..bcad599 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import arg, { ArgError } from "arg"; +import arg from "arg"; import chalk from "chalk"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; @@ -13,9 +13,6 @@ import createLogger from "../src/utils/logger"; const logger = createLogger("bin"); -process.on("SIGINT", () => process.exit(0)); -process.on("SIGTERM", () => process.exit(0)); - const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8")); @@ -104,7 +101,7 @@ async function main(): Promise { usage(); } catch (error) { - if (error instanceof ArgError && error?.code === "ARG_UNKNOWN_OPTION") { + if (error instanceof arg.ArgError && error?.code === "ARG_UNKNOWN_OPTION") { logger.error(`Invalid argument: ${error.message}`); console.log("------"); usage(); diff --git a/src/commands/import.ts b/src/commands/import.ts index 4540e94..8a56821 100644 --- a/src/commands/import.ts +++ b/src/commands/import.ts @@ -1,10 +1,42 @@ +import { readFileSync } from "fs"; import { execSync } from "child_process"; +import { GitKeyKitCodes, GitKeyKitError } from "../gitkeykitCodes"; +import createLogger from "../utils/logger"; -export function importKey(key: string): boolean { +const logger = createLogger("commands:import"); + +/** + * Imports a GPG key from a file + * @param keyPath Path to the key file + * @throws {GitKeyKitError} If key import fails + */ +export async function importKey(keyPath: string): Promise { try { - execSync(`gpg --import ${key}`, { stdio: "inherit" }); - return true; // Indicate success - } catch (error: any) { - throw new Error(`Error importing key: ${(error as Error).message}`); + let keyContent: string; + try { + keyContent = readFileSync(keyPath, "utf-8"); + } catch (error) { + throw new GitKeyKitError(`Failed to read key file: ${keyPath}`, GitKeyKitCodes.KEY_IMPORT_ERROR, error); + } + + if (!keyContent.includes("-----BEGIN PGP PRIVATE KEY BLOCK-----")) { + throw new GitKeyKitError("Invalid key file format: Missing PGP private key block", GitKeyKitCodes.KEY_IMPORT_ERROR); + } + + try { + execSync("gpg --import", { + input: keyContent, + stdio: ["pipe", "inherit", "inherit"], + }); + + logger.green("GPG key imported successfully"); + } catch (error) { + throw new GitKeyKitError("Failed to import GPG key", GitKeyKitCodes.KEY_IMPORT_ERROR, error); + } + } catch (error) { + if (error instanceof GitKeyKitError) { + throw error; + } + throw new GitKeyKitError("Unexpected error during key import", GitKeyKitCodes.KEY_IMPORT_ERROR, error); } -} \ No newline at end of file +} From cc840e38fd3fa25d9d2b70662f35dbdbed946d85 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 14 Jan 2025 20:42:22 +0000 Subject: [PATCH 3/5] chore(release): 4.0.0-next.1 [skip ci] # [4.0.0-next.1](https://github.com/phukon/gitkeykit/compare/v3.0.0...v4.0.0-next.1) (2025-01-14) ### Code Refactoring * **error:** implement consistent error handling system ([8857fde](https://github.com/phukon/gitkeykit/commit/8857fde9e9719a9a635dafa9fc8350021b55d6ba)) ### Features * add input validation for key import ([e6aa7a9](https://github.com/phukon/gitkeykit/commit/e6aa7a9d1f5dbc2b77f55b6c8496077bbacfb389)) ### BREAKING CHANGES * **error:** GitKeyKitCodes enum changed to string literals - Replace numeric error codes with string literals for better debugging - Implement consistent error handling across all modules - Add proper error propagation chain - Improve error messages and debugging information - Add detailed error logging - Centralize error handling in CLI layer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 32a1860..daa8ed2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitkeykit", - "version": "3.0.0", + "version": "4.0.0-next.1", "description": "Setup pgp keys and sign commits with ease on Linux and Windows machines.", "main": "./dist/index.js", "module": "./dist/index.mjs", From feca9260cdf3deb8244d6905e946d15433cd7b39 Mon Sep 17 00:00:00 2001 From: phukon Date: Wed, 15 Jan 2025 02:17:27 +0530 Subject: [PATCH 4/5] docs: update README --- README.md | 55 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index b2df0ff..57e52e1 100644 --- a/README.md +++ b/README.md @@ -6,32 +6,53 @@ Simplify PGP key setup and signing commits on Linux and Windows. -## 📦 Usage +## 📦 Installation ```bash +# Using npx (recommended) npx gitkeykit -``` -or -```bash + +# Or install globally npm install -g gitkeykit ``` -## Features +## 🚀 Usage + +### Basic Setup +```bash +# Start the interactive setup +gitkeykit -- **Effortless PGP Key Management**: Create or import PGP keys with ease to secure your Git commits. -- **Cross-Platform Compatibility**: Works seamlessly on both Linux and Windows machines, ensuring a consistent experience across environments. -- **Git and GPG Configuration**: Automatically configure Git and GPG settings for seamless integration with your workflow. -- **Secure Passphrase Entry**: Enhance security with pinentry-mode loopback, ensuring passphrases are entered securely. -- **Fast and Efficient Operation**: Enjoy a lightning-fast CLI tool that gets the job done quickly and efficiently. +# Import existing PGP key +gitkeykit import my_key.txt + +# Reset configurations +gitkeykit --reset + +# Show version number +gitkeykit --verion + +# Display help information and available commands +gitkeykit --help +``` +### Command Options +- `--reset` Reset Git and GPG configurations +- `--help` Show help information +- `--version` Show version number +- `--import ` Import and configure PGP key from file +## ✨ Features -#### Options: - `--reset` Reset Git and GPG configurations +- **Interactive Setup**: Guided process for creating or importing PGP keys +- **Cross-Platform**: Works seamlessly on both Linux and Windows +- **Secure Configuration**: + - Automatic Git signing setup + - GPG agent configuration + - Secure passphrase handling +- **Error Handling**: Clear error messages and recovery options +- **Backup & Reset**: Automatic backup of existing configurations with reset capability -#### Commands: - `import ` Import and set configuration with the provided PGP key -Examples: - `gitkeykit import my_key.txt` Import and set configuration with 'my_key.txt' - `gitkeykit --reset` Reset all configurations` +## 🤝 Contributing +Contributions are welcome! Please feel free to submit a Pull Request. \ No newline at end of file From 377cc344a914de0642bd7c9dbd809675ef7d5b34 Mon Sep 17 00:00:00 2001 From: phukon Date: Wed, 15 Jan 2025 02:22:30 +0530 Subject: [PATCH 5/5] fix: correct typo in README and ensure async key import handling --- README.md | 2 +- bin/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 57e52e1..4ba4000 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ gitkeykit import my_key.txt gitkeykit --reset # Show version number -gitkeykit --verion +gitkeykit --version # Display help information and available commands gitkeykit --help diff --git a/bin/index.ts b/bin/index.ts index bcad599..d461a26 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -40,7 +40,7 @@ function usage() { async function handleImport(keyPath: string): Promise { try { - importKey(keyPath); + await importKey(keyPath); logger.log(`Imported key from ${keyPath}`); await start(); } catch (error) {