├── .github └── workflows │ └── main.yml ├── .gitignore ├── .pre-commit-config.yaml ├── README.md ├── poetry.lock ├── pyproject.toml ├── src └── lazycodr │ ├── __init__.py │ ├── cli │ ├── __init__.py │ ├── config.py │ ├── pr.py │ └── readme.py │ ├── constants.py │ ├── prompts │ ├── __init__.py │ └── templates │ │ ├── pr │ │ ├── pr-generate-refine-init.prompt │ │ └── pr-generate-refine-loop.prompt │ │ └── readme │ │ ├── readme-file-generate.prompt │ │ ├── readme-file-summary-refine-init.prompt │ │ └── readme-file-summary-refine-loop.prompt │ └── utils │ ├── __init__.py │ ├── credentials.py │ ├── pr.py │ └── readme.py └── tests └── main_test.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python Package 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: 7 | - created 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Install poetry 17 | run: pipx install poetry 18 | 19 | - name: Check if tag matches the Poetry version 20 | run: | 21 | TAG_VERSION=${GITHUB_REF#refs/tags/v} # Strips 'refs/tags/v' leaving just the version number. 22 | POETRY_VERSION=$(poetry version --short) 23 | echo "Tag version: $TAG_VERSION" 24 | echo "Poetry version: $POETRY_VERSION" 25 | if [[ "$TAG_VERSION" != "$POETRY_VERSION" ]]; then 26 | echo "Error: Tag version ($TAG_VERSION) does not match the version in pyproject.toml ($POETRY_VERSION)" 27 | exit 1 28 | fi 29 | shell: bash 30 | 31 | - uses: actions/setup-python@v4 32 | with: 33 | python-version: '3.10' 34 | cache: 'poetry' 35 | 36 | - name: Cache deps 37 | run: poetry install 38 | 39 | - name: Build 40 | run: poetry build 41 | 42 | - name: Publish to PyPI 43 | env: 44 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} 45 | run: poetry publish -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | .credentials.json 163 | .vscode 164 | 165 | .ruff_cache 166 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.0.1 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-toml 7 | - id: check-yaml 8 | - id: end-of-file-fixer 9 | - id: trailing-whitespace 10 | - repo: https://github.com/pycqa/isort 11 | rev: 5.12.0 12 | hooks: 13 | - id: isort 14 | name: isort 15 | # Run the Ruff formatter. 16 | - repo: https://github.com/astral-sh/ruff-pre-commit 17 | # Ruff version. 18 | rev: v0.0.291 19 | hooks: 20 | - id: ruff-format 21 | # Run the Ruff linter. 22 | - repo: https://github.com/astral-sh/ruff-pre-commit 23 | # Ruff version. 24 | rev: v0.0.291 25 | hooks: 26 | - id: ruff 27 | # - repo: https://github.com/pre-commit/mirrors-mypy 28 | # rev: v0.910-1 29 | # hooks: 30 | # - id: mypy 31 | # - repo: local 32 | # hooks: 33 | # - id: pytest 34 | # name: Check pytest unit tests pass 35 | # # entry: poetry run duty test 36 | # entry: poetry run duty test 37 | # pass_filenames: false 38 | # language: system 39 | # types: [python] 40 | - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks 41 | rev: v2.8.0 42 | hooks: 43 | - id: pretty-format-toml 44 | args: [--autofix] 45 | - id: pretty-format-yaml 46 | args: [--autofix] 47 | exclude: .copier-answers.yml 48 | # based on 49 | # https://gitlab.com/smop/pre-commit-hooks/-/blob/master/.pre-commit-hooks.yaml 50 | - repo: local 51 | hooks: 52 | - id: check-poetry 53 | name: Poetry check 54 | description: Validates the structure of the pyproject.toml file 55 | entry: poetry check 56 | language: system 57 | pass_filenames: false 58 | files: pyproject.toml 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LazyCodr 🚀 2 | 3 | A CLI tool designed to help lazy coders get their work done with AI! LazyCodr automates tasks such as generating pull requests, and more, using our beloved AI models. 4 | 5 | [YouTube Video Introduction](https://youtu.be/-_nZhPcOTIA) 6 | 7 | ## Features 💡 8 | 9 | - Generate pull request descriptions 10 | - Automate commit messages (incoming ...) 11 | - Easy configuration 12 | - Works with GitHub API 13 | - Powered by LLMs (for now only OpenAI) 14 | 15 | ## Installation 💻 16 | 17 | ```bash 18 | pip install lazycodr 19 | ``` 20 | 21 | ## Requirements 🔑 22 | 23 | Before you can use LazyCodr, you'll need to create an OpenAI API key and a GitHub token. Here's how to do both: 24 | 25 | ### OpenAI API Key 26 | 27 | 1. Sign up for an account on the [OpenAI website](https://beta.openai.com/signup/) if you don't have one already. 28 | 2. Once logged in, go to the [API Keys page](https://beta.openai.com/account/api-keys). 29 | 3. Click on "Create an API key" and copy the generated key. 30 | 31 | ### GitHub Token 32 | 33 | 1. Log in to your GitHub account and go to the [Personal Access Tokens page](https://github.com/settings/tokens). 34 | 2. Click on "Generate new token" in the top right corner.j 35 | 3. Give your token a descriptive name and select the required scopes (for LazyCodr, you'll need `repo` and `user` scopes. 36 | 4. Click "Generate token" at the bottom of the page and copy the generated token. 37 | 38 | After you have both your OpenAI API key and GitHub token, you can configure LazyCodr by running the following command: 39 | 40 | ```bash 41 | lazycodr config credentials 42 | ``` 43 | 44 | This command will prompt you to enter your API key and GitHub token, which will be securely stored for future use. 45 | 46 | Now you're all set to use LazyCodr! 🚀 47 | 48 | 49 | ## Usage 📚 50 | 51 | 1. Configure LazyCodr with your OpenAI API key and GitHub token: 52 | 53 | ```bash 54 | lazycodr config credentials 55 | ``` 56 | 57 | 2. Use LazyCodr to generate a pull request description: 58 | 59 | ```bash 60 | lazycodr pr generate 61 | ``` 62 | 63 | 3. Use LazyCodr to generate README.md file for a git repo (it will pickup the ignored pattern from the .gitignore if it exists and you can also add additional ones with --ignore option): 64 | 65 | ```bash 66 | lazycodr readme generate --ignore=pattern1 --ignore=pattern2 ... 67 | ``` 68 | 69 | ## Roadmap 🗺️ 70 | 71 | > "A lazy programmer is a great programmer" 72 | 73 | We're on a mission to make all of us even lazier 😅! 74 | There is no clear roadmap, but here are some ideas for LazyCodr's future: 75 | 76 | 🚀 **Commit Message Generation**:
77 | Automatically generate meaningful commit messages based on your code changes, so you can save time and focus on coding. 78 | 79 | 🚀 **Codebase Conversations**:
80 | Chat with your codebase to get AI-powered recommendations and insights about your code, helping you make informed decisions as you work. 81 | 82 | 🚀 **AI-driven Guidance**:
83 | Receive step-by-step guidance from AI on how to write new features or implement specific functionality, making it easier to tackle challenging tasks. 84 | 85 | 🚀 [DONE] **README Generation**:
86 | Automatically generate well-structured and informative README files for your projects, ensuring that your documentation is always up to date. 87 | 88 | 🚀 ... replace yourself entirely so you can take 10 jobs in parallel 🤑🤑🤑 89 | 90 | Remember, even though I'm aiming to make you the laziest programmer possible 😜, I still appreciate your help. 91 | If you have any ideas, suggestions, or improvements, feel free to contribute and help make LazyCodr even better for your fellow lazy programmers. 92 | 93 | Together, we can redefine the art of lazy programming! 94 | 95 | ## Contributing 🤝 96 | 97 | Contributions are welcome! Feel free to submit a pull request or open an issue. 98 | 99 | ## License 📄 100 | 101 | This project is licensed under the MIT License. 102 | 103 | Happy coding! 🎉 104 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "poetry.core.masonry.api" 3 | requires = ["poetry-core"] 4 | 5 | [tool.black] 6 | line-length = 88 7 | 8 | [tool.isort] 9 | profile = "black" 10 | 11 | [tool.poetry] 12 | authors = ["JimZer "] 13 | description = "A CLI tool to help lazy coders get the work done with AI (commit messages, pull requests ...)" 14 | homepage = "https://gprithub.com/bitswired/lazycodr" 15 | name = "lazycodr" 16 | packages = [{include = "lazycodr", from = "src"}] 17 | readme = "README.md" 18 | repository = "https://github.com/bitswired/lazycodr" 19 | version = "0.2.3" 20 | 21 | [tool.poetry.dependencies] 22 | httpx = "^0.24.0" 23 | jinja2 = "^3.1.2" 24 | langchain = "^0.0.325" 25 | openai = "^0.28.1" 26 | pathspec = "^0.11.2" 27 | pyfiglet = "^1.0.2" 28 | pygithub = "^2.1.1" 29 | python = "^3.10" 30 | rich = "^13.6.0" 31 | tiktoken = "^0.4.0" 32 | tqdm = "^4.66.1" 33 | typer = {extras = ["all"], version = "^0.9.0"} 34 | 35 | [tool.poetry.group.dev.dependencies] 36 | pre-commit = "^3.5.0" 37 | pytest = "^7.4.3" 38 | ruff = "^0.1.3" 39 | 40 | [tool.poetry.scripts] 41 | lazycodr = "lazycodr.cli:main" 42 | 43 | [tool.ruff] 44 | ignore = ['D100', 'D103', 'D104', 'D107', 'ANN', 'BLE001', 'FA102'] 45 | select = ['ALL'] 46 | # Note: Ruff supports a top-level `src` option in lieu of isort's `src_paths` setting. 47 | src = ["src", "tests"] 48 | 49 | [tool.ruff.flake8-bugbear] 50 | extend-immutable-calls = ["typer.Option"] 51 | 52 | [tool.ruff.isort] 53 | known-first-party = ["lazycodr"] 54 | 55 | [tool.ruff.per-file-ignores] 56 | "tests/**/*" = ['S101'] 57 | -------------------------------------------------------------------------------- /src/lazycodr/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitswired/lazycodr/62c8b93ae5e15ac7376cce6d1d396dde1618dde6/src/lazycodr/__init__.py -------------------------------------------------------------------------------- /src/lazycodr/cli/__init__.py: -------------------------------------------------------------------------------- 1 | import pyfiglet 2 | import typer 3 | from rich.console import Console 4 | 5 | from .config import app as config 6 | from .pr import app as pr 7 | from .readme import app as readme 8 | 9 | console = Console() 10 | 11 | app = typer.Typer() 12 | app.add_typer(config, name="config") 13 | app.add_typer(pr, name="pr") 14 | app.add_typer(readme, name="readme") 15 | 16 | 17 | def main(): 18 | console.print(pyfiglet.figlet_format("LazyCodr", font="small"), style="bold green") 19 | console.print("💻 Welcome Lazy Coder 🚀", style="bold green") 20 | app() 21 | 22 | 23 | if __name__ == "__main__": 24 | main() 25 | -------------------------------------------------------------------------------- /src/lazycodr/cli/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from typing import Annotated 4 | 5 | import typer 6 | from rich.console import Console 7 | 8 | app = typer.Typer() 9 | 10 | console = Console() 11 | 12 | 13 | @app.command() 14 | def credentials( 15 | openai_api_key: Annotated[str, typer.Option(prompt=True, hide_input=True)], 16 | github_token: Annotated[str, typer.Option(prompt=True, hide_input=True)], 17 | ): 18 | # Save credentials to file json 19 | credentials = { 20 | "openai_api_key": openai_api_key, 21 | "github_token": github_token, 22 | } 23 | with Path.open(Path.home() / ".lazy-coder-credentials.json", "w") as outfile: 24 | json.dump(credentials, outfile) 25 | 26 | console.print("Credentials saved", style="bold green") 27 | 28 | 29 | if __name__ == "__main__": 30 | app() 31 | -------------------------------------------------------------------------------- /src/lazycodr/cli/pr.py: -------------------------------------------------------------------------------- 1 | import typer 2 | from rich.console import Console 3 | from rich.markdown import Markdown 4 | from rich.progress import Progress, SpinnerColumn, TextColumn 5 | 6 | from lazycodr.utils.pr import generate_pr, get_pr_diff 7 | 8 | console = Console() 9 | 10 | 11 | app = typer.Typer() 12 | 13 | 14 | @app.command() 15 | def generate(repo_name: str, pr_number: int): 16 | pr_template = typer.edit("") 17 | with Progress( 18 | SpinnerColumn(), 19 | TextColumn("[progress.description]{task.description}"), 20 | transient=True, 21 | ) as progress: 22 | progress.add_task(description="Getting diff...", total=None) 23 | pr_diff = get_pr_diff(repo_name, pr_number) 24 | 25 | progress.add_task(description="Generating PR description...", total=None) 26 | res = generate_pr(pr_diff, pr_template) 27 | 28 | md = Markdown(res) 29 | console.print(md, width=90) 30 | 31 | 32 | if __name__ == "__main__": 33 | app() 34 | -------------------------------------------------------------------------------- /src/lazycodr/cli/readme.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import typer 4 | from rich.console import Console 5 | 6 | from lazycodr.utils.readme import generate_readme 7 | 8 | console = Console() 9 | 10 | 11 | app = typer.Typer() 12 | 13 | 14 | @app.command() 15 | def generate( 16 | repo_path: Path, 17 | ignore: list[str] = typer.Option([]), 18 | ): 19 | readme = generate_readme(repo_path, ignore) 20 | typer.echo(readme) 21 | 22 | 23 | if __name__ == "__main__": 24 | app() 25 | -------------------------------------------------------------------------------- /src/lazycodr/constants.py: -------------------------------------------------------------------------------- 1 | PR_REFINE_INIT_TEMPLATE_NAME = "pr/pr-generate-refine-init" 2 | PR_REFINE_LOOP_TEMPLATE_NAME = "pr/pr-generate-refine-loop" 3 | 4 | README_FILE_SUMMARY_REFINE_INIT_TEMPLATE_NAME = "readme/readme-file-summary-refine-init" 5 | README_FILE_SUMMARY_REFINE_LOOP_TEMPLATE_NAME = "readme/readme-file-summary-refine-loop" 6 | README_FILE_SUMMARY_GENERATE_TEMPLATE_NAME = "readme/readme-file-generate" 7 | -------------------------------------------------------------------------------- /src/lazycodr/prompts/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from langchain.prompts import PromptTemplate 4 | 5 | 6 | def load_template(name): 7 | path = Path(__file__).parent.absolute() / "templates" / f"{name}.prompt" 8 | template = path.open().read() 9 | return PromptTemplate.from_template(template) 10 | -------------------------------------------------------------------------------- /src/lazycodr/prompts/templates/pr/pr-generate-refine-init.prompt: -------------------------------------------------------------------------------- 1 | Write a description for the following pull request. 2 | It should be well-formatted for readability with spaces and newlines and use Markdown formatting to make it look nice. 3 | It should follow the following template: 4 |