diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2012 Stefan Saasen
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+3. Neither the name of the author nor the names of his contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,89 @@
+Spy
+===
+
+Spy is a compact file system watcher for Mac OS X using the [File System Events API](https://developer.apple.com/library/mac/#documentation/Darwin/Conceptual/FSEvents_ProgGuide/Introduction/Introduction.html) via [hfsevents](https://github.com/luite/hfsevents).
+
+
+Usage
+=====
+
+Spy expects a single argument, the directory or a single file to watch. Spy currently supports two different modes: watch and run mode.
+
+Watch mode
+-----------
+
+In output mode Spy will print the path to a modified file to STDOUT (followed by a newline) whenever a file modification (new file added, file modified, file deleted) occurs.
+
+    $> spy watch .
+    path/to/modified/file
+
+Because watch is the default mode you can omit the `watch` command if you want:
+
+    $> spy .
+
+It's possible to watch a single file (this obviously only shows changes to that particular file):
+
+    $> spy /path/to/file
+
+The default format is the full path to the modified file followed by a newline. To make it easier to parse the output, the `--format=json` changes the output to be printed formatted as a JSON object (again followed by a newline).
+
+    $> spy watch --format=json .
+    {"path": "path/to/modified/file", "flags": ["ItemModified"], "id": 123143234}
+
+For directories the following options apply:
+
+An optional second argument can be used to filter the files in the given directory using a glob pattern:
+
+    $> spy watch /path/to/directory "*.md"
+
+
+Run mode
+--------
+
+In run mode Spy will execute a given command whenever a file modification occurs without printing modifications to stdout. The command will be executed with the path to the modified file as the last argument.
+
+    $> spy run "./run-build.sh"
+
+In the example above the shell script `run-build.sh` would be executed with the path to the modified file as the first argument.
+
+If the command to be executed does not expect any (additional) arguments the `--notify-only` flag can be used. This will cause spy to execute the command without passing the path as an argument:
+
+    $> spy run --notify-only "rake test" .
+
+
+Installation
+============
+
+Spy only works on Mac OS X >= 10.7 (Lion and above)!
+
+Binary distribution
+-------------------
+
+The binary distribution contains a 64bit binary compiled for Mac OS X > 10.7.
+
+Download the tarball and run "make install" to copy the binary and the man page into the correct target directories:
+
+    $> curl -OL https://bitbucket.org/ssaasen/spy/downloads/spy-osx-x86_64-v0.4.tar.gz
+    $> tar xfz spy-osx-x86_64-v0.4.tar.gz
+    $> cd spy
+    $> make install
+
+The user manual should now be available via `man spy` and the `spy` executable should be on your `$PATH`.
+
+
+Source distribution
+-------------------
+
+To install spy from source you need the Haskell platform installed and cabal-install available on your $PATH:
+
+    $> cabal install --only-dependencies
+    $> cabal configure
+    $> cabal build
+
+This will create the spy binary in the ./dist/build/spy directory.
+
+To copy the spy binary to the cabal bin directory (which should be available on your PATH) use:
+
+    $> cabal copy
+
+
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,3 @@
+import Distribution.Simple
+main = defaultMain
+
diff --git a/spy.cabal b/spy.cabal
new file mode 100644
--- /dev/null
+++ b/spy.cabal
@@ -0,0 +1,62 @@
+name:               spy
+version:            0.4
+license:            BSD3
+license-file:       LICENSE
+author:             Stefan Saasen
+maintainer:         stefan@saasen.me
+synopsis:           A compact file system watcher for Mac OS X
+category:           Apple, Development
+description:        Spy can be used to watch for file changes and to either report the modified files or run a command if files change. It can be used to trigger compilation, to run tests or start a deployment.
+homepage:           https://bitbucket.org/ssaasen/spy
+bug-reports:        https://bitbucket.org/ssaasen/spy/issues
+cabal-version:      >= 1.8
+build-type:         Simple
+extra-source-files: README.md
+
+source-repository   head
+  type:             git
+  location:         git@bitbucket.org:ssaasen/spy.git
+
+flag                small_base
+  description:      Choose the new, split-up base package.
+
+test-suite spy-testsuite
+  type:                     exitcode-stdio-1.0
+  main-is:                  Tests.hs
+  hs-source-dirs:           tests, src
+  build-depends:            base < 5 && >= 3,
+                            test-framework >= 0.3.3,
+                            test-framework-quickcheck2 >= 0.2.9,
+                            test-framework-hunit,
+                            HUnit,
+                            QuickCheck >= 2.4.0.1,
+                            hfsevents >= 0.1.3,
+                            cmdargs >= 0.10,
+                            filepath >= 1.3,
+                            filemanip >= 0.3.6.2,
+                            process >= 1.1,
+                            json >= 0.7,
+                            directory >= 1.1
+
+executable  spy
+  main-is:          Main.hs
+  other-modules:    Spy.Watcher
+  build-depends:
+                    base < 5 && >= 3,
+                    hfsevents >= 0.1.3,
+                    cmdargs >= 0.10,
+                    filepath >= 1.3,
+                    filemanip >= 0.3.6.2,
+                    process >= 1.1,
+                    json >= 0.7,
+                    directory >= 1.1
+
+  ghc-options:
+                    -Wall
+                    -rtsopts
+                    -fno-warn-unused-do-bind
+                    -fno-warn-missing-signatures
+
+  hs-source-dirs:
+                    src
+
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,46 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+module Main where
+
+import System.Console.CmdArgs
+import System.Directory (canonicalizePath)
+import Spy.Watcher
+
+version :: String
+version = "spy v0.4, (C) Stefan Saasen"
+
+hiddenOpts x = x &= help "Set to true if hidden files/directories should be included" &= name "i" &= typ  "BOOL"
+
+watch :: Spy
+watch = Watch
+    {hidden     = hiddenOpts False
+    ,dir        = "."               &= argPos 0  &= typ "FILE/DIR"
+    ,format     = Just plainFormat  &= name "f"  &= help "Specify the output format ('json', 'plain')"
+    ,glob       = Nothing           &= args &= typ "GLOB"
+    }
+    &= help "Watch a directory (or file) for file changes"
+    &= auto
+
+run :: Spy
+run = Run
+    {hidden     = hiddenOpts False
+    ,command    = def           &= argPos 0                        &= typ "CMD"
+    ,dir        = "."           &= argPos 1                        &= typ "FILE/DIR"
+    ,glob       = Nothing       &= args                            &= typ "GLOB"
+    ,notifyOnly = False         &= name "n"  &= name "notify-only" &= typ "BOOL"
+    }
+    &= help "Run a command whenever a file changes"
+
+
+mode :: Mode (CmdArgs Spy)
+mode = cmdArgsMode $ modes [watch,run]
+        &= help "spy watches for file changes on the file system"
+        &= program "spy" &= summary version
+
+
+main :: IO ()
+main = do
+    config <- cmdArgsRun mode
+    canonicalizedDir <- canonicalizePath $ dir config
+    spy config { dir = canonicalizedDir }
+    putStrLn "No eyes on the target"
+
diff --git a/src/Spy/Watcher.hs b/src/Spy/Watcher.hs
new file mode 100644
--- /dev/null
+++ b/src/Spy/Watcher.hs
@@ -0,0 +1,142 @@
+{-# LANGUAGE DeriveDataTypeable,RecordWildCards #-}
+
+module Spy.Watcher
+(
+ spy
+,Format
+,plainFormat
+,Spy(..)
+,skipEvent
+,matchesFile
+,containsHiddenPathElement
+) where
+
+import System.OSX.FSEvents
+import System.Console.CmdArgs
+import System.Cmd
+import System.Exit
+import System.IO (stderr, hPrint)
+import System.FilePath.GlobPattern
+import System.FilePath (splitDirectories, takeFileName)
+import Control.Monad (unless)
+import Control.Exception (bracket)
+import Data.Maybe (fromMaybe, maybeToList)
+import Data.Bits
+import Data.Word
+import Text.JSON
+
+-- | The output format when Spy prints out changes to STDOUT
+data Format = Json | Plain deriving (Show, Eq, Data, Typeable)
+
+-- | Spy run modes.
+data Spy = Watch {
+     dir                :: FilePath
+    ,glob               :: Maybe GlobPattern
+    ,format             :: Maybe Format
+    ,hidden             :: Bool
+} | Run {
+     dir                :: FilePath
+    ,command            :: String
+    ,glob               :: Maybe GlobPattern
+    ,hidden             :: Bool
+    ,notifyOnly         :: Bool
+} deriving (Data,Typeable,Show,Eq)
+
+-- | Return the Plain format.
+plainFormat :: Format
+plainFormat = Plain
+
+-- | Register for FS events using the given Spy config.
+spy :: Spy -> IO String
+spy config = bracket
+  (eventStreamCreate [dir config] 1.0 True True True $ handleEvent config)
+  eventStreamDestroy
+  (\_ -> getLine)
+
+-- | Handle the FS event based on the current Spy run configuration
+handleEvent :: Spy -> Event -> IO ()
+handleEvent config@Run{..} event =
+        unless (skipEvent config event) $
+        runCommand command pathAsArg >>=
+            \exit -> case exit of
+                ExitSuccess     -> return ()
+                ExitFailure i   -> hPrint stderr $ "Failed to execute " ++ command ++ " - exit code: " ++ show i
+        where pathAsArg = if notifyOnly then
+                            Nothing
+                            else Just (eventPath event)
+
+handleEvent config@Watch{..} event =
+        unless (skipEvent config event) $
+        putStrLn $ (outputHandler $ fromMaybe Plain format) event
+
+-- =================================================================================
+
+runCommand :: Command -> Maybe FilePath -> IO ExitCode
+runCommand cmd maybePath = rawSystem p $ mergeArgs args'
+    where (p, args')            = case words cmd of
+                                    (x:xs) -> (x, xs)
+                                    _      -> ("", [])
+          mergeArgs defaultArgs = defaultArgs ++ maybeToList maybePath
+
+-- =================================================================================
+
+type Printer = (Event -> String)
+type Command = String
+
+outputHandler :: Format -> Printer
+outputHandler Json  = \event -> encode $ makeObj [
+    ("path", showJSON $ eventPath event),
+    ("id", showJSON $ eventId event),
+    ("flags", showJSONs $ showEventFlags $ eventFlags event)]
+outputHandler Plain = eventPath
+
+
+-- | Skip events based on the configuration given
+skipEvent :: Spy -> Event -> Bool
+skipEvent config event = skipHidden || skipNonMatchingGlob
+    where skipHidden            = let includeHiddenfiles = hidden config
+                                  in not includeHiddenfiles && containsHiddenPathElement path
+          skipNonMatchingGlob   = maybe False (not . matchesFile path) $ glob config
+          path                  = eventPath event
+
+
+matchesFile :: FilePath -> GlobPattern -> Bool
+matchesFile path glob' = takeFileName path ~~ glob'
+
+containsHiddenPathElement :: FilePath -> Bool
+containsHiddenPathElement path = any isHidden paths
+                where paths = splitDirectories path
+                      isHidden name' = case name' of
+                                            (x:_)   -> x == '.'
+                                            _       -> False
+
+-- =================================================================================
+-- The following code is taken from:
+-- https://github.com/luite/hfsevents/blob/master/test/trace.hs
+-- Copyright (c) 2012, Luite Stegeman
+showEventFlags :: Word64 -> [String]
+showEventFlags fl = map fst . filter hasFlag $ flagList
+  where hasFlag (_,f) = fl .&. f /= 0
+
+
+flagList :: [(String, Word64)]
+flagList = [ ("MustScanSubDirs"   , 0x00000001)
+           , ("UserDropped"       , 0x00000002)
+           , ("KernelDropped"     , 0x00000004)
+           , ("EventIdsWrapped"   , 0x00000008)
+           , ("HistoryDone"       , 0x00000010)
+           , ("RootChanged"       , 0x00000020)
+           , ("Mount"             , 0x00000040)
+           , ("Unmount"           , 0x00000080)
+           , ("ItemCreated"       , 0x00000100)
+           , ("ItemRemoved"       , 0x00000200)
+           , ("ItemInodeMetaMod"  , 0x00000400)
+           , ("ItemRenamed"       , 0x00000800)
+           , ("ItemModified"      , 0x00001000)
+           , ("ItemFinderInfoMod" , 0x00002000)
+           , ("ItemChangeOwner"   , 0x00004000)
+           , ("ItemXattrMod"      , 0x00008000)
+           , ("ItemIsFile"        , 0x00010000)
+           , ("ItemIsDir"         , 0x00020000)
+           , ("ItemIsSymlink"     , 0x00040000)
+           ]
diff --git a/tests/Tests.hs b/tests/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Tests.hs
@@ -0,0 +1,56 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main (main) where
+
+import qualified Test.HUnit as H
+import Data.Maybe
+import System.OSX.FSEvents
+import Spy.Watcher
+import Test.QuickCheck hiding ((.&.))
+import Test.Framework (Test, defaultMain, testGroup)
+import Test.Framework.Providers.QuickCheck2 (testProperty)
+import Test.Framework.Providers.HUnit
+
+
+test_globMatchesFile = H.assertBool
+  "*.hs should match a Haskell source file"
+  (matchesFile "/path/to/Main.hs" "*.hs")
+
+test_globDoesNotMatchFile = H.assertBool
+  "*.hs should not match a C source file"
+  (not $ matchesFile "/path/to/Main.c" "*.hs")
+
+test_skipEventHidden = H.assertBool
+  "Skip path if hidden directory and showing hidden files is not enabled"
+  (skipEvent (mockWatch {hidden = False}) (mockEvent "/a/b/.git/refs"))
+
+test_containsHiddenPathElement = H.assertBool
+  "Should identify hidden directory"
+  (containsHiddenPathElement "/a/b/.git/info/exclude")
+
+-- ===========================================================
+mockWatch :: Spy
+mockWatch = Watch { dir = ".", glob = Nothing, format = Nothing, hidden = False }
+
+mockEvent :: FilePath -> Event
+mockEvent path = Event path 0x00000001 0x00000001
+------------------------------------------------------------------------
+-- Test harness
+
+main :: IO ()
+main = defaultMain tests
+
+tests :: [Test]
+tests =
+    [ testGroup "Watcher"
+      [
+        testCase "watcher/shouldMatchFilePath" test_globMatchesFile,
+        testCase "watcher/shouldNotMatchFilePath" test_globDoesNotMatchFile
+      ],
+      testGroup "Watcher opts"
+      [
+        testCase "watcher/optHidden" test_skipEventHidden,
+        testCase "watcher/containsHiddenPathElement" test_containsHiddenPathElement
+      ]
+    ]
+
