diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2016 Joe Hillenbrand
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
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/craft.cabal b/craft.cabal
new file mode 100644
--- /dev/null
+++ b/craft.cabal
@@ -0,0 +1,154 @@
+name:                craft
+
+-- The package version.  See the Haskell package versioning policy (PVP)
+-- for standards guiding when and how versions should be incremented.
+-- http://www.haskell.org/haskellwiki/Package_versioning_policy
+-- PVP summary:      +-+------- breaking API changes
+--                   | | +----- non-breaking API additions
+--                   | | | +--- code changes with no API change
+version:             0.0.0.1
+synopsis:            A UNIX configuration management library in Haskell
+description:         A UNIX configuration management EDSL/library in Haskell
+license:             Apache-2.0
+license-file:        LICENSE
+author:              Joe Hillenbrand
+maintainer:          joehillen@gmail.com
+-- copyright:
+-- category:
+build-type:          Simple
+-- Extra files to be distributed with the package, such as examples or a
+-- README.
+-- extra-source-files:
+cabal-version:       >=1.10
+
+library
+  ghc-options:         -Wall
+  hs-source-dirs:      src
+                     , src/Craft
+  exposed-modules:     Craft
+                     , Craft.Apt
+                     , Craft.Checksum
+                     , Craft.Craftable
+                     , Craft.Config
+                     , Craft.Config.Ini
+                     , Craft.Config.Json
+                     , Craft.Config.SSH
+                     , Craft.Config.Shell
+                     , Craft.Config.Yaml
+                     , Craft.DSL
+                     , Craft.Daemontools
+                     , Craft.Directory
+                     , Craft.Facter
+                     , Craft.File
+                     , Craft.File.Link
+                     , Craft.File.Mode
+                     , Craft.File.Sourced
+                     , Craft.Git
+                     , Craft.Group
+                     , Craft.Helpers
+                     , Craft.Hostname
+                     , Craft.Hosts
+                     , Craft.Internal.Helpers
+                     , Craft.Internal.Helpers.Parsing
+                     , Craft.Internal.UserGroup
+                     , Craft.Nginx
+                     , Craft.Package
+                     , Craft.Pip
+                     , Craft.Run
+                     , Craft.Run.Internal
+                     , Craft.Run.Local
+                     , Craft.Run.Nspawn
+                     , Craft.Run.SSH
+                     , Craft.Run.Vagrant
+                     , Craft.S3File
+                     , Craft.SSH
+                     , Craft.SSH.AuthorizedKey
+                     , Craft.SSH.PrivateKey
+                     , Craft.SSH.PublicKey
+                     , Craft.SysV
+                     , Craft.Types
+                     , Craft.Ubuntu
+                     , Craft.Upstart
+                     , Craft.User
+                     , Craft.Wget
+
+  other-modules:       Craft.Internal
+                     , Craft.Hosts.Parser
+                     , Craft.Hosts.Types
+                     , Craft.Directory.Parser
+                     , Craft.Error
+
+  default-language:    Haskell2010
+  default-extensions:  BangPatterns
+                     , LambdaCase
+                     , MultiParamTypeClasses
+                     , OverloadedStrings
+                     , QuasiQuotes
+                     , RankNTypes
+                     , RecordWildCards
+                     , TemplateHaskell
+                     , TypeFamilies
+
+  build-depends:       base >= 4.8 && < 4.9
+                     , aeson
+                     , aeson-pretty
+                     , ansi-terminal
+                     , async
+                     , bytestring
+                     , conduit
+                     , conduit-combinators
+                     , conduit-extra
+                     , containers
+                     , cryptonite
+                     , derive
+                     , directory
+                     , exceptions
+                     , fast-logger
+                     , monad-logger
+                     , filepath
+                     , formatting
+                     , free
+                     , unordered-containers
+                     , ini
+                     , lifted-base
+                     , lens
+                     , megaparsec
+                     , MissingH
+                     , memory
+                     , monad-logger
+                     , mtl
+                     , path
+                     , path-io
+                     , process
+                     , process-extras
+                     , pureMD5
+                     , QuickCheck
+                     , random
+                     , split
+                     , streaming-commons
+                     , template-haskell
+                     , text
+                     , transformers
+                     , unix
+                     , versions
+                     , wreq
+                     , yaml
+
+test-suite craft-test
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  other-modules:       Craft.File.ModeSpec
+                     , Craft.Config.ShellSpec
+                     , Craft.S3FileSpec
+                     , Craft.SSHSpec
+                     , Craft.SSH.PublicKeySpec
+                     , Craft.SSH.AuthorizedKeySpec
+  build-depends:       base
+                     , craft
+                     , hspec >= 1.3
+                     , hspec-megaparsec
+                     , megaparsec
+                     , QuickCheck
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N -Wall
+  default-language:    Haskell2010
diff --git a/src/Craft.hs b/src/Craft.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft.hs
@@ -0,0 +1,27 @@
+module Craft
+( ExitCode(..)
+, module X
+-- Control.Monad.Reader
+, ask
+, asks
+, local
+-- Control.Lens
+, (&)
+, (.~)
+, (^.)
+, (%~)
+, (?~)
+, view
+, to
+)
+where
+
+import           Control.Lens
+import           Control.Monad.Reader (ask, asks, local)
+
+-- |Re-exports
+import           Control.Monad.Catch  as X
+import           System.Exit          (ExitCode (..))
+
+import           Craft.Craftable      as X
+import           Craft.Internal       as X
diff --git a/src/Craft/Apt.hs b/src/Craft/Apt.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Apt.hs
@@ -0,0 +1,193 @@
+module Craft.Apt where
+
+import           Control.Lens
+import           Control.Monad
+import           Formatting
+
+import           Craft
+import qualified Craft.Package as Package
+
+
+
+apt :: PackageManager
+apt =
+  PackageManager
+  { _pmGetter         = getAptPackage
+  , _pmInstaller      = aptInstall
+  , _pmUpgrader       = aptInstall
+  , _pmUninstaller    = aptPurge
+  }
+
+
+aptGet :: [String] -> Craft ()
+aptGet args = exec_ "apt-get" $ ["-q", "-y"] ++ args
+
+
+update :: Craft ()
+update = aptGet ["update"]
+
+
+autoremove :: Craft ()
+autoremove = aptGet ["autoremove"]
+
+
+dpkgQuery :: [String] -> Craft ExecResult
+dpkgQuery = exec dpkgQueryBin
+
+
+dpkgQueryBin :: String
+dpkgQueryBin = "dpkg-query"
+
+
+dpkgQueryStatus :: String -> Craft ExecResult
+dpkgQueryStatus pn = dpkgQuery ["-s", pn]
+
+
+expectOutput :: String -> [String] -> Craft String
+expectOutput cmd args = do
+  r <- $stdoutOrError =<< exec cmd args
+  when (null r) $ $craftError $ formatToString ("'"%string%"' returned an empty result!")
+                                               (unwords $ cmd:args)
+  return r
+
+
+dpkgQueryShow :: String -> String -> Craft String
+dpkgQueryShow pattern n = expectOutput dpkgQueryBin [ "--show", "--showformat", pattern, n ]
+
+
+dpkgQueryVersion :: String -> Craft String
+dpkgQueryVersion = dpkgQueryShow "${Version}"
+
+
+dpkgQueryPackage :: String -> Craft String
+dpkgQueryPackage = dpkgQueryShow "${Package}"
+
+
+getAptPackage :: PackageName -> Craft (Maybe Package)
+getAptPackage pn =
+  dpkgQueryStatus pn >>= \case
+    ExecFail _ -> return Nothing
+    ExecSucc _ -> Just <$> do
+      r <- dpkgQueryVersion pn
+      return $ Package pn (Version r)
+
+
+aptInstallArgs :: [String]
+aptInstallArgs = ["-o", "DPkg::Options::=--force-confold", "install"]
+
+
+aptInstall :: Package -> Craft ()
+aptInstall pkg = aptGet $ aptInstallArgs ++ [pkgArg pkg]
+
+
+aptInstallMult :: [Package] -> Craft ()
+aptInstallMult []   = return ()
+aptInstallMult pkgs = aptGet $ aptInstallArgs ++ map pkgArg pkgs
+
+
+pkgArg :: Package -> String
+pkgArg (Package pn AnyVersion)  = pn
+pkgArg (Package pn Latest)      = pn
+pkgArg (Package pn (Version v)) = pn ++ "=" ++ v
+
+
+aptRemove :: Package -> Craft ()
+aptRemove Package{..} = aptGet ["remove", _pkgName]
+
+
+aptPurge :: Package -> Craft ()
+aptPurge Package{..} = aptGet ["remove", _pkgName, "--purge"]
+
+
+data Deb = Deb
+  { _debFile :: File
+  , _debPkg  :: Package
+  }
+  deriving (Eq, Show)
+makeLenses ''Deb
+
+
+debName :: Lens' Deb String
+debName = debPkg . pkgName
+
+
+debVersion :: Lens' Deb Version
+debVersion = debPkg . pkgVersion
+
+
+deb :: File -> Craft Deb
+deb f = do
+  pkg <- packageFromDebFile f
+  return $ Deb f pkg
+
+
+packageFromDebFile :: File -> Craft Package
+packageFromDebFile f = do
+  name    <- dpkgDebPackage f
+  version <- dpkgDebVersion f
+  return $ Package name (Version version)
+
+
+dpkgInstall :: File -> Craft ()
+dpkgInstall f =
+  withEnvVar "DEBIAN_FRONTEND" "noninteractive" $
+    exec_ "dpkg" ["-i", fromAbsFile $ f^.path]
+
+
+dpkgDebBin :: String
+dpkgDebBin = "dpkg-deb"
+
+
+dpkgDebShow :: String -> File -> Craft String
+dpkgDebShow pattern f =
+  expectOutput dpkgDebBin ["--show", "--showformat", pattern, fromAbsFile $ f^.path]
+
+
+dpkgDebVersion :: File -> Craft String
+dpkgDebVersion = dpkgDebShow "${Version}"
+
+
+dpkgDebPackage :: File -> Craft String
+dpkgDebPackage = dpkgDebShow "${Package}"
+
+
+instance Craftable Deb Deb where
+  watchCraft d = do
+    let name = d ^. debName
+    let expectedVersion = d ^. debVersion
+    let get = getAptPackage name
+    let error' = formatToString ("craft Deb `"%shown%"` failed! "%string) d
+    let installAndVerify = do
+          dpkgInstall $ d ^. debFile
+          get >>= \case
+            Nothing -> $craftError $ error' "Package Not Found."
+            Just installedPkg ->
+              when (expectedVersion /= installedPkg ^. pkgVersion) $
+                $craftError . error' $ formatToString ("Wrong Version: "%shown%" Expected: "%shown) (installedPkg ^. pkgVersion) expectedVersion
+    get >>= \case
+      Nothing  -> do
+        installAndVerify
+        return (Created, d)
+      Just installedPkg ->
+        if expectedVersion == installedPkg ^. pkgVersion
+        then return (Unchanged, d)
+        else do
+          installAndVerify
+          return (Updated, d)
+
+
+-- |A function that installs a list of Apt packages,
+-- |but that only runs "apt-get update" if a given package is not already installed.
+-- TESTME
+craftPackages :: [Package] -> Craft [Package]
+craftPackages packages = do
+  whenM ((any comparePackages . zip packages) <$> mapM (Package.get . _pkgName) packages)
+    update
+  craft packages
+ where
+  comparePackages (_, Nothing) = True
+  comparePackages (Package _ expectedVersion, Just (Package _ actualVersion)) =
+    case (expectedVersion, actualVersion) of
+      (AnyVersion, _) -> False
+      (Latest,     _) -> True
+      (x,          y) -> x /= y
diff --git a/src/Craft/Checksum.hs b/src/Craft/Checksum.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Checksum.hs
@@ -0,0 +1,66 @@
+module Craft.Checksum where
+
+import           Control.Lens
+
+import           Craft
+
+data Checksum
+  = CRC    String
+  | MD5    String
+  | SHA1   String
+  | SHA224 String
+  | SHA256 String
+  | SHA384 String
+  | SHA512 String
+  deriving (Show, Eq)
+
+
+command :: Checksum -> String
+command (CRC    _) = "cksum"
+command (MD5    _) = "md5sum"
+command (SHA1   _) = "sha1sum"
+command (SHA224 _) = "sha224sum"
+command (SHA256 _) = "sha256sum"
+command (SHA384 _) = "sha384sum"
+command (SHA512 _) = "sha512sum"
+
+
+toString :: Checksum -> String
+toString (CRC    x) = x
+toString (MD5    x) = x
+toString (SHA1   x) = x
+toString (SHA224 x) = x
+toString (SHA256 x) = x
+toString (SHA384 x) = x
+toString (SHA512 x) = x
+
+
+data Result
+  = Failed FailResult
+  | Matched
+  | Mismatched Checksum
+  deriving (Show)
+
+
+-- | Check a 'File' against the given 'Checksum'.
+-- If the 'File' does not exist or the checksum command fails,
+-- 'Failed' is returned.
+-- If 'Checksum' does not match,
+-- 'Mismatched' is returned containing the actual 'Checksum'.
+-- Otherwise 'Matched' is returned.
+check :: File -> Checksum -> Craft Result
+check f chksum = exec (command chksum) [fromAbsFile $ f^.path] >>= \case
+  ExecFail r -> return $ Failed r
+  ExecSucc r -> return $
+    let mk = case chksum of
+               CRC    _ -> CRC
+               MD5    _ -> MD5
+               SHA1   _ -> SHA1
+               SHA224 _ -> SHA224
+               SHA256 _ -> SHA256
+               SHA384 _ -> SHA384
+               SHA512 _ -> SHA512
+        actual = mk $ r ^. stdout
+    in if chksum == actual
+          then Mismatched actual
+          else Matched
diff --git a/src/Craft/Config.hs b/src/Craft/Config.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Config.hs
@@ -0,0 +1,74 @@
+module Craft.Config where
+
+import           Control.Lens
+import qualified Data.ByteString.Char8 as B8
+
+import           Craft
+import qualified Craft.File            as File
+
+
+data Config a
+  = Config
+    { _configFile :: File
+    , _configs    :: a
+    }
+
+
+config :: ConfigFormat a => Path Abs FileP -> a -> Config a
+config fp cfg =
+  Config
+  { _configFile = file fp & strContent .~ showConfig cfg
+  , _configs    = cfg
+  }
+
+
+class ConfigFormat a where
+  showConfig :: a -> String
+
+  parseConfig :: Path Abs FileP -> String -> Craft a
+
+  configFromFile :: File -> Craft (Config a)
+  configFromFile f = do
+    bs <- case f ^. fileContent of
+            Nothing -> fileRead (f ^. path)
+            Just bs -> return bs
+    cfgs <- parseConfig (f^.path) (B8.unpack bs)
+    return Config { _configFile = f
+                  , _configs    = cfgs
+                  }
+
+  fileFromConfig :: Config a -> Craft File
+  fileFromConfig cfg =
+    return $ _configFile cfg & strContent .~ showConfig (_configs cfg) ++ "\n"
+
+
+  {-# MINIMAL showConfig, parseConfig #-}
+
+makeLenses ''Config
+
+
+instance FileLike (Config a) where
+  type FileLikePath (Config a) = Path Abs FileP
+  path = configFile . filePath
+  mode = configFile . fileMode
+  ownerID = configFile . fileOwnerID
+  groupID = configFile . fileGroupID
+
+
+instance ConfigFormat a => Craftable (Config a) (Config a) where
+  watchCraft cfg = do
+    w <- watchCraft_ =<< fileFromConfig cfg
+    return (w, cfg)
+
+
+instance ConfigFormat a => Show (Config a) where
+  show cfg = "Config "
+          ++ "{ _configFile = " ++ show (_configFile cfg)
+          ++ ", _configs = \"" ++ showConfig (_configs cfg) ++ "\""
+          ++ "}"
+
+
+get :: ConfigFormat a => Path Abs FileP -> Craft (Maybe (Config a))
+get fp = File.getWithContent fp >>= \case
+  Nothing -> return Nothing
+  Just f  -> Just <$> configFromFile f
diff --git a/src/Craft/Config/Ini.hs b/src/Craft/Config/Ini.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Config/Ini.hs
@@ -0,0 +1,54 @@
+module Craft.Config.Ini
+( module Craft.Config.Ini
+, module Data.Ini
+, Ini(..)
+, HM.fromList
+)
+where
+
+import           Control.Lens
+import qualified Data.HashMap.Strict as HM
+import           Data.Ini
+import qualified Data.Text           as T
+
+import           Craft.Config
+import           Craft.Types
+
+
+data IniFormat = IniFormat { _inifmt   :: Ini
+                           , _settings :: WriteIniSettings
+                           }
+
+
+iniFormat :: [(T.Text, [(T.Text, T.Text)])] -> IniFormat
+iniFormat ini = IniFormat (Ini (HM.fromList $ fmap (fmap HM.fromList) ini))
+                          (WriteIniSettings EqualsKeySeparator)
+
+
+config :: Path Abs FileP -> IniFormat -> Config IniFormat
+config = Craft.Config.config
+
+
+instance ConfigFormat IniFormat where
+  showConfig format = T.unpack . printIniWith (_settings format) $ _inifmt format
+  parseConfig fp s =
+    case parseIni (T.pack s) of
+      Left err -> $craftError $ "Failed to parse ini file"++show fp++": "++err
+      Right x  -> return $ IniFormat
+                           { _inifmt   = x
+                           , _settings = WriteIniSettings EqualsKeySeparator
+                           }
+
+
+get :: Path Abs FileP -> Craft (Maybe (Config IniFormat))
+get = Craft.Config.get
+
+
+lookup :: String -> String -> IniFormat -> Maybe String
+lookup section key (IniFormat ini _)=
+  case lookupValue (T.pack section) (T.pack key) ini of
+    Left  _ -> Nothing
+    Right r -> Just $ T.unpack r
+
+
+makeLenses ''IniFormat
diff --git a/src/Craft/Config/Json.hs b/src/Craft/Config/Json.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Config/Json.hs
@@ -0,0 +1,34 @@
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE GADTs                     #-}
+module Craft.Config.Json where
+
+import           Control.Lens
+import           Data.Aeson
+import           Data.Aeson.Encode.Pretty (encodePretty)
+
+import           Craft
+import           Craft.Config
+import qualified Data.ByteString.Char8    as B8
+import qualified Data.ByteString.Lazy     as BSL
+
+
+data JsonFormat a = (FromJSON a, ToJSON a) => JsonFormat { _jsonfmt :: a }
+
+
+instance (FromJSON a, ToJSON a) => ConfigFormat (JsonFormat a) where
+  showConfig = B8.unpack . BSL.toStrict . encodePretty . _jsonfmt
+  parseConfig fp s =
+    case eitherDecodeStrict (B8.pack s) of
+      Left err -> $craftError $ "Failed to parse "++show fp++" : "++err
+      Right x  -> return $ JsonFormat x
+
+
+get :: (FromJSON a, ToJSON a) => Path Abs FileP -> Craft (Maybe (Config (JsonFormat a)))
+get = Craft.Config.get
+
+
+config :: (FromJSON a, ToJSON a) => Path Abs FileP -> a -> Config (JsonFormat a)
+config fp = Craft.Config.config fp . JsonFormat
+
+
+makeLenses ''JsonFormat
diff --git a/src/Craft/Config/SSH.hs b/src/Craft/Config/SSH.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Config/SSH.hs
@@ -0,0 +1,146 @@
+module Craft.Config.SSH where
+
+import           Control.Lens                   hiding (noneOf)
+import           Data.Char                      (toLower)
+import           Data.Maybe                     (catMaybes)
+import           Text.Megaparsec
+
+import           Craft                          hiding (try)
+import           Craft.Config
+import           Craft.Internal.Helpers
+import           Craft.Internal.Helpers.Parsing
+import           Craft.SSH
+
+
+data Section
+  = Host  String Body
+  | Match String Body
+  deriving Eq
+
+
+type Sections = [Section]
+
+
+type Body = [(String, String)]
+
+
+newtype SSHConfig = SSHConfig { _sshfmt :: Sections }
+  deriving Eq
+
+
+instance Show SSHConfig where
+  show sshcfg = unlines . map show $ _sshfmt sshcfg
+
+
+data UserConfig
+  = UserConfig
+    { _user        :: User
+    , _userConfigs :: SSHConfig
+    }
+  deriving Eq
+makeLenses ''SSHConfig
+makeLenses ''UserConfig
+
+instance ConfigFormat SSHConfig where
+  showConfig = show
+  parseConfig = sshConfigParse
+
+
+userPath :: UserConfig -> Path Abs FileP
+userPath uc = (userDir (uc ^. user) ^. path) </> $(mkRelFile "config")
+
+
+get :: Path Abs FileP -> Craft (Maybe (Config SSHConfig))
+get = Craft.Config.get
+
+
+bodyLookup :: String -> Body -> Maybe String
+bodyLookup key body =
+  lookup (map toLower key) $ map ((,) <$> map toLower . fst <*> snd) body
+
+
+cfgLookup :: String -> String -> SSHConfig -> Maybe String
+cfgLookup sectionName key (SSHConfig sections') =
+  case body of
+    Just b  -> bodyLookup key b
+    Nothing -> Nothing
+ where
+  body = maybeHead . catMaybes $ map f sections'
+  f (Host name b) | name == sectionName = Just b
+                  | otherwise           = Nothing
+  f             _                       = Nothing
+  maybeHead []    = Nothing
+  maybeHead (x:_) = Just x
+
+
+instance Craftable UserConfig UserConfig where
+  watchCraft cfg = do
+    craft_ $ userDir $ cfg ^. user
+    w <- watchCraft_ $ file (userPath cfg)
+                       & mode          .~ Mode RW O O
+                       & ownerAndGroup .~ cfg ^. user
+                       & strContent .~ show cfg
+    return (w, cfg)
+
+
+sshConfigParse :: Path Abs FileP -> String -> Craft SSHConfig
+sshConfigParse fp s =
+  case runParser parser (show fp) s of
+    Left err   -> $craftError $ show err
+    Right secs -> return $ SSHConfig secs
+
+
+instance Show UserConfig where
+  show uc = show $ uc ^. userConfigs
+
+
+showBody :: Body -> String
+showBody body = indent 4 . unlines $ map (\(n, v) -> n ++ " " ++ v) body
+
+
+instance Show Section where
+  show (Host hostname body) = "Host " ++ hostname ++ "\n" ++ showBody body
+  show (Match match body)   = "Match " ++ match ++ "\n" ++ showBody body
+
+
+parseTitle :: Parsec String (String, String)
+parseTitle = do
+  space
+  type' <- try (string' "host") <|> string' "match"
+  void spaceChar
+  notFollowedBy eol
+  space
+  name <- anyChar `someTill` eol
+  return (type', name)
+
+
+parseBody :: Parsec String [(String, String)]
+parseBody = many bodyLine
+
+
+bodyLine :: Parsec String (String, String)
+bodyLine = do
+  notFollowedBy parseTitle
+  space
+  name <- someTill anyChar spaceChar
+  notFollowedBy eol
+  space
+  val <- between (char '"') (char '"') (many $ noneOf "\"")
+         <|> someTill anyChar end
+  void $ optional (space <|> void (many eol))
+  return (name, trim val)
+
+
+parseSection :: Parsec String Section
+parseSection = do
+  title <- parseTitle
+  body <- parseBody
+  return $ case map toLower (fst title) of
+    "host"  -> Host (snd title) body
+    "match" -> Match (snd title) body
+    _       -> error "Craft.Config.SSH.parse failed. This should be impossible."
+
+
+parser :: Parsec String Sections
+parser = some parseSection
+
diff --git a/src/Craft/Config/Shell.hs b/src/Craft/Config/Shell.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Config/Shell.hs
@@ -0,0 +1,87 @@
+module Craft.Config.Shell where
+
+import           Control.Lens                   hiding (noneOf, set)
+import           Data.Maybe                     (catMaybes)
+import           Text.Megaparsec                hiding (parse)
+
+import           Craft                          hiding (try)
+import           Craft.Config
+import           Craft.Internal.Helpers.Parsing
+
+
+newtype ShellFormat = ShellFormat { _shellfmt :: [(String, String)] }
+  deriving (Eq, Show)
+makeLenses ''ShellFormat
+
+
+instance ConfigFormat ShellFormat where
+  showConfig (ShellFormat cfgs) = unlines $ map showkv cfgs
+   where
+     showkv :: (String, String) -> String
+     showkv (k, v) = k++"="++v
+  parseConfig fp s =
+    case runParser parser (fromAbsFile fp) s of
+      Left err   -> $craftError $ show err
+      Right cfgs -> return cfgs
+
+-- | Replace an existing value if it is already set otherwise add it to the end.
+set :: String -> String -> ShellFormat -> ShellFormat
+set k v (ShellFormat cfgs) =
+  case lookup k cfgs of
+    Nothing -> ShellFormat $ cfgs++[(k,v)]
+    Just _  -> ShellFormat $ map (replace k v) cfgs
+ where
+  replace ka va (kb, vb) | ka == kb  = (ka, va)
+                         | otherwise = (kb, vb)
+
+
+unset :: String -> ShellFormat -> ShellFormat
+unset k (ShellFormat cfgs) = ShellFormat $ filter ((/= k) . fst) cfgs
+
+
+get :: Path Abs FileP -> Craft (Maybe (Config ShellFormat))
+get = Craft.Config.get
+
+
+config :: Path Abs FileP -> [(String, String)] -> Config ShellFormat
+config fp = Craft.Config.config fp . ShellFormat
+
+
+dedup :: ShellFormat -> ShellFormat
+dedup cfgs = go empty cfgs
+ where
+  go m (ShellFormat          []) = m
+  go m (ShellFormat ((k, v):xs)) = go (set k v m) (ShellFormat xs)
+
+
+empty :: ShellFormat
+empty = ShellFormat []
+
+
+-- TESTME
+parser :: Parsec String ShellFormat
+parser = do
+  items <- line `sepEndBy` eol
+  return . ShellFormat $ catMaybes items
+
+
+line :: Parsec String (Maybe (String, String))
+line =  do
+  try (comment >> return Nothing)
+  <|> try (space >> end >> return Nothing)
+  <|> (Just <$> item)
+
+
+comment :: Parsec String ()
+comment = space >> char '#' >> manyTill anyChar end >> return ()
+
+
+item :: Parsec String (String, String)
+item = do
+  space
+  name <- some $ noneOf " ="
+  void $ char '='
+  -- TODO: quoted values
+  value <- try (someTill (noneOf " \r\n#") (lookAhead (oneOf " \r\n#")) <* manyTill anyChar end)
+        <|> (many (oneOf " \t") >> manyTill anyChar end >> return "")
+  return (name, value)
diff --git a/src/Craft/Config/Yaml.hs b/src/Craft/Config/Yaml.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Config/Yaml.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE GADTs                     #-}
+module Craft.Config.Yaml where
+
+import           Craft
+import           Craft.Config
+import qualified Data.ByteString.Char8 as B8
+
+import           Control.Lens
+import           Data.Yaml
+
+
+data YamlFormat a = (FromJSON a, ToJSON a) => YamlFormat { _yamlfmt :: a }
+
+
+instance (FromJSON a, ToJSON a) => ConfigFormat (YamlFormat a) where
+  showConfig = B8.unpack . encode . _yamlfmt
+  parseConfig fp s =
+    case decodeEither (B8.pack s) of
+      Left err -> $craftError $ "Failed to parse " ++ show fp ++ " : " ++ err
+      Right x  -> return $ YamlFormat x
+
+
+get :: (FromJSON a, ToJSON a) => Path Abs FileP -> Craft (Maybe (Config (YamlFormat a)))
+get = Craft.Config.get
+
+
+config :: (FromJSON a, ToJSON a) => Path Abs FileP -> a -> Config (YamlFormat a)
+config fp = Craft.Config.config fp . YamlFormat
+
+
+makeLenses ''YamlFormat
diff --git a/src/Craft/Craftable.hs b/src/Craft/Craftable.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Craftable.hs
@@ -0,0 +1,408 @@
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE MultiParamTypeClasses  #-}
+{-# LANGUAGE UndecidableInstances   #-}
+module Craft.Craftable where
+
+import           Control.Lens
+import           Control.Monad.Reader     (ask)
+import qualified Data.ByteString.Lazy     as BL
+import           Data.List                (intercalate)
+import           Data.Maybe               (catMaybes, isJust)
+import           Formatting               hiding (char)
+
+import           Craft.DSL
+import qualified Craft.File               as File
+import           Craft.Helpers
+import           Craft.Internal.Helpers
+import           Craft.Internal.UserGroup
+import           Craft.Types
+import qualified Craft.User               as User
+
+
+data Watched
+  = Unchanged
+  | Created
+  | Updated
+  | Removed
+  deriving (Eq, Show)
+
+makePrisms ''Watched
+
+changed :: Watched -> Bool
+changed = not . unchanged
+
+
+unchanged :: Watched -> Bool
+unchanged Unchanged = True
+unchanged _         = False
+
+
+created :: Watched -> Bool
+created Created = True
+created _       = False
+
+
+updated :: Watched -> Bool
+updated Updated = True
+updated _       = False
+
+
+removed :: Watched -> Bool
+removed Removed = True
+removed _       = False
+
+
+watch :: Eq a => Craft (Maybe a) -> Craft b -> Craft (Watched, Maybe a, b)
+watch getter action = do
+  !beforeMB <- getter
+  !result   <- action
+  !afterMB  <- getter
+  let (w, x) = case (beforeMB, afterMB) of
+                 (Nothing,     Nothing)    -> (Unchanged, Nothing)
+                 (Just y,      Nothing)    -> (Removed,   Just y)
+                 (Nothing,     Just y)     -> (Created,   Just y)
+                 (Just before, Just after) -> if before == after
+                                                then (Unchanged, Just after)
+                                                else (Updated,   Just after)
+  return (w, x, result)
+
+
+watch_ :: Eq a => Craft (Maybe a) -> Craft b -> Craft Watched
+watch_ getter action = do
+  (w, _, _) <- watch getter action
+  return w
+
+
+class Craftable a b | a -> b where
+  watchCraft :: a -> Craft (Watched, b)
+
+  craft :: a -> Craft b
+  craft x = snd <$> watchCraft x
+
+  craft_ :: a -> Craft ()
+  craft_ = void . craft
+
+  watchCraft_ :: a -> Craft Watched
+  watchCraft_ x = fst <$> watchCraft x
+
+  {-# MINIMAL watchCraft #-}
+
+
+class Destroyable a where
+  watchDestroy :: a -> Craft (Watched, Maybe a)
+
+  destroy :: a -> Craft (Maybe a)
+  destroy x = snd <$> watchDestroy x
+
+  destroy_ :: a -> Craft ()
+  destroy_ = void . destroy
+
+  watchDestroy_ :: a -> Craft Watched
+  watchDestroy_ x = fst <$> watchDestroy x
+
+  {-# MINIMAL watchDestroy #-}
+
+
+instance Craftable a b => Craftable [a] [b] where
+  craft_ = mapM_ craft_
+  craft = mapM craft
+  watchCraft as = do
+    (ws, bs) <- unzip <$> mapM watchCraft as
+    let w = if all (== Unchanged) ws then
+              Unchanged
+            else if all (== Created) ws then
+              Created
+            else
+              Updated
+    return (w, bs)
+
+
+instance Destroyable a => Destroyable [a] where
+  destroy_ = mapM_ destroy_
+  destroy xs = do
+    rs <- mapM destroy xs
+    return $ case catMaybes rs of
+               []  -> Nothing
+               rs' -> Just rs'
+  watchDestroy xs = do
+    (ws, rs) <- unzip <$> mapM watchDestroy xs
+    let w = if all (== Unchanged) ws then
+              Unchanged
+            else if all (== Removed) ws then
+              Removed
+            else
+              Updated
+    let res = case catMaybes rs of
+                []  -> Nothing
+                rs' -> Just rs'
+    return (w, res)
+
+
+instance Craftable User.UserOptions User where
+  watchCraft uopts = do
+    let notfound = "craft `"++uopts ^. User.optName++"` failed. Not Found!"
+    let name = UserName $ uopts ^. User.optName
+    User.fromName name >>= \case
+      Nothing           -> do
+        User.createUser uopts
+        User.fromName name >>= \case
+          Nothing          -> $craftError notfound
+          Just createdUser -> do
+            madeChanges <- User.ensureUserOpts createdUser uopts
+            if not madeChanges
+              then return (Created, createdUser)
+              else User.fromName name >>= \case
+                Nothing -> $craftError notfound
+                Just u  -> return (Created, u)
+      Just existingUser -> do
+        madeChanges <- User.ensureUserOpts existingUser uopts
+        if not madeChanges
+          then return (Unchanged, existingUser)
+          else User.fromName name >>= \case
+            Nothing -> $craftError notfound
+            Just u  -> return (Updated, u)
+
+instance Craftable Package Package where
+  watchCraft pkg = do
+    ce <- ask
+    let pm       = ce ^. craftPackageManager
+    let name     = pkg ^. pkgName
+    let version  = pkg ^. pkgVersion
+    let get      = (pm ^. pmGetter) name
+    let install  = (pm ^. pmInstaller) pkg
+    let upgrade  = (pm ^. pmUpgrader) pkg
+    let pkgError = "craft Package `" ++ name ++ "` failed! "
+    let notFound = pkgError ++ "Not Found."
+    let wrongVersion got = pkgError ++ "Wrong Version: " ++ show got ++ " Excepted: " ++ show version
+    get >>= \case                                                                -- Is the package installed?
+      Nothing           -> do                                                    -- It's not installed.
+        install                                                                  -- Install it.
+        get >>= \case                                                            -- Verify the installation.
+          Nothing           -> $craftError notFound                              -- Not Found. The install failed!
+          Just installedPkg ->
+            let ok = return (Created, installedPkg)
+            in case version of                                                   -- Make sure it's the right version
+                 AnyVersion -> ok
+                 Latest     -> ok
+                 Version  _ -> if version == installedPkg ^. pkgVersion
+                                then ok
+                                else $craftError $ wrongVersion (installedPkg ^. pkgVersion)
+      Just installedPkg -> do                                                    -- Package was already installed.
+        let installedVersion = installedPkg ^. pkgVersion
+        case version of
+          AnyVersion -> return (Unchanged, installedPkg)
+          Latest     -> do                                                       -- Ensure it's the latest version.
+            upgrade
+            get >>= \case
+              Nothing          -> $craftError notFound                           -- Not found. Where did it go?
+              Just upgradedPkg ->
+                return $ if upgradedPkg ^. pkgVersion > installedPkg ^. pkgVersion -- If the package version increased,
+                          then (Updated, upgradedPkg)                            -- Then the package was upgraded
+                          else (Unchanged, upgradedPkg)                          -- Else it was already the latest.
+          Version _  ->                                                          -- Expecting a specific version
+            if version == installedVersion                                       -- Is the correct version installed?
+             then return (Unchanged, installedPkg)
+             else do
+               upgrade                                                           -- Try upgrading to the correct version.
+               get >>= \case
+                 Nothing          -> $craftError notFound                        -- Where did it go?
+                 Just upgradedPkg -> if version == upgradedPkg ^. pkgVersion     -- Is the correct version installed?
+                                      then return (Updated, upgradedPkg)
+                                      else $craftError $ wrongVersion (upgradedPkg ^. pkgVersion)
+
+
+instance Destroyable Package where
+  watchDestroy pkg = do
+    ce <- ask
+    let pm   = ce ^. craftPackageManager
+    let name = pkg ^. pkgName
+    let get  = (pm ^. pmGetter) name
+    get >>= \case
+      Nothing -> return (Unchanged, Nothing)
+      Just installedPkg -> do
+        (pm ^. pmUninstaller) pkg
+        get >>= \case
+          Nothing            -> return (Removed, Just installedPkg)
+          Just unexpectedPkg -> $craftError $ "destroy Package `" ++ name ++ "` failed! " ++ "Found: " ++ show unexpectedPkg
+
+
+instance Craftable User User where
+  watchCraft user = do
+    let name = show $ user ^. userName
+    let notFound = $craftError $ "User `" ++ name ++ "` not found!"
+    userFromStr name >>= \case
+      Nothing     -> do
+        useradd user
+        userFromStr name >>= \case
+          Nothing -> notFound
+          Just actualUser -> do
+            verify user actualUser
+            return (Created, actualUser)
+      Just existingUser -> do
+        res <- mapM (\(test', act) -> if test' user existingUser
+                                      then return True
+                                      else act >> return False)
+                 [ (test userName,            $notImplemented "set username")
+                 , (test uid,                 $notImplemented "set uid")
+                 , (test (userGroup . groupName), $notImplemented "set group")
+                 , (test userGroups,              $notImplemented "set groups")
+                 , (test userHome,                $notImplemented "set home")
+                 , (test userPasswordHash,        $notImplemented "set passwordHash")
+                 , (test userShell,               $notImplemented "set shell")
+                 ]
+        if and res
+        then return (Unchanged, existingUser)
+        else
+          userFromStr (show name) >>= \case
+            Nothing -> notFound
+            Just user'' -> do
+              verify user'' user
+              return (Updated, user'')
+
+   where
+    test :: Eq a => Lens' b a -> b -> b -> Bool
+    test l a b = a ^. l == b ^. l
+    verify :: User -> User -> Craft ()
+    verify _expectedUser _actualUser = $notImplemented "verify User"
+
+
+instance Craftable Group Group where
+  watchCraft grp = do
+    _ <- $notImplemented "craft Group"
+    -- groupFromName . groupname
+    exec_ "groupadd" $ toArg "--gid" (grp ^. Craft.Types.gid) ++ [show $ grp ^. groupName]
+    exec_ "gpasswd" ["--members", intercalate "," (map show (grp ^. groupMembers))
+                             , show $ grp ^. groupName]
+    return (Unchanged, grp)
+
+
+instance Craftable File File where
+  watchCraft f = do
+    let fp = f ^. path
+    -- FIXME: Don't use _Just
+    let expectedMD5 = show . md5 . BL.fromStrict $ f ^. fileContent . _Just
+    let err :: String -> Craft a
+        err str = $craftError $ "craft File `"++show fp++"` failed! "++str
+    let verifyMode m =
+          when (m /= f ^. mode) $
+            err $ "Wrong Mode: " ++ show m ++ " Expected: " ++ show (f ^. mode)
+    let verifyOwner o =
+          when (o /= f ^. ownerID) $
+            err $ "Wrong OwnerID: " ++ show o ++ " Expected: " ++ show (f ^. ownerID)
+    let verifyGroup g =
+          when (g /= f ^. groupID) $
+            err $ "Wrong GroupID: " ++ show g ++ " Expected: " ++ show (f ^. groupID)
+    let verifyStats (m, o, g) = verifyMode m >> verifyOwner o >> verifyGroup g
+    getStats fp >>= \case
+      Nothing          -> do
+        case f ^. fileContent of
+          Nothing -> exec_ "touch" [fromAbsFile fp]
+          Just c  -> File.write fp c
+        File.setStats f
+        when (isJust $ f  ^. fileContent) $ do
+          actualMD5 <- File.md5sum fp
+          unless (actualMD5 == expectedMD5) $
+            err "Content Mismatch."
+        return (Created, f)
+      Just (m', o', g') -> do
+        let checks = [ (f ^. mode    == m', setMode    (f ^. mode)    fp)
+                     , (f ^. ownerID == o', setOwnerID (f ^. ownerID) fp)
+                     , (f ^. groupID == g', setGroupID (f ^. groupID) fp)
+                     ]
+        mapM_ (uncurry unless) checks
+        case f ^. fileContent of
+          Nothing -> if all fst checks
+                     then return (Unchanged, f)
+                     else do
+                       getStats fp >>= \case
+                         Nothing    -> err "Not Found."
+                         Just stats -> verifyStats stats
+                       return (Updated, f)
+          Just c -> do
+            actualMD5 <- File.md5sum fp
+            if actualMD5 == expectedMD5
+            then return $ if all fst checks
+                          then (Unchanged, f)
+                          else (Updated, f)
+            else do
+              File.write fp c
+              md5AfterWrite <- File.md5sum fp
+              if md5AfterWrite == expectedMD5
+                then return (Updated, f)
+                else err "Content Mismatch."
+
+  craft f = do
+    let fp = f^.path
+    exec_ "touch" ["-a", fromAbsFile fp]
+    case f ^. fileContent of
+      Nothing -> return ()
+      Just c  -> do
+        let expectedMD5 = show . md5 $ BL.fromStrict c
+        actualMD5 <- File.md5sum fp
+        unless (actualMD5 == expectedMD5) $ do
+          File.write fp c
+          md5AfterWrite <- File.md5sum fp
+          unless (expectedMD5 == md5AfterWrite) $
+            $craftError $ "craft File `"++show fp++"` failed! Content Mismatch."
+    File.setStats f
+    return f
+
+
+instance Destroyable File where
+  watchDestroy f =
+    File.get (f ^. path) >>= \case
+      Nothing -> return (Unchanged, Nothing)
+      Just f' -> do
+        destroy_ f
+        return (Removed, Just f')
+
+  destroy_ f = do
+    let fp = f^.path
+    exec_ "rm" ["-f", fromAbsFile fp]
+    File.exists fp >>= flip when (
+      $craftError $ "destroy File `"++show fp++"` failed! Found.")
+
+
+instance Craftable Directory Directory where
+  watchCraft d = do
+    let dp = d ^. path
+        setMode'  = setMode (d ^. mode) dp
+        setOwner' = setOwnerID (d ^. ownerID) dp
+        setGroup' = setGroupID (d ^. groupID) dp
+        error' :: String -> Craft a
+        error' str = $craftError
+           $ formatToString ("craft Directory `"%string%"` failed! "%string)
+                            (show dp) str
+        verifyMode m =
+          when (m /= d ^. mode) $
+            error' $ formatToString ("Wrong Mode: "%shown%" Expected: "%shown)
+                                    m (d ^. mode)
+        verifyOwner o =
+          when (o /= d ^. ownerID) $
+            error' $ formatToString ("Wrong Owner ID: "%shown%" Expected: "%shown)
+                                    o (d ^. ownerID)
+        verifyGroup g =
+          when (g /= d ^. groupID) $
+            error' $ formatToString ("Wrong Group ID: "%shown%" Expected: "%shown)
+                                    g (d ^. groupID)
+        verifyStats (m, o, g) =
+          verifyMode m >> verifyOwner o >> verifyGroup g
+    getStats dp >>= \case
+      Nothing -> do
+        exec_ "mkdir" ["-p", fromAbsDir dp]
+        setMode' >> setOwner' >> setGroup'
+        getStats dp >>= \case
+          Nothing -> error' "Not Found."
+          Just stats' -> verifyStats stats' >> return (Created, d)
+      Just (m', o', g') -> do
+        let checks = [ (d^.mode    == m', setMode')
+                     , (d^.ownerID == o', setOwner')
+                     , (d^.groupID == g', setGroup')
+                     ]
+        mapM_ (uncurry unless) checks
+        if all fst checks then
+          return (Unchanged, d)
+        else
+          getStats dp >>= \case
+            Nothing -> error' "Not Found."
+            Just stats' -> verifyStats stats' >> return (Updated, d)
diff --git a/src/Craft/DSL.hs b/src/Craft/DSL.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/DSL.hs
@@ -0,0 +1,167 @@
+module Craft.DSL where
+
+import           Control.Lens
+import           Control.Monad.Free
+import           Control.Monad.Logger   (logDebugNS)
+import           Control.Monad.Reader
+import qualified Data.ByteString        as BS
+import qualified Data.ByteString.Char8  as B8
+import           Data.List              (intercalate)
+import           Data.List.Split        (splitOn)
+import qualified Data.Map.Strict        as Map
+import qualified Data.Text              as T
+import           Text.Megaparsec
+import           Text.Megaparsec.String
+
+import           Craft.Types
+
+
+-- | Craft DSL
+exec :: Command -> Args -> Craft ExecResult
+exec cmd args = do
+  logDebugNS "exec" $ T.unwords $ map T.pack (cmd:args)
+  ce <- ask
+  liftF $ Exec ce cmd args id
+
+
+exec_ :: Command -> Args -> Craft ()
+exec_ cmd args = do
+  ce <- ask
+  liftF $ Exec_ ce cmd args ()
+
+
+fileRead :: Path Abs FileP -> Craft BS.ByteString
+fileRead fp =
+  liftF $ FileRead fp id
+
+
+fileWrite :: Path Abs FileP -> BS.ByteString -> Craft ()
+fileWrite fp content =
+  liftF $ FileWrite fp content ()
+
+
+sourceFile :: (IO FilePath) -> Path Abs FileP -> Craft ()
+sourceFile sourcer dest = liftF $ SourceFile sourcer dest ()
+
+
+parseExecResult :: ExecResult -> Parsec String a -> String -> Craft a
+parseExecResult execr parser str =
+  case parse parser (showProc $ execResultProc execr) str of
+    Right x -> return x
+    Left err -> $craftError $
+      unlines
+      [ "parseExecResult error:"
+      , "<<<< process >>>>"
+      , showProc $ execResultProc execr
+      , "<<<< output >>>>"
+      , str
+      , "<<<< parse error >>>>"
+      , show err
+      ]
+
+
+-- | better than grep
+parseExecStdout :: Parsec StdOut a -> Command -> Args -> Craft a
+parseExecStdout parser cmd args = do
+  r <- exec cmd args
+  s <- $stdoutOrError r
+  parseExecResult r parser s
+
+
+-- XXX: What if the file doesn't exist?
+parseFile :: Parser a -> Path Abs FileP -> Craft a
+parseFile parser fp = do
+  str <- B8.unpack <$> fileRead fp
+  case runParser parser (show fp) str of
+    Right x  -> return x
+    Left err -> $craftError $ "parseFile `"++show fp++"` failed: "++show err
+
+
+craftExecPath :: CraftEnv -> Craft [Path Abs Dir]
+craftExecPath ce = do
+  let dps = maybe [] (splitOn ":") . Map.lookup "PATH" $ ce^.craftExecEnv
+  mapM parseAbsDir dps
+
+
+-- TESTME
+prependPath :: Path Abs Dir -> Craft a -> Craft a
+prependPath newpath go = do
+  ce <- ask
+  execpath <- craftExecPath ce
+  withPath (newpath:execpath) go
+
+
+-- TESTME
+withPath :: [Path Abs Dir] -> Craft a -> Craft a
+withPath = withEnvVar "PATH" . intercalate ":" . map fromAbsDir
+
+
+withEnv :: ExecEnv -> Craft a -> Craft a
+withEnv env = local (\r -> r & craftExecEnv .~ env)
+
+
+withCWD :: Directory -> Craft a -> Craft a
+withCWD dir = local (\r -> r & craftExecCWD .~ dir ^. directoryPath)
+
+
+-- TESTME
+withEnvVar :: String -> String -> Craft a -> Craft a
+withEnvVar name val go = do
+  ce <- ask
+  let env = Map.insert name val $ ce ^. craftExecEnv
+  withEnv env go
+
+
+isExecSucc :: ExecResult -> Bool
+isExecSucc (ExecSucc _) = True
+isExecSucc (ExecFail _) = False
+
+setMode :: Mode -> Path b t -> Craft ()
+setMode m fp = exec_ "chmod" [toOctalString m, toFilePath fp]
+
+setOwnerID :: UserID -> Path b t -> Craft ()
+setOwnerID (UserID i) fp = exec_ "chown" [show i, toFilePath fp]
+
+
+setGroupID :: GroupID -> Path b t -> Craft ()
+setGroupID (GroupID i) fp = exec_ "chgrp" [show i, toFilePath fp]
+
+
+stat :: Command
+stat = "stat"
+
+getMode :: Path Abs t -> Craft Mode
+getMode fp = parseExecStdout modeParser stat ["-c", "%a", toFilePath fp]
+
+
+getOwnerID :: Path Abs t -> Craft UserID
+getOwnerID fp = UserID <$> parseExecStdout digitParser stat ["-c", "%u", toFilePath fp]
+
+
+getGroupID :: Path Abs t -> Craft GroupID
+getGroupID fp = GroupID <$> parseExecStdout digitParser stat ["-c", "%g", toFilePath fp]
+
+
+getStats :: Path Abs t -> Craft (Maybe (Mode, UserID, GroupID))
+getStats fp =
+  exec stat ["-c", "%a:%u:%g", toFilePath fp] >>= \case
+    ExecFail _ -> return Nothing
+    ExecSucc r -> Just <$> parseExecResult (ExecSucc r) statsParser (r^.stdout)
+
+
+statsParser :: Parser (Mode, UserID, GroupID)
+statsParser = do
+  mode' <- modeParser
+  void $ char ':'
+  owner' <- UserID <$> digitParser
+  void $ char ':'
+  group' <- GroupID <$> digitParser
+  return (mode', owner', group')
+
+
+modeParser :: Parser Mode
+modeParser = fromOctalString <$> some digitChar
+
+
+digitParser :: Parser Int
+digitParser = read <$> some digitChar
diff --git a/src/Craft/Daemontools.hs b/src/Craft/Daemontools.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Daemontools.hs
@@ -0,0 +1,120 @@
+module Craft.Daemontools where
+
+import           Control.Lens
+import           Data.ByteString (ByteString)
+
+import           Craft
+import qualified Craft.Upstart   as Upstart
+
+
+data Service
+  = Service
+    { _name     :: String
+    , _home     :: Path Abs Dir
+    , _env      :: [(String, String)]
+    , _runFile  :: ByteString
+    , _restarts :: Bool
+    }
+  deriving (Eq, Show)
+makeLenses ''Service
+
+
+service :: String -> Service
+service name' =
+  Service { _name     = name'
+          , _home     = $(mkAbsDir "/etc/svc")
+          , _env      = []
+          , _runFile  = ""
+          , _restarts = True
+          }
+
+
+setup :: Path Abs Dir -> Craft ()
+setup home' = do
+  craft_ $ map package [ "daemontools"
+                       , "daemontools-run"
+                       ]
+  craft_ $ directory home'
+  Upstart.get "svscan" >>= \case
+    Nothing     -> $craftError "Upstart service `svscan` not found!"
+    Just svscan -> Upstart.start svscan
+
+
+path :: Service -> Craft (Path Abs Dir)
+path Service{..} = do
+  sn <- parseRelDir _name
+  return $ _home </> sn
+
+
+restart :: Service -> Craft ()
+restart s = do
+  sp <- Craft.Daemontools.path s
+  execRestart sp
+
+
+execRestart :: Path Abs Dir -> Craft ()
+execRestart fp = exec_ "svc" ["-t", fromAbsDir fp]
+
+
+logRunDefault :: Path Abs FileP -> String
+logRunDefault logdest =
+  "#!/bin/sh\nexec setuidgid nobody multilog t "++fromAbsFile logdest++" s10000000 n10\n"
+
+
+envFile :: Path Abs Dir -> (String, String) -> Craft File
+envFile envDir (varname, varval) = do
+  fn <- parseRelFile varname
+  return $ file (envDir</>fn)
+           & mode       .~ Mode RW O O
+           & strContent .~ varval
+
+
+{-
+getEnv :: Service -> Craft [(String, String)]
+getEnv svc = do
+  sp <- Craft.Daemontools.path svc
+  fs <- Directory.getFiles sp
+  return $ map go fs
+ where
+  go f = (f ^. File.name, f ^. strContent)
+
+
+instance Craftable Service where
+  checker svc = File.get (path svc </> "run") >>= \case
+    Nothing -> return Nothing
+    Just  f -> do
+      env' <- getEnv svc
+      return . Just $
+        svc { runFile = fromJust $ File.content f
+            , env = env'
+            }
+
+  crafter s@Service{..} _ = do
+    craft_ $ directory home
+
+    let svcdir = path s
+    craft_ $ directory svcdir
+    craft_ $ Link svcdir $ "/etc/service" </> name
+
+    runW <- fst <$> watchCraft ((file $ svcdir </> "run")
+                                { File.mode    = Mode RWX RX RX
+                                , File.content = Just runFile
+                                })
+
+    let logDir = svcdir </> "log"
+    craft_ $ directory logDir
+
+    let logDest = logDir </> "main"
+    nobody  <- fromJust <$> User.fromName "nobody"
+    nogroup <- fromJust <$> Group.fromName "nogroup"
+    craft_ $ (directory logDest)
+                { Directory.ownerID = User.uid nobody
+                , Directory.groupID = Group.gid nogroup
+                }
+
+    let envDir = svcdir </> "env"
+    craft_ $ directory envDir
+    mapM_ (craft . envFile envDir) env
+
+    when (restarts && updated runW) $ restart s
+-}
diff --git a/src/Craft/Directory.hs b/src/Craft/Directory.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Directory.hs
@@ -0,0 +1,65 @@
+module Craft.Directory
+( module Craft.Directory
+, setGroupID
+, setOwnerID
+, getOwnerID
+, getGroupID
+, getMode
+)
+where
+
+import           Control.Lens
+import           Data.Maybe             (catMaybes)
+import           Formatting
+
+import           Craft.Directory.Parser
+import qualified Craft.File             as File
+import           Craft.File.Mode
+import qualified Craft.Group            as Group
+import           Craft.Internal
+import qualified Craft.User             as User
+
+
+getOwner :: Directory -> Craft User
+getOwner d =
+  User.fromID (d ^. ownerID) >>= \case
+    Nothing -> $craftError $ formatToString ("No such owner with `"%shown%"` for: "%shown)
+                                            (d ^. ownerID) d
+    Just g  -> return g
+
+
+getGroup :: Directory -> Craft Group
+getGroup d =
+  Group.fromID (d ^. groupID) >>= \case
+    Nothing -> $craftError $ formatToString ("No such group with `"%shown%"` for: "%shown)
+                                            (d ^. groupID) d
+    Just g -> return g
+
+
+multiple :: [Path Abs Dir] -> Mode -> User -> Group -> [Directory]
+multiple paths mode' owner' group' = map go paths
+ where
+  go p = Directory p mode' (owner'^.uid) (group'^.gid)
+
+
+multipleRootOwned :: [Path Abs Dir] -> Mode -> [Directory]
+multipleRootOwned paths m = map go paths
+ where
+  go p = directory p & mode .~ m
+
+
+exists :: Path Abs Dir -> Craft Bool
+exists p = isExecSucc <$> exec "test" ["-d", fromAbsDir p]
+
+
+get :: Path Abs Dir -> Craft (Maybe Directory)
+get dp =
+  getStats dp >>= \case
+    Nothing -> return Nothing
+    Just (m, o, g) -> return . Just $ Directory dp m o g
+
+
+getFiles :: Path Abs Dir -> Craft [File]
+getFiles dp = do
+  fns <- mapM parseRelFile =<< parseExecStdout getFilesParser "ls" ["-a", "-1", fromAbsDir dp]
+  catMaybes <$> mapM (File.get . (dp </>)) fns
diff --git a/src/Craft/Directory/Parser.hs b/src/Craft/Directory/Parser.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Directory/Parser.hs
@@ -0,0 +1,34 @@
+module Craft.Directory.Parser where
+
+-- |This file is just to keep Megaparsec from conflicting with other modules
+
+import           Control.Monad   (void)
+import           Text.Megaparsec
+
+
+getFilesParser :: Parsec String [String]
+getFilesParser = stuff `sepBy` newline <* optional newline
+ where
+  stuff :: Parsec String String
+  stuff = do
+    void $ optional $ string "." >> newline
+    void $ optional $ string ".." >> newline
+    line
+  line :: Parsec String String
+  line = many $ noneOf "\n"
+
+
+testGetFilesParser :: IO Bool
+testGetFilesParser = do
+  let expected = ["ab", "lkjasd", "912 12391", " ", "~"] :: [String]
+  let resultE = parse getFilesParser "testGetFilesParser" $ unlines (".":"..":expected)
+  case resultE of
+    Left err -> do
+      putStrLn $ "FAILED: error " ++ show err
+      return False
+    Right result ->
+      if result /= expected then do
+        putStrLn $ "FAILED: got " ++ show expected
+        return False
+      else
+        return True
diff --git a/src/Craft/Error.hs b/src/Craft/Error.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Error.hs
@@ -0,0 +1,28 @@
+module Craft.Error where
+
+import           Control.Monad.Catch
+import           Control.Monad.Logger       (logError)
+import           Data.Monoid                ((<>))
+import qualified Data.Text                  as T
+import           Language.Haskell.TH.Syntax (Exp, Q)
+
+data CraftError = CraftError String
+  deriving Show
+
+data CraftNotImplemented = CraftNotImplemented
+  deriving Show
+
+instance Exception CraftError
+
+instance Exception CraftNotImplemented
+
+-- |Log an error and throw a runtime exception
+craftError :: Q Exp
+craftError = [|\s -> $(logError) (T.pack s) >> throwM (CraftError s) |]
+
+
+notImplemented :: Q Exp
+notImplemented = [| \s -> do
+  $(logError) $ "Not Implemented: " <> T.pack s
+  throwM CraftNotImplemented
+  |]
diff --git a/src/Craft/Facter.hs b/src/Craft/Facter.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Facter.hs
@@ -0,0 +1,15 @@
+module Craft.Facter where
+
+import           Craft
+
+
+setup :: Craft ()
+setup = craft_ $ package "facter"
+
+
+fact :: String -> Craft String
+fact f = rmTrailingNL <$> ($stdoutOrError =<< exec "facter" [f])
+
+
+rmTrailingNL :: String -> String
+rmTrailingNL = reverse . dropWhile (=='\n') . reverse
diff --git a/src/Craft/File.hs b/src/Craft/File.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/File.hs
@@ -0,0 +1,92 @@
+module Craft.File
+( module Craft.File
+, setGroupID
+, setOwnerID
+, getOwnerID
+, getGroupID
+, getMode
+)
+where
+
+import           Control.Lens
+import           Data.ByteString (ByteString)
+import           Data.Maybe
+import           Formatting
+
+import qualified Craft.Group     as Group
+import           Craft.Internal
+import qualified Craft.User      as User
+
+
+getOwner :: File -> Craft User
+getOwner f =
+  User.fromID (f ^. ownerID) >>= \case
+    Nothing -> $craftError $ formatToString ("No such owner with `"%shown%"` for: "%shown)
+                                            (f ^. ownerID) f
+    Just g  -> return g
+
+
+getGroup :: File -> Craft Group
+getGroup f =
+  Group.fromID (f ^. groupID) >>= \case
+    Nothing -> $craftError $ formatToString ("No such group with `"%shown%"` for: "%shown)
+                                            (f ^. groupID) f
+    Just g -> return g
+
+
+-- TODO: name :: Lens' File (Path Rel FileP)
+name :: Getter File (Path Rel FileP)
+name = filePath . to filename
+
+
+multiple :: [Path Abs FileP] -> Mode -> User -> Group -> Maybe ByteString -> [File]
+multiple paths mode' owner' group' content' = map go paths
+ where
+  go path' = File path' mode' (owner'^.uid) (group'^.gid) content'
+
+
+setStats :: File -> Craft ()
+setStats f = do
+  let fp = f^.path
+  setMode    (f^.mode)    fp
+  setOwnerID (f^.ownerID) fp
+  setGroupID (f^.groupID) fp
+
+
+write :: Path Abs FileP -> ByteString -> Craft ()
+write = fileWrite
+
+
+exists :: Path Abs FileP -> Craft Bool
+exists fp = isExecSucc <$> exec "test" ["-f", fromAbsFile fp]
+
+
+get :: Path Abs FileP -> Craft (Maybe File)
+get fp =
+  getStats fp >>= \case
+    Nothing -> return Nothing
+    Just (m, o, g) -> do
+      return . Just $ file fp & mode    .~ m
+                              & ownerID .~ o
+                              & groupID .~ g
+                              & fileContent .~ Nothing
+
+
+getWithContent :: Path Abs FileP -> Craft (Maybe File)
+getWithContent fp =
+  get fp >>= \case
+    Nothing -> return Nothing
+    Just f  -> do
+      content' <- fileRead fp
+      return . Just $ f & fileContent ?~ content'
+
+
+md5sum :: Path Abs FileP -> Craft String
+md5sum fp = head . words <$> ($stdoutOrError =<< exec "md5sum" [fromAbsFile fp])
+
+
+-- | A thin wrapper over the Unix find program.
+find :: Path Abs Dir -> Args -> Craft [File]
+find dir args = do
+  fs <- mapM parseAbsFile . lines =<< $stdoutOrError =<< exec "find" ([fromAbsDir dir, "-type", "f"] ++ args)
+  catMaybes <$> (mapM get fs)
diff --git a/src/Craft/File/Link.hs b/src/Craft/File/Link.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/File/Link.hs
@@ -0,0 +1,70 @@
+module Craft.File.Link where
+
+import           Control.Lens
+
+import           Craft
+
+
+newtype TargetPath = TargetPath FilePath
+  deriving (Show, Eq)
+
+newtype LinkPath = LinkPath (Path Abs FileP)
+  deriving (Show, Eq)
+
+data Link
+ = Link
+   { _linkTarget :: TargetPath
+   , _linkPath   :: LinkPath
+   }
+  deriving (Eq, Show)
+makeLenses ''Link
+
+-- TODO: intsance FileLike Link where
+
+
+link :: TargetPath -> LinkPath -> Link
+link = Link
+
+
+exists :: Path Abs FileP -> Craft Bool
+exists lp = isExecSucc <$> exec "test" ["-L", fromAbsFile lp]
+
+
+readlink :: LinkPath -> Craft (Maybe TargetPath)
+readlink (LinkPath lp) =
+  exec "readlink" [fromAbsFile lp] >>= \case
+    ExecFail _ -> return Nothing
+    ExecSucc r -> return . Just . TargetPath . trimTrailing $ r^.stdout
+
+
+create :: Link -> Craft ()
+create (Link t p) = justdoit t p
+ where
+  justdoit (TargetPath tp) (LinkPath lp) = exec_ "ln" ["-snf", tp, fromAbsFile lp]
+
+
+instance Craftable Link Link where
+  watchCraft l = do
+    let lp = l ^. linkPath
+    readlink lp >>= \case
+      Nothing -> do
+        craft_ l
+        return (Created, l)
+      Just target' ->
+        if target' == l^.linkTarget then
+          return (Unchanged, l)
+        else do
+          craft_ l
+          return (Updated, l)
+
+  craft_ l = do
+    let lp = l ^. linkPath
+    let tp = l ^. linkTarget
+    create l
+    readlink lp >>= \case
+      Nothing -> $craftError $ "craft `" ++ show l ++ "` failed! Not Found."
+      Just target' ->
+        when (target' /= tp) $
+          $craftError $ "craft `" ++ show l ++ "` failed! "
+                     ++ "Wrong Target: " ++ show target' ++ " "
+                     ++ "Expected: "    ++ show (l ^. linkTarget)
diff --git a/src/Craft/File/Mode.hs b/src/Craft/File/Mode.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/File/Mode.hs
@@ -0,0 +1,239 @@
+module Craft.File.Mode
+( Mode(..)
+, ModeSet(..)
+, toFileMode
+, toMode
+, toHuman
+, fileModeFromString
+, toOctalString
+, fromOctalString
+)
+where
+
+import           Data.Bits       ((.&.), (.|.))
+import           Data.Char       (digitToInt)
+import           Data.DeriveTH
+import           Data.List
+import           System.Posix    (FileMode)
+import qualified System.Posix
+import           Test.QuickCheck (Arbitrary, arbitrary, choose)
+
+
+toHuman :: Mode -> String
+toHuman (Mode u g o) =
+  modeSetToHuman u ++ modeSetToHuman g ++ s2t (modeSetToHuman o)
+ where
+  s2t = map tr
+  tr 's' = 't'
+  tr 'S' = 'T'
+  tr   c = c
+
+
+data ModeSet
+ = O -- ^ O was chosen because it looks like a zero.
+ | R
+ | W
+ | X
+ | S
+ | RW
+ | RX
+ | RS
+ | WX
+ | WS
+ | XS
+ | RWX
+ | RWS
+ | RXS
+ | WXS
+ | RWXS
+ deriving (Eq, Enum, Show)
+
+data Mode
+  = Mode ModeSet ModeSet ModeSet
+  deriving (Eq, Show)
+
+
+toOctalString :: Mode -> String
+toOctalString (Mode u g o) =
+  concatMap show $ stickies : map modeSetToOctal l
+ where
+  stickies = sum $ zipWith (\n m -> n * modeSetToOctalSticky m) [4,2,1] l
+  l = [u,g,o]
+
+
+modeSetToOctal :: ModeSet -> Int
+modeSetToOctal O    = 0
+modeSetToOctal R    = 4
+modeSetToOctal W    = 2
+modeSetToOctal X    = 1
+modeSetToOctal S    = 0
+modeSetToOctal RW   = 6
+modeSetToOctal RX   = 5
+modeSetToOctal RS   = 4
+modeSetToOctal WX   = 3
+modeSetToOctal WS   = 2
+modeSetToOctal XS   = 1
+modeSetToOctal RWX  = 7
+modeSetToOctal RWS  = 6
+modeSetToOctal RXS  = 5
+modeSetToOctal WXS  = 3
+modeSetToOctal RWXS = 7
+
+
+modeSetToOctalSticky :: ModeSet -> Int
+modeSetToOctalSticky O    = 0
+modeSetToOctalSticky R    = 0
+modeSetToOctalSticky W    = 0
+modeSetToOctalSticky X    = 0
+modeSetToOctalSticky S    = 1
+modeSetToOctalSticky RW   = 0
+modeSetToOctalSticky RX   = 0
+modeSetToOctalSticky RS   = 1
+modeSetToOctalSticky WX   = 0
+modeSetToOctalSticky WS   = 1
+modeSetToOctalSticky XS   = 1
+modeSetToOctalSticky RWX  = 0
+modeSetToOctalSticky RWS  = 1
+modeSetToOctalSticky RXS  = 1
+modeSetToOctalSticky WXS  = 1
+modeSetToOctalSticky RWXS = 1
+
+
+modeSetToHuman :: ModeSet -> String
+modeSetToHuman O    = "---"
+modeSetToHuman R    = "r--"
+modeSetToHuman W    = "-w-"
+modeSetToHuman X    = "--x"
+modeSetToHuman S    = "--S"
+modeSetToHuman RW   = "rw-"
+modeSetToHuman RX   = "r-x"
+modeSetToHuman RS   = "r-S"
+modeSetToHuman WX   = "-wx"
+modeSetToHuman WS   = "-wS"
+modeSetToHuman XS   = "--s"
+modeSetToHuman RWX  = "rwx"
+modeSetToHuman RWS  = "rwS"
+modeSetToHuman RXS  = "r-s"
+modeSetToHuman WXS  = "-ws"
+modeSetToHuman RWXS = "rws"
+
+
+fromOctalString :: String -> Mode
+fromOctalString = toMode . fileModeFromString . filter (`elem` ['0'..'7'])
+
+
+fileModeFromString :: String -> FileMode
+fileModeFromString [] = error "Cannot get Mode from empty string."
+fileModeFromString s@[_] = error $ "Mode `" ++ s ++ "` not long enough."
+fileModeFromString s@[_,_] = error $ "Mode `" ++ s ++ "` not long enough."
+fileModeFromString [u,g,o] =
+  fromIntegral $ digitToInt u * (8*8) .|. digitToInt g * 8 .|. digitToInt o
+fileModeFromString [s,u,g,o] =
+  fromIntegral $ digitToInt s * (8*8*8) .|. digitToInt u * (8*8) .|. digitToInt g * 8 .|. digitToInt o
+fileModeFromString s = error $ "Mode `" ++ s ++ "` is too long"
+
+
+toFileMode :: Mode -> FileMode
+toFileMode (Mode u g o)
+  = uFM u .|. gFM g .|. oFM o
+
+
+toMode :: FileMode -> Mode
+toMode fm = Mode ownerSet groupSet otherSet
+  where
+    convertSet f m =
+      case find (\t -> m == f t) [O ..] of
+        Nothing -> error $ "toMode: Unsupported mode: " ++ show m
+        Just r  -> r
+    ownerSet = convertSet uFM (fm .&. (System.Posix.ownerModes .|. uS))
+    groupSet = convertSet gFM (fm .&. (System.Posix.groupModes .|. gS))
+    otherSet = convertSet oFM (fm .&. (System.Posix.otherModes .|. oT))
+
+
+----------------------------------------
+--   ____       _            _        --
+--  |  _ \ _ __(_)_   ____ _| |_ ___  --
+--  | |_) | '__| \ \ / / _` | __/ _ \ --
+--  |  __/| |  | |\ V / (_| | ||  __/ --
+--  |_|   |_|  |_| \_/ \__,_|\__\___| --
+----------------------------------------
+
+uR, uW, uX, uS :: FileMode
+uR = System.Posix.ownerReadMode
+uW = System.Posix.ownerWriteMode
+uX = System.Posix.ownerExecuteMode
+uS = System.Posix.setUserIDMode
+
+
+uFM :: ModeSet -> FileMode
+uFM O    = System.Posix.nullFileMode
+uFM R    = uR
+uFM  W   =        uW
+uFM   X  =               uX
+uFM    S =                      uS
+uFM RW   = uR .|. uW
+uFM RX   = uR .|.        uX
+uFM RS   = uR .|.               uS
+uFM WX   =        uW .|. uX
+uFM WS   =        uW .|.        uS
+uFM XS   =               uX .|. uS
+uFM RWX  = uR .|. uW .|. uX
+uFM RWS  = uR .|. uW .|.        uS
+uFM RXS  = uR .|.        uX .|. uS
+uFM WXS  =        uW .|. uX .|. uS
+uFM RWXS = uR .|. uW .|. uX .|. uS
+
+
+gR, gW, gX, gS :: FileMode
+gR = System.Posix.groupReadMode
+gW = System.Posix.groupWriteMode
+gX = System.Posix.groupExecuteMode
+gS = System.Posix.setGroupIDMode
+
+gFM :: ModeSet -> FileMode
+gFM O    = System.Posix.nullFileMode
+gFM R    = gR
+gFM  W   =        gW
+gFM   X  =               gX
+gFM    S =                      gS
+gFM RW   = gR .|. gW
+gFM RX   = gR .|.        gX
+gFM RS   = gR .|.               gS
+gFM WX   =        gW .|. gX
+gFM WS   =        gW .|.        gS
+gFM XS   =               gX .|. gS
+gFM RWX  = gR .|. gW .|. gX
+gFM RWS  = gR .|. gW .|.        gS
+gFM RXS  = gR .|.        gX .|. gS
+gFM WXS  =        gW .|. gX .|. gS
+gFM RWXS = gR .|. gW .|. gX .|. gS
+
+
+oR, oW, oX, oT :: FileMode
+oR = System.Posix.otherReadMode
+oW = System.Posix.otherWriteMode
+oX = System.Posix.otherExecuteMode
+oT = (512)
+
+
+oFM :: ModeSet -> FileMode
+oFM O    = System.Posix.nullFileMode
+oFM R    = oR
+oFM  W   =        oW
+oFM   X  =               oX
+oFM    S =                      oT
+oFM RW   = oR .|. oW
+oFM RX   = oR .|.        oX
+oFM RS   = oR .|.               oT
+oFM WX   =        oW .|. oX
+oFM WS   =        oW .|.        oT
+oFM XS   =               oX .|. oT
+oFM RWX  = oR .|. oW .|. oX
+oFM RWS  = oR .|. oW .|.        oT
+oFM RXS  = oR .|.        oX .|. oT
+oFM WXS  =        oW .|. oX .|. oT
+oFM RWXS = oR .|. oW .|. oX .|. oT
+
+
+derive makeArbitrary ''ModeSet
+derive makeArbitrary ''Mode
diff --git a/src/Craft/File/Sourced.hs b/src/Craft/File/Sourced.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/File/Sourced.hs
@@ -0,0 +1,66 @@
+module Craft.File.Sourced where
+
+import           Control.Lens
+
+import           Craft
+import qualified Craft.File   as File
+
+
+data SourcedFile
+  = SourcedFile
+    { _destination :: File
+    , _sourcer     :: IO FilePath
+    }
+
+makeLenses ''SourcedFile
+
+
+sourcedFile :: (IO FilePath) -> Path Abs FileP -> SourcedFile
+sourcedFile sourcer' dest =
+  SourcedFile
+  { _destination = Craft.file dest
+  , _sourcer = sourcer'
+  }
+
+
+instance FileLike SourcedFile where
+  type FileLikePath SourcedFile = Path Abs FileP
+  path    = destination . filePath
+  mode    = destination . fileMode
+  ownerID = destination . fileOwnerID
+  groupID = destination . fileGroupID
+
+
+instance Craftable SourcedFile SourcedFile where
+  craft sf = do
+    let sf' = sf & destination . fileContent .~ Nothing
+    sourceFile (sf'^.sourcer) (sf'^.path)
+    craft_ $ sf' ^. destination
+    return sf'
+
+  watchCraft sf = do
+    let fp = sf ^. destination . filePath
+    let sf' = sf & destination . fileContent .~ Nothing
+    exists <- File.exists fp
+    if exists then do
+      oldsum <- File.md5sum fp
+      sourceFile (sf^.sourcer) fp
+      fw <- watchCraft_ $ sf' ^. destination
+      newsum <- File.md5sum fp
+      if oldsum /= newsum then
+        return (Updated, sf')
+      else
+        return (fw, sf')
+    else do
+      sourceFile (sf^.sourcer) fp
+      craft_ $ sf' ^. destination
+      return (Created, sf')
+
+
+instance Destroyable SourcedFile where
+  watchDestroy sf = do
+    (w, mbf) <- watchDestroy $ sf^.destination
+    let res = case mbf of
+                Nothing -> Nothing
+                Just f  -> Just $ sf & destination .~ f
+    return (w, res)
diff --git a/src/Craft/Git.hs b/src/Craft/Git.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Git.hs
@@ -0,0 +1,165 @@
+module Craft.Git where
+
+import           Control.Lens           hiding (noneOf)
+import           Formatting             hiding (char)
+import           Text.Megaparsec
+import           Text.Megaparsec.String
+
+import           Craft                  hiding (Version (..))
+import qualified Craft.Directory        as Dir
+
+
+type BranchName = String
+type TagName    = String
+type SHA        = String
+
+
+data Version
+  = Branch BranchName
+  | Latest BranchName
+  | Tag    TagName
+  | Commit SHA
+ deriving (Eq)
+
+
+instance Show Version where
+  show (Branch name) = name
+  show (Latest name) = name
+  show (Tag    name) = "tags/" ++ name
+  show (Commit sha)  = sha
+
+
+origin :: String
+origin = "origin"
+
+
+type URL = String
+
+
+data Repo
+  = Repo
+    { _gitUrl       :: URL
+    , _gitDirectory :: Directory
+    , _gitVersion   :: Version
+    , _gitDepth     :: Maybe Int
+    }
+  deriving (Eq, Show)
+makeLenses ''Repo
+
+
+repo :: URL -> Path Abs Dir -> Repo
+repo url' dirpath =
+  Repo
+  { _gitUrl       = url'
+  , _gitDirectory = directory dirpath
+  , _gitVersion   = Latest "master"
+  , _gitDepth     = Nothing
+  }
+
+
+gitBin :: String
+gitBin = "git"
+
+
+git :: String -> [String] -> Craft ()
+git cmd args = exec_ gitBin $ cmd : args
+
+
+remotes :: Craft [String]
+remotes = lines <$> ($stdoutOrError =<< exec gitBin ["remote"])
+
+
+setURL :: URL -> Craft ()
+setURL url' = do
+  rs <- remotes
+  if origin `elem` rs then
+    git "remote" ["set-url", origin, url']
+  else
+    git "remote" ["add", origin, url']
+
+
+getURL :: Craft URL
+getURL = do
+  results <- parseExecStdout repoURLParser gitBin ["remote", "-v"]
+  case lookup (origin, "fetch") results of
+    Nothing -> $craftError $ "git remote `" ++ origin ++ "` not found."
+    Just x  -> return x
+
+
+-- TESTME
+repoURLParser :: Parser [((String, String), String)]
+repoURLParser = some $ do
+  remote <- word
+  url' <- word
+  direction <- char '(' *> some alphaNumChar <* char ')' <* newline
+  return ((remote, direction), url')
+ where
+  word :: Parser String
+  word = some (noneOf " \t") <* some (spaceChar <|> tab)
+
+
+getVersion :: Craft Version
+getVersion = Commit <$> parseExecStdout (some alphaNumChar) gitBin ["rev-parse", "HEAD"]
+
+
+get :: Path Abs Dir -> Craft (Maybe Repo)
+get dp =
+  Dir.get dp >>= \case
+    Nothing -> return Nothing
+    Just dir -> withCWD dir $ do
+      !url'     <- getURL
+      !version' <- getVersion
+      return . Just
+             $ Repo { _gitDirectory = dir
+                    , _gitUrl       = url'
+                    , _gitVersion   = version'
+                    , _gitDepth     = Nothing
+                    }
+
+
+instance Craftable Repo Repo where
+  watchCraft r = do
+    let dp = r ^. gitDirectory . path
+        ver = r ^. gitVersion
+        verify ver' =
+          case ver of
+            Commit _ ->
+              when (ver' /= ver) $
+                $craftError
+                $ formatToString ("craft Git.Repo `"%shown%"` failed! Wrong Commit: "%shown%" Got: "%shown)
+                                 r ver ver'
+            _ -> return ()
+        checkoutCommit :: Craft Version
+        checkoutCommit = do
+          setURL $ r ^. gitUrl
+          git "fetch" [origin]
+          git "checkout" ["--force", show ver]
+          git "reset" ["--hard"]
+          ver' <- getVersion
+          verify ver'
+          return ver'
+
+    Dir.get dp >>= \case
+      Nothing -> do
+        case r ^. gitDepth of
+          Nothing -> git "clone" [r ^. gitUrl, fromAbsDir dp]
+          Just d  -> git "clone" ["--depth", show d, r ^. gitUrl, fromAbsDir dp]
+        Dir.get dp >>= \case
+          Nothing -> $craftError $ "craft Git.Repo `"++show r++"` failed! "
+                                ++ "Clone Failed. Directory `"++show dp++"` not found."
+          Just dir -> do
+            craft_ $ r ^. gitDirectory
+            withCWD dir $ do
+              ver' <- checkoutCommit
+              return (Created, r & gitVersion .~ ver' )
+
+      Just dir -> do
+        craft_ $ r ^. gitDirectory
+        withCWD dir $ do
+          !beforeVersion <- getVersion
+          ver' <- checkoutCommit
+          case ver of
+            Latest branchName -> git "pull" [origin, branchName]
+            _                 -> return ()
+          return ( if beforeVersion == ver' then Unchanged else Updated
+                 , r & gitVersion .~ ver')
diff --git a/src/Craft/Group.hs b/src/Craft/Group.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Group.hs
@@ -0,0 +1,50 @@
+module Craft.Group where
+
+import           Control.Lens
+import           Data.List                (intercalate)
+
+import           Craft.Internal
+import           Craft.Internal.Helpers
+import           Craft.Internal.UserGroup
+
+type Name = GroupName
+
+name :: Lens' Group GroupName
+name = groupName
+
+data Options =
+  Options
+  { optGID       :: Maybe GroupID
+  , optAllowdupe :: Bool
+  , optUsers     :: [UserName]
+  , optSystem    :: Bool
+  }
+
+opts :: Options
+opts =
+  Options
+  { optGID       = Nothing
+  , optAllowdupe = False
+  , optUsers     = []
+  , optSystem    = False
+  }
+
+createGroup :: Name -> Options -> Craft Group
+createGroup gn Options{..} = do
+  exec_ "groupadd" args
+  exec_ "gpasswd" ["--members", intercalate "," (map show optUsers), show gn]
+  fromName gn >>= \case
+    Nothing -> $craftError $ "createGroup `" ++ show gn ++ "` failed. Not Found!"
+    Just g -> return g
+ where
+  args = concat
+   [ toArg "--gid"        optGID
+   , toArg "--non-unique" optAllowdupe
+   , toArg "--system"     optSystem
+   ]
+
+fromName :: GroupName -> Craft (Maybe Group)
+fromName (GroupName s) = groupFromStr s
+
+fromID :: GroupID -> Craft (Maybe Group)
+fromID = groupFromID
diff --git a/src/Craft/Helpers.hs b/src/Craft/Helpers.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Helpers.hs
@@ -0,0 +1,37 @@
+module Craft.Helpers
+( module Craft.Helpers
+, when
+, unless
+, void
+, md5
+)
+where
+
+import           Control.Monad        (unless, void, when)
+import           Data.Digest.Pure.MD5 (md5)
+import           System.Console.ANSI
+
+
+color :: Color -> IO a -> IO a
+color c action = do
+  setSGR [SetColor Foreground Vivid c]
+  r <- action
+  setSGR [Reset]
+  return r
+
+
+trimTrailing :: String -> String
+trimTrailing = reverse . dropWhile (`elem` ("\n\r\t " :: String)) . reverse
+
+
+whenM :: Monad m => m Bool -> m () -> m ()
+whenM mbool action = mbool >>= flip when action
+
+
+unlessM :: Monad m => m Bool -> m () -> m ()
+unlessM mbool action = mbool >>= flip unless action
+
+
+whenJust :: Monad m => Maybe a -> (a -> m ()) -> m ()
+whenJust Nothing  _   = return ()
+whenJust (Just v) act = act v
diff --git a/src/Craft/Hostname.hs b/src/Craft/Hostname.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Hostname.hs
@@ -0,0 +1,47 @@
+module Craft.Hostname where
+
+import           Control.Monad.Logger (logInfo)
+import qualified Data.Text            as T
+
+import           Craft
+import qualified Craft.Hosts          as Hosts
+
+
+data Hostname = Hostname String
+  deriving (Eq, Show)
+
+
+get :: Craft Hostname
+get = Hostname <$> ($stdoutOrError =<< exec "hostname" [])
+
+hostnamePath :: Path Abs FileP
+hostnamePath = $(mkAbsFile "/etc/hostname")
+
+
+instance Craftable Hostname Hostname where
+  watchCraft (Hostname hn) = do
+    (Hostname oldhn) <- get
+    hosts <- Hosts.get
+    if oldhn /= hn then do
+      $logInfo . T.pack $ "Hostname " ++ oldhn ++ " /= " ++ hn
+      hosts' <- craft $ Hosts.set (Hosts.Name hn) (Hosts.IP "127.0.1.1") hosts
+      craft_ $ file hostnamePath & strContent .~ hn
+      exec_ "hostname" [hn]
+      craft_ $ Hosts.deleteName (Hosts.Name oldhn) hosts'
+      (Hostname newhn) <- get
+      when (newhn /= hn) $
+        $craftError $ "craft Hostname failed! Expected: " ++ hn
+                                         ++ " Got: " ++ newhn
+      return (Updated, Hostname hn)
+    else do
+      craft_ $ file hostnamePath & strContent .~ hn
+      case Hosts.lookup (Hosts.IP "127.0.1.1") hosts of
+        Just names ->
+          if Hosts.Name hn `elem` names then
+            return (Unchanged, Hostname hn)
+          else do
+            w <- watchCraft_ $ Hosts.set (Hosts.Name hn) (Hosts.IP "127.0.1.1") hosts
+            return (w, Hostname hn)
+        Nothing -> do
+          w <- watchCraft_ $ Hosts.set (Hosts.Name hn) (Hosts.IP "127.0.1.1") hosts
+          return (w, Hostname hn)
diff --git a/src/Craft/Hosts.hs b/src/Craft/Hosts.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Hosts.hs
@@ -0,0 +1,120 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+module Craft.Hosts
+( IP(..)
+, Name(..)
+, Configs
+, Hosts(..)
+, Craft.Hosts.lookup
+, hostsfp
+, get
+, parse
+, showConfigs
+, toFile
+, fromFile
+, insert
+, deleteIP
+, deleteName
+, delete
+, set
+)
+where
+
+
+import           Control.Lens          hiding (noneOf, set)
+import           Control.Monad         (zipWithM)
+import qualified Data.ByteString.Char8 as B8
+import qualified Data.List             as L
+import           Data.Maybe            (catMaybes)
+
+import           Craft
+import qualified Craft.File            as File
+import           Craft.Hosts.Parser
+import           Craft.Hosts.Types
+
+
+lookup :: IP -> Hosts -> Maybe [Name]
+lookup ip hosts = L.lookup ip $ configs hosts
+
+
+hostsMap :: ((IP, [Name]) -> (IP, [Name])) -> Hosts -> Hosts
+hostsMap f (Hosts cfgs) = Hosts $ Prelude.map f cfgs
+
+
+get :: Craft Hosts
+get =
+  File.getWithContent hostsfp >>= \case
+    Nothing -> $craftError $ fromAbsFile hostsfp ++ " not found!"
+    Just f  -> case f ^. fileContent of
+                 Nothing -> $craftError $ show hostsfp ++ " not found!"
+                 Just bs -> parse $ B8.unpack bs
+
+
+parse :: String -> Craft Hosts
+parse s = do
+  r <- zipWithM parseLine [1..] $ lines s
+  return . Hosts $ catMaybes r
+
+
+instance Craftable Hosts Hosts where
+  watchCraft hosts = do
+    (w, f) <- watchCraft $ toFile hosts
+    r <- fromFile f
+    return (w, r)
+
+
+showConfigs :: Configs -> String
+showConfigs = unlines . map (\(ip, as) -> unwords (show ip:map show as))
+
+
+toFile :: Hosts -> File
+toFile (Hosts cfgs) = file hostsfp & strContent .~ showConfigs cfgs
+
+
+fromFile :: File -> Craft Hosts
+fromFile f =
+  case f ^. fileContent of
+    Nothing -> return $ Hosts []
+    Just c  -> parse $ B8.unpack c
+
+
+insert :: IP -> Name -> Hosts -> Hosts
+insert newip name (Hosts cfgs) = fixUp $ Hosts go
+ where
+  go | any ((== newip) . fst) cfgs = map f cfgs
+     | otherwise           = cfgs ++ [(newip, [name])]
+  f (ip, names) | ip == newip && name `notElem` names = (ip, name:names)
+                | otherwise                           = (ip, names)
+
+
+deleteIP :: IP -> Hosts -> Hosts
+deleteIP ip (Hosts cfgs) = fixUp . Hosts $ filter (\(ip', _) -> ip' /= ip) cfgs
+
+
+deleteName :: Name -> Hosts -> Hosts
+deleteName name = fixUp . hostsMap f
+ where
+   f (ip', names) = (ip', filter (/= name) names)
+
+
+delete :: IP -> Name -> Hosts -> Hosts
+delete ip name = fixUp . hostsMap f
+ where
+  f (ip', names) | ip' == ip = (ip', filter (/= name) names)
+                 | otherwise = (ip', names)
+
+
+set :: Name -> IP -> Hosts -> Hosts
+set name ip hosts = fixUp . insert ip name $ deleteName name hosts
+
+----------------------------------------
+--   ____       _            _        --
+--  |  _ \ _ __(_)_   ____ _| |_ ___  --
+--  | |_) | '__| \ \ / / _` | __/ _ \ --
+--  |  __/| |  | |\ V / (_| | ||  __/ --
+--  |_|   |_|  |_| \_/ \__,_|\__\___| --
+----------------------------------------
+fixUp :: Hosts -> Hosts
+fixUp (Hosts cfgs) = Hosts $ filter f  cfgs
+ where
+  f (IP ip, names) = not (null ip) && not (null names)
+
diff --git a/src/Craft/Hosts/Parser.hs b/src/Craft/Hosts/Parser.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Hosts/Parser.hs
@@ -0,0 +1,91 @@
+module Craft.Hosts.Parser where
+
+-- |This file is just to keep Megaparsec from conflicting with other modules
+
+import           Control.Monad                  (void)
+import           Data.List                      (intercalate)
+import           Text.Megaparsec
+import           Text.Megaparsec.String
+
+import           Craft.Hosts.Types
+import           Craft.Internal.Helpers.Parsing
+import           Craft.Types
+
+
+parseLine :: Int -> String -> Craft (Maybe (IP, [Name]))
+parseLine ln s =
+  case runParser lineParser (fromAbsFile hostsfp) s of
+    Right x  -> return x
+    Left err -> $craftError $ show err
+ where
+  lineParser :: Parser (Maybe (IP, [Name]))
+  lineParser = do
+    pos <- getPosition
+    setPosition $ pos {sourceLine = ln}
+    space
+    try (comment >> return Nothing) <|> try (Just <$> item)
+                                    <|> (end >> return Nothing)
+
+
+comment :: Parser ()
+comment = label "comment" $ do
+  void $ char '#'
+  void $ manyTill (noneOf "\r\n") end
+  return ()
+
+
+item :: Parser (IP, [Name])
+item = do
+  ip <- try ipv4 <|> ipv6
+  skipSome white
+  name <- hostname
+  skipMany white
+  as <- aliases
+  void $ optional comment
+  return (ip, name:as)
+
+
+aliases :: Parser [Name]
+aliases = label "aliases" $ hostname `sepEndBy` many white
+
+
+num :: Parser String
+num = choice [ string "0", nonzero ]
+
+
+nonzero :: Parser String
+nonzero = do
+  a <- oneOf ['1'..'9'] <?> "non-zero digit with non-zero part"
+  rest <- count' 0 2 digitChar
+  if (read (a:rest) :: Int) > 255
+    then fail "IP address parts must be 0 <= x <= 255"
+    else return (a:rest)
+
+
+hostname :: Parser Name
+hostname = Name <$> some (alphaNumChar <|> oneOf ".-")
+
+
+dot :: Parser ()
+dot = void $ char '.'
+
+
+ipv4 :: Parser IP
+ipv4 = label "ipv4 address" $ do
+  p1 <- nonzero
+  dot
+  p2 <- num
+  dot
+  p3 <- num
+  dot
+  p4 <- num
+  notFollowedBy digitChar
+  return . IP $ intercalate "." [p1, p2, p3, p4]
+
+
+ipv6 :: Parser IP
+ipv6 = IP <$> some (hexDigitChar <|> char ':') <?> "ipv6 address"
+
+
+white :: Parser ()
+white = void $ (char ' ' <?> "space character") <|> tab
diff --git a/src/Craft/Hosts/Types.hs b/src/Craft/Hosts/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Hosts/Types.hs
@@ -0,0 +1,29 @@
+module Craft.Hosts.Types where
+
+-- | This file is just to prevent a cyclic import between Hosts.hs
+--  and Hosts/Parser.hs
+import Craft.Types
+
+newtype IP = IP String
+  deriving (Eq)
+
+
+instance Show IP where
+  show (IP ip) = ip
+
+
+newtype Name = Name String
+  deriving (Eq)
+
+
+instance Show Name where
+  show (Name name) = name
+
+
+type Configs = [(IP, [Name])]
+data Hosts = Hosts { configs :: Configs }
+             deriving (Eq, Show)
+
+
+hostsfp :: Path Abs FileP
+hostsfp = $(mkAbsFile "/etc/hosts")
diff --git a/src/Craft/Internal.hs b/src/Craft/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Internal.hs
@@ -0,0 +1,8 @@
+module Craft.Internal
+(module X)
+where
+
+import           Craft.DSL     as X
+import           Craft.Helpers as X
+import           Craft.Run     as X
+import           Craft.Types   as X
diff --git a/src/Craft/Internal/Helpers.hs b/src/Craft/Internal/Helpers.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Internal/Helpers.hs
@@ -0,0 +1,47 @@
+{-# LANGUAGE FlexibleInstances #-}
+module Craft.Internal.Helpers where
+
+import           Data.Char (isSpace)
+import           Path
+
+
+indent :: Int -> String -> String
+indent len text =
+  unlines $ map (replicate len ' ' ++) $ lines text
+
+trim :: String -> String
+trim = f . f
+   where f = reverse . dropWhile isSpace
+
+class ToArg a where
+  toArg :: String -> a -> [String]
+
+instance ToArg String where
+  toArg arg s
+    | s == "" = []
+    | otherwise = [arg, s]
+
+instance ToArg Bool where
+  toArg _   False = []
+  toArg arg True  = [arg]
+
+instance ToArg a => ToArg (Maybe a) where
+  toArg _   Nothing  = []
+  toArg arg (Just v) = toArg arg v
+
+instance ToArg Int where
+  toArg = showArg
+
+instance ToArg (Path b t) where
+  toArg arg v = [arg, toFilePath v]
+
+toArgBool :: String -> String -> Bool -> [String]
+toArgBool a _ True  = [a]
+toArgBool _ b False = [b]
+
+showArg :: Show a => String -> a -> [String]
+showArg arg v = [arg, show v]
+
+toArgs :: ToArg a => String -> [a] -> [String]
+toArgs arg = Prelude.concatMap (toArg arg)
+
diff --git a/src/Craft/Internal/Helpers/Parsing.hs b/src/Craft/Internal/Helpers/Parsing.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Internal/Helpers/Parsing.hs
@@ -0,0 +1,10 @@
+module Craft.Internal.Helpers.Parsing where
+
+import           Control.Monad          (void)
+
+import           Text.Megaparsec        hiding (parse)
+import           Text.Megaparsec.String
+
+
+end :: Parser ()
+end = try (void $ lookAhead eol) <|> eof
diff --git a/src/Craft/Internal/UserGroup.hs b/src/Craft/Internal/UserGroup.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Internal/UserGroup.hs
@@ -0,0 +1,117 @@
+module Craft.Internal.UserGroup where
+
+import           Control.Lens
+import           Data.List                      (intercalate)
+import           Data.Maybe                     (fromJust)
+import           Text.Megaparsec
+import           Text.Megaparsec.String
+
+import           Craft.Internal
+import           Craft.Internal.Helpers
+import           Craft.Internal.Helpers.Parsing
+
+
+colon :: Parser Char
+colon = char ':'
+
+
+-- TESTME
+passwdParser :: Parser (UserName, UserID, GroupID, String, String, String)
+passwdParser = do
+  name      <- UserName <$> someTill anyChar colon
+  _password <- manyTill anyChar colon
+  uid'      <- UserID . read <$> someTill digitChar colon
+  gid'      <- GroupID . read <$> someTill digitChar colon
+  comment'  <- manyTill anyChar colon
+  home'     <- manyTill anyChar colon
+  shell'    <- manyTill anyChar end
+  return (name, uid', gid', comment', home', shell')
+
+
+-- TESTME
+shadowParser :: Parser String
+shadowParser = do
+  _name <- someTill anyChar colon
+  manyTill anyChar colon
+
+
+userFromStr :: String -> Craft (Maybe User)
+userFromStr nameOrIdStr =
+  getent "passwd" nameOrIdStr >>= \case
+    ExecFail _ -> return Nothing
+    ExecSucc r -> do
+      (nameS, uid', gid', comment', home', shell') <-
+            parseGetent passwdParser "passwd" nameOrIdStr (r ^. stdout)
+      home'' <- parseAbsDir home'
+      shell'' <- parseAbsFile shell'
+      grp <- fromJust <$> groupFromID gid'
+      grps <- getGroups nameS
+      s <- $stdoutOrError =<< getent "shadow" (show nameS)
+      passwordHash' <- parseGetent shadowParser "shadow" (show nameS) s
+      return . Just $ User { _userName         = nameS
+                           , _uid              = uid'
+                           , _userGroup        = grp
+                           , _userGroups       = grps
+                           , _userPasswordHash = passwordHash'
+                           , _userHome         = home''
+                           , _userShell        = shell''
+                           , _userComment      = comment'
+                           }
+
+
+getGroups :: UserName -> Craft [GroupName]
+getGroups (UserName name) = do
+  s <- $stdoutOrError =<< exec "id" ["-nG", name]
+  return . map GroupName $ words s
+
+
+getent :: String -> String -> Craft ExecResult
+getent dbase key = exec "getent" [dbase, key]
+
+
+parseGetent :: Parsec String a -> String -> String -> String -> Craft a
+parseGetent parser dbase key input =
+  case parse parser (unwords ["getent", dbase, key]) input of
+    Left  err -> $craftError $ show err
+    Right r   -> return r
+
+
+userFromID :: UserID -> Craft (Maybe User)
+userFromID (UserID n) = userFromStr $ show n
+
+
+useradd :: User -> Craft ()
+useradd User{..} =
+  exec_ "useradd" $ args ++ toArg "--gid" (_gid _userGroup)
+ where
+  args = Prelude.concat
+    [ toArg "--uid"      _uid
+    , toArg "--comment"  _userComment
+    , toArg "--groups"   $ intercalate "," $ map show _userGroups
+    , toArg "--home"     (fromAbsDir _userHome)
+    , toArg "--password" _userPasswordHash
+    , toArg "--shell"    (fromAbsFile _userShell)
+    ]
+
+
+-- TESTME
+groupParser :: Parser Group
+groupParser = do
+  name <- GroupName <$> someTill anyChar colon
+  _ <- manyTill anyChar colon
+  gid' <- GroupID . read <$> someTill digitChar colon
+  members' <- map UserName <$> some alphaNumChar `sepBy` char ','
+  return $ Group name gid' members'
+
+
+groupFromStr :: String -> Craft (Maybe Group)
+groupFromStr nameOrIdStr =
+  getent "group" nameOrIdStr >>= \case
+    ExecFail _ -> return Nothing
+    ExecSucc r -> Just <$> parseGetent groupParser "group" nameOrIdStr (r ^. stdout)
+
+
+groupFromID :: GroupID -> Craft (Maybe Group)
+groupFromID (GroupID n )= groupFromStr $ show n
+
+
diff --git a/src/Craft/Nginx.hs b/src/Craft/Nginx.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Nginx.hs
@@ -0,0 +1,176 @@
+module Craft.Nginx where
+
+import           Control.Lens
+
+import           Craft
+import           Craft.Internal.Helpers
+import qualified Craft.SysV             as SysV
+
+
+setup :: Craft ()
+setup =
+  craft_ $ package "nginx"
+
+
+baseDir, sitesDir :: Path Abs Dir
+baseDir = $(mkAbsDir "/etc/nginx")
+sitesDir = baseDir </> $(mkRelDir "sites-enabled")
+
+
+data Config
+  = Config
+    { _configName       :: Name
+    , _configDirectives :: [Directive]
+    , _configServers    :: Servers
+    , _configPriority   :: Int
+    }
+
+
+type Servers   = [Server]
+
+
+data Server
+  = Server
+    { _serverNames      :: [Name]
+    , _listen           :: (Address, Port, Args)
+    , _serverDirectives :: [Directive]
+    , _locations        :: [Location]
+    }
+
+
+data SSL
+  = SSL
+    { _sslKey  :: File
+    , _sslCert :: File
+    }
+
+
+data Address
+  = Address String
+  | AnyAddress
+
+
+instance Show Address where
+  show AnyAddress  = "*"
+  show (Address t) = t
+
+
+type Port      = Int
+data Location  = Location URL [Directive] [Location]
+type URL       = String
+type Directive = (Name, Args)
+type Name      = String
+
+
+makeLenses ''Config
+makeLenses ''Server
+makeLenses ''SSL
+
+
+config :: Name -> Config
+config name =
+  Config
+  { _configName       = name
+  , _configPriority   = 10
+  , _configDirectives = []
+  , _configServers    = []
+  }
+
+
+server :: Server
+server =
+  Server
+    { _serverNames      = []
+    , _listen           = (AnyAddress, 80, [])
+    , _serverDirectives = []
+    , _locations        = []
+    }
+
+
+sslServer :: SSL -> Server
+sslServer ssl =
+  Server
+  { _serverNames      = []
+  , _listen           = (AnyAddress, 443, ["ssl"])
+  , _serverDirectives =
+      [ ("ssl",                       ["on"])
+      , ("ssl_certificate",           [fromAbsFile $ ssl^.sslCert.path])
+      , ("ssl_certificate_key",       [fromAbsFile $ ssl^.sslKey.path])
+      , ("ssl_session_cache",         ["shared:SSL:10m"])
+      , ("ssl_session_timeout",       ["5m"])
+      , ("ssl_protocols",             ["TLSv1", "TLSv1.1", "TLSv1.2"])
+      , ("ssl_ciphers",               ["ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA"])
+      , ("ssl_prefer_server_ciphers", ["on"])
+      ]
+  , _locations = []
+  }
+
+
+
+redirectWWWtoNoWWW :: [Server] -> [Server]
+redirectWWWtoNoWWW servers =
+  map go servers ++ servers
+ where
+  go s =
+    s & serverNames .~ map ("www." ++) (s ^. serverNames)
+      & locations   .~ []
+      & serverDirectives .~ s ^. serverDirectives ++
+        [ ("return", ["301", "http://" ++ s ^. serverNames . _head ++ "$uri"]) ]
+
+
+logs :: Path Abs Dir -> Path Rel FileP -> [Directive]
+logs logdir name =
+  [ ("access_log", [ logprefix ++ ".access.log", "combined"])
+  , ("error_log",  [ logprefix ++ ".error.log" ])
+  ]
+ where
+  logprefix = fromAbsFile $ logdir </> name
+
+
+root :: [Directive] -> Location
+root dirs = Location "/" dirs []
+
+
+instance Show Config where
+  show cfg = dirs ++ servers
+   where
+    dirs = concatMap showDirective $ cfg ^. configDirectives
+    servers = concatMap show $ cfg ^. configServers
+
+
+showDirective :: Directive -> String
+showDirective (name, args) =
+  name ++ " " ++ unwords args ++ ";"
+
+
+instance Show Server where
+  show Server{..} = "\n" ++
+    "server {\n" ++ indent 2 (
+      "listen " ++ showListen _listen ++ ";\n" ++
+      "server_name " ++ unwords _serverNames ++ ";\n" ++
+      unlines ( map showDirective _serverDirectives ++ map show _locations)) ++
+    "}\n"
+
+
+instance Show Location where
+  show (Location fp dirs sublocations) =
+    "location " ++ fp ++ " {\n" ++ indent 2 (
+        unlines $ map showDirective dirs
+               ++ map show sublocations) ++
+    "}\n"
+
+
+showListen :: (Address, Port, Args) -> String
+showListen (addr, port, args) =
+  show addr ++ ":" ++ show port ++ " " ++ unwords args
+
+
+toFile :: Config -> Craft File
+toFile c = do
+  fn <- parseRelFile $ (show $ c^.configPriority)++"_"++(c^.configName)++".conf"
+  return $ file (sitesDir</>fn)
+           & strContent .~ show c
+
+
+reload :: Craft ()
+reload = SysV.reload $ SysV.service "nginx"
diff --git a/src/Craft/Package.hs b/src/Craft/Package.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Package.hs
@@ -0,0 +1,12 @@
+module Craft.Package where
+
+import           Control.Lens
+import           Control.Monad.Reader
+
+import           Craft.Types
+
+
+get :: PackageName -> Craft (Maybe Package)
+get name = do
+  pm <- asks _craftPackageManager
+  (pm ^. pmGetter) name
diff --git a/src/Craft/Pip.hs b/src/Craft/Pip.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Pip.hs
@@ -0,0 +1,114 @@
+module Craft.Pip where
+
+import           Formatting      hiding (char)
+import qualified Formatting      as F
+import           Text.Megaparsec
+
+import           Craft           hiding (latest, package)
+import qualified Craft
+import qualified Craft.File      as File
+
+
+newtype PipPackage = PipPackage Package
+  deriving (Eq, Show)
+
+pipfp :: Path Abs FileP
+pipfp = $(mkAbsFile "/usr/local/bin/pip")
+
+setup :: Craft ()
+setup = do
+  craft_ $ map Craft.package ["libffi-dev", "libssl-dev", "python-dev"]
+  let pippkg = Craft.package "python-pip"
+  File.exists pipfp >>= flip unless (craft_ pippkg)
+  craft_ $ map package ["pyopenssl", "ndg-httpsclient", "pyasn1"]
+  craft_ $ latest "pip"
+  destroy_ pippkg
+  craft_ $ latest "setuptools"
+  destroy_ $ Craft.package "python-requests"
+
+
+package :: PackageName -> PipPackage
+package pn = PipPackage $ Package pn AnyVersion
+
+
+latest :: PackageName -> PipPackage
+latest pn = PipPackage $ Package pn Latest
+
+
+get :: PackageName -> Craft (Maybe PipPackage)
+get pn = do
+  r <- withPath [$(mkAbsDir "/usr/local/bin"), $(mkAbsDir "/usr/bin")] $ exec "pip" ["show", pn]
+  case r of
+    ExecFail _     -> return Nothing
+    ExecSucc succr -> do
+      results <- parseExecResult r pipShowParser (succr ^. stdout)
+      if null results then
+        return Nothing
+      else case lookup "Version" results of
+        Nothing      -> $craftError "`pip show` did not return a version"
+        Just version -> return . Just
+                               . PipPackage
+                               $ Package pn $ Version version
+
+
+-- TESTME
+pipShowParser :: Parsec String [(String, String)]
+pipShowParser = many $ kv <* many eol
+ where
+  kv :: Parsec String (String, String)
+  kv = do
+    key <- some $ noneOf ":"
+    char ':' >> space
+    value <- many $ noneOf "\n"
+    return (key, value)
+
+
+pip :: [String] -> Craft ()
+pip args = withPath [$(mkAbsDir "/usr/local/bin"), $(mkAbsDir "/usr/bin")] $ exec_ "pip" args
+
+
+pkgArgs :: PipPackage -> [String]
+pkgArgs (PipPackage (Package pn pv)) = go pv
+ where
+  go AnyVersion  = [pn]
+  go Latest      = ["--upgrade", pn]
+  go (Version v) = ["--ignore-installed", pn ++ "==" ++ v]
+
+
+pipInstall :: PipPackage -> Craft ()
+pipInstall pkg = pip $ "install" : pkgArgs pkg
+
+
+instance Craftable PipPackage PipPackage where
+  watchCraft ppkg@(PipPackage pkg) = do
+    let name = pkg ^. pkgName
+        ver = pkg ^. pkgVersion
+        verify =
+          get name >>= \case
+            Nothing -> $craftError $
+                         "craft PipPackage `"++name++"` failed! Not Found."
+            Just ppkg'@(PipPackage pkg') -> do
+              let newver = pkg' ^. pkgVersion
+              case ver of
+                Version _ ->
+                  when (newver /= ver) $
+                    $craftError
+                      $ formatToString
+                        ("craft PipPackage `"%F.string%"` failed! Wrong Version: "%shown%" Expected: "%shown)
+                        name newver ver
+                _ -> return ()
+              return ppkg'
+
+    get name >>= \case
+      Nothing -> do
+        pipInstall ppkg
+        ppkg' <- verify
+        return (Created, ppkg')
+      Just (PipPackage pkg') -> do
+        let ver' = pkg' ^. pkgVersion
+        if ver' == ver then
+          return (Unchanged, ppkg)
+        else do
+          pipInstall ppkg
+          ppkg' <- verify
+          return (Updated, ppkg')
diff --git a/src/Craft/Run.hs b/src/Craft/Run.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Run.hs
@@ -0,0 +1,4 @@
+module Craft.Run (module X) where
+
+import           Craft.Run.Local as X
+import           Craft.Run.SSH   as X
diff --git a/src/Craft/Run/Internal.hs b/src/Craft/Run/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Run/Internal.hs
@@ -0,0 +1,70 @@
+module Craft.Run.Internal where
+
+import           Conduit                 as C
+import           Control.Monad.Logger    (LoggingT, askLoggerIO, logDebugNS,
+                                          runLoggingT)
+import qualified Control.Monad.Trans     as Trans
+import qualified Data.Conduit.List       as CL
+import           Data.Conduit.Process    (sourceProcessWithStreams)
+import           Data.Conduit.Text       as CT
+import           Data.Monoid             ((<>))
+import qualified Data.Text               as T
+import           System.Exit
+import           System.Process
+import qualified System.Process.ListLike as SPLL
+
+import           Craft.Types
+
+
+isSuccessCode :: ExitCode -> Bool
+isSuccessCode ExitSuccess     = True
+isSuccessCode (ExitFailure _) = False
+
+
+execProc :: CreateProcess -> LoggingT IO ExecResult
+execProc p = do
+  logDebugNS "exec|process" $ T.pack $ showProc p
+  (exit', stdoutRaw, stderrRaw) <- Trans.lift $ SPLL.readCreateProcessWithExitCode p "" {- stdin -}
+  let stdout' = trimNL stdoutRaw
+  let stderr' = trimNL stderrRaw
+  return $ case exit' of
+    ExitSuccess      -> ExecSucc $ SuccResult stdout' stderr' p
+    ExitFailure code -> ExecFail $ FailResult code stdout' stderr' p
+
+
+execProc_ :: String -> CreateProcess -> LoggingT IO ()
+execProc_ src p = do
+  let p' = p { std_in  = CreatePipe
+             , std_out = CreatePipe
+             , std_err = CreatePipe
+             }
+  logDebugNS "exec_|process" $ T.pack $ showProc p
+
+  logger <- askLoggerIO
+  let src' = "exec_|" <> T.pack src
+      srcOut = src' <> "|stdout"
+      srcErr = src' <> "|stderr"
+
+  (ec, _, _) <- Trans.lift $ sourceProcessWithStreams
+                               p'
+                               (CL.sourceNull)
+                               (pipeConsumer logger srcOut)
+                               (pipeConsumer logger srcErr)
+  case ec of
+    ExitFailure n -> $craftError $ "exec_ `" ++ src ++ "` failed with code: " ++ show n
+    ExitSuccess   -> return ()
+ where
+   pipeConsumer logger s = decodeUtf8C =$= CT.lines =$ awaitForever (\txt ->
+     (`runLoggingT` logger) (logDebugNS s txt))
+
+
+-- | Remove a single trailing newline character from the end of the String
+trimNL :: String -> String
+trimNL = reverse . rmNL . reverse
+ where
+  rmNL []        = []
+  rmNL ('\n':xs) = rmCR xs
+  rmNL xs        = xs
+  rmCR []        = []
+  rmCR ('\r':xs) = xs
+  rmCR xs        = xs
diff --git a/src/Craft/Run/Local.hs b/src/Craft/Run/Local.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Run/Local.hs
@@ -0,0 +1,51 @@
+module Craft.Run.Local where
+
+import           Control.Lens
+import qualified Control.Monad.Trans as Trans
+import qualified Data.ByteString     as BS
+import qualified Data.Map.Strict     as Map
+import           System.Process      hiding (readCreateProcessWithExitCode,
+                                      readProcessWithExitCode)
+
+import           Craft.Run.Internal
+import           Craft.Types
+
+
+runLocal :: CraftRunner
+runLocal =
+  CraftRunner
+  { runExec =
+      \ce command args ->
+      let p = localProc ce command args
+      in execProc p
+  , runExec_ =
+      \ce command args ->
+      let p = localProc ce command args
+      in execProc_ (showProc p) p
+  , runFileRead =
+      Trans.lift . BS.readFile . fromAbsFile
+  , runFileWrite =
+      \fp content ->
+        Trans.lift (BS.writeFile (fromAbsFile fp) content)
+  , runSourceFile =
+      \src dest ->
+        let p = (proc "/bin/cp" [src, (fromAbsFile dest)])
+                { env           = Nothing
+                , cwd           = Nothing
+                , close_fds     = True
+                , create_group  = True
+                , delegate_ctlc = False
+                }
+        in execProc_ (showProc p) p
+  }
+
+
+localProc :: CraftEnv -> Command -> Args -> CreateProcess
+localProc ce prog args =
+  (proc prog args)
+    { env           = Just . Map.toList $ ce^.craftExecEnv
+    , cwd           = Just . fromAbsDir $ ce^.craftExecCWD
+    , close_fds     = True
+    , create_group  = True
+    , delegate_ctlc = False
+    }
diff --git a/src/Craft/Run/Nspawn.hs b/src/Craft/Run/Nspawn.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Run/Nspawn.hs
@@ -0,0 +1,55 @@
+module Craft.Run.Nspawn where
+
+import           Control.Lens
+import qualified Control.Monad.Trans as Trans
+import qualified Data.ByteString     as BS
+import qualified Data.Map.Strict     as Map
+import           System.Process      hiding (readCreateProcessWithExitCode,
+                                      readProcessWithExitCode)
+
+import           Craft.Run.Internal
+import           Craft.Types
+
+
+runNspawn :: Path Abs Dir -> CraftRunner
+runNspawn dir =
+  CraftRunner
+  { runExec =
+      \ce command args ->
+        let p = nspawnProc dir ce command args
+        in execProc p
+  , runExec_ =
+      \ce command args ->
+        let p = nspawnProc dir ce command args
+        in execProc_ (showProc p) p
+  , runFileRead =
+      \fp -> do
+        fp' <- stripDir $(mkAbsDir "/") fp
+        Trans.lift . BS.readFile . fromAbsFile $ dir</>fp'
+  , runFileWrite =
+      \fp content -> do
+        fp' <- stripDir $(mkAbsDir "/") fp
+        Trans.lift (BS.writeFile (fromAbsFile (dir</>fp')) content)
+  , runSourceFile =
+      \src dest -> do
+        dest' <- stripDir $(mkAbsDir "/") dest
+        let p = (proc "/bin/cp" [src, fromAbsFile (dir</>dest')])
+                { env           = Nothing
+                , cwd           = Nothing
+                , close_fds     = True
+                , create_group  = True
+                , delegate_ctlc = False
+                }
+        execProc_ (showProc p) p
+  }
+
+
+nspawnProc :: Path Abs Dir -> CraftEnv -> Command -> Args -> CreateProcess
+nspawnProc dir ce cmd args =
+  (proc "systemd-nspawn" ("-D":(fromAbsDir dir):"-q":cmd:args))
+  { env           = Just $ Map.toList (ce ^. craftExecEnv)
+  , cwd           = Just $ fromAbsDir (ce^.craftExecCWD)
+  , close_fds     = True
+  , create_group  = True
+  , delegate_ctlc = False
+  }
diff --git a/src/Craft/Run/SSH.hs b/src/Craft/Run/SSH.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Run/SSH.hs
@@ -0,0 +1,212 @@
+module Craft.Run.SSH where
+
+import           Control.Exception.Lifted  (bracket)
+import           Control.Lens
+import           Control.Monad.Logger      (LoggingT)
+import           Control.Monad.Reader
+import qualified Control.Monad.Trans       as Trans
+import qualified Data.ByteString.Char8     as B8
+import           Data.List                 (intersperse)
+import qualified Data.Map.Strict           as Map
+import           Data.Maybe                (fromMaybe)
+import           Data.String.Utils         (replace)
+import           Path.IO
+import           System.Process            (ProcessHandle)
+import qualified System.Process            as Process
+import qualified System.Process.ByteString as Proc.BS
+import           System.Random             hiding (next)
+
+import           Craft.Helpers
+import           Craft.Run.Internal
+import           Craft.Types
+
+
+data SSHEnv = SSHEnv
+  { _sshKey         :: Path Abs FileP
+  , _sshAddr        :: String
+  , _sshPort        :: Int
+  , _sshUser        :: String
+  , _sshSudo        :: Bool
+  , _sshControlPath :: Maybe (Path Rel FileP)
+  , _sshOptions     :: [String]
+  }
+makeLenses ''SSHEnv
+
+
+connStr :: Optic' (->) (Const String) SSHEnv String
+connStr = to (\env -> concat [env ^. sshUser, "@", env ^. sshAddr])
+
+
+sshEnv :: String -> Path Abs FileP -> SSHEnv
+sshEnv addr key =
+  SSHEnv
+  { _sshAddr        = addr
+  , _sshPort        = 22
+  , _sshKey         = key
+  , _sshUser        = "root"
+  , _sshSudo        = True
+  , _sshControlPath = Nothing
+  , _sshOptions     = sshDefaultOptions
+  }
+
+
+sshDefaultOptions :: [String]
+sshDefaultOptions =
+  [ "UserKnownHostsFile=/dev/null"
+  , "StrictHostKeyChecking=no"
+  , "LogLevel=ERROR"
+  ]
+
+
+data Session
+ = Session
+   { _sessionMasterProcHandle :: ProcessHandle
+   , _sessionMasterProc       :: Process.CreateProcess
+   , _sessionControlPath      :: Path Rel FileP
+   , _sessionEnv              :: SSHEnv
+   , _sessionArgs             :: Args
+   }
+makeLenses ''Session
+
+
+prependEachWith :: String -> [String] -> [String]
+prependEachWith _    []   = []
+prependEachWith flag opts = flag:(intersperse flag opts)
+
+
+newSession :: SSHEnv -> IO Session
+newSession env = do
+  defaultControlPath <- parseRelFile =<< ((".craft-ssh-session-"++) . show . abs <$> (randomIO :: IO Int))
+  let controlPath = fromMaybe defaultControlPath (env^.sshControlPath)
+  let args =    [ "-p", show $ env^.sshPort ] -- port
+             ++ [ "-i", fromAbsFile $ env^.sshKey ] -- private key
+             ++ prependEachWith "-o"
+                  ((env ^. sshOptions)
+                   ++ [ "ControlPath=" ++ (fromRelFile controlPath)
+                      , "BatchMode=yes" -- never prompt for a password
+                      ])
+  let masterProc = (Process.proc "ssh"
+                      (args
+                       ++ (prependEachWith "-o" [ "ControlMaster=yes"
+                                                , "ControlPersist=yes" ])
+                       ++ [env ^. connStr]))
+                    { Process.std_in  = Process.NoStream
+                    , Process.std_out = Process.NoStream
+                    , Process.std_err = Process.NoStream
+                    }
+  (_, _, _, ph) <- Process.createProcess masterProc
+  return Session { _sessionEnv = env
+                 , _sessionMasterProc = masterProc
+                 , _sessionMasterProcHandle = ph
+                 , _sessionControlPath = controlPath
+                 , _sessionArgs = args
+                 }
+
+
+closeSession :: Session -> IO ()
+closeSession Session{..} = do
+  Process.terminateProcess _sessionMasterProcHandle
+  whenM (doesFileExist _sessionControlPath) $
+    removeFile _sessionControlPath
+
+
+withSession :: SSHEnv -> (Session -> LoggingT IO a) -> LoggingT IO a
+withSession env =
+  bracket (Trans.lift $ newSession env)
+          (Trans.lift . closeSession)
+
+
+runSSHSession :: Session -> CraftRunner
+runSSHSession session =
+  CraftRunner
+  { runExec =
+      \ce command args ->
+        execProc $ sshProc session ce command args
+  , runExec_ =
+      \ce command args ->
+        let p = sshProc session ce command args
+        in execProc_ (unwords (command:args)) p
+  , runFileRead =
+      \fp -> do
+        let p = sshProc session craftEnvOverride "cat" [fromAbsFile fp]
+        (ec, content, stderr') <-
+          Trans.lift $ Proc.BS.readCreateProcessWithExitCode p ""
+        unless (isSuccessCode ec) $
+          $craftError $ "Failed to read file '"++ fromAbsFile fp ++"': " ++ B8.unpack stderr'
+        return content
+  , runFileWrite =
+      \fp content -> do
+        let p = sshProc session craftEnvOverride "tee" [fromAbsFile fp]
+        (ec, _, stderr') <-
+          Trans.lift $ Proc.BS.readCreateProcessWithExitCode p content
+        unless (isSuccessCode ec) $
+          $craftError $ "Failed to write file '" ++ fromAbsFile fp ++ "': " ++ B8.unpack stderr'
+  , runSourceFile =
+      \src dest ->
+        let p = Process.proc "rsync"
+                  (   [ "--quiet" -- suppress non-error messages
+                      , "--checksum" -- skip based on checksum, not mod-time & size
+                      , "--compress" -- compress file data during the transfer
+                        -- specify the remote shell to use
+                      , "--rsh=ssh " ++ unwords (session ^. sessionArgs)]
+                  ++ (if session ^. sessionEnv . sshSudo
+                          then ["--super", "--rsync-path=sudo rsync"]
+                          else [])
+                  ++ [ src , (session ^. sessionEnv . connStr) ++ ":" ++ fromAbsFile dest ])
+        in execProc_ (showProc p) p
+  }
+ where
+  craftEnvOverride :: CraftEnv
+  craftEnvOverride =
+    craftEnv noPackageManager
+    & craftExecEnv .~ Map.empty
+    & craftExecCWD .~ $(mkAbsDir "/")
+
+
+runCraftSSH :: SSHEnv -> CraftEnv -> Craft a -> LoggingT IO a
+runCraftSSH env ce configs =
+  withSession env $ \session -> runCraft (runSSHSession session) ce configs
+
+
+sshProc :: Session -> CraftEnv -> Command -> Args
+        -> Process.CreateProcess
+sshProc session ce command args =
+  Process.proc "ssh" $ session ^. sessionArgs
+                    ++ (prependEachWith "-o" [ "ControlMaster=auto"
+                                             , "ControlPersist=no"
+                                             ])
+                    ++ [ session ^. sessionEnv . connStr
+                       , fullExecStr
+                       ]
+ where
+  fullExecStr :: String
+  fullExecStr = unwords (sudoArgs ++ ["sh", "-c", "'", shellStr, "'"])
+
+  sudoArgs :: [String]
+  sudoArgs = if session ^. sessionEnv . sshSudo
+              then ["sudo", "-n", "-H"]
+              else []
+
+  shellStr :: String
+  shellStr = unwords (cdArgs ++ execEnvArgs ++ (command : map (escape specialChars) args))
+
+  specialChars :: [String]
+  specialChars = [" ", "*", "$", "'"]
+
+  execEnvArgs :: [String]
+  execEnvArgs = map (escape specialChars) . renderEnv $ ce ^. craftExecEnv
+
+  cdArgs :: [String]
+  cdArgs = ["cd", (fromAbsDir $ ce^.craftExecCWD), ";"]
+
+  escape :: [String] -> String -> String
+  escape = recur backslash
+
+  recur _ []     s = s
+  recur f (a:as) s = recur f as $ f a s
+
+  backslash x = replace x ('\\':x)
+
+
+renderEnv :: ExecEnv -> [String]
+renderEnv = map (\(k, v) -> k++"="++v) . Map.toList
diff --git a/src/Craft/Run/Vagrant.hs b/src/Craft/Run/Vagrant.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Run/Vagrant.hs
@@ -0,0 +1,66 @@
+module Craft.Run.Vagrant where
+
+import           Control.Lens
+import           Control.Monad.Logger (LoggingT)
+import qualified Control.Monad.Trans  as Trans
+import qualified Data.Map.Strict      as Map
+import           Data.Maybe           (fromMaybe)
+import qualified System.Directory
+import qualified System.Environment
+
+import           Craft
+import           Craft.Config.SSH     (SSHConfig (..), cfgLookup, parser)
+
+
+data VagrantSettings
+  = VagrantSettings
+    { vagrantUp  :: Bool
+    , vagrantBox :: String
+    }
+
+
+vagrantSettings :: VagrantSettings
+vagrantSettings =
+  VagrantSettings
+  { vagrantUp = False
+  , vagrantBox = "default"
+  }
+
+
+runCraftVagrant :: VagrantSettings -> CraftEnv -> Craft a -> LoggingT IO a
+runCraftVagrant settings env configs = do
+  let box = vagrantBox settings
+  sysEnv <- Trans.lift System.Environment.getEnvironment
+  cwd <- parseAbsDir =<< Trans.lift System.Directory.getCurrentDirectory
+  -- vagrant ssh-config
+  sshcfg <-
+    runCraft
+      runLocal
+      (craftEnv (env ^. craftPackageManager)
+       & craftExecEnv .~ Map.fromList sysEnv
+       & craftExecCWD .~ cwd)
+      $ do
+        when (vagrantUp settings) $ exec_ "vagrant" ["up", box]
+        SSHConfig <$> parseExecStdout parser "vagrant" ["ssh-config", box]
+  let addr = cfgLookupOrError box "hostname" sshcfg
+  let port = read $ cfgLookupOrError box "port" sshcfg
+  let user = cfgLookupOrError box "user" sshcfg
+  key <- parseAbsFile $ cfgLookupOrError box "identityfile" sshcfg
+  -- vagrant ssh
+  withSession
+    (sshEnv addr key
+     & sshUser .~ user
+     & sshPort .~ port
+     & sshSudo .~ True)
+    $ \session ->
+        runCraft
+          (runSSHSession session)
+          env
+          configs
+
+
+cfgLookupOrError :: String -> String -> SSHConfig -> String
+cfgLookupOrError box name sshcfg =
+  fromMaybe
+    (error $ "'"++name++"' not found in output of 'vagrant ssh-config "++box++"'")
+    (cfgLookup box name sshcfg)
diff --git a/src/Craft/S3File.hs b/src/Craft/S3File.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/S3File.hs
@@ -0,0 +1,150 @@
+module Craft.S3File where
+
+import           Control.Lens            hiding (noneOf)
+import           Control.Monad.Logger    (logDebug)
+import           Crypto.Hash             (SHA1)
+import           Crypto.MAC.HMAC         (HMAC, hmac)
+import           Data.ByteArray.Encoding
+import qualified Data.ByteString.Char8   as B8
+import qualified Data.Text               as T
+import           Text.Megaparsec
+import           Text.Megaparsec.String
+
+import           Craft                   hiding (try)
+import qualified Craft.File              as File
+
+
+data S3File
+  = S3File
+    { _file    :: File
+    , _source  :: String
+    , _domain  :: String
+    , _version :: Version
+    , _auth    :: Maybe (String, String) -- ^ (AWSAccessKeyId, AWSSecretAccessKey)
+    }
+    deriving (Eq, Show)
+
+makeLenses ''S3File
+
+-- TODO: instance FileLike S3File
+
+
+s3file :: Path Abs FileP -> String -> S3File
+s3file fp source' =
+  S3File
+  { _file    = Craft.file fp
+  , _domain  = "s3.amazonaws.com"
+  , _source  = source'
+  , _version = AnyVersion
+  , _auth    = Nothing
+  }
+
+
+url :: Getter S3File String
+url = to (\f -> "https://" ++ f ^. domain ++ "/" ++ f ^. source)
+
+
+-- | Add AWS Authentication Headers to curl commands
+-- http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#ConstructingTheAuthenticationHeader
+authHeaders :: String -> S3File -> Craft [String]
+authHeaders method s3f =
+  case s3f ^. auth of
+    Nothing -> return []
+    Just (awsKeyID, awsSecretKey) -> do
+      date <- $stdoutOrError =<< exec "date" ["-u", "--rfc-2822"]
+      sourceUrl <- case s3f ^. source of
+                     []       -> $craftError $ "S3File.source is empty! " ++ show s3f
+                     '/':_    -> return $ s3f ^. source
+                     s3source -> return $ '/':s3source
+      let contentMD5 = ""
+          contentType = ""
+          canonicalizedAmzHeaders = ""
+          toSign = method ++ "\n"
+                ++ contentMD5 ++ "\n"
+                ++ contentType ++ "\n"
+                ++ date ++ "\n"
+                ++ canonicalizedAmzHeaders
+                ++ sourceUrl
+          sigHMAC :: HMAC SHA1
+          sigHMAC = hmac (B8.pack awsSecretKey)
+                         (B8.pack toSign)
+          sig = B8.unpack $ convertToBase Base64 sigHMAC
+      $logDebug . T.pack $ "awsSecretKey == " ++ awsSecretKey
+      $logDebug . T.pack $ "awsKeyID == " ++ awsKeyID
+      $logDebug . T.pack $ "toSign == " ++ show toSign
+
+      return [ "--header", "Date:" ++ date
+             , "--header", "Authorization:AWS " ++ awsKeyID ++ ":" ++ sig
+             ]
+
+
+getS3Sum :: S3File -> Craft (Maybe String)
+getS3Sum f = do
+  hdrs <- authHeaders "HEAD" f
+  headers <- parseExecStdout httpHeaders "curl" (hdrs ++ ["-s", "-XHEAD", "-I", f ^. url])
+  return $ filter ('"' /=) <$> lookup "ETag" headers
+
+
+httpHeaders :: Parser [(String, String)]
+httpHeaders = do
+  _ <- string "HTTP/1." >> oneOf "01" >> string " 200 OK" >> eol
+  headers <- header `sepEndBy1` eol
+  return headers
+
+
+header :: Parser (String, String)
+header = do
+  k <- noneOf ":\r\n" `someTill` string ": "
+  v <- many $ noneOf "\r\n"
+  return (k,v)
+
+
+instance Craftable S3File S3File where
+  watchCraft s3f = do
+    let s3f' = s3f & Craft.S3File.file . fileContent .~ Nothing
+    let fp   = s3f' ^. Craft.S3File.file . path
+    let downloadFile = do
+          hdrs <- authHeaders "GET" s3f
+          exec_ "curl" $ hdrs ++ ["-XGET", "-s", "-L", "-o", fromAbsFile fp, s3f'^.url]
+    let verify expected = do
+          md5chksum <- File.md5sum fp
+          when (md5chksum /= expected) (
+            $craftError $ "verify S3File failed! Expected `" ++ expected ++ "` "
+                       ++ "Got `" ++ md5chksum ++ "` for " ++ show s3f')
+
+    getS3Sum s3f' >>= \case
+      Nothing -> $craftError $ "Failed to get chksum from S3 for: " ++ show s3f'
+      Just etag -> do
+        exists <- File.exists fp
+        w <- if exists then do
+                curSum <- File.md5sum fp
+                case s3f' ^. version of
+                  AnyVersion -> return Unchanged
+                  Latest
+                    | etag == curSum -> return Unchanged
+                    | otherwise -> do
+                        downloadFile
+                        verify etag
+                        return Updated
+                  Version verStr
+                    | curSum == verStr -> return Unchanged
+                    | verStr /= etag ->
+                        $craftError $
+                           "Cannot download specific file version from S3. "
+                           ++ "Found version " ++ etag ++ " for " ++ show s3f'
+                    | otherwise -> do
+                        downloadFile
+                        verify verStr
+                        return Updated
+              else do
+                downloadFile
+                case s3f' ^. version of
+                  Version verStr -> verify verStr
+                  _              -> verify etag
+                return Created
+
+        fw <- watchCraft_ $ s3f' ^. Craft.S3File.file
+        if changed w then
+          return (w, s3f')
+        else
+          return (fw, s3f')
diff --git a/src/Craft/SSH.hs b/src/Craft/SSH.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/SSH.hs
@@ -0,0 +1,51 @@
+module Craft.SSH where
+
+import           Control.Lens
+import           Data.Char              (toLower)
+import           Text.Megaparsec
+import           Text.Megaparsec.String
+
+import           Craft
+
+
+data KeyType
+  = DSA
+  | RSA
+  | RSA1
+  | ECDSA
+  | ED25519
+  | KeyType String
+  deriving (Eq)
+
+
+instance Show KeyType where
+  show DSA         = "ssh-dsa"
+  show RSA         = "ssh-rsa"
+  show RSA1        = "ssh-rsa1"
+  show ECDSA       = "ssh-ecdsa"
+  show ED25519     = "ssh-ed25519"
+  show (KeyType s) = s
+
+
+userDir :: User -> Directory
+userDir user =
+  directory ((user^.userHome) </> $(mkRelDir ".ssh"))
+  & mode          .~ Mode RWX O O
+  & ownerAndGroup .~ user
+
+
+parseKeyType :: Parser KeyType
+parseKeyType = do
+  void $ string "ssh-"
+  s <- alphaNumChar `someTill` (void spaceChar <|> eof)
+  -- The reason for converting the string to lowercase is because
+  -- KeyType is allowed to be any string.
+  -- This prevents "ssh-rSa" from parsing to `KeyType "ssh-rSa"` instead of `RSA`
+  -- even if "ssh-rSa" is not technically valid.
+  return $ case map toLower s of
+    "dsa"     -> DSA
+    "rsa"     -> RSA
+    "rsa1"    -> RSA1
+    "ecdsa"   -> ECDSA
+    "ed25519" -> ED25519
+    x         -> KeyType ("ssh-"++x)
diff --git a/src/Craft/SSH/AuthorizedKey.hs b/src/Craft/SSH/AuthorizedKey.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/SSH/AuthorizedKey.hs
@@ -0,0 +1,68 @@
+module Craft.SSH.AuthorizedKey where
+
+import           Control.Lens
+import qualified Data.Map.Strict        as Map
+import           Data.Maybe             (catMaybes)
+import           Text.Megaparsec
+import           Text.Megaparsec.String
+
+import           Craft                  hiding (try)
+import qualified Craft.File             as File
+import           Craft.SSH
+import           Craft.SSH.PublicKey    as PK
+
+
+data AuthorizedKey
+  = AuthorizedKey
+    { _authkeyUser :: User
+    , _authkey     :: PublicKey
+    }
+    deriving (Show, Eq)
+makeLenses ''AuthorizedKey
+
+
+instance Craftable AuthorizedKey AuthorizedKey where
+  watchCraft ak@(AuthorizedKey user pk) = do
+    pubKeys <- Map.elems . Map.insert (pk^.pubkeyValue) pk
+                         . authkeysToMap
+                       <$> get user
+    craft_ $ userDir user
+    w <- watchCraft_ $ userFile user
+                       & strContent .~ (unlines $ map PK.toString pubKeys)
+    return (w, ak)
+
+
+instance Destroyable AuthorizedKey where
+  watchDestroy (AuthorizedKey user pk) = do
+    pubKeysMap <- authkeysToMap <$> get user
+    let val = pk^.pubkeyValue
+    let newPubKeys = Map.elems $ Map.delete val pubKeysMap
+    craft_ $ userDir user
+    craft_ $ userFile user
+             & strContent .~ (unlines $ map PK.toString newPubKeys)
+    return $ case Map.lookup val pubKeysMap of
+               Nothing -> (Unchanged, Nothing)
+               Just x  -> (Removed, Just (AuthorizedKey user x))
+
+
+userFile :: User -> File
+userFile user =
+  file ((user^.to userDir.path) </> $(mkRelFile "authorized_keys"))
+  & mode          .~ Mode RW O O
+  & ownerAndGroup .~ user
+
+
+get :: User -> Craft [AuthorizedKey]
+get user = File.get (user ^. to userFile . path) >>= \case
+  Nothing -> return []
+  Just f  -> map (AuthorizedKey user) <$> parseFile parsePublicKeys (f^.path)
+
+
+authkeysToMap :: [AuthorizedKey] -> Map.Map String PublicKey
+authkeysToMap = Map.fromList . map (((,)<$>_pubkeyValue<*>id) . _authkey)
+
+
+parsePublicKeys :: Parser [PublicKey]
+parsePublicKeys = do
+  keys <- (try (Just <$> parsePublicKey) <|> (space >> return Nothing)) `sepEndBy` eol
+  return $ catMaybes keys
diff --git a/src/Craft/SSH/PrivateKey.hs b/src/Craft/SSH/PrivateKey.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/SSH/PrivateKey.hs
@@ -0,0 +1,33 @@
+module Craft.SSH.PrivateKey where
+
+import           Control.Lens
+
+import           Craft
+import           Craft.SSH
+
+
+data PrivateKey
+  = PrivateKey
+    { _user    :: User
+    , _name    :: String
+    , _content :: String
+    }
+  deriving (Eq, Show)
+makeLenses ''PrivateKey
+
+
+path :: PrivateKey -> Craft (Path Abs FileP)
+path pk = do
+  n <- parseRelFile $ pk^.name
+  return $ (pk^.user.to userDir.Craft.path)</>n
+
+
+instance Craftable PrivateKey PrivateKey where
+  watchCraft pk = do
+    craft_ $ userDir $ pk^.user
+    pkPath <- Craft.SSH.PrivateKey.path pk
+    w <- watchCraft_ $ file pkPath
+                       & mode          .~ Mode RW O O
+                       & ownerAndGroup .~ pk ^. user
+                       & strContent    .~ pk ^. content
+    return (w, pk)
diff --git a/src/Craft/SSH/PublicKey.hs b/src/Craft/SSH/PublicKey.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/SSH/PublicKey.hs
@@ -0,0 +1,50 @@
+module Craft.SSH.PublicKey where
+
+import           Control.Lens
+import           Text.Megaparsec
+import           Text.Megaparsec.String
+
+import           Craft
+import           Craft.Internal.Helpers.Parsing
+import           Craft.SSH
+
+
+data PublicKey
+  = PublicKey
+    { _pubkeyType    :: KeyType
+    , _pubkeyValue   :: String
+    , _pubkeyComment :: String
+    }
+    deriving (Show, Eq)
+makeLenses ''PublicKey
+
+
+publicKey :: KeyType -> String -> PublicKey
+publicKey pktype pkval =
+  PublicKey
+  { _pubkeyType    = pktype
+  , _pubkeyValue   = pkval
+  , _pubkeyComment = ""
+  }
+
+
+toString :: PublicKey -> String
+toString pk =
+  let comment = pk ^. pubkeyComment
+  in show (pk ^. pubkeyType) ++ " " ++ pk ^. pubkeyValue
+     ++ if null comment then "" else " " ++ comment
+
+
+parsePublicKey :: Parser PublicKey
+parsePublicKey = do
+  space
+  kt <- parseKeyType
+  space
+  key <- base64Char `someTill` (void spaceChar <|> eof)
+  space
+  comment <- anyChar `manyTill` end
+  return $ PublicKey kt key comment
+
+
+base64Char :: Parser Char
+base64Char = alphaNumChar <|> char '+' <|> char '/' <|> char '='
diff --git a/src/Craft/SysV.hs b/src/Craft/SysV.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/SysV.hs
@@ -0,0 +1,135 @@
+module Craft.SysV where
+
+import           Control.Lens
+
+import           Craft
+import qualified Craft.File   as File
+
+
+data Status = Running
+            | Stopped
+  deriving (Eq, Show)
+
+
+data RunLevel = RunLevel0
+              | RunLevel1
+              | RunLevel2
+              | RunLevel3
+              | Runlevel4
+              | Runlevel5
+              | Runlevel6
+              | RunlevelS
+  deriving (Eq)
+
+
+instance Show RunLevel where
+  show RunLevel0 = "0"
+  show RunLevel1 = "1"
+  show RunLevel2 = "2"
+  show RunLevel3 = "3"
+  show Runlevel4 = "4"
+  show Runlevel5 = "5"
+  show Runlevel6 = "6"
+  show RunlevelS = "S"
+
+
+defaults :: [RunLevel]
+defaults = [ RunLevel0
+           , RunLevel1
+           , RunLevel2
+           , RunLevel3
+           , Runlevel4
+           , Runlevel5
+           , Runlevel6
+           ]
+
+
+data Service = Service
+  { _serviceName   :: String
+  , _serviceStatus :: Status
+  , _serviceAtBoot :: Maybe Bool
+  }
+  deriving (Eq, Show)
+makeLenses ''Service
+
+
+service :: String -> Service
+service sn =
+  Service
+  { _serviceName   = sn
+  , _serviceStatus = Running
+  , _serviceAtBoot = Just True
+  }
+
+
+servicePath :: String -> Craft (Path Abs FileP)
+servicePath sn = do
+  fn <- parseRelFile sn
+  return $ $(mkAbsDir "/etc/init.d")</>fn
+
+
+get :: String -> Craft (Maybe Service)
+get sn = do
+  sp <- servicePath sn
+  exists <- File.exists sp
+  if exists then do
+    status' <- exec (fromAbsFile sp) ["status"]>>= \case
+                 (ExecFail _) -> return Stopped
+                 (ExecSucc _) -> return Running
+    return . Just $ Service { _serviceName   = sn
+                            , _serviceStatus = status'
+                            , _serviceAtBoot = Nothing
+                            }
+  else
+    return Nothing
+
+
+status :: Service -> Craft Status
+status svc = do
+  sp <- servicePath $ svc^.serviceName
+  exec (fromAbsFile sp) ["status"] >>= \case
+    (ExecFail _) -> return Stopped
+    (ExecSucc _) -> return Running
+
+
+run_ :: String -> Service -> Craft ()
+run_ cmd svc = do
+  sp <- servicePath $ svc^.serviceName
+  exec_ (fromAbsFile sp) [cmd]
+
+
+start :: Service -> Craft ()
+start = run_ "start"
+
+
+stop :: Service -> Craft ()
+stop = run_ "stop"
+
+
+restart :: Service -> Craft ()
+restart = run_ "restart"
+
+
+reload :: Service -> Craft ()
+reload = run_ "reload"
+
+
+updateRcD :: Service -> String -> Craft ()
+updateRcD svc cmd =
+  exec_ "update-rc.d" ["-f", svc^.serviceName, cmd]
+
+
+instance Craftable Service Service where
+  watchCraft svc = do
+    whenJust (svc^.serviceAtBoot) $ \x ->
+      updateRcD svc (if x then "defaults" else "remove")
+    status' <- status svc
+    case (svc^.serviceStatus, status') of
+      (Running, Running) -> return (Unchanged, svc)
+      (Stopped, Stopped) -> return (Unchanged, svc)
+      (Running, Stopped) -> do
+        start svc
+        return (Updated, svc)
+      (Stopped, Running) -> do
+        stop svc
+        return (Updated, svc)
diff --git a/src/Craft/Types.hs b/src/Craft/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Types.hs
@@ -0,0 +1,458 @@
+{-# LANGUAGE DeriveFunctor              #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+module Craft.Types
+( module Craft.Types
+, module Craft.Error
+, module Craft.File.Mode
+, module Path
+)
+where
+
+import           Control.Lens
+import           Control.Monad.Catch        (MonadCatch, MonadThrow)
+import           Control.Monad.IO.Class     (MonadIO)
+import           Control.Monad.Logger       (LoggingT, MonadLogger, logDebugN,
+                                             monadLoggerLog)
+import           Control.Monad.Reader       (MonadReader, ReaderT, runReaderT)
+import qualified Control.Monad.Trans.Class  as Trans
+import           Control.Monad.Trans.Free   (FreeT, MonadFree, iterT)
+import           Data.ByteString            (ByteString)
+import qualified Data.ByteString            as BS
+import qualified Data.ByteString.Char8      as B8
+import           Data.ByteString.Lens       (unpackedChars)
+import           Data.Map.Strict            (Map)
+import qualified Data.Map.Strict            as Map
+import           Data.Maybe                 (isNothing)
+import qualified Data.Text                  as T
+import           Data.Versions              (parseV)
+import           Formatting
+import           Language.Haskell.TH.Syntax (Exp, Q)
+import           Path                       hiding (File)
+import qualified Path
+import           Prelude                    hiding (FilePath)
+import qualified Prelude
+import           System.Process
+
+import           Craft.Error
+import           Craft.File.Mode
+import           Craft.Internal.Helpers
+
+
+-- | FileP is an alias because 'Path.File' collides with 'Craft.File'.
+-- The alias was not named FilePath because it would conflict with Prelude.
+type FileP = Path.File
+
+data CraftEnv
+  = CraftEnv
+    { _craftPackageManager :: PackageManager
+    , _craftExecEnv        :: ExecEnv
+    , _craftExecCWD        :: Path Abs Dir
+    }
+
+
+craftEnv :: PackageManager -> CraftEnv
+craftEnv pm =
+  CraftEnv
+  { _craftPackageManager = pm
+  , _craftExecEnv        = Map.fromList [("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")]
+  , _craftExecCWD        = $(mkAbsDir "/")
+  }
+
+
+newtype Craft a = Craft { unCraft :: ReaderT CraftEnv (FreeT CraftDSL (LoggingT IO)) a }
+  deriving ( Functor, Monad, MonadIO, Applicative, MonadReader CraftEnv
+           , MonadFree CraftDSL, MonadThrow, MonadCatch, MonadLogger)
+
+
+instance (MonadLogger m, Functor f) => MonadLogger (FreeT f m) where
+  monadLoggerLog a b c d = Trans.lift $ monadLoggerLog a b c d
+
+
+interpretCraft :: CraftEnv -> (CraftDSL (LoggingT IO a) -> LoggingT IO a) -> Craft a -> LoggingT IO a
+interpretCraft ce interpreter = iterT interpreter . flip runReaderT ce . unCraft
+
+
+data CraftRunner = CraftRunner
+  { runExec       :: CraftEnv -> Command -> Args -> LoggingT IO ExecResult
+  , runExec_      :: CraftEnv -> Command -> Args -> LoggingT IO ()
+  , runFileRead   :: Path Abs FileP -> LoggingT IO ByteString
+  , runFileWrite  :: Path Abs FileP -> ByteString -> LoggingT IO ()
+  , runSourceFile :: Prelude.FilePath -> Path Abs FileP -> LoggingT IO ()
+  }
+
+
+runCraft :: CraftRunner -> CraftEnv -> Craft a -> LoggingT IO a
+runCraft runner ce dsl = do
+  iterT interpreter $ flip runReaderT ce $ unCraft dsl
+ where
+   interpreter (Exec ce' cmd args next) = do
+     logDebugN $ sformat ("Exec "%string%" "%string) cmd (unwords args)
+     (runExec runner) ce' cmd args >>= next
+   interpreter (Exec_ ce' cmd args next) = do
+     logDebugN $ sformat ("Exec_ "%string%" "%string) cmd (unwords args)
+     (runExec_ runner) ce' cmd args >> next
+   interpreter (FileRead fp next) = do
+     logDebugN $ sformat ("FileRead "%shown) fp
+     (runFileRead runner) fp >>= next
+   interpreter (FileWrite fp content next) = do
+     logDebugN $ sformat ("FileWrite "%shown) fp
+     (runFileWrite runner) fp content >> next
+   interpreter (SourceFile sourcer dest next) = do
+     src <- Trans.lift sourcer
+     logDebugN $ sformat ("SourceFile "%string%" "%shown) src dest
+     (runSourceFile runner) src dest >> next
+
+
+type StdOut  = String
+type StdErr  = String
+type Args    = [String]
+type Command = String
+
+
+data SuccResult
+  = SuccResult
+    { _stdout   :: StdOut
+    , _stderr   :: StdErr
+    , _succProc :: CreateProcess
+    }
+
+
+data FailResult
+  = FailResult
+    { _exitcode   :: Int
+    , _failStdout :: StdOut
+    , _failStderr :: StdErr
+    , _failProc   :: CreateProcess
+    }
+
+
+data ExecResult
+  = ExecFail FailResult
+  | ExecSucc SuccResult
+
+
+isSuccess :: ExecResult -> Bool
+isSuccess (ExecSucc _) = True
+isSuccess (ExecFail _) = False
+
+
+isFailure :: ExecResult -> Bool
+isFailure = not . isSuccess
+
+
+errorOnFail :: Q Exp
+errorOnFail = [|
+  \case
+    ExecSucc r -> return r
+    ExecFail r -> $craftError $ show r|]
+
+
+-- | Try to get STDOUT from a process.
+-- If the command exits with an error code, throw a CraftError.
+stdoutOrError :: Q Exp
+stdoutOrError = [|
+  \case
+    ExecSucc r -> return $ _stdout r
+    ExecFail r -> $craftError $ show r|]
+
+
+type ExecEnv = Map String String
+type CWD = Path Abs FileP
+type PackageName = String
+
+newtype UserName = UserName String
+                   deriving (Eq, Ord)
+newtype UserID = UserID Int
+                 deriving (Eq, Show, Ord)
+newtype GroupName = GroupName String
+                    deriving (Eq, Ord)
+newtype GroupID = GroupID Int
+                  deriving (Eq, Show, Ord)
+
+instance Show GroupName where
+  show (GroupName n) = n
+
+instance Show UserName where
+  show (UserName n) = n
+
+instance ToArg UserID where
+  toArg arg (UserID n) = [arg, show n]
+
+instance ToArg GroupID where
+  toArg arg (GroupID n) = [arg, show n]
+
+
+
+class Eq (FileLikePath a) => FileLike a where
+  type FileLikePath a :: *
+  path    :: Lens' a (FileLikePath a)
+  mode    :: Lens' a Mode
+  ownerID :: Lens' a UserID
+  groupID :: Lens' a GroupID
+
+
+data File
+  = File
+  { _filePath    :: Path Abs FileP
+  , _fileMode    :: Mode
+  , _fileOwnerID :: UserID
+  , _fileGroupID :: GroupID
+  , _fileContent :: Maybe ByteString
+  }
+
+
+file :: Path Abs FileP -> File
+file fp =
+  File
+  { _filePath    = fp
+  , _fileMode    = Mode RW R R
+  , _fileOwnerID = UserID 0
+  , _fileGroupID = GroupID 0
+  , _fileContent = Nothing
+  }
+
+
+
+data Directory
+  = Directory
+  { _directoryPath    :: Path Abs Dir
+  , _directoryMode    :: Mode
+  , _directoryOwnerID :: UserID
+  , _directoryGroupID :: GroupID
+  }
+  deriving (Show, Eq)
+
+
+directory :: Path Abs Dir -> Directory
+directory dp =
+  Directory
+  { _directoryPath    = dp
+  , _directoryMode    = Mode RWX RX RX
+  , _directoryOwnerID = UserID 0
+  , _directoryGroupID = GroupID 0
+  }
+
+
+data Version
+  = Version String
+  | AnyVersion
+  | Latest
+  deriving (Show)
+
+
+-- Note: This may or may not make sense.
+-- Open to suggestions if any of this seems incorrect.
+instance Eq Version where
+  (==) AnyVersion  _           = True
+  (==) _           AnyVersion  = True
+  (==) Latest      Latest      = True
+  (==) Latest      (Version _) = False
+  (==) (Version _) Latest      = False
+  (==) (Version a) (Version b) = a == b
+
+
+data Package
+  = Package
+    { _pkgName    :: PackageName
+    , _pkgVersion :: Version
+    }
+  deriving (Eq, Show)
+
+
+data PackageManager
+ = PackageManager
+   { _pmGetter      :: PackageName -> Craft (Maybe Package)
+   , _pmInstaller   :: Package     -> Craft ()
+   , _pmUpgrader    :: Package     -> Craft ()
+   , _pmUninstaller :: Package     -> Craft ()
+   }
+
+
+noPackageManager :: PackageManager
+noPackageManager =
+  let err _ = $craftError "No Package Manager" in
+  PackageManager
+  { _pmGetter         = err
+  , _pmInstaller      = err
+  , _pmUpgrader       = err
+  , _pmUninstaller    = err
+  }
+
+
+data CraftDSL next
+  = Exec  CraftEnv Command Args (ExecResult -> next)
+  | Exec_ CraftEnv Command Args next
+  | FileRead (Path Abs FileP) (ByteString -> next)
+  | FileWrite (Path Abs FileP) ByteString next
+  | SourceFile (IO Prelude.FilePath) (Path Abs FileP) next
+ deriving Functor
+
+
+data CraftRunDSL next
+  = CraftRunDSL (CraftDSL next)
+
+
+makeLenses ''PackageManager
+makeLenses ''CraftEnv
+makeLenses ''Package
+makePrisms ''Version
+makeLenses ''FailResult
+makeLenses ''SuccResult
+makeLenses ''File
+makeLenses ''Directory
+
+
+
+strContent :: Lens' File String
+strContent = lens (view $ fileContent . _Just . unpackedChars)
+                  (\f s -> f & fileContent .~ Just (B8.pack s))
+
+instance Eq File where
+  (==) a b = (a ^. filePath == b ^. filePath)
+          && (a ^. fileMode == b ^. fileMode)
+          && (a ^. fileOwnerID == b ^. fileOwnerID)
+          && (a ^. fileGroupID == b ^. fileGroupID)
+          && (  isNothing (a ^. fileContent)
+             || isNothing (b ^. fileContent)
+             || (a ^. fileContent == b ^. fileContent))
+
+
+instance Show File where
+  show f = "File { _filePath = " ++ show (f ^. filePath) ++
+                ", _fileMode = " ++ show (f ^. fileMode) ++
+                ", _fileOwnerID = " ++ show (f ^. fileOwnerID) ++
+                ", _fileGroupID = " ++ show (f ^. fileGroupID) ++
+                ", _fileContent = " ++ showContent (f ^. fileContent) ++
+               " }"
+    where
+      maxlen = 30
+      showContent Nothing = "Nothing"
+      showContent (Just c)
+        | BS.length c > maxlen = "Just " ++ show (BS.take maxlen c) ++ "..."
+        | otherwise            = "Just " ++ show c
+
+
+
+
+
+instance FileLike File where
+  type FileLikePath File = Path Abs FileP
+  path = filePath
+  mode = fileMode
+  ownerID = fileOwnerID
+  groupID = fileGroupID
+
+
+instance FileLike Directory where
+  type FileLikePath Directory = Path Abs Dir
+  path = directoryPath
+  mode = directoryMode
+  ownerID = directoryOwnerID
+  groupID = directoryGroupID
+
+
+data User
+  = User
+    { _userName         :: UserName
+    , _uid              :: UserID
+    , _userComment      :: String
+    , _userGroup        :: Group
+    , _userGroups       :: [GroupName]
+    , _userHome         :: Path Abs Dir
+    , _userPasswordHash :: String
+    --, _salt         :: String
+    --, _locked       :: Bool
+    , _userShell        :: Path Abs FileP
+    --, system       :: Bool
+    }
+ deriving (Eq, Show)
+
+
+data Group
+  = Group
+    { _groupName    :: GroupName
+    , _gid          :: GroupID
+    , _groupMembers :: [UserName]
+    }
+  deriving (Eq, Show)
+
+
+makeLenses ''User
+makeLenses ''Group
+
+
+
+
+owner :: FileLike a => Setter a a () User
+owner = sets (\functor filelike -> doit filelike (functor ()))
+ where doit filelike o = filelike & ownerID .~ (o ^. uid)
+
+
+group :: FileLike a => Setter a a () Group
+group = sets (\functor filelike -> doit filelike (functor ()))
+ where doit filelike g = filelike & groupID .~ (g ^. gid)
+
+
+ownerAndGroup :: FileLike a => Setter a a () User
+ownerAndGroup = sets (\functor filelike -> doit filelike (functor ()))
+ where doit filelike u = filelike & owner .~ u
+                                  & group .~ (u ^. userGroup)
+
+
+execResultProc :: ExecResult -> CreateProcess
+execResultProc (ExecFail failr) = failr ^. failProc
+execResultProc (ExecSucc succr) = succr ^. succProc
+
+
+instance Show FailResult where
+  show r = unlines
+           [ "exec failed!"
+           , "<<<< process >>>>"
+           , showProc (r ^. failProc)
+           , "<<<< exit code >>>>"
+           , show (r ^. exitcode)
+           , "<<<< stdout >>>>"
+           , r ^. failStdout
+           , "<<<< stderr >>>>"
+           , r ^. failStderr
+           ]
+
+
+showProc :: CreateProcess -> String
+showProc p =
+  case cmdspec p of
+    ShellCommand s     -> s
+    RawCommand fp args -> unwords [fp, unwords args]
+
+
+instance Ord Version where
+  compare AnyVersion  AnyVersion  = EQ
+  compare AnyVersion  Latest      = LT
+  compare AnyVersion  (Version _) = EQ
+  compare Latest      AnyVersion  = GT
+  compare Latest      Latest      = EQ
+  compare Latest      (Version _) = GT
+  compare (Version _) AnyVersion  = EQ
+  compare (Version _) Latest      = LT
+  compare (Version a) (Version b) = compareVersions a b
+
+
+compareVersions :: String -> String -> Ordering
+compareVersions a b = compare (ver a) (ver b)
+ where
+  ver x = case parseV (T.pack x) of
+            Left err -> error $ "Failed to parse version '" ++ x ++ "': " ++ show err
+            Right v  -> v
+
+
+package :: PackageName -> Package
+package n = Package n AnyVersion
+
+
+latest :: PackageName -> Package
+latest n = Package n Latest
+
+
diff --git a/src/Craft/Ubuntu.hs b/src/Craft/Ubuntu.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Ubuntu.hs
@@ -0,0 +1,49 @@
+module Craft.Ubuntu where
+
+import           Control.Lens
+import           Data.ByteString.Lens
+import           Data.String.Utils    (replace)
+import           Formatting
+
+import           Craft
+import           Craft.Apt            as Apt
+import qualified Craft.File           as File
+
+
+data PPA = PPA { _ppaURL :: String }
+  deriving (Eq, Show)
+makeLenses ''PPA
+
+
+findPPAFiles :: PPA -> Craft [File]
+findPPAFiles (PPA url) = do
+  fs <- File.find $(mkAbsDir "/etc/apt/sources.list.d") ["-name", "*" ++ replace "/" "*" url ++ "*.list"]
+  let nonEmpty = (> 0) . length . view (fileContent . _Just . unpackedChars)
+  return $ filter nonEmpty fs
+
+
+instance Craftable PPA PPA where
+  watchCraft ppa@(PPA url) = do
+    fs <- findPPAFiles ppa
+    if null fs
+    then do
+      craft_ $ package "software-properties-common"
+      exec_ "add-apt-repository" ["-y", "ppa:" ++ url]
+      Apt.update
+      return (Created, ppa)
+    else return (Unchanged, ppa)
+
+
+instance Destroyable PPA where
+  watchDestroy ppa@(PPA url) = do
+    fsBefore <- findPPAFiles ppa
+    if null fsBefore
+    then return (Unchanged, Nothing)
+    else do
+      craft_ $ package "software-properties-common"
+      exec_ "add-apt-repository" ["-y", "-r", "ppa:" ++ url]
+      fsAfter <- findPPAFiles ppa
+      unless (null fsAfter) $
+        $craftError $ formatToString ("destroy PPA `"%shown%"` failed! Found: "%shown) ppa fsAfter
+      Apt.update
+      return (Removed, Just ppa)
diff --git a/src/Craft/Upstart.hs b/src/Craft/Upstart.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Upstart.hs
@@ -0,0 +1,37 @@
+module Craft.Upstart where
+
+import           Craft
+
+import           Control.Lens    hiding (noneOf)
+import           Text.Megaparsec
+
+type ServiceName = String
+
+data Service
+  = Service
+    { _name   :: String
+    , _status :: String
+    }
+
+makeLenses ''Service
+
+
+get :: ServiceName -> Craft (Maybe Service)
+get sn =
+  exec "/sbin/status" [sn] >>= \case
+    ExecFail _ -> return Nothing
+    ExecSucc r -> Just . Service sn <$> parseExecResult (ExecSucc r) (statusParser sn) (r ^. stdout)
+
+
+statusParser :: String -> Parsec String String
+statusParser sn = do
+  void $ string sn >> space >> some (noneOf "/") >> char '/'
+  some $ noneOf ","
+
+
+start :: Service -> Craft ()
+start Service{..} = when (_status /= "running") $ exec_ "/sbin/start" [_name]
+
+
+restart :: Service -> Craft ()
+restart Service{..} = exec_ "/sbin/restart" [_name]
diff --git a/src/Craft/User.hs b/src/Craft/User.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/User.hs
@@ -0,0 +1,216 @@
+module Craft.User where
+
+import           Control.Lens             hiding (un)
+import           Data.List                (intercalate)
+import qualified Data.Set                 as S
+import           Formatting
+
+import qualified Craft.Group              as Group
+import           Craft.Internal
+import           Craft.Internal.Helpers
+import           Craft.Internal.UserGroup
+
+
+type Name = UserName
+
+
+name :: Lens' User Name
+name = userName
+
+
+gid :: Lens' User GroupID
+gid = userGroup . Craft.Internal.gid
+
+
+data UserOptions =
+  UserOptions
+  { _optName       :: String
+  , _optUID        :: Maybe UserID
+  , _optComment    :: String
+
+  --, optAllowdupe  :: Bool
+  -- Whether to allow duplicate UIDs. Default: False
+
+  , _optGroup      :: Maybe GroupName
+  -- ^ The default group for a user
+
+  , _optUserGroup  :: Bool
+  -- ^ Create a user group for this user. Default: True
+
+  , _optGroups     :: [GroupName]
+  -- ^ Other groups
+
+  , _optHome       :: Path Abs Dir
+
+  , _optCreateHome :: Bool
+  -- ^ Create the user's home directory when creating user
+
+  , _optPassword   :: Maybe String
+
+  , _optSalt       :: Maybe String
+  -- ^ The salt to use when creating the user's password
+
+
+  --, password_max_age :: Maybe Int
+  --, password_min_age :: Maybe Int
+
+  --, _optLocked     :: Bool
+  -- Lock the user's account
+
+  , _optShell      :: Maybe (Path Abs FileP)
+  -- User's shell
+
+  , _optSystem     :: Bool
+  -- Is the user a system user. Default: False
+  }
+  deriving (Eq, Show)
+makeLenses ''UserOptions
+
+
+-- Nothing means rely on the system's default behavior
+userOptions :: String -> Craft UserOptions
+userOptions n = do
+  dirName <- parseRelDir n
+  return UserOptions
+         { _optName       = n
+         , _optUID        = Nothing
+         , _optComment    = n
+         , _optGroup      = Nothing
+         , _optUserGroup  = True
+         , _optGroups     = []
+         , _optHome       = $(mkAbsDir "/home")</>dirName
+         , _optCreateHome = True
+         , _optPassword   = Nothing
+         , _optSalt       = Nothing
+         , _optShell      = Nothing
+         , _optSystem     = False
+         --, _optLocked     = False
+         }
+
+systemUserOptions :: String -> Craft UserOptions
+systemUserOptions n = do
+  uo <- userOptions n
+  return $ uo & optHome       .~ $(mkAbsDir "/")
+              & optShell      ?~ $(mkAbsFile "/usr/sbin/nologin")
+              & optCreateHome .~ False
+              & optPassword   ?~ ""
+              & optSystem     .~ True
+              -- & optLocked     .~ True
+
+
+userMod :: UserName -> [String] -> Craft ()
+userMod (UserName un) args =
+  exec_ "usermod" $ args ++ [un]
+
+
+getUID :: UserName -> Craft (Maybe UserID)
+getUID (UserName "root") = return . Just $ UserID 0
+getUID (UserName un) =
+  exec "id" ["--user", un] >>= \case
+    ExecSucc r -> return . Just . UserID . read $ r ^. stdout
+    ExecFail _ -> return Nothing
+
+
+setUID :: UserName -> UserID -> Craft ()
+setUID un uid' = userMod un ["--uid", show uid']
+
+
+setShell :: UserName -> Path Abs FileP -> Craft ()
+setShell un shell' = userMod un ["--shell", fromAbsFile shell']
+
+
+setComment :: UserName -> String -> Craft ()
+setComment un comment' = userMod un ["--comment", comment']
+
+
+setGroup :: UserName -> GroupName -> Craft ()
+setGroup un gn =
+  Group.fromName gn  >>= \case
+    Just g  -> userMod un $ toArg "--gid" (g ^. Craft.Internal.gid)
+    Nothing -> $craftError $ formatToString ("setGroup `"%shown%"` `"%shown%"` failed. Group `"%shown%"` not found!") un gn gn
+
+
+setGroups :: UserName -> [GroupName] -> Craft ()
+setGroups _  []  = return ()
+setGroups un gns = userMod un ["--groups", intercalate "," $ map show gns]
+
+
+setHome :: UserName -> Path Abs Dir -> Craft ()
+setHome un homepath = userMod un ["--home", fromAbsDir homepath]
+
+
+ensureUserOpts :: User -> UserOptions -> Craft Bool
+ensureUserOpts user UserOptions{..} = do
+  let checks =
+        [ maybeOpt _optUID     (user^.uid)                   setUID
+        , opt      _optComment (user^.userComment)           setComment
+        , maybeOpt _optGroup   (user^.userGroup . groupName) setGroup
+        , opt      _optHome    (user^.userHome)              setHome
+        , maybeOpt _optShell   (user^.userShell)             setShell
+        , if sameElems _optGroups (user^.userGroups)
+            then setGroups username _optGroups >> return True
+            else return False
+        --TODO: password, salt, lock
+        ] :: [Craft Bool]
+  or <$> sequence checks
+   where
+    username = UserName _optName
+    opt :: Eq a => a -> a -> (UserName -> a -> Craft ()) -> Craft Bool
+    opt expected actual setter
+      | expected == actual = return False
+      | otherwise          = setter username expected >> return True
+    maybeOpt :: Eq a => Maybe a -> a -> (UserName -> a -> Craft ()) -> Craft Bool
+    maybeOpt Nothing _ _                   = return False
+    maybeOpt (Just expected) actual setter = opt expected actual setter
+
+
+sameElems :: Ord a => [a] -> [a] -> Bool
+sameElems xs ys = S.fromList xs == S.fromList ys
+
+
+createUser :: UserOptions -> Craft ()
+createUser UserOptions{..} = do
+  groupArg <- getGroupArg _optGroup
+  exec_ "useradd" $ optsToArgs ++ groupArg ++ groupsArg ++ [show _optName]
+ where
+  getGroupArg :: Maybe GroupName -> Craft [String]
+  getGroupArg Nothing = return []
+  getGroupArg (Just gn) =
+    Group.fromName gn >>= \case
+      Just g  -> return $ toArg "--gid" $ g ^. Craft.Internal.gid
+      Nothing -> $craftError $ "Failed to create User `"++_optName++"` " ++ "with group `"++show gn++"`. Group not found!"
+  groupsArg = toArg "--groups" $ intercalate "," (map show _optGroups)
+  optsToArgs =
+    concat
+      [ toArg "--uid"         _optUID
+      , toArg "--comment"     _optComment
+      , toArg "--home"        _optHome
+      , toArg "--create-home" _optCreateHome
+      , toArgBool "--user-group" "--no-user-group" _optUserGroup
+      , toArg "--password"    (encrypt _optSalt _optPassword)
+      , toArg "--shell"       _optShell
+      , toArg "--system"      _optSystem
+      ]
+
+
+lock :: UserName -> Craft ()
+lock un = userMod un ["--lock"]
+
+
+fromName :: Name -> Craft (Maybe User)
+fromName (UserName n) = userFromStr n
+
+
+fromID :: UserID -> Craft (Maybe User)
+fromID = userFromID
+
+
+--   ___ ___ _____   ___ _____ ___
+--  | _ \ _ \_ _\ \ / /_\_   _| __|
+--  |  _/   /| | \ V / _ \| | | _|
+--  |_| |_|_\___| \_/_/ \_\_| |___|
+
+
+-- TODO
+encrypt :: Maybe String -> Maybe String -> Maybe String
+encrypt _ _ = Nothing
diff --git a/src/Craft/Wget.hs b/src/Craft/Wget.hs
new file mode 100644
--- /dev/null
+++ b/src/Craft/Wget.hs
@@ -0,0 +1,70 @@
+module Craft.Wget where
+
+import           Control.Lens
+import           Data.Monoid    ((<>))
+
+import           Craft
+import           Craft.Checksum (Checksum)
+import qualified Craft.Checksum as Checksum
+import qualified Craft.File     as File
+
+data Wget = Wget
+  { _url    :: String
+  , _dest   :: File
+  , _chksum :: Maybe Checksum
+  , _args   :: Args -- ^Additional arguments to add to the wget command, such as timeouts.
+  }
+  deriving (Show, Eq)
+makeLenses ''Wget
+
+
+wget :: String -> Path Abs FileP -> Wget
+wget url' destfp =
+  Wget
+  { _url    = url'
+  , _dest   = file destfp
+  , _chksum = Nothing
+  , _args   = []
+  }
+
+
+instance Craftable Wget Wget where
+  watchCraft wg = do
+    let destf  = wg ^. dest & fileContent .~ Nothing
+    let destfp = wg ^. dest . path
+    let mismatchError actual = "Checksum Mismatch for `"<>wg^.url<>"`. "
+                            <> "Expected `"<>show (wg^.chksum)<>"` "
+                            <> "Got `"<>show actual<>"`"
+    let check = Checksum.check destf
+    let watchDest = do
+          w <- watchCraft_ destf
+          return (w, wg)
+    File.exists destfp >>= \case
+      True ->
+        case wg ^. chksum of
+          Nothing      -> watchDest
+          Just chksum' ->
+            check chksum' >>= \case
+              Checksum.Matched      -> watchDest
+              Checksum.Failed r     -> $craftError $ show r
+              Checksum.Mismatched _ -> do
+                run wg -- Try redownloading the file if the checksum doesn't match.
+                check chksum' >>= \case
+                  Checksum.Matched           -> watchDest
+                  Checksum.Failed r          -> $craftError $ show r
+                  Checksum.Mismatched actual -> $craftError $ mismatchError actual
+      False -> do
+        run wg
+        case wg ^. chksum of
+          Nothing      -> return ()
+          Just chksum' ->
+            check chksum' >>= \case
+              Checksum.Matched           -> return ()
+              Checksum.Failed r          -> $craftError $ show r
+              Checksum.Mismatched actual -> $craftError $ mismatchError actual
+        void watchDest
+        return (Created, wg)
+
+
+run :: Wget -> Craft ()
+run wg = exec_ "wget" $ (wg^.args) ++ ["-O", (fromAbsFile $ wg^.dest.path), wg^.url]
diff --git a/test/Craft/Config/ShellSpec.hs b/test/Craft/Config/ShellSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Craft/Config/ShellSpec.hs
@@ -0,0 +1,89 @@
+module Craft.Config.ShellSpec (spec) where
+
+import           Test.Hspec
+import           Test.Hspec.Megaparsec
+import           Text.Megaparsec
+
+import           Craft.Config.Shell
+
+
+spec :: Spec
+spec = do
+  describe "set" $ do
+    it "sets on empty" $ do
+      set "a" "b" empty `shouldBe` ShellFormat [("a", "b")]
+    it "replaces existing key" $ do
+      set "a" "c" (ShellFormat [("a", "b")]) `shouldBe` ShellFormat [("a", "c")]
+    it "appends a new keys" $ do
+      set "a" "b" (ShellFormat [("b", "c")])
+        `shouldBe` ShellFormat [("b", "c"), ("a", "b")]
+    it "replaces multiple keys" $ do
+      set "a" "c" (ShellFormat [("a", "foo"), ("a", "bar")])
+      `shouldBe` ShellFormat [("a", "c"), ("a", "c")]
+
+  describe "unset" $ do
+    it "preserves empty" $ do
+      unset "foo" empty `shouldBe` empty
+    it "preserves unmatched keys" $ do
+      unset "a" (ShellFormat [("foo", "")]) `shouldBe` ShellFormat [("foo", "")]
+    it "unsets" $ do
+      unset "a" (ShellFormat [("a", "b")]) `shouldBe` empty
+    it "unsets multiple keys" $ do
+      unset "a" (ShellFormat [("a", "b"), ("a", "c")]) `shouldBe` empty
+    it "unsets multiple keys and leaves unmatched" $ do
+      unset "a" (ShellFormat [("a", "b"), ("b", "foo"), ("a", "c")])
+        `shouldBe` (ShellFormat [("b", "foo")])
+
+  describe "dedup" $ do
+    it "supports empty" $ do
+      dedup empty `shouldBe` empty
+    it "preserves single item lists" $ do
+      dedup (ShellFormat [("a", "b")]) `shouldBe` ShellFormat [("a", "b")]
+    it "dedup uses the final value" $ do
+      dedup (ShellFormat [("a", "b"), ("a", "c")]) `shouldBe` ShellFormat [("a", "c")]
+    it "dedups and preserves order" $ do
+      dedup (ShellFormat [("a", "b"), ("b", ""), ("a", "c")]) `shouldBe` ShellFormat [("a", "c"), ("b", "")]
+
+  describe "parser" $ do
+    it "reads empty files" $ do
+      parse parser "" "" `shouldParse` empty
+    it "ignores empty lines" $ do
+      parse parser "" "\n\n\n" `shouldParse` empty
+    it "ignores empty lines with spaces" $ do
+      parse parser "" "\n   \n\n" `shouldParse` empty
+    it "ignores empty comment" $ do
+      parse parser "" "#" `shouldParse` empty
+    it "ignores comment" $ do
+      parse parser "" "# lkajs #  akjsd\n#lkjaslkdj" `shouldParse` empty
+    it "ignores comment with leading space" $ do
+      parse parser "" "   # foo bar" `shouldParse` empty
+    it "accepts an empty value" $ do
+      parse parser "" "foo=" `shouldParse` ShellFormat [("foo", "")]
+    it "accepts an value with a space in it" $ do
+      parse parser "" "foo=a b " `shouldParse` ShellFormat [("foo", "a")]
+    it "accepts space after equals" $ do
+      parse parser "" "foo= foo" `shouldParse` ShellFormat [("foo", "")]
+    it "ignores comment after a value" $ do
+      parse parser "" "FOO_BAR=foobar # a comment" `shouldParse` ShellFormat [("FOO_BAR", "foobar")]
+    it "ignores comment after a value with no spaces" $ do
+      parse parser "" "FOO_BAR=foobar#comment" `shouldParse` ShellFormat [("FOO_BAR", "foobar")]
+    it "ignores comment after empty value with no spaces" $ do
+      parse parser "" "FOO_BAR=#comment" `shouldParse` ShellFormat [("FOO_BAR", "")]
+    it "parses multiple values" $ do
+      parse parser "" "a=a\nb=b\nc=c\n"
+      `shouldParse` ShellFormat [ ("a", "a")
+                                , ("b", "b")
+                                , ("c", "c")
+                                ]
+    it "parses multiple values with an empty field" $ do
+      parse parser "" "a=a\nb=\nc=c\n"
+      `shouldParse` ShellFormat [ ("a", "a")
+                                , ("b", "")
+                                , ("c", "c")
+                                ]
+    it "works" $ do
+      parse parser "" "#foo\na=a\nb=\n#c=c\nd= foo"
+      `shouldParse` ShellFormat [ ("a", "a")
+                                , ("b", "")
+                                , ("d", "")
+                                ]
diff --git a/test/Craft/File/ModeSpec.hs b/test/Craft/File/ModeSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Craft/File/ModeSpec.hs
@@ -0,0 +1,16 @@
+module Craft.File.ModeSpec (spec) where
+
+import           Test.Hspec
+import           Test.Hspec.QuickCheck
+
+import           Craft.File.Mode
+
+
+spec :: Spec
+spec = do
+  describe "toFileMode" $
+    prop "is inverse of toMode" $
+      \x ->  (x :: Mode) == (toMode . toFileMode) x
+  describe "toOctalString" $
+    prop "is inverse of fromOctalString" $
+      \x ->  (x :: Mode) == (fromOctalString . toOctalString) x
diff --git a/test/Craft/S3FileSpec.hs b/test/Craft/S3FileSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Craft/S3FileSpec.hs
@@ -0,0 +1,49 @@
+module Craft.S3FileSpec (spec) where
+
+import           Data.List             (intercalate)
+import           Test.Hspec
+import           Test.Hspec.Megaparsec
+import qualified Text.Megaparsec       as MP
+
+import           Craft.S3File
+
+respCRLF :: String
+respCRLF = intercalate "\r\n" $ status:headerLines
+
+respNL :: String
+respNL = unlines $ status:headerLines
+
+headerLines :: [String]
+headerLines = map (\(k,v)->k++": "++v) expected
+
+status :: String
+status = "HTTP/1.1 200 OK"
+
+
+expected :: [(String, String)]
+expected =
+  [ ("x-amz-id-2", "3PEnvyK2lmZYO2eOcG31YV7cMtrIHh4LVkIHma4vTLUSlTp2MHWYVytfbv6KwtDcfpjv9mKmdZE=")
+  , ("x-amz-request-id", "534CFEB94E255448")
+  , ("Date", "Wed, 13 Jul 2016 21:08:41 GMT")
+  , ("Last-Modified", "Wed, 13 Jul 2016 21:04:12 GMT")
+  , ("ETag", "\"0d329bab9fe82cae99a6777767efc144\"")
+  , ("x-amz-storage-class", "REDUCED_REDUNDANCY")
+  , ("Accept-Ranges", "bytes")
+  , ("Content-Type", "binary/octet-stream")
+  , ("Content-Length", "29323142")
+  , ("Server", "AmazonS3")
+  ]
+
+
+spec :: Spec
+spec = do
+  describe "httpHeaders" $ do
+    let parse = MP.parse httpHeaders ""
+    it "parses response separated by newlines" $
+      parse respNL `shouldParse` expected
+    it "parses response with trailing newline" $
+      parse (respNL++"\n") `shouldParse` expected
+    it "parses response separated by crlf" $
+      parse respCRLF `shouldParse` expected
+    it "parses response with trailing crlf" $
+      parse (respCRLF++"\r\n") `shouldParse` expected
diff --git a/test/Craft/SSH/AuthorizedKeySpec.hs b/test/Craft/SSH/AuthorizedKeySpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Craft/SSH/AuthorizedKeySpec.hs
@@ -0,0 +1,30 @@
+module Craft.SSH.AuthorizedKeySpec (spec) where
+
+import           Test.Hspec
+import           Test.Hspec.Megaparsec
+import qualified Text.Megaparsec         as MP
+
+import           Craft.SSH
+import           Craft.SSH.AuthorizedKey
+import           Craft.SSH.PublicKey
+
+
+spec :: Spec
+spec = do
+  describe "parsePublicKeys" $ do
+    let parse = MP.parse parsePublicKeys ""
+    it "reads empty file" $
+      parse "" `shouldParse` []
+    it "reads empty lines" $
+      parse "\n\n\n" `shouldParse` []
+    it "reads single RSA public key with no eol or comment" $
+      parse "ssh-rsa AAAiumas98dada8ud9m8ad" `shouldParse` [PublicKey RSA "AAAiumas98dada8ud9m8ad" ""]
+    it "reads single DSA public key ending in newline" $
+      parse "ssh-dsa AAAiumas98dada8ud9m8ad\n" `shouldParse` [PublicKey DSA "AAAiumas98dada8ud9m8ad" ""]
+    it "reads single public key with comment ending in newline" $
+      parse "ssh-ed25519 AAAiumas98dada8ud9m8ad ED25519\n" `shouldParse` [PublicKey ED25519 "AAAiumas98dada8ud9m8ad" "ED25519"]
+    it "reads multiple keys" $ do
+      parse "ssh-rsa AAAiumas98dada8ud9m8ad a comment\nssh-dsa M123YMOSDlkjsadiuasoim8asdoum"
+        `shouldParse` [ PublicKey RSA "AAAiumas98dada8ud9m8ad" "a comment"
+                      , PublicKey DSA "M123YMOSDlkjsadiuasoim8asdoum" ""
+                      ]
diff --git a/test/Craft/SSH/PublicKeySpec.hs b/test/Craft/SSH/PublicKeySpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Craft/SSH/PublicKeySpec.hs
@@ -0,0 +1,26 @@
+module Craft.SSH.PublicKeySpec (spec) where
+
+import           Test.Hspec
+import           Test.Hspec.Megaparsec
+import qualified Text.Megaparsec       as MP
+
+import           Craft.SSH
+import           Craft.SSH.PublicKey
+
+
+spec :: Spec
+spec = do
+  describe "parsePublicKey" $ do
+    let parse = MP.parse parsePublicKey ""
+    it "fails to read empty string" $
+      parse `shouldFailOn` ""
+    it "fails to read just keytype" $
+      parse `shouldFailOn` "ssh-rsa "
+    it "reads RSA public key with no eol or comment" $
+      parse "ssh-rsa AAAiumas98dada8ud9m8ad" `shouldParse` (PublicKey RSA "AAAiumas98dada8ud9m8ad" "")
+    it "reads RSA public key with comment and no eol" $
+      parse "ssh-rsa AAA/umas98dad+8ud9m8ad foo@bar" `shouldParse` (PublicKey RSA "AAA/umas98dad+8ud9m8ad" "foo@bar")
+    it "reads DSA public key ending in newline" $
+      parse "ssh-dsa AAAiumas98dad/8ud9m8ad=\n" `shouldParse` (PublicKey DSA "AAAiumas98dad/8ud9m8ad=" "")
+    it "reads public key with comment ending in newline" $
+      parse "ssh-ed25519 AAAiumas9+dada8ud9m8ad== ED25519\n" `shouldParse` (PublicKey ED25519 "AAAiumas9+dada8ud9m8ad==" "ED25519")
diff --git a/test/Craft/SSHSpec.hs b/test/Craft/SSHSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Craft/SSHSpec.hs
@@ -0,0 +1,32 @@
+module Craft.SSHSpec (spec) where
+
+import           Test.Hspec
+import           Test.Hspec.Megaparsec
+import qualified Text.Megaparsec       as MP
+
+import           Craft.SSH
+
+spec :: Spec
+spec = do
+  describe "parseKeyType" $ do
+    let parse = MP.parse parseKeyType ""
+    it "fails on empty string" $
+      parse `shouldFailOn` ""
+    it "fails on just prefix" $
+      parse `shouldFailOn` "ssh-"
+    it "fails on uppercase prefix" $
+      parse `shouldFailOn` "SSH-RSA"
+    it "parses DSA" $
+      parse "ssh-dsa" `shouldParse` DSA
+    it "parses RSA" $
+      parse "ssh-rsa" `shouldParse` RSA
+    it "parses RSA1" $
+      parse "ssh-rsa1" `shouldParse` RSA1
+    it "parses ECDSA" $
+      parse "ssh-ecdsa" `shouldParse` ECDSA
+    it "parses ED25519" $
+      parse "ssh-ed25519" `shouldParse` ED25519
+    it "parses other keytypes" $
+      parse "ssh-foobar" `shouldParse` (KeyType "ssh-foobar")
+    it "parses mixed case" $
+      parse "ssh-rSa" `shouldParse` RSA
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
