mdify-cli 2.8.0__tar.gz → 2.9.5__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdify-cli
3
- Version: 2.8.0
3
+ Version: 2.9.5
4
4
  Summary: Convert PDFs and document images into structured Markdown for LLM workflows
5
5
  Author: tiroq
6
6
  License-Expression: MIT
@@ -42,7 +42,10 @@ A lightweight CLI for converting documents to Markdown. The CLI is fast to insta
42
42
  ## Requirements
43
43
 
44
44
  - **Python 3.8+**
45
- - **Docker** or **Podman** (for document conversion)
45
+ - **Docker**, **Podman**, or native macOS container tools (for document conversion)
46
+ - On macOS: Supports Apple Container (macOS 26+), OrbStack, Colima, Podman, or Docker Desktop
47
+ - On Linux: Docker or Podman
48
+ - Auto-detects available tools
46
49
 
47
50
  ## Installation
48
51
 
@@ -56,6 +59,13 @@ pipx install mdify-cli
56
59
 
57
60
  Restart your terminal after installation.
58
61
 
62
+ For containerized document conversion, install one of these (or use Docker Desktop):
63
+ - **Apple Container** (macOS 26+): Download from https://github.com/apple/container/releases
64
+ - **OrbStack** (recommended): `brew install orbstack`
65
+ - **Colima**: `brew install colima && colima start`
66
+ - **Podman**: `brew install podman && podman machine init && podman machine start`
67
+ - **Docker Desktop**: Available at https://www.docker.com/products/docker-desktop
68
+
59
69
  ### Linux
60
70
 
61
71
  ```bash
@@ -142,13 +152,50 @@ The first conversion takes longer (~30-60s) as the container loads ML models int
142
152
  | `-m, --mask` | ⚠️ **Deprecated**: PII masking not supported in current version |
143
153
  | `--gpu` | Use GPU-accelerated container (requires NVIDIA GPU and nvidia-container-toolkit) |
144
154
  | `--port PORT` | Container port (default: 5001) |
145
- | `--runtime RUNTIME` | Container runtime: docker or podman (auto-detected) |
155
+ | `--runtime RUNTIME` | Container runtime: docker, podman, orbstack, colima, or container (auto-detected) |
146
156
  | `--image IMAGE` | Custom container image (default: ghcr.io/docling-project/docling-serve-cpu:main) |
147
157
  | `--pull POLICY` | Image pull policy: always, missing, never (default: missing) |
148
158
  | `--check-update` | Check for available updates and exit |
149
159
  | `--version` | Show version and exit |
150
160
 
151
- ### Flat Mode
161
+ ### Container Runtime Selection
162
+
163
+ mdify automatically detects and uses the best available container runtime. The detection order differs by platform:
164
+
165
+ **macOS (recommended):**
166
+ 1. Apple Container (native, macOS 26+ required)
167
+ 2. OrbStack (lightweight, fast)
168
+ 3. Colima (open-source alternative)
169
+ 4. Podman (via Podman machine)
170
+ 5. Docker Desktop (full Docker)
171
+
172
+ **Linux:**
173
+ 1. Docker
174
+ 2. Podman
175
+
176
+ **Override runtime:**
177
+ Use the `MDIFY_CONTAINER_RUNTIME` environment variable to force a specific runtime:
178
+
179
+ ```bash
180
+ export MDIFY_CONTAINER_RUNTIME=orbstack
181
+ mdify document.pdf
182
+ ```
183
+
184
+ Or inline:
185
+ ```bash
186
+ MDIFY_CONTAINER_RUNTIME=colima mdify document.pdf
187
+ ```
188
+
189
+ **Supported values:** `docker`, `podman`, `orbstack`, `colima`, `container`
190
+
191
+ If the selected runtime is installed but not running, mdify will display a helpful warning:
192
+ ```
193
+ Warning: Found container runtime(s) but daemon is not running:
194
+ - orbstack (/opt/homebrew/bin/orbstack)
195
+
196
+ Please start one of these tools before running mdify.
197
+ macOS tip: Start OrbStack, Colima, or Podman Desktop application
198
+ ```
152
199
 
153
200
  With `--flat`, all output files are placed directly in the output directory. Directory paths are incorporated into filenames to prevent collisions:
154
201
 
@@ -11,7 +11,10 @@ A lightweight CLI for converting documents to Markdown. The CLI is fast to insta
11
11
  ## Requirements
12
12
 
13
13
  - **Python 3.8+**
14
- - **Docker** or **Podman** (for document conversion)
14
+ - **Docker**, **Podman**, or native macOS container tools (for document conversion)
15
+ - On macOS: Supports Apple Container (macOS 26+), OrbStack, Colima, Podman, or Docker Desktop
16
+ - On Linux: Docker or Podman
17
+ - Auto-detects available tools
15
18
 
16
19
  ## Installation
17
20
 
@@ -25,6 +28,13 @@ pipx install mdify-cli
25
28
 
26
29
  Restart your terminal after installation.
27
30
 
31
+ For containerized document conversion, install one of these (or use Docker Desktop):
32
+ - **Apple Container** (macOS 26+): Download from https://github.com/apple/container/releases
33
+ - **OrbStack** (recommended): `brew install orbstack`
34
+ - **Colima**: `brew install colima && colima start`
35
+ - **Podman**: `brew install podman && podman machine init && podman machine start`
36
+ - **Docker Desktop**: Available at https://www.docker.com/products/docker-desktop
37
+
28
38
  ### Linux
29
39
 
30
40
  ```bash
@@ -111,13 +121,50 @@ The first conversion takes longer (~30-60s) as the container loads ML models int
111
121
  | `-m, --mask` | ⚠️ **Deprecated**: PII masking not supported in current version |
112
122
  | `--gpu` | Use GPU-accelerated container (requires NVIDIA GPU and nvidia-container-toolkit) |
