diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,10 @@
 # Revision history for stack-clean-old-work
 
+## 0.2.1 (2020-11-11)
+- remove-earlier-minor can now take a major version
+- major internal refactors with VersionSnapshots type
+- split code into modules
+
 ## 0.2 (2020-10-26)
 - add --dryrun to all remove commands
 - remove-earlier-minor subcommands to purge for previous ghc minor versions
diff --git a/Main.hs b/Main.hs
deleted file mode 100644
--- a/Main.hs
+++ /dev/null
@@ -1,278 +0,0 @@
-{-# LANGUAGE CPP #-}
-
-import Control.Monad.Extra
-import Data.List.Extra
-import Data.Maybe
-import Data.Version.Extra
-import SimpleCmd
-#if MIN_VERSION_simple_cmd(0,2,1)
-  hiding (ifM, whenM)
-#endif
-import SimpleCmdArgs
-import System.Directory
-import System.FilePath
-import System.IO (BufferMode(NoBuffering), hSetBuffering, stdout)
-import Text.Printf
-
-import Paths_stack_clean_old (version)
-
-main :: IO ()
-main = do
-  hSetBuffering stdout NoBuffering
-  simpleCmdArgs (Just version) "Stack clean up tool"
-    "Cleans away old stack-work builds (and pending: stack snapshots) to recover diskspace." $
-    subcommands
-    [ Subcommand "project" "Commands for project .stack-work builds" $
-      subcommands
-      [ Subcommand "size" "Total size of project's .stack-work/install" $
-        sizeStackWork <$> dirOption <*> notHumanOpt
-      , Subcommand "list" "List builds in .stack-work/install per ghc version" $
-        listGhcSnapshots . setStackWorkDir <$> dirOption <*> optional ghcVerArg
-      , Subcommand "remove-version" "Remove builds in .stack-work/install for a ghc version" $
-        cleanGhcSnapshots . setStackWorkDir <$> dirOption <*> dryrun <*> ghcVerArg
-      , Subcommand "remove-earlier-minor" "Remove builds in .stack-work/install for previous ghc minor versions" $
-        cleanMinorSnapshots . setStackWorkDir <$> dirOption <*> dryrun <*> optional ghcVerArg
-      , Subcommand "remove-older" "Purge older builds in .stack-work/install" $
-        cleanOldStackWork <$> dryrun <*> keepOption <*> optional (strArg "PROJECTDIR")
-      ]
-    , Subcommand "snapshots" "Commands for ~/.stack/snapshots" $
-      subcommands
-      [ Subcommand "size" "Total size of all stack build snapshots" $
-        sizeSnapshots <$> notHumanOpt
-      , Subcommand "list" "List build snapshots per ghc version" $
-        listGhcSnapshots setStackSnapshotsDir <$> optional ghcVerArg
-      , Subcommand "remove-version" "Remove build snapshots for a ghc version" $
-        cleanGhcSnapshots setStackSnapshotsDir <$> dryrun <*> ghcVerArg
-      , Subcommand "remove-earlier-minor" "Remove build snapshots for previous ghc minor versions" $
-        cleanMinorSnapshots setStackSnapshotsDir <$> dryrun <*> optional ghcVerArg
-      ]
-    , Subcommand "ghc" "Commands on stack's ghc compiler installations" $
-      subcommands
-      [ Subcommand "size" "Total size of installed stack ghc compilers" $
-        sizeGhcInstalls <$> notHumanOpt
-      , Subcommand "list" "List installed stack ghc compiler versions" $
-        listGhcInstallation <$> optional ghcVerArg
-      , Subcommand "remove-version" "Remove installation of a stack ghc compiler version" $
-        removeGhcVersionInstallation <$> dryrun <*> ghcVerArg
-      , Subcommand "remove-earlier-minor" "Remove installations of stack ghc previous minor versions" $
-        removeGhcMinorInstallation <$> dryrun <*> optional ghcVerArg
-      ]
-    ]
-  where
-    dryrun = switchWith 'n' "dryrun" "Show what would be done, without removing"
-
-    notHumanOpt = switchWith 'H' "not-human-size" "Do not use du --human-readable"
-
-    dirOption = optional (strOptionWith 'd' "dir" "PROJECTDIR" "Path to project")
-    ghcVerArg = readVersion <$> strArg "GHCVER"
-
-    keepOption = positive <$> optionalWith auto 'k' "keep" "INT" "number of project builds per ghc version [default 5]" 5
-
-    positive :: Int -> Int
-    positive n = if n > 0 then n else error' "Must be positive integer"
-
-stackWorkInstall :: FilePath
-stackWorkInstall = ".stack-work/install"
-
-sizeStackWork :: Maybe FilePath -> Bool -> IO ()
-sizeStackWork mdir nothuman = do
-  let path = fromMaybe "" mdir </> stackWorkInstall
-  whenM (doesDirectoryExist path) $
-    cmd_ "du" $ ["-h" | not nothuman] ++ ["-s", path]
-
-printTotalGhcSize :: [FilePath] -> IO ()
-printTotalGhcSize ds = do
-  total <- head . words . last <$> cmdLines "du" ("-shc":ds)
-  printf "%4s  %-6s (%d dirs)\n" total ((takeFileName . head) ds) (length ds)
-
-setStackWorkDir :: Maybe FilePath -> IO ()
-setStackWorkDir mdir = do
-  whenJust mdir $ \ dir -> setCurrentDirectory dir
-  switchToSystemDirUnder stackWorkInstall
-
-setStackSnapshotsDir :: IO ()
-setStackSnapshotsDir = do
-  home <- getHomeDirectory
-  switchToSystemDirUnder $ home </> ".stack/snapshots"
-
-getSnapshotDirs :: IO [FilePath]
-getSnapshotDirs = do
-  lines <$> shell ( unwords $ "ls" : ["-d", "*/*"])
-
-takeGhcSnapshots :: Version -> [FilePath] -> [FilePath]
-takeGhcSnapshots ghcver =
-  map takeDirectory . filter ((== ghcver) . (if isMajorVersion ghcver then majorVersion else id) . readVersion . takeFileName)
-
-sizeSnapshots :: Bool -> IO ()
-sizeSnapshots nothuman = do
-  home <- getHomeDirectory
-  cmd_ "du" $ ["-h" | not nothuman] ++ ["-s", home </> ".stack/snapshots"]
-
-listGhcSnapshots :: IO () -> Maybe Version -> IO ()
-listGhcSnapshots setdir mghcver = do
-  setdir
-  dirs <- sortOn (readVersion . takeFileName) <$> getSnapshotDirs
-  case mghcver of
-    Nothing -> do
-      let ghcs = groupOn takeFileName dirs
-      mapM_ printTotalGhcSize ghcs
-    Just ghcver | isMajorVersion ghcver -> do
-      let ghcs = groupOn takeFileName $ filter ((== ghcver) . majorVersion . readVersion . takeFileName) dirs
-      mapM_ printTotalGhcSize ghcs
-    Just ghcver -> do
-      let ds = takeGhcSnapshots ghcver dirs
-      unless (null ds) $
-        cmd_ "du" ("-shc":ds)
-
-doRemoveDirectory :: Bool -> FilePath -> IO ()
-doRemoveDirectory dryrun dir =
-  unless dryrun $
-  removeDirectoryRecursive dir
-
-cleanGhcSnapshots :: IO () -> Bool -> Version -> IO ()
-cleanGhcSnapshots setDir dryrun ghcver = do
-  setDir
-  dirs <- takeGhcSnapshots ghcver <$> getSnapshotDirs
-  unless (dryrun || null dirs) $
-    when (isMajorVersion ghcver) $ do
-    putStr $ "Press Enter to delete all " ++ showVersion ghcver ++ " builds: "
-    void getLine
-  unless (null dirs) $ do
-    mapM_ (doRemoveDirectory dryrun) dirs
-    putStrLn $ show (length dirs) ++ " snapshots removed for " ++ showVersion ghcver
-
-cleanMinorSnapshots :: IO () -> Bool -> Maybe Version -> IO ()
-cleanMinorSnapshots setDir dryrun mghcver = do
-  setDir
-  dirs <- sortOn (readVersion . takeFileName) <$> getSnapshotDirs
-  case mghcver of
-    Nothing -> do
-      let ghcs = map (groupOn takeFileName) $ groupOn (majorVersion . readVersion) dirs
-      forM_ ghcs $ \ gmajor ->
-        when (length gmajor > 1) $
-        forM_ (init gmajor) $ \ gminor -> do
-          mapM_ (doRemoveDirectory dryrun) gminor
-          putStrLn $ show (length gminor) ++ " snapshots removed for " ++ takeFileName (head gminor)
-    Just ghcver -> do
-      let major =
-            if length (versionBranch ghcver) == 2
-            then majorVersion ghcver
-            else error' "Please specify minor version X.Y.Z"
-          gmajor = groupOn takeFileName $ filter (olderMinor major ghcver) dirs
-      when (length gmajor > 1) $
-        forM_ (init gmajor) $ \ gminor -> do
-          mapM_ (doRemoveDirectory dryrun) gminor
-          putStrLn $ show (length gminor) ++ " snapshots removed for " ++ takeFileName (head gminor)
-  where
-    olderMinor :: Version -> Version -> FilePath -> Bool
-    olderMinor major ghcver d =
-      ((== major) . majorVersion . readVersion) d &&
-      ((< ghcver) . readVersion . takeFileName) d
-
-majorVersion :: Version -> Version
-majorVersion ver =
-  let vernums = versionBranch ver in
-    case length vernums of
-      2 -> ver
-      3 -> (makeVersion . init) vernums
-      _ -> error' $ "Bad ghc version " ++ showVersion ver
-
-switchToSystemDirUnder :: FilePath -> IO ()
-switchToSystemDirUnder dir = do
-  ifM (doesDirectoryExist dir)
-    (setCurrentDirectory dir)
-    (error' $ dir ++ "not found")
-  systems <- listDirectory "."
-  let system = case systems of
-        [] -> error' $ "No OS system in " ++ dir
-        [sys] -> sys
-        _ -> error' "More than one OS systems found " ++ dir ++ " (unsupported)"
-  setCurrentDirectory system
-
-cleanOldStackWork :: Bool -> Int -> Maybe FilePath -> IO ()
-cleanOldStackWork dryrun keep mdir = do
-  setStackWorkDir mdir
-  dirs <- sortOn takeFileName . lines <$> shell ( unwords $ "ls" : ["-d", "*/*"])
-  let ghcs = groupOn takeFileName dirs
-  mapM_ removeOlder ghcs
-  where
-    removeOlder :: [FilePath] -> IO ()
-    removeOlder dirs = do
-      let ghcver = (takeFileName . head) dirs
-      oldfiles <- drop keep . reverse <$> sortedByAge
-      mapM_ (doRemoveDirectory dryrun . takeDirectory) oldfiles
-      unless (null oldfiles) $
-        putStrLn $ show (length oldfiles) ++ " dirs removed for " ++ ghcver
-      where
-        sortedByAge = do
-          fileTimes <- mapM newestTimeStamp dirs
-          return $ map fst $ sortOn snd fileTimes
-
-        newestTimeStamp dir = do
-          withCurrentDirectory dir $ do
-            files <- listDirectory "."
-            timestamp <- maximum <$> mapM getModificationTime files
-            return (dir, timestamp)
-
-stackProgramsDir :: FilePath
-stackProgramsDir = ".stack/programs"
-
-sizeGhcInstalls :: Bool -> IO ()
-sizeGhcInstalls nothuman = do
-  home <- getHomeDirectory
-  cmd_ "du" $ ["-h" | not nothuman] ++ ["-s", home </> stackProgramsDir]
-
-setStackProgramsDir :: IO ()
-setStackProgramsDir = do
-  home <- getHomeDirectory
-  switchToSystemDirUnder $ home </> stackProgramsDir
-
-getGhcInstallDirs :: IO [FilePath]
-getGhcInstallDirs = do
-  setStackProgramsDir
-  listDirectory "." >>= fmap sort . filterM doesDirectoryExist
-
-ghcInstallVersion :: FilePath -> Version
-ghcInstallVersion =
-  readVersion . takeWhileEnd (/= '-')
-
-isMajorVersion :: Version -> Bool
-isMajorVersion ver =
-  majorVersion ver == ver
-
-listGhcInstallation :: Maybe Version -> IO ()
-listGhcInstallation mghcver = do
-  dirs <- sortOn ghcInstallVersion <$> getGhcInstallDirs
-  mapM_ putStrLn $ case mghcver of
-    Nothing -> dirs
-    Just ghcver -> filter ((== ghcver) . (if isMajorVersion ghcver then majorVersion else id) . ghcInstallVersion) dirs
-
-removeGhcVersionInstallation :: Bool -> Version -> IO ()
-removeGhcVersionInstallation dryrun ghcver = do
-  installs <- filter ((== ghcver) . (if isMajorVersion ghcver then majorVersion else id) . ghcInstallVersion) <$> getGhcInstallDirs
-  case installs of
-    [] -> error' $ "stack ghc compiler version " ++ showVersion ghcver ++ " not found"
-    [g] | not (isMajorVersion ghcver) -> doRemoveGhcVersion dryrun g
-    gs -> if isMajorVersion ghcver then do
-      putStr $ "Press Enter to delete all stack " ++ showVersion ghcver ++ " installations: "
-      void getLine
-      mapM_ (doRemoveGhcVersion dryrun) gs
-      else error' "more than one match found!!"
-
-removeGhcMinorInstallation :: Bool -> Maybe Version -> IO ()
-removeGhcMinorInstallation dryrun mghcver = do
-  dirs <- sortOn ghcInstallVersion <$> getGhcInstallDirs
-  case mghcver of
-    Nothing -> do
-      let majors = groupOn (majorVersion . ghcInstallVersion) dirs
-      forM_ majors $ \ minors ->
-        forM_ (init minors) $ doRemoveGhcVersion dryrun
-    Just ghcver -> do
-      let minors = filter ((== majorVersion ghcver) . majorVersion . readVersion) dirs
-      forM_ (init minors) $ doRemoveGhcVersion dryrun
-
-doRemoveGhcVersion :: Bool -> FilePath -> IO ()
-doRemoveGhcVersion dryrun ghcinst = do
-  doRemoveDirectory dryrun ghcinst
-  unless dryrun (removeFile (ghcinst <.> "installed"))
-  putStrLn $ ghcinst ++ " removed"
diff --git a/src/Directories.hs b/src/Directories.hs
new file mode 100644
--- /dev/null
+++ b/src/Directories.hs
@@ -0,0 +1,36 @@
+module Directories (
+  getStackSubdir,
+  globDirs,
+  switchToSystemDirUnder
+  )
+where
+
+import Control.Monad.Extra
+import Data.List.Extra
+import SimpleCmd (error')
+import System.Directory
+import System.FilePath
+import System.FilePath.Glob
+
+globDirs :: String -> IO [FilePath]
+globDirs pat = do
+  map (dropPrefix "./") <$> namesMatching (pat ++ "/")
+
+getStackSubdir :: FilePath -> IO FilePath
+getStackSubdir subdir = do
+  home <- getHomeDirectory
+  return $ home </> ".stack" </> subdir
+
+switchToSystemDirUnder :: FilePath -> IO ()
+switchToSystemDirUnder dir = do
+  ifM (doesDirectoryExist dir)
+    (setCurrentDirectory dir)
+    (error' $ dir ++ "not found")
+  systems <- listDirectory "."
+  -- FIXME be more precise/check "system" dirs
+  -- eg 64bit intel Linux: x86_64-linux-tinfo6
+  let system = case systems of
+        [] -> error' $ "No OS system in " ++ dir
+        [sys] -> sys
+        _ -> error' "More than one OS systems found " ++ dir ++ " (unsupported)"
+  setCurrentDirectory system
diff --git a/src/GHC.hs b/src/GHC.hs
new file mode 100644
--- /dev/null
+++ b/src/GHC.hs
@@ -0,0 +1,82 @@
+module GHC (
+  listGhcInstallation,
+  removeGhcMinorInstallation,
+  removeGhcVersionInstallation,
+  sizeGhcInstalls
+  )
+where
+
+import Control.Monad.Extra
+import Data.List.Extra
+import Data.Version.Extra
+import SimpleCmd
+import System.FilePath
+
+import Directories (getStackSubdir, globDirs, switchToSystemDirUnder)
+import qualified Remove
+import Versions
+
+getStackProgramsDir :: IO FilePath
+getStackProgramsDir =
+  getStackSubdir "programs"
+
+sizeGhcInstalls :: Bool -> IO ()
+sizeGhcInstalls nothuman = do
+  programs <- getStackProgramsDir
+  cmd_ "du" $ ["-h" | not nothuman] ++ ["-s", programs]
+
+setStackProgramsDir :: IO ()
+setStackProgramsDir =
+  getStackProgramsDir >>= switchToSystemDirUnder
+
+getGhcInstallDirs :: Maybe Version -> IO [FilePath]
+getGhcInstallDirs mghcver = do
+  setStackProgramsDir
+  sortOn ghcInstallVersion <$> globDirs matchVersion
+  where
+    matchVersion =
+      case mghcver of
+        Nothing -> "*"
+        Just ver ->
+          "*-" ++ showVersion ver ++ if isMajorVersion ver then "*" else ""
+
+ghcInstallVersion :: FilePath -> Version
+ghcInstallVersion =
+  readVersion . takeWhileEnd (/= '-')
+
+listGhcInstallation :: Maybe Version -> IO ()
+listGhcInstallation mghcver = do
+  dirs <- getGhcInstallDirs mghcver
+  mapM_ (putStrLn . showVersion . ghcInstallVersion) $ case mghcver of
+    Nothing -> dirs
+    Just ghcver -> filter ((== ghcver) . (if isMajorVersion ghcver then majorVersion else id) . ghcInstallVersion) dirs
+
+removeGhcVersionInstallation :: Bool -> Version -> IO ()
+removeGhcVersionInstallation dryrun ghcver = do
+  installs <- getGhcInstallDirs (Just ghcver)
+  case installs of
+    [] -> error' $ "stack ghc compiler version " ++ showVersion ghcver ++ " not found"
+    [g] | not (isMajorVersion ghcver) -> doRemoveGhcVersion dryrun g
+    gs -> if isMajorVersion ghcver then do
+      putStr $ "Press Enter to delete all stack " ++ showVersion ghcver ++ " installations: "
+      void getLine
+      mapM_ (doRemoveGhcVersion dryrun) gs
+      else error' "more than one match found!!"
+
+removeGhcMinorInstallation :: Bool -> Maybe Version -> IO ()
+removeGhcMinorInstallation dryrun mghcver = do
+  dirs <- getGhcInstallDirs (majorVersion <$> mghcver)
+  case mghcver of
+    Nothing -> do
+      let majors = groupOn (majorVersion . ghcInstallVersion) dirs
+      forM_ majors $ \ minors ->
+        forM_ (init minors) $ doRemoveGhcVersion dryrun
+    Just ghcver -> do
+      let minors = filter ((< ghcver) . ghcInstallVersion) dirs
+      forM_ minors $ doRemoveGhcVersion dryrun
+
+doRemoveGhcVersion :: Bool -> FilePath -> IO ()
+doRemoveGhcVersion dryrun ghcinst = do
+  Remove.doRemoveDirectory dryrun ghcinst
+  unless dryrun (Remove.removeFile (ghcinst <.> "installed"))
+  putStrLn $ ghcinst ++ " removed"
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,65 @@
+module Main (main) where
+
+import Data.Version.Extra
+import SimpleCmd
+import SimpleCmdArgs
+import System.IO (BufferMode(NoBuffering), hSetBuffering, stdout)
+
+import GHC
+import Paths_stack_clean_old (version)
+import Snapshots
+
+main :: IO ()
+main = do
+  hSetBuffering stdout NoBuffering
+  simpleCmdArgs (Just version) "Stack clean up tool"
+    "Cleans away old stack-work builds (and pending: stack snapshots) to recover diskspace." $
+    subcommands
+    [ Subcommand "project" "Commands for project .stack-work builds" $
+      subcommands
+      [ Subcommand "size" "Total size of project's .stack-work/install" $
+        sizeStackWork <$> dirOption <*> notHumanOpt
+      , Subcommand "list" "List builds in .stack-work/install per ghc version" $
+        listGhcSnapshots . setStackWorkDir <$> dirOption <*> optional ghcVerArg
+      , Subcommand "remove-version" "Remove builds in .stack-work/install for a ghc version" $
+        cleanGhcSnapshots . setStackWorkDir <$> dirOption <*> dryrun <*> ghcVerArg
+      , Subcommand "remove-earlier-minor" "Remove builds in .stack-work/install for previous ghc minor versions" $
+        cleanMinorSnapshots . setStackWorkDir <$> dirOption <*> dryrun <*> optional ghcVerArg
+      , Subcommand "remove-older" "Purge older builds in .stack-work/install" $
+        cleanOldStackWork <$> dryrun <*> keepOption <*> optional (strArg "PROJECTDIR")
+      ]
+    , Subcommand "snapshots" "Commands for ~/.stack/snapshots" $
+      subcommands
+      [ Subcommand "size" "Total size of all stack build snapshots" $
+        sizeSnapshots <$> notHumanOpt
+      , Subcommand "list" "List build snapshots per ghc version" $
+        listGhcSnapshots setStackSnapshotsDir <$> optional ghcVerArg
+      , Subcommand "remove-version" "Remove build snapshots for a ghc version" $
+        cleanGhcSnapshots setStackSnapshotsDir <$> dryrun <*> ghcVerArg
+      , Subcommand "remove-earlier-minor" "Remove build snapshots for previous ghc minor versions" $
+        cleanMinorSnapshots setStackSnapshotsDir <$> dryrun <*> optional ghcVerArg
+      ]
+    , Subcommand "ghc" "Commands on stack's ghc compiler installations" $
+      subcommands
+      [ Subcommand "size" "Total size of installed stack ghc compilers" $
+        sizeGhcInstalls <$> notHumanOpt
+      , Subcommand "list" "List installed stack ghc compiler versions" $
+        listGhcInstallation <$> optional ghcVerArg
+      , Subcommand "remove-version" "Remove installation of a stack ghc compiler version" $
+        removeGhcVersionInstallation <$> dryrun <*> ghcVerArg
+      , Subcommand "remove-earlier-minor" "Remove installations of stack ghc previous minor versions" $
+        removeGhcMinorInstallation <$> dryrun <*> optional ghcVerArg
+      ]
+    ]
+  where
+    dryrun = switchWith 'n' "dryrun" "Show what would be done, without removing"
+
+    notHumanOpt = switchWith 'H' "not-human-size" "Do not use du --human-readable"
+
+    dirOption = optional (strOptionWith 'd' "dir" "PROJECTDIR" "Path to project")
+    ghcVerArg = readVersion <$> strArg "GHCVER"
+
+    keepOption = positive <$> optionalWith auto 'k' "keep" "INT" "number of project builds per ghc version [default 5]" 5
+
+    positive :: Int -> Int
+    positive n = if n > 0 then n else error' "Must be positive integer"
diff --git a/src/Remove.hs b/src/Remove.hs
new file mode 100644
--- /dev/null
+++ b/src/Remove.hs
@@ -0,0 +1,13 @@
+module Remove (
+  doRemoveDirectory,
+  removeFile
+  )
+where
+
+import Control.Monad
+import System.Directory
+
+doRemoveDirectory :: Bool -> FilePath -> IO ()
+doRemoveDirectory dryrun dir =
+  unless dryrun $
+  removeDirectoryRecursive dir
diff --git a/src/Snapshots.hs b/src/Snapshots.hs
new file mode 100644
--- /dev/null
+++ b/src/Snapshots.hs
@@ -0,0 +1,166 @@
+{-# LANGUAGE CPP #-}
+
+module Snapshots (
+  --getSnapshotDirs
+  --, VersionSnapshots(..)
+  cleanGhcSnapshots,
+  cleanMinorSnapshots,
+  cleanOldStackWork,
+  listGhcSnapshots,
+  setStackSnapshotsDir,
+  setStackWorkDir,
+  sizeSnapshots,
+  sizeStackWork
+  )
+where
+
+import Control.Monad.Extra
+import Data.List.Extra
+import Data.Maybe
+import Data.Version.Extra
+import SimpleCmd
+#if MIN_VERSION_simple_cmd(0,2,1)
+  hiding (whenM)
+#endif
+import System.Directory hiding (removeDirectoryRecursive, removeFile)
+import System.FilePath
+import Text.Printf
+
+import Directories (globDirs, getStackSubdir, switchToSystemDirUnder)
+import qualified Remove
+import Versions
+
+data SnapshotInstall =
+  SnapInst { snapHash :: String,
+             snapGHC :: Version}
+  deriving Eq
+
+instance Ord SnapshotInstall where
+  compare s1 s2 = compare (snapGHC s1) (snapGHC s2)
+
+data VersionSnapshots =
+  VersionSnaps {snapsVersion :: Version,
+                snapsHashes :: [String]}
+  deriving Eq
+
+instance Ord VersionSnapshots where
+  compare s1 s2 = compare (snapsVersion s1) (snapsVersion s2)
+
+instance Show VersionSnapshots where
+  show (VersionSnaps ver snaps) = showVersion ver ++ ":" ++ show snaps
+
+readVersionedSnaps :: [FilePath] -> [VersionSnapshots]
+readVersionedSnaps snaps =
+  let ghcs = (groupOn snapGHC . sort) (map readSnapshot snaps)
+  in map installVerSnaps ghcs
+  where
+    installVerSnaps :: [SnapshotInstall] -> VersionSnapshots
+    installVerSnaps versnaps =
+      let ver = snapGHC (head versnaps) in
+        VersionSnaps ver (map snapHash versnaps)
+
+    readSnapshot :: FilePath -> SnapshotInstall
+    readSnapshot pth =
+      let (hash,ver) = splitFileName pth
+      -- remove trailing '/'
+      in SnapInst (init hash) (readVersion ver)
+
+getSnapshotDirs :: Maybe Version -> IO [VersionSnapshots]
+getSnapshotDirs mghcver = do
+  let pat = maybe "*" versionMatch mghcver
+  readVersionedSnaps <$> globDirs ("*" </> pat)
+  where
+    versionMatch :: Version -> String
+    versionMatch ghcver =
+      showVersion ghcver ++ if isMajorVersion ghcver then ".*" else ""
+
+sizeSnapshots :: Bool -> IO ()
+sizeSnapshots nothuman = do
+  snapshots <- getStackSubdir "snapshots"
+  cmd_ "du" $ ["-h" | not nothuman] ++ ["-s", snapshots]
+
+listGhcSnapshots :: IO () -> Maybe Version -> IO ()
+listGhcSnapshots setdir mghcver = do
+  setdir
+  ghcs <- getSnapshotDirs mghcver
+  mapM_ printTotalGhcSize ghcs
+
+removeVersionSnaps :: Bool -> VersionSnapshots -> IO ()
+removeVersionSnaps dryrun versnap = do
+  let dirs = snapsHashes versnap
+  mapM_ (Remove.doRemoveDirectory dryrun) dirs
+  putStrLn $ show (length dirs) ++ " snapshots removed for " ++ showVersion (snapsVersion versnap)
+
+cleanGhcSnapshots :: IO () -> Bool -> Version -> IO ()
+cleanGhcSnapshots setDir dryrun ghcver = do
+  setDir
+  ghcs <- getSnapshotDirs (Just ghcver)
+  when (isMajorVersion ghcver) $ do
+    putStr $ "Press Enter to delete all " ++ showVersion ghcver ++ " builds: "
+    void getLine
+  mapM_ (removeVersionSnaps dryrun) ghcs
+
+cleanMinorSnapshots :: IO () -> Bool -> Maybe Version -> IO ()
+cleanMinorSnapshots setDir dryrun mghcver = do
+  setDir
+  ghcs <- getSnapshotDirs (majorVersion <$> mghcver)
+  case mghcver of
+    Nothing -> do
+      let majors = groupOn (majorVersion . snapsVersion) ghcs
+      forM_ majors $ \ gmajor ->
+        when (length gmajor > 1) $
+        mapM_ (removeVersionSnaps dryrun) (init gmajor)
+    Just ghcver -> do
+      let newestMinor = if isMajorVersion ghcver
+                        then (snapsVersion . last) ghcs
+                        else ghcver
+          gminors = filter ((< newestMinor) . snapsVersion) ghcs
+      mapM_ (removeVersionSnaps dryrun) gminors
+
+cleanOldStackWork :: Bool -> Int -> Maybe FilePath -> IO ()
+cleanOldStackWork dryrun keep mdir = do
+  setStackWorkDir mdir
+  dirs <- sortOn takeFileName . lines <$> shell ( unwords $ "ls" : ["-d", "*/*"])
+  let ghcs = groupOn takeFileName dirs
+  mapM_ removeOlder ghcs
+  where
+    removeOlder :: [FilePath] -> IO ()
+    removeOlder dirs = do
+      let ghcver = (takeFileName . head) dirs
+      oldfiles <- drop keep . reverse <$> sortedByAge
+      mapM_ (Remove.doRemoveDirectory dryrun . takeDirectory) oldfiles
+      unless (null oldfiles) $
+        putStrLn $ show (length oldfiles) ++ " dirs removed for " ++ ghcver
+      where
+        sortedByAge = do
+          fileTimes <- mapM newestTimeStamp dirs
+          return $ map fst $ sortOn snd fileTimes
+
+        newestTimeStamp dir = do
+          withCurrentDirectory dir $ do
+            files <- listDirectory "."
+            timestamp <- maximum <$> mapM getModificationTime files
+            return (dir, timestamp)
+
+stackWorkInstall :: FilePath
+stackWorkInstall = ".stack-work/install"
+
+sizeStackWork :: Maybe FilePath -> Bool -> IO ()
+sizeStackWork mdir nothuman = do
+  let path = fromMaybe "" mdir </> stackWorkInstall
+  whenM (doesDirectoryExist path) $
+    cmd_ "du" $ ["-h" | not nothuman] ++ ["-s", path]
+
+printTotalGhcSize :: VersionSnapshots -> IO ()
+printTotalGhcSize versnaps = do
+  total <- head . words . last <$> cmdLines "du" ("-shc":snapsHashes versnaps)
+  printf "%4s  %-6s (%d dirs)\n" total ((showVersion . snapsVersion) versnaps) (length (snapsHashes versnaps))
+
+setStackWorkDir :: Maybe FilePath -> IO ()
+setStackWorkDir mdir = do
+  whenJust mdir $ \ dir -> setCurrentDirectory dir
+  switchToSystemDirUnder stackWorkInstall
+
+setStackSnapshotsDir :: IO ()
+setStackSnapshotsDir = do
+  getStackSubdir "snapshots" >>= switchToSystemDirUnder
diff --git a/src/Versions.hs b/src/Versions.hs
new file mode 100644
--- /dev/null
+++ b/src/Versions.hs
@@ -0,0 +1,19 @@
+module Versions (
+  isMajorVersion,
+  majorVersion
+  )
+where
+
+import Data.Version
+
+majorVersion :: Version -> Version
+majorVersion ver =
+  let vernums = versionBranch ver in
+    case length vernums of
+      2 -> ver
+      3 -> (makeVersion . init) vernums
+      _ -> error $ "Bad ghc version " ++ showVersion ver
+
+isMajorVersion :: Version -> Bool
+isMajorVersion ver =
+  majorVersion ver == ver
diff --git a/stack-clean-old.cabal b/stack-clean-old.cabal
--- a/stack-clean-old.cabal
+++ b/stack-clean-old.cabal
@@ -1,5 +1,5 @@
 name:                stack-clean-old
-version:             0.2
+version:             0.2.1
 synopsis:            Clean away old stack build artefacts
 description:
         A tool for cleaning away old .stack snapshots and .stack-work builds
@@ -24,11 +24,17 @@
 executable stack-clean-old
   main-is:             Main.hs
   other-modules:       Paths_stack_clean_old
---  hs-source-dirs:      app
+                       Directories
+                       GHC
+                       Remove
+                       Snapshots
+                       Versions
+  hs-source-dirs:      src
   build-depends:       base < 5
                      , directory >= 1.2.5
                      , extra >= 1.4.3
                      , filepath
+                     , filemanip
                      , simple-cmd >= 0.1.4
                      , simple-cmd-args >= 0.1.2
   default-language:    Haskell2010
