diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for cabal-hoogle
+
+## 0.1.0.0 -- 2022-07-22
+
+* First version. Supports `cabal-hoogle generate` and `cabal-hoogle run`
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,113 @@
+# cabal-hoogle
+
+![GitHub Action Badge](https://github.com/kokobd/cabal-hoogle/actions/workflows/test.yml/badge.svg?branch=main)
+
+Like `stack hoogle`, but works for cabal projects. Generates hoogle database of your local packages and all dependencies.
+
+<img src="https://user-images.githubusercontent.com/16440269/180609310-643ff9a1-c1eb-479a-b9ca-0cf69d65a62a.gif" width="600"/>
+
+## Installation
+
+Compiling from source requires a minimum GHC version of 8.10. Having `cabal-hoogle` installed, you may use it on a wider range of GHC,
+as long as the `Cabal` used to build `cabal-hoogle` and `cabal-install` match.
+
+Step 1, run `cabal update` to make sure you are using the latest hackage index
+
+Step 2, check the `Cabal` library version used by your cabal
+```
+cabal --version
+```
+It shows my cabal is using `3.6.3.0` of Cabal library. **`cabal-hoogle` requires at least Cabal 3.2**. Update it before continue.
+```
+cabal-install version 3.6.2.0
+compiled using version 3.6.3.0 of the Cabal library 
+```
+
+Step 3, use `cabal install` to install `cabal-hoogle` from Hackage, specifying the `Cabal` library version you fetched above.
+```
+cabal install --constraint="Cabal == 3.6.3.0" cabal-hoogle
+```
+
+## Usage
+
+### Prerequisite
+
+#### Install Hoogle
+Make sure `hoogle` is installed in your `$PATH`. You can do this by running `cabal install hoogle`
+
+#### Enable Hoogle for Cabal
+Make sure your `~/.cabal/config` has the following entries. (DON'T remove existing entries!) [Check the official docs](https://cabal.readthedocs.io/en/3.6/installing-packages.html) if you don't know where is your cabal's global configuration file 
+
+```
+documentation: True
+haddock
+  hoogle: True
+  html: True
+```
+
+Or, run this command in bash:
+```
+cabal user-config update -a "documentation: True
+haddock
+  hoogle: True
+  html: True"
+```
+
+#### Build Project with Cabal
+
+Then, run `cabal build all` on your project, so that cabal will build haddock and
+hoogle files for your local packages and dependencies.
+
+> NOTE: Adding `--enable-documentation --haddock-hoogle --haddock-html` to your build command won't work for dependencies as I tried.
+
+### Generate
+```
+cabal-hoogle generate
+```
+
+This generates the hoogle database. You will see logs very similar to `stack hoogle --rebuild`
+
+### Run Hoogle
+
+With `cabal-hoogle run --`, extra arguments are passed directly to `hoogle`.
+
+You may start a server like this:
+```
+cabal-hoogle run -- server --local --port 9000
+```
+
+Or directly search in command line like this:
+```
+cabal-hoogle run -- search catMaybes
+```
+
+### More Options
+
+Run `cabal-hoogle --help` to see more options. For example if you are using a non-default build dir (not `dist-newstyle`), or you have multiple GHC verisons on the same project.
+
+Below is a complex example:
+```
+cabal-hoogle --compiler ghc-9.2.3 --platform "x86_64-linux" --builddir mydist generate
+```
+
+## FAQ
+
+### Many packages missing documentation
+
+If you see output like this when running `cabal-hoogle generate`
+```
+Packages missing documentation: ansi-terminal ansi-wl-pprint async base-orphans base16-bytestring base64-bytestring Cabal cabal-hoogle clock colour extra hashable haskell-src-exts haskell-src-meta optparse-applicative primitive regex-base regex-tdfa safe split string-interpolate syb text-conversions th-abstraction th-compat th-expand-syns th-lift th-lift-instances th-orphans th-reify-many transformers-compat typed-process unliftio-core utf8-string vector vector-stream
+Found 27 warnings when processing items
+```
+
+Probably you haven't built dependencies with hoogle enabled. Check [prerequisites](#prerequisite) more carefully.
+
+### Cabal version mismatch
+
+Make sure to build `cabal-hoogle` with the exact same `Cabal` library as your `cabal-install`. You can use `cabal --version`
+to check the version of `Cabal` library. See [Installation](#installation).
+
+### Core libraries missing documentation
+
+If you are on macOS or Windows, and using `ghcup`, your `ghc` probably doesn't come with docs.
+See [this](https://gitlab.haskell.org/ghc/ghc/-/issues/20903) and [this](https://github.com/haskell/haskell-language-server/issues/208#issuecomment-1162169087) for details of the upstream issue.
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,3 @@
+module Main(main) where
+
+import           Hoogle.Cabal (main)
diff --git a/cabal-hoogle.cabal b/cabal-hoogle.cabal
new file mode 100644
--- /dev/null
+++ b/cabal-hoogle.cabal
@@ -0,0 +1,66 @@
+cabal-version:      3.0
+name:               cabal-hoogle
+version:            0.1.0.0
+synopsis:           Just like "stack hoogle", but for cabal projects
+description:        This tool
+bug-reports:        https://github.com/kokobd/cabal-hoogle/issues
+license:            BSD-3-Clause
+author:             kokobd <contact@zelinf.net>
+maintainer:         kokobd <contact@zelinf.net>
+stability:          experimental
+copyright:          (c) 2022 kokobd
+category:           Development
+build-type:         Simple
+extra-source-files:
+    CHANGELOG.md
+    README.md
+
+library
+    build-depends:
+        base >= 4.14 && < 5
+      , Cabal >= 3.2.0.0 && < 4
+      , containers ^>= 0.6.2.1
+      , directory ^>= 1.3.6
+      , filepath ^>= 1.4.2
+      , optparse-applicative ^>= 0.17.0
+      , regex-tdfa ^>= 1.3.1
+      , string-interpolate ^>= 0.3.1.2
+      , text ^>= 1.2.4
+      , transformers ^>= 0.5.6
+      , typed-process ^>= 0.2.10
+      , extra ^>= 1.7.10
+    default-language: Haskell2010
+    ghc-options: -Wall
+    hs-source-dirs: src
+    default-extensions:
+        OverloadedStrings
+        QuasiQuotes
+    exposed-modules:
+        Hoogle.Cabal
+        Hoogle.Cabal.CmdOptions
+
+executable cabal-hoogle
+    main-is: Main.hs
+    hs-source-dirs: app
+    default-language: Haskell2010
+    ghc-options: -threaded -rtsopts
+    build-depends:
+        base
+      , cabal-hoogle
+
+test-suite tests
+  type:             exitcode-stdio-1.0
+  default-language: Haskell2010
+  hs-source-dirs:   test
+  main-is:          Main.hs
+  other-modules:
+    IntegrationTest
+  build-depends:
+      base
+    , cabal-hoogle
+    , tasty ^>= 1.4.2.3
+    , tasty-hunit ^>= 0.10.0.3
+    , silently ^>= 1.2.5.2
+  build-tool-depends:
+    , tasty-discover:tasty-discover
+    , hoogle:hoogle
diff --git a/src/Hoogle/Cabal.hs b/src/Hoogle/Cabal.hs
new file mode 100644
--- /dev/null
+++ b/src/Hoogle/Cabal.hs
@@ -0,0 +1,131 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TupleSections #-}
+
+module Hoogle.Cabal (main) where
+
+import Control.Exception (catch, throw)
+import Control.Monad (unless)
+import Data.Bifunctor (Bifunctor (second))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.List.NonEmpty.Extra as NonEmpty
+import qualified Data.Map.Strict as Map
+import Data.Maybe (catMaybes)
+import Data.String.Interpolate (i)
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+import Data.Traversable (forM)
+import Distribution.InstalledPackageInfo (InstalledPackageInfo (haddockHTMLs, installedUnitId))
+import Distribution.Simple.Configure (getPersistBuildConfig)
+import Distribution.Simple.PackageIndex (allPackagesByName)
+import Distribution.Types.LocalBuildInfo (LocalBuildInfo, installedPkgs, localPkgDescr)
+import Distribution.Types.PackageDescription (PackageDescription (package))
+import Distribution.Types.PackageId (pkgName)
+import Distribution.Types.PackageName (PackageName, unPackageName)
+import Hoogle.Cabal.CmdOptions
+import System.Directory (createDirectoryIfMissing, createDirectoryLink, listDirectory, makeAbsolute, removeDirectoryLink, removeDirectoryRecursive, withCurrentDirectory)
+import System.Exit (exitFailure)
+import System.FilePath
+import System.IO (stderr)
+import System.IO.Error (isDoesNotExistError)
+import System.Process.Typed
+import Text.Regex.TDFA ((=~))
+import Prelude hiding (log)
+
+main :: IO ()
+main = do
+  CmdOptions {..} <- readCmdOptions
+  localPackagesDir <- findLocalPackagesBuildDir cmdOptions_compiler cmdOptions_platform cmdOptions_builddir
+  let hoogleDir = localPackagesDir </> ".hoogle"
+      hoogleLocalPackagesDir = hoogleDir </> "local"
+      hoogleDependenciesDir = hoogleDir </> "dependencies"
+
+  case cmdOptions_command of
+    CommandGenerate -> do
+      catch (removeDirectoryRecursive hoogleDir) $ \(err :: IOError) ->
+        if isDoesNotExistError err then pure () else throw err
+      createDirectoryIfMissing True hoogleLocalPackagesDir
+      createDirectoryIfMissing True hoogleDependenciesDir
+      localPackages <- symlinkLocalPackages localPackagesDir hoogleLocalPackagesDir
+      let localPkgsName = fmap (pkgName . package . localPkgDescr) localPackages
+      dependenciesName <- symlinkDependencies localPackages hoogleDependenciesDir
+      let nameStrs = fmap unPackageName (localPkgsName <> dependenciesName)
+      withCurrentDirectory hoogleDir $ do
+        runProcess_ . proc "hoogle" $
+          ["generate", databaseArg, "--local=local", "--local=dependencies"] ++ nameStrs
+    CommandRun hoogleArgs -> do
+      let hoogleArgs' = case hoogleArgs of
+            (x : xs) -> x : databaseArg : xs
+            [] -> [databaseArg]
+      withCurrentDirectory hoogleDir . runProcess_ . proc "hoogle" $
+        hoogleArgs'
+  where
+    databaseArg = "--database=all.hoo"
+
+findLocalPackagesBuildDir ::
+  -- | compiler
+  Maybe Text ->
+  -- | platform
+  Maybe Text ->
+  -- | cabal project build dir (often dist-newstyle)
+  FilePath ->
+  IO FilePath
+findLocalPackagesBuildDir compiler platform projectBuildDir = do
+  -- directory layout: dist-newstyle/build/x86_64-linux/ghc-9.2.3/$PACKAGE_NAME
+  projectBuildDir' <- makeAbsolute projectBuildDir
+  platformDir <- enterSubDir (projectBuildDir' </> "build") "platform" (fmap T.unpack platform)
+  enterSubDir platformDir "compiler" (fmap T.unpack compiler)
+
+exitOnError :: Text -> IO a
+exitOnError log = do
+  T.hPutStrLn stderr log
+  exitFailure
+
+enterSubDir :: FilePath -> Text -> Maybe FilePath -> IO FilePath
+enterSubDir baseDir realm givenSubDir = do
+  subDirs <- listDirectory baseDir
+  case givenSubDir >>= (\d -> if d `notElem` subDirs then Just d else Nothing) of
+    Just wrongSubDir -> exitOnError [i|specified #{realm} #{wrongSubDir} doesn't not exist, make sure to build first|]
+    Nothing -> case selectSubDir subDirs givenSubDir of
+      Just subDir -> pure $ baseDir </> subDir
+      _ -> exitOnError [i|failed to guess #{realm}, please specify one|]
+  where
+    selectSubDir :: [FilePath] -> Maybe FilePath -> Maybe FilePath
+    selectSubDir [exactlyOne] _ = Just exactlyOne
+    selectSubDir _ given = given
+
+symlinkLocalPackages :: FilePath -> FilePath -> IO [LocalBuildInfo]
+symlinkLocalPackages localPackagesDir destDir = do
+  localPackages <- filter (=~ packageNameRegex) <$> listDirectory localPackagesDir
+  forM localPackages $ \pkg -> do
+    catch (removeDirectoryLink (destDir </> pkg)) $ \(e :: IOError) ->
+      if isDoesNotExistError e then pure () else throw e
+    createDirectoryLink (localPackagesDir </> pkg) (destDir </> pkg)
+    getPersistBuildConfig (localPackagesDir </> pkg)
+  where
+    packageNameRegex :: String =
+      "[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*-[0-9]+([.][0-9]+)*"
+
+symlinkDependencies :: [LocalBuildInfo] -> FilePath -> IO [PackageName]
+symlinkDependencies localPackages hoogleDependenciesDir = do
+  let nameToPkgs =
+        fmap (NonEmpty.nubOrdOn installedUnitId) . Map.fromListWith (<>) $
+          concatMap collectDependenciesForPkg localPackages
+  pkgs <- fmap catMaybes . forM (Map.toList nameToPkgs) $ \(name, allPkgs@(pkg NonEmpty.:| pkgs)) -> do
+    unless (null pkgs) $
+      T.putStrLn [i|Warning: package #{name} has more than 1 version installed, this should not happen. all pkgs: #{fmap installedUnitId allPkgs}|]
+    case haddockHTMLs pkg of
+      [htmlDir] -> pure $ Just (name, htmlDir)
+      htmlDirs -> do
+        T.putStrLn [i|Warning: package #{name} doesn't have exactly one haddock html directory, actual: #{htmlDirs}|]
+        pure Nothing
+  forM pkgs $ \(name, dir) -> do
+    createDirectoryLink dir (hoogleDependenciesDir </> unPackageName name)
+    pure name
+  where
+    collectDependenciesForPkg pkg =
+      let depsWithName = allPackagesByName (installedPkgs pkg)
+       in fmap (second (NonEmpty.:| []))
+            . concatMap (\(name, pkgs) -> fmap (name,) pkgs)
+            $ depsWithName
diff --git a/src/Hoogle/Cabal/CmdOptions.hs b/src/Hoogle/Cabal/CmdOptions.hs
new file mode 100644
--- /dev/null
+++ b/src/Hoogle/Cabal/CmdOptions.hs
@@ -0,0 +1,59 @@
+module Hoogle.Cabal.CmdOptions
+  ( CmdOptions (..),
+    readCmdOptions,
+    Command (..),
+  )
+where
+
+import Data.Text (Text)
+import Options.Applicative
+
+data CmdOptions = CmdOptions
+  { cmdOptions_compiler :: Maybe Text,
+    cmdOptions_platform :: Maybe Text,
+    cmdOptions_builddir :: FilePath,
+    cmdOptions_command :: Command
+  }
+  deriving (Show, Eq)
+
+data Command
+  = CommandGenerate
+  | CommandRun [String]
+  deriving (Show, Eq)
+
+parser :: Parser CmdOptions
+parser =
+  CmdOptions
+    <$> (optional . strOption)
+      ( long "compiler"
+          <> help "compiler name and version, for example 'ghc-9.2.3'"
+      )
+    <*> (optional . strOption)
+      ( long "platform"
+          <> help "platform, for example x86_64-linux"
+      )
+    <*> strOption
+      ( long "builddir"
+          <> value "dist-newstyle"
+          <> help "cabal project build dir"
+      )
+    <*> hsubparser
+      ( command "generate" (info (pure CommandGenerate) (progDesc "generate hoogle database"))
+          <> command "run" (info commandRunParser (progDesc "run hoogle, with arbitrary arguments"))
+      )
+
+commandRunParser :: Parser Command
+commandRunParser = CommandRun <$> (many . strArgument) (metavar "ARGS")
+
+readCmdOptions :: IO CmdOptions
+readCmdOptions = execParser parserInfo
+  where
+    parserInfo =
+      info
+        (parser <**> helper)
+        ( fullDesc
+            <> progDesc
+              ( "Run hoogle on your local packages and dependencies. "
+                  <> "See https://github.com/kokobd/cabal-hoogle for more information"
+              )
+        )
diff --git a/test/IntegrationTest.hs b/test/IntegrationTest.hs
new file mode 100644
--- /dev/null
+++ b/test/IntegrationTest.hs
@@ -0,0 +1,24 @@
+module IntegrationTest where
+
+import Data.List (isInfixOf)
+import Hoogle.Cabal
+import System.Environment (withArgs)
+import System.IO.Silently (capture_)
+import Test.Tasty
+import Test.Tasty.HUnit
+
+test_searchInCommandLine :: IO [TestTree]
+test_searchInCommandLine = do
+  withArgs ["generate"] main
+
+  let testCase' :: String -> [String] -> String -> TestTree
+      testCase' name args wantResult =
+        testCase name $ do
+          output <- withArgs (["run", "--"] ++ args) (capture_ main)
+          wantResult `isInfixOf` output @? "actual output: " <> output
+
+  pure
+    [ testCase' "test dependency doesn't work" ["testCase"] "No results found",
+      testCase' "core library" ["lift"] "Control.Monad.Trans.Class lift",
+      testCase' "non-core library" ["runProcess_"] "System.Process.Typed runProcess_"
+    ]
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF tasty-discover -optF --tree-display -j1 #-}
