-- SPDX-FileCopyrightText: 2026 Alexandra de Wit
--
-- SPDX-License-Identifier: MIT

{- | The @ecluse@ command line: the subcommand grammar and the flags each one settles. A bare
invocation is @proxy@, so an operator who names no role gets the single-process pipeline.
'Ecluse.Startup.runWith' dispatches the 'AppCommand' this yields.
-}
module Ecluse.CLI (
    AppCommand (..),
    commandParser,
    execCLI,
) where

import Options.Applicative

import Ecluse.Composition.Types (MirrorRole (MirrorOnly, ServeAndMirror, ServeOnly))
import Ecluse.Core.BuildIdentity (productVersion)
import Ecluse.Dredger.Plan (
    DredgerOptions (DredgerOptions),
    SweepMode (SweepDeletes, SweepPreviews),
    SweepRepetition (SweepContinuously, SweepOnce),
 )
import Ecluse.Pilot (PilotCompileOptions (..))

data AppCommand
    = -- | The mirror pipeline in the selected role: @proxy@, @proxy --no-worker@, or @mirror@.
      RunService MirrorRole
    | RunPilot
    | RunPilotCompile PilotCompileOptions
    | -- | @ecluse dredger@, with the repetition and the mode its flags settled.
      RunDredger DredgerOptions
    | RunCheckConfig
    deriving stock (AppCommand -> AppCommand -> Bool
(AppCommand -> AppCommand -> Bool)
-> (AppCommand -> AppCommand -> Bool) -> Eq AppCommand
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: AppCommand -> AppCommand -> Bool
== :: AppCommand -> AppCommand -> Bool
$c/= :: AppCommand -> AppCommand -> Bool
/= :: AppCommand -> AppCommand -> Bool
Eq, Int -> AppCommand -> ShowS
[AppCommand] -> ShowS
AppCommand -> FilePath
(Int -> AppCommand -> ShowS)
-> (AppCommand -> FilePath)
-> ([AppCommand] -> ShowS)
-> Show AppCommand
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> AppCommand -> ShowS
showsPrec :: Int -> AppCommand -> ShowS
$cshow :: AppCommand -> FilePath
show :: AppCommand -> FilePath
$cshowList :: [AppCommand] -> ShowS
showList :: [AppCommand] -> ShowS
Show)

commandParser :: Parser AppCommand
commandParser :: Parser AppCommand
commandParser =
    Mod CommandFields AppCommand -> Parser AppCommand
