# Directory Structure
```
├── .github
│ └── workflows
│ └── release.yml
├── .gitignore
├── .gitmessage
├── .python-version
├── CHANGELOG.md
├── ollamatools.py
├── pyproject.toml
├── README.md
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
3.14
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
.ruff_cache/
.python-version
```
--------------------------------------------------------------------------------
/.gitmessage:
--------------------------------------------------------------------------------
```
# <type>(<scope>): <subject>
#
# <body>
#
# <footer>
# Type should be one of the following:
# * feat: A new feature
# * fix: A bug fix
# * docs: Documentation only changes
# * style: Changes that do not affect the meaning of the code
# * refactor: A code change that neither fixes a bug nor adds a feature
# * perf: A code change that improves performance
# * test: Adding missing tests or correcting existing tests
# * build: Changes that affect the build system or external dependencies
# * ci: Changes to our CI configuration files and scripts
# * chore: Other changes that don't modify src or test files
# * revert: Reverts a previous commit
# Scope is optional and should be the name of the package affected
# (as perceived by the person reading the changelog)
# Subject line should:
# * use the imperative, present tense: "change" not "changed" nor "changes"
# * not capitalize the first letter
# * not end with a dot (.)
# Body should include the motivation for the change and contrast this with previous behavior
# Footer should contain:
# * Information about Breaking Changes
# * Reference GitHub issues that this commit closes
# Examples:
# feat(parser): add ability to parse arrays
# fix(release): need to depend on latest rxjs and zone.js
# docs(changelog): update changelog to beta.5
# fix(release): need to depend on latest rxjs and zone.js
# feat(lang): add polish language
# perf(core): improve bundle size by removing debug code
# BREAKING CHANGE: The graphiteWidth option has been removed.
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Ollama Tool CLI 🦙
A modern CLI tool for managing Ollama models - backup, restore, update, and list your models with ease.
## Installation
### Using pip (recommended)
```bash
pip install ollama-tool-cli
```
### Using uv
```bash
uv add ollama-tool-cli
```
### From source
```bash
git clone https://github.com/arian24b/ollamatools.git
cd ollamatools
uv sync
```
## Requirements
- Python 3.10 or higher
- Ollama installed and running
## Usage
### Basic Commands
```bash
# Show help
ollama-tool-cli
# List all installed models
ollama-tool-cli list
# Update all models
ollama-tool-cli update
# Update a specific model
ollama-tool-cli update llama3.2
# Backup all models to default location (~/Downloads/ollama_model_backups)
ollama-tool-cli backup
# Backup to custom path
ollama-tool-cli backup --path /path/to/backup
# Backup a specific model
ollama-tool-cli backup --model llama3.2
# Restore from backup
ollama-tool-cli restore /path/to/backup.zip
# Show Ollama version
ollama-tool-cli version
# Show installation information
ollama-tool-cli info
# Check if Ollama is installed
ollama-tool-cli check
```
### Command Details
#### `list`
Display all installed Ollama models with their versions.
#### `update [model]`
Update one or all Ollama models. If no model name is provided, updates all models.
#### `backup [--path PATH] [--model MODEL]`
Backup Ollama models to zip files. By default backs up all models to `~/Downloads/ollama_model_backups`.
- `--path, -p`: Custom backup directory path
- `--model, -m`: Backup only a specific model
#### `restore <path>`
Restore Ollama models from a backup zip file or directory.
#### `version`
Display the installed Ollama version.
#### `info`
Show detailed Ollama installation information including version, models path, platform, and number of installed models.
#### `check`
Verify that Ollama is installed and accessible.
## Development
### Setup development environment
```bash
uv sync
```
### Build the package
```bash
uv build
```
## License
MIT License - see LICENSE file for details.
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# CHANGELOG
<!-- version list -->
## v1.1.1 (2025-12-27)
### Bug Fixes
- Update Git configuration to use GitHub actor and modify URLs in pyproject.toml
([`ebc97c2`](https://github.com/arian24b/ollamatools/commit/ebc97c2d46ad90ea1e5f2eec3da25682a60d25f6))
## v1.1.0 (2025-12-27)
### Bug Fixes
- Change build command to uv build for semantic-release compatibility
([`0ec8798`](https://github.com/arian24b/ollamatools/commit/0ec87980bd70119ec2a775d9427f15fc4a19832f))
### Chores
- Update version to 1.0.1 and modify build settings
([`e55f3ff`](https://github.com/arian24b/ollamatools/commit/e55f3ffdd2303d8b8c55b6086b1e78fb380e9964))
### Features
- Rename package to ollama-tool-cli and CLI command to ollama-tool-cli
([`aa2843f`](https://github.com/arian24b/ollamatools/commit/aa2843fd341753263cd5600051b3108d00dceed7))
## v1.0.0 (2025-12-27)
- Initial Release
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
name: Release
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: write
id-token: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up UV
uses: astral-sh/setup-uv@v7
with:
python-version: '3.14'
- name: Install dependencies
run: |
uv tool install python-semantic-release
- name: Configure Git
run: |
git config user.name ${{ github.actor }}
git config user.email ${{ github.actor }}@users.noreply.github.com
- name: Semantic Release (Version, Tag, GitHub Release)
id: semantic-release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
semantic-release version --changelog --tag
- name: Build Package
if: steps.semantic-release.outputs.version != ''
run: |
uv sync
uv build
- name: Publish to PyPI
if: steps.semantic-release.outputs.version != ''
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[project]
name = "ollama-tool-cli"
version = "1.1.1"
description = "CLI tool for managing Ollama models - backup, restore, update, and list models"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"typer>=0.12.0",
]
authors = [
{name = "Arian Omrani", email = "[email protected]"}
]
license = "MIT"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
keywords = ["ollama", "cli", "models", "backup", "restore", "llm"]
[project.optional-dependencies]
dev = ["ruff", "pytest", "build"]
[project.scripts]
ollama-tool-cli = "ollamatools:app"
[project.urls]
Homepage = "https://github.com/arian24b/ollamatools"
Repository = "https://github.com/arian24b/ollamatools"
Issues = "https://github.com/arian24b/ollamatools/issues"
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.semantic_release]
commit_parser = "conventional"
commit_author = "Arian Omrani <[email protected]>"
branch = "main"
version_toml = ["pyproject.toml:project.version"]
upload_to_vcs_release = false
build_command = "uv build"
```
--------------------------------------------------------------------------------
/ollamatools.py:
--------------------------------------------------------------------------------
```python
from dataclasses import dataclass
from json import loads
from pathlib import Path
from subprocess import PIPE, Popen
from sys import platform
from zipfile import ZipFile
import typer
# Test commit for patch release
@dataclass
class CMDOutput:
output_text: str
error_text: str
return_code: int
def __str__(self) -> str:
return f"Output Text: {self.output_text}\nError Text: {self.error_text}\nReturn Code: {self.return_code}"
MODELS_PATH = {
"linux": Path("/usr/share/ollama/.ollama/models").expanduser(),
"macos": Path("~/.ollama/models").expanduser(),
"windows": Path("C:\\Users\\%USERNAME%\\.ollama\\models").expanduser(),
}
BACKUP_PATH = Path("~/Downloads/ollama_model_backups").expanduser()
def run_command(command: str | list) -> CMDOutput:
process = Popen(
command,
shell=True,
stdout=PIPE,
stderr=PIPE,
stdin=PIPE,
text=True,
encoding="utf-8",
)
output_text, error_text = process.communicate()
return CMDOutput(
output_text=output_text.strip(),
error_text=error_text.strip(),
return_code=process.returncode,
)
def check_ollama_installed() -> bool:
result = run_command("which ollama")
return result.return_code == 0
def ollama_version() -> str:
result = run_command("ollama --version")
return result.output_text.strip()
def create_backup(path_to_backup: list[Path], backup_path: Path) -> None:
with ZipFile(backup_path, "w") as zfile:
for file in path_to_backup:
zfile.write(file)
def ollama_models_path() -> Path:
match platform.lower():
case "linux":
return MODELS_PATH["linux"]
case "darwin":
return MODELS_PATH["macos"]
case "win32":
return MODELS_PATH["windows"]
case _:
msg = "Unsupported operating system"
raise OSError(msg)
def models() -> list[str]:
result = run_command("ollama list").output_text.strip().split("\n")
return [line.split()[0] for line in result[1:]]
def update_models(model_names: list[str]) -> None:
for model_name in model_names:
run_command(f"ollama pull {model_name}")
def backup_models(backup_path: Path = BACKUP_PATH, model: str | None = None) -> None:
models_path = ollama_models_path()
backup_path = Path(backup_path)
backup_path.mkdir(parents=True, exist_ok=True)
for model in models():
model_name, model_version = (
model.split(":") if ":" in model else (model, "latest")
)
model_schema_path = (
models_path
/ f"manifests/registry.ollama.ai/library/{model_name}/{model_version}"
)
model_layers = loads(Path(model_schema_path).read_bytes())["layers"]
digests_path = [
models_path / "blobs" / layer["digest"].replace(":", "-")
for layer in model_layers
]
digests_path.append(model_schema_path)
archive_path = backup_path / f"{model_name}-{model_version}.zip"
create_backup(digests_path, archive_path)
def restore_models(backup_path: Path) -> None:
backup_path = Path(backup_path).expanduser()
models_path = ollama_models_path()
with ZipFile(backup_path, "r") as zfile:
zfile.extractall(models_path)
app = typer.Typer(no_args_is_help=True)
def check_installation() -> None:
if not check_ollama_installed():
typer.echo(
"Error: Ollama is not installed. Please install Ollama to proceed.",
err=True,
)
raise typer.Exit(code=1)
@app.command()
def list() -> None:
"""List all installed Ollama models."""
check_installation()
model_list = models()
if not model_list:
typer.echo("No models installed.")
return
typer.echo("\nInstalled Models:")
typer.echo("-" * 40)
for model in model_list:
typer.echo(f" • {model}")
typer.echo("-" * 40)
typer.echo(f"\nTotal: {len(model_list)} model(s)")
@app.command()
def update(
model: str = typer.Argument(
None,
help="Model name to update (updates all if not provided)",
),
) -> None:
"""Update one or all Ollama models."""
check_installation()
all_models = models()
models_to_update = [model] if model else all_models
if not models_to_update:
typer.echo("No models to update.")
return
typer.echo(f"Updating {len(models_to_update)} model(s)...\n")
update_models(models_to_update)
typer.echo("\nUpdate complete.")
@app.command()
def backup(
backup_path: Path = typer.Option(
BACKUP_PATH,
"--path",
"-p",
help="Directory to save backups (default: ~/Downloads/ollama_model_backups)",
),
model: str = typer.Option(
None,
"--model",
"-m",
help="Specific model to backup (backs up all if not provided)",
),
) -> None:
"""Backup Ollama models to a zip file."""
check_installation()
backup_path = Path(backup_path).expanduser()
typer.echo(f"Backing up models to: {backup_path}")
backup_models(backup_path, model)
typer.echo("\nBackup complete.")
@app.command()
def restore(
backup_path: Path = typer.Argument(
...,
help="Path to backup zip file or directory",
),
) -> None:
"""Restore Ollama models from backup."""
check_installation()
backup_path = Path(backup_path).expanduser()
if not backup_path.exists():
typer.echo(f"Error: Backup path does not exist: {backup_path}", err=True)
raise typer.Exit(code=1)
typer.echo(f"Restoring models from: {backup_path}")
restore_models(backup_path)
typer.echo("\nRestore complete.")
@app.command()
def version() -> None:
"""Show Ollama version."""
check_installation()
typer.echo(f"Ollama Version: {ollama_version()}")
@app.command()
def info() -> None:
"""Show Ollama installation information."""
check_installation()
typer.echo(f"Ollama Version: {ollama_version()}")
typer.echo(f"Models Path: {ollama_models_path()}")
typer.echo(f"Platform: {platform}")
typer.echo(f"Installed Models: {len(models())}")
@app.command()
def check() -> None:
"""Check if Ollama is installed and accessible."""
if check_ollama_installed():
typer.echo("✓ Ollama is installed and accessible")
typer.echo(f" Version: {ollama_version()}")
typer.echo(f" Models: {len(models())}")
else:
typer.echo("✗ Ollama is not installed or not accessible", err=True)
raise typer.Exit(code=1)
def main() -> None:
app()
if __name__ == "__main__":
main()
```