diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,51 @@
+# 1.1.0.4 - 2026-07-01
+
+- Adjusted cabal version to be lower for better coverage.
+- GHC2021 for better coverage
+
+## Added
+
+- Cross-distro `sak kernel` command
+  - Now works on Arch, Debian, Ubuntu, Linux Mint, Pop!<sub>OS</sub>, Fedora, RHEL, CentOS, Rocky Linux, and AlmaLinux
+  - Detects running kernel via `uname -r`
+  - Shows installed kernels using the native package manager (`pacman`, `dpkg-query`, or `rpm`)
+  - Graceful handling when optional kernel packages (e.g. `linux-lts`) are not installed
+
+## Changed
+
+- `Knives.Kernel` module is now fully distribution-agnostic
+- Cleaner output formatting with proper newlines
+- More robust command execution (no crashes on missing packages)
+
+## Fixed
+
+- Trailing newlines from `readProcess`
+- Ambiguous exception type error on GHC 9.14
+
+# 1.0.0.1 and earlier
+
+## Versions
+
+- 1.1.0.0 - Hackage release, renamed to swiss-army-knife (dropping the "-hs" from the name. AUR will pull from Hackage instead. of GitHub.
+
+- 1.0.0.1 - enable and disable daemons, wg -l list has an extra field.
+
+  - Changelog.org removed. Basically a duplication of Versions here.
+  - version 1.0.0.0 was stillborn. Installation issues fixed
+
+- 0.1.0.4 - fix for install issues with 0.1.0.3
+
+- 0.1.0.3 - 2 new feature, internally invokes sudo, adopted "standard" for 4 number versioning X.Y.Z.W:
+
+  | \#  | Description                                   |
+  |-----|-----------------------------------------------|
+  | X   | Major version (breaking changes)              |
+  | Y   | Minor version (new features)                  |
+  | Z   | Patch version (bug fixes)                     |
+  | W   | Build or revision number (specific iteration) |
+
+  So from now on, this will be applied.
+
+- 0.0.1.1 - new feature added
+
+- 0.0.0.2 - bug fixes
diff --git a/CHANGELOG.org b/CHANGELOG.org
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.org
@@ -0,0 +1,45 @@
+#+TITLE: Changelog for swiss-army-knife
+#+AUTHOR: Fred Mitchell
+#+EMAIL: fred.mitchell@atomlogik.de
+
+* 1.1.0.4 - 2026-07-01
+  + Adjusted cabal version to be lower for better coverage.
+  + GHC2021 for better coverage
+** Added
+- Cross-distro =sak kernel= command
+  - Now works on Arch, Debian, Ubuntu, Linux Mint, Pop!_OS, Fedora, RHEL, CentOS, Rocky Linux, and AlmaLinux
+  - Detects running kernel via =uname -r=
+  - Shows installed kernels using the native package manager (=pacman=, =dpkg-query=, or =rpm=)
+  - Graceful handling when optional kernel packages (e.g. =linux-lts=) are not installed
+
+** Changed
+- =Knives.Kernel= module is now fully distribution-agnostic
+- Cleaner output formatting with proper newlines
+- More robust command execution (no crashes on missing packages)
+
+** Fixed
+- Trailing newlines from =readProcess=
+- Ambiguous exception type error on GHC 9.14
+
+* 1.0.0.1 and earlier
+** Versions
+   + 1.1.0.0 - Hackage release, renamed to swiss-army-knife (dropping the
+                "-hs" from the name. AUR will pull from Hackage instead.
+                of GitHub.
+   + 1.0.0.1 - enable and disable daemons, wg -l list has an extra field.
+              - Changelog.org removed. Basically a duplication of Versions here.
+              - version 1.0.0.0 was stillborn. Installation issues fixed
+   + 0.1.0.4 - fix for install issues with 0.1.0.3
+   + 0.1.0.3 - 2 new feature, internally invokes sudo,
+      adopted "standard" for 4 number versioning X.Y.Z.W:
+
+      | # | Description                                   |
+      |---+-----------------------------------------------|
+      | X | Major version (breaking changes)              |
+      | Y | Minor version (new features)                  |
+      | Z | Patch version (bug fixes)                     |
+      | W | Build or revision number (specific iteration) |
+
+      So from now on, this will be applied.
+   + 0.0.1.1 - new feature added
+   + 0.0.0.2 - bug fixes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,28 @@
+Copyright Fred Mitchell (c) 2024
+
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/README.org b/README.org
new file mode 100644
--- /dev/null
+++ b/README.org
@@ -0,0 +1,79 @@
+* swiss-army-knife :TOC_7_gh:
+  - [[#created-by][Created By]]
+  - [[#introduction][Introduction]]
+  - [[#documentation][Documentation]]
+    - [[#sak---help][sak --help]]
+    - [[#sak-extip][sak extip]]
+    - [[#sak-kernel][sak kernel]]
+    - [[#sak-sleep][sak sleep]]
+    - [[#sak-zfscheck][sak zfscheck]]
+    - [[#sak-wg][sak wg]]
+    - [[#sak-nm][sak nm]]
+    - [[#sak-sn][sak sn]]
+  - [[#examples][Examples]]
+
+** Created By
+   Fred Mitchell & Atomlogik
+      
+** Introduction
+   Swiss-Army-Knife is a collection of useful
+   tools that you can do quickly.
+   
+   This is now in Hackage and the AUR. So in theory,
+   it can run "anywhere", though I have not had the time to test this,
+   so I am open to comments and pull requests. In particular, it is 
+   gauranteed to run on Arch, and I do have some support for other
+   distros, and that will grow. But I have not had the time to fully
+   test other distros.
+
+   So some knives will run "anywhere", but some that call out to
+   the system for system-specific information may have issues.
+   As I find out about them, I will fix them as quickly as I can.
+
+** Documentation
+   These are the current "knives" implemented. More to come. Open
+   to suggestions.
+   + Note that you no longer have to prepend "sudo" to do system-level
+      operations.
+*** sak --help
+    Will list all the current commands.
+*** sak extip
+    Will tell you what your external IP is for your connection,
+    either in IPv4 or IPv6 Useful if you use a VPN
+*** sak kernel
+    Will tell you the version of the installed kernel as well
+    as the running version. Support is in for Arch, Debian, and
+    RedHat, but only Arch has been tested.
+*** sak sleep
+    Will put your system in standby mode.
+*** sak zfscheck
+    If you use the zfs from archzfs, sometimes it 
+    does not keep up with the latest kernel release.
+    Check to see the latest kernel zfs supports before
+    you do a system upgrade. Other distros will probably
+    not have the zfs sync issues that Arch does.
+*** sak wg
+    If you use WireGuard for your VPN, manage it from
+    here. List your installed WireGuard options,
+    and swap among them. you can start, stop, and list your
+    WireGuard VPNs.
+
+    Now supports enabling and disabling of the underlying daemon.
+*** sak nm
+    Manage NetworkManager
+    Now supports enabling and disabling of the underlying daemon.
+
+*** sak sn
+    Manage systemd-networkd
+    Now supports enabling and disabling of the underlying daemon.
+
+** Examples
+   | Command   | Description                                                                                                        |
+   |-----------+--------------------------------------------------------------------------------------------------------------------|
+   | sak extip | Will list your external IP and location information                                                                |
+   | sak wg -l | (If you are using wg_quick) will list all of your available WG vpns, as well as their active and enabled statuses. |
+   | sak wg -d | Will deactivate (but not disable) all WireGuard VPNs.                                                            |
+   | sak wg -D | Will disable (but not deactivate) all WireGuard VPNs.                                                              |
+   | sak sn -A | Will enable and start systemd-networkd.                                                                            |
+   | sak nm -D | Will disable and stop NetworkManager.                                                                              |
+   |-----------+--------------------------------------------------------------------------------------------------------------------|
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/SwissArmyKnife.hs b/app/SwissArmyKnife.hs
new file mode 100644
--- /dev/null
+++ b/app/SwissArmyKnife.hs
@@ -0,0 +1,25 @@
+-- main for Swiss Army Knife, cli.
+
+module Main where
+
+import Options.Applicative
+import Data.Semigroup ((<>))
+import CommandLine
+import Knives
+
+main :: IO ()
+main = do
+  (globalOpts, cmd) <- execParser $ info (opts <**> helper)
+    ( fullDesc
+      <> progDesc "Many useful utilities, such as getting your external IP address, installed kernel, controlling your WireGuard, etc."
+      <> header "Swiss Army Knife -- Many useful functions for the hacker in all of us." )
+  
+  case cmd of
+    ExtIP extipOpts     -> knifeExtIP     extipOpts
+    Kernel kernelOpts   -> knifeKernel    kernelOpts
+    Sleep sleepOpts     -> knifeSleep     sleepOpts 
+    Version versionOpts -> knifeVersion   versionOpts 
+    ZfsCheck zfsOpts    -> knifeZfsCheck  zfsOpts
+    WireGuard wgOpts    -> knifeWireGuard wgOpts
+    NetMan nmOpts       -> knifeNetMan    nmOpts
+    SysNet snOpts       -> knifeSysNet    snOpts
diff --git a/lib/CommandLine.hs b/lib/CommandLine.hs
new file mode 100644
--- /dev/null
+++ b/lib/CommandLine.hs
@@ -0,0 +1,178 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module CommandLine where
+
+import Options.Applicative
+
+-- Define data types for our commands and options
+data Command
+  = ExtIP     ExtIPOptions
+  | Version   VersionOptions
+  | Kernel    KernelOptions
+  | Sleep     SleepOptions
+  | ZfsCheck  ZfsCheckOptions
+  | WireGuard WireGuardOptions
+  | NetMan    NetManOptions
+  | SysNet    SysNetOptions
+  deriving Show 
+
+data ExtIPOptions = ExtIPOptions
+  { nolocation :: Bool
+  , ipv6       :: Bool
+  } deriving Show
+
+data KernelOptions = KernelOptions
+  { justVersion :: Bool } deriving Show
+
+data SleepOptions = SleepOptions
+  { secondsToSleep :: Maybe Int } deriving Show
+
+data VersionOptions = VersionOptions Bool deriving Show
+
+data ZfsCheckOptions = ZfsCheckOptions
+  { full :: Bool } deriving Show
+
+data WireGuardOptions = WireGuardOptions
+  { listWGs      :: Bool
+  , activateWG   :: Maybe String
+  , enableWG     :: Maybe String
+  , deactivateWG :: Bool
+  , disableWG    :: Bool
+  , reactivateWG :: Bool
+  } deriving Show
+
+data NetManOptions = NetManOptions
+  { activateNM   :: Bool
+  , enableNM     :: Bool
+  , deactivateNM :: Bool
+  , disableNM    :: Bool
+  , reactivateNM :: Bool
+  } deriving Show
+
+data SysNetOptions = SysNetOptions
+  { activateSN   :: Bool
+  , enableSN     :: Bool
+  , deactivateSN :: Bool
+  , disableSN    :: Bool
+  , reactivateSN :: Bool
+  } deriving Show
+
+data GlobalOptions = GlobalOptions
+  { verbose :: Bool } deriving Show
+
+globalOptionsParser :: Parser GlobalOptions
+globalOptionsParser = GlobalOptions
+  <$> switch ( long     "verbose"
+               <> short 'v'
+               <> help  "Enable verbose mode" )
+
+extipOptionsParser :: Parser ExtIPOptions
+extipOptionsParser = ExtIPOptions
+  <$> switch ( long "nolocation"
+               <> short 'n'
+               <> help "Don't display location information" )
+  <*> switch ( long "ipv6"
+               <> short 'i'
+               <> help "Display the IPv6 address" )
+
+kernelOptionsParser :: Parser KernelOptions
+kernelOptionsParser = KernelOptions
+  <$> switch ( long "justversion"
+               <> short 'j'
+               <> help "Just display the numerical version number, no adjoining text." )
+
+-- version has no options
+versionOptionsParser :: Parser VersionOptions
+versionOptionsParser = VersionOptions
+  <$> switch ( long "noop" )
+  
+sleepOptionsParser :: Parser SleepOptions
+sleepOptionsParser = SleepOptions
+  <$> optional (option auto ( long "wait"
+                              <> short 'w'
+                              <> metavar "SECONDS"
+                              <> help "Seconds to wait before going to sleep"))
+
+zfscheckOptionsParser :: Parser ZfsCheckOptions
+zfscheckOptionsParser = ZfsCheckOptions
+  <$> switch
+  ( long "full"
+    <> short 'f'
+    <> help "Check all kernel versions" )
+
+wgOptionsParser :: Parser WireGuardOptions
+wgOptionsParser = WireGuardOptions
+  <$> switch   ( long "list"
+                 <> short 'l'
+                 <> help "List all installed WireGuardG VPNs")
+  <*> optional (strOption ( long "activate"
+                              <> short 'a'
+                              <> metavar "WGVPN"
+                              <> help "WireGuard VPN to activate"))
+  <*> optional (strOption ( long "enable"
+                              <> short 'A'
+                              <> metavar "WGVPN"
+                              <> help "WireGuard VPN to enable and activate"))
+  <*> switch   ( long "deactivate"
+                 <> short 'd'
+                 <> help "Deactivate (but not disable) all WireGuard VPNs")
+  <*> switch   ( long "disable"
+                 <> short 'D'
+                 <> help "Disable (but not deactivate) all WireGuard VPNs")
+  <*> switch   ( long "reactivate"
+                 <> short 'r'
+                 <> help "Reactivate currently active WireGuardG VPNs")
+
+nmOptionsParser :: Parser NetManOptions
+nmOptionsParser = NetManOptions
+  <$> switch   ( long "activate"
+                 <> short 'a'
+                 <> help "Activate (start) NetworkManager")
+  <*> switch   ( long "enable"
+                 <> short 'A'
+                 <> help "Enable and Activate (start) NetworkManager")
+  <*> switch   ( long "deactivate"
+                 <> short 'd'
+                 <> help "Deactivate (stop) NetworkManager")
+  <*> switch   ( long "disable"
+                 <> short 'D'
+                 <> help "Disable and Deactivate (stop) NetworkManager")
+  <*> switch   ( long "reactivate"
+                 <> short 'r'
+                 <> help "Reactivate (restart) NetworkManager")
+
+snOptionsParser :: Parser SysNetOptions
+snOptionsParser = SysNetOptions
+  <$> switch   ( long "activate"
+                 <> short 'a'
+                 <> help "Activate (start) systemd-networkd socket and service")
+  <*> switch   ( long "enable"
+                 <> short 'A'
+                 <> help "Enable and Activate (start) systemd-networkd socket and service")
+  <*> switch   ( long "deactivate"
+                 <> short 'd'
+                 <> help "Deactivate (stop) systemd-networkd socket and service")
+  <*> switch   ( long "disable"
+                 <> short 'D'
+                 <> help "Disable and Deactivate (stop) systemd-networkd socket and service")
+  <*> switch   ( long "reactivate"
+                 <> short 'r'
+                 <> help "Reactivate (restart) systemd-networkd socket and service")
+
+
+-- Combine the subcommand parsers
+commandParser :: Parser Command
+commandParser = subparser
+  (    command "extip"    (info (ExtIP     <$> extipOptionsParser)    (progDesc "Display external IP address"))
+    <> command "kernel"   (info (Kernel    <$> kernelOptionsParser)   (progDesc "Display kernel information, both installed and currently running"))
+    <> command "sleep"    (info (Sleep     <$> sleepOptionsParser)    (progDesc "Put the machine to sleep"))
+    <> command "version"  (info (Version   <$> versionOptionsParser)  (progDesc "Version of Swiss Army Knife"))
+    <> command "zfscheck" (info (ZfsCheck  <$> zfscheckOptionsParser) (progDesc "Check zfs availibility for the current kernel"))
+    <> command "wg"       (info (WireGuard <$> wgOptionsParser)       (progDesc "Manage WireGuard VPNs"))
+    <> command "nm"       (info (NetMan    <$> nmOptionsParser)       (progDesc "Manage NetworkManager"))
+    <> command "sn"       (info (SysNet    <$> snOptionsParser)       (progDesc "Manage systemd-networkd"))
+  )
+
+-- Combine global options with the command parser
+opts :: Parser (GlobalOptions, Command)
+opts = (,) <$> globalOptionsParser <*> commandParser
diff --git a/lib/Knives.hs b/lib/Knives.hs
new file mode 100644
--- /dev/null
+++ b/lib/Knives.hs
@@ -0,0 +1,23 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Knives
+    ( knifeKernel
+    , knifeExtIP
+    , knifeSleep
+    , knifeVersion
+    , knifeZfsCheck
+    , knifeWireGuard
+    , knifeNetMan
+    , knifeSysNet
+    ) where
+
+
+import Knives.ExtIP
+import Knives.Kernel
+import Knives.Sleep
+import Knives.Version
+import Knives.WireGuard
+import Knives.ZfsCheck
+import Knives.NetMan
+import Knives.SysNet
+
diff --git a/lib/Knives/ExtIP.hs b/lib/Knives/ExtIP.hs
new file mode 100644
--- /dev/null
+++ b/lib/Knives/ExtIP.hs
@@ -0,0 +1,89 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Knives.ExtIP where
+
+import CommandLine
+import Network.HTTP.Simple
+import Data.Aeson
+import Data.Bool (bool)
+import Control.Monad ((>=>))
+
+data RExtIP = RExtIP
+  { ip :: String } deriving Show
+
+instance FromJSON RExtIP where
+  parseJSON = withObject "RExtIP" $ \v -> RExtIP
+    <$> v .: "ip"
+
+data RLoc = RLoc
+  {status      :: String
+  , country    :: String
+  , countryCode:: String
+  , region     :: String
+  , regionName :: String
+  , city       :: String
+  , zip        :: String
+  , lat        :: Float
+  , lon        :: Float
+  , timezone   :: String
+  , isp        :: String
+  , org        :: String
+  , as         :: String
+  , query      :: String
+  } deriving Show
+
+instance FromJSON RLoc where
+  parseJSON = withObject "RLoc" $ \v -> RLoc
+    <$> v .: "status"
+    <*> v .: "country"
+    <*> v .: "countryCode"
+    <*> v .: "region"
+    <*> v .: "regionName"
+    <*> v .: "city"
+    <*> v .: "zip"
+    <*> v .: "lat"
+    <*> v .: "lon"
+    <*> v .: "timezone"
+    <*> v .: "isp"
+    <*> v .: "org"
+    <*> v .: "as"
+    <*> v .: "query"
+  
+knifeExtIP :: ExtIPOptions -> IO ()
+knifeExtIP optsE = do
+  res <- (parseRequest >=> httpLBS) $ bool url_ipv4 url_ipv6 $ ipv6 optsE
+  putStrLn $ ipaddr res
+  if (not $ nolocation optsE)
+    then do
+      loc <- location
+      putStrLn $ loc
+    else return ()
+
+  where
+    url_ipv4 = "https://ipv4.jsonip.com"
+    url_ipv6 = "https://ipv6.jsonip.com"
+    url_geo  = "http://ip-api.com/json/"
+    ipaddr res = do
+      let ipresult = eitherDecode $ getResponseBody res :: Either String RExtIP
+      case ipresult of
+        Left err -> do
+          "Failed to obtain IP: " ++ err
+        Right extIP -> do
+          ip extIP
+
+    location :: IO String
+    location = do
+      request <- parseRequest url_geo
+      res <- httpLBS request
+      let locres = eitherDecode $ getResponseBody res :: Either String RLoc
+      case locres of
+        Left err -> do
+          return $ "Failed to obtain location: " ++ err
+        Right loc -> do
+          let str_result = "Location: "
+                ++ city loc ++ ", "
+                ++ country loc
+                ++ " (" ++ countryCode loc ++ ")\n"
+                ++ "Timezone: " ++ timezone loc ++ "\n"
+                
+          return str_result
diff --git a/lib/Knives/Kernel.hs b/lib/Knives/Kernel.hs
new file mode 100644
--- /dev/null
+++ b/lib/Knives/Kernel.hs
@@ -0,0 +1,143 @@
+module Knives.Kernel
+  ( knifeKernel
+  , KernelOptions(..)
+  ) where
+
+import CommandLine (KernelOptions(..))
+import System.Process (readProcess)
+import Control.Exception (try, IOException)
+import Data.Char (isSpace)
+import Data.List (dropWhileEnd, isInfixOf, isPrefixOf)
+import System.Directory (doesFileExist)
+import Data.Maybe (catMaybes)
+
+-- | Trim leading and trailing whitespace/newlines
+trim :: String -> String
+trim = dropWhileEnd isSpace . dropWhile isSpace
+
+-- | Safely run a command. Returns Nothing if it fails or command not found.
+tryReadProcess :: String -> [String] -> IO (Maybe String)
+tryReadProcess cmd args = do
+  result <- try @IOException (readProcess cmd args "")
+  pure $ either (const Nothing) (Just . trim) result
+
+-- | Detect Debian-based systems (Debian, Ubuntu, Mint, Pop!_OS, Kali, etc.)
+isDebianBased :: IO Bool
+isDebianBased = do
+  hasDebian <- doesFileExist "/etc/debian_version"
+  if hasDebian
+    then pure True
+    else checkOsRelease
+  where
+    checkOsRelease = do
+      hasOs <- doesFileExist "/etc/os-release"
+      if not hasOs then pure False else do
+        content <- readFile "/etc/os-release"
+        let debianLike = ["debian", "ubuntu", "linuxmint", "pop", "kali", "elementary", "zorin"]
+        pure $ any (\idName ->
+                      ("ID="        ++ idName) `isInfixOf` content ||
+                      ("ID_LIKE="   ++ idName) `isInfixOf` content
+                   ) debianLike
+
+-- | Detect Red Hat family (RHEL, CentOS, Fedora, Rocky, AlmaLinux, Oracle, etc.)
+isRedHatBased :: IO Bool
+isRedHatBased = do
+  hasRedhat <- doesFileExist "/etc/redhat-release"
+  if hasRedhat
+    then pure True
+    else checkOsRelease
+  where
+    checkOsRelease = do
+      hasOs <- doesFileExist "/etc/os-release"
+      if not hasOs then pure False else do
+        content <- readFile "/etc/os-release"
+        let redhatLike = ["rhel", "centos", "fedora", "rocky", "almalinux", "ol", "scientific"]
+        pure $ any (\idName ->
+                      ("ID="      ++ idName) `isInfixOf` content ||
+                      ("ID_LIKE=" ++ idName) `isInfixOf` content
+                   ) redhatLike
+
+-- | Get the currently running kernel (works on all Linux distros)
+getRunningKernel :: IO String
+getRunningKernel = do
+  mVer <- tryReadProcess "uname" ["-r"]
+  pure $ maybe "unknown" id mVer
+
+-- | Get installed kernel packages (distro-aware)
+getInstalledKernels :: IO [String]
+getInstalledKernels = do
+  debian <- isDebianBased
+  if debian
+    then getDebianKernels
+    else do
+      redhat <- isRedHatBased
+      if redhat
+        then getRedHatKernels
+        else getArchKernels
+
+-- | Debian / Ubuntu / Mint style
+getDebianKernels :: IO [String]
+getDebianKernels = do
+  mOut <- tryReadProcess "dpkg-query"
+            ["-W", "-f=${Package} ${Version}\n", "linux-image-*"]
+  case mOut of
+    Just out | not (null out) ->
+      pure $ filter (not . null) $ lines out
+    _ -> do
+      -- Fallback to dpkg -l
+      mOut2 <- tryReadProcess "dpkg" ["-l"]
+      case mOut2 of
+        Just out2 ->
+          pure $ take 8
+               $ map (unwords . take 4 . words)
+               $ filter ("linux-image" `isInfixOf`) (lines out2)
+        Nothing -> pure []
+
+-- | Red Hat family (RHEL, CentOS, Fedora, Rocky, AlmaLinux, etc.)
+getRedHatKernels :: IO [String]
+getRedHatKernels = do
+  mOut <- tryReadProcess "rpm"
+            [ "-qa"
+            , "--qf"
+            , "%{NAME}-%{VERSION}-%{RELEASE}\n"
+            , "kernel*"
+            ]
+  case mOut of
+    Just out | not (null (trim out)) ->
+      pure $ take 8
+           $ filter (\pkg ->
+               "kernel" `isPrefixOf` pkg &&
+               not ("headers"   `isInfixOf` pkg) &&
+               not ("devel"     `isInfixOf` pkg) &&
+               not ("doc"       `isInfixOf` pkg) &&
+               not ("tools"     `isInfixOf` pkg)
+             )
+           $ lines (trim out)
+    _ -> pure []
+
+-- | Arch Linux style
+getArchKernels :: IO [String]
+getArchKernels = do
+  mLinux <- tryReadProcess "pacman" ["-Q", "linux"]
+  mLts   <- tryReadProcess "pacman" ["-Q", "linux-lts"]
+  mZen   <- tryReadProcess "pacman" ["-Q", "linux-zen"]
+  mHard  <- tryReadProcess "pacman" ["-Q", "linux-hardened"]
+  pure $ catMaybes [mLinux, mLts, mZen, mHard]
+
+-- | Main entry point for the `sak kernel` command
+knifeKernel :: KernelOptions -> IO ()
+knifeKernel optsK = do
+  running <- getRunningKernel
+
+  if justVersion optsK
+    then putStrLn running
+    else do
+      installed <- getInstalledKernels
+
+      putStrLn $ "Running kernel:  " ++ running
+
+      if null installed
+        then putStrLn "Installed kernels: (could not determine on this distribution)"
+        else do
+          putStrLn "Installed kernels:"
+          mapM_ (putStrLn . ("  " ++)) installed
diff --git a/lib/Knives/NetMan.hs b/lib/Knives/NetMan.hs
new file mode 100644
--- /dev/null
+++ b/lib/Knives/NetMan.hs
@@ -0,0 +1,31 @@
+module Knives.NetMan where
+
+import Utils
+import CommandLine
+import Control.Monad (when)
+
+knifeNetMan :: NetManOptions -> IO ()
+knifeNetMan NetManOptions { activateNM
+                          , enableNM
+                          , deactivateNM
+                          , disableNM
+                          , reactivateNM}
+  | isExclusiveOr [ activateNM
+                  , enableNM
+                  , deactivateNM
+                  , disableNM
+                  , reactivateNM] = do
+      when activateNM   activate
+      when enableNM     enable
+      when deactivateNM deactivate
+      when disableNM    disable
+      when reactivateNM reactivate
+      return ()
+  | otherwise = putStrLn("You must specify one and only one option for NetMan.")
+  where
+    service    = "NetworkManager.service"
+    activate   = systemctl_ ["start",   service]
+    enable     = systemctl_ ["enable", "--now", service]
+    deactivate = systemctl_ ["stop",    service]
+    disable    = systemctl_ ["disable", "--now", service]
+    reactivate = systemctl_ ["restart", service]
diff --git a/lib/Knives/Sleep.hs b/lib/Knives/Sleep.hs
new file mode 100644
--- /dev/null
+++ b/lib/Knives/Sleep.hs
@@ -0,0 +1,17 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Knives.Sleep where
+
+import System.Process
+import CommandLine
+
+knifeSleep :: SleepOptions -> IO ()
+knifeSleep optsS = do
+  putStrLn $ "Put the machine to sleep." ++ show (secondsToSleep optsS)
+  case (secondsToSleep optsS) of
+    Just secs -> do putStrLn $ "sleep in " ++ show secs ++ " seconds."
+                    _n <- readProcess "sleep" [show secs] ""
+                    return ()
+    Nothing  ->  putStrLn "sleep immediatly"
+  _nn <-  readProcess "systemctl" ["suspend", "-i"] ""
+  return ()
diff --git a/lib/Knives/SysNet.hs b/lib/Knives/SysNet.hs
new file mode 100644
--- /dev/null
+++ b/lib/Knives/SysNet.hs
@@ -0,0 +1,46 @@
+module Knives.SysNet where
+
+import Utils
+import CommandLine
+import Control.Monad (when)
+
+knifeSysNet :: SysNetOptions -> IO ()
+knifeSysNet SysNetOptions { activateSN
+                          , enableSN
+                          , deactivateSN
+                          , disableSN
+                          , reactivateSN}
+  | isExclusiveOr [ activateSN
+                  , enableSN
+                  , deactivateSN
+                  , disableSN
+                  , reactivateSN] = do
+      when activateSN   activate
+      when enableSN     enable
+      when deactivateSN deactivate
+      when disableSN    disable
+      when reactivateSN reactivate
+  | otherwise = putStrLn("You must specify one and only one option for SysNet.")
+  where
+    socket  = "systemd-networkd.socket"
+    service = "systemd-networkd.service"
+    
+    activate   = do
+      systemctl_ ["start",   socket]
+      systemctl_ ["start",   service]
+
+    enable     = do
+      systemctl_ ["start",   socket]
+      systemctl_ ["enable", "--now",  service]
+
+    deactivate = do
+      systemctl_ ["stop",   socket]
+      systemctl_ ["stop",   service]
+ 
+    disable    = do
+      systemctl_ ["stop",   socket]
+      systemctl_ ["disable", "--now", service]
+ 
+    reactivate = do
+      systemctl_ ["restart",   socket]
+      systemctl_ ["restart",   service]
diff --git a/lib/Knives/Version.hs b/lib/Knives/Version.hs
new file mode 100644
--- /dev/null
+++ b/lib/Knives/Version.hs
@@ -0,0 +1,12 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Knives.Version where
+
+import CommandLine
+import qualified Paths_swiss_army_knife as SAK
+import Data.Version (showVersion)
+
+knifeVersion :: VersionOptions -> IO ()
+knifeVersion _ = do
+  putStrLn $ "Swiss Army Knife version: " ++ showVersion SAK.version
+  return ()
diff --git a/lib/Knives/WireGuard.hs b/lib/Knives/WireGuard.hs
new file mode 100644
--- /dev/null
+++ b/lib/Knives/WireGuard.hs
@@ -0,0 +1,108 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_GHC -Wno-unused-top-binds #-}
+
+module Knives.WireGuard where
+
+import Utils (systemctl)
+import CommandLine
+-- import System.IO
+-- import System.Process
+import System.Directory (getDirectoryContents)
+import System.FilePath (dropExtension, takeExtension)
+import Control.Monad (when)
+import Data.List
+
+knifeWireGuard :: WireGuardOptions -> IO ()
+knifeWireGuard WireGuardOptions { listWGs
+                                , activateWG
+                                , enableWG
+                                , deactivateWG
+                                , disableWG
+                                , reactivateWG}
+  | checkMutualExclusivity = do
+      wgs <- wgList
+      ss  <- wgStatus wgs
+      when listWGs $ putStrLn $ formatStatus ss
+      case (activateWG , enableWG , deactivateWG , disableWG , reactivateWG) of
+            (Just vpn  , Nothing  , False        , False     , False) -> activate vpn
+            (Nothing   , Just vpn , False        , False     , False) -> enable vpn
+            (Nothing   , Nothing  , True         , False     , False) -> deactivate
+            (Nothing   , Nothing  , False        , True      , False) -> disable
+            (Nothing   , Nothing  , False        , False     , True ) -> reactivate
+            _                        -> return ()
+  | otherwise = putStrLn "Options aside from --list or -l are mutually exclusive"
+  where
+    wgDir = "/etc/wireguard/"
+
+    wgList :: IO [String]
+    wgList = do
+      rawlist <- getDirectoryContents wgDir
+      let wgconf = filter (\f -> takeExtension f == ".conf") rawlist
+      let wgs    = map (\f -> dropExtension f) wgconf
+      return wgs
+      
+    wgStatus :: [String] -> IO [(String, String, String, Bool, Bool)]
+    wgStatus wgs = do
+      let quick = map (\w -> vpn2wg w) wgs
+      let cmd_a   = ["is-active"]  <> quick
+      let cmd_e   = ["is-enabled"] <> quick
+      stats_a <- systemctl cmd_a
+      stats_e <- systemctl cmd_e
+      return $ [(wg
+                , sact
+                , sena
+                , sact == "active"
+                , sena == "enabled") | (wg, sact, sena) <- zip3 wgs stats_a stats_e ]
+
+    formatStatus :: [(String, String, String, Bool, Bool)] -> String
+    formatStatus wss = intercalate "\n" [w <> ": " <> sa <> " " <> se |(w, sa, se, _, _) <- wss]
+
+    checkMutualExclusivity = check [ activateWG /= Nothing
+                                   , enableWG   /= Nothing
+                                   , deactivateWG
+                                   , disableWG
+                                   , reactivateWG]
+
+    check bs   = (length $ filter id bs) `elem` [0, 1]
+    vpn2wg vpn = "wg-quick@" <> vpn
+    
+    activate :: String -> IO ()
+    activate vpn = do
+      _ <- systemctl ["start", vpn2wg vpn]
+      return ()
+
+    enable :: String -> IO ()
+    enable vpn = do
+      _ <- systemctl ["enable", "--now", vpn2wg vpn]
+      return ()
+    
+    deactivate :: IO ()
+    deactivate   = do
+      awgs <- activeWG
+      mapM_ (\w -> systemctl $ ["stop"] <> [vpn2wg w]) awgs
+      return ()
+
+    disable :: IO ()
+    disable = do
+      awgs <- enabledWG
+      mapM_ (\w -> systemctl $ ["disable", "--now"] <> [vpn2wg w]) awgs
+      return ()
+
+
+    reactivate :: IO ()
+    reactivate   = do
+      awgs <- activeWG
+      mapM_ (\w -> systemctl $ ["restart"] <>  [vpn2wg w]) awgs
+      return()
+
+    activeWG :: IO [String]
+    activeWG = do
+      wgl <- wgList
+      wgs <- wgStatus wgl
+      return [s | (s, _, _, a, _) <- wgs, a]
+
+    enabledWG :: IO [String]
+    enabledWG = do
+      wgl <- wgList
+      wgs <- wgStatus wgl
+      return [s | (s, _, _, _, e) <- wgs, e]
diff --git a/lib/Knives/ZfsCheck.hs b/lib/Knives/ZfsCheck.hs
new file mode 100644
--- /dev/null
+++ b/lib/Knives/ZfsCheck.hs
@@ -0,0 +1,44 @@
+
+module Knives.ZfsCheck where
+
+import CommandLine
+import Data.List.Split (splitOn)
+import Data.Char (isSpace)
+import Data.List (isInfixOf)
+import qualified Data.ByteString.Lazy as LBS
+import qualified Data.ByteString.Lazy.Char8 as LBS8
+import System.Process
+
+
+knifeZfsCheck :: ZfsCheckOptions -> IO ()
+knifeZfsCheck _ = do
+  putStrLn $ "Check the version of archzfs with repo kernel version so you can determine if an upgrade is possible."
+
+  let url_zfs = "https://raw.githubusercontent.com/openzfs/zfs/master/META"
+
+  repo_version  <- readProcess "pacman" ["-Si", "linux"] "" -- | awk '/^Version/ { print $3 }'" ""
+  archzfs_version <- readProcess "curl" ["-sSL", url_zfs] ""
+  skernel <- readProcess "uname" ["-r"] ""
+  sinstalled <- readProcess "pacman" ["-Q", "linux"] ""
+
+  let repo    = splitAndTrim
+                $ LBS8.unpack
+                $ head [v | v <- LBS8.lines (LBS8.pack repo_version), (LBS8.pack "Version") `LBS.isPrefixOf` v]
+      archzfs = splitAndTrim
+                $ LBS8.unpack
+                $ head [v | v <- LBS8.lines (LBS8.pack archzfs_version), (LBS8.pack "Linux-Maximum:") `LBS.isPrefixOf` v]
+      running = trim skernel
+      installed = unwords
+                $ filter (not . ("linux" `isInfixOf`))
+                $ words
+                $ trim sinstalled
+
+  putStrLn $ "\n      repo: " ++ repo
+    ++ "\n       zfs: " ++ archzfs
+    ++ "\n   running: " ++ running
+    ++ "\n installed: " ++ installed
+
+  where
+    splitAndTrim line = trim $ last $ splitOn ":" line
+    trim = f . f
+      where f = reverse . dropWhile isSpace
diff --git a/lib/Utils.hs b/lib/Utils.hs
new file mode 100644
--- /dev/null
+++ b/lib/Utils.hs
@@ -0,0 +1,32 @@
+module Utils where
+
+import System.IO
+import System.Process
+-- import System.Directory (getDirectoryContents)
+-- import System.FilePath (dropExtension, takeExtension)
+
+-- WARN: Cannot handle inflinite lists.
+isExclusiveOr :: [Bool] -> Bool
+isExclusiveOr bs = (length $ filter id bs) == 1
+
+-- WARN: Cannot handle inflinite lists.
+isExclusiveOrNone :: [Bool] -> Bool
+isExclusiveOrNone bs = (length $ filter id bs) `elem` exorno
+  where 
+   exorno = [0, 1]
+
+systemctl :: [String] -> IO [String]
+systemctl parms = systemG $ "sudo" : "systemctl" : parms  
+      
+systemctl_ :: [String] -> IO ()
+systemctl_ parms = do
+  _ <- systemG $ "sudo" : "systemctl" : parms
+  return ()
+
+systemG :: [String] -> IO [String]
+systemG [] = return []
+systemG (cmd:parms) = do
+  (_, Just hout, _, _) <- createProcess (proc cmd parms) { std_out = CreatePipe }
+  out <- hGetContents hout
+  return $ lines out
+      
diff --git a/swiss-army-knife.cabal b/swiss-army-knife.cabal
new file mode 100644
--- /dev/null
+++ b/swiss-army-knife.cabal
@@ -0,0 +1,91 @@
+cabal-version: 2.4
+name:           swiss-army-knife
+version:        1.1.0.4
+synopsis:       A collection of small tools to make life a bit easier for the command-line.
+category:       System
+description:    Please see the README on GitHub at <https://github.com/flajann2/swiss-army-knife#readme>
+homepage:       https://github.com/flajann2/swiss-army-knife#readme
+bug-reports:    https://github.com/flajann2/swiss-army-knife/issues
+author:         Fred Mitchell
+maintainer:     fred.mitchell@atomlogik.de
+copyright:      2024-2026 Fred Mitchell & Atomlogik
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.org
+    CHANGELOG.org
+    LICENSE
+extra-doc-files:
+    README.org
+    CHANGELOG.org
+    CHANGELOG.md
+    
+tested-with: GHC == 9.14.1
+
+source-repository head
+  type: git
+  location: https://github.com/flajann2/swiss-army-knife
+
+library
+  exposed-modules:
+      CommandLine
+      Utils
+      Knives
+      Knives.Version
+      Knives.Sleep
+      Knives.ExtIP
+      Knives.WireGuard
+      Knives.ZfsCheck
+      Knives.Kernel
+      Knives.SysNet
+      Knives.NetMan
+  autogen-modules:
+      Paths_swiss_army_knife
+  other-modules:
+      Paths_swiss_army_knife
+  hs-source-dirs:
+      lib
+  ghc-options: -Wall
+  build-depends:
+      aeson                   >= 2.1     && < 2.4
+    , base                    >= 4.18    && < 5
+    , bytestring              >= 0.11    && < 0.13
+    , directory               >= 1.3.8   && < 1.4
+    , filepath                >= 1.4.2   && < 1.6
+    , http-conduit            >= 2.3.8   && < 2.4
+    , optparse-applicative    >= 0.18    && < 0.20
+    , process                 >= 1.6.18  && < 1.7
+    , resolv                  >= 0.1.2   && < 0.3
+    , split                   >= 0.2.5   && < 0.3
+  default-language: GHC2021
+
+executable sak
+  main-is: SwissArmyKnife.hs
+  autogen-modules:
+      Paths_swiss_army_knife
+  other-modules:
+      Paths_swiss_army_knife
+  hs-source-dirs:
+      app
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base                    >= 4.18    && < 5
+    , optparse-applicative    >= 0.18    && < 0.20
+    , swiss-army-knife
+  default-language: GHC2021
+
+test-suite swiss-army-knife-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  autogen-modules:
+      Paths_swiss_army_knife
+  other-modules:
+      Paths_swiss_army_knife
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base                    >= 4.18    && < 5
+    , swiss-army-knife
+  default-language: GHC2021
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,2 @@
+main :: IO ()
+main = putStrLn "Test suite not yet implemented"
