Files
jbrechtel 7feff5abfa converge: phase 2 — SSE relay client for realtime pull on push
- RepoConfig gains rcGiteaRepo :: Maybe Text for relay matching
- Options gains optRelayUrl :: Maybe Text (--relay-url CLI, relay_url config)
- connectRelay connects to converge-relay SSE endpoint, parses push events,
  triggers gitPull on matching repos (by gitea_repo + branch)
- Exponential backoff reconnection (1s .. 60s) on disconnect/error
- Skips pull if repo is mid-operation (merge/rebase/etc.)
- Dependencies: http-client, http-client-tls, http-types, aeson, bytestring
- SSE parser handles event:/data: fields, blank-line delimiters, CRLF endings
- Updated SPEC.md, example config, and help text
2026-05-14 14:13:43 -04:00

194 lines
6.9 KiB
Haskell

module Main (main) where
import Converge
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Options.Applicative
import System.IO (hPutStrLn, stderr)
data CliArgs = CliArgs
{ cliConfig :: !(Maybe FilePath)
-- ^ Optional path to a YAML config file.
, cliRepo :: !(Maybe FilePath)
-- ^ Optional single repo path (requires --config to be absent).
, cliRemote :: !Text
-- ^ Remote name (used only in single-repo mode).
, cliBranch :: !Text
-- ^ Branch name (used only in single-repo mode).
, cliDebounce :: !(Maybe Int)
-- ^ Debounce override for both modes.
, cliLogLevel :: !(Maybe LogLevel)
-- ^ Log level override.
, cliLogFile :: !(Maybe FilePath)
-- ^ Log file path.
, cliSync :: !Bool
-- ^ Run a single sync cycle and exit (useful for wake-from-sleep hooks).
, cliRelayUrl :: !(Maybe Text)
-- ^ URL of a converge-relay SSE endpoint (overrides config).
}
cliParser :: Parser CliArgs
cliParser =
CliArgs
<$> optional
( strOption
( long "config"
<> short 'c'
<> metavar "FILE"
<> help "Path to YAML config file listing repositories"
)
)
<*> optional
( strOption
( long "repo"
<> short 'r'
<> metavar "PATH"
<> help "Path to a single git repository to watch (default: cwd)"
)
)
<*> strOption
( long "remote"
<> metavar "NAME"
<> value "origin"
<> showDefault
<> help "Remote name to pull from (single-repo mode)"
)
<*> strOption
( long "branch"
<> short 'b'
<> metavar "NAME"
<> value "main"
<> showDefault
<> help "Branch to pull (single-repo mode)"
)
<*> optional
( option
auto
( long "debounce"
<> short 'd'
<> metavar "MS"
<> help "Debounce window in milliseconds (overrides config file)"
)
)
<*> optional
( option
( maybeReader
( \s -> case T.toLower (T.pack s) of
"debug" -> Just Debug
"info" -> Just Info
"warn" -> Just Warn
"error" -> Just Error
_ -> Nothing
)
)
( long "log-level"
<> metavar "LEVEL"
<> help "Minimum log level: debug, info, warn, or error (default: info)"
)
)
<*> optional
( strOption
( long "log-file"
<> metavar "FILE"
<> help "Write logs to a file instead of stderr"
)
)
<*> switch
( long "sync"
<> short 's'
<> help "Run a single sync cycle (commit + pull + push) and exit"
)
<*> optional
( strOption
( long "relay-url"
<> metavar "URL"
<> help "URL of a converge-relay SSE endpoint for realtime push notifications"
)
)
parserInfo :: ParserInfo CliArgs
parserInfo =
info
(cliParser <**> helper)
( fullDesc
<> progDesc "Auto-sync git repos by watching, committing, and pulling"
<> header "converge - keep git repos in sync"
)
main :: IO ()
main = do
cli <- execParser parserInfo
let mDebounceOverride = cliDebounce cli
(options, configSource) <- case cliConfig cli of
Just cfgPath -> do
cfg <- loadConfig cfgPath
let opts = applyOverrides mDebounceOverride (cliLogLevel cli) (cliLogFile cli) (cliRelayUrl cli) (configToOptions cfg)
pure (opts, Just cfgPath)
Nothing -> do
exists <- configFileExists
if exists
then do
cfgPath <- defaultConfigPath
cfg <- loadConfig cfgPath
let opts = applyOverrides mDebounceOverride (cliLogLevel cli) (cliLogFile cli) (cliRelayUrl cli) (configToOptions cfg)
pure (opts, Just cfgPath)
else
pure
( applyOverrides
mDebounceOverride
(cliLogLevel cli)
(cliLogFile cli)
(cliRelayUrl cli)
defaultOptions
{ optRepos =
[ defaultRepoConfig
{ rcPath = fromMaybe "." (cliRepo cli)
, rcRemote = cliRemote cli
, rcBranch = cliBranch cli
}
]
, optDebounceMs = fromMaybe 5000 mDebounceOverride
}
, Nothing
)
case configSource of
Just path ->
hPutStrLn stderr ("Using config: " <> path)
Nothing ->
hPutStrLn stderr "No config file found, using single-repo mode"
let repos = optRepos options
if cliSync cli
then do
hPutStrLn stderr $
"Syncing "
<> show (length repos)
<> " repo(s)"
mapM_ (\r -> hPutStrLn stderr (" - " <> rcPath r)) repos
logger <- newLogger (optLogLevel options) (optLogFile options)
mapM_ (syncRepo options logger) repos
else do
hPutStrLn stderr $
"Watching "
<> show (length repos)
<> " repo(s)"
<> " (debounce: "
<> show (optDebounceMs options)
<> "ms)"
mapM_ (\r -> hPutStrLn stderr (" - " <> rcPath r)) repos
runConverge options
-- | Apply CLI overrides (debounce, log level, log file, relay url) to Options.
applyOverrides :: Maybe Int -> Maybe LogLevel -> Maybe FilePath -> Maybe Text -> Options -> Options
applyOverrides mDebounce mLogLevel mLogFile mRelayUrl opts =
opts
{ optDebounceMs = maybe (optDebounceMs opts) id mDebounce
, optLogLevel = fromMaybe (optLogLevel opts) mLogLevel
, optLogFile = case mLogFile of
Just _ -> mLogFile -- CLI --log-file overrides config
Nothing -> optLogFile opts
, optRelayUrl = case mRelayUrl of
Just _ -> mRelayUrl -- CLI --relay-url overrides config
Nothing -> optRelayUrl opts
}