diff --git a/TODO.txt b/TODO.txt
deleted file mode 100644
--- a/TODO.txt
+++ /dev/null
@@ -1,91 +0,0 @@
-Testing:
-
-The following Lint has a strict parser
-  http://www.javascriptlint.com/online_lint.php
-
-This one does not
-  http://www.jslint.com/
-
----
-
-Language reference https://developer.mozilla.org/en/JavaScript/Reference
-http://msdn.microsoft.com/en-us/library/ttyab5c8.aspx
-
---- 
-
-Look at 
-
-  http://dean.edwards.name/download/#packer
-
-  http://code.google.com/p/minify/
-
-Examples of parsers
-
-  JSon parser in Parsec
-    http://snippets.dzone.com/posts/show/3660
-
-GOLD Parser, using the Javascript.grm from
-http://www.devincook.com/GOLDParser/grammars/index.htm
-
-http://oss.org.cn/ossdocs/web/js/js20/formal/parser-grammar.html
-
--------------------------
-
-- Generate output using standard pretty print library, so it can be
-  used with various backends
-
-
-- Sort out semicolon insertion, as per http://oss.org.cn/ossdocs/web/js/js20/rationale/syntax.html
-  Also: http://inimino.org/~inimino/blog/javascript_semicolons
-
-  Grammatical Semicolon Insertion
-
-    Semicolons before a closing } and the end of the program are
-    optional in both JavaScript 1.5 and 2.0. In addition, the
-    JavaScript 2.0 parser allows semicolons to be omitted before the
-    else of an if-else statement and before the while of a do-while
-    statement.
-
-  Line-Break Semicolon Insertion
-
-    If the first through the nth tokens of a JavaScript program form
-    are grammatically valid but the first through the n+1st tokens are
-    not and there is a line break between the nth tokens and the n+1st
-    tokens, then the parser tries to parse the program again after
-    inserting a VirtualSemicolon token between the nth and the n+1st
-    tokens.
-
-- remove un-needed semicolons in pretty printer
-
-- put in tests for all cases of elementList
-
--- Look at "in" keyword, as used : if (x in list) {}
-
-------------
-
-Integrating language-haskell.
------------------------------
-
-Baseline before starting, on my laptop (intel Core2 Duo SU7300)
-
-$ time ./dist/build/runtests/runtests
-
-real	0m1.625s
-user	0m1.616s
-sys	0m0.016s
-
-Baseline 2010-12-20, after getting all tests to pass at last
-
-real	0m0.209s
-user	0m0.196s
-sys	0m0.004s
-
-After working in unicode, in language-javascript-0.0.3 [2010-12-28]
-
-real	0m0.236s
-user	0m0.224s
-sys	0m0.012s
-
-
-
-EOF
diff --git a/Text/Jasmine.hs b/Text/Jasmine.hs
deleted file mode 100644
--- a/Text/Jasmine.hs
+++ /dev/null
@@ -1,49 +0,0 @@
-module Text.Jasmine
-    ( minify
-    , minifym
-    , minifyBb
-    , minifyFile
-    ) where
-
-import Control.Applicative ((<$>))
-import Data.Text.Lazy (unpack)
-import Data.Text.Lazy.Encoding (decodeUtf8With)
-import Data.Text.Encoding.Error (lenientDecode)
-import Language.JavaScript.Parser (readJs, parse, JSAST)
-import Language.JavaScript.Pretty.Printer
-import Language.JavaScript.Process.Minify
-
-import qualified Blaze.ByteString.Builder as BB
-import qualified Data.ByteString.Lazy as LB
-import qualified Data.ByteString.Lazy.Char8 as S8
-
-
-
-minifym :: LB.ByteString -> Either String LB.ByteString
-minifym s =
-    case myParse s of
-        Left msg -> Left (show msg)
-        Right p  -> Right $ BB.toLazyByteString $ renderJS $ minifyJS p
-
-
-minifyBb :: LB.ByteString -> Either String BB.Builder
-minifyBb s =
-    case myParse s  of
-        Left msg -> Left (show msg)
-        Right p  -> Right (renderJS $ minifyJS p)
-
-
-minify :: LB.ByteString -> LB.ByteString
-minify s = BB.toLazyByteString . renderJS . minifyJS . readJs $ lbToStr s
-
-
-minifyFile :: FilePath -> IO LB.ByteString
-minifyFile filename = minify <$> LB.readFile filename
-
-
-myParse :: S8.ByteString -> Either String JSAST
-myParse input = parse (lbToStr input) "src"
-
-
-lbToStr :: S8.ByteString -> String
-lbToStr = unpack . decodeUtf8With lenientDecode
diff --git a/Text/Jasmine/Pretty.hs b/Text/Jasmine/Pretty.hs
deleted file mode 100644
--- a/Text/Jasmine/Pretty.hs
+++ /dev/null
@@ -1,5 +0,0 @@
-module Text.Jasmine.Pretty
-    ( renderJS
-    ) where
-
-import Language.JavaScript.Pretty.Printer (renderJS)
diff --git a/hjsmin.cabal b/hjsmin.cabal
--- a/hjsmin.cabal
+++ b/hjsmin.cabal
@@ -1,5 +1,5 @@
 name:            hjsmin
