From ab31659e2bb58e7704a891e390e9d762e713074c Mon Sep 17 00:00:00 2001
From: glenux <glenux@glenux.net>
Date: Sun, 2 Jun 2024 07:06:36 +0000
Subject: [PATCH 1/5] Update .drone.yml

---
 .drone.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.drone.yml b/.drone.yml
index be6a72d..030c2e3 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -30,7 +30,7 @@ steps:
   - name: publish:tag
     image: curlimages/curl
     environment:
-      PACKAGE_UPLOAD_URL: https://code.apps.glenux.net/api/packages/glenux/generic/docmachine-utils
+      PACKAGE_UPLOAD_URL: https://code.apps.glenux.net/api/packages/glenux/generic/docmachine-cli
       PACKAGE_BASENAME: docmachine_linux_amd64
       PACKAGE_UPLOAD_TOKEN:
         from_secret: PACKAGE_UPLOAD_TOKEN

From f55c06c05ed12c2db8cf85379966ac0f89c30c7f Mon Sep 17 00:00:00 2001
From: "Glenn Y. Rolland" <glenux@glenux.net>
Date: Sun, 2 Jun 2024 21:21:50 +0200
Subject: [PATCH 2/5] docs: add missing information to README

---
 README.md | 54 +++++++++++++++++++++++++++++++++++-------------------
 1 file changed, 35 insertions(+), 19 deletions(-)

diff --git a/README.md b/README.md
index 2ffdd91..bc4cd24 100644
--- a/README.md
+++ b/README.md
@@ -1,40 +1,53 @@
-# DocMachine (Utils)
+# DocMachine Cli
 
-DocMachine is a CLI tool designed to simplify the process of creating technical documentation and presentations. 
+DocMachine Cli is a tool designed to simplify the process of creating technical
+documentation and presentations.
 
 ## Motivation
 
 This project aims to address the following challenges:
 
-* **Automation:** Automate the generation of high-quality technical content, including documentation and presentation slides.
-* **Consistency:** Ensure a consistent and polished look and feel across all content pieces.
-* **Efficiency:** Reduce the time and effort required to produce content by leveraging AI tools.
+* **Automation:** Automate the generation of high-quality technical content,
+  including documentation and presentation slides.
+* **Consistency:** Ensure a consistent and polished look and feel across all
+  content pieces.
+* **Efficiency:** Reduce the time and effort required to produce content by
+  leveraging AI tools.
 
 ## Features
 
 DocMachine offers a range of features to streamline the content creation process:
 
-* **Scaffolding:** Generate a well-structured project directory with all the necessary files.
-* **Building:** Compile and publish your content as HTML and PDF documents using Dockerized build processes.
+* **Scaffolding:** Generate a well-structured project directory with all the
+  necessary files.
+* **Building:** Compile and publish your content as HTML and PDF documents
+  using Dockerized build processes.
 
 We are actively developing the following features for future releases:
 
-* **Planning:** Leverage LLMs (Large Language Models) to generate content outlines tailored to your specific needs and requirements.
-* **Writing:** Utilize LLMs to draft content for each section and subsection, saving you valuable time and effort.
+* **Planning:** Leverage LLMs (Large Language Models) to generate content
+  outlines tailored to your specific needs and requirements.
+* **Writing:** Utilize LLMs to draft content for each section and subsection,
+  saving you valuable time and effort.
 
 ## Prerequisites
 
-FIXME: list prerequisites for crystal lang & dependencies
+You'll need a recent version of Crystal (>= 1.11.0) to use this project.
+
+You'll also need to install a few dependencies:
+
+* libreadline-dev
+* libncurses-dev
 
 ## Getting Started
 
-Follow these steps to start using DocMachine:
+Follow these steps to start using DocMachine Cli:
 
 ### Installation
 
 ```bash
-git clone https://code.apps.glenux.net/glenux/docmachine-utils.git docmachine-utils
-cd docmachine-utils
+git clone https://code.apps.glenux.net/glenux/docmachine-cli.git docmachine-cli
+cd docmachine-cli
 make build
 make install
 ```
@@ -42,13 +55,14 @@ make install
 ### Create a New Project
 
 ```bash
-docmachine scaffold my-doc-project 
+docmachine scaffold my-documentation-project
 ```
 
