kioku-cli-0.1.0.0: src/Kioku/Cli/Options.hs
-- | Shared option readers and helpers for the kioku CLI.
module Kioku.Cli.Options
( boundedIntReader,
yesWriteEventsFlag,
redactConnectionString,
)
where
import Data.Text (Text)
import Data.Text qualified as Text
import Options.Applicative
-- | An integer option reader that enforces an inclusive range at parse time.
--
-- @label@ names the value in the error message, e.g. @\"LIMIT\"@. Without this, @--limit -1@
-- travelled all the way to Postgres and came back as @LIMIT must not be negative@, and an
-- unbounded large value was a cost and latency footgun rather than a parse error.
boundedIntReader :: String -> Int -> Int -> ReadM Int
boundedIntReader label lo hi = do
n <- auto
if n >= lo && n <= hi
then pure n
else
readerError
(label <> " must be between " <> show lo <> " and " <> show hi <> " (got " <> show n <> ")")
-- | Required opt-in for a command that appends permanent events.
--
-- 'flag'' with no default, so omitting it is a parse error (@Missing: --yes-write-events@) and
-- the requirement is visible in @--help@. Deliberately not an environment variable: an
-- exported @KIOKU_ALLOW_DEMO=1@ is sticky state that outlives the moment of consent, which is
-- exactly how accidents happen in a long-lived shell.
yesWriteEventsFlag :: Parser ()
yesWriteEventsFlag =
flag'
()
( long "yes-write-events"
<> help
"Required confirmation: this command appends PERMANENT events (kioku has no delete) to the database at PG_CONNECTION_STRING"
)
-- | Best-effort password redaction for printing a libpq connection string.
--
-- Handles the keyword form (@password=...@) and the URI form (@user:pass\@host@). Best-effort
-- is the honest description: it exists so the preflight can show the operator which database
-- they are about to write to without echoing a secret into a terminal or a CI log.
redactConnectionString :: Text -> Text
redactConnectionString conn =
case Text.stripPrefix "postgres://" conn of
Just rest -> "postgres://" <> redactUserInfo rest
Nothing ->
case Text.stripPrefix "postgresql://" conn of
Just rest -> "postgresql://" <> redactUserInfo rest
Nothing -> Text.unwords (map redactPair (Text.words conn))
where
redactPair kv
| "password=" `Text.isPrefixOf` kv = "password=REDACTED"
| otherwise = kv
redactUserInfo rest =
case Text.breakOn "@" rest of
(_, "") -> rest
(userinfo, hostPart) ->
case Text.breakOn ":" userinfo of
(_, "") -> userinfo <> hostPart
(user, _) -> user <> ":REDACTED" <> hostPart