# Directory Structure
```
├── .github
│ └── workflows
│ └── release.yml
├── .gitignore
├── .gitmessage
├── .python-version
├── CHANGELOG.md
├── ollamatools.py
├── pyproject.toml
├── README.md
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
1 | 3.14
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 |
9 | # Virtual environments
10 | .venv
11 | .ruff_cache/
12 | .python-version
```
--------------------------------------------------------------------------------
/.gitmessage:
--------------------------------------------------------------------------------
```
1 | # <type>(<scope>): <subject>
2 | #
3 | # <body>
4 | #
5 | # <footer>
6 |
7 | # Type should be one of the following:
8 | # * feat: A new feature
9 | # * fix: A bug fix
10 | # * docs: Documentation only changes
11 | # * style: Changes that do not affect the meaning of the code
12 | # * refactor: A code change that neither fixes a bug nor adds a feature
13 | # * perf: A code change that improves performance
14 | # * test: Adding missing tests or correcting existing tests
15 | # * build: Changes that affect the build system or external dependencies
16 | # * ci: Changes to our CI configuration files and scripts
17 | # * chore: Other changes that don't modify src or test files
18 | # * revert: Reverts a previous commit
19 |
20 | # Scope is optional and should be the name of the package affected
21 | # (as perceived by the person reading the changelog)
22 |
23 | # Subject line should:
24 | # * use the imperative, present tense: "change" not "changed" nor "changes"
25 | # * not capitalize the first letter
26 | # * not end with a dot (.)
27 |
28 | # Body should include the motivation for the change and contrast this with previous behavior
29 |
30 | # Footer should contain:
31 | # * Information about Breaking Changes
32 | # * Reference GitHub issues that this commit closes
33 |
34 | # Examples:
35 | # feat(parser): add ability to parse arrays
36 | # fix(release): need to depend on latest rxjs and zone.js
37 | # docs(changelog): update changelog to beta.5
38 | # fix(release): need to depend on latest rxjs and zone.js
39 | # feat(lang): add polish language
40 | # perf(core): improve bundle size by removing debug code
41 | # BREAKING CHANGE: The graphiteWidth option has been removed.
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Ollama Tool CLI 🦙
2 |
3 | A modern CLI tool for managing Ollama models - backup, restore, update, and list your models with ease.
4 |
5 | ## Installation
6 |
7 | ### Using pip (recommended)
8 |
9 | ```bash
10 | pip install ollama-tool-cli
11 | ```
12 |
13 | ### Using uv
14 |
15 | ```bash
16 | uv add ollama-tool-cli
17 | ```
18 |
19 | ### From source
20 |
21 | ```bash
22 | git clone https://github.com/arian24b/ollamatools.git
23 | cd ollamatools
24 | uv sync
25 | ```
26 |
27 | ## Requirements
28 |
29 | - Python 3.10 or higher
30 | - Ollama installed and running
31 |
32 | ## Usage
33 |
34 | ### Basic Commands
35 |
36 | ```bash
37 | # Show help
38 | ollama-tool-cli
39 |
40 | # List all installed models
41 | ollama-tool-cli list
42 |
43 | # Update all models
44 | ollama-tool-cli update
45 |
46 | # Update a specific model
47 | ollama-tool-cli update llama3.2
48 |
49 | # Backup all models to default location (~/Downloads/ollama_model_backups)
50 | ollama-tool-cli backup
51 |
52 | # Backup to custom path
53 | ollama-tool-cli backup --path /path/to/backup
54 |
55 | # Backup a specific model
56 | ollama-tool-cli backup --model llama3.2
57 |
58 | # Restore from backup
59 | ollama-tool-cli restore /path/to/backup.zip
60 |
61 | # Show Ollama version
62 | ollama-tool-cli version
63 |
64 | # Show installation information
65 | ollama-tool-cli info
66 |
67 | # Check if Ollama is installed
68 | ollama-tool-cli check
69 | ```
70 |
71 | ### Command Details
72 |
73 | #### `list`
74 | Display all installed Ollama models with their versions.
75 |
76 | #### `update [model]`
77 | Update one or all Ollama models. If no model name is provided, updates all models.
78 |
79 | #### `backup [--path PATH] [--model MODEL]`
80 | Backup Ollama models to zip files. By default backs up all models to `~/Downloads/ollama_model_backups`.
81 |
82 | - `--path, -p`: Custom backup directory path
83 | - `--model, -m`: Backup only a specific model
84 |
85 | #### `restore <path>`
86 | Restore Ollama models from a backup zip file or directory.
87 |
88 | #### `version`
89 | Display the installed Ollama version.
90 |
91 | #### `info`
92 | Show detailed Ollama installation information including version, models path, platform, and number of installed models.
93 |
94 | #### `check`
95 | Verify that Ollama is installed and accessible.
96 |
97 | ## Development
98 |
99 | ### Setup development environment
100 |
101 | ```bash
102 | uv sync
103 | ```
104 |
105 | ### Build the package
106 |
107 | ```bash
108 | uv build
109 | ```
110 |
111 | ## License
112 |
113 | MIT License - see LICENSE file for details.
114 |
115 | ## Contributing
116 |
117 | Contributions are welcome! Please feel free to submit a Pull Request.
118 |
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # CHANGELOG
2 |
3 | <!-- version list -->
4 |
5 | ## v1.1.1 (2025-12-27)
6 |
7 | ### Bug Fixes
8 |
9 | - Update Git configuration to use GitHub actor and modify URLs in pyproject.toml
10 | ([`ebc97c2`](https://github.com/arian24b/ollamatools/commit/ebc97c2d46ad90ea1e5f2eec3da25682a60d25f6))
11 |
12 |
13 | ## v1.1.0 (2025-12-27)
14 |
15 | ### Bug Fixes
16 |
17 | - Change build command to uv build for semantic-release compatibility
18 | ([`0ec8798`](https://github.com/arian24b/ollamatools/commit/0ec87980bd70119ec2a775d9427f15fc4a19832f))
19 |
20 | ### Chores
21 |
22 | - Update version to 1.0.1 and modify build settings
23 | ([`e55f3ff`](https://github.com/arian24b/ollamatools/commit/e55f3ffdd2303d8b8c55b6086b1e78fb380e9964))
24 |
25 | ### Features
26 |
27 | - Rename package to ollama-tool-cli and CLI command to ollama-tool-cli
28 | ([`aa2843f`](https://github.com/arian24b/ollamatools/commit/aa2843fd341753263cd5600051b3108d00dceed7))
29 |
30 |
31 | ## v1.0.0 (2025-12-27)
32 |
33 | - Initial Release
34 |
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | workflow_dispatch:
8 |
9 | permissions:
10 | contents: write
11 | id-token: write
12 |
13 | jobs:
14 | release:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Checkout repository
19 | uses: actions/checkout@v4
20 | with:
21 | fetch-depth: 0
22 |
23 | - name: Set up UV
24 | uses: astral-sh/setup-uv@v7
25 | with:
26 | python-version: '3.14'
27 |
28 | - name: Install dependencies
29 | run: |
30 | uv tool install python-semantic-release
31 |
32 | - name: Configure Git
33 | run: |
34 | git config user.name ${{ github.actor }}
35 | git config user.email ${{ github.actor }}@users.noreply.github.com
36 |
37 | - name: Semantic Release (Version, Tag, GitHub Release)
38 | id: semantic-release
39 | env:
40 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 | run: |
42 | semantic-release version --changelog --tag
43 |
44 | - name: Build Package
45 | if: steps.semantic-release.outputs.version != ''
46 | run: |
47 | uv sync
48 | uv build
49 |
50 | - name: Publish to PyPI
51 | if: steps.semantic-release.outputs.version != ''
52 | uses: pypa/gh-action-pypi-publish@release/v1
53 | with:
54 | password: ${{ secrets.PYPI_API_TOKEN }}
55 |
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [project]
2 | name = "ollama-tool-cli"
3 | version = "1.1.1"
4 | description = "CLI tool for managing Ollama models - backup, restore, update, and list models"
5 | readme = "README.md"
6 | requires-python = ">=3.10"
7 | dependencies = [
8 | "typer>=0.12.0",
9 | ]
10 | authors = [
11 | {name = "Arian Omrani", email = "[email protected]"}
12 | ]
13 | license = "MIT"
14 | classifiers = [
15 | "Development Status :: 3 - Alpha",
16 | "Intended Audience :: Developers",
17 | "Operating System :: OS Independent",
18 | "Programming Language :: Python :: 3",
19 | "Programming Language :: Python :: 3.10",
20 | "Programming Language :: Python :: 3.11",
21 | "Programming Language :: Python :: 3.12",
22 | "Programming Language :: Python :: 3.13",
23 | "Programming Language :: Python :: 3.14",
24 | ]
25 | keywords = ["ollama", "cli", "models", "backup", "restore", "llm"]
26 |
27 | [project.optional-dependencies]
28 | dev = ["ruff", "pytest", "build"]
29 |
30 | [project.scripts]
31 | ollama-tool-cli = "ollamatools:app"
32 |
33 | [project.urls]
34 | Homepage = "https://github.com/arian24b/ollamatools"
35 | Repository = "https://github.com/arian24b/ollamatools"
36 | Issues = "https://github.com/arian24b/ollamatools/issues"
37 |
38 | [build-system]
39 | requires = ["setuptools>=61.0", "wheel"]
40 | build-backend = "setuptools.build_meta"
41 |
42 | [tool.semantic_release]
43 | commit_parser = "conventional"
44 | commit_author = "Arian Omrani <[email protected]>"
45 | branch = "main"
46 | version_toml = ["pyproject.toml:project.version"]
47 | upload_to_vcs_release = false
48 | build_command = "uv build"
49 |
```
--------------------------------------------------------------------------------
/ollamatools.py:
--------------------------------------------------------------------------------
```python
1 | from dataclasses import dataclass
2 | from json import loads
3 | from pathlib import Path
4 | from subprocess import PIPE, Popen
5 | from sys import platform
6 | from zipfile import ZipFile
7 |
8 | import typer
9 |
10 | # Test commit for patch release
11 |
12 |
13 | @dataclass
14 | class CMDOutput:
15 | output_text: str
16 | error_text: str
17 | return_code: int
18 |
19 | def __str__(self) -> str:
20 | return f"Output Text: {self.output_text}\nError Text: {self.error_text}\nReturn Code: {self.return_code}"
21 |
22 |
23 | MODELS_PATH = {
24 | "linux": Path("/usr/share/ollama/.ollama/models").expanduser(),
25 | "macos": Path("~/.ollama/models").expanduser(),
26 | "windows": Path("C:\\Users\\%USERNAME%\\.ollama\\models").expanduser(),
27 | }
28 | BACKUP_PATH = Path("~/Downloads/ollama_model_backups").expanduser()
29 |
30 |
31 | def run_command(command: str | list) -> CMDOutput:
32 | process = Popen(
33 | command,
34 | shell=True,
35 | stdout=PIPE,
36 | stderr=PIPE,
37 | stdin=PIPE,
38 | text=True,
39 | encoding="utf-8",
40 | )
41 |
42 | output_text, error_text = process.communicate()
43 |
44 | return CMDOutput(
45 | output_text=output_text.strip(),
46 | error_text=error_text.strip(),
47 | return_code=process.returncode,
48 | )
49 |
50 |
51 | def check_ollama_installed() -> bool:
52 | result = run_command("which ollama")
53 | return result.return_code == 0
54 |
55 |
56 | def ollama_version() -> str:
57 | result = run_command("ollama --version")
58 | return result.output_text.strip()
59 |
60 |
61 | def create_backup(path_to_backup: list[Path], backup_path: Path) -> None:
62 | with ZipFile(backup_path, "w") as zfile:
63 | for file in path_to_backup:
64 | zfile.write(file)
65 |
66 |
67 | def ollama_models_path() -> Path:
68 | match platform.lower():
69 | case "linux":
70 | return MODELS_PATH["linux"]
71 | case "darwin":
72 | return MODELS_PATH["macos"]
73 | case "win32":
74 | return MODELS_PATH["windows"]
75 | case _:
76 | msg = "Unsupported operating system"
77 | raise OSError(msg)
78 |
79 |
80 | def models() -> list[str]:
81 | result = run_command("ollama list").output_text.strip().split("\n")
82 | return [line.split()[0] for line in result[1:]]
83 |
84 |
85 | def update_models(model_names: list[str]) -> None:
86 | for model_name in model_names:
87 | run_command(f"ollama pull {model_name}")
88 |
89 |
90 | def backup_models(backup_path: Path = BACKUP_PATH, model: str | None = None) -> None:
91 | models_path = ollama_models_path()
92 | backup_path = Path(backup_path)
93 | backup_path.mkdir(parents=True, exist_ok=True)
94 |
95 | for model in models():
96 | model_name, model_version = (
97 | model.split(":") if ":" in model else (model, "latest")
98 | )
99 | model_schema_path = (
100 | models_path
101 | / f"manifests/registry.ollama.ai/library/{model_name}/{model_version}"
102 | )
103 | model_layers = loads(Path(model_schema_path).read_bytes())["layers"]
104 |
105 | digests_path = [
106 | models_path / "blobs" / layer["digest"].replace(":", "-")
107 | for layer in model_layers
108 | ]
109 | digests_path.append(model_schema_path)
110 |
111 | archive_path = backup_path / f"{model_name}-{model_version}.zip"
112 | create_backup(digests_path, archive_path)
113 |
114 |
115 | def restore_models(backup_path: Path) -> None:
116 | backup_path = Path(backup_path).expanduser()
117 | models_path = ollama_models_path()
118 |
119 | with ZipFile(backup_path, "r") as zfile:
120 | zfile.extractall(models_path)
121 |
122 |
123 | app = typer.Typer(no_args_is_help=True)
124 |
125 |
126 | def check_installation() -> None:
127 | if not check_ollama_installed():
128 | typer.echo(
129 | "Error: Ollama is not installed. Please install Ollama to proceed.",
130 | err=True,
131 | )
132 | raise typer.Exit(code=1)
133 |
134 |
135 | @app.command()
136 | def list() -> None:
137 | """List all installed Ollama models."""
138 | check_installation()
139 | model_list = models()
140 |
141 | if not model_list:
142 | typer.echo("No models installed.")
143 | return
144 |
145 | typer.echo("\nInstalled Models:")
146 | typer.echo("-" * 40)
147 | for model in model_list:
148 | typer.echo(f" • {model}")
149 | typer.echo("-" * 40)
150 | typer.echo(f"\nTotal: {len(model_list)} model(s)")
151 |
152 |
153 | @app.command()
154 | def update(
155 | model: str = typer.Argument(
156 | None,
157 | help="Model name to update (updates all if not provided)",
158 | ),
159 | ) -> None:
160 | """Update one or all Ollama models."""
161 | check_installation()
162 |
163 | all_models = models()
164 | models_to_update = [model] if model else all_models
165 |
166 | if not models_to_update:
167 | typer.echo("No models to update.")
168 | return
169 |
170 | typer.echo(f"Updating {len(models_to_update)} model(s)...\n")
171 | update_models(models_to_update)
172 | typer.echo("\nUpdate complete.")
173 |
174 |
175 | @app.command()
176 | def backup(
177 | backup_path: Path = typer.Option(
178 | BACKUP_PATH,
179 | "--path",
180 | "-p",
181 | help="Directory to save backups (default: ~/Downloads/ollama_model_backups)",
182 | ),
183 | model: str = typer.Option(
184 | None,
185 | "--model",
186 | "-m",
187 | help="Specific model to backup (backs up all if not provided)",
188 | ),
189 | ) -> None:
190 | """Backup Ollama models to a zip file."""
191 | check_installation()
192 |
193 | backup_path = Path(backup_path).expanduser()
194 | typer.echo(f"Backing up models to: {backup_path}")
195 | backup_models(backup_path, model)
196 | typer.echo("\nBackup complete.")
197 |
198 |
199 | @app.command()
200 | def restore(
201 | backup_path: Path = typer.Argument(
202 | ...,
203 | help="Path to backup zip file or directory",
204 | ),
205 | ) -> None:
206 | """Restore Ollama models from backup."""
207 | check_installation()
208 |
209 | backup_path = Path(backup_path).expanduser()
210 | if not backup_path.exists():
211 | typer.echo(f"Error: Backup path does not exist: {backup_path}", err=True)
212 | raise typer.Exit(code=1)
213 |
214 | typer.echo(f"Restoring models from: {backup_path}")
215 | restore_models(backup_path)
216 | typer.echo("\nRestore complete.")
217 |
218 |
219 | @app.command()
220 | def version() -> None:
221 | """Show Ollama version."""
222 | check_installation()
223 | typer.echo(f"Ollama Version: {ollama_version()}")
224 |
225 |
226 | @app.command()
227 | def info() -> None:
228 | """Show Ollama installation information."""
229 | check_installation()
230 | typer.echo(f"Ollama Version: {ollama_version()}")
231 | typer.echo(f"Models Path: {ollama_models_path()}")
232 | typer.echo(f"Platform: {platform}")
233 | typer.echo(f"Installed Models: {len(models())}")
234 |
235 |
236 | @app.command()
237 | def check() -> None:
238 | """Check if Ollama is installed and accessible."""
239 | if check_ollama_installed():
240 | typer.echo("✓ Ollama is installed and accessible")
241 | typer.echo(f" Version: {ollama_version()}")
242 | typer.echo(f" Models: {len(models())}")
243 | else:
244 | typer.echo("✗ Ollama is not installed or not accessible", err=True)
245 | raise typer.Exit(code=1)
246 |
247 |
248 | def main() -> None:
249 | app()
250 |
251 |
252 | if __name__ == "__main__":
253 | main()
254 |
```