diff --git a/penify_hook/commands/config_commands.py b/penify_hook/commands/config_commands.py index 19b1c35..8125407 100644 --- a/penify_hook/commands/config_commands.py +++ b/penify_hook/commands/config_commands.py @@ -125,6 +125,26 @@ def do_GET(self): content = f.read() self.wfile.write(content.encode()) + elif self.path == "/get_config": + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + + # Get current LLM configuration + current_config = get_llm_config() + + if current_config: + response = { + "success": True, + "config": current_config + } + else: + response = { + "success": False, + "message": "No configuration found" + } + + self.wfile.write(json.dumps(response).encode()) else: self.send_response(404) self.send_header("Content-type", "text/plain") @@ -186,7 +206,6 @@ def config_jira_web(): """ Open a web browser interface for configuring JIRA settings. """ - # Similar implementation to config_llm_web but for JIRA settings redirect_port = random.randint(30000, 50000) server_url = f"http://localhost:{redirect_port}" @@ -208,6 +227,26 @@ def do_GET(self): content = f.read() self.wfile.write(content.encode()) + elif self.path == "/get_config": + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + + # Get current JIRA configuration + current_config = get_jira_config() + + if current_config: + response = { + "success": True, + "config": current_config + } + else: + response = { + "success": False, + "message": "No JIRA configuration found" + } + + self.wfile.write(json.dumps(response).encode()) else: self.send_response(404) self.send_header("Content-type", "text/plain") diff --git a/penify_hook/commit_command.py b/penify_hook/commit_command.py new file mode 100644 index 0000000..a04a27d --- /dev/null +++ b/penify_hook/commit_command.py @@ -0,0 +1,48 @@ +import argparse + + + +def setup_commit_parser(parser): + commit_parser_description = """ +It generates smart commit messages. By default, it will just generate just the Title of the commit message. +1. If you have not configured LLM, it will give an error. You either need to configure LLM or use the API key. +2. If you have not configured JIRA. It will not enhance the commit message with JIRA issue details. +3. For more information, visit https://docs.penify.dev/ +""" + parser.help = "Generate smart commit messages using local-LLM(no login required)." + parser.description = commit_parser_description + parser.formatter_class = argparse.RawDescriptionHelpFormatter + + # Add the message argument with better help + parser.add_argument("-m", "--message", required=False, help="Commit with contextual commit message.", default="N/A") + parser.add_argument("-e", "--terminal", action="store_true", help="Open edit terminal before committing.") + parser.add_argument("-d", "--description", action="store_false", help="It will generate commit message with title and description.", default=False) + +def handle_commit(args): + from penify_hook.commands.commit_commands import commit_code + from penify_hook.commands.config_commands import get_jira_config, get_llm_config, get_token + from penify_hook.constants import API_URL + + # Only import dependencies needed for commit functionality here + open_terminal = args.terminal + generate_description = args.description + print(f"Generate Description: {generate_description}") + # Try to get from config + llm_config = get_llm_config() + llm_model = llm_config.get('model') + llm_api_base = llm_config.get('api_base') + llm_api_key = llm_config.get('api_key') + token = get_token() + + + + # Try to get from config + jira_config = get_jira_config() + jira_url = jira_config.get('url') + jira_user = jira_config.get('username') + jira_api_token = jira_config.get('api_token') + + + commit_code(API_URL, token, args.message, open_terminal, generate_description, + llm_model, llm_api_base, llm_api_key, + jira_url, jira_user, jira_api_token) diff --git a/penify_hook/config_command.py b/penify_hook/config_command.py new file mode 100644 index 0000000..6f76894 --- /dev/null +++ b/penify_hook/config_command.py @@ -0,0 +1,68 @@ + + + +def setup_config_parser(parent_parser): + # Config subcommand: Create subparsers for config types + parser = parent_parser.add_subparsers(title="config_type", dest="config_type") + + # Config subcommand: llm + llm_config_parser = parser.add_parser("llm", help="Configure LLM settings.") + llm_config_parser.add_argument("--model", required=True, help="LLM model to use") + llm_config_parser.add_argument("--api-base", help="API base URL for the LLM service") + llm_config_parser.add_argument("--api-key", help="API key for the LLM service") + + # Config subcommand: llm-web + parser.add_parser("llm-web", help="Configure LLM settings through a web interface") + + # Config subcommand: jira + jira_config_parser = parser.add_parser("jira", help="Configure JIRA settings.") + jira_config_parser.add_argument("--url", required=True, help="JIRA base URL") + jira_config_parser.add_argument("--username", required=True, help="JIRA username or email") + jira_config_parser.add_argument("--api-token", required=True, help="JIRA API token") + jira_config_parser.add_argument("--verify", action="store_true", help="Verify JIRA connection") + + # Config subcommand: jira-web + parser.add_parser("jira-web", help="Configure JIRA settings through a web interface") + + # Add all other necessary arguments for config command + +def handle_config(args): + # Only import dependencies needed for config functionality here + from penify_hook.commands.config_commands import save_llm_config + from penify_hook.jira_client import JiraClient # Import moved here + from penify_hook.commands.config_commands import config_jira_web, config_llm_web, save_jira_config + + if args.config_type == "llm": + save_llm_config(args.model, args.api_base, args.api_key) + print(f"LLM configuration set: Model={args.model}, API Base={args.api_base or 'default'}") + + elif args.config_type == "llm-web": + config_llm_web() + + elif args.config_type == "jira": + save_jira_config(args.url, args.username, args.api_token) + print(f"JIRA configuration set: URL={args.url}, Username={args.username}") + + # Verify connection if requested + if args.verify: + if JiraClient: + jira_client = JiraClient( + jira_url=args.url, + jira_user=args.username, + jira_api_token=args.api_token + ) + if jira_client.is_connected(): + print("JIRA connection verified successfully!") + else: + print("Failed to connect to JIRA. Please check your credentials.") + else: + print("JIRA package not installed. Cannot verify connection.") + + elif args.config_type == "jira-web": + config_jira_web() + + else: + print("Please specify a config type: llm, llm-web, jira, or jira-web") + return 1 + + return 0 diff --git a/penify_hook/constants.py b/penify_hook/constants.py new file mode 100644 index 0000000..631c61b --- /dev/null +++ b/penify_hook/constants.py @@ -0,0 +1,2 @@ +API_URL = 'https://production-gateway.snorkell.ai/api' +DASHBOARD_URL = "https://dashboard.penify.dev/auth/localhost/login" \ No newline at end of file diff --git a/penify_hook/docgen_command.py b/penify_hook/docgen_command.py new file mode 100644 index 0000000..a638872 --- /dev/null +++ b/penify_hook/docgen_command.py @@ -0,0 +1,55 @@ +import logging +import os + + + +# Define the docgen description text +docgen_description = """Generate code documentation using Penify. + +This command requires you to be logged in to your Penify account. +You can generate documentation for: +- Current Git diff (default) +- Specific file +- Specific folder +""" + +def setup_docgen_parser(parser): + # We don't need to create a new docgen_parser since it's passed as a parameter + docgen_subparsers = parser.add_subparsers(title="docgen_subcommand", dest="docgen_subcommand") + + # Docgen main options (for direct documentation generation) + parser.add_argument("-l", "--location", help="[Optional] Path to the folder or file to Generate Documentation. By default it will pick the root directory.", default=None) + + # Subcommand: install-hook (as part of docgen) + install_hook_parser = docgen_subparsers.add_parser("install-hook", help="Install the Git post-commit hook.") + install_hook_parser.add_argument("-l", "--location", required=False, + help="Location in which to install the Git hook. Defaults to current directory.", + default=os.getcwd()) + + # Subcommand: uninstall-hook (as part of docgen) + uninstall_hook_parser = docgen_subparsers.add_parser("uninstall-hook", help="Uninstall the Git post-commit hook.") + uninstall_hook_parser.add_argument("-l", "--location", required=False, + help="Location from which to uninstall the Git hook. Defaults to current directory.", + default=os.getcwd()) + +def handle_docgen(args): + # Only import dependencies needed for docgen functionality here + from penify_hook.commands.config_commands import get_token + import sys + from penify_hook.commands.doc_commands import generate_doc + from penify_hook.commands.hook_commands import install_git_hook, uninstall_git_hook + from penify_hook.constants import API_URL + + token = get_token() + if not token: + logging.error("Error: Unable to authenticate. Please run 'penifycli login'.") + sys.exit(1) + + if args.docgen_subcommand == "install-hook": + install_git_hook(args.location, token) + + elif args.docgen_subcommand == "uninstall-hook": + uninstall_git_hook(args.location) + + else: # Direct documentation generation + generate_doc(API_URL, token, args.location) diff --git a/penify_hook/file_analyzer.py b/penify_hook/file_analyzer.py index d98b987..2a0eda8 100644 --- a/penify_hook/file_analyzer.py +++ b/penify_hook/file_analyzer.py @@ -46,13 +46,13 @@ def process_file(self, file_path, pbar): # --- STAGE 1: Validating --- update_stage(pbar, "Validating") if not file_extension: - logger.info(f"File {file_path} has no extension. Skipping.") + print_warning(f" Empty extension is not supported. Skipping '{self.relative_file_path}'.") return False file_extension = file_extension[1:] # Remove the leading dot if file_extension not in self.supported_file_types: - logger.info(f"File type {file_extension} is not supported. Skipping {file_path}.") + print_warning(f" File type '{file_extension}' is not supported. Skipping '{self.relative_file_path}'.") return False # Update progress bar to indicate we're moving to next stage diff --git a/penify_hook/login_command.py b/penify_hook/login_command.py new file mode 100644 index 0000000..d0a1469 --- /dev/null +++ b/penify_hook/login_command.py @@ -0,0 +1,11 @@ +def setup_login_parser(parser): + parser.add_argument("--token", help="Specify API token directly") + # Add all other necessary arguments for login command + +def handle_login(args): + from penify_hook.constants import API_URL, DASHBOARD_URL + from penify_hook.commands.auth_commands import login + + + # Only import dependencies needed for login functionality here + return login(API_URL, DASHBOARD_URL) diff --git a/penify_hook/main.py b/penify_hook/main.py index 576e06f..19c0750 100644 --- a/penify_hook/main.py +++ b/penify_hook/main.py @@ -1,221 +1,75 @@ -import os -import sys import argparse -import logging - -# Import command modules -from .commands.hook_commands import install_git_hook, uninstall_git_hook -from .commands.doc_commands import generate_doc -from .commands.commit_commands import commit_code -from .commands.auth_commands import login -from .commands.config_commands import ( - save_llm_config, save_jira_config, - config_llm_web, config_jira_web, - get_llm_config, get_jira_config, get_token -) - -# Try importing optional dependencies -try: - from .jira_client import JiraClient -except ImportError: - JiraClient = None - -# Constants -API_URL = 'https://production-gateway.snorkell.ai/api' -DASHBOARD_URL = "https://dashboard.penify.dev/auth/localhost/login" -# API_URL = 'http://localhost:8000/api' +import sys +import time def main(): - """Main entry point for the Penify CLI tool. + parser = argparse.ArgumentParser( + description="""Penify CLI tool for: +1. AI commit message generation with JIRA integration to enhance commit messages. +2. Generating Code Documentation, it requires SignUp to Penify +3. For more information, visit https://docs.penify.dev/""", + formatter_class=argparse.RawDescriptionHelpFormatter + ) - This function serves as the main interface for the Penify command-line - tool, which provides various functionalities including AI commit message - generation, JIRA integration for enhancing commit messages, code - documentation generation, and Git hook installation for automatic - documentation generation. It sets up the command-line argument parser - with subcommands for basic and advanced operations. Basic commands do - not require user login, while advanced commands do. The function also - handles the parsing of arguments and the execution of the corresponding - commands based on user input. For more information about the tool and - its capabilities, please visit https://docs.penify.dev/. - """ - # Configure logging - logging.basicConfig(level=logging.WARNING, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') + # Add version flag + parser.add_argument('--version', '-v', action='store_true', help='Show version information') - # Multi-line description using triple quotes - description = """Penify CLI tool for: -1. AI commit message generation with JIRA integration to enhance commit messages. By default, it uses local-LLM but can be configured to use Penify's LLM. -2. Generating Code Documentation, it requires SignUp to Penify -3. For more information, visit https://docs.penify.dev/ -""" + subparsers = parser.add_subparsers(title="options", dest="subcommands") - parser = argparse.ArgumentParser(description=description, formatter_class=argparse.RawDescriptionHelpFormatter) - # Create subparsers for the main commands - subparsers = parser.add_subparsers(title="subcommands", dest="subcommand") - # Group commands logically basic_title = "Basic Commands (No login required)" advanced_title = "Advanced Commands (Login required)" - + # Create grouped subparsers (visually separated in help output) parser.add_argument_group(basic_title) parser.add_argument_group(advanced_title) - # ===== BASIC COMMANDS (No login required) ===== - - - commit_parser_description = """ -It generates smart commit messages. By default, it will just generate just the Title of the commit message. - -1. If you have not configured LLM, it will give an error. You either need to configure LLM or use the API key. -2. If you have not configured JIRA. It will not enhance the commit message with JIRA issue details. -3. For more information, visit https://docs.penify.dev/ -""" + # Set up subparsers with proper imports upfront + commit_parser = subparsers.add_parser("commit", help="Generate smart commit messages using local-LLM(no login required).") + from .commit_command import setup_commit_parser + setup_commit_parser(commit_parser) - # Subcommand: commit - commit_parser = subparsers.add_parser("commit", help="Generate smart commit messages using local-LLM(no login required).", description=commit_parser_description, formatter_class=argparse.RawDescriptionHelpFormatter) - commit_parser.add_argument("-m", "--message", required=False, help="Commit with contextual commit message.", default="N/A") - commit_parser.add_argument("-e", "--terminal", action="store_true", help="Open edit terminal before committing.") - commit_parser.add_argument("-d", "--description", action="store_false", help="It will generate commit message with title and description.", default=False) - - # Subcommand: config config_parser = subparsers.add_parser("config", help="Configure local-LLM and JIRA.") - config_subparsers = config_parser.add_subparsers(title="config_type", dest="config_type") - - # Config subcommand: llm - llm_config_parser = config_subparsers.add_parser("llm", help="Configure LLM settings.") - llm_config_parser.add_argument("--model", required=True, help="LLM model to use") - llm_config_parser.add_argument("--api-base", help="API base URL for the LLM service") - llm_config_parser.add_argument("--api-key", help="API key for the LLM service") - - # Config subcommand: llm-web - config_subparsers.add_parser("llm-web", help="Configure LLM settings through a web interface") - - # Config subcommand: jira - jira_config_parser = config_subparsers.add_parser("jira", help="Configure JIRA settings.") - jira_config_parser.add_argument("--url", required=True, help="JIRA base URL") - jira_config_parser.add_argument("--username", required=True, help="JIRA username or email") - jira_config_parser.add_argument("--api-token", required=True, help="JIRA API token") - jira_config_parser.add_argument("--verify", action="store_true", help="Verify JIRA connection") + from .config_command import setup_config_parser + setup_config_parser(config_parser) - # Config subcommand: jira-web - config_subparsers.add_parser("jira-web", help="Configure JIRA settings through a web interface") - - # ===== ADVANCED COMMANDS (Login required) ===== - - # Subcommand: login (bridge between basic and advanced) login_parser = subparsers.add_parser("login", help="Log in to Penify to use advanced features like 'docgen' generation.") - - docgen_description="""By default, 'docgen' generates documentation for the latest Git commit diff. Use the -l flag to document a specific file or folder. - -The 'install-hook' command sets up a Git post-commit hook to auto-generate documentation after each commit. -""" - - # Advanced Subcommand: docgen - docgen_parser = subparsers.add_parser("docgen", help="[REQUIRES LOGIN] Generate code documentation for the Git diff, file or folder.", description=docgen_description, formatter_class=argparse.RawDescriptionHelpFormatter) - docgen_subparsers = docgen_parser.add_subparsers(title="docgen_subcommand", dest="docgen_subcommand") - - # Docgen main options (for direct documentation generation) - docgen_parser.add_argument("-l", "--location", help="[Optional] Path to the folder or file to Generate Documentation. By default it will pick the root directory.", default=None) - - # Subcommand: install-hook (as part of docgen) - install_hook_parser = docgen_subparsers.add_parser("install-hook", help="Install the Git post-commit hook.") - install_hook_parser.add_argument("-l", "--location", required=False, - help="Location in which to install the Git hook. Defaults to current directory.", - default=os.getcwd()) - - # Subcommand: uninstall-hook (as part of docgen) - uninstall_hook_parser = docgen_subparsers.add_parser("uninstall-hook", help="Uninstall the Git post-commit hook.") - uninstall_hook_parser.add_argument("-l", "--location", required=False, - help="Location from which to uninstall the Git hook. Defaults to current directory.", - default=os.getcwd()) - - args = parser.parse_args() - - # Get the token based on priority - token = get_token() - - # Process commands - if args.subcommand == "docgen": - # Check for login for all advanced commands - if not token: - logging.error("Error: Unable to authenticate. Please run 'penifycli login'.") - sys.exit(1) - - if args.docgen_subcommand == "install-hook": - install_git_hook(args.location, token) - - elif args.docgen_subcommand == "uninstall-hook": - uninstall_git_hook(args.location) - - else: # Direct documentation generation - generate_doc(API_URL, token, args.location) - - elif args.subcommand == "commit": - # For commit, token is now optional - some functionality may be limited without it - open_terminal = args.terminal - generate_description = args.description - print(f"Generate Description: {generate_description}") - # Try to get from config - llm_config = get_llm_config() - llm_model = llm_config.get('model') - llm_api_base = llm_config.get('api_base') - llm_api_key = llm_config.get('api_key') - - - - # Try to get from config - jira_config = get_jira_config() - jira_url = jira_config.get('url') - jira_user = jira_config.get('username') - jira_api_token = jira_config.get('api_token') - - - commit_code(API_URL, token, args.message, open_terminal, generate_description, - llm_model, llm_api_base, llm_api_key, - jira_url, jira_user, jira_api_token) - - elif args.subcommand == "config": - # Config doesn't require token - if args.config_type == "llm": - save_llm_config(args.model, args.api_base, args.api_key) - print(f"LLM configuration set: Model={args.model}, API Base={args.api_base or 'default'}") - - elif args.config_type == "llm-web": - config_llm_web() - - elif args.config_type == "jira": - save_jira_config(args.url, args.username, args.api_token) - print(f"JIRA configuration set: URL={args.url}, Username={args.username}") - - # Verify connection if requested - if args.verify: - if JiraClient: - jira_client = JiraClient( - jira_url=args.url, - jira_user=args.username, - jira_api_token=args.api_token - ) - if jira_client.is_connected(): - print("JIRA connection verified successfully!") - else: - print("Failed to connect to JIRA. Please check your credentials.") - else: - print("JIRA package not installed. Cannot verify connection.") - - elif args.config_type == "jira-web": - config_jira_web() - - else: - config_parser.print_help() - - elif args.subcommand == "login": - login(API_URL, DASHBOARD_URL) - + from .login_command import setup_login_parser + setup_login_parser(login_parser) + + docgen_parser = subparsers.add_parser("docgen", help="[REQUIRES LOGIN] Generate code documentation for the Git diff, file or folder.") + from .docgen_command import setup_docgen_parser + setup_docgen_parser(docgen_parser) + + # Parse args without validation first to check for simple flags like --version + if '--version' in sys.argv or '-v' in sys.argv: + from importlib.metadata import version + try: + print(f"penifycli version {version('penifycli')}") + except: + print("penifycli version 0.2.2") + return 0 + + # Parse the arguments to determine which command was requested + args = parser.parse_args() + # Handle the commands + if args.subcommands == "commit": + print("Please wait while we generate the commit message...") + from .commit_command import handle_commit + time.sleep(1) + return handle_commit(args) + elif args.subcommands == "config": + from .config_command import handle_config + return handle_config(args) + elif args.subcommands == "login": + from .login_command import handle_login + return handle_login(args) + elif args.subcommands == "docgen": + from .docgen_command import handle_docgen + return handle_docgen(args) else: parser.print_help() - sys.exit(1) + return 1 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/penify_hook/templates/jira_config.html b/penify_hook/templates/jira_config.html index 4ba4803..9d7fec7 100644 --- a/penify_hook/templates/jira_config.html +++ b/penify_hook/templates/jira_config.html @@ -65,6 +65,29 @@ .checkbox-group input { margin-right: 10px; } + .current-config { + background-color: #e0f7fa; + padding: 10px 15px; + border-radius: 4px; + margin-bottom: 20px; + border-left: 4px solid #0066cc; + } + .input-group { + position: relative; + } + .toggle-password { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + cursor: pointer; + user-select: none; + font-size: 14px; + color: #666; + background: none; + border: none; + padding: 0; + }
@@ -72,6 +95,11 @@Configure your JIRA integration for Penify CLI.
+