forall a. Mod CommandFields a -> Parser a
hsubparser
        ( FilePath -> ParserInfo AppCommand -> Mod CommandFields AppCommand
forall a. FilePath -> ParserInfo a -> Mod CommandFields a
command FilePath
"proxy" (Parser AppCommand -> InfoMod AppCommand -> ParserInfo AppCommand
forall a. Parser a -> InfoMod a -> ParserInfo a
info (MirrorRole -> AppCommand
RunService (MirrorRole -> AppCommand)
-> Parser MirrorRole -> Parser AppCommand
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Parser MirrorRole
proxyRoleParser) (FilePath -> InfoMod AppCommand
forall a. FilePath -> InfoMod a
progDesc FilePath
"Run the Écluse proxy server"))
            Mod CommandFields AppCommand
-> Mod CommandFields AppCommand -> Mod CommandFields AppCommand
forall a. Semigroup a => a -> a -> a
<> FilePath -> ParserInfo AppCommand -> Mod CommandFields AppCommand
forall a. FilePath -> ParserInfo a -> Mod CommandFields a
command FilePath
"mirror" (Parser AppCommand -> InfoMod AppCommand -> ParserInfo AppCommand
forall a. Parser a -> InfoMod a -> ParserInfo a
info (AppCommand -> Parser AppCommand
forall a. a -> Parser a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (MirrorRole -> AppCommand
RunService MirrorRole
MirrorOnly)) (FilePath -> InfoMod AppCommand
forall a. FilePath -> InfoMod a
progDesc FilePath
"Run the Écluse mirror worker alone, for a worker fleet scaled on queue depth"))
            Mod CommandFields AppCommand
-> Mod CommandFields AppCommand -> Mod CommandFields AppCommand
forall a. Semigroup a => a -> a -> a
<> FilePath -> ParserInfo AppCommand -> Mod CommandFields AppCommand
forall a. FilePath -> ParserInfo a -> Mod CommandFields a
command FilePath
"pilot" (Parser AppCommand -> InfoMod AppCommand -> ParserInfo AppCommand
forall a. Parser a -> InfoMod a -> ParserInfo a
info Parser AppCommand
pilotCommandParser (FilePath -> InfoMod AppCommand
forall a. FilePath -> InfoMod a
progDesc FilePath
"Run the Écluse Pilot (OSV ingestion pipeline)"))
            Mod CommandFields AppCommand
-> Mod CommandFields AppCommand -> Mod CommandFields AppCommand
forall a. Semigroup a => a -> a -> a
<> FilePath -> ParserInfo AppCommand -> Mod CommandFields AppCommand
forall a. FilePath -> ParserInfo a -> Mod CommandFields a
command FilePath
"dredger" (Parser AppCommand -> InfoMod AppCommand -> ParserInfo AppCommand
forall a. Parser a -> InfoMod a -> ParserInfo a
info (DredgerOptions -> AppCommand
RunDredger (DredgerOptions -> AppCommand)
-> Parser DredgerOptions -> Parser AppCommand
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Parser DredgerOptions
dredgerOptionsParser) (FilePath -> InfoMod AppCommand
forall a. FilePath -> InfoMod a
progDesc FilePath
"Run the Écluse Dredger (mirror pruning worker)"))
            Mod CommandFields AppCommand
-> Mod CommandFields AppCommand -> Mod CommandFields AppCommand
forall a. Semigroup a => a -> a -> a
<> FilePath -> ParserInfo AppCommand -> Mod CommandFields AppCommand
forall a. FilePath -> ParserInfo a -> Mod CommandFields a
command FilePath
"check-config" (Parser AppCommand -> InfoMod AppCommand -> ParserInfo AppCommand
forall a. Parser a -> InfoMod a -> ParserInfo a
info (AppCommand -> Parser AppCommand
forall a. a -> Parser a
forall (f :: * -> *) a. Applicative f => a -> f a
pure AppCommand
RunCheckConfig) (FilePath -> InfoMod AppCommand
forall a. FilePath -> InfoMod a
progDesc FilePath
"Validate the configuration and print the resolved posture, then exit (0 valid, 2 refused)"))
        )
        Parser AppCommand -> Parser AppCommand -> Parser AppCommand
forall a. Parser a -> Parser a -> Parser a
forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> AppCommand -> Parser AppCommand
forall a. a -> Parser a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (MirrorRole -> AppCommand
RunService MirrorRole
ServeAndMirror)

-- Absent, the proxy embeds the worker, which is what the in-memory queue requires.
proxyRoleParser :: Parser MirrorRole
proxyRoleParser :: Parser MirrorRole
proxyRoleParser =
    MirrorRole
-> MirrorRole -> Mod FlagFields MirrorRole -> Parser MirrorRole
forall a. a -> a -> Mod FlagFields a -> Parser a
flag
        MirrorRole
ServeAndMirror
        MirrorRole
ServeOnly
        (FilePath -> Mod FlagFields MirrorRole
forall (f :: * -> *) a. HasName f => FilePath -> Mod f a
long FilePath
"no-worker" Mod FlagFields MirrorRole
-> Mod FlagFields MirrorRole -> Mod FlagFields MirrorRole
forall a. Semigroup a => a -> a -> a
<> FilePath -> Mod FlagFields MirrorRole
forall (f :: * -> *) a. FilePath -> Mod f a
help FilePath
"Serve without the embedded mirror worker; needs a durable ECLUSE_QUEUE__URL and an 'ecluse mirror' fleet to drain it")

{- Both flags narrow what one invocation does, so absent they give the shipped behaviour: cycle
for the life of the process, and delete what a named decisive deny condemns. -}
dredgerOptionsParser :: Parser DredgerOptions
dredgerOptionsParser :: Parser DredgerOptions
dredgerOptionsParser =
    SweepMode -> SweepRepetition -> DredgerOptions
DredgerOptions
        (SweepMode -> SweepRepetition -> DredgerOptions)
-> Parser SweepMode -> Parser (SweepRepetition -> DredgerOptions)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> SweepMode
-> SweepMode -> Mod FlagFields SweepMode -> Parser SweepMode
forall a. a -> a -> Mod FlagFields a -> Parser a
flag
            SweepMode
SweepDeletes
            SweepMode
SweepPreviews
            (FilePath -> Mod FlagFields SweepMode
forall (f :: * -> *) a. HasName f => FilePath -> Mod f a
long FilePath
"dry-run" Mod FlagFields SweepMode
-> Mod FlagFields SweepMode -> Mod FlagFields SweepMode
forall a. Semigroup a => a -> a -> a
<> FilePath -> Mod FlagFields SweepMode
forall (f :: * -> *) a. FilePath -> Mod f a
help FilePath
"Report what a cycle would delete and delete nothing; it holds no delete, writes no walk marker, and needs no deletion consent")
        Parser (SweepRepetition -> DredgerOptions)