113
123
  | `--port PORT` | Container port (default: 5001) |
114
- | `--runtime RUNTIME` | Container runtime: docker or podman (auto-detected) |
124
+ | `--runtime RUNTIME` | Container runtime: docker, podman, orbstack, colima, or container (auto-detected) |
115
125
  | `--image IMAGE` | Custom container image (default: ghcr.io/docling-project/docling-serve-cpu:main) |
116
126
  | `--pull POLICY` | Image pull policy: always, missing, never (default: missing) |
117
127
  | `--check-update` | Check for available updates and exit |
118
128
  | `--version` | Show version and exit |
119
129
 
120
- ### Flat Mode
130
+ ### Container Runtime Selection
131
+
132
+ mdify automatically detects and uses the best available container runtime. The detection order differs by platform:
133
+
134
+ **macOS (recommended):**
135
+ 1. Apple Container (native, macOS 26+ required)
136
+ 2. OrbStack (lightweight, fast)
137
+ 3. Colima (open-source alternative)
138
+ 4. Podman (via Podman machine)
139
+ 5. Docker Desktop (full Docker)
140
+
141
+ **Linux:**
142
+ 1. Docker
143
+ 2. Podman
144
+
145
+ **Override runtime:**
146
+ Use the `MDIFY_CONTAINER_RUNTIME` environment variable to force a specific runtime:
147
+
148
+ ```bash
149
+ export MDIFY_CONTAINER_RUNTIME=orbstack
150
+ mdify document.pdf
151
+ ```
152
+
153
+ Or inline:
154
+ ```bash
155
+ MDIFY_CONTAINER_RUNTIME=colima mdify document.pdf
156
+ ```
157
+
158
+ **Supported values:** `docker`, `podman`, `orbstack`, `colima`, `container`
159
+
160
+ If the selected runtime is installed but not running, mdify will display a helpful warning:
161
+ ```
162
+ Warning: Found container runtime(s) but daemon is not running:
163
+ - orbstack (/opt/homebrew/bin/orbstack)
164
+
165
+ Please start one of these tools before running mdify.
166
+ macOS tip: Start OrbStack, Colima, or Podman Desktop application
167
+ ```
121
168
 
122
169
  With `--flat`, all output files are placed directly in the output directory. Directory paths are incorporated into filenames to prevent collisions:
123
170
 
@@ -1,3 +1,3 @@
1
1
  """mdify - Convert documents to Markdown via Docling container."""
2
2
 
3
- __version__ = "2.8.0"
3
+ __version__ = "2.9.5"
@@ -10,6 +10,7 @@ is lightweight and has no ML dependencies.
10
10
  import argparse
11
11
  import json
12
12
  import os
13
+ import platform
13
14
  import shutil
14
15
  import subprocess
15
16
  import sys
@@ -33,7 +34,9 @@ CHECK_INTERVAL_SECONDS = 86400 # 24 hours
33
34
  # Container configuration
34
35
  DEFAULT_IMAGE = "ghcr.io/docling-project/docling-serve-cpu:main"
35
36
  GPU_IMAGE = "ghcr.io/docling-project/docling-serve-cu126:main"
36
- SUPPORTED_RUNTIMES = ("docker", "podman")
37
+ SUPPORTED_RUNTIMES = ("docker", "podman", "orbstack", "colima", "container")
38
+ MACOS_RUNTIMES_PRIORITY = ("container", "orbstack", "colima", "podman", "docker")
39
+ OTHER_RUNTIMES_PRIORITY = ("docker", "podman")
37
40
 
38
41
 
39
42
  # =============================================================================
@@ -151,34 +154,117 @@ def check_for_update(force: bool = False) -> None:
151
154
  # =============================================================================
152
155
 
153
156
 
154
- def detect_runtime(preferred: str, explicit: bool = True) -> Optional[str]:
157
+ def is_daemon_running(runtime: str) -> bool:
158
+ """
159
+ Check if a container runtime daemon is running.
160
+
161
+ Args:
162
+ runtime: Path to container runtime executable
163
+
164
+ Returns:
165
+ True if daemon is running and responsive, False otherwise.
166
+ """
167
+ try:
168
+ runtime_name = os.path.basename(runtime)
169
+
170
+ # Apple Container uses 'container system status' to check daemon
171
+ if runtime_name == "container":
172
+ result = subprocess.run(
173
+ [runtime, "system", "status"],
174
+ capture_output=True,
175
+ timeout=5,
176
+ check=False,
177
+ )
178
+ return result.returncode == 0
179
+
180
+ # Other runtimes use --version check
181
+ result = subprocess.run(
182
+ [runtime, "--version"],
183
+ capture_output=True,
184
+ timeout=5,
185
+ check=False,
186
+ )
187
+ return result.returncode == 0
188
+ except (OSError, subprocess.TimeoutExpired):
189
+ return False
190
+
191
+
192
+ def detect_runtime(preferred: Optional[str] = None, explicit: bool = True) -> Optional[str]:
155
193
  """
156
194
  Detect available container runtime.
157
195
 
196
+ First checks MDIFY_CONTAINER_RUNTIME environment variable for explicit override.
197
+ On macOS, tries native tools first (OrbStack → Colima → Podman → Docker).
198
+ On other platforms, tries Docker → Podman.
199
+
158
200
  Args:
159
- preferred: Preferred runtime ('docker' or 'podman')
160
- explicit: If True, warn when falling back to alternative.
161
- If False, silently use alternative without warning.
162
- Note: This only controls warning emission; selection order
163
- is always preferred → alternative regardless of this flag.
201
+ preferred: Preferred runtime name override (deprecated, use MDIFY_CONTAINER_RUNTIME)
202
+ explicit: If True, print info about detection/fallback choices.
164
203
 
165
204
  Returns:
166
205
  Path to runtime executable, or None if not found.
167
206
  """