-This command will create a new directory named `my-doc-project` with the following structure:
+This command will create a new directory named `my-documentation-project` with
+the following structure:
 
 ```
-my-doc-project
+my-documentation-project
 ├── _build
 ├── docs
 │   └── images  # link to ../images
@@ -60,7 +74,8 @@ my-doc-project
 ### Start Writing Content
 
 * **Documentation:** Place your Markdown files inside the `docs` directory.
-* **Presentations:** Place your Markdown files (using Marp syntax) inside the `slides` directory.
+* **Presentations:** Place your Markdown files (using Marp syntax) inside the
+  `slides` directory.
 * **Images:** Store your images in the respective `images` directories.
 
 ### Live-reload during writing
@@ -70,6 +85,7 @@ docmachine build -a watch
 ```
 
 This command will start a Docker container and build your documentation and presentations:
+
 * **Documentation:** Built using MkDocs and served on `http://localhost:5100`.
 * **Presentations:** Built using Marp and served on `http://localhost:5200`.
 
@@ -100,6 +116,6 @@ We welcome contributions to DocMachine! To contribute:
 
 ## License
 
-DocMachine is licensed under the GPL-3.0-or-later license. See the `LICENSE`
-file for details.
+DocMachine Cli is licensed under the GPL-3.0-or-later license. See the
+`LICENSE` file for details.
 

From c0ae494c5761cc0e98eba0e2591037cd5cfcdf2c Mon Sep 17 00:00:00 2001
From: "Glenn Y. Rolland" <glenux@glenux.net>
Date: Mon, 18 Nov 2024 21:49:02 +0100
Subject: [PATCH 3/5] feat: Add support for specifying custom Docker image tag
 in CLI options

---
 src/build/cli.cr    |  4 ++++
 src/build/config.cr |  1 +
 src/build/run.cr    | 53 +++++++++++++++++++++++++++++++--------------
 3 files changed, 42 insertions(+), 16 deletions(-)

diff --git a/src/build/cli.cr b/src/build/cli.cr
index 968db9c..529a882 100644
--- a/src/build/cli.cr
+++ b/src/build/cli.cr
@@ -40,6 +40,10 @@ module DocMachine::Build
           config.enable_tty = true
         end
 
+        opts.on("-i", "--image", "Use specific image:tag (default glenux/docmachine:latest)") do |image_tag|
+          config.image_tag = image_tag
+        end
+
         commands << ->() : Nil do
           app = DocMachine::Build::Run.new(config)
           app.prepare
diff --git a/src/build/config.cr b/src/build/config.cr
index ec33c4d..d65fcef 100644
--- a/src/build/config.cr
+++ b/src/build/config.cr
@@ -7,6 +7,7 @@ module DocMachine::Build
     property port : Int32 = 5100
     property enable_multiple : Bool = false
     property enable_cache : Bool = false
+    property image_tag : String = "glenux/docmachine:latest"
 
     def initialize(@parent : DocMachine::Config)
     end
diff --git a/src/build/run.cr b/src/build/run.cr
index 89ed74e..b5fcd3a 100644
--- a/src/build/run.cr
+++ b/src/build/run.cr
@@ -15,7 +15,6 @@ module DocMachine::Build
       data = "#{@config.data_dir}:#{@config.port}"
       @basehash = Digest::SHA256.hexdigest(data)[0..6]
       @docker_name = "docmachine-#{@basehash}"
-      @docker_image = "glenux/docmachine:latest"
       @docker_opts = [] of String
       @process = nil
     end
@@ -25,7 +24,7 @@ module DocMachine::Build
     # setup permissions
     def prepare()
       Log.info { "basedir      = #{@config.data_dir}" }
-      Log.info { "docker_image = #{@docker_image}" }
+      Log.info { "docker_image = #{@config.image_tag}" }
       Log.info { "action       = #{@config.action}" }
 
       self._pull_image()
@@ -56,13 +55,13 @@ module DocMachine::Build
       data_cache_file = data_cache_dir  / "image.tar"
       Log.info { "Checking cache #{data_cache_file}..." }
       if ! File.exists? data_cache_file.to_s