-> Parser SweepRepetition -> Parser DredgerOptions
forall a b. Parser (a -> b) -> Parser a -> Parser b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> SweepRepetition
-> SweepRepetition
-> Mod FlagFields SweepRepetition
-> Parser SweepRepetition
forall a. a -> a -> Mod FlagFields a -> Parser a
flag
            SweepRepetition
SweepContinuously
            SweepRepetition
SweepOnce
            (FilePath -> Mod FlagFields SweepRepetition
forall (f :: * -> *) a. HasName f => FilePath -> Mod f a
long FilePath
"once" Mod FlagFields SweepRepetition
-> Mod FlagFields SweepRepetition -> Mod FlagFields SweepRepetition
forall a. Semigroup a => a -> a -> a
<> FilePath -> Mod FlagFields SweepRepetition
forall (f :: * -> *) a. FilePath -> Mod f a
help FilePath
"Run one cycle, then exit: 0 when it completed, 1 when it halted")

-- A bare @pilot@ keeps its serve-and-export meaning. @pilot compile@ selects the
-- one-shot mode.
pilotCommandParser :: Parser AppCommand
pilotCommandParser :: Parser AppCommand
pilotCommandParser =
    Mod CommandFields AppCommand -> Parser AppCommand
forall a. Mod CommandFields a -> Parser a
hsubparser
        ( FilePath -> ParserInfo AppCommand -> Mod CommandFields AppCommand
forall a. FilePath -> ParserInfo a -> Mod CommandFields a
command
            FilePath
"compile"
            (Parser AppCommand -> InfoMod AppCommand -> ParserInfo AppCommand
forall a. Parser a -> InfoMod a -> ParserInfo a
info (PilotCompileOptions -> AppCommand
RunPilotCompile (PilotCompileOptions -> AppCommand)
-> Parser PilotCompileOptions -> Parser AppCommand
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Parser PilotCompileOptions
pilotCompileOptionsParser) (FilePath -> InfoMod AppCommand
forall a. FilePath -> InfoMod a
progDesc FilePath
"Compile one ecosystem's OSV export into a local osv.db artifact, then exit"))
        )
        Parser AppCommand -> Parser AppCommand -> Parser AppCommand
forall a. Parser a -> Parser a -> Parser a
forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> AppCommand -> Parser AppCommand
forall a. a -> Parser a
forall (f :: * -> *) a. Applicative f => a -> f a
pure AppCommand
RunPilot

pilotCompileOptionsParser :: Parser PilotCompileOptions
pilotCompileOptionsParser :: Parser PilotCompileOptions
pilotCompileOptionsParser =
    Text -> FilePath -> Bool -> PilotCompileOptions
PilotCompileOptions
        (Text -> FilePath -> Bool -> PilotCompileOptions)
-> Parser Text -> Parser (FilePath -> Bool -> PilotCompileOptions)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Mod OptionFields Text -> Parser Text
forall s. IsString s => Mod OptionFields s -> Parser s
strOption (FilePath -> Mod OptionFields Text
forall (f :: * -> *) a. HasName f => FilePath -> Mod f a
long FilePath
"ecosystem" Mod OptionFields Text
-> Mod OptionFields Text -> Mod OptionFields Text
forall a. Semigroup a => a -> a -> a
<> FilePath -> Mod OptionFields Text
forall (f :: * -> *) a. HasMetavar f => FilePath -> Mod f a
metavar FilePath
"ECOSYSTEM" Mod OptionFields Text
-> Mod OptionFields Text -> Mod OptionFields Text
forall a. Semigroup a => a -> a -> a
<> Text -> Mod OptionFields Text
forall (f :: * -> *) a. HasValue f => a -> Mod f a
value Text
"npm" Mod OptionFields Text
-> Mod OptionFields Text -> Mod OptionFields Text
forall a. Semigroup a => a -> a -> a
<> Mod OptionFields Text
forall a (f :: * -> *). Show a => Mod f a
showDefault Mod OptionFields Text
-> Mod OptionFields Text -> Mod OptionFields Text
forall a. Semigroup a => a -> a -> a
<> FilePath -> Mod OptionFields Text
forall (f :: * -> *) a. FilePath -> Mod f a
help FilePath
"Ecosystem whose OSV export to compile")
        Parser (FilePath -> Bool -> PilotCompileOptions)