168
- # Try preferred runtime first
169
- runtime_path = shutil.which(preferred)
170
- if runtime_path:
171
- return runtime_path
172
-
173
- # Try alternative
174
- alternative = "podman" if preferred == "docker" else "docker"
175
- runtime_path = shutil.which(alternative)
176
- if runtime_path:
207
+ # Check for explicit environment variable override
208
+ env_runtime = os.environ.get("MDIFY_CONTAINER_RUNTIME", "").strip().lower()
209
+ if env_runtime:
210
+ if env_runtime not in SUPPORTED_RUNTIMES:
211
+ print(
212
+ f"Warning: MDIFY_CONTAINER_RUNTIME='{env_runtime}' is not supported. "
213
+ f"Supported: {', '.join(SUPPORTED_RUNTIMES)}",
214
+ file=sys.stderr,
215
+ )
216
+ else:
217
+ runtime_path = shutil.which(env_runtime)
218
+ if runtime_path:
219
+ if explicit:
220
+ print(f"Using runtime from MDIFY_CONTAINER_RUNTIME: {env_runtime}")
221
+ return runtime_path
222
+ else:
223
+ print(
224
+ f"Warning: MDIFY_CONTAINER_RUNTIME='{env_runtime}' specified but not found in PATH",
225
+ file=sys.stderr,
226
+ )
227
+
228
+ # Determine runtime priority based on OS
229
+ is_macos = platform.system() == "Darwin"
230
+ if is_macos:
231
+ runtime_priority = MACOS_RUNTIMES_PRIORITY
177
232
  if explicit:
233
+ print(f"Detected macOS: checking for native container tools...")
234
+ else:
235
+ runtime_priority = OTHER_RUNTIMES_PRIORITY
236
+
237
+ # Try each runtime in priority order
238
+ found_but_not_running = []
239
+ for runtime_name in runtime_priority:
240
+ runtime_path = shutil.which(runtime_name)
241
+ if runtime_path:
242
+ # Check if daemon is running
243
+ if is_daemon_running(runtime_path):
244
+ if explicit:
245
+ print(f"Using container runtime: {runtime_name}")
246
+ return runtime_path
247
+ else:
248
+ found_but_not_running.append((runtime_name, runtime_path))
249
+
250
+ # If we found tools but none are running, warn and ask user to start one
251
+ if found_but_not_running:
252
+ print(
253
+ f"\nWarning: Found container runtime(s) but daemon is not running:",
254
+ file=sys.stderr,
255
+ )
256
+ for runtime_name, runtime_path in found_but_not_running:
257
+ print(f" - {runtime_name} ({runtime_path})", file=sys.stderr)
258
+ print(
259
+ "\nPlease start one of these tools before running mdify.",
260
+ file=sys.stderr,
261
+ )
262
+ if is_macos:
178
263
  print(
179
- f"Warning: {preferred} not found, using {alternative}", file=sys.stderr
264
+ " macOS tip: Start OrbStack, Colima, or Podman Desktop application",
265
+ file=sys.stderr,
180
266
  )
181
- return runtime_path
267
+ return None
182
268
 
183
269
  return None
184
270
 
@@ -195,6 +281,27 @@ def check_image_exists(runtime: str, image: str) -> bool:
195
281
  True if image exists locally.
196
282
  """
197
283
  try:
284
+ runtime_name = os.path.basename(runtime)
285
+
286
+ # Apple Container uses 'image list' command (two words)
287
+ if runtime_name == "container":
288
+ result = subprocess.run(
289
+ [runtime, "image", "list", "--format", "json"],
290
+ capture_output=True,
291
+ check=False,
292
+ )
293
+ if result.returncode == 0 and result.stdout:
294
+ try:
295
+ images = json.loads(result.stdout.decode())
296
+ # Check if image exists in the list
297
+ for img in images:
298
+ if img.get("name") == image or image in img.get("repoTags", []):
299
+ return True
300
+ except json.JSONDecodeError:
301
+ pass
302
+ return False
303
+
304
+ # Docker/Podman/OrbStack/Colima use standard 'image inspect'
198
305
  result = subprocess.run(
199
306
  [runtime, "image", "inspect", image],
200
307
  capture_output=True,
@@ -221,6 +328,18 @@ def pull_image(runtime: str, image: str, quiet: bool = False) -> bool:
221
328
  print(f"Pulling image: {image}")
222
329
 
223
330
  try:
331
+ runtime_name = os.path.basename(runtime)
332
+
333
+ # Apple Container uses 'image pull' command (two words)
334
+ if runtime_name == "container":
335
+ result = subprocess.run(
336
+ [runtime, "image", "pull", image],
337
+ capture_output=quiet,
338
+ check=False,
339
+ )
340
+ return result.returncode == 0
341
+
342
+ # Docker/Podman/OrbStack/Colima use standard 'pull'
224
343
  result = subprocess.run(
225
344
  [runtime, "pull", image],
226
345
  capture_output=quiet,
@@ -302,7 +421,7 @@ def get_free_space(path: str) -> int:
302
421
 
303
422
  def get_storage_root(runtime: str) -> Optional[str]:
304
423
  """
305
- Get the storage root directory for Docker or Podman.
424
+ Get the storage root directory for Docker, Podman, OrbStack, or Colima.
306
425
 
307
426
  Args:
308
427
  runtime: Path to container runtime executable
@@ -331,6 +450,18 @@ def get_storage_root(runtime: str) -> Optional[str]:
331
450
  if result.stdout:
332
451
  info = json.loads(result.stdout.decode())
333
452
  return info.get("store", {}).get("graphRoot")
