#
tokens: 3816/50000 8/8 files
lines: off (toggle) GitHub
raw markdown copy
# 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()

```