git-alchemist 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: git-alchemist
3
+ Version: 1.0.0
4
+ Summary: A unified AI stack to optimize, describe, and architect your GitHub repositories.
5
+ Author: abduznik
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: google-genai
11
+ Requires-Dist: rich
12
+ Requires-Dist: python-dotenv
13
+ Requires-Dist: requests
14
+ Dynamic: license-file
15
+
16
+ # Git-Alchemist ⚗️
17
+
18
+ **Git-Alchemist ⚗️** is a unified AI-powered CLI tool for automating GitHub repository management. It consolidates multiple technical utilities into a single, intelligent system powered by Google's Gemini 3 and Gemma 3 models.
19
+
20
+ ### 🌐 [Visit the Official Site](https://abduznik.github.io/Git-Alchemist/)
21
+
22
+ ---
23
+
24
+ ## Features
25
+
26
+ * **Smart Profile Generator:** Intelligently generates or updates your GitHub Profile README.
27
+ * **Topic Generator:** Auto-tag your repositories with AI-suggested topics for better discoverability.
28
+ * **Description Refiner:** Automatically generates repository descriptions by analyzing your README content.
29
+ * **Issue Drafter:** Translates loose ideas into structured, technical GitHub Issue drafts.
30
+ * **Architect (Scaffold):** Generates and executes project scaffolding commands in a safe, temporary workspace.
31
+ * **Fix & Explain:** Apply AI-powered patches to specific files or get concise technical explanations for complex code.
32
+ * **Gold Score Audit:** Measure your repository's professional quality and health.
33
+ * **The Sage:** Contextual codebase chat to answer deep technical questions about your code.
34
+ * **Commit Alchemist:** Automated semantic commit message suggestions from staged changes.
35
+
36
+ ## Model Tiers
37
+
38
+ Git-Alchemist features a dynamic fallback system to ensure you never hit a quota wall:
39
+
40
+ * **Fast Mode (Default):** Utilizes **Gemma 3 (27B)** and **Gemini 3 Flash**. Optimized for speed and high-volume tasks.
41
+ * **Smart Mode (`--smart`):** Utilizes **Gemini 3 Pro** and **Gemini 2.5 Pro**. Optimized for complex architecture and deep code analysis.
42
+
43
+ ## Installation
44
+
45
+ 1. **Clone the repository:**
46
+ ```bash
47
+ git clone https://github.com/abduznik/Git-Alchemist.git
48
+ cd Git-Alchemist
49
+ ```
50
+
51
+ 2. **Install as a Global Library:**
52
+ ```bash
53
+ pip install .
54
+ ```
55
+
56
+ 3. **Set up your Environment:**
57
+ Create a `.env` file in the directory or export it in your shell:
58
+ ```env
59
+ GEMINI_API_KEY=your_actual_api_key_here
60
+ ```
61
+
62
+ ## Usage
63
+
64
+ Once installed, you can run the `alchemist` command from **any directory**:
65
+
66
+ ```bash
67
+ # Audit a repository
68
+ alchemist audit
69
+
70
+ # Optimize repository topics
71
+ alchemist topics
72
+
73
+ # Generate semantic commit messages
74
+ alchemist commit
75
+
76
+ # Ask the Sage a question
77
+ alchemist sage "How does the audit scoring work?"
78
+
79
+ # Scaffold a new project (Safe Mode)
80
+ alchemist scaffold "A FastAPI backend with a React frontend" --smart
81
+ ```
82
+
83
+ ## Requirements
84
+
85
+ * Python 3.10+
86
+ * GitHub CLI (`gh`) installed and authenticated (`gh auth login`).
87
+ * A Google Gemini API Key.
88
+
89
+ ## Migration Note
90
+
91
+ This tool replaces and consolidates the following legacy scripts:
92
+ * `AI-Gen-Profile`
93
+ * `AI-Gen-Topics`
94
+ * `AI-Gen-Description`
95
+ * `AI-Gen-Issue`
96
+ * `Ai-Pro-Arch`
97
+
98
+ ---
99
+ *Created by [abduznik](https://github.com/abduznik)*
@@ -0,0 +1,17 @@
1
+ git_alchemist-1.0.0.dist-info/licenses/LICENSE,sha256=m0_AWYEhbGuv2DeqHpVotJvfJSbWafyGQwivleB3tUI,1065
2
+ src/architect.py,sha256=zt9nqjeLJhj_98rT5GHDH1rj0s0-j1CQav7NbVPPkQA,5481
3
+ src/audit.py,sha256=fuIuoxRk1yC4-2vy2qqgz1EkTavQvyHHFRxuJQjpc3s,2788
4
+ src/cli.py,sha256=-_NksBB4jG2NKZmg-5OYfkPbqIQKhOKFORfdWiu0czQ,3688
5
+ src/committer.py,sha256=O9KFavCgJa8mbsFGZoLBdtMEccwlN7yKNuaxhPtdQ-c,2249
6
+ src/core.py,sha256=mwz5QgTIyIMKVHvfcP2Ndol_DHdZz5azx-zT_FE3VOY,1806
7
+ src/issue_gen.py,sha256=Sdn2DKnihWIDwRJIRmrql-9q8mqLy7LscEuk4celt0c,2474
8
+ src/profile_gen.py,sha256=i6N-gTE0rM1hIxWfN8sEBLItm1gu2_6xE94YaeHRa7g,6772
9
+ src/promote.py,sha256=nSmFywPI5mmHJO15dTpwZ2TO_IZT9w04XOJWF3BE0xM,3224
10
+ src/repo_tools.py,sha256=QGkCDYRBsl4g1DAecNJtOj91nLYZkJriadY3rCLfzE4,3929
11
+ src/sage.py,sha256=nxUEc146pKO_6TR4nBONKkRdREY08NEa2ir4BF--vX8,2227
12
+ src/utils.py,sha256=Ncz4vczfIXcQGgXaFa-8bL3SD3Sykg46yiHDScV8-CA,1187
13
+ git_alchemist-1.0.0.dist-info/METADATA,sha256=McyH9frbTwtCOm1B22_fpGZ012iDyPmisr8mk92oBEI,3219
14
+ git_alchemist-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
15
+ git_alchemist-1.0.0.dist-info/entry_points.txt,sha256=OcOmJI8khwZFsdRSUGGppfKdVfdGOKRRkwcSlZ-rJTI,43
16
+ git_alchemist-1.0.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
17
+ git_alchemist-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ alchemist = src.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 abduznik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ src
src/architect.py ADDED
@@ -0,0 +1,149 @@
1
+ import os
2
+ import json
3
+ import shutil
4
+ import tempfile
5
+ import subprocess
6
+ from rich.console import Console
7
+ from rich.prompt import Confirm
8
+ from .core import generate_content
9
+ from .utils import run_shell
10
+
11
+ console = Console()
12
+
13
+ def scaffold_project(instruction, mode="fast"):
14
+ """
15
+ Generates shell commands to scaffold a project in a temporary directory.
16
+ """
17
+ console.print(f"[cyan]Architecting solution for: {instruction} ({mode} mode)...[/cyan]")
18
+
19
+ # Create temp env
20
+ temp_dir = tempfile.mkdtemp(prefix="git_alchemist_scaffold_")
21
+ console.print(f"[gray]Created temporary workspace: {temp_dir}[/gray]")
22
+
23
+ prompt = f"""
24
+ Task: Project Scaffolding.
25
+ User Goal: "{instruction}"
26
+ Operating System: Linux/Unix (Termux)
27
+ Constraint: Return ONLY a JSON object with a single key "commands" containing an array of shell strings.
28
+ The commands should assume they are running INSIDE the project root.
29
+ Example: {{"commands": ["mkdir src", "touch src/main.py", "echo 'print(1)' > src/main.py"]}}
30
+ Do NOT use markdown blocks.
31
+ """
32
+
33
+ result = generate_content(prompt, mode=mode)
34
+ if not result:
35
+ shutil.rmtree(temp_dir)
36
+ return
37
+
38
+ try:
39
+ clean_result = result.replace("```json", "").replace("```", "").strip()
40
+ data = json.loads(clean_result)
41
+ commands = data.get("commands", [])
42
+
43
+ console.print("[green]Generated Plan:[/green]")
44
+ for cmd in commands:
45
+ console.print(f" > {cmd}")
46
+
47
+ if Confirm.ask("Execute these commands in the temporary workspace?"):
48
+ # Execute in temp dir
49
+ cwd = os.getcwd()
50
+ os.chdir(temp_dir)
51
+ try:
52
+ for cmd in commands:
53
+ console.print(f"[cyan]Running:[/cyan] {cmd}")
54
+ run_shell(cmd)
55
+
56
+ console.print("[green]Scaffolding complete in temporary workspace.[/green]")
57
+ console.print(f"[gray]Contents of {temp_dir}:[/gray]")
58
+ run_shell("ls -R")
59
+
60
+ if Confirm.ask("Keep these files? (Moves them to current directory)"):
61
+ # Move files from temp_dir to cwd
62
+ # We iterate over items in temp_dir and move them
63
+ for item in os.listdir(temp_dir):
64
+ s = os.path.join(temp_dir, item)
65
+ d = os.path.join(cwd, item)
66
+ if os.path.exists(d):
67
+ console.print(f"[yellow]Warning:[/yellow] {item} already exists in current directory. Skipping.")
68
+ else:
69
+ shutil.move(s, d)
70
+ console.print("[green]Files moved successfully.[/green]")
71
+ else:
72
+ console.print("[yellow]Discarding workspace.[/yellow]")
73
+
74
+ except Exception as e:
75
+ console.print(f"[red]Execution failed:[/red] {e}")
76
+ finally:
77
+ os.chdir(cwd)
78
+
79
+ except json.JSONDecodeError:
80
+ console.print(f"[red]Failed to parse AI response:[/red] {result}")
81
+ finally:
82
+ if os.path.exists(temp_dir):
83
+ shutil.rmtree(temp_dir)
84
+ console.print("[gray]Temporary workspace cleaned up.[/gray]")
85
+
86
+ def fix_code(file_path, instruction, mode="fast"):
87
+ """
88
+ Reads a file, applies an AI fix, and optionally creates a PR.
89
+ """
90
+ if not os.path.exists(file_path):
91
+ console.print(f"[red]File not found:[/red] {file_path}")
92
+ return
93
+
94
+ console.print(f"[cyan]Reading {file_path} ({mode} mode)...[/cyan]")
95
+ with open(file_path, "r", encoding="utf-8") as f:
96
+ content = f.read()
97
+
98
+ prompt = f"""
99
+ Task: Fix/Modify Code.
100
+ User Instructions:
101
+ '''
102
+ {instruction}
103
+ '''
104
+
105
+ Target File Content:
106
+ '''
107
+ {content}
108
+ '''
109
+
110
+ Goal: Return ONLY the complete, corrected file content based on the User Instructions. Do not use markdown blocks.
111
+ """
112
+
113
+ console.print(f"[magenta]Consulting Gemini ({mode} mode)...[/magenta]")
114
+ result = generate_content(prompt, mode=mode)
115
+ if not result:
116
+ return
117
+
118
+ clean_result = result.replace("```python", "").replace("```", "").strip() # Generic cleanup
119
+
120
+ # Backup
121
+ backup_path = f"{file_path}.bak"
122
+ shutil.copy(file_path, backup_path)
123
+ console.print(f"[gray]Backup created: {backup_path}[/gray]")
124
+
125
+ with open(file_path, "w", encoding="utf-8") as f:
126
+ f.write(clean_result)
127
+
128
+ console.print(f"[green]File updated.[/green]")
129
+
130
+ if Confirm.ask("Create a PR for this fix?"):
131
+ # This assumes we are in a git repo
132
+ try:
133
+ branch_name = f"fix/ai-{os.urandom(4).hex()}"
134
+ run_shell(f"git checkout -b {branch_name}")
135
+ run_shell(f"git add {file_path}")
136
+ run_shell(f'git commit -m "AI Fix: {instruction}"')
137
+ run_shell(f"git push -u origin {branch_name}")
138
+ run_shell(f'gh pr create --title "AI Fix: {instruction}" --body "Automated fix." --web')
139
+ except Exception as e:
140
+ console.print(f"[red]PR creation failed:[/red] {e}")
141
+
142
+ def explain_code(context, mode="fast"):
143
+ """
144
+ Explains a concept or code snippet.
145
+ """
146
+ prompt = f"Task: Explain Concept/Code. Context: '{context}'. Keep it concise and technical."
147
+ result = generate_content(prompt, mode=mode)
148
+ if result:
149
+ console.print(f"\n[bold white]--- Explanation ---[/bold white]\n{result}\n[bold white]-------------------[/bold white]")
src/audit.py ADDED
@@ -0,0 +1,61 @@
1
+ import os
2
+ import json
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+ from rich.progress import Progress
6
+ from .utils import run_shell, check_gh_auth
7
+
8
+ console = Console()
9
+
10
+ def run_audit(user=None, repo_name=None):
11
+ """
12
+ Audits a repository for 'Gold Standard' items and returns a score.
13
+ """
14
+ username = user or check_gh_auth()
15
+ if not username:
16
+ console.print("[red]Not authenticated with gh CLI.[/red]")
17
+ return
18
+
19
+ # Use current directory if no repo specified
20
+ target_repo = repo_name or run_shell("gh repo view --json name --jq .name", check=False)
21
+ if not target_repo:
22
+ console.print("[yellow]Not inside a Git repository. Auditing current directory files only.[/yellow]")
23
+ repo_data = {}
24
+ else:
25
+ console.print(f"[cyan]Auditing Repository:[/cyan] [bold]{username}/{target_repo}[/bold]")
26
+ repo_data_raw = run_shell(f"gh repo view {username}/{target_repo} --json description,repositoryTopics,licenseInfo", check=False)
27
+ repo_data = json.loads(repo_data_raw) if repo_data_raw else {}
28
+
29
+ checks = {
30
+ "README.md": {"score": 20, "found": os.path.exists("README.md")},
31
+ "LICENSE": {"score": 10, "found": bool(repo_data.get("licenseInfo")) or any(os.path.exists(f) for f in ["LICENSE", "LICENSE.md", "LICENSE.txt"])},
32
+ "CONTRIBUTING.md": {"score": 10, "found": any(os.path.exists(f) for f in ["CONTRIBUTING.md", "CONTRIBUTING"])},
33
+ "Metadata: Description": {"score": 20, "found": bool(repo_data.get("description"))},
34
+ "Metadata: Topics": {"score": 20, "found": len(repo_data.get("repositoryTopics", [])) >= 3},
35
+ "CI/CD: GitHub Actions": {"score": 20, "found": os.path.exists(".github/workflows")},
36
+ }
37
+
38
+ total_score = sum(c["score"] for c in checks.values() if c["found"])
39
+
40
+ # Display Table
41
+ table = Table(title=f"Repository Audit: {target_repo or 'Local'}", border_style="blue")
42
+ table.add_column("Criterion", style="cyan")
43
+ table.add_column("Status", justify="center")
44
+ table.add_column("Weight", justify="right")
45
+
46
+ for name, data in checks.items():
47
+ status = "[green]GOLD[/green]" if data["found"] else "[red]LEAD[/red]"
48
+ table.add_row(name, status, f"{data['score']}")
49
+
50
+ console.print(table)
51
+
52
+ # Final Score Display
53
+ color = "green" if total_score >= 80 else "yellow" if total_score >= 50 else "red"
54
+ console.print(f"\n[bold]Transmutation Score:[/bold] [{color}]{total_score}%[/{color}]")
55
+
56
+ if total_score < 100:
57
+ console.print(f"\n[italic gray]The Alchemist suggests adding the missing components to reach 100% Gold.[/italic gray]")
58
+ else:
59
+ console.print(f"\n[bold yellow]✨ Pure Gold! This repository is optimized for the community.[/bold yellow]")
60
+
61
+ return total_score
src/cli.py ADDED
@@ -0,0 +1,84 @@
1
+ import argparse
2
+ import sys
3
+ from rich.console import Console
4
+ from .profile_gen import generate_profile
5
+ from .architect import scaffold_project, fix_code, explain_code
6
+ from .repo_tools import optimize_topics, generate_descriptions
7
+ from .issue_gen import create_issue
8
+ from .audit import run_audit
9
+ from .sage import ask_sage
10
+ from .committer import suggest_commits
11
+
12
+ console = Console()
13
+
14
+ def main():
15
+ parser = argparse.ArgumentParser(description="Git-Alchemist: AI-powered Git Operations")
16
+ parser.add_argument("--smart", action="store_true", help="Use high-end Gemini Pro models (slower/lower quota)")
17
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
18
+
19
+ # Commit Command
20
+ commit_parser = subparsers.add_parser("commit", help="Generate semantic commit messages from changes")
21
+
22
+ # Sage Command
23
+ sage_parser = subparsers.add_parser("sage", help="Ask the Sage questions about your codebase")
24
+ sage_parser.add_argument("question", help="The question about your code")
25
+
26
+ # Audit Command
27
+ audit_parser = subparsers.add_parser("audit", help="Check repository 'Gold' status and metadata")
28
+ audit_parser.add_argument("--repo", help="Specific repository name to audit")
29
+
30
+ # Profile Generator Command
31
+ profile_parser = subparsers.add_parser("profile", help="Generate or update GitHub Profile README")
32
+ profile_parser.add_argument("--force", action="store_true", help="Force full regeneration")
33
+ profile_parser.add_argument("--user", help="GitHub username (optional, detects automatically)")
34
+
35
+ # Repo Tools
36
+ topics_parser = subparsers.add_parser("topics", help="Optimize repository topics/tags")
37
+ topics_parser.add_argument("--user", help="GitHub username")
38
+
39
+ describe_parser = subparsers.add_parser("describe", help="Generate missing repository descriptions")
40
+ describe_parser.add_argument("--user", help="GitHub username")
41
+
42
+ # Issue Generator
43
+ issue_parser = subparsers.add_parser("issue", help="Draft a technical issue from an idea")
44
+ issue_parser.add_argument("idea", help="The feature or bug idea")
45
+
46
+ # Architect Commands
47
+ scaffold_parser = subparsers.add_parser("scaffold", help="Generate a new project structure (safe mode)")
48
+ scaffold_parser.add_argument("instruction", help="What to build (e.g., 'A Flask app with Docker')")
49
+
50
+ fix_parser = subparsers.add_parser("fix", help="Modify a file using AI")
51
+ fix_parser.add_argument("file", help="Path to the file to fix")
52
+ fix_parser.add_argument("instruction", help="What to change")
53
+
54
+ explain_parser = subparsers.add_parser("explain", help="Explain code or concepts")
55
+ explain_parser.add_argument("context", help="The code or concept to explain")
56
+
57
+ args = parser.parse_args()
58
+ mode = "smart" if args.smart else "fast"
59
+
60
+ if args.command == "profile":
61
+ generate_profile(args.user, args.force, mode=mode)
62
+ elif args.command == "topics":
63
+ optimize_topics(args.user, mode=mode)
64
+ elif args.command == "describe":
65
+ generate_descriptions(args.user, mode=mode)
66
+ elif args.command == "issue":
67
+ create_issue(args.idea, mode=mode)
68
+ elif args.command == "scaffold":
69
+ scaffold_project(args.instruction, mode=mode)
70
+ elif args.command == "fix":
71
+ fix_code(args.file, args.instruction, mode=mode)
72
+ elif args.command == "explain":
73
+ explain_code(args.context, mode=mode)
74
+ elif args.command == "audit":
75
+ run_audit(repo_name=args.repo)
76
+ elif args.command == "sage":
77
+ ask_sage(args.question, mode=mode)
78
+ elif args.command == "commit":
79
+ suggest_commits(mode=mode)
80
+ else:
81
+ parser.print_help()
82
+
83
+ if __name__ == "__main__":
84
+ main()
src/committer.py ADDED
@@ -0,0 +1,70 @@
1
+ import os
2
+ from rich.console import Console
3
+ from rich.prompt import Prompt
4
+ from .core import generate_content
5
+ from .utils import run_shell
6
+
7
+ console = Console()
8
+
9
+ def get_staged_diff():
10
+ """Returns the diff of staged changes."""
11
+ return run_shell("git diff --cached", check=False)
12
+
13
+ def suggest_commits(mode="fast"):
14
+ """
15
+ Analyzes staged changes and suggests 3 semantic commit messages.
16
+ """
17
+ diff = get_staged_diff()
18
+
19
+ if not diff:
20
+ console.print("[yellow]No staged changes found.[/yellow]")
21
+ if Prompt.ask("Stage all changes now? (git add .)", choices=["y", "n"], default="y") == "y":
22
+ run_shell("git add .")
23
+ diff = get_staged_diff()
24
+ else:
25
+ return
26
+
27
+ console.print("[cyan]Analyzing changes for the perfect commit message...[/cyan]")
28
+
29
+ prompt = f"""
30
+ Task: Suggest 3 professional, semantic commit messages based on the git diff below.
31
+ Format: <type>(<scope>): <subject>
32
+ Types: feat, fix, docs, style, refactor, test, chore
33
+
34
+ DIFF:
35
+ '''
36
+ {diff[:5000]}
37
+ '''
38
+
39
+ Instructions:
40
+ 1. Return ONLY a numbered list of 3 options.
41
+ 2. No explanations or extra text.
42
+ 3. Ensure they are concise and accurate.
43
+ """
44
+
45
+ result = generate_content(prompt, mode=mode)
46
+ if not result:
47
+ return
48
+
49
+ options = [line.strip() for line in result.strip().split("\n") if line.strip()]
50
+ # Remove numbering if AI added it (e.g., "1. feat: ...")
51
+ clean_options = []
52
+ for opt in options:
53
+ # Match "1. ", "1) ", etc.
54
+ import re
55
+ clean_opt = re.sub(r'^\d+[\.\)]\s*', '', opt).strip()
56
+ if clean_opt:
57
+ clean_options.append(clean_opt)
58
+
59
+ console.print("\n[bold green]Recommended Transmutations:[/bold green]")
60
+ for i, opt in enumerate(clean_options, 1):
61
+ console.print(f" [bold cyan]{i}.[/bold cyan] {opt}")
62
+
63
+ choice = Prompt.ask("\nSelect a message to commit (or 'c' to cancel)", choices=[str(i) for i in range(1, len(clean_options)+1)] + ["c"])
64
+
65
+ if choice != "c":
66
+ selected_msg = clean_options[int(choice)-1]
67
+ console.print(f"[green]Committing with message:[/green] {selected_msg}")
68
+ run_shell(f'git commit -m "{selected_msg}"')
69
+ else:
70
+ console.print("[yellow]Commit aborted.[/yellow]")
src/core.py ADDED
@@ -0,0 +1,59 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ from google import genai
5
+ from dotenv import load_dotenv
6
+ from rich.console import Console
7
+
8
+ console = Console()
9
+
10
+ # Define model tiers based on your available list
11
+ SMART_MODELS = [
12
+ "gemini-3-pro-preview",
13
+ "gemini-2.5-pro",
14
+ "gemini-1.5-pro", # Fallback to stable if preview fails
15
+ ]
16
+
17
+ FAST_MODELS = [
18
+ "gemma-3-27b-it",
19
+ "gemma-3-12b-it",
20
+ "gemini-3-flash-preview",
21
+ "gemini-2.0-flash",
22
+ ]
23
+
24
+ def get_gemini_client():
25
+ load_dotenv()
26
+ api_key = os.getenv("GEMINI_API_KEY")
27
+ if not api_key:
28
+ console.print("[bold red]Error:[/bold red] GEMINI_API_KEY not found.")
29
+ sys.exit(1)
30
+ return genai.Client(api_key=api_key, http_options={'api_version':'v1alpha'})
31
+
32
+ def generate_content(prompt, mode="fast"):
33
+ """
34
+ Generates content with automatic fallback.
35
+ Mode: 'fast' (Gemma/Flash) or 'smart' (Pro/3-Pro)
36
+ """
37
+ client = get_gemini_client()
38
+ models = SMART_MODELS if mode == "smart" else FAST_MODELS
39
+
40
+ for model_name in models:
41
+ try:
42
+ console.print(f"[gray]Attempting with {model_name}...[/gray]")
43
+ response = client.models.generate_content(
44
+ model=model_name,
45
+ contents=prompt
46
+ )
47
+ if response and response.text:
48
+ return response.text
49
+ except Exception as e:
50
+ err_msg = str(e)
51
+ if "429" in err_msg or "RESOURCE_EXHAUSTED" in err_msg:
52
+ console.print(f"[yellow]Quota hit for {model_name}. Trying next...[/yellow]")
53
+ continue
54
+ else:
55
+ console.print(f"[red]Error with {model_name}:[/red] {err_msg}")
56
+ continue
57
+
58
+ console.print("[bold red]Critical:[/bold red] All models exhausted or failed.")
59
+ return None
src/issue_gen.py ADDED
@@ -0,0 +1,76 @@
1
+ import json
2
+ import tempfile
3
+ import os
4
+ from rich.console import Console
5
+ from .core import generate_content
6
+ from .utils import run_shell
7
+
8
+ console = Console()
9
+
10
+ def create_issue(idea, mode="fast"):
11
+ """
12
+ Translates an idea into a technical GitHub issue.
13
+ """
14
+ console.print(f"[cyan]Drafting technical issue for: {idea} ({mode} mode)...[/cyan]")
15
+
16
+ prompt = f"""
17
+ You are a Senior Tech Lead.
18
+ User Input: '{idea}'
19
+ TASK: Translate this into a technical implementation plan.
20
+ STRUCTURE:
21
+ - **Context**: 1 sentence explaining WHY.
22
+ - **Directives**: Bullet points of EXACTLY what to change.
23
+ OUTPUT FORMAT: Return ONLY a JSON object with keys: "title", "body", "label", "easy" (boolean).
24
+ No markdown blocks.
25
+ """
26
+
27
+ result = generate_content(prompt, mode=mode)
28
+ if not result: return
29
+
30
+ try:
31
+ clean_json = result.replace("```json", "").replace("```", "").strip()
32
+ issue = json.loads(clean_json)
33
+
34
+ title = f"[DRAFT] {issue['title']}"
35
+ body = f"{issue['body']}\n\n> Automated by Git-Alchemist"
36
+ label = issue.get('label', 'enhancement')
37
+
38
+ console.print(f"[yellow]Uploading Draft: {title}[/yellow]")
39
+
40
+ # Create labels if they don't exist
41
+ run_shell('gh label create "automated" --color "505050" 2>/dev/null', check=False)
42
+ run_shell('gh label create "status: draft" --color "333333" 2>/dev/null', check=False)
43
+ run_shell(f'gh label create "{label}" 2>/dev/null', check=False)
44
+
45
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as tf:
46
+ tf.write(body)
47
+ temp_name = tf.name
48
+
49
+ cmd = [
50
+ 'gh',
51
+ 'issue',
52
+ 'create',
53
+ '--title',
54
+ f'"{title}"',
55
+ '--body-file',
56
+ f'"{temp_name}"',
57
+ '--label',
58
+ '"status: draft"',
59
+ '--label',
60
+ '"automated"',
61
+ '--label',
62
+ f'"{label}"'
63
+ ]
64
+
65
+ if issue.get('easy'):
66
+ run_shell('gh label create "good first issue" --color "7057ff" 2>/dev/null', check=False)
67
+ cmd.extend(['--label', '"good first issue"'])
68
+
69
+ run_shell(" ".join(cmd))
70
+ os.unlink(temp_name)
71
+
72
+ console.print("[green]Success! Issue created as draft.[/green]")
73
+
74
+ except Exception as e:
75
+ console.print(f"[red]Failed to create issue:[/red] {e}")
76
+ console.print(f"[gray]Raw Output:[/gray] {result}")
src/profile_gen.py ADDED
@@ -0,0 +1,197 @@
1
+ import os
2
+ import json
3
+ import tempfile
4
+ import shutil
5
+ import re
6
+ from pathlib import Path
7
+ from rich.console import Console
8
+ from rich.prompt import Confirm
9
+ from .core import generate_content
10
+ from .utils import run_shell, check_gh_auth, get_user_email
11
+
12
+ console = Console()
13
+
14
+ def fetch_repos(username):
15
+ """
16
+ Fetches public repositories for the user.
17
+ """
18
+ console.print("[cyan]Fetching repositories...[/cyan]")
19
+ cmd = 'gh repo list --visibility=public --limit 100 --json name,description,url,isPrivate,isArchived,stargazerCount'
20
+ try:
21
+ output = run_shell(cmd)
22
+ return json.loads(output)
23
+ except Exception as e:
24
+ console.print(f"[red]Failed to fetch repos:[/red] {e}")
25
+ return []
26
+
27
+ def filter_repos(repos, username, strategy="FULL_GEN", existing_content=""):
28
+ """
29
+ Filters out junk, private, archived, and irrelevant repos (like Awesome lists).
30
+ """
31
+ candidates = []
32
+
33
+ # Blocklist for low-value repos
34
+ junk_patterns = ["test", "export", "WPy64", "PROFILE_DRAFT.md", "temp", "awesome-"]
35
+
36
+ for r in repos:
37
+ name = r['name']
38
+
39
+ # Basic filters
40
+ if name == username: continue
41
+ if r.get('isPrivate'): continue
42
+ if r.get('isArchived'): continue
43
+ if any(p in name.lower() for p in junk_patterns): continue
44
+ if name.endswith(".exe"): continue
45
+
46
+ # Strategy filter
47
+ if strategy == "SMART_UPDATE":
48
+ # Check if name or URL exists in current profile content
49
+ if name in existing_content or r['url'] in existing_content:
50
+ continue
51
+
52
+ candidates.append(r)
53
+
54
+ return candidates
55
+
56
+ def generate_profile(username, force=False, mode="fast"):
57
+ """
58
+ Main function to generate or update the profile.
59
+ """
60
+ if not username:
61
+ username = check_gh_auth()
62
+ if not username:
63
+ console.print("[red]Not logged into GitHub CLI. Run 'gh auth login' first.[/red]")
64
+ return
65
+
66
+ console.print(f"[green]Authenticated as: {username}[/green]")
67
+
68
+ # Discovery
69
+ current_content = ""
70
+ strategy = "FULL_GEN"
71
+
72
+ try:
73
+ console.print("[cyan]Checking for existing profile...[/cyan]")
74
+ current_content = run_shell(f'gh api "repos/{username}/{username}/readme" --headers "Accept: application/vnd.github.raw"', check=False)
75
+ if current_content and len(current_content) > 200 and not force:
76
+ console.print("[green]Found existing robust profile. Switching to SMART_UPDATE.[/green]")
77
+ strategy = "SMART_UPDATE"
78
+ else:
79
+ console.print("[yellow]Profile basic or missing. Using FULL_GEN.[/yellow]")
80
+ except:
81
+ pass
82
+
83
+ # Fetch & Filter
84
+ repos = fetch_repos(username)
85
+ candidates = filter_repos(repos, username, strategy, current_content)
86
+
87
+ if not candidates:
88
+ console.print("[green]No new repositories to add.[/green]")
89
+ return
90
+
91
+ # Prompt Engineering
92
+ candidates_str = "\n".join([f"- Name: {r['name']}\n Desc: {r['description'] or ''}\n URL: {r['url']}" for r in candidates])
93
+
94
+ prompt = ""
95
+ if strategy == "SMART_UPDATE":
96
+ prompt = f"""
97
+ Task: Update a GitHub Profile README.
98
+ Existing Markdown:
99
+ '''
100
+ {current_content}
101
+ '''
102
+
103
+ New Projects to Add:
104
+ {candidates_str}
105
+
106
+ Instructions:
107
+ 1. Integrate the 'New Projects' into the 'Existing Markdown' naturally.
108
+ 2. Place them under appropriate categories.
109
+ 3. PRESERVE THE EXISTING STRUCTURE: Do not delete existing sections, badges, or headers.
110
+ 4. MANDATORY LINKING: Use the provided URL for each project in the format '- **[Name](URL)** - Description'.
111
+ 5. MATCH THE STYLE: If the existing list uses bullets, stick to it.
112
+ 6. STRICTLY NO EMOJIS in new additions.
113
+ 7. Output the FULL updated Markdown content.
114
+ """
115
+ else:
116
+ # Full Gen
117
+ prompt = f"""
118
+ Task: Generate a professional Project Showcase for a GitHub Profile.
119
+ Username: {username}
120
+
121
+ Projects List:
122
+ {candidates_str}
123
+
124
+ Instructions:
125
+ 1. Group projects into 3-6 meaningful categories (e.g., '## AI & Automation', '## Hardware').
126
+ 2. MANDATORY LINKING: Use the format '- **[Name](URL)** - Description'.
127
+ 3. Use a clean, professional header at the top.
128
+ 4. STRICTLY NO EMOJIS.
129
+ 5. Output the FULL Markdown.
130
+ """
131
+
132
+ console.print(f"[magenta]Generating content with Gemini ({mode} mode)...[/magenta]")
133
+ result = generate_content(prompt, mode=mode)
134
+
135
+ if not result:
136
+ return
137
+
138
+ # Post-processing
139
+ final_md = result.replace("```markdown", "").replace("```", "").strip()
140
+
141
+ # Ensure branding is preserved but clean
142
+ footer = "\n\n---\n*Generated by Git-Alchemist ⚗️*"
143
+ if "*Generated by Git-Alchemist*" not in final_md:
144
+ final_md += footer
145
+
146
+ # Save Draft
147
+ with open("PROFILE_DRAFT.md", "w", encoding="utf-8") as f:
148
+ f.write(final_md)
149
+ console.print(f"[magenta]Draft saved to PROFILE_DRAFT.md[/magenta]")
150
+
151
+ # Deploy (Strictly PR)
152
+ if Confirm.ask("Deploy these changes via Pull Request?"):
153
+ deploy_profile(username, final_md)
154
+
155
+ def deploy_profile(username, content):
156
+ """
157
+ Clones the profile repo, updates README, and opens a PR.
158
+ """
159
+ temp_dir = tempfile.mkdtemp(prefix="git_alchemist_")
160
+ try:
161
+ console.print("[cyan]Cloning profile repository...[/cyan]")
162
+ run_shell(f'gh repo clone {username}/{username} {temp_dir}')
163
+
164
+ # Determine the target dir (sometimes gh clones into a subdir, sometimes not)
165
+ repo_dir = Path(temp_dir)
166
+ if (repo_dir / username).exists():
167
+ repo_dir = repo_dir / username
168
+
169
+ # Write file
170
+ readme_path = repo_dir / "README.md"
171
+ with open(readme_path, "w", encoding="utf-8") as f:
172
+ f.write(content)
173
+
174
+ # Git ops
175
+ cwd = os.getcwd()
176
+ os.chdir(repo_dir)
177
+
178
+ branch_name = f"profile-update-{os.urandom(2).hex()}"
179
+ run_shell(f'git checkout -b {branch_name}')
180
+
181
+ user_email = get_user_email() or f"{username}@users.noreply.github.com"
182
+ run_shell(f'git config user.name "{username}"')
183
+ run_shell(f'git config user.email "{user_email}"')
184
+
185
+ run_shell('git add README.md')
186
+ run_shell(f'git commit -m "docs: Update profile README via Git-Alchemist"')
187
+ run_shell(f'git push -u origin {branch_name} --force')
188
+
189
+ console.print("[green]Opening PR...[/green]")
190
+ run_shell(f'gh pr create --title "AI Profile Update" --body "Automated profile update generated by Git-Alchemist. Added missing repo links and organized new projects."')
191
+
192
+ os.chdir(cwd)
193
+
194
+ except Exception as e:
195
+ console.print(f"[red]Deployment failed:[/red] {e}")
196
+ finally:
197
+ shutil.rmtree(temp_dir, ignore_errors=True)
src/promote.py ADDED
@@ -0,0 +1,74 @@
1
+ import os
2
+ import requests
3
+ from rich.console import Console
4
+
5
+ console = Console()
6
+
7
+ STORY = """
8
+ I’ve been working on this tool for a couple of months now. It actually started as a
9
+ collection of loose PowerShell scripts I wrote to handle my own GitHub maintenance
10
+ (updating my profile, tagging repos, etc.).
11
+
12
+ I finally decided to combine them all into a unified project. I ported everything to
13
+ Python and built Git-Alchemist. It uses Gemini 3 and Gemma 3 to automate the boring
14
+ parts of Git management—like writing descriptions or scaffolding project structures
15
+ in safe, temporary workspaces. I'm really happy with how the consolidation turned out
16
+ and wanted to share it with the community.
17
+ """
18
+
19
+ def post_to_devto(api_key):
20
+ """Automates posting to Dev.to"""
21
+ url = "https://dev.to/api/articles"
22
+ headers = {"api-key": api_key}
23
+
24
+ article = {
25
+ "article": {
26
+ "title": "I consolidated my Git automation scripts into a unified AI stack: Git-Alchemist",
27
+ "published": True,
28
+ "body_markdown": f"# Git-Alchemist ⚗️\n\n{STORY}\n\nCheck it out here: [https://github.com/abduznik/Git-Alchemist](https://github.com/abduznik/Git-Alchemist)\n\nLanding Page: [https://abduznik.github.io/Git-Alchemist/](https://abduznik.github.io/Git-Alchemist/)",
29
+ "tags": ["python", "github", "ai", "opensource"],
30
+ "series": "Git Automation"
31
+ }
32
+ }
33
+
34
+ try:
35
+ response = requests.post(url, json=article, headers=headers)
36
+ if response.status_code == 201:
37
+ console.print("[green]Successfully posted to Dev.to![/green]")
38
+ else:
39
+ console.print(f"[red]Failed to post to Dev.to:[/red] {response.text}")
40
+ except Exception as e:
41
+ console.print(f"[red]Error:[/red] {e}")
42
+
43
+ def generate_manual_submissions():
44
+ """Generates text for manual form submissions"""
45
+ submissions = {
46
+ "Hackaday (https://hackaday.com/submit-a-tip/)": {
47
+ "Subject": "AI-Powered GitHub Maintenance: Git-Alchemist",
48
+ "Message": f"Hi Hackaday! I thought you might find this interesting. {STORY}\nRepo: https://github.com/abduznik/Git-Alchemist"
49
+ },
50
+ "TLDR Newsletter (https://tldr.tech/submit)": {
51
+ "Link": "https://github.com/abduznik/Git-Alchemist",
52
+ "Description": "A unified AI stack for GitHub repository management and project scaffolding."
53
+ },
54
+ "Console.dev (Newsletter for Dev Tools)": {
55
+ "Email": "hello@console.dev",
56
+ "Subject": "Tool Submission: Git-Alchemist",
57
+ "Body": f"Hi Console team, {STORY}\nRepo: https://github.com/abduznik/Git-Alchemist"
58
+ }
59
+ }
60
+
61
+ console.print("\n[bold cyan]--- Manual Submission Drafts ---[/bold cyan]")
62
+ for platform, data in submissions.items():
63
+ console.print(f"\n[bold yellow]{platform}[/bold yellow]")
64
+ for k, v in data.items():
65
+ console.print(f"[bold]{k}:[/bold] {v}")
66
+
67
+ if __name__ == "__main__":
68
+ devto_key = os.getenv("DEVTO_API_KEY")
69
+ if devto_key:
70
+ post_to_devto(devto_key)
71
+ else:
72
+ console.print("[yellow]No DEVTO_API_KEY found. Skipping automated post.[/yellow]")
73
+
74
+ generate_manual_submissions()
src/repo_tools.py ADDED
@@ -0,0 +1,111 @@
1
+ import json
2
+ import time
3
+ from rich.console import Console
4
+ from .core import generate_content
5
+ from .utils import run_shell, check_gh_auth
6
+
7
+ console = Console()
8
+
9
+ def optimize_topics(user=None, mode="fast"):
10
+ """
11
+ Analyzes repositories and adds relevant topics using Gemini.
12
+ """
13
+ username = user or check_gh_auth()
14
+ if not username:
15
+ console.print("[red]Not authenticated with gh CLI.[/red]")
16
+ return
17
+
18
+ console.print(f"[cyan]Optimizing topics for {username} ({mode} mode)...[/cyan]")
19
+ repos_raw = run_shell('gh repo list --visibility=public --limit 100 --json name,description,repositoryTopics')
20
+ repos = json.loads(repos_raw)
21
+
22
+ count = 0
23
+ for repo in repos:
24
+ name = repo['name']
25
+ desc = repo.get('description') or "No description provided"
26
+
27
+ # Safe access to topics
28
+ raw_topics = repo.get('repositoryTopics')
29
+ if raw_topics is None:
30
+ existing = []
31
+ else:
32
+ existing = [t['name'] for t in raw_topics]
33
+
34
+ if len(existing) >= 5:
35
+ continue
36
+
37
+ console.print(f"[white]Analyzing {name}...[/white]")
38
+
39
+ prompt = f"""
40
+ Task: Suggest search-friendly GitHub topics for project "{name}".
41
+ Description: "{desc}".
42
+ Existing Topics: {existing}.
43
+ Return ONLY a JSON array of strings (max 5 total topics).
44
+ Focus on technical keywords like 'python', 'api', 'automation', 'cli'.
45
+ Output Example: ["python", "automation"]
46
+ """
47
+ result = generate_content(prompt, mode=mode)
48
+ if not result: continue
49
+
50
+ try:
51
+ clean_json = result.replace("```json", "").replace("```", "").strip()
52
+ new_tags = json.loads(clean_json)
53
+
54
+ # Filter out existing
55
+ to_add = [t for t in new_tags if t not in existing]
56
+
57
+ if to_add:
58
+ tag_str = ",".join(to_add)
59
+ console.print(f" [green]Adding tags:[/green] {tag_str}")
60
+ run_shell(f'gh repo edit {username}/{name} --add-topic "{tag_str}"')
61
+ count += 1
62
+ time.sleep(0.5)
63
+ except:
64
+ console.print(f" [red]Failed to parse topics for {name}[/red]")
65
+
66
+ console.print(f"[cyan]Done! Optimized {count} repositories.[/cyan]")
67
+
68
+ def generate_descriptions(user=None, mode="fast"):
69
+ """
70
+ Generates descriptions for repositories that are missing them.
71
+ """
72
+ username = user or check_gh_auth()
73
+ if not username: return
74
+
75
+ console.print(f"[cyan]Generating descriptions for {username} ({mode} mode)...[/cyan]")
76
+ repos_raw = run_shell('gh repo list --visibility=public --limit 100 --json name,description')
77
+ repos = json.loads(repos_raw)
78
+
79
+ count = 0
80
+ for repo in repos:
81
+ name = repo['name']
82
+ if name == username: continue # Skip profile repo
83
+ if repo.get('description'): continue # Skip if already has desc
84
+
85
+ console.print(f"[white]Analyzing {name}...[/white]")
86
+
87
+ # Fetch Readme
88
+ try:
89
+ readme = run_shell(f'gh repo view {username}/{name} --json body -q .body', check=False)
90
+ context = readme[:1500] if readme else "No readme available."
91
+ except:
92
+ context = "No readme available."
93
+
94
+ prompt = f"""
95
+ Task: Generate a GitHub repository description for project "{name}".
96
+ Readme Context: "{context}".
97
+ Constraint: Max 20 words. Start with an action verb.
98
+ Output ONLY the description. No quotes.
99
+ """
100
+ result = generate_content(prompt, mode=mode)
101
+ if not result: continue
102
+
103
+ new_desc = result.strip().replace('"', '').replace("'", "")
104
+ if len(new_desc) > 200: new_desc = new_desc[:197] + "..."
105
+
106
+ console.print(f" [green]New Desc:[/green] {new_desc}")
107
+ run_shell(f'gh repo edit {username}/{name} --description "{new_desc}"')
108
+ count += 1
109
+ time.sleep(0.5)
110
+
111
+ console.print(f"[cyan]Done! Updated {count} descriptions.[/cyan]")
src/sage.py ADDED
@@ -0,0 +1,71 @@
1
+ import os
2
+ from rich.console import Console
3
+ from .core import generate_content
4
+ from .utils import run_shell
5
+
6
+ console = Console()
7
+
8
+ def get_codebase_context():
9
+ """
10
+ Scans the repository and aggregates source code into a single context string.
11
+ """
12
+ context = []
13
+ # Extensions to include
14
+ extensions = {'.py', '.md', '.ps1', '.sh', '.js', '.ts', '.c', '.cpp', '.h', '.yml', '.yaml', '.Dockerfile'}
15
+ # Folders to ignore
16
+ ignore_dirs = {'__pycache__', '.git', 'venv', 'node_modules', '.tmp', 'docs'}
17
+
18
+ for root, dirs, files in os.walk("."):
19
+ # Filter directories in-place
20
+ dirs[:] = [d for d in dirs if d not in ignore_dirs]
21
+
22
+ for file in files:
23
+ ext = os.path.splitext(file)[1]
24
+ if ext in extensions:
25
+ path = os.path.join(root, file)
26
+ try:
27
+ with open(path, "r", encoding="utf-8") as f:
28
+ content = f.read()
29
+ context.append(f"--- FILE: {path} ---\n{content}\n")
30
+ except Exception:
31
+ continue
32
+
33
+ return "\n".join(context)
34
+
35
+ def ask_sage(question, mode="fast"):
36
+ """
37
+ Queries Gemini using the aggregated codebase as context.
38
+ """
39
+ console.print("[cyan]The Sage is meditating on your codebase...[/cyan]")
40
+
41
+ code_context = get_codebase_context()
42
+
43
+ if not code_context:
44
+ console.print("[yellow]Warning: No source files found to analyze.[/yellow]")
45
+ code_context = "No code found in repository."
46
+
47
+ prompt = f"""
48
+ You are "The Sage", an expert software architect and technical lead.
49
+ Below is the source code context for the current project.
50
+ Use this context to answer the user's question precisely and technically.
51
+
52
+ CONTEXT:
53
+ '''
54
+ {code_context}
55
+ '''
56
+
57
+ USER QUESTION:
58
+ {question}
59
+
60
+ Instructions:
61
+ 1. Base your answer ONLY on the provided code.
62
+ 2. Be concise but deep.
63
+ 3. If the answer isn't in the code, say so.
64
+ """
65
+
66
+ result = generate_content(prompt, mode=mode)
67
+
68
+ if result:
69
+ console.print("\n[bold fuchsia]--- The Sage's Wisdom ---[/bold fuchsia]")
70
+ console.print(result)
71
+ console.print("[bold fuchsia]-----------------------[/bold fuchsia]")
src/utils.py ADDED
@@ -0,0 +1,47 @@
1
+ import subprocess
2
+ import json
3
+ import shutil
4
+ from rich.console import Console
5
+
6
+ console = Console()
7
+
8
+ def run_shell(command, check=True, capture_output=True):
9
+ """
10
+ Runs a shell command and returns the result.
11
+ """
12
+ try:
13
+ result = subprocess.run(
14
+ command,
15
+ shell=True,
16
+ check=check,
17
+ capture_output=capture_output,
18
+ text=True
19
+ )
20
+ return result.stdout.strip()
21
+ except subprocess.CalledProcessError as e:
22
+ if check:
23
+ console.print(f"[bold red]Command Failed:[/bold red] {command}")
24
+ console.print(f"[red]Error:[/red] {e.stderr}")
25
+ raise e
26
+ return None
27
+
28
+ def check_gh_auth():
29
+ """
30
+ Checks if the user is authenticated with GitHub CLI.
31
+ Returns the username if authenticated, else None.
32
+ """
33
+ try:
34
+ user_login = run_shell('gh api user -q ".login"')
35
+ return user_login
36
+ except:
37
+ return None
38
+
39
+ def get_user_email():
40
+ """
41
+ Gets the user email from GitHub CLI.
42
+ """
43
+ try:
44
+ email = run_shell('gh api user -q ".email"', check=False)
45
+ return email if email else None
46
+ except:
47
+ return None