453
+ elif runtime_name == "orbstack":
454
+ # OrbStack stores containers in ~/.orbstack
455
+ home = os.path.expanduser("~")
456
+ return os.path.join(home, ".orbstack")
457
+ elif runtime_name == "colima":
458
+ # Colima stores containers in ~/.colima
459
+ home = os.path.expanduser("~")
460
+ return os.path.join(home, ".colima")
461
+ elif runtime_name == "container":
462
+ # Apple Container stores data in Application Support
463
+ home = os.path.expanduser("~")
464
+ return os.path.join(home, "Library", "Application Support", "com.apple.container")
334
465
  return None
335
466
  except (OSError, json.JSONDecodeError):
336
467
  return None
@@ -639,6 +770,7 @@ Examples:
639
770
 
640
771
  def main() -> int:
641
772
  """Main entry point for the CLI."""
773
+ print(f"mdify v{__version__}", file=sys.stderr)
642
774
  args = parse_args()
643
775
 
644
776
  # Handle --check-update flag
@@ -660,15 +792,14 @@ def main() -> int:
660
792
  return 1
661
793
 
662
794
  # Detect container runtime
663
- preferred = args.runtime if args.runtime else "docker"
795
+ # If --runtime is specified, treat as explicit user choice
664
796
  explicit = args.runtime is not None
665
- runtime = detect_runtime(preferred, explicit=explicit)
797
+ runtime = detect_runtime(preferred=args.runtime, explicit=explicit)
666
798
  if runtime is None:
667
799
  print(
668
800
  f"Error: Container runtime not found ({', '.join(SUPPORTED_RUNTIMES)})",
669
801
  file=sys.stderr,
670
802
  )
671
- print("Please install Docker or Podman to use mdify.", file=sys.stderr)
672
803
  return 2
673
804
 
674
805
  # Handle image pull policy
@@ -752,7 +883,8 @@ def main() -> int:
752
883
  return 1
753
884
  elif args.pull == "never" and not image_exists:
754
885
  print(f"Error: Image not found locally: {image}", file=sys.stderr)
755
- print(f"Run with --pull=missing or pull manually: {preferred} pull {image}")
886
+ runtime_name = os.path.basename(runtime)
887
+ print(f"Run with --pull=missing or pull manually: {runtime_name} pull {image}")
756
888
  return 1
757
889
 
758
890
  # Resolve paths (use absolute() as fallback if resolve() fails due to permissions)
@@ -786,6 +918,8 @@ def main() -> int:
786
918
 
787
919
  if not args.quiet:
788
920
  print(f"Found {total_files} file(s) to convert ({format_size(total_size)})")
921
+ print(f"Source: {input_path.resolve()}")
922
+ print(f"Output: {output_dir.resolve()}")
789
923
  print(f"Using runtime: {runtime}")
790
924
  print(f"Using image: {image}")
791
925
  print()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdify-cli
3
- Version: 2.8.0
3
+ Version: 2.9.5
4
4
  Summary: Convert PDFs and document images into structured Markdown for LLM workflows
5
5
  Author: tiroq
6
6
  License-Expression: MIT
@@ -42,7 +42,10 @@ A lightweight CLI for converting documents to Markdown. The CLI is fast to insta
42
42
  ## Requirements
43
43
 
44
44
  - **Python 3.8+**
45
- - **Docker** or **Podman** (for document conversion)
45
+ - **Docker**, **Podman**, or native macOS container tools (for document conversion)
46
+ - On macOS: Supports Apple Container (macOS 26+), OrbStack, Colima, Podman, or Docker Desktop
47
+ - On Linux: Docker or Podman
48
+ - Auto-detects available tools
46
49
 
47
50
  ## Installation
48
51
 
@@ -56,6 +59,13 @@ pipx install mdify-cli
56
59
 
57
60
  Restart your terminal after installation.
58
61
 
62
+ For containerized document conversion, install one of these (or use Docker Desktop):
63
+ - **Apple Container** (macOS 26+): Download from https://github.com/apple/container/releases
64
+ - **OrbStack** (recommended): `brew install orbstack`
65
+ - **Colima**: `brew install colima && colima start`
66
+ - **Podman**: `brew install podman && podman machine init && podman machine start`
67
+ - **Docker Desktop**: Available at https://www.docker.com/products/docker-desktop
68
+
59
69
  ### Linux
60
70
 
