relm 0.1.1__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.
relm/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1"
relm/core.py ADDED
@@ -0,0 +1,75 @@
1
+ import os
2
+ import tomllib
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+ @dataclass
8
+ class Project:
9
+ name: str
10
+ version: str
11
+ path: Path
12
+ description: Optional[str] = None
13
+
14
+ @property
15
+ def pyproject_path(self) -> Path:
16
+ return self.path / "pyproject.toml"
17
+
18
+ def __str__(self) -> str:
19
+ return f"{self.name} (v{self.version}) - {self.path}"
20
+
21
+ def load_project(path: Path) -> Optional[Project]:
22
+ """
23
+ Loads a project from a directory if it contains a valid pyproject.toml.
24
+ """
25
+ pyproject_file = path / "pyproject.toml"
26
+ if not pyproject_file.exists():
27
+ return None
28
+
29
+ try:
30
+ with open(pyproject_file, "rb") as f:
31
+ data = tomllib.load(f)
32
+
33
+ project_data = data.get("project", {})
34
+ name = project_data.get("name")
35
+ version = project_data.get("version")
36
+ description = project_data.get("description")
37
+
38
+ if name and version:
39
+ return Project(
40
+ name=name,
41
+ version=version,
42
+ path=path,
43
+ description=description
44
+ )
45
+ except Exception as e:
46
+ # We might want to log this error in a real app
47
+ pass
48
+
49
+ return None
50
+
51
+ def find_projects(root_path: Path) -> List[Project]:
52
+ """
53
+ Scans the immediate subdirectories of root_path for valid projects.
54
+ """
55
+ projects = []
56
+ if not root_path.exists() or not root_path.is_dir():
57
+ return projects
58
+
59
+ # Check if the root itself is a project
60
+ root_project = load_project(root_path)
61
+ if root_project:
62
+ projects.append(root_project)
63
+
64
+ # Check subdirectories
65
+ for item in root_path.iterdir():
66
+ if item.is_dir() and item != root_path:
67
+ # Avoid recursing too deep or checking hidden dirs for now
68
+ if item.name.startswith("."):
69
+ continue
70
+
71
+ project = load_project(item)
72
+ if project:
73
+ projects.append(project)
74
+
75
+ return sorted(projects, key=lambda p: p.name)
relm/git_ops.py ADDED
@@ -0,0 +1,85 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+ from typing import List
4
+
5
+ def run_git_command(args: List[str], cwd: Path) -> str:
6
+ """
7
+ Runs a git command in the specified directory.
8
+ Raises subprocess.CalledProcessError on failure.
9
+ """
10
+ result = subprocess.run(
11
+ ["git"] + args,
12
+ cwd=cwd,
13
+ capture_output=True,
14
+ text=True,
15
+ check=True
16
+ )
17
+ return result.stdout.strip()
18
+
19
+ def is_git_clean(path: Path) -> bool:
20
+ """
21
+ Checks if the git repository is clean (no uncommitted changes).
22
+ """
23
+ try:
24
+ # update-index -q --refresh is good practice before diff-index
25
+ subprocess.run(["git", "update-index", "-q", "--refresh"], cwd=path, check=False)
26
+ # check for unstaged changes
27
+ subprocess.run(["git", "diff-files", "--quiet"], cwd=path, check=True)
28
+ # check for staged changes
29
+ subprocess.run(["git", "diff-index", "--cached", "--quiet", "HEAD", "--"], cwd=path, check=True)
30
+ return True
31
+ except subprocess.CalledProcessError:
32
+ return False
33
+
34
+ def git_add(path: Path, files: List[str]):
35
+ run_git_command(["add"] + files, cwd=path)
36
+
37
+ def git_commit(path: Path, message: str):
38
+ run_git_command(["commit", "-m", message], cwd=path)
39
+
40
+ def git_tag(path: Path, tag_name: str, message: str = None):
41
+ args = ["tag", tag_name]
42
+ if message:
43
+ args.extend(["-m", message])
44
+ run_git_command(args, cwd=path)
45
+
46
+ def git_push(path: Path):
47
+ run_git_command(["push"], cwd=path)
48
+ run_git_command(["push", "--tags"], cwd=path)
49
+
50
+ def git_fetch_tags(path: Path):
51
+ """
52
+ Fetches tags from the remote to ensure local knowledge is up to date.
53
+ """
54
+ run_git_command(["fetch", "--tags"], cwd=path)
55
+
56
+ def git_tag_exists(path: Path, tag_name: str) -> bool:
57
+ """
58
+ Checks if a specific tag exists locally.
59
+ """
60
+ try:
61
+ # git rev-parse -q --verify "refs/tags/v1.0.0"
62
+ run_git_command(["rev-parse", "-q", "--verify", f"refs/tags/{tag_name}"], cwd=path)
63
+ return True
64
+ except subprocess.CalledProcessError:
65
+ return False
66
+
67
+ def git_has_changes(path: Path, tag_name: str) -> bool:
68
+ """
69
+ Checks if there are changes between the given tag and HEAD.
70
+ Returns True if changes exist, False otherwise.
71
+ """
72
+ try:
73
+ # git diff --quiet tag_name HEAD -- .
74
+ # If exit code is 1, there are changes. If 0, no changes.
75
+ # We use check=True which raises error on non-zero... wait.
76
+ # diff --quiet returns 1 if diffs found. So we want to catch the error.
77
+
78
+ subprocess.run(
79
+ ["git", "diff", "--quiet", tag_name, "HEAD", "--", "."],
80
+ cwd=path,
81
+ check=True
82
+ )
83
+ return False # Exit code 0 means NO differences
84
+ except subprocess.CalledProcessError:
85
+ return True # Exit code 1 means differences exist (or error, but usually diffs)
relm/main.py ADDED
@@ -0,0 +1,108 @@
1
+ import argparse
2
+ import sys
3
+ from pathlib import Path
4
+ from rich.console import Console
5
+ from rich.panel import Panel
6
+ from rich.table import Table
7
+ from .core import find_projects
8
+ from .release import perform_release
9
+
10
+ console = Console()
11
+
12
+ def list_projects(path: Path):
13
+ projects = find_projects(path)
14
+ if not projects:
15
+ console.print("[yellow]No projects found in this directory.[/yellow]")
16
+ return
17
+
18
+ table = Table(title=f"Found {len(projects)} Projects")
19
+ table.add_column("Name", style="cyan", no_wrap=True)
20
+ table.add_column("Version", style="magenta")
21
+ table.add_column("Path", style="green")
22
+ table.add_column("Description")
23
+
24
+ for project in projects:
25
+ table.add_row(
26
+ project.name,
27
+ project.version,
28
+ str(project.path),
29
+ project.description or ""
30
+ )
31
+
32
+ console.print(table)
33
+
34
+ def main():
35
+ parser = argparse.ArgumentParser(
36
+ description="Manage releases and versioning for local Python projects."
37
+ )
38
+ parser.add_argument(
39
+ "--path",
40
+ default=".",
41
+ help="Path to the root directory containing projects (default: current dir)."
42
+ )
43
+
44
+ subparsers = parser.add_subparsers(dest="command", required=True)
45
+
46
+ # List command
47
+ list_parser = subparsers.add_parser("list", help="List all discovered projects")
48
+
49
+ # Release command
50
+ release_parser = subparsers.add_parser("release", help="Release a new version of a project")
51
+ release_parser.add_argument("project_name", help="Name of the project to release (must match pyproject.toml name)")
52
+ release_parser.add_argument("type", choices=["major", "minor", "patch"], default="patch", nargs="?", help="Type of version bump")
53
+ release_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompts (assume yes)")
54
+
55
+ args = parser.parse_args()
56
+ root_path = Path(args.path).resolve()
57
+
58
+ if args.command == "list":
59
+ list_projects(root_path)
60
+
61
+ elif args.command == "release":
62
+ all_projects = find_projects(root_path)
63
+
64
+ target_projects = []
65
+ check_changes_flag = False
66
+
67
+ if args.project_name == "all":
68
+ target_projects = all_projects
69
+ check_changes_flag = True
70
+ console.print(f"[bold]Running Bulk Release on {len(target_projects)} projects...[/bold]")
71
+ else:
72
+ # Find single project
73
+ target = next((p for p in all_projects if p.name == args.project_name), None)
74
+ if not target:
75
+ console.print(f"[red]Project '{args.project_name}' not found in {root_path}[/red]")
76
+ sys.exit(1)
77
+ target_projects = [target]
78
+
79
+ # Execute releases
80
+ results = {"released": [], "skipped": [], "failed": []}
81
+
82
+ for project in target_projects:
83
+ # Skip template/meta repos if needed, but git_has_changes handles most logic
84
+ try:
85
+ success = perform_release(
86
+ project,
87
+ args.type,
88
+ yes_mode=args.yes,
89
+ check_changes=check_changes_flag
90
+ )
91
+ if success:
92
+ results["released"].append(project.name)
93
+ else:
94
+ results["skipped"].append(project.name)
95
+ except Exception as e:
96
+ console.print(f"[red]Critical error releasing {project.name}: {e}[/red]")
97
+ results["failed"].append(project.name)
98
+
99
+ # Summary
100
+ if args.project_name == "all":
101
+ console.rule("Bulk Release Summary")
102
+ console.print(f"[green]Released: {len(results['released'])}[/green] {results['released']}")
103
+ console.print(f"[yellow]Skipped: {len(results['skipped'])}[/yellow]")
104
+ if results["failed"]:
105
+ console.print(f"[red]Failed: {len(results['failed'])}[/red] {results['failed']}")
106
+
107
+ if __name__ == "__main__":
108
+ main()
relm/release.py ADDED
@@ -0,0 +1,166 @@
1
+ import sys
2
+ import subprocess
3
+ from pathlib import Path
4
+ from typing import Literal
5
+
6
+ from rich.console import Console
7
+ from rich.prompt import Confirm, Prompt
8
+
9
+ from .core import Project
10
+ from .versioning import bump_version_string, update_file_content, update_version_tests
11
+ from .git_ops import is_git_clean, git_add, git_commit, git_tag, git_push, git_fetch_tags, git_tag_exists, git_has_changes
12
+
13
+ console = Console()
14
+
15
+ def run_tests(project_path: Path) -> bool:
16
+ """
17
+ Runs pytest in the project directory. Returns True if successful.
18
+ """
19
+ # Check if pytest is installed/available?
20
+ # We assume the user has the env set up correctly or it's in path.
21
+ console.print("[bold blue]Running tests...[/bold blue]")
22
+ try:
23
+ # We use sys.executable -m pytest to use the same env
24
+ subprocess.run(
25
+ [sys.executable, "-m", "pytest"],
26
+ cwd=project_path,
27
+ check=True
28
+ )
29
+ return True
30
+ except subprocess.CalledProcessError:
31
+ return False
32
+ except FileNotFoundError:
33
+ console.print("[yellow]pytest not found. Skipping tests.[/yellow]")
34
+ return True # Treat missing pytest as "pass" (or warn?) - safer to warn and pass for now
35
+
36
+ def revert_changes(project_path: Path):
37
+ """
38
+ Reverts local changes using git checkout.
39
+ """
40
+ console.print("[yellow]Reverting changes...[/yellow]")
41
+ try:
42
+ subprocess.run(["git", "checkout", "."], cwd=project_path, check=True)
43
+ except Exception as e:
44
+ console.print(f"[red]Failed to revert changes: {e}[/red]")
45
+
46
+ def perform_release(project: Project, part: Literal['major', 'minor', 'patch'], yes_mode: bool = False, check_changes: bool = False) -> bool:
47
+ console.rule(f"Releasing {project.name}")
48
+
49
+ # 0. Fetch Tags & Check State
50
+ console.print("[dim]Fetching remote tags...[/dim]")
51
+ try:
52
+ git_fetch_tags(project.path)
53
+ except Exception:
54
+ console.print("[yellow]Warning: Could not fetch remote tags. Proceeding with local info.[/yellow]")
55
+
56
+ current_version_tag = f"v{project.version}"
57
+ is_already_tagged = git_tag_exists(project.path, current_version_tag)
58
+
59
+ # Smart Skip Logic
60
+ if check_changes and is_already_tagged:
61
+ if not git_has_changes(project.path, current_version_tag):
62
+ console.print(f"[dim]No changes detected since {current_version_tag}. Skipping.[/dim]")
63
+ return False
64
+
65
+ should_bump = True
66
+ target_version = project.version # Default to current if not bumping
67
+
68
+ if not is_already_tagged:
69
+ console.print(f"[yellow]Notice: Current version [bold]{project.version}[/bold] is NOT tagged locally.[/yellow]")
70
+ # If yes_mode is on, we default to RETRY (True), i.e. skip bump
71
+ if yes_mode or Confirm.ask(f"Retry release for v{project.version} (skip bump)?", default=True):
72
+ should_bump = False
73
+ target_version = project.version
74
+
75
+ # 1. Check Git Cleanliness
76
+ if not is_git_clean(project.path):
77
+ console.print("[red]Error: Git repository is not clean. Commit or stash changes first.[/red]")
78
+ # We could potentially auto-commit if yes_mode is on, but that's risky.
79
+ return False
80
+
81
+ # 2. Calculate New Version (if bumping)
82
+ if should_bump:
83
+ try:
84
+ target_version = bump_version_string(project.version, part)
85
+ console.print(f"Current version: [cyan]{project.version}[/cyan]")
86
+ console.print(f"New version: [green]{target_version}[/green]")
87
+ except ValueError as e:
88
+ console.print(f"[red]Error parsing version: {e}[/red]")
89
+ return False
90
+ else:
91
+ console.print(f"Releasing existing version: [green]{target_version}[/green]")
92
+
93
+
94
+ if not yes_mode and not Confirm.ask("Proceed with release?"):
95
+ console.print("[yellow]Release cancelled.[/yellow]")
96
+ return False
97
+
98
+ # 3. Update Files (Only if bumping)
99
+ if should_bump:
100
+ console.print("[bold blue]Updating files...[/bold blue]")
101
+ files_updated = []
102
+
103
+ # Update pyproject.toml
104
+ if update_file_content(project.pyproject_path, project.version, target_version):
105
+ files_updated.append("pyproject.toml")
106
+
107
+ # Update __init__.py
108
+ # Try src/{name}/__init__.py first
109
+ init_path = project.path / "src" / project.name.replace("-", "_") / "__init__.py"
110
+ if not init_path.exists():
111
+ # Try {name}/__init__.py
112
+ init_path = project.path / project.name.replace("-", "_") / "__init__.py"
113
+
114
+ if init_path.exists():
115
+ if update_file_content(init_path, project.version, target_version):
116
+ files_updated.append(str(init_path.relative_to(project.path)))
117
+
118
+ # Update Tests (Hardcoded versions)
119
+ updated_tests = update_version_tests(project.path, project.version, target_version)
120
+ if updated_tests:
121
+ console.print(f"[green]Automatically updated version assertions in {len(updated_tests)} test files.[/green]")
122
+ files_updated.extend(updated_tests)
123
+
124
+ if not files_updated:
125
+ console.print("[red]No files were updated! Check version strings.[/red]")
126
+ return False
127
+
128
+ # 4. Run Tests Locally
129
+ if not run_tests(project.path):
130
+ console.print("[bold red]Tests failed! Aborting release.[/bold red]")
131
+ if yes_mode or Confirm.ask("Revert changes to files?", default=True):
132
+ revert_changes(project.path)
133
+ return False
134
+
135
+ # 5. Git Commit
136
+ console.print("[bold blue]Committing...[/bold blue]")
137
+ try:
138
+ git_add(project.path, files_updated)
139
+ git_commit(project.path, f"release: bump version to {target_version}")
140
+ except Exception as e:
141
+ console.print(f"[red]Git commit error: {e}[/red]")
142
+ return False
143
+
144
+ # 6. Tag (Always needed)
145
+ # We double check if tag exists now, just in case
146
+ if git_tag_exists(project.path, f"v{target_version}"):
147
+ console.print(f"[yellow]Tag v{target_version} already exists locally. Skipping creation.[/yellow]")
148
+ else:
149
+ console.print(f"[bold blue]Tagging v{target_version}...[/bold blue]")
150
+ try:
151
+ git_tag(project.path, f"v{target_version}", f"Release v{target_version}")
152
+ except Exception as e:
153
+ console.print(f"[red]Git tag error: {e}[/red]")
154
+ return False
155
+
156
+ # 7. Push
157
+ if yes_mode or Confirm.ask("Push changes to remote? (This will trigger the GitHub Action release)"):
158
+ try:
159
+ git_push(project.path)
160
+ except Exception as e:
161
+ console.print(f"[red]Push error: {e}[/red]")
162
+ return False
163
+
164
+ console.print(f"[bold green]Successfully tagged and pushed {project.name} v{target_version}![/bold green]")
165
+ console.print("[dim]The GitHub Action workflow should now handle the PyPI release.[/dim]")
166
+ return True
relm/versioning.py ADDED
@@ -0,0 +1,111 @@
1
+ import re
2
+ from pathlib import Path
3
+ from typing import Tuple, List
4
+
5
+ def parse_version(version: str) -> Tuple[int, int, int]:
6
+ """
7
+ Parses a version string 'x.y.z' into a tuple of integers.
8
+ """
9
+ try:
10
+ parts = version.split('.')
11
+ if len(parts) < 3:
12
+ # Handle cases like '0.1' -> '0.1.0'
13
+ parts.extend(['0'] * (3 - len(parts)))
14
+ return int(parts[0]), int(parts[1]), int(parts[2])
15
+ except ValueError:
16
+ raise ValueError(f"Invalid version format: {version}")
17
+
18
+ def bump_version_string(version: str, part: str) -> str:
19
+ """
20
+ Bumps the version string based on the part ('major', 'minor', 'patch').
21
+ """
22
+ major, minor, patch = parse_version(version)
23
+
24
+ if part == 'major':
25
+ major += 1
26
+ minor = 0
27
+ patch = 0
28
+ elif part == 'minor':
29
+ minor += 1
30
+ patch = 0
31
+ elif part == 'patch':
32
+ patch += 1
33
+ else:
34
+ raise ValueError(f"Invalid bump part: {part}")
35
+
36
+ return f"{major}.{minor}.{patch}"
37
+
38
+ def update_file_content(path: Path, old_version: str, new_version: str) -> bool:
39
+ """
40
+ Replaces occurrences of old_version with new_version in the file at path.
41
+ Returns True if changes were made.
42
+ """
43
+ if not path.exists():
44
+ return False
45
+
46
+ try:
47
+ content = path.read_text(encoding="utf-8")
48
+
49
+ new_content = content
50
+
51
+ # Pattern for pyproject.toml: version = "1.0.0"
52
+ toml_pattern = re.compile(rf'version\s*=\s*"{re.escape(old_version)}"')
53
+ if toml_pattern.search(content):
54
+ new_content = toml_pattern.sub(f'version = "{new_version}"', new_content)
55
+
56
+ # Pattern for __init__.py: __version__ = "1.0.0"
57
+ init_pattern = re.compile(rf'__version__\s*=\s*"{re.escape(old_version)}"')
58
+ if init_pattern.search(content):
59
+ new_content = init_pattern.sub(f'__version__ = "{new_version}"', new_content)
60
+
61
+ if new_content != content:
62
+ path.write_text(new_content, encoding="utf-8")
63
+ return True
64
+
65
+ except Exception as e:
66
+ print(f"Error updating {path}: {e}")
67
+ return False
68
+
69
+ return False
70
+
71
+ def update_version_tests(project_path: Path, old_version: str, new_version: str) -> List[str]:
72
+ """
73
+ Scans the 'tests' directory for files containing the old version string
74
+ in an assertion context and updates them. returns list of updated files.
75
+ """
76
+ updated_files = []
77
+ tests_dir = project_path / "tests"
78
+ if not tests_dir.exists():
79
+ return updated_files
80
+
81
+ # Regex to match: assert ... == "1.2.3" or assert "1.2.3" == ...
82
+ # We are generous with whitespace
83
+ # This might need refinement but covers standard cases.
84
+ # We actually just look for the literal string "1.2.3" inside test files
85
+ # because replacing it strictly in context is safer than broad replace,
86
+ # but parsing python AST is too heavy.
87
+ # Let's look for the exact string "old_version" to be safe,
88
+ # but only if it looks like a version check?
89
+ # actually, if a test file has the version string "1.0.0", it is 99% likely the version check.
90
+
91
+ for test_file in tests_dir.rglob("*.py"):
92
+ try:
93
+ content = test_file.read_text(encoding="utf-8")
94
+ if old_version in content:
95
+ # Check if it's surrounded by quotes to avoid partial matches
96
+ # e.g. matching "1.0" in "1.0.0" (though old_version is usually full)
97
+
98
+ # Simple string replace for "old_version" -> "new_version"
99
+ # We use quotes to ensure we match string literals
100
+ if f'"{old_version}"' in content:
101
+ new_content = content.replace(f'"{old_version}"', f'"{new_version}"')
102
+ test_file.write_text(new_content, encoding="utf-8")
103
+ updated_files.append(str(test_file.relative_to(project_path)))
104
+ elif f"'{old_version}'" in content:
105
+ new_content = content.replace(f"'{old_version}'", f"'{new_version}'")
106
+ test_file.write_text(new_content, encoding="utf-8")
107
+ updated_files.append(str(test_file.relative_to(project_path)))
108
+ except Exception:
109
+ pass
110
+
111
+ return updated_files
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: relm
3
+ Version: 0.1.1
4
+ Summary: A unified CLI tool to manage versioning, git, and PyPI releases for multiple projects.
5
+ Author-email: dhruv13x <dhruv13x@gmail.com>
6
+ License: MIT © dhruv13x
7
+ Project-URL: Homepage, https://github.com/dhruv13x/relm
8
+ Project-URL: Source, https://github.com/dhruv13x/relm
9
+ Project-URL: Issues, https://github.com/dhruv13x/relm/issues
10
+ Keywords: cli,release,versioning,automation,pypi,git
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Environment :: Console
15
+ Classifier: Topic :: Software Development :: Build Tools
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: tomli; python_version < "3.11"
19
+ Requires-Dist: rich>=13.0.0
20
+ Requires-Dist: rich-argparse>=1.0.0
21
+ Requires-Dist: build>=1.0.0
22
+ Requires-Dist: twine>=4.0.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
25
+ Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
26
+ Requires-Dist: pytest-timeout>=2.2.0; extra == "dev"
27
+ Requires-Dist: pytest-json-report>=1.5.0; extra == "dev"
28
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
29
+ Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
30
+ Requires-Dist: pyfakefs>=5.0.0; extra == "dev"
31
+ Requires-Dist: ruff>=0.6.0; extra == "dev"
32
+ Requires-Dist: black>=24.3.0; extra == "dev"
33
+ Requires-Dist: mypy>=1.11.0; extra == "dev"
34
+ Requires-Dist: PyYAML>=6.0; extra == "dev"
35
+ Requires-Dist: types-PyYAML>=6.0; extra == "dev"
36
+
37
+ # Repo Manager
38
+
39
+ A unified CLI tool to automate versioning, git operations, and PyPI releases for the dhruv13x tool suite.
40
+
41
+ ## Features
42
+
43
+ - **Project Discovery**: Automatically detects Python projects with `pyproject.toml`.
44
+ - **Smart Versioning**: Bumps versions (major, minor, patch) in `pyproject.toml` and `__init__.py`.
45
+ - **Git Automation**: Stages, commits, and pushes release changes.
46
+ - **PyPI Release**: Builds and uploads packages to PyPI.
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install -e .
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ ```bash
57
+ relm --help
58
+ ```
@@ -0,0 +1,11 @@
1
+ relm/__init__.py,sha256=rnObPjuBcEStqSO0S6gsdS_ot8ITOQjVj_-P1LUUYpg,22
2
+ relm/core.py,sha256=EqsB9DaUD987OZXOQ2bZXoW5yMolkJ27ExNhtpl7X_Q,2080
3
+ relm/git_ops.py,sha256=bL0k2fiW7IOBUtpY6lb_wBaCkmUUxRZ11pEnnoKqoa4,2871
4
+ relm/main.py,sha256=P78DY4QSABudY16CkYoPrWbSNA0YWcXHNTZ4SvkbedM,3973
5
+ relm/release.py,sha256=xuJUkaSVsazQguB1tBVfEROCzOJInVXPQSzEvCzn_n8,6947
6
+ relm/versioning.py,sha256=D4_MoPHd31mPGe0EBeHmYEOUDMYMh3ySBI2CrT3EZV0,4337
7
+ relm-0.1.1.dist-info/METADATA,sha256=uFYKrDxqOb0qDaa8OzXQcEUpCGkc9WGGH1Ut8gEtYqI,2042
8
+ relm-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ relm-0.1.1.dist-info/entry_points.txt,sha256=mt1ZiWqOl3MbE8ZrRhryrxE1owGgNH-v84px6w23pmg,40
10
+ relm-0.1.1.dist-info/top_level.txt,sha256=0-YNy4YWXcpbBb5DdW6wnOzqYGz2PMhQC0x2g85-MPE,5
11
+ relm-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ relm = relm.main:main
@@ -0,0 +1 @@
1
+ relm