-        Log.info { "Downloading #{@docker_image} image..." }
-        Process.run("docker", ["pull", @docker_image], output: STDOUT)
+        Log.info { "Downloading #{@config.image_tag} image..." }
+        Process.run("docker", ["pull", @config.image_tag], output: STDOUT)
         Log.info { "Building cache for image (#{data_cache_dir})" }
         FileUtils.mkdir_p(data_cache_dir)
         status = Process.run(
           "docker", 
-          ["image", "save", @docker_image, "-o", data_cache_file.to_s], 
+          ["image", "save", @config.image_tag, "-o", data_cache_file.to_s], 
           output: STDOUT
         )
         if status.success? 
@@ -77,7 +76,7 @@ module DocMachine::Build
       end
 
       if @config.enable_cache
-        Log.info { "Loading #{@docker_image} image from cache..." }
+        Log.info { "Loading #{@config.image_tag} image from cache..." }
         docker_image_loaded = false
         status = Process.run(
           "docker", 
@@ -91,16 +90,34 @@ module DocMachine::Build
           exit 1
         end
       else
-        Log.info { "Loading #{@docker_image} image from local registry..." }
+        Log.info { "Loading #{@config.image_tag} image from local registry..." }
         # FIXME: check that local image exists
       end
     end
 
     def start()
-      uid = %x{id -u}.strip
-      gid = %x{id -g}.strip
-      Log.info { "uid: #{uid}" }
-      Log.info { "cid: #{gid}" }
+      # start with default uid/gid
+      ext_uid = %x{id -u}.strip.to_i
+      ext_gid = %x{id -g}.strip.to_i
+
+      # ...but use subuid/subgid if available
+      File.each_line("/etc/subuid") do |line|
+        split = line.split(":")
+        next if split[0] != %x{id -u -n}
+
+        subuid = split[1].to_i 
+        ext_uid += subuid - 1
+      end
+      File.each_line("/etc/subgid") do |line|
+        split = line.split(":")
+        next if split[0] != %x{id -g -n}
+
+        subgid = split[1].to_i
+        ext_gid += subgid - 1
+      end
+
+      Log.info { "ext uid: #{ext_uid}" }
+      Log.info { "ext cid: #{ext_gid}" }
 
       docker_opts = [] of String
       docker_opts << "run"
@@ -111,8 +128,9 @@ module DocMachine::Build
       docker_opts.concat ["--name", @docker_name]
       docker_opts << "--rm"
       docker_opts << "--shm-size=1gb"
-      docker_opts.concat ["-e", "EXT_UID=#{uid}"]
-      docker_opts.concat ["-e", "EXT_GID=#{gid}"]
+      # docker_opts << "--privileged"
+      docker_opts.concat ["-e", "EXT_UID=#{ext_uid}"]
+      docker_opts.concat ["-e", "EXT_GID=#{ext_gid}"]
       docker_opts.concat ["-v", "#{@config.data_dir}/docs:/app/docs"]
       docker_opts.concat ["-v", "#{@config.data_dir}/slides:/app/slides"]
       docker_opts.concat ["-v", "#{@config.data_dir}/images:/app/images"]
@@ -166,11 +184,14 @@ module DocMachine::Build
         Log.info { "Slides: no slides directory detected." }
         end
 
-      docker_opts << @docker_image
+      docker_opts << @config.image_tag
       docker_opts << @config.action
 
-      Log.info { docker_opts.inspect.colorize(:yellow) }
-      @process = Process.new("docker", docker_opts, output: STDOUT, error: STDERR)
+      Log.info { 
+        docker_str = ["docker"].concat(docker_opts).join(" ").colorize(:yellow)
+        "Docker: #{docker_str.to_s}"
+      }
+      @process = Process.new("docker", docker_opts, input: STDIN, output: STDOUT, error: STDERR)
     end
 
     def wait()

From 60ab198a694f189ae710815d49b003d6d94c4564 Mon Sep 17 00:00:00 2001
From: "Glenn Y. Rolland" <glenux@glenux.net>
Date: Tue, 19 Nov 2024 20:40:21 +0100
Subject: [PATCH 4/5] chore: Update .gitignore to include .aider* and .env
 files

---
 .gitignore | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.gitignore b/.gitignore
index ea479e5..e01d78f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
 /bin
 /lib
 
+.aider*
+.env

From e59ea9ff4441214810e050d6de6053d28dc84ad4 Mon Sep 17 00:00:00 2001
From: "Glenn Y. Rolland" <glenux@glenux.net>
Date: Thu, 27 Mar 2025 11:37:00 +0100
Subject: [PATCH 5/5] feat: implement and refactor container engine module

This commit introduces a comprehensive implementation and refactoring of
the container engine module. Key enhancements include the addition of
abstract classes and the implementation of container engines.

New features:
- Developed `DockerEngine` and `PodmanEngine` classes, deriving from
  `AbstractContainerEngine` to provide modular functionality.
- Added a CLI `--container-runtime` option, allowing users to select
  their preferred runtime, defaulting to Docker if unspecified.
- Introduced a `container_runtime` property in
  `DocMachine::Build::Config` for improved runtime management.

Refactoring:
- Refactored code to replace hardcoded Docker commands with method calls
  to `AbstractContainerEngine`, promoting code reuse and abstraction.

Signed-off-by: Glenn Y. Rolland <glenux@glenux.net>
---
 src/build/cli.cr                           |   4 +
 src/build/config.cr                        |   2 +-
 src/build/run.cr                           | 134 ++++++++++-----------
 src/container/abstract_container_engine.cr |  41 +++++++
 src/container/docker_engine.cr             | 109 +++++++++++++++++
 src/container/podman_engine.cr             | 107 ++++++++++++++++
 6 files changed, 328 insertions(+), 69 deletions(-)
 create mode 100644 src/container/abstract_container_engine.cr
 create mode 100644 src/container/docker_engine.cr
 create mode 100644 src/container/podman_engine.cr

diff --git a/src/build/cli.cr b/src/build/cli.cr
index 529a882..d317bca 100644
--- a/src/build/cli.cr
+++ b/src/build/cli.cr
@@ -36,6 +36,10 @@ module DocMachine::Build
           config.enable_multiple = true
         end
 
+        opts.on("-r", "--container-runtime RUNTIME", "Container runtime (default docker)") do |runtime|
+          config.container_runtime = runtime
+        end
+
         opts.on("-t", "--tty", "Enable TTY mode (needed for shell)") do
           config.enable_tty = true
         end
diff --git a/src/build/config.cr b/src/build/config.cr
index d65fcef..8df6b66 100644
--- a/src/build/config.cr
+++ b/src/build/config.cr
@@ -1,4 +1,3 @@
-
 module DocMachine::Build
   class Config
     property data_dir : String = Dir.current
@@ -8,6 +7,7 @@ module DocMachine::Build
     property enable_multiple : Bool = false
     property enable_cache : Bool = false
     property image_tag : String = "glenux/docmachine:latest"
+    property container_runtime : String = "docker"
 
     def initialize(@parent : DocMachine::Config)
     end
diff --git a/src/build/run.cr b/src/build/run.cr
index b5fcd3a..10f11b5 100644
--- a/src/build/run.cr
+++ b/src/build/run.cr
@@ -1,4 +1,3 @@
-
 require "path"
 require "file_utils"
 require "socket"
@@ -6,25 +5,46 @@ require "socket"
 require "./module"
 require "./config"
 require "../common/network"
+require "../container/abstract_container_engine"
+require "../container/docker_engine"
+require "../container/podman_engine"
 
 module DocMachine::Build
   class Run
     Log = DocMachine::Build::Log.for("run")
 
+    # Instance variable for the container engine
+    property container_engine : DocMachine::Container::AbstractContainerEngine
+
+    @basehash : String
+    @docker_name : String
+    @docker_opts : Array(String)
+    @process : Process?
+
     def initialize(@config : DocMachine::Build::Config)
       data = "#{@config.data_dir}:#{@config.port}"
       @basehash = Digest::SHA256.hexdigest(data)[0..6]
       @docker_name = "docmachine-#{@basehash}"
       @docker_opts = [] of String
       @process = nil
+
+      # Initialize the container engine based on configuration
+      @container_engine = (
+        if @config.container_runtime == "podman"
+          DocMachine::Container::PodmanEngine.new
+        else
+          DocMachine::Container::DockerEngine.new
+        end
+      )
     end
 
-    # cleanup environment
-    # create directories
-    # setup permissions
+    # Cleanup environment
+    # Create directories
+    # Setup permissions
     def prepare()
       Log.info { "basedir      = #{@config.data_dir}" }
-      Log.info { "docker_image = #{@config.image_tag}" }
+      Log.info { "container_image = #{@config.image_tag}" }
+      Log.info { "container_runtime = #{@config.container_runtime}" }
       Log.info { "action       = #{@config.action}" }
 
       self._pull_image()
@@ -33,62 +53,41 @@ module DocMachine::Build
 
     private def _avoid_duplicates
       Log.info { "Multiple Instances: stopping duplicate containers (for #{@docker_name})" }
-      docker_cid = %x{docker ps -f "name=#{@docker_name}" -q}.strip
+      container_id = @container_engine.find_container_id(@docker_name)
 
-      Log.info { "Multiple Instances: docker_name: #{@docker_name}" }
-      Log.info { "Multiple Instances: docker_cid: #{docker_cid || "-"}" }
+      Log.info { "Multiple Instances: container_name: #{@docker_name}" }
+      Log.info { "Multiple Instances: container_id: #{container_id || "-"}" }
 
-      if !docker_cid.empty?
-        Process.run("docker", ["kill", @docker_name])
-        Process.run("docker", ["rm", @docker_name])
+      if !container_id.empty?
+        @container_engine.kill_container(@docker_name)
+        @container_engine.remove_container(@docker_name)
       end
     end
 
     def _pull_image
       # FIXME: add option to force update
-      data_cache_dir = if ENV["XDG_CACHE_HOME"]? 
+      data_cache_dir = if ENV["XDG_CACHE_HOME"]?
                          Path[ENV["XDG_CACHE_HOME"], "docmachine"]
-                       else Path[ENV["HOME"], ".cache", "docmachine"]
+                       else
+                         Path[ENV["HOME"], ".cache", "docmachine"]
                        end
 
-      ## Build cache if it doesnt exist
-      data_cache_file = data_cache_dir  / "image.tar"
+      ## Build cache if it doesn't exist
+      data_cache_file = data_cache_dir / "image.tar"
       Log.info { "Checking cache #{data_cache_file}..." }
-      if ! File.exists? data_cache_file.to_s
+      if !File.exists? data_cache_file.to_s
         Log.info { "Downloading #{@config.image_tag} image..." }
-        Process.run("docker", ["pull", @config.image_tag], output: STDOUT)
+        @container_engine.pull_image(@config.image_tag)
         Log.info { "Building cache for image (#{data_cache_dir})" }
         FileUtils.mkdir_p(data_cache_dir)
-        status = Process.run(
-          "docker", 
-          ["image", "save", @config.image_tag, "-o", data_cache_file.to_s], 
-          output: STDOUT
-        )
-        if status.success? 
-          Log.info { "done" }
-        else
-          Log.error { "Unable to save cache image" }
-          exit 1
-        end
-
-      else 
-        Log.info { "Cache already exist. Skipping." }
+        status = @container_engine.save_image(@config.image_tag, data_cache_file.to_s)
+      else
+        Log.info { "Cache already exists. Skipping." }
       end
 
       if @config.enable_cache
         Log.info { "Loading #{@config.image_tag} image from cache..." }
-        docker_image_loaded = false
-        status = Process.run(
-          "docker", 
-          ["image", "load", "-i", data_cache_file.to_s], 
-          output: STDOUT
-        )
-        if status.success? 
-          Log.info { "done" }
-        else
-          Log.error { "Unable to load cache image" }
-          exit 1
-        end
+        status = @container_engine.load_image(data_cache_file.to_s)
       else
         Log.info { "Loading #{@config.image_tag} image from local registry..." }
         # FIXME: check that local image exists
@@ -96,35 +95,34 @@ module DocMachine::Build
     end
 
     def start()
-      # start with default uid/gid
+      # Start with default uid/gid
       ext_uid = %x{id -u}.strip.to_i
       ext_gid = %x{id -g}.strip.to_i
 
       # ...but use subuid/subgid if available
       File.each_line("/etc/subuid") do |line|
         split = line.split(":")
-        next if split[0] != %x{id -u -n}
+        next if split[0] != %x{id -u -n}.strip
 
-        subuid = split[1].to_i 
+        subuid = split[1].to_i
         ext_uid += subuid - 1
       end
       File.each_line("/etc/subgid") do |line|
         split = line.split(":")
-        next if split[0] != %x{id -g -n}
+        next if split[0] != %x{id -g -n}.strip
 
         subgid = split[1].to_i
         ext_gid += subgid - 1
       end
 
       Log.info { "ext uid: #{ext_uid}" }
-      Log.info { "ext cid: #{ext_gid}" }
+      Log.info { "ext gid: #{ext_gid}" }
 
       docker_opts = [] of String
-      docker_opts << "run"
       docker_opts << "-i"
-      # add tty support
+      # Add tty support
       docker_opts << "-t" if @config.enable_tty
-      # add container name
+      # Add container name
       docker_opts.concat ["--name", @docker_name]
       docker_opts << "--rm"
       docker_opts << "--shm-size=1gb"
@@ -143,7 +141,7 @@ module DocMachine::Build
         Log.info { "Theme: detected Marp files. Adding option to command line (#{docker_opt_marp_theme})" }
       else
         Log.info { "Theme: no theme detected. Using default files" }
-        end
+      end
 
       ## Detect Mkdocs configuration - old format (full)
       if File.exists?("#{@config.data_dir}/mkdocs.yml")
@@ -158,7 +156,7 @@ module DocMachine::Build
         Log.info { "Docs: detected mkdocs-patch.yml file. Adding option to command line (#{docker_opt_mkdocs_config})" }
       else
         Log.info { "Docs: no mkdocs-patch.yml detected. Using default files" }
-        end
+      end
 
       ## Detect docs
       if Dir.exists?("#{@config.data_dir}/docs")
@@ -175,23 +173,30 @@ module DocMachine::Build
       ## Detect slides
       if Dir.exists?("#{@config.data_dir}/slides")
         Log.info { "Slides: detected slides directory." }
-        marp_port = Network.find_port(@config.port+100)
+        marp_port = Network.find_port(@config.port + 100)
         docker_opt_marp_port = ["-p", "#{marp_port}:5200"]
-        docker_opts.concat docker_opt_marp_port 
+        docker_opts.concat docker_opt_marp_port
         Log.info { "Slides: Adding option to command line (#{docker_opt_marp_port})" }
         Log.notice { "Slides: Using port #{marp_port} for slides" }
       else
         Log.info { "Slides: no slides directory detected." }
-        end
+      end
 
       docker_opts << @config.image_tag
       docker_opts << @config.action
 
-      Log.info { 
-        docker_str = ["docker"].concat(docker_opts).join(" ").colorize(:yellow)
-        "Docker: #{docker_str.to_s}"
+      Log.info {
+        docker_str = [@config.container_runtime].concat(docker_opts).join(" ").colorize(:yellow)
+        "#{@config.container_runtime.capitalize}: #{docker_str}"
       }
-      @process = Process.new("docker", docker_opts, input: STDIN, output: STDOUT, error: STDERR)
+
+      @process = @container_engine.run_container(
+        @config.image_tag,
+        @config.action,
+        @docker_name,
+        docker_opts,
+        @config.enable_tty
+      )
     end
 
     def wait()
@@ -200,16 +205,9 @@ module DocMachine::Build
 
       Signal::INT.trap do
         Log.warn { "Received CTRL-C" }
-        process.signal(Signal::KILL)
-        Process.run("docker", ["kill", @docker_name])
+        @container_engine.kill_container(@docker_name)
       end
       process.wait
     end
-
-    def stop()
-    end
-
-    def docker_opts()
-    end
   end
 end
diff --git a/src/container/abstract_container_engine.cr b/src/container/abstract_container_engine.cr
new file mode 100644
index 0000000..6341a2c
--- /dev/null
+++ b/src/container/abstract_container_engine.cr
@@ -0,0 +1,41 @@
+module DocMachine::Container
+
+  class ContainerError < Exception ; end
+
+  class SaveError < ContainerError ; end
+  class KillError < ContainerError ; end
+  class RemoveError < ContainerError ; end
+  class LoadError < ContainerError ; end
+  class RunError < ContainerError ; end
+  class PullError < ContainerError ; end
+
+  abstract class AbstractContainerEngine
+    # Pulls the specified Docker/Podman image
+    abstract def pull_image(image_tag : String) : Nil
+
+    # Loads the Docker/Podman image from a local cache
+    abstract def load_image(cache_path : String) : Nil
+
+    # Saves the Docker/Podman image to a local cache
+    abstract def save_image(image_tag : String, cache_path : String) : Nil
+
+    # Runs a container with the given options
+    abstract def run_container(
+      image_tag : String,
+      action : String,
+      docker_name : String,
+      docker_opts : Array(String),
+      enable_tty : Bool
+    ) : Process
+
+    # Kills a running container by name
+    abstract def kill_container(container_name : String) : Nil
+
+    # Removes a container by name
+    abstract def remove_container(container_name : String) : Nil
+
+    # Finds the container ID based on its name
+    abstract def find_container_id(name : String) : String
+  end
+
+end
diff --git a/src/container/docker_engine.cr b/src/container/docker_engine.cr
new file mode 100644
index 0000000..1b82fbb
--- /dev/null
+++ b/src/container/docker_engine.cr
@@ -0,0 +1,109 @@
+module DocMachine::Container
+
+  class DockerEngine < AbstractContainerEngine
+    # Pulls the specified Docker image
+    def pull_image(image_tag : String) : Nil
+      Log.info { "Pulling Docker image: #{image_tag}" }
+      status = Process.run("docker", ["pull", image_tag], output: STDOUT, error: STDERR)
+      if status.success?
+        Log.info { "Successfully pulled Docker image: #{image_tag}" }
+        true
+      else
+        Log.error { "Failed to pull Docker image: #{image_tag}" }
+        false
+      end
+    end
+
+    # Loads the Docker image from a local cache
+    def load_image(cache_path : String) : Nil
+      Log.info { "Loading Docker image from cache: #{cache_path}" }
+      status = Process.run("docker", ["image", "load", "-i", cache_path], output: STDOUT, error: STDERR)
+      if status.success?
+        Log.info { "Successfully loaded Docker image from cache." }
+        true
+      else
+        Log.error { "Failed to load Docker image from cache." }
+        false
+      end
+    end
+
+    # Saves the Docker image to a local cache
+    def save_image(image_tag : String, cache_path : String) : Nil
+      Log.info { "Saving Docker image #{image_tag} to cache at #{cache_path}" }
+      status = Process.run("docker", ["image", "save", image_tag, "-o", cache_path], output: STDOUT, error: STDERR)
+      if status.success?
+        Log.info { "Successfully saved Docker image to cache." }
+        true
+      else
+        Log.error { "Failed to save Docker image to cache." }
+        false
+      end
+    end
+
+    # Runs a Docker container with the given options
+    def run_container(
+      image_tag : String,
+      action : String,
+      docker_name : String,
+      docker_opts : Array(String),
+      enable_tty : Bool
+    ) : Process
+      Log.info { "Running Docker container: #{docker_name}" }
+      
+      # Construct the full Docker run command
+      cmd = ["docker", "run"] + docker_opts + [image_tag, action]
+      
+      # Log the command for debugging
+      Log.debug { "Docker run command: #{cmd.join(" ")}" }
+      
+      # Start the Docker container process
+      process = Process.new("docker", ["run"] + docker_opts + [image_tag, action], input: STDIN, output: STDOUT, error: STDERR)
+      
+      Log.info { "Docker container #{docker_name} started." }
+      
+      process
+    end
+
+    # Kills a running Docker container by name
+    def kill_container(container_name : String) : Nil
+      Log.info { "Killing Docker container: #{container_name}" }
+      status = Process.run("docker", ["kill", container_name], output: STDOUT, error: STDERR)
+      if status.success?
+        Log.info { "Successfully killed Docker container: #{container_name}" }
+        true
+      else
+        Log.error { "Failed to kill Docker container: #{container_name}" }
+        false
+      end
+    end
+
+    # Removes a Docker container by name
+    def remove_container(container_name : String) : Nil
+      Log.info { "Removing Docker container: #{container_name}" }
+      status = Process.run("docker", ["rm", container_name], output: STDOUT, error: STDERR)
+      if status.success?
+        Log.info { "Successfully removed Docker container: #{container_name}" }
+        true
+      else
+        Log.error { "Failed to remove Docker container: #{container_name}" }
+        false
+      end
+    end
+
+    # Finds the container ID based on its name
+    def find_container_id(name : String) : String
+      Log.info { "Finding Docker container ID for name: #{name}" }
+      output = IO::Memory.new
+      status = Process.run("docker", ["ps", "-f", "name=#{name}", "-q"], output: output, error: STDERR)
+      if status.success?
+        container_id = output.to_s.strip
+        Log.info { "Found Docker container ID: #{container_id}" }
+        container_id
+      else
+        Log.error { "Failed to find Docker container ID for name: #{name}" }
+        ""
+      end
+    end
+  end
+
+end
diff --git a/src/container/podman_engine.cr b/src/container/podman_engine.cr
new file mode 100644
index 0000000..0b02904
--- /dev/null
+++ b/src/container/podman_engine.cr
@@ -0,0 +1,107 @@
+module DocMachine::Container
+
+  class PodmanEngine < AbstractContainerEngine
+    # Pulls the specified Podman image
+    def pull_image(image_tag : String) : Nil
+      Log.info { "Pulling Podman image: #{image_tag}" }
+      status = Process.run("podman", ["pull", image_tag], output: STDOUT, error: STDERR)
+      if status.success?
+        Log.info { "Successfully pulled Podman image: #{image_tag}" }
+        true
+      else
+        Log.error { "Failed to pull Podman image: #{image_tag}" }
+        false
+      end
+    end
+
+    # Loads the Podman image from a local cache
+    def load_image(cache_path : String) : Nil
+      Log.info { "Loading Podman image from cache: #{cache_path}" }
+      status = Process.run("podman", ["image", "load", "-i", cache_path], output: STDOUT, error: STDERR)
+      if status.success?
+        Log.info { "Successfully loaded Podman image from cache." }
+      else
+        Log.error { "Failed to load Podman image from cache." }
+        raise LoadError.new("Failed to load Podman image from cache.")
+      end
+    end
+
+    # Saves the Podman image to a local cache
+    def save_image(image_tag : String, cache_path : String) : Nil
+      Log.info { "Saving Podman image #{image_tag} to cache at #{cache_path}" }
+      status = Process.run("podman", ["image", "save", image_tag, "-o", cache_path], output: STDOUT, error: STDERR)
+      if status.success?
+        Log.info { "Successfully saved Podman image to cache." }
+      else
+        Log.error { "Failed to save Podman image to cache." }
+        raise SaveError.new("Failed to save Podman image to cache.")
+      end
+    end
+
+    # Runs a Podman container with the given options
+    def run_container(
+      image_tag : String,
+      action : String,
+      docker_name : String,
+      docker_opts : Array(String),
+      enable_tty : Bool
+    ) : Process
+      Log.info { "Running Podman container: #{docker_name}" }
+      
+      # Construct the full Podman run command
+      cmd = ["podman", "run"] + docker_opts + [image_tag, action]
+      
+      # Log the command for debugging
+      Log.debug { "Podman run command: #{cmd.join(" ")}" }
+      
+      # Start the Podman container process
+      process = Process.new("podman", ["run"] + docker_opts + [image_tag, action], input: STDIN, output: STDOUT, error: STDERR)
+      
+      Log.info { "Podman container #{docker_name} started." }
+      
+      process
+    end
+
+    # Kills a running Podman container by name
+    def kill_container(container_name : String) : Nil
+      Log.info { "Killing Podman container: #{container_name}" }
+      status = Process.run("podman", ["kill", container_name], output: STDOUT, error: STDERR)
+      if status.success?
+        Log.info { "Successfully killed Podman container: #{container_name}" }
+        true
+      else
+        Log.error { "Failed to kill Podman container: #{container_name}" }
+        false
+      end
+    end
+
+    # Removes a Podman container by name
+    def remove_container(container_name : String) : Nil
+      Log.info { "Removing Podman container: #{container_name}" }
+      status = Process.run("podman", ["rm", container_name], output: STDOUT, error: STDERR)
+      if status.success?
+        Log.info { "Successfully removed Podman container: #{container_name}" }
+        true
+      else
+        Log.error { "Failed to remove Podman container: #{container_name}" }
+        false
+      end
+    end
+
+    # Finds the container ID based on its name
+    def find_container_id(name : String) : String
+      Log.info { "Finding Podman container ID for name: #{name}" }
+      output = IO::Memory.new
+      status = Process.run("podman", ["ps", "-f", "name=#{name}", "-q"], output: output, error: STDERR)
+      if status.success?
+        container_id = output.to_s.strip
+        Log.info { "Found Podman container ID: #{container_id}" }
+        container_id
+      else
+        Log.error { "Failed to find Podman container ID for name: #{name}" }
+        ""
+      end
+    end
+  end
+
+end