61
71
  ```bash
@@ -142,13 +152,50 @@ The first conversion takes longer (~30-60s) as the container loads ML models int
142
152
  | `-m, --mask` | ⚠️ **Deprecated**: PII masking not supported in current version |
143
153
  | `--gpu` | Use GPU-accelerated container (requires NVIDIA GPU and nvidia-container-toolkit) |
144
154
  | `--port PORT` | Container port (default: 5001) |
145
- | `--runtime RUNTIME` | Container runtime: docker or podman (auto-detected) |
155
+ | `--runtime RUNTIME` | Container runtime: docker, podman, orbstack, colima, or container (auto-detected) |
146
156
  | `--image IMAGE` | Custom container image (default: ghcr.io/docling-project/docling-serve-cpu:main) |
147
157
  | `--pull POLICY` | Image pull policy: always, missing, never (default: missing) |
148
158
  | `--check-update` | Check for available updates and exit |
149
159
  | `--version` | Show version and exit |
150
160
 
151
- ### Flat Mode
161
+ ### Container Runtime Selection
162
+
163
+ mdify automatically detects and uses the best available container runtime. The detection order differs by platform:
164
+
165
+ **macOS (recommended):**
166
+ 1. Apple Container (native, macOS 26+ required)
167
+ 2. OrbStack (lightweight, fast)
168
+ 3. Colima (open-source alternative)
169
+ 4. Podman (via Podman machine)
170
+ 5. Docker Desktop (full Docker)
171
+
172
+ **Linux:**
173
+ 1. Docker
174
+ 2. Podman
175
+
176
+ **Override runtime:**
177
+ Use the `MDIFY_CONTAINER_RUNTIME` environment variable to force a specific runtime:
178
+
179
+ ```bash
180
+ export MDIFY_CONTAINER_RUNTIME=orbstack
181
+ mdify document.pdf
182
+ ```
183
+
184
+ Or inline:
185
+ ```bash
186
+ MDIFY_CONTAINER_RUNTIME=colima mdify document.pdf
187
+ ```
188
+
189
+ **Supported values:** `docker`, `podman`, `orbstack`, `colima`, `container`
190
+
191
+ If the selected runtime is installed but not running, mdify will display a helpful warning:
192
+ ```
193
+ Warning: Found container runtime(s) but daemon is not running:
194
+ - orbstack (/opt/homebrew/bin/orbstack)
195
+
196
+ Please start one of these tools before running mdify.
197
+ macOS tip: Start OrbStack, Colima, or Podman Desktop application
198
+ ```
152
199
 
153
200
  With `--flat`, all output files are placed directly in the output directory. Directory paths are incorporated into filenames to prevent collisions:
154
201
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mdify-cli"
3
- version = "2.8.0"
3
+ version = "2.9.5"
4
4
  description = "Convert PDFs and document images into structured Markdown for LLM workflows"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.8"
@@ -1,6 +1,7 @@
1
1
  """Tests for mdify CLI runtime detection."""
2
2
 
3
3
  import json
4
+ import subprocess
4
5
  import sys
5
6
  from pathlib import Path
6
7
  from unittest.mock import patch, Mock
@@ -9,6 +10,7 @@ from urllib.error import URLError
9
10
 
10
11
  from mdify.cli import (
11
12
  detect_runtime,
13
+ is_daemon_running,
12
14
  parse_args,
13
15
  format_size,
14
16
  format_duration,
@@ -45,44 +47,47 @@ class TestDetectRuntime:
45
47
 
46
48
  def test_auto_docker_exists(self):
47
49
  with patch("mdify.cli.shutil.which") as mock_which:
48
- mock_which.side_effect = (
49
- lambda x: "/usr/bin/docker" if x == "docker" else None
50
- )
51
- result = detect_runtime("docker", explicit=False)
52
- assert result == "/usr/bin/docker"
50
+ with patch("mdify.cli.is_daemon_running", return_value=True):
51
+ mock_which.side_effect = (
52
+ lambda x: "/usr/bin/docker" if x == "docker" else None
53
+ )
54
+ result = detect_runtime(explicit=False)
55
+ assert result == "/usr/bin/docker"
53
56
 
54
57
  def test_auto_only_podman_exists(self, capsys):
55
58
  with patch("mdify.cli.shutil.which") as mock_which:
56
- mock_which.side_effect = (
57
- lambda x: "/usr/bin/podman" if x == "podman" else None
58
- )
59
- result = detect_runtime("docker", explicit=False)
60
- assert result == "/usr/bin/podman"
61
- captured = capsys.readouterr()
62
- assert captured.err == ""
59
+ with patch("mdify.cli.is_daemon_running", return_value=True):
60
+ mock_which.side_effect = (
61
+ lambda x: "/usr/bin/podman" if x == "podman" else None
62
+ )
63
+ result = detect_runtime(explicit=False)
64
+ assert result == "/usr/bin/podman"
65
+ captured = capsys.readouterr()
66
+ assert captured.err == ""
63
67
 
64
68
  def test_auto_neither_exists(self):
65
69
  with patch("mdify.cli.shutil.which", return_value=None):
66
- result = detect_runtime("docker", explicit=False)
70
+ result = detect_runtime(explicit=False)
67
71
  assert result is None
68
72
 
69
73
  def test_explicit_docker_exists(self):
70
74
  with patch("mdify.cli.shutil.which") as mock_which:
71
- mock_which.side_effect = (
72
- lambda x: "/usr/bin/docker" if x == "docker" else None
73
- )
74
- result = detect_runtime("docker", explicit=True)
75
- assert result == "/usr/bin/docker"
75
+ with patch("mdify.cli.is_daemon_running", return_value=True):
76
+ mock_which.side_effect = (
77
+ lambda x: "/usr/bin/docker" if x == "docker" else None
78
+ )
79
+ result = detect_runtime("docker", explicit=True)
80
+ assert result == "/usr/bin/docker"
76
81
 
77
82
  def test_explicit_docker_fallback_to_podman(self, capsys):
78
83
  with patch("mdify.cli.shutil.which") as mock_which:
79
- mock_which.side_effect = (
80
- lambda x: "/usr/bin/podman" if x == "podman" else None
81
- )
82
- result = detect_runtime("docker", explicit=True)
83
- assert result == "/usr/bin/podman"
84
- captured = capsys.readouterr()
85
- assert "Warning: docker not found, using podman" in captured.err
84
+ with patch("mdify.cli.is_daemon_running", return_value=True):
85
+ mock_which.side_effect = (
86
+ lambda x: "/usr/bin/podman" if x == "podman" else None
87
+ )
88
+ result = detect_runtime("docker", explicit=True)
89
+ assert result == "/usr/bin/podman"
90
+ # With new macOS priority-based detection, priority order is used
86
91
 
87
92
  def test_explicit_docker_neither_exists(self):
88
93
  with patch("mdify.cli.shutil.which", return_value=None):
@@ -91,27 +96,166 @@ class TestDetectRuntime:
91
96
 
92
97
  def test_explicit_podman_exists(self):
93
98
  with patch("mdify.cli.shutil.which") as mock_which:
94
- mock_which.side_effect = (
95
- lambda x: "/usr/bin/podman" if x == "podman" else None
96
- )
97
- result = detect_runtime("podman", explicit=True)
98
- assert result == "/usr/bin/podman"
99
+ with patch("mdify.cli.is_daemon_running", return_value=True):
100
+ mock_which.side_effect = (
101
+ lambda x: "/usr/bin/podman" if x == "podman" else None
102
+ )
103
+ result = detect_runtime("podman", explicit=True)
104
+ assert result == "/usr/bin/podman"
99
105
 
100
106
  def test_explicit_podman_fallback_to_docker(self, capsys):
101
107
  with patch("mdify.cli.shutil.which") as mock_which:
102
- mock_which.side_effect = (
103
- lambda x: "/usr/bin/docker" if x == "docker" else None
104
- )
105
- result = detect_runtime("podman", explicit=True)
106
- assert result == "/usr/bin/docker"
107
- captured = capsys.readouterr()
108
- assert "Warning: podman not found, using docker" in captured.err
108
+ with patch("mdify.cli.is_daemon_running", return_value=True):
109
+ mock_which.side_effect = (
110
+ lambda x: "/usr/bin/docker" if x == "docker" else None
111
+ )
112
+ result = detect_runtime("podman", explicit=True)
113
+ assert result == "/usr/bin/docker"
114
+ # With new macOS priority-based detection, priority order is used
109
115
 
110
116
  def test_explicit_podman_neither_exists(self):
111
117
  with patch("mdify.cli.shutil.which", return_value=None):
112
118
  result = detect_runtime("podman", explicit=True)
113
119
  assert result is None
114
120
 
121
+ # Tests for new macOS native tool support
122
+ def test_env_var_override_orbstack(self, monkeypatch):
123
+ """Test MDIFY_CONTAINER_RUNTIME env var overrides detection."""
124
+ monkeypatch.setenv("MDIFY_CONTAINER_RUNTIME", "orbstack")
125
+ with patch("mdify.cli.shutil.which") as mock_which:
126
+ mock_which.return_value = "/usr/local/bin/orbstack"
127
+ result = detect_runtime(explicit=False)
128
+ assert result == "/usr/local/bin/orbstack"
129
+
130
+ def test_env_var_override_colima(self, monkeypatch):
131
+ """Test MDIFY_CONTAINER_RUNTIME env var works for colima."""
132
+ monkeypatch.setenv("MDIFY_CONTAINER_RUNTIME", "colima")
133
+ with patch("mdify.cli.shutil.which") as mock_which:
134
+ mock_which.return_value = "/usr/local/bin/colima"
135
+ result = detect_runtime(explicit=False)
136
+ assert result == "/usr/local/bin/colima"
137
+
138
+ def test_env_var_not_found_in_path(self, monkeypatch, capsys):
139
+ """Test MDIFY_CONTAINER_RUNTIME env var when tool not in PATH."""
140
+ monkeypatch.setenv("MDIFY_CONTAINER_RUNTIME", "orbstack")
141
+ with patch("mdify.cli.shutil.which", return_value=None):
142
+ result = detect_runtime(explicit=False)
143
+ assert result is None
144
+ captured = capsys.readouterr()
145
+ assert "MDIFY_CONTAINER_RUNTIME='orbstack' specified but not found in PATH" in captured.err
146
+
147
+ def test_env_var_invalid_name(self, monkeypatch, capsys):
148
+ """Test MDIFY_CONTAINER_RUNTIME with invalid runtime name."""
149
+ monkeypatch.setenv("MDIFY_CONTAINER_RUNTIME", "invalid")
150
+ with patch("mdify.cli.shutil.which", return_value=None):
151
+ result = detect_runtime(explicit=False)
152
+ assert result is None
153
+ captured = capsys.readouterr()
154
+ assert "MDIFY_CONTAINER_RUNTIME='invalid' is not supported" in captured.err
155
+
156
+ def test_macos_priority_orbstack_first(self, monkeypatch):
157
+ """Test macOS prefers OrbStack over other tools."""
158
+ monkeypatch.setenv("MDIFY_CONTAINER_RUNTIME", "") # Clear env override
159
+ with patch("mdify.cli.platform.system", return_value="Darwin"):
160
+ with patch("mdify.cli.shutil.which") as mock_which:
161
+ with patch("mdify.cli.is_daemon_running") as mock_running:
162
+ # Setup: orbstack exists and running
163
+ mock_which.side_effect = lambda x: f"/usr/local/bin/{x}" if x == "orbstack" else None
164
+ mock_running.return_value = True
165
+ result = detect_runtime(explicit=False)
166
+ assert result == "/usr/local/bin/orbstack"
167
+
168
+ def test_macos_fallback_colima_when_orbstack_not_running(self, monkeypatch):
169
+ """Test macOS falls back to Colima if OrbStack not running."""
170
+ monkeypatch.setenv("MDIFY_CONTAINER_RUNTIME", "")
171
+ with patch("mdify.cli.platform.system", return_value="Darwin"):
172
+ with patch("mdify.cli.shutil.which") as mock_which:
173
+ with patch("mdify.cli.is_daemon_running") as mock_running:
174
+ # Setup: orbstack exists but not running, colima exists and running
175
+ def which_side_effect(x):
176
+ if x in ("orbstack", "colima"):
177
+ return f"/usr/local/bin/{x}"
178
+ return None
179
+
180
+ def running_side_effect(path):
181
+ return "colima" in path
182
+
183
+ mock_which.side_effect = which_side_effect
184
+ mock_running.side_effect = running_side_effect
185
+ result = detect_runtime(explicit=False)
186
+ assert result == "/usr/local/bin/colima"
187
+
188
+ def test_non_macos_priority_docker_first(self, monkeypatch):
189
+ """Test non-macOS prefers Docker over other tools."""
190
+ monkeypatch.setenv("MDIFY_CONTAINER_RUNTIME", "")
191
+ with patch("mdify.cli.platform.system", return_value="Linux"):
192
+ with patch("mdify.cli.shutil.which") as mock_which:
193
+ with patch("mdify.cli.is_daemon_running") as mock_running:
194
+ # Setup: docker and podman exist, docker running
195
+ def which_side_effect(x):
196
+ if x in ("docker", "podman"):
197
+ return f"/usr/bin/{x}"
198
+ return None
199
+
200
+ def running_side_effect(path):
201
+ return "docker" in path
202
+
203
+ mock_which.side_effect = which_side_effect
204
+ mock_running.side_effect = running_side_effect
205
+ result = detect_runtime(explicit=False)
206
+ assert result == "/usr/bin/docker"
207
+
208
+ def test_macos_priority_apple_container_first(self, monkeypatch):
209
+ """Test macOS prefers Apple Container over other tools."""
210
+ monkeypatch.setenv("MDIFY_CONTAINER_RUNTIME", "")
211
+ with patch("mdify.cli.platform.system", return_value="Darwin"):
212
+ with patch("mdify.cli.shutil.which") as mock_which:
213
+ with patch("mdify.cli.is_daemon_running") as mock_running:
214
+ # Setup: container exists and running
215
+ mock_which.side_effect = lambda x: f"/usr/local/bin/{x}" if x == "container" else None
216
+ mock_running.return_value = True
217
+ result = detect_runtime(explicit=False)
218
+ assert result == "/usr/local/bin/container"
219
+
220
+ def test_macos_fallback_orbstack_when_container_not_running(self, monkeypatch):
221
+ """Test macOS falls back to OrbStack if Apple Container not running."""
222
+ monkeypatch.setenv("MDIFY_CONTAINER_RUNTIME", "")
223
+ with patch("mdify.cli.platform.system", return_value="Darwin"):
224
+ with patch("mdify.cli.shutil.which") as mock_which:
225
+ with patch("mdify.cli.is_daemon_running") as mock_running:
226
+ # Setup: container exists but not running, orbstack exists and running
227
+ def which_side_effect(x):
228
+ if x in ("container", "orbstack"):
229
+ return f"/usr/local/bin/{x}"
230
+ return None
231
+
232
+ def running_side_effect(path):
233
+ return "orbstack" in path
234
+
235
+ mock_which.side_effect = which_side_effect
236
+ mock_running.side_effect = running_side_effect
237
+ result = detect_runtime(explicit=False)
238
+ assert result == "/usr/local/bin/orbstack"
239
+
240
+ def test_all_tools_exist_but_not_running(self, monkeypatch, capsys):
241
+ """Test warning when tools exist but none are running."""
242
+ monkeypatch.setenv("MDIFY_CONTAINER_RUNTIME", "")
243
+ with patch("mdify.cli.platform.system", return_value="Darwin"):
244
+ with patch("mdify.cli.shutil.which") as mock_which:
245
+ with patch("mdify.cli.is_daemon_running", return_value=False):
246
+ def which_side_effect(x):
247
+ if x in ("orbstack", "colima", "podman", "docker"):
248
+ return f"/usr/local/bin/{x}"
249
+ return None
250
+
251
+ mock_which.side_effect = which_side_effect
252
+ result = detect_runtime(explicit=False)
253
+ assert result is None
254
+ captured = capsys.readouterr()
255
+ assert "daemon is not running" in captured.err
256
+ assert "orbstack" in captured.err
257
+ assert "colima" in captured.err
258
+
115
259
 
116
260
  class TestNewCLIArgs:
117
261
  """Test new CLI arguments for docling-serve."""
@@ -673,6 +817,66 @@ class TestFileHandling:
673
817
  assert result == output_dir / "doc.md"
674
818
 
675
819
 
820
+ class TestIsDaemonRunning:
821
+ """Tests for is_daemon_running() function."""
822
+
823
+ def test_daemon_running_returns_true(self):
824
+ """Test is_daemon_running returns True when daemon is responsive."""
825
+ mock_result = Mock()
826
+ mock_result.returncode = 0
827
+ with patch("mdify.cli.subprocess.run", return_value=mock_result):
828
+ result = is_daemon_running("/usr/bin/docker")
829
+ assert result is True
830
+
831
+ def test_daemon_not_running_returns_false(self):
832
+ """Test is_daemon_running returns False when daemon is not responsive."""
833
+ mock_result = Mock()
834
+ mock_result.returncode = 1
835
+ with patch("mdify.cli.subprocess.run", return_value=mock_result):
836
+ result = is_daemon_running("/usr/bin/docker")
837
+ assert result is False
838
+
839
+ def test_daemon_timeout_returns_false(self):
840
+ """Test is_daemon_running returns False on timeout."""
841
+ with patch("mdify.cli.subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 5)):
842
+ result = is_daemon_running("/usr/bin/docker")
843
+ assert result is False
844
+
845
+ def test_daemon_oserror_returns_false(self):
846
+ """Test is_daemon_running returns False on OSError."""
847
+ with patch("mdify.cli.subprocess.run", side_effect=OSError("No such file")):
848
+ result = is_daemon_running("/usr/bin/nonexistent")
849
+ assert result is False
850
+
851
+ def test_apple_container_daemon_running(self):
852
+ """Test is_daemon_running uses 'system status' for Apple Container."""
853
+ mock_result = Mock()
854
+ mock_result.returncode = 0
855
+ with patch("mdify.cli.subprocess.run", return_value=mock_result) as mock_run:
856
+ result = is_daemon_running("/usr/local/bin/container")
857
+ assert result is True
858
+ mock_run.assert_called_once_with(
859
+ ["/usr/local/bin/container", "system", "status"],
860
+ capture_output=True,
861
+ timeout=5,
862
+ check=False,
863
+ )
864
+
865
+ def test_apple_container_daemon_not_running(self):
866
+ """Test is_daemon_running returns False when Apple Container daemon not running."""
867
+ mock_result = Mock()
868
+ mock_result.returncode = 1
869
+ with patch("mdify.cli.subprocess.run", return_value=mock_result) as mock_run:
870
+ result = is_daemon_running("/usr/local/bin/container")
871
+ assert result is False
872
+ mock_run.assert_called_once_with(
873
+ ["/usr/local/bin/container", "system", "status"],
874
+ capture_output=True,
875
+ timeout=5,
876
+ check=False,
877
+ )
878
+
879
+
676
880
  class TestContainerRuntime:
677
881
  """Tests for container runtime functions."""
678
882
 
@@ -774,6 +978,47 @@ class TestContainerRuntime:
774
978
  captured = capsys.readouterr()
775
979
  assert "Error pulling image" in captured.err
776
980
 
981
+ def test_apple_container_pull_success(self):
982
+ """Test pull_image uses 'image pull' for Apple Container."""
983
+ mock_result = Mock()
984
+ mock_result.returncode = 0
985
+ with patch("mdify.cli.subprocess.run", return_value=mock_result) as mock_run:
986
+ result = pull_image("/usr/local/bin/container", "test-image", quiet=True)
987
+ assert result is True
988
+ mock_run.assert_called_once_with(
989
+ ["/usr/local/bin/container", "image", "pull", "test-image"],
990
+ capture_output=True,
991
+ check=False,
992
+ )
993
+
994
+ def test_apple_container_image_exists(self):
995
+ """Test check_image_exists uses 'image list' for Apple Container."""
996
+ mock_result = Mock()
997
+ mock_result.returncode = 0
998
+ mock_result.stdout = json.dumps([
999
+ {"name": "test-image", "repoTags": ["test-image:latest"]},
1000
+ {"name": "other-image", "repoTags": ["other-image:latest"]}
1001
+ ]).encode()
1002
+ with patch("mdify.cli.subprocess.run", return_value=mock_result) as mock_run:
1003
+ result = check_image_exists("/usr/local/bin/container", "test-image")
1004
+ assert result is True
1005
+ mock_run.assert_called_once_with(
1006
+ ["/usr/local/bin/container", "image", "list", "--format", "json"],
1007
+ capture_output=True,
1008
+ check=False,
1009
+ )
1010
+
1011
+ def test_apple_container_image_not_exists(self):
1012
+ """Test check_image_exists returns False when image not in list."""
1013
+ mock_result = Mock()
1014
+ mock_result.returncode = 0
1015
+ mock_result.stdout = json.dumps([
1016
+ {"name": "other-image", "repoTags": ["other-image:latest"]}
1017
+ ]).encode()
1018
+ with patch("mdify.cli.subprocess.run", return_value=mock_result):
1019
+ result = check_image_exists("/usr/local/bin/container", "test-image")
1020
+ assert result is False
1021
+
777
1022
 
778
1023
  class TestGetStorageRoot:
779
1024
  """Tests for get_storage_root() function."""
@@ -874,6 +1119,60 @@ class TestGetStorageRoot:
874
1119
  result = get_storage_root("podman")
875
1120
  assert result is None
876
1121
 
1122
+ def test_orbstack_storage_root(self, monkeypatch):
1123
+ """Test get_storage_root returns OrbStack storage root."""
1124
+ from mdify.cli import get_storage_root
1125
+
1126
+ home = "/Users/testuser"
1127
+ monkeypatch.setenv("HOME", home)
1128
+ result = get_storage_root("/usr/local/bin/orbstack")
1129
+ assert result == f"{home}/.orbstack"
1130
+
1131
+ def test_colima_storage_root(self, monkeypatch):
1132
+ """Test get_storage_root returns Colima storage root."""
1133
+ from mdify.cli import get_storage_root
1134
+
1135
+ home = "/Users/testuser"
1136
+ monkeypatch.setenv("HOME", home)
1137
+ result = get_storage_root("/usr/local/bin/colima")
1138
+ assert result == f"{home}/.colima"
1139
+
1140
+ def test_orbstack_storage_root_with_full_path(self, monkeypatch):
1141
+ """Test get_storage_root works with full path to OrbStack executable."""
1142
+ from mdify.cli import get_storage_root
1143
+
1144
+ home = "/Users/apple"
1145
+ monkeypatch.setenv("HOME", home)
1146
+ result = get_storage_root("/opt/homebrew/bin/orbstack")
1147
+ assert result == f"{home}/.orbstack"
1148
+
1149
+ def test_colima_storage_root_with_full_path(self, monkeypatch):
1150
+ """Test get_storage_root works with full path to Colima executable."""
1151
+ from mdify.cli import get_storage_root
1152
+
1153
+ home = "/Users/apple"
1154
+ monkeypatch.setenv("HOME", home)
1155
+ result = get_storage_root("/opt/homebrew/bin/colima")
1156
+ assert result == f"{home}/.colima"
1157
+
1158
+ def test_apple_container_storage_root(self, monkeypatch):
1159
+ """Test get_storage_root returns Apple Container storage root."""
1160
+ from mdify.cli import get_storage_root
1161
+
1162
+ home = "/Users/testuser"
1163
+ monkeypatch.setenv("HOME", home)
1164
+ result = get_storage_root("/usr/local/bin/container")
1165
+ assert result == f"{home}/Library/Application Support/com.apple.container"
1166
+
1167
+ def test_apple_container_storage_root_with_full_path(self, monkeypatch):
1168
+ """Test get_storage_root works with full path to Apple Container executable."""
1169
+ from mdify.cli import get_storage_root
1170
+
1171
+ home = "/Users/apple"
1172
+ monkeypatch.setenv("HOME", home)
1173
+ result = get_storage_root("/opt/homebrew/bin/container")
1174
+ assert result == f"{home}/Library/Application Support/com.apple.container"
1175
+
877
1176
 
878
1177
  class TestGetImageSizeEstimate:
879
1178
  """Tests for get_image_size_estimate function."""
File without changes
File without changes
File without changes
File without changes
File without changes