diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
 # Changelog
 
+## 0.2.1 (2022-02-17)
+- new commands: 'project', 'issue', 'userinfo'
+- drop --url and use aeson-pretty
+- use new pagure library
+- 'user': new '--fork" option
+- support aeson-2.0 and drop lens dependency
+- 'branches' and 'username' now error if entity does not exist
+- errors for non-existent API paths (eg unknown repo or user)
+
 ## 0.2 (2020-04-06)
 - new commands: 'branches', 'issues', 'users', 'username', 'groups', 'git-url'
 - 'list': new options --namespace, --username, --include-forks, --only-forks
diff --git a/Main.hs b/Main.hs
deleted file mode 100644
--- a/Main.hs
+++ /dev/null
@@ -1,277 +0,0 @@
-{-# LANGUAGE CPP #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-module Main (main) where
-
-import Control.Applicative (
-#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,8,0))
-#else
-                            (<$>), (<*>),
-#endif
-#if (defined(MIN_VERSION_simple_cmd_args) && MIN_VERSION_simple_cmd_args(0,1,3))
-#else
-                            (<|>)
-#endif
-                           )
-import Control.Monad (unless, when)
-import Data.Aeson (eitherDecode)
-import Data.Aeson.Types
-#if (defined(MIN_VERSION_http_conduit) && MIN_VERSION_http_conduit(2,3,1))
-#else
-import Data.ByteString (ByteString)
-#endif
-#if (defined(VERSION_lens_aeson))
-import Control.Lens
-import Data.Aeson.Lens
-#else
-import Lens.Micro
-import Lens.Micro.Aeson
-#endif
-import qualified Data.ByteString.Char8 as B
-import qualified Data.ByteString.Lazy.Char8 as BL
-import Data.Maybe
-#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,11,0))
-#else
-import Data.Semigroup ((<>))
-#endif
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Text.IO as T
-import Network.HTTP.Conduit (queryString)
-import Network.HTTP.Simple
-import SimpleCmdArgs
-import System.FilePath ((</>))
-
-import Paths_pagure_cli (version)
-
-main :: IO ()
-main =
-  simpleCmdArgs (Just version) "Pagure client" "Simple pagure CLI" $
-  subcommands
-  [ Subcommand "list" "list projects" $
-    listProjects <$> serverOpt <*> countOpt <*> urlOpt <*> jsonOpt <*> forksOpt <*> optional namespaceOpt <*> optional packagerOpt <*> optional (strArg "PATTERN")
-  , Subcommand "user" "user repos" $
-    userRepos <$> serverOpt <*> countOpt <*> urlOpt <*> jsonOpt <*> strArg "USER"
-  , Subcommand "branches" "list project branches" $
-    repoBranches <$> serverOpt <*> urlOpt <*> jsonOpt <*> strArg "REPO"
-  , Subcommand "issues" "list project issues" $
-    projectIssues <$> serverOpt <*> countOpt <*> urlOpt <*> jsonOpt <*> strArg "REPO" <*> switchWith 'A' "all" "list Open and Closed issues" <*> optional (strOptionWith 'a' "author" "AUTHOR" "Filter issues by creator") <*> optional (strOptionWith 'S' "since" "Y-M-D" "Filter issues updated after date") <*> optional (strOptionWith 't' "title" "pattern" "Filter issues by title")
-  , Subcommand "users" "list users" $
-    users <$> serverOpt <*> urlOpt <*> jsonOpt <*> strArg "PATTERN"
-  , Subcommand "username" "fullname of user" $
-    username <$> serverOpt <*> urlOpt <*> jsonOpt <*> strArg "USERNAME"
-  , Subcommand "groups" "list groups" $
-    groups <$> serverOpt <*> countOpt <*> urlOpt <*> jsonOpt <*> optional (strArg "PATTERN")
-  , Subcommand "git-url" "show project repo's git urls" $
-    gitUrl <$> serverOpt <*> urlOpt <*> jsonOpt <*> strArg "REPO"
-  ]
-  where
-    countOpt = switchWith 'c' "count" "Show number only"
-    jsonOpt = switchWith 'j' "json" "Print raw json response"
-    -- FIXME should this be no-op?
-    urlOpt = switchWith 'U' "url" "Print API url"
-    namespaceOpt = strOptionWith 'n' "namespace" "NAMESPACE" "Specify project repo namespace"
-    packagerOpt = Owner <$> ownerOpt <|> Committer <$> usernameOpt
-    usernameOpt = strOptionWith 'u' "username" "USERNAME" "Projects to which username can commit"
-    ownerOpt = strOptionWith 'o' "owner" "OWNER" "Projects with certain owner"
-    serverOpt = strOptionalWith 's' "server" "SERVER" "Pagure server" srcFedoraprojectOrg
-    forksOpt = flagWith' OnlyForks 'F' "only-forks" "Only list forks" <|>
-               flagWith NoForks IncludeForks 'f' "include-forks" "Include forks [default: ignore forks]"
-
-data Packager = Owner String | Committer String
-
-data Forks = NoForks | IncludeForks | OnlyForks
-
-srcFedoraprojectOrg :: String
-srcFedoraprojectOrg = "src.fedoraproject.org"
-
-#if (defined(MIN_VERSION_http_conduit) && MIN_VERSION_http_conduit(2,3,1))
-#else
-type Query = [(ByteString, Maybe ByteString)]
-#endif
-
-listProjects :: String -> Bool -> Bool -> Bool -> Forks -> Maybe String -> Maybe Packager -> Maybe String -> IO ()
-listProjects server count showurl json forks mnamespace mpackager mpattern = do
-  unless (count || isJust mpackager || isJust mpattern) $
-    error' "Please give a package pattern, --count, or --owner/--username"
-  let path = "projects"
-      params = makeKey "short" "1" ++ fork ++ packager ++ maybeKey "namespace" mnamespace ++ maybeKey "pattern" mpattern
-  pages <- queryPaged server count showurl json path params ("pagination", "page")
-  unless json $
-    mapM_ printPage pages
-  where
-    -- (!orphan only works on pagure >=0.29)
-    packager = case mpackager of
-      Nothing -> boolKey "owner" (server == srcFedoraprojectOrg) "!orphan"
-      Just (Owner o) -> makeKey "owner" o
-      Just (Committer c) -> makeKey "username" c
-
-    fork = case forks of
-      NoForks -> makeKey "fork" "0"
-      IncludeForks -> []
-      OnlyForks -> makeKey "fork" "1"
-
-    printPage :: Value -> IO ()
-    printPage result =
-      let key' = if isJust mnamespace then "name" else "fullname" in
-      mapM_ T.putStrLn $ result ^.. key "projects" . values . key (T.pack key') . _String
-
-userRepos :: String -> Bool -> Bool -> Bool -> String -> IO ()
-userRepos server count showurl json user = do
-  let path = "user" </> user
-  pages <- queryPaged server count showurl json path [] ("repos_pagination", "repopage")
-  unless json $
-    mapM_ printPage pages
-  where
-    printPage :: Value -> IO ()
-    printPage result = do
-      let repos = result ^.. key (T.pack "repos") . values . _Object
-      mapM_ printRepo repos
-
-    printRepo :: Object -> IO ()
-    printRepo repo = do
-      let mfields = parseRepo repo
-      case mfields of
-        Nothing -> error' "parsing repo failed"
-        Just (mnamespace,name) ->
-          T.putStrLn $ maybe "" (<> "/") mnamespace  <> name
-
-    parseRepo :: Object -> Maybe (Maybe Text, Text)
-    parseRepo =
-      parseMaybe $ \obj -> do
-        namespace <- obj .:? "namespace"
-        name <- obj .: "name"
-        return (namespace,name)
-
-maybeKey :: String -> Maybe String -> Query
-maybeKey _ Nothing = []
-maybeKey k mval = [(B.pack k, fmap B.pack mval)]
-
-makeKey :: String -> String -> Query
-makeKey k val = [(B.pack k, Just (B.pack val))]
-
-boolKey :: String -> Bool -> String -> Query
-boolKey _ False _ = []
-boolKey k True val = makeKey k val
-
--- FIXME limit max number of issues
-projectIssues :: String -> Bool -> Bool -> Bool -> String -> Bool -> Maybe String -> Maybe String -> Maybe String -> IO ()
-projectIssues server count showurl json repo allstatus mauthor msince mpat = do
-  let path = repo </> "issues"
-      params = [("status", Just "all") | allstatus] ++
-               maybeKey "author" mauthor ++ maybeKey "since" msince
-  results <- queryPaged server count showurl json path params ("pagination", "page")
-  unless json $
-    mapM_ printIssues results
-  where
-    printIssues :: Value -> IO ()
-    printIssues result = do
-      let issues = result ^.. key (T.pack "issues") . values . _Object
-      mapM_ printIssue issues
-
-    printIssue :: Object -> IO ()
-    printIssue issue = do
-      let mfields = parseIssue issue
-      case mfields of
-        Nothing -> putStrLn "parsing issue failed"
-        Just (id',title,status) ->
-          when (isNothing mpat || T.pack (fromJust mpat) `T.isInfixOf` title) $
-          putStrLn $ "https://" <> server </> repo </> "issue" </> show id' <> " (" <> T.unpack status <> "): " <> T.unpack title
-
-    parseIssue :: Object -> Maybe (Integer, Text, Text)
-    parseIssue =
-      parseMaybe $ \obj -> do
-        id' <- obj .: "id"
-        title <- obj .: "title"
-        status <- obj .: "status"
-        return (id',title,status)
-
--- FIXME limit max number of pages (10?) or --pages
-queryPaged :: String -> Bool -> Bool -> Bool -> String -> Query -> (String,String) -> IO [Value]
-queryPaged server count showurl json path params (pagination,paging) = do
-  res1 <- pagureQuery showurl server json path (params ++ makeKey "per_page" (if count then "1" else "100"))
-  let mpages = res1 ^? key (T.pack pagination) . key "pages" . _Integer
-  if count
-    then do
-    print $ fromMaybe (error' "pages not found") mpages
-    return []
-    else do
-    rest <- mapM nextPage [2..(fromMaybe 0 mpages)]
-    return $ res1 : rest
-      where
-        nextPage p =
-          pagureQuery False server json path (params ++ makeKey "per_page" "100" ++ makeKey paging (show p))
-
-pagureQuery :: Bool -> String -> Bool -> String -> Query -> IO Value
-pagureQuery showurl server json path params = do
-  let url = "https://" <> server </> "api/0" </> path
-  req <- setRequestQueryString params <$> parseRequest url
-  when showurl $ putStrLn $ url ++ B.unpack (queryString req)
-  if json then do
-    res <- getResponseBody <$> httpLBS req
-    BL.putStrLn res
-    case eitherDecode res of
-      Left e -> error' e
-      Right v -> return v
-    else getResponseBody <$> httpJSON req
-
-repoBranches :: String -> Bool -> Bool -> String -> IO ()
-repoBranches server showurl json repo = do
-  let namespace =
-        if server == srcFedoraprojectOrg && '/' `notElem` repo
-        then "rpms/" else ""
-      path = namespace ++ repo </> "git/branches"
-  res <- pagureQuery showurl server json path []
-  unless json $
-    printKeyList "branches" res
-
-users :: String -> Bool -> Bool -> String -> IO ()
-users server showurl json pat = do
-  let path = "users"
-      params = makeKey "pattern" pat
-  res <- pagureQuery showurl server json path params
-  unless json $
-    printKeyList "users" res
-
-username :: String -> Bool -> Bool -> String -> IO ()
-username server showurl json user = do
-  let path = "user" </> user
-  res <- pagureQuery showurl server json path $ makeKey "per_page" "1"
-  unless json $
-    case res ^? key "user" . key "fullname" . _String of
-      Nothing -> error' "User fullname not found"
-      Just fn -> T.putStrLn fn
-
-groups :: String -> Bool -> Bool -> Bool -> Maybe String -> IO ()
-groups server count showurl json mpat = do
-  let path = "groups"
-      params = maybeKey "pattern" mpat
-  results <- queryPaged server count showurl json path params ("pagination", "page")
-  unless json $
-    mapM_ (printKeyList "groups") results
-
-printKeyList :: String -> Value -> IO ()
-printKeyList key' res =
-  mapM_ T.putStrLn $ res ^.. key (T.pack key') . values . _String
-
-gitUrl :: String -> Bool -> Bool -> String -> IO ()
-gitUrl server showurl json repo = do
-  let namespace =
-        if server == srcFedoraprojectOrg && '/' `notElem` repo
-        then "rpms/" else ""
-      path = namespace ++ repo </> "git/urls"
-  res <- pagureQuery showurl server json path []
-  unless json $
-    printURLs res
-  where
-    printURLs :: Value -> IO ()
-    printURLs result =
-      mapM_ T.putStrLn $ result ^.. key (T.pack "urls") . members . _String
-
--- from simple-cmd
-error' :: String -> a
-#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,9,0))
-error' = errorWithoutStackTrace
-#else
-error' = error
-#endif
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -4,8 +4,6 @@
 [![GPL-2 license](https://img.shields.io/badge/license-GPL--2-blue.svg)](LICENSE)
 [![Stackage Lts](http://stackage.org/package/pagure-cli/badge/lts)](http://stackage.org/lts/package/pagure-cli)
 [![Stackage Nightly](http://stackage.org/package/pagure-cli/badge/nightly)](http://stackage.org/nightly/package/pagure-cli)
-[![Build status](https://secure.travis-ci.org/juhp/pagure-cli.svg)](https://travis-ci.org/juhp/pagure-cli)
-[![Copr build](https://copr.fedorainfracloud.org/coprs/petersen/pagure-cli/package/pagure-cli/status_image/last_build.png)](https://copr.fedorainfracloud.org/coprs/petersen/pagure-cli/)
 
 A [pagure](https://docs.pagure.org/pagure/) client for querying
 projects and users.
@@ -59,7 +57,7 @@
 
 ```
 $ pagure list --help
-Usage: pagure list [-s|--server SERVER] [-c|--count] [-U|--url] [-j|--json]
+Usage: pagure list [-s|--server SERVER] [-c|--count] [(-j|--json) | (-y|--yaml)]
                    [(-F|--only-forks) | (-f|--include-forks)]
                    [-n|--namespace NAMESPACE]
                    [(-o|--owner OWNER) | (-u|--username USERNAME)] [PATTERN]
@@ -68,8 +66,8 @@
 Available options:
   -s,--server SERVER       Pagure server
   -c,--count               Show number only
-  -U,--url                 Print API url
-  -j,--json                Print raw json response
+  -j,--json                Output JSON
+  -y,--yaml                YAML output
   -F,--only-forks          Only list forks
   -f,--include-forks       Include forks [default: ignore forks]
   -n,--namespace NAMESPACE Specify project repo namespace
@@ -88,8 +86,7 @@
 
 ## Binaries
 
-If you are using Fedora you can install the package from my
-[copr repo](https://copr.fedorainfracloud.org/coprs/petersen/pagure-cli/).
+pagure-cli is packaged in Fedora (since F34).
 
 ## Contributions
 
@@ -100,3 +97,5 @@
 ## Other clients
 After writing the initial version I discovered that
 Ricky Elrod (relrod) had made <https://github.com/fedora-infra/pagure-cli>.
+
+I discovered a client in Rust <https://pagure.io/ironthree/bodhi-cli> by decathorpe.
diff --git a/Setup.hs b/Setup.hs
deleted file mode 100644
--- a/Setup.hs
+++ /dev/null
@@ -1,2 +0,0 @@
-import Distribution.Simple
-main = defaultMain
diff --git a/pagure-cli.cabal b/pagure-cli.cabal
--- a/pagure-cli.cabal
+++ b/pagure-cli.cabal
@@ -1,52 +1,44 @@
-cabal-version:       2.0
+cabal-version:       1.18
 name:                pagure-cli
-version:             0.2
+version:             0.2.1
 synopsis:            Pagure client
-description:         A commandline Pagure client for querying projects and users.
+description:         A command-line Pagure client for querying projects and users.
 homepage:            https://github.com/juhp/pagure-cli
 bug-reports:         https://github.com/juhp/pagure-cli/issues
 license:             GPL-2
 license-file:        LICENSE
 author:              Jens Petersen <juhpetersen@gmail.com>
 maintainer:          Jens Petersen <juhpetersen@gmail.com>
-copyright:           2019 Jens Petersen
+copyright:           2019-2020,2022 Jens Petersen
 category:            Utility
 build-type:          Simple
 extra-doc-files:     README.md
                    , CHANGELOG.md
 tested-with:         GHC == 8.0.2, GHC == 8.2.2, GHC == 8.4.4, GHC == 8.6.5,
-                     GHC == 8.8.3
+                     GHC == 8.8.4, GHC == 8.10.4, GHC == 9.0.2
 
 source-repository head
   type:                git
   location:            https://github.com/juhp/pagure-cli.git
 
-
-flag microlens
-  description:       Use smaller microlens instead of lens
-  default:           False
-
 executable pagure
-  main-is:             Main.hs
+  main-is:             src/Main.hs
 
   build-depends:       aeson
+                     , aeson-pretty
                      , base < 5
                      , bytestring
                      , filepath
-                     , http-conduit
                      , optparse-applicative
+                     , pagure >= 0.1.1
                      , simple-cmd-args >= 0.1.6
                      , text
-  if flag(microlens)
-      build-depends:   microlens, microlens-aeson
-      cpp-options:   -DMICROLENS
-  else
-      build-depends:   lens, lens-aeson
+                     , unordered-containers
+                     , yaml
 
   if impl(ghc<8.0)
       build-depends: semigroups
 
-  autogen-modules:     Paths_pagure_cli
   other-modules:       Paths_pagure_cli
 
   ghc-options:         -Wall
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,274 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main (main) where
+
+import Control.Applicative (
+#if !MIN_VERSION_base(4,8,0)
+                            (<$>), (<*>),
+#endif
+#if !MIN_VERSION_simple_cmd_args(0,1,3)
+                            (<|>)
+#endif
+                           )
+import Control.Monad (unless, when)
+import Data.Aeson.Encode.Pretty (encodePretty)
+import Data.Aeson.Types
+#if MIN_VERSION_aeson(2,0,0)
+import qualified Data.Aeson.KeyMap as M
+#else
+import qualified Data.HashMap.Lazy as M
+#endif
+import qualified Data.ByteString.Char8 as B
+import qualified Data.ByteString.Lazy.Char8 as BL
+import Data.Maybe
+#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,11,0))
+#else
+import Data.Semigroup ((<>))
+#endif
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+import Data.Yaml (encode)
+import SimpleCmdArgs
+import System.FilePath ((</>))
+import Fedora.Pagure
+
+import Paths_pagure_cli (version)
+
+main :: IO ()
+main =
+  simpleCmdArgs (Just version) "Pagure client" "Simple pagure CLI" $
+  subcommands
+  [ Subcommand "list" "list projects" $
+    listProjects <$> serverOpt <*> countOpt <*> formatOpt <*> forksOpt <*> optional namespaceOpt <*> optional packagerOpt <*> optional (strArg "PATTERN")
+  , Subcommand "user" "user repos" $
+    userRepos <$> serverOpt <*> countOpt <*> switchWith 'f' "forks" "List user's forks" <*> strArg "USER"
+  , Subcommand "branches" "list project branches" $
+    repoBranches <$> serverOpt <*> formatOpt <*> strArg "REPO"
+  , Subcommand "issues" "list project issues" $
+    projectIssues <$> serverOpt <*> countOpt <*> formatOpt <*> strArg "REPO" <*> switchWith 'A' "all" "list Open and Closed issues" <*> optional (strOptionWith 'a' "author" "AUTHOR" "Filter issues by creator") <*> optional (strOptionWith 'S' "since" "Y-M-D" "Filter issues updated after date") <*> optional (strOptionWith 't' "title" "pattern" "Filter issues by title")
+  , Subcommand "users" "list users" $
+    users <$> serverOpt <*> formatOpt <*> strArg "PATTERN"
+  , Subcommand "username" "fullname of user" $
+    username <$> serverOpt <*> formatOpt <*> strArg "USERNAME"
+  , Subcommand "groups" "list groups" $
+    groups <$> serverOpt <*> countOpt <*> formatOpt <*> optional (strArg "PATTERN")
+  , Subcommand "git-url" "show project repo's git urls" $
+    gitUrl <$> serverOpt <*> formatOpt <*> strArg "REPO"
+  , Subcommand "project" "show project details" $
+    projectInfo <$> serverOpt <*> formatOpt <*> strArg "PROJECT"
+  , Subcommand "issue" "show project issue" $
+    projectIssue <$> serverOpt <*> formatOpt <*> strArg "REPO" <*> argumentWith auto "ISSUE"
+  , Subcommand "userinfo" "show user details" $
+    userInfo <$> serverOpt <*> formatOpt <*> strArg "USERNAME"
+  ]
+  where
+    countOpt = switchWith 'c' "count" "Show number only"
+    formatOpt = flagWith' FormatJson 'j' "json" "Output JSON" <|> flagWith FormatDefault FormatYaml 'y' "yaml" "YAML output"
+    namespaceOpt = strOptionWith 'n' "namespace" "NAMESPACE" "Specify project repo namespace"
+    packagerOpt = Owner <$> ownerOpt <|> Committer <$> usernameOpt
+    usernameOpt = strOptionWith 'u' "username" "USERNAME" "Projects to which username can commit"
+    ownerOpt = strOptionWith 'o' "owner" "OWNER" "Projects with certain owner"
+    serverOpt = strOptionalWith 's' "server" "SERVER" "Pagure server" srcFedoraprojectOrg
+    forksOpt = flagWith' OnlyForks 'F' "only-forks" "Only list forks" <|>
+               flagWith NoForks IncludeForks 'f' "include-forks" "Include forks [default: ignore forks]"
+
+data OutputFormat = FormatDefault | FormatJson | FormatYaml
+
+data Packager = Owner String | Committer String
+
+data Forks = NoForks | IncludeForks | OnlyForks
+
+srcFedoraprojectOrg :: String
+srcFedoraprojectOrg = "src.fedoraproject.org"
+
+defaultPrinter :: OutputFormat -> (Object -> IO ()) -> Object -> IO ()
+defaultPrinter FormatDefault pr = pr
+defaultPrinter FormatJson _ = BL.putStrLn . encodePretty
+defaultPrinter FormatYaml _ = B.putStrLn . encode
+
+listProjects :: String -> Bool -> OutputFormat -> Forks -> Maybe String -> Maybe Packager -> Maybe String -> IO ()
+listProjects server count format forks mnamespace mpackager mpattern = do
+  unless (count || isJust mpackager || isJust mpattern) $
+    error' "Please give a package pattern, --count, or --owner/--username"
+  let path = "projects"
+      params = makeKey "short" "1" ++ fork ++ packager ++ maybeKey "namespace" mnamespace ++ maybeKey "pattern" mpattern
+  pages <- queryPaged server count path params ("pagination", "page")
+  mapM_ (defaultPrinter format printPage) pages
+  where
+    -- (!orphan only works on pagure >=0.29)
+    packager = case mpackager of
+      Nothing -> boolKey "owner" (server == srcFedoraprojectOrg) "!orphan"
+      Just (Owner o) -> makeKey "owner" o
+      Just (Committer c) -> makeKey "username" c
+
+    fork = case forks of
+      NoForks -> makeKey "fork" "0"
+      IncludeForks -> []
+      OnlyForks -> makeKey "fork" "1"
+
+    printPage :: Object -> IO ()
+    printPage result = do
+      let key' = if isJust mnamespace then "name" else "fullname"
+          projects = lookupKey' "projects" result
+      (mapM_ T.putStrLn . mapMaybe (lookupKey key')) projects
+
+userRepos :: String -> Bool -> Bool -> String -> IO ()
+userRepos server count forks user =
+  if count then do
+    let path = "user" </> user
+    mcnt <- queryPagureCount server path [] $
+            if forks then "forks_pagination" else "repos_pagination"
+    print $
+      fromMaybe
+      (error' ("number of " ++ (if forks then "forks" else "repos") ++ " could not be determined"))
+      mcnt
+    else do
+    repos <- (if forks then pagureUserForks else pagureUserRepos) server user
+    mapM_ T.putStrLn repos
+
+-- maybeKey :: String -> Maybe String -> Query
+-- maybeKey _ Nothing = []
+-- maybeKey k mval = [(B.pack k, fmap B.pack mval)]
+
+-- makeKey :: String -> String -> Query
+-- makeKey k val = [(B.pack k, Just (B.pack val))]
+
+boolKey :: String -> Bool -> String -> Query
+boolKey _ False _ = []
+boolKey k True val = makeKey k val
+
+-- FIXME limit max number of issues
+projectIssues :: String -> Bool -> OutputFormat -> String -> Bool -> Maybe String -> Maybe String -> Maybe String -> IO ()
+projectIssues server count format repo allstatus mauthor msince mpat = do
+  let path = repo </> "issues"
+      params = [("status", Just "all") | allstatus] ++
+               maybeKey "author" mauthor ++ maybeKey "since" msince
+  pages <- queryPaged server count path params ("pagination", "page")
+  mapM_ (defaultPrinter format printIssues) pages
+  where
+    printIssues :: Object -> IO ()
+    printIssues result = do
+      let issues = lookupKey' "issues" result :: [Object]
+      mapM_ printIssue issues
+
+    printIssue :: Object -> IO ()
+    printIssue issue = do
+      let mfields = parseIssue issue
+      case mfields of
+        Nothing -> putStrLn "parsing issue failed"
+        Just (id',title,status) ->
+          when (isNothing mpat || T.pack (fromJust mpat) `T.isInfixOf` title) $
+          putStrLn $ "https://" <> server </> repo </> "issue" </> show id' <> " (" <> T.unpack status <> "): " <> T.unpack title
+
+    parseIssue :: Object -> Maybe (Integer, Text, Text)
+    parseIssue =
+      parseMaybe $ \obj -> do
+        id' <- obj .: "id"
+        title <- obj .: "title"
+        status <- obj .: "status"
+        return (id',title,status)
+
+-- FIXME limit max number of pages (10?) or --pages
+queryPaged :: String -> Bool -> String -> Query -> (String,String)
+           -> IO [Object]
+queryPaged server count path params (pagination,paging) =
+  if count
+    then do
+    mnum <- queryPagureCount server path params pagination
+    print $ fromMaybe (error' "pages not found") mnum
+    return []
+    else
+    queryPagurePaged server path params (pagination,paging)
+
+repoBranches :: String -> OutputFormat -> String -> IO ()
+repoBranches server format repo = do
+  let namespace =
+        if server == srcFedoraprojectOrg && '/' `notElem` repo
+        then "rpms/" else ""
+      path = namespace ++ repo </> "git/branches"
+  eres <- queryPagureSingle server path []
+  either error' (defaultPrinter format (printKeyList "branches")) eres
+
+users :: String -> OutputFormat -> String -> IO ()
+users server format pat = do
+  let path = "users"
+      params = makeKey "pattern" pat
+  res <- queryPagure server path params
+  defaultPrinter format (printKeyList "users") res
+
+username :: String -> OutputFormat -> String -> IO ()
+username server format user = do
+  let path = "user" </> user
+  res <- queryPagure' server path $ makeKey "per_page" "1"
+  defaultPrinter format printName res
+  where
+    printName res =
+      case lookupKey "user" res >>= lookupKey "fullname" of
+        Nothing -> error' "User fullname not found"
+        Just fn -> T.putStrLn fn
+
+groups :: String -> Bool -> OutputFormat -> Maybe String -> IO ()
+groups server count format mpat = do
+  let path = "groups"
+      params = maybeKey "pattern" mpat
+  pages <- queryPaged server count path params ("pagination", "page")
+  mapM_ (defaultPrinter format (printKeyList "groups")) pages
+
+printKeyList :: String -> Object -> IO ()
+printKeyList key' res =
+  mapM_ T.putStrLn (lookupKey' (T.pack key') res :: [Text])
+
+gitUrl :: String -> OutputFormat -> String -> IO ()
+gitUrl server format repo = do
+  let namespace =
+        if server == srcFedoraprojectOrg && '/' `notElem` repo
+        then "rpms/" else ""
+      path = namespace ++ repo </> "git/urls"
+  res <- queryPagure server path []
+  defaultPrinter format printURLs res
+  where
+    printURLs result =
+      mapM_ T.putStrLn $
+      M.elems $ localLookupKey "urls" result
+
+    localLookupKey :: String -> Object ->
+#if MIN_VERSION_aeson(2,0,0)
+                      M.KeyMap Text
+#else
+                      M.HashMap Text Text
+#endif
+    localLookupKey = lookupKey' . T.pack
+
+-- from simple-cmd
+error' :: String -> a
+#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,9,0))
+error' = errorWithoutStackTrace
+#else
+error' = error
+#endif
+
+yamlPrinter :: OutputFormat -> Object -> IO ()
+yamlPrinter FormatDefault = yamlPrinter FormatYaml
+yamlPrinter FormatJson = BL.putStrLn . encodePretty
+yamlPrinter FormatYaml = B.putStrLn . encode
+
+projectInfo :: String -> OutputFormat -> String -> IO ()
+projectInfo server format repo = do
+  let namespace =
+        if server == srcFedoraprojectOrg && '/' `notElem` repo
+        then "rpms/" else ""
+      path = namespace ++ repo
+  eval <- pagureProjectInfo server path
+  either error' (yamlPrinter format) eval
+
+projectIssue :: String -> OutputFormat -> String -> Int -> IO ()
+projectIssue server format repo issue = do
+  eval <- pagureProjectIssueInfo server repo issue
+  either error' (yamlPrinter format) eval
+
+userInfo :: String -> OutputFormat -> String -> IO ()
+userInfo server format user = do
+  eval <- pagureUserInfo server user []
+  either error' (yamlPrinter format) eval