-version:         0.2.0.2
+version:         0.2.0.3
 license:         BSD3
 license-file:    LICENSE
 author:          Alan Zimmerman <alan.zimm@gmail.com>
@@ -10,38 +10,56 @@
    other syntactic elements, without changing the semantics.
 category:        Web
 stability:       unstable
-cabal-version:   >= 1.9.2
+cabal-version:   >= 1.10
 build-type:      Simple
 homepage:        http://github.com/erikd/hjsmin
 bug-reports:     http://github.com/erikd/hjsmin/issues
 
 Extra-source-files:
-   TODO.txt
- , Readme.md
+  Readme.md
 
 
 library
-    build-depends:   base                >= 4.5
-                   , bytestring          >= 0.9
-                   , blaze-builder       >= 0.2
-                   , text                >= 0.8
-                   , containers          >= 0.2
-                   , language-javascript >= 0.6 && < 0.7
-    exposed-modules: Text.Jasmine
-    other-modules:   Text.Jasmine.Pretty
-    ghc-options:     -Wall
+  default-language:     Haskell2010
+  ghc-options:          -Wall
+  hs-source-dirs:       src
 
-executable hjsmin
-  main-is:             hjsmin.hs
+  exposed-modules:      Text.Jasmine
 
+  build-depends:        base                    >= 4.8          && < 5
+                      , bytestring              == 0.10.*
+                      , language-javascript     >= 0.6          && < 0.8
+                      , text                    == 1.2.*
+
+
+executable hjsmin
+  default-language:     Haskell2010
   ghc-options:         -Wall -threaded
-  build-depends:   base                 >= 4       && < 5
-                 , bytestring           >= 0.9
-                 , blaze-builder        >= 0.2
-                 , text                 >= 0.8
-                 , containers           >= 0.2
-                 , language-javascript  >= 0.6
-                 , optparse-applicative >= 0.7
+  hs-source-dirs:       src main
+  main-is:              hjsmin.hs
+
+  -- Need this here because the library and the executable have the same name.
+  other-modules:        Text.Jasmine
+
+  build-depends:        base                    >= 4.8          && < 5
+                      , bytestring              == 0.10.*
+                      , language-javascript
+                      , optparse-applicative    >= 0.7
+                      , text
+
+test-suite test-cli
+  type:                 exitcode-stdio-1.0
+  ghc-options:          -Wall -fwarn-tabs
+  default-language:     Haskell2010
+  hs-source-dirs:       test
+  main-is:              test-cli.hs
+
+  build-depends:        base
+                      , directory
+                      , extra
+                      , filepath
+                      , process
+                      , unix
 
 source-repository head
   type:     git
