diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Avi Press
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,122 @@
+Confetti
+==========
+
+[![Build Status](https://travis-ci.org/aviaviavi/confetti.svg?branch=master)](https://travis-ci.org/aviaviavi/confetti)
+
+Confetti is a tiny tool for managing different versions of a particular
+configuration file, and quickly swapping them.<br />
+
+Confetti is set up via a spec file, `~/.confetti.yml`. 
+You can specify one or more `groups`, each with one or more `targets`, 
+which are the config files you wish to manage. 
+
+Suppose we have multiple AWS credential files we want to easily switch between (ie use various `~/.aws/credentials`
+variants). To use confetti for this task, we'll put our different files in the target directory, `~/.aws`, 
+named `~/.aws/${variant-name}.credentials`. We might have:
+
+```
+~/.aws/personal.credentials
+~/.aws/work.credentials
+~/.aws/org_name.credentials
+```
+
+A simple example group specification in `~/confetti.yml` might look like:
+```
+groups:
+  - name: aws
+    targets:
+      - /home/you/.aws/credentials
+```
+
+To switch to `work.credentials` simply run: 
+```
+$ confetti [required group_name] [optional variant_name]
+```
+eg.
+```
+$ confetti aws work
+> Setting aws to "work"
+> ~/.aws/credentials -> ~/.aws/work.credentials
+> Success (ﾉ◕ヮ◕)ﾉ*:･ﾟ✧
+```
+
+As you can see from the example output,
+this will symlink `~/.aws/credentials` -> `~/.aws/work.credentials`. If the target file is 
+_not_ a symlink when you invoke confetti, a backup will be made before your variant file is 
+linked. If you have multiple target files in your group, they will all be symlinked to their
+respective variants.
+
+You can specify alternative `search_paths` for each `group`, and whether or not to search recursively.
+Adding a new group with this functionality could make our .confetti.yml look like:
+
+```
+groups:
+  - name: aws
+    targets:
+      - ~/.aws/credentials
+  - name: dotfiles
+    targets:
+      - ~/.spacemacs
+      - ~/.vimrc
+      - ~/.zshrc
+      - ~/.zpreztorc
+      - ~/.tmux.conf
+      - ~/.tmux.style.conf
+    search_paths:
+      - path: ~/dotfiles
+        recursive: true
+```
+
+This makes confetti a great tool for managing things like dotfiles, where the target config files
+are located in a github repo anywhere on the machine. Simply running:
+`confetti dotfiles` could link `~/.spacemacs` -> the first `.spacemacs` found in a recursive search of
+`~/dotfiles`. If your workflow involves regularly swapping any such files, the small amount of 
+initial configuration can be well worth the cost!
+
+One last feature is that you can specify a top level `common` section that will 
+be applied to all other groups. For instance:
+
+```
+common:
+  targets:
+      - ~/dir/some.file
+  search_paths:
+    - path: ~/some/directory
+      recursive: false
+```
+
+Now, every group we try to use confetti for will also link `~/dir/some.file`, and
+will also use `/some/directory` as a search path.
+
+## Installing
+
+### MacOS
+
+You can install with brew via:
+```
+$ brew tap aviaviavi/homebrew-tap
+$ brew install aviaviavi/homebrew-tap/confetti
+```
+
+### Linux 
+
+A linux binary can be found on the releases page.
+
+### Source
+
+You can install from source with [stack](https://docs.haskellstack.org/en/stable/README/).
+
+### Windows
+
+Confetti does not currently work on Windows, as there is some reliance on POSIX
+for dealing with the file system. In theory it should be fairly straight-forward to
+get it working on Windows if anyone is up for implementing it. Feel encouraged to submit a PR
+or reach out for any related discussion.
+
+## Contributing
+
+Contributions in any form are welcome!
+
+## Future Features
+
+* Config groups should be able to source bash files or sets of environment variables.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,54 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+import           Data.Version       (showVersion)
+import           Lib
+import           Paths_confetti     (version)
+import           System.Environment
+import           System.Exit
+import           Text.Printf
+
+import qualified Data.Text          as T
+
+-- Structure for our command line arguments
+data MainArgs = MainArgs
+  { groupName     :: String
+  , variantPrefix :: ConfigVariantPrefix
+  } deriving (Show)
+
+-- Parse our command line arguments for a confetti command,
+-- or display some information and exit
+parseMainArgs :: [String] -> IO MainArgs
+parseMainArgs [g, v] = return (MainArgs g $ Just v)
+parseMainArgs a =
+  let usage =
+        "Usage: `confetti [required group_name] [optional variant_prefix]`"
+      showHelp = (putStrLn usage >> exitSuccess)
+      confettiVersion =
+        putStrLn ("confetti " ++ showVersion version) >> exitSuccess
+  in case a of
+       ["-h"]        -> showHelp
+       ["--help"]    -> showHelp
+       ["-v"]        -> confettiVersion
+       ["--version"] -> confettiVersion
+       [g]           -> return (MainArgs g Nothing)
+       _             -> printFail usage >> exitWith (ExitFailure 1)
+
+main :: IO ()
+main = do
+  args <- getArgs
+  parsed <- parseMainArgs args
+  printf
+    "Setting %s to %s\n"
+    (groupName parsed)
+    (showPrefix $ variantPrefix parsed)
+  specPath <- absolutePath "~/.confetti.yml"
+  group <- parseGroup specPath (T.pack $ groupName parsed)
+  let validatedGroup = group >>= validateSpec
+  either (printFail . show) (runSpec (variantPrefix parsed)) validatedGroup
+
+-- Run our configuration spec. Prints whatever error message we get back,
+-- or a success message
+runSpec :: ConfigVariantPrefix -> ConfigGroup -> IO ()
+runSpec prefix group =
+  applySpec (ConfigSpec group prefix) >>= \maybeError ->
+    maybe (printSuccess "Success (ﾉ◕ヮ◕)ﾉ*:･ﾟ✧") (printFail . show) maybeError
diff --git a/confetti.cabal b/confetti.cabal
new file mode 100644
--- /dev/null
+++ b/confetti.cabal
@@ -0,0 +1,64 @@
+name:                confetti
+version:             0.3.1
+synopsis:            A simple config file swapping tool
+category:            Command Line Tools
+description:         See the README on GitHub at <https://github.com/aviaviavi/confetti#readme>
+
+license:             MIT
+license-file:        LICENSE
+author:              Avi Press
+maintainer:          avipress@gmail.com
+homepage:            https://github.com/aviaviavi/confetti
+bug-reports:         https://github.com/aviaviavi/confetti/issues
+
+build-type:          Simple
+stability:           alpha (experimental)
+cabal-version:       >=1.10
+
+extra-source-files:
+  README.md
+  stack.yaml
+
+source-repository head
+  type:     git
+  location: https://github.com/aviaviavi/confetti
+
+library
+  default-language:  Haskell2010
+  hs-source-dirs:    src
+  exposed-modules:   Lib
+  build-depends:     base >= 4.8 && < 5
+                   , directory
+                   , text
+                   , text >= 1.2.2.1
+                   , unix
+                   , MissingH
+                   , time
+                   , yaml
+                   , filepath
+  other-modules:
+      Paths_confetti
+
+executable confetti
+  default-language:  Haskell2010
+  hs-source-dirs:    app
+  main-is:           Main.hs
+  build-depends:     base >= 4.8 && < 5
+                   , directory
+                   , confetti
+                   , text >= 1.2.2.1
+  other-modules:
+      Paths_confetti
+
+test-suite confetti-test
+  type:              exitcode-stdio-1.0
+  default-language:  Haskell2010
+  hs-source-dirs:    test
+  main-is:           Main.hs
+  build-depends:     base >= 4.8 && < 5
+                   , confetti
+                   , tasty >= 0.11
+                   , tasty-hunit >= 0.9
+                   , tasty-smallcheck >= 0.8
+                   , text >= 1.2.2.1
+
diff --git a/src/Lib.hs b/src/Lib.hs
new file mode 100644
--- /dev/null
+++ b/src/Lib.hs
@@ -0,0 +1,354 @@
+{-# LANGUAGE DeriveGeneric         #-}
+{-# LANGUAGE OverloadedStrings     #-}
+
+module Lib where
+
+import           Control.Exception
+import           Control.Monad
+import           Data.List
+import           Data.List.Utils
+import           Data.Either.Utils
+import           Data.Maybe
+import           Data.Monoid
+import           Data.Time.Clock.POSIX
+import           Data.Yaml
+import           GHC.Generics
+import           System.Directory
+import           System.FilePath.Posix
+import           System.IO.Error
+import           System.Posix.Files
+import           Text.Printf
+
+
+import qualified Data.Text             as T
+
+-- Errors from parsing or validating .confetti.yml
+data ParseError a = ConfettiYamlNotFound | GroupNotFound a | ConfettiYamlInvalid a | DuplicateNameError a deriving (Generic)
+
+instance (Show a) =>
+         Show (ParseError a) where
+  show (ConfettiYamlInvalid a) =
+    "There was an issue parsing your ~/.confetti.yml: " ++ show a
+  show (GroupNotFound a) =
+    printf "No match for group %s found in your ~/.confetti.yml: " (show a)
+  show ConfettiYamlNotFound =
+    "No confetti spec file found! See https://github.com/aviaviavi/confetti if you need help setting one up"
+  show (DuplicateNameError a) =
+    printf
+      "Multiple targets in this group share the same name: %s.\
+       \ Confetti doesn't yet know how to figure out target to link a search match to,\
+       \ but a future version will. Try breaking these targets into multiple groups."
+      (show a)
+
+-- Errors from applying our config spec
+newtype ApplyError a = VariantsMissing [a]
+instance (Show a) => Show (ApplyError a)  where
+  show (VariantsMissing a) = "Couldn't find one or more of your variant files to use: " ++ show a
+
+-- A config file version we want to swap in or out
+type ConfigVariant = String
+
+-- The config file version we want to swap in or out, with no directory
+-- information, eg `credentials`, not `~/.aws/credentials`
+type ConfigVariantFileName = String
+
+-- The prefix specifies how we construct a variant.
+-- If the target is config.json, and we want to link local.config.json,
+-- (Just "local") would be the prefix
+type ConfigVariantPrefix = Maybe String
+
+-- Small helper for printing prefix
+showPrefix :: ConfigVariantPrefix -> String
+showPrefix prefix = case prefix of
+  (Just p) -> show p
+  Nothing  -> "bare matches in search paths"
+
+-- The actual config file in question.
+type ConfigTarget = String
+
+-- Lets us specify paths where the variant files in a group can be found.
+data SearchPath = SearchPath
+  { path      :: FilePath
+  , recursive :: Maybe Bool
+  } deriving (Show, Eq, Generic)
+
+instance FromJSON SearchPath
+
+-- Represents a search result for a given target, variant and directory
+data VariantSearch = VariantSearch
+  { searchDirectory :: FilePath
+  , fileName        :: ConfigVariantFileName
+  , recursiveSearch :: Bool
+  , result          :: Maybe ConfigVariant -- populated when find the file we want to swap in
+  , linkToCreate    :: ConfigTarget
+  } deriving (Show, Generic)
+
+-- A set of configuration targets
+data ConfigGroup = ConfigGroup
+  { name        :: T.Text
+  , targets     :: [FilePath]
+  , searchPaths :: Maybe [SearchPath]
+  } deriving (Show, Generic)
+
+instance FromJSON ConfigGroup where
+  parseJSON (Object x) =
+    ConfigGroup <$> x .: "name" <*> x .: "targets" <*> x .:? "search_paths"
+  parseJSON _ = fail "Expected an object"
+
+data CommonConfigGroup = CommonConfigGroup
+  { commonTargets     :: [FilePath]
+  , commonSearchPaths :: Maybe [SearchPath]
+  } deriving (Show, Generic)
+
+instance FromJSON CommonConfigGroup where
+  parseJSON (Object x) =
+    CommonConfigGroup <$> x .: "targets" <*> x .:? "search_paths"
+  parseJSON _ = fail "Expected an object"
+
+-- A valid .confetti.yml gets parsed into this structure
+data ParsedSpecFile = ParsedSpecFile
+  { groups      :: [ConfigGroup]
+  , commonGroup :: Maybe CommonConfigGroup
+  } deriving (Show, Generic)
+
+instance FromJSON ParsedSpecFile where
+  parseJSON (Object x) =
+    ParsedSpecFile <$> x .: "groups" <*> x .:? "common"
+  parseJSON _ = fail "Expected an object"
+
+-- A full config specification:
+-- a group of files, and a variant to swap in for
+-- every target in the group
+data ConfigSpec = ConfigSpec
+  { configGroup         :: ConfigGroup
+  , configVariantPrefix :: ConfigVariantPrefix
+  }
+
+-- Given a yaml file and a group name, parse the group into a ConfigGroup, or
+-- a ParseError.
+-- If a `common` group is specified, that group will be combined with the parsed one
+parseGroup :: FilePath -> T.Text -> IO (Either (ParseError T.Text) ConfigGroup)
+parseGroup specFile groupName =
+  doesFileExist specFile >>= \exists -> parseGroup' exists
+  where
+    parseGroup' exists
+      | not exists = return $ Left ConfettiYamlNotFound
+      | otherwise = do
+        eitherSpec <- decodeFileEither specFile
+        eitherGroup <-
+          either
+            (return .
+             Left . ConfettiYamlInvalid . T.pack . prettyPrintParseException)
+            (`findGroup` groupName)
+            eitherSpec
+        either
+          (return . Left)
+          (\g ->
+             let spec = fromRight eitherSpec
+             in if isJust $ commonGroup spec
+                  then Right <$>
+                       appendCommonGroup g (fromJust $ commonGroup spec)
+                  else return $ Right g)
+          eitherGroup
+
+-- Combine whatever group we parsed with the common group, if one was
+-- specified
+appendCommonGroup :: ConfigGroup -> CommonConfigGroup -> IO ConfigGroup
+appendCommonGroup g common = do
+  cTargets <- mapM absolutePath $ commonTargets common
+  cSearchPaths <- defaultSearchPaths cTargets (commonSearchPaths common)
+  adjustedGroupSearchPaths <- defaultSearchPaths (targets g) (searchPaths g)
+  return
+    ConfigGroup
+    { name = name g
+    , targets = uniq $ targets g ++ cTargets
+    , searchPaths = Just . uniq $ adjustedGroupSearchPaths <> cSearchPaths
+    }
+
+-- Expand all search paths. If search paths are empty, use the paths of the
+-- supplied targets
+defaultSearchPaths :: [ConfigTarget] -> Maybe [SearchPath] -> IO [SearchPath]
+defaultSearchPaths ts ss =
+  let unadjusted =
+        fromMaybe
+           (map
+              (\t -> SearchPath {path = takeDirectory t, recursive = Just False})
+              ts)
+           ss
+  in
+  mapM
+    (\s -> do
+       absolute <- absolutePath (path s)
+       return $ s {path = absolute})
+    unadjusted
+
+-- Any custom validation we want to do on a config group we've successfully parsed
+-- goes here
+validateSpec :: ConfigGroup -> Either (ParseError T.Text) ConfigGroup
+validateSpec groupSpec =
+  let targetFileNames = map takeFileName $ targets groupSpec
+  in if length targetFileNames > length (uniq targetFileNames)
+       then Left
+              (DuplicateNameError $
+               T.pack . head $ targetFileNames \\ uniq targetFileNames)
+       else Right groupSpec
+
+-- Finds a given group in a parsed spec file. Returns a GroupNotFound if the group
+-- is missing
+findGroup :: ParsedSpecFile
+          -> T.Text
+          -> IO (Either (ParseError T.Text) ConfigGroup)
+findGroup spec groupName =
+  sequence $
+  maybe (Left $ GroupNotFound groupName) (Right . expandPathsForGroup) $
+  find (\g -> name g == groupName) (groups spec)
+
+-- Replaces ~ with the value of $HOME for all files in the group
+expandPathsForGroup :: ConfigGroup -> IO ConfigGroup
+expandPathsForGroup confGroup =
+  mapM absolutePath (targets confGroup) >>= \expanded ->
+    return $ confGroup {targets = expanded}
+
+-- If a target config file is _not_ a symlink,
+-- make a backup before we swap out the config
+backUpIfNonSymLink :: FilePath -> IO ()
+backUpIfNonSymLink file = do
+  exists <- doesFileExist file
+  isLink <-
+    if exists
+      then pathIsSymbolicLink file
+      else return False
+  when (exists && not isLink) $ createBackup file
+
+-- Backs up a file, eg config.json -> config.json.$time.backup
+createBackup :: FilePath -> IO ()
+createBackup file =
+  let newName =
+        (round <$> getPOSIXTime) >>=
+        (\t -> return $ file ++ "." ++ show t ++ ".backup")
+  in newName >>= \backup -> copyFile file backup
+
+removeIfExists :: FilePath -> IO ()
+removeIfExists f = removeFile f `catch` handleExists
+  where
+    handleExists e
+      | isDoesNotExistError e = return ()
+      | otherwise = throwIO e
+
+-- Given a list of variant configs, returns the variants that
+-- do not exist
+filterMissingVariants :: [ConfigVariant] -> IO [FilePath]
+filterMissingVariants = filterM (fmap not . doesFileExist)
+
+-- This is where the actual config swapping happens. For every ConfigTarget,
+-- creates a symlink from the target -> variant.
+-- For instance, for target = ~/.aws/credentials, and variant "work",
+-- the link created would be
+-- ~/.aws/credentials -> ~/.aws/work.credentials
+linkTargets :: [VariantSearch] -> IO ()
+linkTargets =
+  mapM_
+    (\pair -> createSymbolicLink (fromJust $ result pair) (linkToCreate pair))
+
+-- Given a variant prefix, eg `dev`, and a target, eg `~/.aws/credentials`,
+-- construct the full path of the variant, `~/.aws/dev.credentials`
+makeVariant :: ConfigVariantPrefix -> ConfigTarget -> ConfigVariantFileName
+makeVariant prefix target =
+  let p = maybe "" (++ ".") prefix
+  in p ++ takeFileName target
+
+-- Given a prefix, a list of targets, construct a list of variant filenames, and search
+-- for matches in all of the given search paths
+searchVariants :: ConfigVariantPrefix
+               -> [ConfigTarget]
+               -> [SearchPath]
+               -> IO [VariantSearch]
+searchVariants variant targetFiles vPaths =
+  concat <$>
+  mapM (\target -> mapM (findVariantInPath variant target) vPaths) targetFiles
+
+-- Get all the contents of a directory recursively
+getRecursiveContents :: FilePath -> IO [FilePath]
+getRecursiveContents topdir = do
+  names <- getDirectoryContents topdir
+  let properNames = filter (`notElem` [".", ".."]) names
+  paths <-
+    forM properNames $ \n -> do
+      let nextPath = topdir </> n
+      isDir <- doesDirectoryExist nextPath
+      if isDir
+        then getRecursiveContents nextPath
+        else return [nextPath]
+  return (concat paths)
+
+-- Perform a search for a single file, and return the search result
+findVariantInPath :: ConfigVariantPrefix
+                  -> ConfigTarget
+                  -> SearchPath
+                  -> IO VariantSearch
+findVariantInPath prefix target searchPath =
+  let fileToFind = makeVariant prefix target
+  in do pathName <- absolutePath $ path searchPath
+        let isRecursive = fromMaybe False $ recursive searchPath
+        searchResult <-
+          if isRecursive
+            then find (\f -> endswith fileToFind f && (target /= f)) <$>
+                 getRecursiveContents pathName
+            else do
+              let potentialVariant = pathName </> fileToFind
+              exists <- doesFileExist potentialVariant
+              if exists
+                then return $ Just potentialVariant
+                else return Nothing
+        return
+          VariantSearch
+          { searchDirectory = path searchPath
+          , fileName = fileToFind
+          , recursiveSearch = isRecursive
+          , linkToCreate = target
+          , result = searchResult
+          }
+
+-- Run the spec! Every target will be a symlink to the variant
+-- file
+applySpec :: ConfigSpec -> IO (Maybe (ApplyError FilePath))
+applySpec spec = do
+  let groupTargets = targets $ configGroup spec
+  searchResults <-
+    searchVariants
+      (configVariantPrefix spec)
+      groupTargets
+      (fromMaybe
+         (map
+            (\t -> SearchPath {path = takeDirectory t, recursive = Just False})
+            groupTargets)
+         (searchPaths $ configGroup spec))
+  mapM_ backUpIfNonSymLink groupTargets
+  mapM_ removeIfExists groupTargets
+  let confirmedVariantFiles = filter (isJust . result) searchResults
+      foundFiles =
+        uniq $ map (takeFileName . fromJust . result) confirmedVariantFiles
+      allFiles = uniq $ map fileName searchResults
+      missingVariants = allFiles \\ foundFiles
+  if null missingVariants
+    then do
+      mapM_
+        (\s -> printSuccess $ linkToCreate s ++ " -> " ++ fromJust (result s))
+        confirmedVariantFiles
+      linkTargets confirmedVariantFiles
+      return Nothing
+    else return $ Just (VariantsMissing missingVariants)
+
+-- Expands ~ to $HOME in a path
+absolutePath :: FilePath -> IO FilePath
+absolutePath p = do
+  home <- getHomeDirectory
+  return $ replace "~" home p
+
+-- Prints green
+printSuccess :: String -> IO ()
+printSuccess s = putStrLn $ "\x1b[32m" ++ s ++ "\x1B[0m"
+
+-- Prints red
+printFail :: String -> IO ()
+printFail s = putStrLn $ "\x1b[31m" ++  s ++ "\x1B[0m"
diff --git a/stack.yaml b/stack.yaml
new file mode 100644
--- /dev/null
+++ b/stack.yaml
@@ -0,0 +1,6 @@
+flags: {}
+extra-package-dbs: []
+packages:
+- .
+extra-deps: []
+resolver: lts-12.7
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,12 @@
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.SmallCheck
+
+import Lib
+
+main :: IO ()
+main = defaultMain $ testGroup "all-tests" tests
+
+-- TODO: some tests would be good for future avi to write.
+tests :: [TestTree]
+tests = []
