From 1996a095ce8aceaeffd7cb4205e2c3acf6723224 Mon Sep 17 00:00:00 2001 From: Thiru P <123931175+ThiruNithish28@users.noreply.github.com> Date: Sun, 26 Apr 2026 23:27:17 +0530 Subject: [PATCH 1/6] developed cli for auditgen (#1) * initial setup for basi cli * fix issue in basi cli --- audigen_cli/banner.py | 22 +++++ audigen_cli/cli.py | 197 +++++++++++++++++++++++++++++++++++++ audigen_cli/config.py | 35 +++++++ audigen_cli/excelWriter.py | 100 +++++++++++++++---- audigen_cli/extractor.py | 8 +- audigen_cli/llm_client.py | 1 - poetry.lock | 72 +++++++++++++- pyproject.toml | 6 +- 8 files changed, 414 insertions(+), 27 deletions(-) create mode 100644 audigen_cli/banner.py create mode 100644 audigen_cli/config.py diff --git a/audigen_cli/banner.py b/audigen_cli/banner.py new file mode 100644 index 0000000..f71ba5a --- /dev/null +++ b/audigen_cli/banner.py @@ -0,0 +1,22 @@ +from rich.console import Console +from rich.text import Text + +console =Console() + +VERSION = "0.1.0" + +ASCII_ART = """\ + █████╗ ██╗ ██╗██████╗ ██╗ ██████╗ ███████╗███╗ ██╗ +██╔══██╗██║ ██║██╔══██╗██║██╔════╝ ██╔════╝████╗ ██║ +███████║██║ ██║██║ ██║██║██║ ███╗█████╗ ██╔██╗ ██║ +██╔══██║██║ ██║██║ ██║██║██║ ██║██╔══╝ ██║╚████║ +██║ ██║╚██████╔╝██████╔╝██║╚██████╔╝███████╗██║ ╚███║ +╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚══╝""" + +def print_banner(): + art= Text(ASCII_ART, style="bold cyan") + subtitle=Text(f"v{VERSION} | Audit Document Generator | by Thiru", style="dim white") + console.print() + console.print(art) + console.print(subtitle) + console.print() diff --git a/audigen_cli/cli.py b/audigen_cli/cli.py index e69de29..6fe9529 100644 --- a/audigen_cli/cli.py +++ b/audigen_cli/cli.py @@ -0,0 +1,197 @@ +from datetime import datetime +import os + +import click +from pathlib import Path +from rich.prompt import Prompt, Confirm +from rich.console import Console +from rich.table import Table +from rich.panel import Panel +from rich import box + +from audigen_cli import config as cfg +from audigen_cli.banner import print_banner + +console = Console() +DATE_FORMAT = "%d-%m-%Y" + +def parse_date(value: str) -> str: + """Validate and normalize date input.""" + try: + datetime.strptime(value, DATE_FORMAT) + return value + except ValueError: + raise click.BadParameter(f"Expected DD-MM-YYYY, got: {value}") + +def resolve_output_dir(ticket_id: str, output_arg: str | None) -> Path: + """ + Priority: --output arg > config default > current working directory. + Always creates a subfolder named after the ticket. + """ + base = output_arg or cfg.get("output_dir") or os.getcwd() + out = Path(base) / ticket_id + out.mkdir(parents=True, exist_ok=True) + return out + +# ───────────────────────────────────────────── +# Root group +# ───────────────────────────────────────────── +@click.group() +def cli(): + """AudiGen — Audit document generator CLI.""" + pass + + +# ───────────────────────────────────────────── +# Config commands +# ───────────────────────────────────────────── +@cli.group() +def config(): + """Manage AuditGen configuration""" + pass + +@config.command("set-key") +def config_set_key(): + """set your Gemini API key.""" + key = Prompt.ask("[bold cyan]Enter your Gemini API key[/bold cyan]", password=True) + if not key.strip(): + console.print("[red]API key cannot be empty.[/red]") + return + cfg.set_value("api_key", key.strip()) + console.print("[green]✔[/green] API key saved.") + +@config.command("set-user") +def config_set_user(): + """Set your default username (used in generated documents).""" + name = Prompt.ask("[bold cyan]Enter your name[/bold cyan]") + if not name.strip(): + console.print("[red]Name cannot be empty.[/red]") + return + cfg.set_value("default_user", name.strip()) + console.print(f"[green]✔[/green] Default user set to [bold]{name.strip()}[/bold].") + +@config.command("set-output") +def config_set_output(): + path_str = Prompt.ask("[bold cyan] Enter output folder path[/bold cyan]") + path= Path(path_str.strip()) + if not path.exists(): + create = Confirm.ask(f"[yellow]Folder does not exist. Create it?[/yellow]") + if create: + path.mkdir(parents=True, exist_ok=True) + else: + console.print("[red]Aborted.[/red]") + return + cfg.set_value("output_dir", str(path)) + console.print(f"[green]✔[/green] Output folder set to [bold]{path}[/bold].") + +@config.command("show") +def config_show(): + """Display current confiuration""" + current = cfg.load_config() + table = Table(title="AuditGen Configuration", box=box.ROUNDED, show_header=True) + table.add_column("Key", style='cyan', no_wrap=True) + table.add_column("Value", style='white') + + api_key = current.get('api_key') + masked_key = (api_key[6]+"..."+api_key[-4]) if api_key and len(api_key) > 10 else ("[dim] not set [/dim]" if not api_key else api_key) + table.add_row("api-key", masked_key) + table.add_row("default user",current.get('default_user') or "[dim] not set [/dim]") + table.add_row("output path", current.get("output_dir")or "[dim] not set [/dim]") + + console.print() + console.print(table) + console.print() + +# ───────────────────────────────────────────── +# Generate command +# ───────────────────────────────────────────── + +COMPLEXITY_CHOICES = click.Choice(["LOW", "MEDIUM", "HIGH"], case_sensitive=False) +PRIORITY_CHOICES = click.Choice(["P1", "P2", "P3"], case_sensitive=False) + +@cli.command() +@click.option("--brd", "-b", required=True, type=click.Path(exists=True), help="Path to the BRD .docx file.") +@click.option("--ticket","-t", required=True,help="Ticket /issue Id") +@click.option("--start", "-s", required=True, help="BRD start date (DD-MM-YYYY).") +@click.option("--end","-e", required=True, help="BRD end date (DD-MM-YYYY).") +@click.option("--user", "-u", default=None, help="Your name. Falls back to config default.") +@click.option("--complexity", "-c", default="MEDIUM", type=COMPLEXITY_CHOICES, show_default=True, help="Ticket complexity.") +@click.option("--priority", "-p", default="P2", type=PRIORITY_CHOICES, show_default=True, help="Ticket priority.") +@click.option("--approver", "-a", required=True, help="Approver name for the test case sheet.") +@click.option("--output", "-o", default=None, help="Output folder. Falls back to config, then current directory.") +def generate(brd, ticket, start, end, user, complexity, priority, approver, output): + print_banner() + # ── Validate dates ─────────────────────── + try: + start=parse_date(start) + end=parse_date(end) + except click.BadParameter as e: + console.print(f"[red]✘ Date error:[/red] {e}") + raise SystemExit(1) + + if datetime.strptime(start, DATE_FORMAT) > datetime.strptime(end, DATE_FORMAT): + console.print("[red]✘ Start date cannot be after end date.[/red]") + raise SystemExit(1) + + # ── Resolve user ───────────────────────── + resolved_user = user or cfg.get("default_user") + if not resolved_user: + resolved_user=Prompt.ask("[yellow]No default user set. Enter your name[/yellow]") + + # ── Check API key ───────────────────────── + api_key = cfg.get("api_key") + if not api_key: + console.print("[red]✘ Gemini API key not configured.[/red]") + console.print(" Run [bold cyan]auditgen config set-key[/bold cyan] first.") + raise SystemExit(1) + + os.environ["GEMINI_API_KEY"] = api_key + # ── Summary panel before running ────────── + summary = ( + f"[cyan]BRD:[/cyan] {brd}\n" + f"[cyan]Ticket:[/cyan] {ticket}\n" + f"[cyan]Dates:[/cyan] {start} → {end}\n" + f"[cyan]User:[/cyan] {resolved_user}\n" + f"[cyan]Complexity:[/cyan] {complexity} [cyan]Priority:[/cyan] {priority}\n" + f"[cyan]Approver:[/cyan] {approver}" + ) + console.print(Panel(summary, title="[bold white]Generate Run[/bold white]", box=box.ROUNDED)) + console.print() + + # ── Resolve output dir ──────────────────── + out_dir = resolve_output_dir(ticket, output) + + # ── Step 1: Extract BRD ─────────────────── + with console.status("[bold cyan][1/3] Extracting BRD...[/bold cyan]", spinner="dots"): + from audigen_cli.extractor import extractDoc + sanitized_text =extractDoc(brd) + console.print("[green]✔[/green] [1/3] BRD extracted.") + + # ── Step 2: LLM ─────────────────────────── + with console.status("[bold cyan][2/3] Generating test cases via Gemini...[/bold cyan]", spinner="dots2"): + from audigen_cli.llm_client import callLLM + llm_result = callLLM(sanitized_text) + console.print("[green]✔[/green] [2/3] Test cases generated.") + + # ── Step 3: Write Excel ─────────────────── + with console.status("[bold cyan][3/3] Writing Excel files...[/bold cyan]", spinner="dots"): + from audigen_cli.excelWriter import startExcelChange + startExcelChange( + llm_generateTestCase=llm_result, + BRD_startDate=start, + BRD_endDate=end, + out_dir=str(out_dir), + approver=approver, + user=resolved_user, + ticket=ticket, + ) + console.print("[green]✔[/green] [3/3] Excel files written.") + # ── Done ────────────────────────────────── + console.print() + console.print(Panel( + f"[green]All documents generated successfully![/green]\n" + f"[dim]Saved to:[/dim] [bold]{out_dir}[/bold]", + box=box.ROUNDED, + border_style="green" + )) + console.print() \ No newline at end of file diff --git a/audigen_cli/config.py b/audigen_cli/config.py new file mode 100644 index 0000000..c3a554b --- /dev/null +++ b/audigen_cli/config.py @@ -0,0 +1,35 @@ +import json +from pathlib import Path + +CONFIG_DIR = Path.home() / ".auditgen" +CONFIG_FILE=CONFIG_DIR / "config.json" + +DEFAULTS = { + "api_key": None, + "default_user": None, + "output_dir" : None, +} + +def _ensure_config_dir(): + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + +def load_config() -> dict: + _ensure_config_dir() + if not CONFIG_FILE.exists(): + return dict(DEFAULTS) + with open(CONFIG_FILE, "r") as f: + return {**DEFAULTS,**json.load(f)} + +# save the configuration +def save_config(data: dict): + _ensure_config_dir() + with open(CONFIG_FILE, "w") as f: + json.dump(data, f, indent=2) + +def get(key: str): + return load_config().get(key) + +def set_value(key:str , value:str): + config = load_config() + config[key]=value + save_config(config) \ No newline at end of file diff --git a/audigen_cli/excelWriter.py b/audigen_cli/excelWriter.py index 61a6e83..43a65f9 100644 --- a/audigen_cli/excelWriter.py +++ b/audigen_cli/excelWriter.py @@ -1,11 +1,14 @@ from openpyxl import load_workbook from openpyxl.styles import Alignment +from pathlib import Path from audigen_cli import utils -def revisionHistoryChanges(wb, BRD_endDate): +def revisionHistoryChanges(wb, BRD_endDate,approver: str): revision_sheet = wb['Revision History'] revision_sheet['B8']=BRD_endDate revision_sheet['D8']=BRD_endDate + revision_sheet['C8']=approver + def addTestCases(wb, llm_generateTestCase): test_cases_sheet = wb['Test Cases'] @@ -50,51 +53,110 @@ def addTestCases(wb, llm_generateTestCase): test_cases_sheet.column_dimensions['B'].width = 45 test_cases_sheet.column_dimensions['C'].width = 45 -def updateImpactAnaylsis(wb,llm_generateTestCase,BRD_startDate,BRD_endDate): +def updateImpactAnaylsis( + wb, + llm_generateTestCase, + BRD_startDate, + BRD_endDate, + ticket_id, + user: str, + approver:str +): sheet = wb['Impact Analysis'] effort = utils.calculateEffort(BRD_startDate,BRD_endDate) + #Ticket id + sheet['C4'].value=ticket_id + #complexity + + #priority + + #prepared by + sheet['C7'].value= user + #assessed by + sheet['C13'].value=approver sheet['E4'].value = llm_generateTestCase.additional_info.brd_rasiedBy + # Prepared Date sheet['E7'].value = BRD_startDate + sheet['B10'].value = llm_generateTestCase.additional_info.componets_affected + #Estimated Effort sheet['F10'].value = effort + sheet['G10'].value = BRD_startDate + # Actual End Date sheet['H10'].value = BRD_endDate + # Actual Effort sheet['I10'].value = effort + # assessed Date sheet['E13'].value = BRD_endDate -def updateCodeCheckList(wb,llm_generateTestCase, BRD_endDate): +def updateCodeCheckList(wb,llm_generateTestCase, BRD_endDate, user:str, approver:str): sheet = wb['Java Checklist'] sheet['B6'].value = llm_generateTestCase.additional_info.componets_affected + # developed date sheet['D7'].value = BRD_endDate + #reviwed date sheet['D8'].value = BRD_endDate + #Developed by + sheet['B7'].value=user + # Reviewed by + sheet['B8'].value=approver + + # ------------------------------- # main method # ------------------------------- -def startExcelChange(llm_generateTestCase,BRD_startDate,BRD_endDate): +_TEMPLATES = Path(__file__).parent.parent / "template" +def startExcelChange( + llm_generateTestCase, + BRD_startDate: str, + BRD_endDate: str, + out_dir: str, + approver: str, + user: str, + ticket: str, +): + out =Path(out_dir) + # Load template - impact_analysis_xlsx = load_workbook('Impact Analysis.xlsx') - test_case_xlsx = load_workbook('template/Test Cases.xlsx') - code_checkList_xlsx = load_workbook('template/Code Review Checklist.xlsx') - print(f"Images in template: {len(impact_analysis_xlsx['Impact Analysis']._images)}") + impact_analysis_xlsx = load_workbook(_TEMPLATES / 'Impact Analysis.xlsx') + test_case_xlsx = load_workbook(_TEMPLATES / 'Test Cases.xlsx') + code_checkList_xlsx = load_workbook(_TEMPLATES / 'Code Review Checklist.xlsx') + + # print(f"Images in template: {len(impact_analysis_xlsx['Impact Analysis']._images)}") + # 1 impact analysis changes - updateImpactAnaylsis(impact_analysis_xlsx, llm_generateTestCase,BRD_startDate,BRD_endDate) + updateImpactAnaylsis( + impact_analysis_xlsx, + llm_generateTestCase, + BRD_startDate, + BRD_endDate, + ticket_id=ticket, + user=user, + approver=approver + ) + # 2 testcase xl - revisionHistoryChanges(test_case_xlsx,BRD_endDate) + revisionHistoryChanges(test_case_xlsx,BRD_endDate,approver) + addTestCases(test_case_xlsx,llm_generateTestCase) + # 3 codeCheckList - updateCodeCheckList(code_checkList_xlsx,llm_generateTestCase, BRD_endDate) + updateCodeCheckList(code_checkList_xlsx,llm_generateTestCase, BRD_endDate,user, approver) #save test_case_xlsx.calculation.fullCalcOnLoad = True - impact_analysis_xlsx.save('Impact Analysis Template.xlsx') - test_case_xlsx.save('sampleCases.xlsx') - code_checkList_xlsx.save('Code Checklist.xlsx') - - wb2 = load_workbook('Impact Analysis Template.xlsx') - ws2 = wb2['Impact Analysis'] - print(f"Images after save: {len(ws2._images)}") - print("All Excel files generated successfully") + + impact_analysis_xlsx.save(out / f'{ticket}-Impact Analysis Template.xlsx') + test_case_xlsx.save(out / f'{ticket}-sampleCases.xlsx') + code_checkList_xlsx.save(out / f'{ticket}-Code Checklist.xlsx') + + + # wb2 = load_workbook('Impact Analysis Template.xlsx') + # ws2 = wb2['Impact Analysis'] + # print(f"Images after save: {len(ws2._images)}") + # print("All Excel files generated successfully") diff --git a/audigen_cli/extractor.py b/audigen_cli/extractor.py index cb9b9d5..ebc652a 100644 --- a/audigen_cli/extractor.py +++ b/audigen_cli/extractor.py @@ -9,8 +9,9 @@ "Neeyamo Enterprise Solutions":"[COMPANY]" } -def extractDoc(): - documnet = Document('template/Vendor initiation date and time should be captured in the checklevel report.docx') +def extractDoc(brd_path): + """Extract and sanitize text from a BRD .docx file.""" + documnet = Document(brd_path) full_text ="" @@ -31,12 +32,9 @@ def iter_block_items(parent): row_data = [cell.text.strip() for cell in row.cells] full_text += "\t".join(row_data) + "\n" - print(full_text) sanitized_text = full_text for original,placeholder in SENSITIVE_DATA.items(): sanitized_text = sanitized_text.replace(original, placeholder) - print("============================") - print(sanitized_text) return sanitized_text diff --git a/audigen_cli/llm_client.py b/audigen_cli/llm_client.py index da5209f..6207abe 100644 --- a/audigen_cli/llm_client.py +++ b/audigen_cli/llm_client.py @@ -47,6 +47,5 @@ def callLLM(sanitized_text) : ) ) - print(response.text) llm_generateTestCase=response.parsed return llm_generateTestCase \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index c8fdafb..34606fb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -686,6 +686,42 @@ html-clean = ["lxml_html_clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "openpyxl" version = "3.1.5" @@ -1005,6 +1041,21 @@ files = [ [package.dependencies] typing-extensions = ">=4.14.1" +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "python-docx" version = "1.2.0" @@ -1058,6 +1109,25 @@ urllib3 = ">=1.26,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] +[[package]] +name = "rich" +version = "15.0.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.9.0" +groups = ["main"] +files = [ + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "sniffio" version = "1.3.1" @@ -1205,4 +1275,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "909adca63e5afa5a86d3dbdb0f578e728852266d44331923a03cbe3642f0f0e3" +content-hash = "9feabc44a0d6469a9872bc19615bb9dfaaf0c18524859d2e69e4b376c81a301c" diff --git a/pyproject.toml b/pyproject.toml index fe5fb7a..cbeb6b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,10 +13,14 @@ dependencies = [ "python-dotenv (>=1.2.2,<2.0.0)", "pydantic (>=2.13.2,<3.0.0)", "google-genai (>=1.73.1,<2.0.0)", - "pillow (>=12.2.0,<13.0.0)" + "pillow (>=12.2.0,<13.0.0)", + "rich (>=15.0.0,<16.0.0)" ] [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +auditgen = "audigen_cli.cli:cli" \ No newline at end of file From 0f6e51e78d2b0e08e1c97e77b7fa9ce6d9b9ec8c Mon Sep 17 00:00:00 2001 From: Thiru P <123931175+ThiruNithish28@users.noreply.github.com> Date: Sun, 3 May 2026 17:19:43 +0530 Subject: [PATCH 2/6] feat: improve CLI UX, prompts, and generate workflow * change desing of CLI * fix: address CodeRabbit review comments * fix: check brd file is docx in the flag option * fix: output path issue --- audigen_cli/banner.py | 51 ++++++++- audigen_cli/cli.py | 247 ++++++++++++++++++++++++++---------------- audigen_cli/config.py | 1 + audigen_cli/ui.py | 18 +++ audigen_cli/utils.py | 49 +++++++++ poetry.lock | 44 +++++++- pyproject.toml | 3 +- 7 files changed, 314 insertions(+), 99 deletions(-) create mode 100644 audigen_cli/ui.py diff --git a/audigen_cli/banner.py b/audigen_cli/banner.py index f71ba5a..be6a8db 100644 --- a/audigen_cli/banner.py +++ b/audigen_cli/banner.py @@ -1,17 +1,20 @@ from rich.console import Console from rich.text import Text +from rich.console import Console +from rich.panel import Panel +from rich.align import Align console =Console() VERSION = "0.1.0" ASCII_ART = """\ - █████╗ ██╗ ██╗██████╗ ██╗ ██████╗ ███████╗███╗ ██╗ -██╔══██╗██║ ██║██╔══██╗██║██╔════╝ ██╔════╝████╗ ██║ -███████║██║ ██║██║ ██║██║██║ ███╗█████╗ ██╔██╗ ██║ -██╔══██║██║ ██║██║ ██║██║██║ ██║██╔══╝ ██║╚████║ -██║ ██║╚██████╔╝██████╔╝██║╚██████╔╝███████╗██║ ╚███║ -╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚══╝""" + █████╗ ██╗ ██╗██████╗ ██╗████████╗ ██████╗ ███████╗███╗ ██╗ +██╔══██╗██║ ██║██╔══██╗██║╚══██╔══╝██╔════╝ ██╔════╝████╗ ██║ +███████║██║ ██║██║ ██║██║ ██║ ██║ ███╗█████╗ ██╔██╗ ██║ +██╔══██║██║ ██║██║ ██║██║ ██║ ██║ ██║██╔══╝ ██║╚██╗██║ +██║ ██║╚██████╔╝██████╔╝██║ ██║ ╚██████╔╝███████╗██║ ╚████║ +╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝""" def print_banner(): art= Text(ASCII_ART, style="bold cyan") @@ -20,3 +23,39 @@ def print_banner(): console.print(art) console.print(subtitle) console.print() + + +def print_banner2(): + # Brand line + title = Text() + title.append("AuditGen", style="bold #ff8c69") + title.append(f" v{VERSION}", style="bold white") + + # Body + body = Text() + body.append("AI Audit Document Generator\n", style="bold white") + body.append("Generate test cases, review outputs, export Excel.\n", style="white") + body.append("\n") + body.append("/generate", style="bold #7aa2f7") + body.append(" Create audit documents from BRD\n", style="dim white") + body.append("/config", style="bold #7aa2f7") + body.append(" Manage API key, user, output folder\n", style="dim white") + body.append("/review", style="bold #7aa2f7") + body.append(" Inspect latest generated run\n", style="dim white") + body.append("/help", style="bold #7aa2f7") + body.append(" Show available commands", style="dim white") + + panel = Panel( + Align.left(body), + title=title, + title_align="left", + border_style="#ff8c69", + padding=(1, 2), + expand=True, + ) + + console.print() + console.print(panel) + console.print("[dim]Ready. Type a command to continue.[/dim]") + console.print() + diff --git a/audigen_cli/cli.py b/audigen_cli/cli.py index 6fe9529..2a01278 100644 --- a/audigen_cli/cli.py +++ b/audigen_cli/cli.py @@ -1,9 +1,8 @@ -from datetime import datetime import os import click +import questionary from pathlib import Path -from rich.prompt import Prompt, Confirm from rich.console import Console from rich.table import Table from rich.panel import Panel @@ -11,27 +10,82 @@ from audigen_cli import config as cfg from audigen_cli.banner import print_banner +from audigen_cli.ui import custom_style +from audigen_cli.utils import is_word_file,_validate_date,_validate_date_range,resolve_output_dir console = Console() -DATE_FORMAT = "%d-%m-%Y" -def parse_date(value: str) -> str: - """Validate and normalize date input.""" - try: - datetime.strptime(value, DATE_FORMAT) - return value - except ValueError: - raise click.BadParameter(f"Expected DD-MM-YYYY, got: {value}") - -def resolve_output_dir(ticket_id: str, output_arg: str | None) -> Path: - """ - Priority: --output arg > config default > current working directory. - Always creates a subfolder named after the ticket. - """ - base = output_arg or cfg.get("output_dir") or os.getcwd() - out = Path(base) / ticket_id - out.mkdir(parents=True, exist_ok=True) - return out +CONFIG_FIELDS = { + "api_key": { + "label": "Gemini API Key", + "prompt": "Enter your Gemini API key", + "password": True, + "mask_fn": lambda v: v[:6] + "..." + v[-4:] if v and len(v) > 10 else v, + }, + "default_user": { + "label": "Default User", + "prompt": "Enter your name", + "password": False, + "mask_fn": None, + }, + "default_approver": { + "label": "Default Approver", + "prompt": "Enter default approver name", + "password": False, + "mask_fn": None, + }, + "output_dir": { + "label": "Output Directory", + "prompt": "Enter output folder path", + "password": False, + "mask_fn": None, + }, +} + +def _prompt_for_field(key: str) -> str | None: + """Prompt the user for a single config field. Returns value or None if skipped.""" + field= CONFIG_FIELDS[key] + + if field["password"]: + value = _ask(questionary.password(field["prompt"] + ":", style=custom_style)) + else: + value = _ask(questionary.text(field["prompt"] + ":", style=custom_style)) + + if not value or not value.strip(): + console.print(f"[yellow]⚠ Skipped {field['label']} (no input)[/yellow]") + return None + # Special handling for output_dir to validate path + if key == "output_dir": + path = Path(value.strip()) + if not path.exists(): + create = _ask(questionary.confirm(f"Folder does not exist. Create it?", style=custom_style)) + if create: + try: + path.mkdir(parents=True, exist_ok=True) + console.print(f"[green]✔ Created folder:[/green] {path}") + except Exception as e: + console.print(f"[red]✘ Failed to create folder:[/red] {e}") + return None + else: + console.print("[yellow]⚠ Skipped output directory.[/yellow]") + return None + return str(path) + return value.strip() + +def _ask(question_fn): + """Run a questionary prompt. Exit cleanly if user presses Ctrl+C.""" + result = question_fn.ask() + if result is None: + console.print("\n[yellow]Aborted.[/yellow]") + raise SystemExit(0) + return result + +# For validating date inputs if user enter the date via flag +def _assert_valid_date(value: str): + result=_validate_date(value) + if result is not True: + console.print(f"[red]✘ {result}[/red]") + raise SystemExit(1) # ───────────────────────────────────────────── # Root group @@ -50,56 +104,48 @@ def config(): """Manage AuditGen configuration""" pass -@config.command("set-key") -def config_set_key(): - """set your Gemini API key.""" - key = Prompt.ask("[bold cyan]Enter your Gemini API key[/bold cyan]", password=True) - if not key.strip(): - console.print("[red]API key cannot be empty.[/red]") - return - cfg.set_value("api_key", key.strip()) - console.print("[green]✔[/green] API key saved.") - -@config.command("set-user") -def config_set_user(): - """Set your default username (used in generated documents).""" - name = Prompt.ask("[bold cyan]Enter your name[/bold cyan]") - if not name.strip(): - console.print("[red]Name cannot be empty.[/red]") +@config.command("setup") +def config_setup(): + """Interactive setup — select which fields to configure.""" + choices = [ + questionary.Choice(title=meta["label"], value=key) + for key, meta in CONFIG_FIELDS.items() + ] + selected_keys = questionary.checkbox( + "Select fields to configure:(space to toggle, enter to confirm)", + choices=choices + ).ask() + + if not selected_keys: + console.print("[yellow]⚠ No fields selected. Aborting setup.[/yellow]") return - cfg.set_value("default_user", name.strip()) - console.print(f"[green]✔[/green] Default user set to [bold]{name.strip()}[/bold].") - -@config.command("set-output") -def config_set_output(): - path_str = Prompt.ask("[bold cyan] Enter output folder path[/bold cyan]") - path= Path(path_str.strip()) - if not path.exists(): - create = Confirm.ask(f"[yellow]Folder does not exist. Create it?[/yellow]") - if create: - path.mkdir(parents=True, exist_ok=True) - else: - console.print("[red]Aborted.[/red]") - return - cfg.set_value("output_dir", str(path)) - console.print(f"[green]✔[/green] Output folder set to [bold]{path}[/bold].") + + console.print() + for key in selected_keys: + value = _prompt_for_field(key) + if value: + cfg.set_value(key, value) + console.print(f"[green]✔[/green] {CONFIG_FIELDS[key]['label']} saved.\n") @config.command("show") def config_show(): - """Display current confiuration""" + """Display current configuration""" current = cfg.load_config() table = Table(title="AuditGen Configuration", box=box.ROUNDED, show_header=True) table.add_column("Key", style='cyan', no_wrap=True) table.add_column("Value", style='white') - api_key = current.get('api_key') - masked_key = (api_key[6]+"..."+api_key[-4]) if api_key and len(api_key) > 10 else ("[dim] not set [/dim]" if not api_key else api_key) - table.add_row("api-key", masked_key) - table.add_row("default user",current.get('default_user') or "[dim] not set [/dim]") - table.add_row("output path", current.get("output_dir")or "[dim] not set [/dim]") + for key, meta in CONFIG_FIELDS.items(): + raw = current.get(key) + if raw and meta["mask_fn"]: + display = meta["mask_fn"](raw) + else: + display = raw or "[dim] not set [/dim]" + table.add_row(meta["label"], display) console.print() console.print(table) + console.print("Use 'auditgen config setup' to update these values.") console.print() # ───────────────────────────────────────────── @@ -110,42 +156,60 @@ def config_show(): PRIORITY_CHOICES = click.Choice(["P1", "P2", "P3"], case_sensitive=False) @cli.command() -@click.option("--brd", "-b", required=True, type=click.Path(exists=True), help="Path to the BRD .docx file.") -@click.option("--ticket","-t", required=True,help="Ticket /issue Id") -@click.option("--start", "-s", required=True, help="BRD start date (DD-MM-YYYY).") -@click.option("--end","-e", required=True, help="BRD end date (DD-MM-YYYY).") +@click.argument("brd", type=click.Path(exists=True),required=False, default=None) +@click.argument("ticket",required=False, default=None) +@click.option("--start", "-s",default=None , help="BRD start date (DD-MM-YYYY).") +@click.option("--end","-e",default=None , help="BRD end date (DD-MM-YYYY).") @click.option("--user", "-u", default=None, help="Your name. Falls back to config default.") -@click.option("--complexity", "-c", default="MEDIUM", type=COMPLEXITY_CHOICES, show_default=True, help="Ticket complexity.") -@click.option("--priority", "-p", default="P2", type=PRIORITY_CHOICES, show_default=True, help="Ticket priority.") -@click.option("--approver", "-a", required=True, help="Approver name for the test case sheet.") +@click.option("--complexity", "-c", default=None,type=COMPLEXITY_CHOICES,help="Ticket complexity.") +@click.option("--priority", "-p", default=None,type=PRIORITY_CHOICES,help="Ticket priority.") +@click.option("--approver", "-a", default=None, help="Approver name for the test case sheet.") @click.option("--output", "-o", default=None, help="Output folder. Falls back to config, then current directory.") def generate(brd, ticket, start, end, user, complexity, priority, approver, output): print_banner() - # ── Validate dates ─────────────────────── - try: - start=parse_date(start) - end=parse_date(end) - except click.BadParameter as e: - console.print(f"[red]✘ Date error:[/red] {e}") + + # ── Check API key ───────────────────────── + api_key = cfg.get("api_key") + if not api_key: + console.print("[red]✘ Gemini API key not configured.[/red]") + console.print(" Run [bold cyan]auditgen config setup[/bold cyan] first.") + raise SystemExit(1) + + + # ── Resolve every input — flag → prompt fallback ────────────────── + brd = brd or _ask(questionary.path("BRD file path:", style=custom_style, validate=is_word_file,)) + if not is_word_file(brd): # Validate if user provided date via flag + console.print("[red]✘ BRD must be a .doc or .docx file.[/red]") raise SystemExit(1) - if datetime.strptime(start, DATE_FORMAT) > datetime.strptime(end, DATE_FORMAT): + ticket = ticket or _ask(questionary.text("Ticket ID:", validate=lambda text: True if len(text) > 0 else "Please enter a value", style=custom_style)) + + start= start or _ask(questionary.text ("Start date (DD-MM-YYYY):", style=custom_style, validate=_validate_date)) + _assert_valid_date(start) # Validate if user provided date via flag + + end = end or _ask(questionary.text ("End date (DD-MM-YYYY):", style=custom_style, validate=_validate_date)) + _assert_valid_date(end) # Validate if user provided date via flag + + # Date range check + if not _validate_date_range(start, end): console.print("[red]✘ Start date cannot be after end date.[/red]") raise SystemExit(1) - # ── Resolve user ───────────────────────── - resolved_user = user or cfg.get("default_user") - if not resolved_user: - resolved_user=Prompt.ask("[yellow]No default user set. Enter your name[/yellow]") + resolved_user = user or cfg.get("default_user") or _ask(questionary.text("Your name:", style=custom_style)) - # ── Check API key ───────────────────────── - api_key = cfg.get("api_key") - if not api_key: - console.print("[red]✘ Gemini API key not configured.[/red]") - console.print(" Run [bold cyan]auditgen config set-key[/bold cyan] first.") + resolved_approver = approver or cfg.get("default_approver") or _ask(questionary.text("Approver name:", style=custom_style)) + + complexity = complexity or _ask(questionary.select("Complexity:", choices=["LOW", "MEDIUM", "HIGH"], default="MEDIUM", style=custom_style)) + + priority = priority or _ask(questionary.select("Priority:", choices=["P1", "P2", "P3"], default="P2", style=custom_style)) + + # ── Resolve output dir ──────────────────── + try: + out_dir = resolve_output_dir(ticket, output) + except ValueError as e: + console.print(f"[red]✘ {e}[/red]") raise SystemExit(1) - - os.environ["GEMINI_API_KEY"] = api_key + # ── Summary panel before running ────────── summary = ( f"[cyan]BRD:[/cyan] {brd}\n" @@ -153,14 +217,11 @@ def generate(brd, ticket, start, end, user, complexity, priority, approver, outp f"[cyan]Dates:[/cyan] {start} → {end}\n" f"[cyan]User:[/cyan] {resolved_user}\n" f"[cyan]Complexity:[/cyan] {complexity} [cyan]Priority:[/cyan] {priority}\n" - f"[cyan]Approver:[/cyan] {approver}" + f"[cyan]Approver:[/cyan] {resolved_approver}" ) - console.print(Panel(summary, title="[bold white]Generate Run[/bold white]", box=box.ROUNDED)) + console.print(Panel(summary, title="[bold white]Generate Run[/bold white]", box=box.ROUNDED,title_align="left")) console.print() - # ── Resolve output dir ──────────────────── - out_dir = resolve_output_dir(ticket, output) - # ── Step 1: Extract BRD ─────────────────── with console.status("[bold cyan][1/3] Extracting BRD...[/bold cyan]", spinner="dots"): from audigen_cli.extractor import extractDoc @@ -168,9 +229,10 @@ def generate(brd, ticket, start, end, user, complexity, priority, approver, outp console.print("[green]✔[/green] [1/3] BRD extracted.") # ── Step 2: LLM ─────────────────────────── + os.environ["GEMINI_API_KEY"] = api_key with console.status("[bold cyan][2/3] Generating test cases via Gemini...[/bold cyan]", spinner="dots2"): from audigen_cli.llm_client import callLLM - llm_result = callLLM(sanitized_text) + llm_result = callLLM(sanitized_text) console.print("[green]✔[/green] [2/3] Test cases generated.") # ── Step 3: Write Excel ─────────────────── @@ -181,16 +243,19 @@ def generate(brd, ticket, start, end, user, complexity, priority, approver, outp BRD_startDate=start, BRD_endDate=end, out_dir=str(out_dir), - approver=approver, + approver=resolved_approver, user=resolved_user, ticket=ticket, ) console.print("[green]✔[/green] [3/3] Excel files written.") # ── Done ────────────────────────────────── + info = llm_result.additional_info console.print() console.print(Panel( f"[green]All documents generated successfully![/green]\n" - f"[dim]Saved to:[/dim] [bold]{out_dir}[/bold]", + f"[dim]Saved to:[/dim] [bold]{out_dir}[/bold]\n" + f"[dim]Components Affected:[/dim] [bold]{info.componets_affected}[/bold]\n" + f"[dim]Raised By:[/dim] [bold]{info.brd_rasiedBy}[/bold]", box=box.ROUNDED, border_style="green" )) diff --git a/audigen_cli/config.py b/audigen_cli/config.py index c3a554b..6c2845b 100644 --- a/audigen_cli/config.py +++ b/audigen_cli/config.py @@ -7,6 +7,7 @@ DEFAULTS = { "api_key": None, "default_user": None, + "default_approver": None, "output_dir" : None, } diff --git a/audigen_cli/ui.py b/audigen_cli/ui.py new file mode 100644 index 0000000..7dacecb --- /dev/null +++ b/audigen_cli/ui.py @@ -0,0 +1,18 @@ +from questionary import Style + +custom_style = Style([ + ("qmark", "fg:#00d7ff bold"), + ("question", "fg:#00d7ff bold"), + + # Input answers (text prompts) + ("answer", "fg:#ffffff bold"), + + # Select-specific + ("pointer", "fg:#00d7ff bold"), # ❯ arrow + ("highlighted", "fg:#00d7ff bold"), # active option + ("selected", "fg:#00d7ff bold"), # after selection + + # Secondary UI + ("separator", "fg:#6c6c6c"), + ("instruction", "fg:#6c6c6c"), +]) \ No newline at end of file diff --git a/audigen_cli/utils.py b/audigen_cli/utils.py index faa8423..f40a44a 100644 --- a/audigen_cli/utils.py +++ b/audigen_cli/utils.py @@ -1,6 +1,55 @@ import math from datetime import datetime, timedelta +import os +from pathlib import Path +from audigen_cli import config as cfg + +DATE_FORMAT = "%d-%m-%Y" + +def _validate_date(value: str) -> bool | str: + try: + datetime.strptime(value, DATE_FORMAT) + return True + except ValueError: + return f"Expected date in format DD-MM-YYYY e.g. 20-04-2025, got: {value}" + +def _validate_date_range(start: str , end: str) -> bool: + return datetime.strptime(start, DATE_FORMAT) <= datetime.strptime(end, DATE_FORMAT) + +def resolve_output_dir(ticket_id: str, output_arg: str | None) -> Path: + """ + Priority: --output arg > config default > current working directory. + Always creates a subfolder named after the ticket. + """ + # Sanitize ticket_id — reject absolute paths, traversal, nested segments + ticket_path = Path(ticket_id) + if ( + ticket_path.is_absolute() + or ".." in ticket_path.parts + or len(ticket_path.parts) != 1 + ): + raise ValueError(f"Invalid ticket ID '{ticket_id}'. It should be a simple name without slashes or traversal.") + + base_path = Path(output_arg or cfg.get("output_dir") or os.getcwd()).expanduser() + if base_path.exists() and not base_path.is_dir(): + raise ValueError(f"Output path '{base_path}' is not a directory.") + + out = base_path / ticket_path.name + try: + out.mkdir(parents=True, exist_ok=True) + except OSError as err: + raise ValueError(f"Could not create output directory '{out}': {err}") from err + + return out + +# ------------------------------- +# checking select file is word or not +# ------------------------------- +def is_word_file(path: str) -> bool: + # Check if it is a file (not a folder) and ends with .docx or .doc + return os.path.isfile(path) and path.lower().endswith((".doc", ".docx")) + # ------------------------------- # autofit row diff --git a/poetry.lock b/poetry.lock index 34606fb..e2b16f9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -846,6 +846,21 @@ test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] xmp = ["defusedxml"] +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"}, + {file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"}, +] + +[package.dependencies] +wcwidth = "*" + [[package]] name = "pyasn1" version = "0.6.3" @@ -1087,6 +1102,21 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "questionary" +version = "2.1.1" +description = "Python library to build pretty command line user prompts ⭐️" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59"}, + {file = "questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d"}, +] + +[package.dependencies] +prompt_toolkit = ">=2.0,<4.0" + [[package]] name = "requests" version = "2.33.1" @@ -1201,6 +1231,18 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] +[[package]] +name = "wcwidth" +version = "0.7.0" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2"}, + {file = "wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0"}, +] + [[package]] name = "websockets" version = "16.0" @@ -1275,4 +1317,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "9feabc44a0d6469a9872bc19615bb9dfaaf0c18524859d2e69e4b376c81a301c" +content-hash = "5b18b1cc48d568acc68de383e4b01ce2761c75c63a1f1c75170cf8f4a7a6a0f0" diff --git a/pyproject.toml b/pyproject.toml index cbeb6b2..579e089 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,8 @@ dependencies = [ "pydantic (>=2.13.2,<3.0.0)", "google-genai (>=1.73.1,<2.0.0)", "pillow (>=12.2.0,<13.0.0)", - "rich (>=15.0.0,<16.0.0)" + "rich (>=15.0.0,<16.0.0)", + "questionary (>=2.1.1,<3.0.0)" ] From e279f71d49c21916084e165303fc57677ed7d940 Mon Sep 17 00:00:00 2001 From: Thiru P <123931175+ThiruNithish28@users.noreply.github.com> Date: Sun, 3 May 2026 21:30:34 +0530 Subject: [PATCH 3/6] feat: add windows exe build pipeline and fix bundled template path (#3) * feat: add windows exe build pipeline and fix bundled template path * fix: get_base_path method --- .github/workflows/build.yml | 40 +++++ audigen_cli/excelWriter.py | 8 +- poetry.lock | 142 +++++++++++++++++- pyproject.toml | 8 +- ... be captured in the checklevel report.docx | Bin 68091 -> 0 bytes 5 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 template/Vendor initiation date and time should be captured in the checklevel report.docx diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..986bfbe --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,40 @@ +name: Build Windows EXE + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install Poetry + run: pip install poetry + + - name: Install dependencies + run: poetry install + + - name: Build EXE + run: > + poetry run pyinstaller + --onefile + --name auditgen + --add-data "template;template" + audigen_cli/cli.py + + - name: Upload EXE + uses: actions/upload-artifact@v4 + with: + name: auditgen-windows + path: dist/auditgen.exe + retention-days: 30 \ No newline at end of file diff --git a/audigen_cli/excelWriter.py b/audigen_cli/excelWriter.py index 43a65f9..f1be17b 100644 --- a/audigen_cli/excelWriter.py +++ b/audigen_cli/excelWriter.py @@ -2,6 +2,12 @@ from openpyxl.styles import Alignment from pathlib import Path from audigen_cli import utils +import sys + +def _get_base_path() -> Path: + if getattr(sys,'frozen',False) and hasattr(sys, '_MEIPASS'): # ← "are we running inside an EXE?" + return Path(sys._MEIPASS) # ← yes: use PyInstaller's temp folder + return Path(__file__).parent.parent # ← no: use normal project folder def revisionHistoryChanges(wb, BRD_endDate,approver: str): revision_sheet = wb['Revision History'] @@ -109,7 +115,7 @@ def updateCodeCheckList(wb,llm_generateTestCase, BRD_endDate, user:str, approver # ------------------------------- # main method # ------------------------------- -_TEMPLATES = Path(__file__).parent.parent / "template" +_TEMPLATES = _get_base_path() / "template" def startExcelChange( llm_generateTestCase, BRD_startDate: str, diff --git a/poetry.lock b/poetry.lock index e2b16f9..44d0ac4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,17 @@ # This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand. +[[package]] +name = "altgraph" +version = "0.17.5" +description = "Python graph (network) package" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597"}, + {file = "altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7"}, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -686,6 +698,22 @@ html-clean = ["lxml_html_clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] +[[package]] +name = "macholib" +version = "1.16.4" +description = "Mach-O header analysis and editing" +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform == \"darwin\"" +files = [ + {file = "macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea"}, + {file = "macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362"}, +] + +[package.dependencies] +altgraph = ">=0.17" + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -737,6 +765,31 @@ files = [ [package.dependencies] et-xmlfile = "*" +[[package]] +name = "packaging" +version = "26.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, + {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, +] + +[[package]] +name = "pefile" +version = "2024.8.26" +description = "Python PE parsing module" +optional = false +python-versions = ">=3.6.0" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f"}, + {file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"}, +] + [[package]] name = "pillow" version = "12.2.0" @@ -1071,6 +1124,57 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyinstaller" +version = "6.20.0" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +optional = false +python-versions = "<3.15,>=3.8" +groups = ["dev"] +files = [ + {file = "pyinstaller-6.20.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:bf3be4e1284ee78ddccba5e29f99443a12a7b4673168288ffc4c9d38c6f7b90e"}, + {file = "pyinstaller-6.20.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:72ae9c1fdea134afa791f58bdc9a1934d5c7609753c111e0026bfc272b32b712"}, + {file = "pyinstaller-6.20.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1031bcc307f3fbeffd4e162723e64d46dbf591c82dd0997413afb2a07328b941"}, + {file = "pyinstaller-6.20.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:8df3b3f347659fa2562d8d193a98ad4600133b8b8d07c268df89e4154376750e"}, + {file = "pyinstaller-6.20.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b0d3cc9dd8120d448459bd3880a12e2f9774c51443af49047801446377999a59"}, + {file = "pyinstaller-6.20.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:03696bb6350177c6bc23bcaf78e71a33c4a89b6754dd90d1be2f318e978c918b"}, + {file = "pyinstaller-6.20.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:6357f1699f6af84f37e7367f031d4f68abdba65543b83990c9e8f5a4cebed0b7"}, + {file = "pyinstaller-6.20.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0ab39c690abad26ba148e8f664f0478acc82a733997f4f22e757774832802da9"}, + {file = "pyinstaller-6.20.0-py3-none-win32.whl", hash = "sha256:9a7637e8e44b4387b13667fdcaac86ab6b29c446c16d34d8401539b81838759c"}, + {file = "pyinstaller-6.20.0-py3-none-win_amd64.whl", hash = "sha256:d588844e890ee80c4365867f98146636e1849bbca8e4284bbf0c809aff0f161a"}, + {file = "pyinstaller-6.20.0-py3-none-win_arm64.whl", hash = "sha256:bd53282c0a73e5c95573e1ddc8e5d564d4932bec91efbaed4dc5fdff9c2ae7f2"}, + {file = "pyinstaller-6.20.0.tar.gz", hash = "sha256:95c5c7e03d5d61e9dfb8ef259c699cf492bb1041beb6dbe83696608cec07347a"}, +] + +[package.dependencies] +altgraph = "*" +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +packaging = ">=22.0" +pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2026.4" +pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} +setuptools = ">=42.0.0" + +[package.extras] +completion = ["argcomplete"] +hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2026.4" +description = "Community maintained hooks for PyInstaller" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyinstaller_hooks_contrib-2026.4-py3-none-any.whl", hash = "sha256:1de1a5e49a878122010b88c7e295502bc69776c157c4a4dc78741a4e6178b00f"}, + {file = "pyinstaller_hooks_contrib-2026.4.tar.gz", hash = "sha256:766c281acb1ecc32e21c8c667056d7ebf5da0aabd5e30c219f9c2a283620eeaa"}, +] + +[package.dependencies] +packaging = ">=22.0" +setuptools = ">=42.0.0" + [[package]] name = "python-docx" version = "1.2.0" @@ -1102,6 +1206,19 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + [[package]] name = "questionary" version = "2.1.1" @@ -1158,6 +1275,27 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "setuptools" +version = "82.0.1" +description = "Most extensible Python build backend with support for C/C++ extension modules" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"}, + {file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] + [[package]] name = "sniffio" version = "1.3.1" @@ -1316,5 +1454,5 @@ files = [ [metadata] lock-version = "2.1" -python-versions = ">=3.12" -content-hash = "5b18b1cc48d568acc68de383e4b01ce2761c75c63a1f1c75170cf8f4a7a6a0f0" +python-versions = ">=3.12,<3.15" +content-hash = "d9d7c2d6a1e0473d1a0c32af173b88b889f84b98d0afd5cd817f3a54d7b8d58f" diff --git a/pyproject.toml b/pyproject.toml index 579e089..ae04d30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "a audit doc generation automancs tool cli tool" authors = [ {name = "thiru"} ] -requires-python = ">=3.12" +requires-python = ">=3.12,<3.15" dependencies = [ "python-docx (>=1.2.0,<2.0.0)", "openpyxl (>=3.1.5,<4.0.0)", @@ -24,4 +24,8 @@ requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] -auditgen = "audigen_cli.cli:cli" \ No newline at end of file +auditgen = "audigen_cli.cli:cli" +[dependency-groups] +dev = [ + "pyinstaller (>=6.20.0,<7.0.0)" +] diff --git a/template/Vendor initiation date and time should be captured in the checklevel report.docx b/template/Vendor initiation date and time should be captured in the checklevel report.docx deleted file mode 100644 index 4499a14ba19c13015c68964c752de711451807d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68091 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 Date: Mon, 4 May 2026 00:05:22 +0530 Subject: [PATCH 4/6] Sync main into develop (#7) * release: merge develop into main (CLI + UX + Windows exe support) (#4) * developed cli for auditgen (#1) * initial setup for basi cli * fix issue in basi cli * feat: improve CLI UX, prompts, and generate workflow * change desing of CLI * fix: address CodeRabbit review comments * fix: check brd file is docx in the flag option * fix: output path issue * feat: add windows exe build pipeline and fix bundled template path (#3) * feat: add windows exe build pipeline and fix bundled template path * fix: get_base_path method * fix: resolve exe crash due to lazy import handling (#5) * fix: lazy import not catch by PyInstaller * fix: add debug in the cli to check what issue it crash (#6) --- .github/workflows/build.yml | 16 ++++++++++++++++ audigen_cli/cli.py | 13 ++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 986bfbe..946365f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,8 @@ name: Build Windows EXE +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + on: push: branches: [main] @@ -30,6 +33,19 @@ jobs: --onefile --name auditgen --add-data "template;template" + --hidden-import audigen_cli.extractor + --hidden-import audigen_cli.llm_client + --hidden-import audigen_cli.excelWriter + --hidden-import audigen_cli.config + --hidden-import audigen_cli.banner + --hidden-import audigen_cli.utils + --collect-all google.genai + --collect-all docx + --collect-all openpyxl + --collect-all questionary + --collect-all rich + --collect-all pydantic + --collect-all PIL audigen_cli/cli.py - name: Upload EXE diff --git a/audigen_cli/cli.py b/audigen_cli/cli.py index 2a01278..372d12b 100644 --- a/audigen_cli/cli.py +++ b/audigen_cli/cli.py @@ -259,4 +259,15 @@ def generate(brd, ticket, start, end, user, complexity, priority, approver, outp box=box.ROUNDED, border_style="green" )) - console.print() \ No newline at end of file + console.print() + +if __name__ == "__main__": + import traceback + try: + cli() + except Exception as e: + log_path = Path.home() / "auditgen_crash.log" + with open(log_path, "w") as f: + f.write(traceback.format_exc()) + print(f"Crashed. Log saved to: {log_path}") + raise SystemExit(1) From af76a2b8e21be400590b880ff913b269b0111661 Mon Sep 17 00:00:00 2001 From: Thiru P <123931175+ThiruNithish28@users.noreply.github.com> Date: Mon, 4 May 2026 00:22:21 +0530 Subject: [PATCH 5/6] docs: add README, CHANGELOG and update yml file (#8) --- .github/workflows/build.yml | 12 ++++++++-- CHANGELOG.md | 11 +++++++++ README.md | 45 +++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 README.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 946365f..d285cb5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,12 +5,14 @@ env: on: push: - branches: [main] + tags: ['v*'] workflow_dispatch: jobs: build: runs-on: windows-latest + permissions: + contents: write steps: - name: Checkout code @@ -53,4 +55,10 @@ jobs: with: name: auditgen-windows path: dist/auditgen.exe - retention-days: 30 \ No newline at end of file + retention-days: 30 + + - name: Create Github Release + uses: softprops/action-gh-release@v2 + with: + files: dist/auditgen.exe + generate_release_notes: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3f6d85e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +## [v0.1.0] - 2025-04-26 +### Added +- Interactive dual-mode CLI (flags or questionary prompts) +- Config registry with `auditgen config setup` +- Generates Impact Analysis, Test Cases, Code Checklist from BRD +- Windows EXE via GitHub Actions +- Input validation with friendly error messages +- Path traversal protection on ticket ID +- Ctrl+C handling across all prompts \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0dc400 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# AudiGen CLI + +Audit document generator CLI tool — generates Impact Analysis, Test Cases, +and Code Review Checklist from a BRD document using AI. + +## Requirements +- Windows 10/11 +- Gemini API key ([get one free here](https://aistudio.google.com/)) + +## Installation +1. Download `auditgen.exe` from [Releases](../../releases) +2. Place it in a folder e.g. `C:\Tools\auditgen\` +3. Add that folder to your Windows PATH +4. Open a new terminal and run `auditgen --help` + +## First Time Setup +```cmd +auditgen config setup +``` +Select all fields and enter your details when prompted. + +## Usage +```cmd +# Interactive mode — prompts for everything +auditgen generate + +# Direct mode — pass everything as flags +auditgen generate "path\to\brd.docx" TKT-001 -s 20-04-2025 -e 30-04-2025 + +# View your config +auditgen config show +``` + +## Output +Running `generate` produces three Excel files in your output folder: +- `TKT-001-Impact Analysis Template.xlsx` +- `TKT-001-Test Cases.xlsx` +- `TKT-001-Code Checklist.xlsx` + +## Built With +- Python 3.12 +- Click — CLI framework +- Google Gemini — test case generation +- openpyxl — Excel generation +- Rich + Questionary — terminal UI \ No newline at end of file From df042a18c25580f8238076db517c12fdfdd7b517 Mon Sep 17 00:00:00 2001 From: Thiru P <123931175+ThiruNithish28@users.noreply.github.com> Date: Mon, 4 May 2026 09:06:59 +0530 Subject: [PATCH 6/6] Fix/sync main to develop (#9) * release: merge develop into main (CLI + UX + Windows exe support) (#4) * developed cli for auditgen (#1) * initial setup for basi cli * fix issue in basi cli * feat: improve CLI UX, prompts, and generate workflow * change desing of CLI * fix: address CodeRabbit review comments * fix: check brd file is docx in the flag option * fix: output path issue * feat: add windows exe build pipeline and fix bundled template path (#3) * feat: add windows exe build pipeline and fix bundled template path * fix: get_base_path method * fix: resolve exe crash due to lazy import handling (#5) * fix: lazy import not catch by PyInstaller * fix: add debug in the cli to check what issue it crash (#6)