-> Parser FilePath -> Parser (Bool -> PilotCompileOptions)
forall a b. Parser (a -> b) -> Parser a -> Parser b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Mod OptionFields FilePath -> Parser FilePath
forall s. IsString s => Mod OptionFields s -> Parser s
strOption (FilePath -> Mod OptionFields FilePath
forall (f :: * -> *) a. HasName f => FilePath -> Mod f a
long FilePath
"out" Mod OptionFields FilePath
-> Mod OptionFields FilePath -> Mod OptionFields FilePath
forall a. Semigroup a => a -> a -> a
<> FilePath -> Mod OptionFields FilePath
forall (f :: * -> *) a. HasMetavar f => FilePath -> Mod f a
metavar FilePath
"DIR" Mod OptionFields FilePath
-> Mod OptionFields FilePath -> Mod OptionFields FilePath
forall a. Semigroup a => a -> a -> a
<> FilePath -> Mod OptionFields FilePath
forall (f :: * -> *) a. FilePath -> Mod f a
help FilePath
"Directory the artifact is written into")
        Parser (Bool -> PilotCompileOptions)
-> Parser Bool -> Parser PilotCompileOptions
forall a b. Parser (a -> b) -> Parser a -> Parser b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Mod FlagFields Bool -> Parser Bool
switch (FilePath -> Mod FlagFields Bool
forall (f :: * -> *) a. HasName f => FilePath -> Mod f a
long FilePath
"upload" Mod FlagFields Bool -> Mod FlagFields Bool -> Mod FlagFields Bool
forall a. Semigroup a => a -> a -> a
<> FilePath -> Mod FlagFields Bool
forall (f :: * -> *) a. FilePath -> Mod f a
help FilePath
"After compiling, upload the artifact to the configured advisory store (one full sync cycle)")

execCLI :: IO AppCommand
execCLI :: IO AppCommand
execCLI =
    ParserInfo AppCommand -> IO AppCommand
forall a. ParserInfo a -> IO a
execParser (ParserInfo AppCommand -> IO AppCommand)
-> ParserInfo AppCommand -> IO AppCommand
forall a b. (a -> b) -> a -> b
$
        Parser AppCommand -> InfoMod AppCommand -> ParserInfo AppCommand
forall a. Parser a -> InfoMod a -> ParserInfo a
info
            (Parser AppCommand
commandParser Parser AppCommand
-> Parser (AppCommand -> AppCommand) -> Parser AppCommand
forall (f :: * -> *) a b. Applicative f => f a -> f (a -> b) -> f b
<**> Parser (AppCommand -> AppCommand)
forall a. Parser (a -> a)
helper Parser AppCommand
-> Parser (AppCommand -> AppCommand) -> Parser AppCommand
forall (f :: * -> *) a b. Applicative f => f a -> f (a -> b) -> f b
<**> Parser (AppCommand -> AppCommand)
forall a. Parser (a -> a)
versionOption)
            ( InfoMod AppCommand
forall a. InfoMod a
fullDesc
                InfoMod AppCommand -> InfoMod AppCommand -> InfoMod AppCommand
forall a. Semigroup a => a -> a -> a
<> FilePath -> InfoMod AppCommand
forall a. FilePath -> InfoMod a
progDesc FilePath
"Écluse - supply-chain resilience proxy"
                InfoMod AppCommand -> InfoMod AppCommand -> InfoMod AppCommand
forall a. Semigroup a => a -> a -> a
<> FilePath -> InfoMod AppCommand
forall a. FilePath -> InfoMod a
header FilePath
"ecluse - a configurable policy gate for package registries"
            )
  where
    versionOption :: Parser (a -> a)
versionOption = FilePath -> Mod OptionFields (a -> a) -> Parser (a -> a)
forall a. FilePath -> Mod OptionFields (a -> a) -> Parser (a -> a)
infoOption (Text -> FilePath
forall a. ToString a => a -> FilePath
toString Text
productVersion) (FilePath -> Mod OptionFields (a -> a)
forall (f :: * -> *) a. HasName f => FilePath -> Mod f a
long FilePath
"version" Mod OptionFields (a -> a)
-> Mod OptionFields (a -> a) -> Mod OptionFields (a -> a)
forall a. Semigroup a => a -> a -> a
<> FilePath -> Mod OptionFields (a -> a)
forall (f :: * -> *) a. FilePath -> Mod f a
help FilePath
"Show version")