diff --git a/hjsmin.hs b/hjsmin.hs
deleted file mode 100644
--- a/hjsmin.hs
+++ /dev/null
@@ -1,52 +0,0 @@
-{-# LANGUAGE CPP #-}
-module Main where
-
-#include "cabal_macros.h"
-
-import Data.Monoid ((<>))
-import Options.Applicative
-import Text.Jasmine
-
-import qualified Data.ByteString.Lazy as B
-import qualified Data.ByteString.Lazy.Char8 as C8
-
-
-data Options = Options
-    { inputFile :: String
-    , outputFile :: Maybe String
-    }
-
-main :: IO ()
-main =
-    execParser opts >>= minify'
-  where
-    opts = info (helper <*> options)
-        ( fullDesc
-            <> progDesc
-                ( "Minify JavaScript files (using language-javascript version "
-                ++ languageJavascriptVersion ++ ")."
-                )
-            <> header "hjsmin - a simple command-line interface to the 'hjsmin' library"
-            )
-
-options :: Parser Options
-options = Options
-      <$> argument str (metavar "INPUT_FILE"
-                     <> help "The unminified, original JavaScript file")
-      <*> optional
-            ( strOption (long "output-file"
-                    <> short 'o'
-                    <> metavar "OUTPUT_FILE"
-                    <> help "The minified output file. Default: stdout")
-                    )
-
-minify' :: Options -> IO ()
-minify' o = do
-    minified <- minifyFile (inputFile o)
-    case outputFile o of
-        Nothing -> C8.putStrLn minified
-        Just f  -> B.writeFile f minified
-
-
-languageJavascriptVersion :: String
-languageJavascriptVersion = VERSION_language_javascript
diff --git a/main/hjsmin.hs b/main/hjsmin.hs
new file mode 100644
--- /dev/null
+++ b/main/hjsmin.hs
@@ -0,0 +1,80 @@
+{-# LANGUAGE CPP #-}
+
+#include "cabal_macros.h"
+
+import qualified Data.ByteString.Lazy.Char8 as LBS
+import           Data.Monoid ((<>))
+import           Options.Applicative (Parser, ParserInfo, ParserPrefs)
+import qualified Options.Applicative as Opt
+import           Text.Jasmine (minify)
+
+import           System.IO (hPutStrLn, stderr)
+import           System.Exit (exitFailure)
+
+data Command
+  = Process FilePath (Maybe FilePath)
+
+main :: IO ()
+main =
+  Opt.customExecParser p opts >>= processFile
+  where
+    opts :: ParserInfo Command
+    opts = Opt.info (Opt.helper <*> pVersion <*> pProcess)
+      ( Opt.fullDesc
+      <> Opt.header "hjsmin - Haskell implementation of a Javascript and JSON minifier"
+      )
+
+    p :: ParserPrefs
+    p = Opt.prefs Opt.showHelpOnEmpty
+
+pVersion :: Parser (a -> a)
+pVersion =
+  Opt.infoOption versionString
+    (  Opt.long "version"
+    <> Opt.short 'v'
+    <> Opt.help "Print the version and exit"
+    )
+
+pProcess :: Parser Command
+pProcess =
+  Process <$> pInputFile <*> pMaybeOutputFile
+  where
+    pInputFile =
+      Opt.strOption
+        (  Opt.long "input"
+        <> Opt.short 'i'
+        <> Opt.metavar "INPUT_FILE"
+        <> Opt.help "The original JavaScript file"
+        )
+
+    pMaybeOutputFile =
+      Opt.optional $ Opt.strOption
+        (  Opt.long "output"
+        <> Opt.short 'o'
+        <> Opt.metavar "OUTPUT_FILE"
+        <> Opt.help "The minified output file. Default: stdout"
+        )
+
+
+processFile :: Command -> IO ()
+processFile (Process inputFile outputFile) = do
+  lbs <- LBS.readFile inputFile
+  if LBS.null lbs
+    then emptyFileError
+    else do
+      let minified = minify lbs
+      case outputFile of
+        Nothing -> LBS.putStrLn minified
+        Just f  -> LBS.writeFile f minified
+  where
+    emptyFileError = do
+      hPutStrLn stderr $ "Error: input file '" ++ inputFile ++ "' is empty."
+      exitFailure
+
+
+versionString :: String
+versionString =
+  concat
+    [ "hjsmin version ", VERSION_hjsmin
+    , " (using language-javascript version ", VERSION_language_javascript, ")"
+    ]
diff --git a/src/Text/Jasmine.hs b/src/Text/Jasmine.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Jasmine.hs
@@ -0,0 +1,54 @@
+module Text.Jasmine
+    ( minify
+    , minifym
+    , minifyBb
+    , minifyFile
+    ) where
+
+import           Control.Applicative ((<$>))
+
+import           Data.ByteString.Builder (Builder)
+import qualified Data.ByteString.Builder as Builder
+import qualified Data.ByteString.Lazy.Char8 as LBS
+import           Data.Text.Lazy (unpack)
+import           Data.Text.Lazy.Encoding (decodeUtf8With)
+import           Data.Text.Encoding.Error (lenientDecode)
+
+import           Language.JavaScript.Parser (readJs, parse, JSAST)
+import           Language.JavaScript.Pretty.Printer (renderJS)
+import           Language.JavaScript.Process.Minify (minifyJS)
+
+
+
+minifym :: LBS.ByteString -> Either String LBS.ByteString
+minifym s =
+  case myParse s of
+    Left msg -> Left (show msg)
+    Right p  -> Right $ Builder.toLazyByteString $ renderJS $ minifyJS p
+
+
+minifyBb :: LBS.ByteString -> Either String Builder
+minifyBb s =
+  case myParse s  of
+    Left msg -> Left (show msg)
+    Right p  -> Right (renderJS $ minifyJS p)
+
+
+minify :: LBS.ByteString -> LBS.ByteString
+minify =
+  Builder.toLazyByteString . renderJS . minifyJS . readJs . lbToStr
+
+
+minifyFile :: FilePath -> IO LBS.ByteString
+minifyFile filename =
+  minify <$> LBS.readFile filename
+
+
+myParse :: LBS.ByteString -> Either String JSAST
+myParse input =
+  parse (lbToStr input) "src"
+
+
+lbToStr :: LBS.ByteString -> String
+lbToStr =
+  unpack . decodeUtf8With lenientDecode
diff --git a/test/test-cli.hs b/test/test-cli.hs
new file mode 100644
--- /dev/null
+++ b/test/test-cli.hs
@@ -0,0 +1,57 @@
+{-# LANGUAGE MultiWayIf #-}
+
+import           Control.Monad (foldM, forM)
+import           Control.Monad.Extra (concatMapM)
+
+import           System.Directory (canonicalizePath, getCurrentDirectory, listDirectory)
+import           System.Environment (setEnv)
+import           System.FilePath (takeFileName, (</>))
+import           System.Exit (ExitCode (..), exitFailure, exitSuccess)
+import           System.Posix.Files (getFileStatus, isDirectory, isRegularFile)
+import           System.Process (rawSystem)
+import           System.IO (BufferMode (..))
+import qualified System.IO as IO
+
+main :: IO ()
+main = do
+  IO.hSetBuffering IO.stdout LineBuffering
+  IO.hSetBuffering IO.stderr LineBuffering
+
+  cwd <- getCurrentDirectory
+  topdir <- canonicalizePath $ cwd </> "dist-newstyle"
+
+  -- Set an environment variable for all the exectuables we want to test.
+  setExecutableEnvVar "HJSMIN" topdir "hjsmin"
+
+  tests <- filter (`notElem` ["core", "data"]) <$> listDirectory "test/cli/"
+  res <- forM tests $ \ t -> rawSystem ("test/cli/" ++ t ++ "/run") []
+  if all (== ExitSuccess) res
+    then exitSuccess
+    else exitFailure
+
+
+setExecutableEnvVar :: String -> FilePath -> FilePath -> IO ()
+setExecutableEnvVar envName startDir target = do
+  xs <- listDirectoryRecursive startDir
+  case filter match xs of
+    [] -> error "Unable to find hjsmin binary"
+    [x] -> setEnv envName x
+    _ -> error $ "Multiple binaries: " ++ show xs
+ where
+  match :: FilePath -> Bool
+  match fp = takeFileName fp == target
+
+listDirectoryRecursive :: FilePath -> IO [FilePath]
+listDirectoryRecursive fpath = do
+  xs <- fmap (fpath </>) <$> listDirectory fpath
+  (files, dirs) <- foldM partitioner ([], []) xs
+  rest <- concatMapM listDirectoryRecursive (dirs :: [FilePath])
+  pure $ files ++ rest
+ where
+  partitioner :: ([FilePath], [FilePath]) -> FilePath -> IO ([FilePath], [FilePath])
+  partitioner (files, dirs) fp = do
+    st <- getFileStatus fp
+    if
+      | isRegularFile st -> pure (fp : files, dirs)
+      | isDirectory st -> pure (files, fp : dirs)
+      | otherwise -> pure (files, dirs)
