gachi-cli (empty) → 0.1.0.0
raw patch · 9 files changed
+2511/−0 lines, 9 filesdep +basedep +directorydep +filepath
Dependencies added: base, directory, filepath, gachi-cli, gachi-core, gachi-retrofit, gachi-scaffold, gachi-schema, gachi-sync, hspec, optparse-applicative, process, text, time
Files
- app/Main.hs +48/−0
- gachi-cli.cabal +89/−0
- src/Gachi/CLI.hs +149/−0
- src/Gachi/CLI/Doctor.hs +221/−0
- src/Gachi/CLI/Init.hs +413/−0
- src/Gachi/CLI/Options.hs +470/−0
- src/Gachi/CLI/Retrofit.hs +166/−0
- src/Gachi/CLI/Sync.hs +373/−0
- test/Main.hs +582/−0
+ app/Main.hs view
@@ -0,0 +1,48 @@+-- | Module: Main+-- Description: Executable entry point for the gachi CLI tool+--+-- This module serves as the physical binary entry point for the @gachi@ command-line+-- utility. It handles top-level process execution, argument consumption, and exit code+-- normalization by delegating immediately to 'Gachi.CLI.defaultMain'.+--+-- = Architectural Context & Invariants+--+-- In Gachi's infrastructure model (per Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization]), the CLI executable provides turnkey+-- operations for repository scaffolding, configuration synchronization, codebase+-- retrofitting, and strategic planning.+--+-- == Process Execution Model+--+-- 1. __Environment Agnostic:__+-- The binary does not rely on ambient bash or Python scripts for execution logic;+-- all schema evaluation, filesystem synthesis, and Git configuration occur within+-- compiled Haskell runtime space.+--+-- 2. __Hermetic Process Termination:__+-- Process termination strictly reflects the outcome of the invoked subcommand.+-- Successful operations exit cleanly with 'System.Exit.ExitSuccess' (code 0).+-- Any syntax failure, schema mismatch, or filesystem collision terminates+-- immediately with 'System.Exit.ExitFailure' (code 1), preventing partial or+-- unstable workspaces from misleading downstream CI runners.+--+-- = Usage Summary+--+-- Invoked directly from the terminal or launcher scripts:+--+-- @+-- $ gachi init my-project+-- $ gachi init --config project.dhall --target-dir ./target+-- $ gachi version+-- @+module Main (main) where++import Gachi.CLI (defaultMain)++-- | Primary operating system process entry point.+--+-- Evaluates command-line flags and dispatches across operational subcommands via+-- 'Gachi.CLI.defaultMain'. Terminates the calling process with the appropriate+-- exit code upon completion.+main :: IO ()+main = defaultMain
+ gachi-cli.cabal view
@@ -0,0 +1,89 @@+cabal-version: 3.0+name: gachi-cli+version: 0.1.0.0+synopsis: Unified command-line interface for the gachi ecosystem engine+description:+ Turnkey command-line interface providing declarative project initialization ('gachi init'),+ harmonic configuration propagation ('gachi sync'), non-destructive codebase retrofitting+ ('gachi retrofit'), and ecosystem roadmap governance ('gachi plan').+license: EUPL-1.2+author: Nadia Yvette Chambers+maintainer: nadia.yvette.chambers@ik.me+category: Development, CLI, Tools+build-type: Simple+tested-with: GHC == 9.14.1++library+ exposed-modules:+ Gachi.CLI+ Gachi.CLI.Doctor+ Gachi.CLI.Init+ Gachi.CLI.Options+ Gachi.CLI.Retrofit+ Gachi.CLI.Sync+ hs-source-dirs: src+ default-language: Haskell2010+ default-extensions:+ OverloadedStrings+ DerivingStrategies+ ghc-options:+ -Wall+ -Wcompat+ -Wredundant-constraints+ build-depends:+ base >= 4.14 && < 5,+ text >= 2.0,+ directory >= 1.3,+ filepath >= 1.4,+ process >= 1.6,+ optparse-applicative >= 0.17,+ gachi-core >= 0.1,+ gachi-schema >= 0.1,+ gachi-scaffold >= 0.1,+ gachi-sync >= 0.1,+ gachi-retrofit >= 0.1++executable gachi+ main-is: Main.hs+ hs-source-dirs: app+ default-language: Haskell2010+ default-extensions:+ OverloadedStrings+ ghc-options:+ -Wall+ -Wcompat+ -Wredundant-constraints+ -threaded+ -rtsopts+ -with-rtsopts=-N+ build-depends:+ base >= 4.14 && < 5,+ gachi-cli++test-suite tests+ type: exitcode-stdio-1.0+ main-is: Main.hs+ hs-source-dirs: test+ default-language: Haskell2010+ default-extensions:+ OverloadedStrings+ DerivingStrategies+ ghc-options:+ -Wall+ -Wcompat+ -Wredundant-constraints+ -threaded+ build-depends:+ base >= 4.14 && < 5,+ gachi-cli,+ gachi-schema,+ gachi-scaffold,+ gachi-sync,+ gachi-retrofit,+ text >= 2.0,+ directory >= 1.3,+ filepath >= 1.4,+ process >= 1.6,+ optparse-applicative >= 0.17,+ time >= 1.9,+ hspec >= 2.10
+ src/Gachi/CLI.hs view
@@ -0,0 +1,149 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Module: Gachi.CLI+-- Description: Unified front-door dispatcher and entry point for gachi+--+-- This is the root library module for @gachi-cli@. It provides top-level command+-- line argument parsing, dispatching, version reporting, and error handling.+--+-- = Architectural Context & Invariants+--+-- Gachi serves as the foundational project skeleton generator and configuration+-- propagation engine across the ecosystem (per Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization]).+--+-- == Operational Architecture+--+-- This module acts as the primary orchestrator and dispatcher across all CLI subcommands:+--+-- 1. __Argument Processing:__+-- Uses @optparse-applicative@ configured with 'showHelpOnEmpty' to display context-aware+-- usage hints whenever an empty invocation or ambiguous argument sequence is detected.+--+-- 2. __Execution Lifecycle:__+-- Dispatches parsed 'Command' representations to their specialized execution modules.+-- 'CmdInit' delegates to 'runInit' in "Gachi.CLI.Init", while placeholder routines+-- for Phase 3 ('CmdSync'), Phase 4 ('CmdRetrofit'), and Phase 5 ('CmdPlan') provide+-- informative scheduling notices without failing abruptly.+--+-- 3. __Exit Code Normalization:__+-- Every subcommand produces a deterministic 'ExitCode' ('ExitSuccess' or 'ExitFailure'),+-- ensuring predictable interaction with shell scripts, CI runners, and pre-commit hooks.+--+-- = Command Matrix+--+-- * 'gachi init': Full turnkey multi-package repository materialization (Milestone 2.3).+-- * 'gachi sync': Bidirectional configuration synchronization (Phase 3).+-- * 'gachi retrofit': Non-destructive legacy codebase upgrade (Phase 4).+-- * 'gachi plan': Detached git snapshotting and rollback (Phase 5).+-- * 'gachi version': Compiler and tooling pinning diagnostics.+--+-- = Worked Example+--+-- >>> import Gachi.CLI+-- >>> -- Invoked from executable Main:+-- >>> -- defaultMain+module Gachi.CLI+ ( -- * Main CLI Dispatcher+ defaultMain+ , runCommand++ -- * Package Metadata+ , gachiVersion++ -- * Re-exports+ , module Gachi.CLI.Doctor+ , module Gachi.CLI.Init+ , module Gachi.CLI.Options+ , module Gachi.CLI.Retrofit+ , module Gachi.CLI.Sync+ ) where++import Data.Text (Text)+import qualified Data.Text.IO as TIO+import Options.Applicative (customExecParser, prefs, showHelpOnEmpty)+import System.Exit (ExitCode(..), exitWith)++import Gachi.CLI.Doctor+ ( checkBinaryVersion+ , checkToolchainHealth+ , formatToolchainStatus+ , runDoctor+ , runSheafAudit+ )+import Gachi.CLI.Init+ ( detectGitAuthor+ , executeScaffolding+ , initializeGitRepository+ , promptUser+ , resolveProject+ , runInit+ )+import Gachi.CLI.Options+ ( Command(..)+ , DoctorOptions(..)+ , InitOptions(..)+ , PlanOptions(..)+ , RetrofitOptions(..)+ , SyncOptions(..)+ , commandParser+ , doctorOptionsParser+ , initOptionsParser+ , parseMode+ , parsePackageList+ , planOptionsParser+ , retrofitOptionsParser+ , syncOptionsParser+ )+import Gachi.CLI.Retrofit (runRetrofit)+import Gachi.CLI.Sync (runSync)++--------------------------------------------------------------------------------+-- 1. Metadata+--------------------------------------------------------------------------------++-- | Canonical version identifier for the gachi toolchain.+gachiVersion :: Text+gachiVersion = "0.1.0.0"++--------------------------------------------------------------------------------+-- 2. Top-Level Entry Points+--------------------------------------------------------------------------------++-- | High-level executable entry point.+--+-- Parses ambient command-line arguments using 'commandParser' and delegates+-- to 'runCommand', terminating the process with the resulting 'ExitCode'.+defaultMain :: IO ()+defaultMain = do+ let parserPrefs = prefs showHelpOnEmpty+ cmd <- customExecParser parserPrefs (commandParser gachiVersion)+ code <- runCommand cmd+ exitWith code++-- | Pure/IO command dispatcher executing a parsed 'Command'.+--+-- Maps high-level AST commands to operational routines:+--+-- * 'CmdInit': Dispatches directly to 'runInit', executing the complete project+-- synthesis, filesystem materialization, and Git governance lifecycle.+-- * 'CmdSync': Dispatches directly to 'runSync', harmonizing configuration and+-- governance stanzas across single repositories or the entire ecosystem.+-- * 'CmdRetrofit': Dispatches directly to 'runRetrofit', executing diagnostic+-- repository inspection (Milestone 4.1) and non-destructive overlay synthesis (Phase 4).+-- * 'CmdPlan': Emits roadmap schedule notice for Phase 5 detached Git ledger governance.+-- * 'CmdVersion': Outputs compiler pinning, Cabal target, and EUPL-1.2 licensing stanzas.+--+-- Guarantees total evaluation with deterministic exit code propagation.+runCommand :: Command -> IO ExitCode+runCommand (CmdInit opts) = runInit opts+runCommand (CmdSync opts) = runSync opts+runCommand (CmdDoctor opts) = runDoctor opts+runCommand (CmdRetrofit opts) = runRetrofit opts+runCommand (CmdPlan _opts) = do+ TIO.putStrLn "gachi plan: Detached Git store & snapshot rollback engine (Phase 5 - Scheduled)"+ pure ExitSuccess+runCommand CmdVersion = do+ TIO.putStrLn ("gachi v" <> gachiVersion <> " (GHC 9.14.1 / Cabal 3.18 / EUPL-1.2)")+ pure ExitSuccess
+ src/Gachi/CLI/Doctor.hs view
@@ -0,0 +1,221 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module: Gachi.CLI.Doctor+-- Description: Diagnostic health checks and cellular sheaf coherence auditing+--+-- This module implements the operational dispatch for the @gachi doctor@ command,+-- including ambient toolchain verification and 1-dimensional cellular sheaf+-- cohomology auditing across ecosystem checkouts (~/src/PLAN.md §1.A, Milestone 3.5).+--+-- = Architectural Context & Homological Invariants+--+-- Per Note [Cellular Sheaf Coherence and Homological Obstruction Localization]+-- [ref:note-cellular-sheaf-coherence], @gachi doctor@ evaluates the structural+-- health of single repositories or the entire multi-repository ecosystem:+--+-- 1. __Toolchain Health Verification:__+-- Audits ambient system binaries required by the physical verification harnesses:+-- * GHC compiler (pinned to 9.14.1)+-- * Cabal build tool (pinned to 3.18)+-- * HLint static analyzer (zero-hint policy)+-- * Tagref cross-reference validator (zero dangling tags)+--+-- 2. __Cellular Sheaf Coherence Audit ('doctorSheaf'):__+-- Constructs the 1-D cellular sheaf \(\mathcal{F}\) over cell complex \(G = (V, E)\)+-- where vertices \(V\) represent repository checkouts and edges \(E\) represent+-- ten standardization contract dimensions.+-- * Evaluates the sheaf coboundary operator \(\delta^0\).+-- * Confirms 0-th cohomology \(H^0(G; \mathcal{F}) = \ker \delta^0\) harmonic equilibrium (0 drift).+-- * Localizes 1-st cohomology \(H^1(G; \mathcal{F}) = \operatorname{coker} \delta^0\)+-- boundary obstructions, reporting exact drifting repositories, breached contracts,+-- and actionable remediation instructions.+--+-- = Worked Example+--+-- >>> import Gachi.CLI.Options (DoctorOptions(..))+-- >>> -- Invoked via CLI:+-- >>> -- code <- runDoctor (DoctorOptions Nothing True False)+-- >>> -- code == ExitSuccess+module Gachi.CLI.Doctor+ ( -- * High-Level Doctor Command Runners+ runDoctor+ , runSheafAudit+ , checkToolchainHealth++ -- * Helper Binary Inspection+ , checkBinaryVersion+ , formatToolchainStatus+ ) where++import Control.Exception (SomeException, catch)+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import System.Directory (doesDirectoryExist, doesFileExist, findExecutable, getHomeDirectory)+import System.Exit (ExitCode(..))+import System.FilePath ((</>))+import System.Process (readProcessWithExitCode)++import Gachi.CLI.Options (DoctorOptions(..))+import Gachi.Sync.Sheaf+ ( SheafReport(..)+ , auditEcosystemSheaf+ , auditSheafTarget+ , formatSheafReport+ )++--------------------------------------------------------------------------------+-- 1. Doctor Command Dispatcher+--------------------------------------------------------------------------------++-- | Main execution entry point for 'gachi doctor'.+--+-- Dispatches based on 'DoctorOptions':+--+-- * When @--all@ / @-a@ is specified: Executes whole-ecosystem cellular sheaf+-- coherence auditing across all discovered checkouts in the parent directory.+-- * When @--sheaf@ / @-s@ is specified: Executes targeted cellular sheaf+-- coherence auditing on the designated or current repository.+-- * Default mode: Evaluates ambient toolchain binaries, checks repository+-- governance pinning, and performs the cellular sheaf coherence audit.+--+-- Produces 'ExitSuccess' if all boundary restrictions are in harmonic equilibrium,+-- or 'ExitFailure 1' if fatal cohomological obstructions are detected.+runDoctor :: DoctorOptions -> IO ExitCode+runDoctor opts = do+ if doctorAll opts+ then do+ let parentDir = fromMaybe ".." (doctorTargetDir opts)+ runSheafAudit parentDir True+ else do+ let targetDir = fromMaybe "." (doctorTargetDir opts)+ if doctorSheaf opts+ then runSheafAudit targetDir False+ else do+ toolchainOk <- checkToolchainHealth+ sheafExit <- runSheafAudit targetDir False+ if toolchainOk && sheafExit == ExitSuccess+ then pure ExitSuccess+ else pure (ExitFailure 1)++--------------------------------------------------------------------------------+-- 2. Cellular Sheaf Coherence Audit Dispatcher+--------------------------------------------------------------------------------++-- | Executes 1-D cellular sheaf cohomology auditing and formats results.+--+-- When 'isEcosystem' is 'True', iterates over all checkouts discovered via+-- 'discoverEcosystemRepositories'. Otherwise, inspects a single target repository.+runSheafAudit :: FilePath -> Bool -> IO ExitCode+runSheafAudit targetPath isEcosystem = do+ isDir <- doesDirectoryExist targetPath+ if not isDir+ then do+ TIO.putStrLn ("[-] Error: Target directory does not exist: " <> T.pack targetPath)+ pure (ExitFailure 1)+ else do+ report <- if isEcosystem+ then auditEcosystemSheaf targetPath+ else auditSheafTarget targetPath++ TIO.putStr (formatSheafReport report)++ if reportIsHarmonic report+ then pure ExitSuccess+ else pure (ExitFailure 1)++--------------------------------------------------------------------------------+-- 3. Ambient Toolchain Health Checks+--------------------------------------------------------------------------------++-- | Status descriptor for an ambient system binary.+data ToolStatus = ToolStatus+ { toolName :: !Text+ -- ^ Command binary name (e.g. @"ghc"@).+ , toolExpected :: !Text+ -- ^ Pinned target version or requirement.+ , toolDetected :: !(Maybe Text)+ -- ^ Detected version string on PATH.+ , toolIsCompliant :: !Bool+ -- ^ Whether detected version satisfies ecosystem pinning.+ } deriving stock (Eq, Show)++-- | Audits ambient system toolchains required by the 5 physical verification gates.+--+-- Verifies availability and version compliance of GHC (9.14.1), Cabal (3.18),+-- HLint, and Tagref. Outputs an aligned status table.+checkToolchainHealth :: IO Bool+checkToolchainHealth = do+ ghcVer <- checkBinaryVersion "ghc" ["--numeric-version"]+ cabalVer <- checkBinaryVersion "cabal" ["--numeric-version"]+ hlintVer <- checkBinaryVersion "hlint" ["--numeric-version"]+ tagrefVer <- checkBinaryVersion "tagref" ["--version"]++ let tools =+ [ ToolStatus "GHC Compiler" "9.14.1" ghcVer (ghcVer == Just "9.14.1")+ , ToolStatus "Cabal Tool" "3.18" cabalVer (checkPrefix "3." cabalVer)+ , ToolStatus "HLint Linter" "Clean (>=3.5)" hlintVer (checkPresent hlintVer)+ , ToolStatus "Tagref Engine" "Active (>=1.4)" tagrefVer (checkPresent tagrefVer)+ ]++ TIO.putStr (formatToolchainStatus tools)+ pure (all toolIsCompliant tools)+ where+ checkPrefix pfx (Just v) = pfx `T.isPrefixOf` v+ checkPrefix _ Nothing = False++ checkPresent (Just _) = True+ checkPresent Nothing = False++-- | Inspects an external binary on @PATH@ or fallback paths, executing arguments and returning stdout.+checkBinaryVersion :: String -> [String] -> IO (Maybe Text)+checkBinaryVersion bin args = do+ mbExec <- findExecutable bin >>= \case+ Just p -> pure (Just p)+ Nothing -> do+ home <- getHomeDirectory+ let candidates =+ [ home </> ".local" </> "bin" </> bin+ , home </> ".ghcup" </> "bin" </> bin+ ]+ findExisting candidates+ case mbExec of+ Nothing -> pure Nothing+ Just execPath -> do+ (code, out, _) <- readProcessWithExitCode execPath args "" `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))+ case code of+ ExitSuccess ->+ let firstLine = case T.lines (T.strip (T.pack out)) of+ (l:_) -> l+ [] -> ""+ in pure (if T.null firstLine then Nothing else Just firstLine)+ ExitFailure _ -> pure Nothing+ where+ findExisting [] = pure Nothing+ findExisting (c:cs) = do+ exists <- doesFileExist c+ if exists then pure (Just c) else findExisting cs++-- | Formats toolchain audit results as an aligned ASCII diagnostic table.+formatToolchainStatus :: [ToolStatus] -> Text+formatToolchainStatus tools = T.unlines $+ [ "================================================================================"+ , " Gachi Ambient Toolchain Health Diagnostics"+ , "================================================================================"+ , " Tool | Expected Version | Detected Version | Status"+ , "---------------------+------------------+--------------------------+------------"+ ]+ ++ map formatRow tools+ +++ [ "================================================================================" ]+ where+ formatRow t =+ let nameCol = T.justifyLeft 20 ' ' (toolName t)+ expCol = T.justifyLeft 17 ' ' (toolExpected t)+ detCol = T.justifyLeft 25 ' ' (fromMaybe "Not Found on PATH" (toolDetected t))+ statCol = if toolIsCompliant t then "[+] OK" else "[-] DRIFT"+ in nameCol <> "| " <> expCol <> "| " <> detCol <> "| " <> statCol
+ src/Gachi/CLI/Init.hs view
@@ -0,0 +1,413 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module: Gachi.CLI.Init+-- Description: Core execution engine and interactive wizard for 'gachi init'+--+-- This module implements the operational workflow for 'gachi init', providing+-- turnkey generation of standardized Haskell repositories from Dhall schemas,+-- command-line options, or interactive terminal prompts.+--+-- = Architectural Context & Invariants+--+-- {-+-- Note [CLI Ergonomics and Non-Destructive Initialization]+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+-- [tag:note-cli-init-ergonomics]+--+-- The `gachi init` command serves as the turnkey front door for the entire ecosystem.+-- To satisfy the ecosystem standardization matrix (Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization]), the CLI must uphold four foundational ergonomic invariants:+--+-- 1. Dual-Path Specification:+-- The generator must support both declarative, machine-readable specifications via+-- Dhall configuration files (`--config project.dhall`), and zero-overhead CLI flags /+-- interactive wizardry for rapid prototyping.+--+-- 2. Non-Destructive Safety:+-- File materialization is governed by explicit execution modes ('FailOnExisting',+-- 'SkipExisting', 'OverwriteExisting'). The default mode is 'FailOnExisting' to prevent+-- accidental data loss. Dry-run mode (`--dry-run`) previews all file paths, sizes,+-- and permissions without touching the filesystem.+--+-- 3. Turnkey Git Governance:+-- Immediately upon materializing files, `gachi init` executes hermetic Git initialization+-- unless explicitly disabled via `--no-git`. It pins `core.hooksPath` to `.githooks` and+-- `commit.template` to `.gitmessage`, enforcing Sarutahiko RFC 2822 commit trailer+-- discipline from commit zero.+--+-- 4. Five-Gate Verification Readiness:+-- Any workspace instantiated by `gachi init` must immediately pass all five physical+-- verification gates (`./bin/<project>-verify`) without requiring manual fixes or edits.+-- -}+--+-- = Worked Example+--+-- >>> import Gachi.CLI.Options+-- >>> import Gachi.Scaffold+-- >>> let opts = InitOptions (Just "sample") Nothing (Just "/tmp/sample") FailOnExisting Nothing Nothing Nothing [] True True False+-- >>> exitCode <- runInit opts+-- >>> exitCode == ExitSuccess+-- True+module Gachi.CLI.Init+ ( -- * High-Level Execution Entry Point+ runInit++ -- * Project Resolution Operations+ , resolveProject+ , detectGitAuthor+ , promptUser++ -- * Filesystem Materialization & Git Setup+ , executeScaffolding+ , initializeGitRepository+ ) where++import Control.Exception (SomeException, catch, try)+import Control.Monad (forM_, unless, when)+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import System.Directory+ ( createDirectoryIfMissing+ , doesDirectoryExist+ , doesFileExist+ )+import System.Exit (ExitCode(..))+import System.FilePath ((</>))+import System.IO (hFlush, stderr, stdout)+import System.Process (readProcessWithExitCode)++import Gachi.CLI.Options (InitOptions(..))+import Gachi.Schema.Dhall (decodeProjectFile)+import Gachi.Schema.Project+ ( Author(..)+ , PackageSpec(..)+ , Project(..)+ , authorEmail+ , authorName+ , defaultProject+ , projectAuthor+ , projectDescription+ , projectName+ , projectPackages+ )+import Gachi.Scaffold+ ( FileEntry(..)+ , ScaffoldError(..)+ , ScaffoldResult(..)+ , generateAndWriteScaffold+ , generateProjectScaffold+ )++--------------------------------------------------------------------------------+-- 1. Main Subcommand Dispatcher+--------------------------------------------------------------------------------++-- | Executes the complete 'gachi init' lifecycle.+--+-- Dispatches across Dhall configuration decoding, interactive terminal wizardry,+-- filesystem materialization, and hermetic Git repository setup.+--+-- Returns 'ExitSuccess' upon successful completion, or 'ExitFailure 1' on error.+runInit :: InitOptions -> IO ExitCode+runInit opts = do+ res <- resolveProject opts+ case res of+ Left err -> do+ TIO.hPutStrLn stderr ("[-] Error: " <> err)+ pure (ExitFailure 1)+ Right (proj, targetDir) -> do+ handleScaffold proj targetDir opts++-- | Encapsulates scaffolding execution and exception handling.+--+-- Catches known 'ScaffoldError' exceptions ('DestinationExists' and 'FileWriteFailure')+-- and renders user-friendly diagnostic messages to stderr rather than crashing with an+-- unhandled runtime exception stack trace.+--+-- Returns 'ExitFailure 1' on error, or 'ExitSuccess' upon complete materialization.+handleScaffold :: Project -> FilePath -> InitOptions -> IO ExitCode+handleScaffold proj targetDir opts =+ executeScaffolding proj targetDir opts `catch` handler+ where+ handler :: ScaffoldError -> IO ExitCode+ handler (DestinationExists path) = do+ TIO.hPutStrLn stderr+ ( "[-] Error: Target file or directory already exists: "+ <> T.pack path+ <> "\n Use '--mode overwrite' or '--mode skip' to permit writing to existing trees."+ )+ pure (ExitFailure 1)+ handler (FileWriteFailure path msg) = do+ TIO.hPutStrLn stderr+ ( "[-] Filesystem I/O Error at "+ <> T.pack path+ <> ": "+ <> T.pack msg+ )+ pure (ExitFailure 1)++--------------------------------------------------------------------------------+-- 2. Project Specification Resolution+--------------------------------------------------------------------------------++-- | Resolves a strongly-typed 'Project' specification and target destination directory.+--+-- Evaluates initialization configuration across three strictly prioritized channels:+--+-- 1. __Declarative Dhall File (@--config FILE@):__+-- Evaluates a user-supplied Dhall specification file on disk via 'decodeProjectFile'.+-- Relative schema imports are resolved hermetically relative to the file.+-- Any command-line flags provided simultaneously (@--author-name@, @--desc@, etc.)+-- are overlaid as overrides onto the resulting 'Project'.+--+-- 2. __Interactive Wizard (@--interactive@ / @-i@):__+-- Launches a friendly interactive terminal dialogue prompting for missing or+-- customized parameters, using ambient Git defaults as baseline suggestions.+--+-- 3. __CLI Flags & Ambient Defaults:__+-- When neither a configuration file nor interactive mode is specified, synthesizes+-- a canonical 'Project' using 'defaultProject', reading ambient Git author metadata+-- and applying CLI overrides.+--+-- Returns 'Left error' on missing configuration files or decoding failure, or+-- 'Right (project, targetDirectory)' ready for filesystem materialization.+resolveProject :: InitOptions -> IO (Either Text (Project, FilePath))+resolveProject opts = case initConfigFile opts of+ Just cfgPath -> do+ exists <- doesFileExist cfgPath+ if not exists+ then pure (Left ("Dhall configuration file not found: " <> T.pack cfgPath))+ else do+ try (decodeProjectFile cfgPath) >>= \case+ Left (err :: SomeException) -> pure (Left ("Failed to decode Dhall specification: " <> T.show err))+ Right baseProj -> do+ let proj = applyCliOverrides opts baseProj+ targetDir = case initTargetDir opts of+ Just d -> d+ Nothing -> T.unpack (projectName proj)+ pure (Right (proj, targetDir))+ Nothing+ | initInteractive opts -> do+ (proj, targetDir) <- runInteractiveWizard opts+ pure (Right (proj, targetDir))+ | otherwise -> do+ gitAuthor <- detectGitAuthor+ let pName = fromMaybe "my-project" (initProjectName opts)+ baseProj = defaultProject pName gitAuthor+ proj = applyCliOverrides opts baseProj+ targetDir = case initTargetDir opts of+ Just d -> d+ Nothing -> T.unpack pName+ pure (Right (proj, targetDir))++-- | Overlays optional CLI command-line flags onto an existing 'Project' record.+--+-- Merges CLI flag inputs into the baseline specification according to precedence:+-- * 'initProjectName': Updates canonical project slug 'name' if provided.+-- * 'initDescription': Overrides 'description' if specified.+-- * 'initAuthorName' / 'initAuthorEmail': Overlays contact fields onto 'author'.+-- * 'initPackages': Replaces package list with new 'PackageSpec' records when non-empty.+--+-- Preserves all non-overridden fields (such as pinned toolchain versions, licenses,+-- and git remote mirrors).+applyCliOverrides :: InitOptions -> Project -> Project+applyCliOverrides opts proj =+ let pName = fromMaybe (projectName proj) (initProjectName opts)+ pDesc = fromMaybe (projectDescription proj) (initDescription opts)+ curAuth = projectAuthor proj+ aName = fromMaybe (authorName curAuth) (initAuthorName opts)+ aEmail = fromMaybe (authorEmail curAuth) (initAuthorEmail opts)+ auth = Author { name = aName, email = aEmail }+ pkgs = case initPackages opts of+ [] -> projectPackages proj+ ps -> [ PackageSpec p ("Domain package for " <> p) [] | p <- ps ]+ in proj+ { name = pName+ , description = pDesc+ , author = auth+ , packages = pkgs+ }++-- | Inspects ambient Git user configuration to auto-populate author metadata.+--+-- Queries @git config --get user.name@ and @git config --get user.email@ via+-- 'readProcessWithExitCode'. If either call fails (for example, when Git is not+-- installed or no global Git config exists), falls back to canonical ecosystem+-- defaults:+--+-- * Name: @"Nadia Yvette Chambers"@+-- * Email: @"nadia.yvette.chambers@ik.me"@+--+-- This ensures that automated CI environments and container builds always succeed+-- with reproducible author metadata.+detectGitAuthor :: IO Author+detectGitAuthor = do+ (cName, oName, _) <- readProcessWithExitCode "git" ["config", "--get", "user.name"] ""+ `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))+ (cEmail, oEmail, _) <- readProcessWithExitCode "git" ["config", "--get", "user.email"] ""+ `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))+ let nameTrimmed = T.strip (T.pack oName)+ emailTrimmed = T.strip (T.pack oEmail)+ authName = if cName == ExitSuccess && not (T.null nameTrimmed)+ then nameTrimmed+ else "Nadia Yvette Chambers"+ authEmail = if cEmail == ExitSuccess && not (T.null emailTrimmed)+ then emailTrimmed+ else "nadia.yvette.chambers@ik.me"+ pure Author { name = authName, email = authEmail }++--------------------------------------------------------------------------------+-- 3. Interactive Wizard+--------------------------------------------------------------------------------++-- | Interactive terminal prompt wizard gathering project initialization parameters.+runInteractiveWizard :: InitOptions -> IO (Project, FilePath)+runInteractiveWizard opts = do+ TIO.putStrLn "================================================================================"+ TIO.putStrLn " gachi init — Interactive Workspace Scaffolding Wizard"+ TIO.putStrLn "================================================================================"+ defaultAuth <- detectGitAuthor++ let defaultName = fromMaybe "my-project" (initProjectName opts)+ pName <- promptUser "Project identifier slug" defaultName++ let defaultDesc = fromMaybe "Standardized multi-package Haskell workspace" (initDescription opts)+ pDesc <- promptUser "Project synopsis" defaultDesc++ let defaultAName = fromMaybe (authorName defaultAuth) (initAuthorName opts)+ aName <- promptUser "Author full name" defaultAName++ let defaultAEmail = fromMaybe (authorEmail defaultAuth) (initAuthorEmail opts)+ aEmail <- promptUser "Author email address" defaultAEmail++ let defaultPkgs = case initPackages opts of+ [] -> pName <> "-core"+ ps -> T.intercalate "," ps+ pkgsRaw <- promptUser "Packages to scaffold (comma-separated)" defaultPkgs+ let pkgNames = filter (not . T.null) (map T.strip (T.splitOn "," pkgsRaw))+ pkgSpecs = [ PackageSpec p ("Domain implementation for " <> p) [] | p <- pkgNames ]++ let defaultTarget = maybe pName T.pack (initTargetDir opts)+ targetRaw <- promptUser "Destination directory" defaultTarget++ let proj = Project+ { name = pName+ , description = pDesc+ , license = "EUPL-1.2"+ , author = Author { name = aName, email = aEmail }+ , ghcVersion = "9.14.1"+ , cabalVersion = "3.18"+ , packages = pkgSpecs+ , remotes = []+ }+ pure (proj, T.unpack targetRaw)++-- | Prompts the user on stdout with a default value fallback.+promptUser :: Text -> Text -> IO Text+promptUser label defVal = do+ TIO.putStr (label <> " [" <> defVal <> "]: ")+ hFlush stdout+ line <- TIO.getLine+ let trimmed = T.strip line+ pure (if T.null trimmed then defVal else trimmed)++--------------------------------------------------------------------------------+-- 4. Scaffolding & Filesystem Materialization+--------------------------------------------------------------------------------++-- | Materializes project assets or prints dry-run execution preview.+--+-- Evaluates execution branching:+-- * __Dry-Run Mode ('initDryRun' is True):__+-- Presents an in-memory preview of all planned files, their sizes in characters,+-- and executable permission flags without performing any disk mutations.+-- * __Materialization Mode ('initDryRun' is False):__+-- Creates the destination directory tree, writes files according to 'initMode'+-- ('FailOnExisting', 'SkipExisting', or 'OverwriteExisting'), initializes Git+-- governance unless disabled via 'initNoGit', and outputs the quick-start guide.+--+-- Returns 'ExitSuccess' upon successful execution, or raises 'ScaffoldError'.+executeScaffolding :: Project -> FilePath -> InitOptions -> IO ExitCode+executeScaffolding proj targetDir opts+ | initDryRun opts = do+ let entries = generateProjectScaffold proj+ TIO.putStrLn "--------------------------------------------------------------------------------"+ TIO.putStrLn ("DRY RUN: Simulating workspace generation for '" <> projectName proj <> "'")+ TIO.putStrLn ("Target directory: " <> T.pack targetDir)+ TIO.putStrLn "Planned file materialization:"+ forM_ entries \e -> do+ let execMark = if entryExecutable e then " [executable]" else ""+ size = T.show (T.length (entryContent e))+ TIO.putStrLn (" - " <> T.pack (entryPath e) <> " (" <> size <> " chars)" <> execMark)+ TIO.putStrLn ("Total: " <> T.show (length entries) <> " files planned.")+ TIO.putStrLn "--------------------------------------------------------------------------------"+ pure ExitSuccess+ | otherwise = do+ createDirectoryIfMissing True targetDir+ res <- generateAndWriteScaffold targetDir (initMode opts) proj+ TIO.putStrLn ("[+] Successfully materialized " <> T.show (length (writtenFiles res)) <> " files in " <> T.pack targetDir)+ unless (null (skippedFiles res)) $ do+ TIO.putStrLn ("[!] Skipped " <> T.show (length (skippedFiles res)) <> " pre-existing files (mode: SkipExisting)")++ unlessNoGit (initNoGit opts) $ do+ initializeGitRepository targetDir++ printNextSteps proj targetDir+ pure ExitSuccess+ where+ unlessNoGit True _ = pure ()+ unlessNoGit False act = act++-- | Configures canonical Git governance (.githooks and .gitmessage) within target directory.+--+-- Executes hermetic Git setup steps:+-- 1. Checks if a @.git@ directory exists; if not, invokes @git init <targetDir>@.+-- 2. Sets local repository configuration @core.hooksPath = .githooks@ to activate+-- the Sarutahiko RFC 2822 commit message hook and pre-commit validation hook.+-- 3. Sets @commit.template = .gitmessage@ to standardize commit metadata prompts.+--+-- Silently handles failures if the @git@ executable is absent, allowing offline+-- workspace bootstrapping to proceed gracefully.+initializeGitRepository :: FilePath -> IO ()+initializeGitRepository targetDir = do+ let dotGit = targetDir </> ".git"+ gitExists <- doesDirectoryExist dotGit+ unless gitExists $ do+ (cInit, _, _) <- readProcessWithExitCode "git" ["init", targetDir] ""+ `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))+ when (cInit == ExitSuccess) $ do+ TIO.putStrLn " [+] Initialized new Git repository."++ -- Configure Git hooks and commit template+ (cHooks, _, _) <- readProcessWithExitCode "git" ["-C", targetDir, "config", "core.hooksPath", ".githooks"] ""+ `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))+ (cTmpl, _, _) <- readProcessWithExitCode "git" ["-C", targetDir, "config", "commit.template", ".gitmessage"] ""+ `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))++ when (cHooks == ExitSuccess && cTmpl == ExitSuccess) $ do+ TIO.putStrLn " [+] Configured Git governance (.githooks and .gitmessage commit trailer template)."++-- | Outputs standard quick-start instructions for the newly scaffolded project.+--+-- Displays terminal navigation commands and the canonical verification script+-- invocation (@./bin/<project>-verify@) to guide the developer immediately to+-- successful compilation and test execution.+printNextSteps :: Project -> FilePath -> IO ()+printNextSteps proj targetDir = do+ let pName = projectName proj+ TIO.putStrLn "================================================================================"+ TIO.putStrLn (" Workspace '" <> pName <> "' is ready!")+ TIO.putStrLn ""+ TIO.putStrLn " Quick Start:"+ TIO.putStrLn (" cd " <> T.pack targetDir)+ TIO.putStrLn " cabal build all"+ TIO.putStrLn " cabal test all"+ TIO.putStrLn (" ./bin/" <> pName <> "-verify")+ TIO.putStrLn "================================================================================"
+ src/Gachi/CLI/Options.hs view
@@ -0,0 +1,470 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Module: Gachi.CLI.Options+-- Description: Command-line option parsing and data structures for gachi+--+-- This module defines the complete command hierarchy, flags, execution modes,+-- and applicative parsers for the unified @gachi@ command-line interface.+--+-- = Architectural Context & Invariants+--+-- Per Note [CLI Ergonomics and Non-Destructive Initialization]+-- [ref:note-cli-init-ergonomics], the command-line interface balances declarative+-- precision (loading typed Dhall schemas) with rapid-prototyping convenience+-- (CLI flags and interactive prompts).+--+-- == Command Hierarchy and Strategic Roadmap+--+-- The Gachi CLI establishes five primary operational subcommands designed to+-- govern the entire repository lifecycle across the ecosystem:+--+-- 1. __Workspace Initialization ('init'):__+-- Bootstraps a complete, standardized multi-package repository skeleton adhering to+-- all five verification gates immediately upon creation (Milestone 2.3).+-- Supports declarative Dhall files (@--config FILE@), CLI flag overrides, and+-- an interactive step-by-step terminal wizard (@--interactive@ / @-i@).+--+-- 2. __Harmonic Configuration Propagation ('sync'):__+-- Propagates unified toolchain pins, linters, pre-commit hooks, and verification+-- harnesses across heterogeneous repositories (Phase 3). Evaluates differential+-- changes using a bidirectional merge strategy without manual editing.+--+-- 3. __Non-Destructive Codebase Retrofitting ('retrofit'):__+-- Migrates legacy or non-standard Haskell repositories into the canonical Gachi+-- multi-package workspace layout (Phase 4). Preserves existing business logic while+-- inserting the universal verification infrastructure.+--+-- 4. __Roadmap & Snapshot Governance ('plan'):__+-- Interacts with detached Git storage and immutable ledger checkpoints (Phase 5).+-- Maintains atomic session states, branch histories, and multi-step rollbacks.+--+-- 5. __Version & Toolchain Diagnostics ('version'):__+-- Emits detailed build metadata, including active GHC compiler pinning (GHC 9.14.1),+-- Cabal specification target (Cabal 3.18), and licensing status (EUPL-1.2).+--+-- == Safety & Verification Invariants+--+-- * __Fail-Safe Defaults:__ All filesystem mutations default to 'FailOnExisting'+-- mode, preventing accidental clobbering of uncommitted user files or directories.+-- * __Dry-Run Capability:__ Every mutating subcommand provides a @--dry-run@ flag+-- enabling developers to inspect planned actions and file trees prior to execution.+-- * __Total Failure Modes:__ Option parsers fail cleanly with informative usage+-- diagnostics on invalid inputs rather than terminating with runtime exceptions.+--+-- = Worked Example+--+-- >>> import Options.Applicative+-- >>> let parseArgs = execParserPure defaultPrefs (info (commandParser "0.1.0.0") idm)+-- >>> case parseArgs ["init", "my-project", "--no-git"] of { Success (CmdInit opts) -> initNoGit opts; _ -> False }+-- True+module Gachi.CLI.Options+ ( -- * High-Level CLI Command Types+ Command(..)+ , InitOptions(..)+ , SyncOptions(..)+ , DoctorOptions(..)+ , RetrofitOptions(..)+ , PlanOptions(..)++ -- * Mode and List Parsers+ , parseMode+ , parsePackageList++ -- * Optparse-Applicative Parsers+ , commandParser+ , initOptionsParser+ , syncOptionsParser+ , doctorOptionsParser+ , retrofitOptionsParser+ , planOptionsParser+ ) where++import Data.Char (toLower)+import Data.Text (Text)+import qualified Data.Text as T+import Options.Applicative+ ( Parser+ , ParserInfo+ , command+ , eitherReader+ , fullDesc+ , header+ , help+ , helper+ , hsubparser+ , info+ , long+ , many+ , metavar+ , option+ , optional+ , progDesc+ , short+ , strArgument+ , strOption+ , switch+ , value+ )++import Gachi.Scaffold (ScaffoldMode(..))++--------------------------------------------------------------------------------+-- 1. Command-Line AST & Option Types+--------------------------------------------------------------------------------++-- | Top-level command dispatcher covering all ecosystem operational subcommands.+data Command+ = CmdInit !InitOptions+ -- ^ Initialize a new standardized multi-package workspace ('gachi init').+ | CmdSync !SyncOptions+ -- ^ Harmonically synchronize configuration across repositories ('gachi sync').+ | CmdDoctor !DoctorOptions+ -- ^ Diagnose workspace health, toolchain binaries, and 1-D cellular sheaf coherence ('gachi doctor').+ | CmdRetrofit !RetrofitOptions+ -- ^ Non-destructively retrofit an existing codebase ('gachi retrofit').+ | CmdPlan !PlanOptions+ -- ^ Manage ecosystem strategic roadmaps and rollback ('gachi plan').+ | CmdVersion+ -- ^ Print toolchain version, GHC pinning, and licensing stanzas.+ deriving stock (Eq, Show)++-- | Configuration parameters controlling the 'gachi init' scaffolding engine.+--+-- Governs pure specification resolution, terminal interaction, target destination paths,+-- filesystem write modes, and automated Git governance bootstrapping.+data InitOptions = InitOptions+ { initProjectName :: !(Maybe Text)+ -- ^ Positional project name (e.g., @"gongju"@). When supplied, defines the canonical+ -- project identifier, directory basename, Cabal workspace name, and verification harness slug.+ , initConfigFile :: !(Maybe FilePath)+ -- ^ Optional path to declarative Dhall configuration file (@--config FILE@ / @-c FILE@).+ -- When specified, the schema is hermetically evaluated from disk, superseding positional defaults.+ , initTargetDir :: !(Maybe FilePath)+ -- ^ Destination directory path (@--target-dir DIR@ / @-d DIR@).+ -- Defaults to @./<project-name>@ when omitted. Created recursively if non-existent.+ , initMode :: !ScaffoldMode+ -- ^ Filesystem write mode: 'FailOnExisting' (abort on collision), 'SkipExisting' (preserve+ -- pre-existing files untouched), or 'OverwriteExisting' (clobber existing destination files).+ , initAuthorName :: !(Maybe Text)+ -- ^ Override for author full personal or organizational name (@--author-name NAME@).+ -- Overrides values detected from ambient Git configuration (@user.name@).+ , initAuthorEmail :: !(Maybe Text)+ -- ^ Override for author contact email address (@--author-email EMAIL@).+ -- Overrides values detected from ambient Git configuration (@user.email@).+ , initDescription :: !(Maybe Text)+ -- ^ Override for high-level project synopsis or mission description (@--desc TEXT@).+ -- Embedded directly in generated README.md, cabal files, and package documentation.+ , initPackages :: ![Text]+ -- ^ Optional comma-separated list of satellite packages to instantiate in the workspace.+ -- Each package receives a dedicated cabal manifest, test suite, and module hierarchy.+ , initNoGit :: !Bool+ -- ^ Skip hermetic Git repository initialization and .githooks configuration (@--no-git@).+ -- Intended for generating sub-trees inside existing Git working copies or container sandboxes.+ , initDryRun :: !Bool+ -- ^ Preview file tree generation and permissions without writing to disk (@--dry-run@).+ -- Prints an audit table containing relative paths, character counts, and executable marks.+ , initInteractive :: !Bool+ -- ^ Interactively prompt for missing project parameters (@--interactive@ / @-i@).+ -- Launches terminal wizard offering intelligent defaults drawn from ambient Git configuration.+ } deriving stock (Eq, Show)++-- | Configuration parameters controlling Phase 3 'gachi sync'.+--+-- Used to synchronize standardized ecosystem configuration across disparate repositories.+data SyncOptions = SyncOptions+ { syncTargetDir :: !(Maybe FilePath)+ -- ^ Target repository checkout directory to synchronize (@--target DIR@ / @-t DIR@).+ -- Defaults to current working directory when omitted.+ , syncDryRun :: !Bool+ -- ^ Preview differential synchronization changes without mutating filesystem (@--dry-run@).+ -- Displays unified diffs and file permission updates planned by the synchronization engine.+ , syncAll :: !Bool+ -- ^ Discover and synchronize all sibling repositories across the ecosystem parent directory (@--all@ / @-a@).+ , syncRemotes :: !Bool+ -- ^ Audit and synchronize git remote replication topology (@--remotes@ / @-r@).+ } deriving stock (Eq, Show)++-- | Configuration parameters controlling the 'gachi doctor' diagnostic command.+--+-- Evaluates ambient system toolchains, repository health, and executes 1-D cellular+-- sheaf cohomology audits to detect and localize cross-repository contract drift.+data DoctorOptions = DoctorOptions+ { doctorTargetDir :: !(Maybe FilePath)+ -- ^ Target repository directory to inspect (@--target DIR@ / @-t DIR@).+ -- Defaults to current working directory when omitted.+ , doctorSheaf :: !Bool+ -- ^ Run 1-D cellular sheaf cohomology coherence audit (@--sheaf@ / @-s@).+ , doctorAll :: !Bool+ -- ^ Discover and audit all sibling repositories across ecosystem parent directory (@--all@ / @-a@).+ } deriving stock (Eq, Show)++-- | Configuration parameters controlling Phase 4 'gachi retrofit'.+--+-- Used to migrate legacy or ad-hoc Haskell codebases into the standardized ecosystem hierarchy.+data RetrofitOptions = RetrofitOptions+ { retrofitTargetDir :: !(Maybe FilePath)+ -- ^ Legacy repository checkout directory to inspect and overlay (@--target DIR@ / @-t DIR@).+ -- Defaults to current working directory when omitted.+ , retrofitDryRun :: !Bool+ -- ^ Preview retrofitting actions without modifying existing source files (@--dry-run@).+ -- Reports missing verification harnesses, hooks, and documentation skeletons.+ , retrofitMarkdown :: !Bool+ -- ^ Output diagnostic report in GitHub-Flavored Markdown format (@--markdown@ / @-m@).+ } deriving stock (Eq, Show)++-- | Configuration parameters controlling Phase 5 'gachi plan'.+--+-- Interacts with immutable ledger checkpoints and atomic rollback via detached git storage.+data PlanOptions = PlanOptions+ { planSubcommand :: !Text+ -- ^ Plan governance subcommand (e.g. @"checkpoint"@, @"diff"@, @"log"@, @"rollback"@).+ -- Dictates state store transitions and audit trail queries.+ , planArgs :: ![Text]+ -- ^ Additional positional or keyword arguments supplied to the plan action.+ -- Passed verbatim to the underlying detached Git storage engine.+ } deriving stock (Eq, Show)++--------------------------------------------------------------------------------+-- 2. Parsing Helpers+--------------------------------------------------------------------------------++-- | Parses a textual write mode into a canonical 'ScaffoldMode'.+--+-- Accepts case-insensitive strings: @"fail"@, @"skip"@, or @"overwrite"@.+parseMode :: String -> Either String ScaffoldMode+parseMode raw = case map toLower raw of+ "fail" -> Right FailOnExisting+ "skip" -> Right SkipExisting+ "overwrite" -> Right OverwriteExisting+ other -> Left ("Invalid write mode '" ++ other ++ "'. Expected 'fail', 'skip', or 'overwrite'.")++-- | Parses a comma-delimited string of package names into a cleaned list.+parsePackageList :: String -> Either String [Text]+parsePackageList raw =+ let pieces = map T.strip (T.splitOn "," (T.pack raw))+ filtered = filter (not . T.null) pieces+ in Right filtered++--------------------------------------------------------------------------------+-- 3. Top-Level Optparse Parser+--------------------------------------------------------------------------------++-- | Constructs the top-level 'ParserInfo' for the @gachi@ executable.+commandParser :: Text -> ParserInfo Command+commandParser ver =+ info (helper <*> mainParser)+ ( fullDesc+ <> header ("gachi v" ++ T.unpack ver ++ " — Ecosystem Skeleton Generator & Chattering Synchronizer")+ <> progDesc "Declarative Haskell project scaffolding, configuration propagation, and plan governance."+ )++-- | Dispatches across CLI subcommands.+mainParser :: Parser Command+mainParser = hsubparser+ ( command "init"+ (info (CmdInit <$> initOptionsParser)+ (progDesc "Initialize a new standardized multi-package Haskell workspace or satellite package"))+ <> command "sync"+ (info (CmdSync <$> syncOptionsParser)+ (progDesc "Harmonically synchronize configuration and governance stanzas across repositories (Phase 3)"))+ <> command "doctor"+ (info (CmdDoctor <$> doctorOptionsParser)+ (progDesc "Diagnose workspace health, toolchain binaries, and 1-D cellular sheaf coherence (Milestone 3.5)"))+ <> command "retrofit"+ (info (CmdRetrofit <$> retrofitOptionsParser)+ (progDesc "Non-destructively retrofit an existing codebase into the canonical ecosystem skeleton (Phase 4)"))+ <> command "plan"+ (info (CmdPlan <$> planOptionsParser)+ (progDesc "Manage ecosystem strategic roadmaps and atomic rollback via detached git storage (Phase 5)"))+ <> command "version"+ (info (pure CmdVersion)+ (progDesc "Display gachi version, compiler pinning, and licensing stanzas"))+ )++--------------------------------------------------------------------------------+-- 4. Subcommand Parsers+--------------------------------------------------------------------------------++-- | Applicative parser for 'InitOptions'.+--+-- Assembles options governing the 'gachi init' subcommand:+-- * Positional argument: Optional project identifier slug (e.g. @"gongju"@).+-- * Configuration flag (@--config@ / @-c@): Path to declarative Dhall schema file.+-- * Target directory (@--target-dir@ / @-d@): Output path (defaults to @./<project-name>@).+-- * Write mode (@--mode@ / @-m@): Safety policy ('FailOnExisting', 'SkipExisting', 'OverwriteExisting').+-- * Metadata overrides: @--author-name@, @--author-email@, and @--desc@.+-- * Satellite packages (@--packages@): Comma-delimited list of packages to instantiate.+-- * Operational switches: @--no-git@ (skip VCS setup), @--dry-run@ (simulation), @-i@ (interactive wizard).+initOptionsParser :: Parser InitOptions+initOptionsParser = InitOptions+ <$> optional+ ( strArgument+ ( metavar "PROJECT_NAME"+ <> help "Name of the project to initialize (e.g., 'gongju')"+ )+ )+ <*> optional+ ( strOption+ ( long "config"+ <> short 'c'+ <> metavar "FILE"+ <> help "Path to declarative Dhall project specification file (.dhall)"+ )+ )+ <*> optional+ ( strOption+ ( long "target-dir"+ <> short 'd'+ <> metavar "DIR"+ <> help "Destination directory for generated workspace (defaults to ./<project-name>)"+ )+ )+ <*> option (eitherReader parseMode)+ ( long "mode"+ <> short 'm'+ <> metavar "MODE"+ <> value FailOnExisting+ <> help "Filesystem write mode: 'fail', 'skip', or 'overwrite' (default: 'fail')"+ )+ <*> optional+ ( strOption+ ( long "author-name"+ <> metavar "NAME"+ <> help "Author full name override"+ )+ )+ <*> optional+ ( strOption+ ( long "author-email"+ <> metavar "EMAIL"+ <> help "Author email address override"+ )+ )+ <*> optional+ ( strOption+ ( long "desc"+ <> metavar "TEXT"+ <> help "High-level project synopsis or description override"+ )+ )+ <*> option (eitherReader parsePackageList)+ ( long "packages"+ <> metavar "PKG1,PKG2,..."+ <> value []+ <> help "Comma-separated list of satellite packages to instantiate"+ )+ <*> switch+ ( long "no-git"+ <> help "Skip Git repository initialization and .githooks configuration"+ )+ <*> switch+ ( long "dry-run"+ <> help "Simulate scaffolding generation and print summary without writing to disk"+ )+ <*> switch+ ( long "interactive"+ <> short 'i'+ <> help "Interactively prompt for missing configuration values"+ )++-- | Applicative parser for 'SyncOptions'.+--+-- Parses flags for cross-repository configuration synchronization (Phase 3):+-- * Target repository directory (@--target@ / @-t@).+-- * Dry-run simulation switch (@--dry-run@).+-- * All repositories discovery switch (@--all@ / @-a@).+syncOptionsParser :: Parser SyncOptions+syncOptionsParser = SyncOptions+ <$> optional+ ( strOption+ ( long "target"+ <> short 't'+ <> metavar "DIR"+ <> help "Target repository directory to synchronize"+ )+ )+ <*> switch+ ( long "dry-run"+ <> help "Preview synchronization diffs without mutating filesystem"+ )+ <*> switch+ ( long "all"+ <> short 'a'+ <> help "Discover and synchronize all repositories across the ecosystem parent directory"+ )+ <*> switch+ ( long "remotes"+ <> short 'r'+ <> help "Audit and synchronize git remote replication topology"+ )++-- | Applicative parser for 'DoctorOptions'.+--+-- Parses flags for workspace diagnostics and cellular sheaf coherence auditing (Milestone 3.5):+-- * Target repository directory (@--target@ / @-t@).+-- * Cellular sheaf coherence audit switch (@--sheaf@ / @-s@).+-- * All ecosystem repositories switch (@--all@ / @-a@).+doctorOptionsParser :: Parser DoctorOptions+doctorOptionsParser = DoctorOptions+ <$> optional+ ( strOption+ ( long "target"+ <> short 't'+ <> metavar "DIR"+ <> help "Target repository directory to diagnose (defaults to current directory)"+ )+ )+ <*> switch+ ( long "sheaf"+ <> short 's'+ <> help "Evaluate 1-D cellular sheaf coherence and localize H^1 boundary obstructions"+ )+ <*> switch+ ( long "all"+ <> short 'a'+ <> help "Audit all discovered ecosystem repositories across the parent directory"+ )++-- | Applicative parser for 'RetrofitOptions'.+--+-- Parses flags for non-destructive legacy codebase migration (Phase 4):+-- * Target codebase directory (@--target@ / @-t@).+-- * Dry-run simulation switch (@--dry-run@).+retrofitOptionsParser :: Parser RetrofitOptions+retrofitOptionsParser = RetrofitOptions+ <$> optional+ ( strOption+ ( long "target"+ <> short 't'+ <> metavar "DIR"+ <> help "Legacy repository directory to inspect and overlay"+ )+ )+ <*> switch+ ( long "dry-run"+ <> help "Preview retrofitting actions without modifying existing files"+ )+ <*> switch+ ( long "markdown"+ <> short 'm'+ <> help "Format diagnostic inspection report as GitHub-Flavored Markdown"+ )++-- | Applicative parser for 'PlanOptions'.+--+-- Parses commands and arguments for detached Git state store management (Phase 5):+-- * Action subcommand (@checkpoint@, @diff@, @log@, @rollback@).+-- * Trailing positional arguments supplied directly to the plan engine.+planOptionsParser :: Parser PlanOptions+planOptionsParser = PlanOptions+ <$> strArgument+ ( metavar "SUBCOMMAND"+ <> help "Plan action: checkpoint, diff, log, or rollback"+ )+ <*> many+ ( strArgument+ ( metavar "ARGS..."+ <> help "Arguments supplied to the plan action"+ )+ )
+ src/Gachi/CLI/Retrofit.hs view
@@ -0,0 +1,166 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module: Gachi.CLI.Retrofit+-- Description: Command execution engine for 'gachi retrofit'+--+-- This module coordinates the diagnostic inspection and non-destructive overlay+-- workflows for the @gachi retrofit@ subcommand (Milestones 4.1 & 4.2, Phase 4).+--+-- = Architectural Context & Invariants+--+-- Per Note [Non-Destructive Skeleton Overlay] [ref:note-nondestructive-retrofit],+-- retrofitting operations adopt legacy, single-package, or external repositories+-- into the canonical ecosystem skeleton without altering domain code.+--+-- == Operational Workflow+--+-- The retrofitting execution lifecycle unfolds in two stages:+--+-- 1. __Diagnostic Inspection (Milestone 4.1):__+-- Executes the comprehensive 'DiagnosticReport' scanner, evaluating:+-- - Programming language distribution and file percentages.+-- - Build tool manifests and workspace topography.+-- - Test infrastructure and declared test suites.+-- - Documentation gaps relative to @DOC_STRATEGY.md@.+-- - Git governance, hook executability, and RFC 2822 @Assisted-by:@ trailer validation.+-- - Physical verification harnesses and quality gates.+--+-- 2. __Non-Destructive Overlay (Milestone 4.2):__+-- Computes the 'OverlayPlan', categorizing missing standardized assets as 'ActionCreate'+-- and pre-existing files as 'ActionPreserve'. In live execution mode, writes missing+-- documentation, agent rules, Git hooks, and verification runners with executable permissions+-- and configures Git governance without touching domain source files.+--+-- == Formatting & Execution Modes+--+-- * __Standard Terminal Mode (Default):__ Emits an 80-column aligned ASCII terminal+-- dashboard featuring boxed sections, Unicode score progress bars, and aligned checklists.+-- * __Markdown Mode (@--markdown@ / @-m@):__ Emits clean GitHub-Flavored Markdown tables+-- suitable for piping into @RETROFIT_PLAN.md@ or CI workflow artifacts.+-- * __Simulation Mode (@--dry-run@):__ Previews the diagnostic inspection report and+-- the proposed non-destructive overlay plan without modifying any filesystem files.+-- * __Materialization Mode (Live):__ Materializes the overlay plan and activates Git+-- governance hooks in the target repository.+--+-- = Worked Example+--+-- >>> import Gachi.CLI.Options (RetrofitOptions(..))+-- >>> let opts = RetrofitOptions (Just ".") True False+-- >>> code <- runRetrofit opts+-- >>> code == ExitSuccess+-- True+module Gachi.CLI.Retrofit+ ( -- * High-Level Subcommand Runner+ runRetrofit++ -- * Operational Execution Routines+ , runInspectionAndOverlay+ ) where++import Control.Exception (SomeException, catch)+import Control.Monad (unless)+import Data.Maybe (fromMaybe)+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import System.Directory (doesDirectoryExist)+import System.Exit (ExitCode(..))+import System.IO (stderr)++import Gachi.CLI.Options (RetrofitOptions(..))+import Gachi.Retrofit+ ( OverlayResult(..)+ , computeOverlayPlan+ , evaluateQualityGates+ , executeOverlayPlan+ , formatDiagnosticMarkdown+ , formatDiagnosticReport+ , formatOverlayPlan+ , formatOverlayPlanMarkdown+ , formatOverlayResult+ , formatQualityGateMarkdown+ , formatQualityGateReport+ , scanRepository+ )++--------------------------------------------------------------------------------+-- 1. High-Level Subcommand Runner+--------------------------------------------------------------------------------++-- | Executes the @gachi retrofit@ subcommand from parsed 'RetrofitOptions'.+--+-- === Execution Steps+--+-- 1. Resolves target directory from 'retrofitTargetDir', defaulting to current working+-- directory (@.@) when omitted.+-- 2. Verifies physical existence of the target directory on disk, failing cleanly+-- with 'ExitFailure 1' if missing.+-- 3. Dispatches to 'runInspectionAndOverlay' to execute diagnostic analysis, compute+-- the non-destructive overlay plan, and optionally apply disk materializations.+--+-- === Exit Codes+--+-- * 'ExitSuccess': Inspection and overlay completed successfully.+-- * 'ExitFailure 1': Target directory does not exist or was inaccessible.+runRetrofit :: RetrofitOptions -> IO ExitCode+runRetrofit opts = do+ let targetDir = fromMaybe "." (retrofitTargetDir opts)+ isDir <- doesDirectoryExist targetDir `catch` (\(_ :: SomeException) -> pure False)+ if not isDir+ then do+ TIO.hPutStrLn stderr ("gachi retrofit error: Target directory '" <> T.pack targetDir <> "' does not exist.")+ pure (ExitFailure 1)+ else runInspectionAndOverlay targetDir (retrofitDryRun opts) (retrofitMarkdown opts)++--------------------------------------------------------------------------------+-- 2. Operational Routines+--------------------------------------------------------------------------------++-- | Performs the static diagnostic analysis of the target directory, computes+-- the non-destructive overlay plan, and conditionally executes the materialization.+--+-- Observes Note [Non-Destructive Skeleton Overlay] [ref:note-nondestructive-retrofit]:+-- in simulation mode (@--dry-run@), displays the proposed overlay actions without disk mutations;+-- in materialization mode, writes missing standardized assets and pins Git governance.+runInspectionAndOverlay :: FilePath -> Bool -> Bool -> IO ExitCode+runInspectionAndOverlay targetDir isDryRun isMarkdown = do+ -- 1. Milestone 4.1 Diagnostic Inspection+ report <- scanRepository targetDir+ if isMarkdown+ then TIO.putStr (formatDiagnosticMarkdown report)+ else TIO.putStr (formatDiagnosticReport report)++ -- 2. Milestone 4.2 Non-Destructive Overlay Plan Computation+ plan <- computeOverlayPlan targetDir report++ -- 3. Execution or Simulation Branching+ if isDryRun+ then do+ if isMarkdown+ then TIO.putStr (formatOverlayPlanMarkdown plan)+ else TIO.putStr (formatOverlayPlan plan)+ TIO.putStrLn " [*] Simulation mode (--dry-run): No filesystem changes planned or applied."+ else do+ unlessMarkdown $ TIO.putStr (formatOverlayPlan plan)+ result <- executeOverlayPlan plan+ if isMarkdown+ then do+ TIO.putStrLn ""+ TIO.putStrLn "## Overlay Materialization Result"+ TIO.putStrLn ""+ TIO.putStrLn ("- **Files Created:** " <> T.pack (show (length (resultCreatedFiles result))))+ TIO.putStrLn ("- **Files Preserved:** " <> T.pack (show (length (resultPreservedFiles result))))+ TIO.putStrLn ("- **Git Actions:** " <> T.pack (show (length (resultGitActions result))))+ else TIO.putStr (formatOverlayResult result)+ TIO.putStrLn " [+] Milestone 4.2 Non-Destructive Skeleton Overlay applied successfully."++ -- 4. Milestone 4.3 Progressive Quality Gate Evaluation+ qgReport <- evaluateQualityGates targetDir+ if isMarkdown+ then TIO.putStr (formatQualityGateMarkdown qgReport)+ else TIO.putStr (formatQualityGateReport qgReport)++ pure ExitSuccess+ where+ unlessMarkdown = unless isMarkdown
+ src/Gachi/CLI/Sync.hs view
@@ -0,0 +1,373 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module: Gachi.CLI.Sync+-- Description: Command execution engine for 'gachi sync'+--+-- This module implements the operational workflow for the @gachi sync@ subcommand.+-- It coordinates differential scanning, plan calculation, and atomic propagation+-- across single repositories or the entire multi-repository ecosystem.+--+-- = Architectural Context & Invariants+--+-- In accordance with Note [Harmonic Chattering Synchronization and Differential Propagation]+-- [ref:note-harmonic-sync] and Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization], @gachi sync@ provides the automated mechanism+-- that brings disparate repositories into structural resonance with the canonical+-- Standardization Matrix.+--+-- == Operational Workflow+--+-- The synchronization command executes through the following phased sequence:+--+-- 1. __Target Resolution:__+-- Resolves whether the user intends to synchronize a single target repository+-- (the default, pointing to current working directory or explicit @--target@)+-- or perform ecosystem-wide orchestration via @--all@.+--+-- 2. __Differential Scan & Plan Construction:__+-- Evaluates each target checkout against the 9-dimensional canonical matrix+-- ('canonicalMatrix'), computing a pure 'SyncPlan' that categorizes every asset+-- into 'ActionCreate', 'ActionUpdate', 'ActionChmod', or 'ActionNoOp'.+--+-- 3. __Execution Mode Enforcement:__+-- * __'DryRun' (@--dry-run@):__ Emits ASCII audit tables detailing proposed+-- creations, diffs, and permission adjustments without mutating disk.+-- * __'ApplySync' (Default):__ Materializes missing files, writes updated+-- canonical contents, and sets owner executable bits where required.+--+-- 4. __Idempotency Verification:__+-- All updates are deterministic and mathematically idempotent. A secondary+-- invocation over a synchronized workspace results in zero modifications.+--+-- = Worked Example+--+-- >>> import Gachi.CLI.Options (SyncOptions(..))+-- >>> let opts = SyncOptions (Just "/home/nyc/src/gachi") True False+-- >>> code <- runSync opts+-- >>> code == ExitSuccess+-- True+module Gachi.CLI.Sync+ ( -- * High-Level Subcommand Runner+ runSync++ -- * Operational Execution Routines+ , runSingleSync+ , runEcosystemSync+ , runRemoteSync+ , runEcosystemRemoteSync++ -- * Summary Formatting+ , formatEcosystemSummary+ ) where++import Control.Monad (forM, forM_, unless, when)+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import System.Directory (doesDirectoryExist)+import System.Exit (ExitCode(..))+import System.FilePath (takeFileName)+import System.IO (stderr)++import Gachi.CLI.Options (SyncOptions(..))+import Gachi.Sync+ ( StandardizationMatrix+ , SyncMode(..)+ , SyncResult(..)+ , canonicalMatrix+ , computeRemoteSyncPlan+ , discoverEcosystemRepositories+ , formatRemoteReport+ , formatRemoteSyncPlan+ , formatSyncResult+ , planRemoteActions+ , syncEcosystem+ , syncRemoteTopologyQuick+ , syncRepositoryQuick+ )++--------------------------------------------------------------------------------+-- 1. High-Level Subcommand Runner+--------------------------------------------------------------------------------++-- | Executes the @gachi sync@ subcommand from parsed 'SyncOptions'.+--+-- === Dispatch Strategy & Flags+--+-- 1. __Execution Mode Selection:__+-- Evaluates 'syncDryRun':+-- * When @--dry-run@ is specified, selects 'DryRun' mode, performing read-only+-- cryptographic differential auditing without disk mutation.+-- * Otherwise, selects 'ApplySync' mode to write canonical contents and enforce+-- executable bit permissions.+--+-- 2. __Scope Resolution:__+-- * __Remote Topology Synchronization (@--remotes@ / @-r@):__+-- Delegates to 'runRemoteSync' (or 'runEcosystemRemoteSync' when @--all@ is active),+-- auditing configured Git remotes against Note [Multi-Remote Replication Topology and Provider Compliance]+-- [ref:note-remote-topology] and reconciling missing or drifted remotes.+-- * __Ecosystem Scope (@--all@ / @-a@):__+-- Delegates to 'runEcosystemSync', discovering all sibling repositories in the+-- target directory and harmonizing each discovered checkout in sequence.+-- * __Targeted Repository Scope (Default):__+-- Delegates to 'runSingleSync', synchronizing only the specified directory.+--+-- 3. __Exit Code Normalization:__+-- * Returns 'ExitSuccess' when all planned synchronization tasks conclude without+-- disk writing exceptions or unparseable schema files.+-- * Returns 'ExitFailure 1' if unrecoverable filesystem errors or schema errors occur.+runSync :: SyncOptions -> IO ExitCode+runSync opts = do+ let mode = if syncDryRun opts then DryRun else ApplySync+ target = fromMaybe "." (syncTargetDir opts)+ if syncRemotes opts+ then if syncAll opts+ then runEcosystemRemoteSync mode target+ else runRemoteSync mode target+ else if syncAll opts+ then runEcosystemSync mode target canonicalMatrix+ else runSingleSync mode target++--------------------------------------------------------------------------------+-- 2. Single Repository Synchronization+--------------------------------------------------------------------------------++-- | Synchronizes a single repository checkout against 'canonicalMatrix'.+--+-- Automatically locates and decodes @project.dhall@ if present, or infers+-- default project identity from directory naming. Emits detailed diagnostic+-- ASCII summary tables describing all executed or simulated actions.+--+-- === Operational Safety+--+-- * Checks for target directory existence prior to performing any operations.+-- * Delegates to 'syncRepositoryQuick', which runs under pure SHA-256 differential+-- scanning.+-- * Emits the formatted plan/result table via 'formatSyncResult'.+-- * Returns 'ExitSuccess' if all actions succeeded, or 'ExitFailure 1' on errors.+runSingleSync :: SyncMode -> FilePath -> IO ExitCode+runSingleSync mode targetDir = do+ exists <- doesDirectoryExist targetDir+ if not exists+ then do+ TIO.hPutStrLn stderr ("Target directory does not exist: " <> T.pack targetDir)+ pure (ExitFailure 1)+ else do+ syncRepositoryQuick mode targetDir >>= \case+ Left err -> do+ TIO.hPutStrLn stderr ("Synchronization failed for " <> T.pack targetDir <> ": " <> err)+ pure (ExitFailure 1)+ Right result -> do+ TIO.putStrLn (formatSyncResult result)+ pure if resultSuccess result then ExitSuccess else ExitFailure 1++--------------------------------------------------------------------------------+-- 3. Ecosystem-Wide Multi-Repository Orchestration+--------------------------------------------------------------------------------++-- | Discovers and harmonically synchronizes all repositories in a parent directory.+--+-- Scans the specified parent directory for sibling repository checkouts, evaluates+-- differential synchronization plans for each repository, and renders a consolidated+-- audit matrix detailing the health and synchronization status across the ecosystem.+--+-- === Discovery and Batch Processing+--+-- 1. Verifies existence of the parent directory.+-- 2. Discovers child repositories via 'discoverEcosystemRepositories', identifying+-- folders containing @.git@, @cabal.project@, or @project.dhall@.+-- 3. Displays discovered repository paths and begins sequential synchronization.+-- 4. Emits a consolidated ecosystem table via 'formatEcosystemSummary'.+-- 5. Returns 'ExitSuccess' if every repository synchronized without error, or+-- 'ExitFailure 1' if any repository encountered failures.+runEcosystemSync :: SyncMode -> FilePath -> StandardizationMatrix -> IO ExitCode+runEcosystemSync mode parentDir matrix = do+ exists <- doesDirectoryExist parentDir+ if not exists+ then do+ TIO.hPutStrLn stderr ("Ecosystem directory does not exist: " <> T.pack parentDir)+ pure (ExitFailure 1)+ else do+ repos <- discoverEcosystemRepositories parentDir+ if null repos+ then do+ TIO.putStrLn ("No repositories discovered under: " <> T.pack parentDir)+ pure ExitSuccess+ else do+ TIO.putStrLn ("Discovered " <> T.show (length repos) <> " repositories under " <> T.pack parentDir <> ":")+ forM_ repos \repo ->+ TIO.putStrLn (" • " <> T.pack (takeFileName repo) <> " (" <> T.pack repo <> ")")+ TIO.putStrLn ""++ results <- syncEcosystem mode parentDir matrix+ let summary = formatEcosystemSummary mode results+ TIO.putStrLn summary++ let allSuccessful = all isResultSuccess results+ pure if allSuccessful then ExitSuccess else ExitFailure 1+ where+ isResultSuccess :: (FilePath, Either Text SyncResult) -> Bool+ isResultSuccess (_, Right res) = resultSuccess res+ isResultSuccess (_, Left _) = False++--------------------------------------------------------------------------------+-- 4. Ecosystem Summary Reporting+--------------------------------------------------------------------------------++-- | Formats a tabular ASCII overview of ecosystem synchronization outcomes.+--+-- Renders a multi-column report displaying the structural alignment across all+-- discovered repositories in the target ecosystem directory.+--+-- === Table Architecture & Columns+--+-- The generated matrix contains eight informative columns:+--+-- 1. __Repository (20 chars):__ The folder basename of the discovered checkout.+-- 2. __Mode (10 chars):__ @DRY-RUN@ preview simulation vs @APPLIED@ live mutation.+-- 3. __Created (7 chars):__ Number of previously missing files synthesized.+-- 4. __Updated (7 chars):__ Number of drifted files restored to canonical form.+-- 5. __Chmod (7 chars):__ Number of scripts whose executable bits were corrected.+-- 6. __Unchanged (9 chars):__ Number of assets already in clean compliance.+-- 7. __Errors (6 chars):__ Count of I/O failures or Dhall decoding exceptions.+-- 8. __Disposition (11 chars):__+-- * @RESONANT@: Repository is in full compliance with zero errors.+-- * @DRIFTED@: Discrepancies exist (only possible under DryRun mode).+-- * @FAILED@: One or more I/O or evaluation exceptions occurred.+--+-- === Summary Footer+--+-- Emits aggregate counters indicating the total number of evaluated repositories,+-- how many achieved full resonance, and how many encountered errors.+formatEcosystemSummary :: SyncMode -> [(FilePath, Either Text SyncResult)] -> Text+formatEcosystemSummary mode results =+ let sep = "+----------------------+------------+---------+---------+---------+-----------+--------+-------------+"+ hdr = "| Repository | Mode | Created | Updated | Chmod | Unchanged | Errors | Disposition |"+ rows = map formatRow results+ totalRepos = length results+ successfulRepos = length (filter (\(_, r) -> case r of Right res -> resultSuccess res; Left _ -> False) results)+ footer = "Total Repositories: " <> T.show totalRepos+ <> " | Resonant: " <> T.show successfulRepos+ <> " | Failed: " <> T.show (totalRepos - successfulRepos)+ in T.unlines ([sep, hdr, sep] ++ rows ++ [sep, footer])+ where+ -- | Formats an individual repository's synchronization outcome into a padded table row.+ -- Handles both failed evaluations (schema errors, missing paths) and successful runs.+ formatRow (path, outcome) =+ let repoSlug = T.pack (takeFileName path)+ -- Helper to right-pad text values to fixed column width+ pad n t = if T.length t >= n then T.take n t else t <> T.replicate (n - T.length t) " "+ modeStr = case mode of DryRun -> "DRY-RUN"; ApplySync -> "APPLIED"+ in case outcome of+ -- Branch A: Unhandled failure during Dhall evaluation or repository access+ Left _err ->+ "| " <> pad 20 repoSlug+ <> " | " <> pad 10 modeStr+ <> " | " <> pad 7 "-"+ <> " | " <> pad 7 "-"+ <> " | " <> pad 7 "-"+ <> " | " <> pad 9 "-"+ <> " | " <> pad 6 "1"+ <> " | " <> pad 11 "FAILED" <> " |"+ -- Branch B: Structured synchronization outcome with per-category counters+ Right res ->+ let cStr = T.show (length (resultCreated res))+ uStr = T.show (length (resultUpdated res))+ mStr = T.show (length (resultChmodded res))+ kStr = T.show (length (resultUnchanged res))+ eStr = T.show (length (resultErrors res))+ disp = if resultSuccess res then "RESONANT" else "DRIFTED"+ in "| " <> pad 20 repoSlug+ <> " | " <> pad 10 modeStr+ <> " | " <> pad 7 cStr+ <> " | " <> pad 7 uStr+ <> " | " <> pad 7 mStr+ <> " | " <> pad 9 kStr+ <> " | " <> pad 6 eStr+ <> " | " <> pad 11 disp <> " |"++--------------------------------------------------------------------------------+-- 5. Multi-Remote Replication Topology Synchronization+--------------------------------------------------------------------------------++-- | Synchronizes or audits the Git remote replication topology for a repository checkout.+--+-- Evaluates the target checkout against Note [Multi-Remote Replication Topology and Provider Compliance]+-- [ref:note-remote-topology], identifying missing canonical remotes (@rad@, @github@,+-- @gitcode@, @framagit@, @disroot@), URL drift, and prohibited hosting providers+-- (such as SourceHut or plaintext HTTP).+--+-- === Phased Remote Reconciliation Sequence+--+-- 1. __Checkout Validation:__+-- Verifies that the target checkout directory exists prior to initiating Git queries.+-- 2. __Porcelain Topology Audit:__+-- Invokes 'syncRemoteTopologyQuick' to inspect @git remote -v@ metadata and compare+-- active mirrors against canonical expectations.+-- 3. __Diagnostic Report Emission:__+-- Outputs the formatted tabular report ('formatRemoteReport') highlighting missing,+-- drifted, and prohibited endpoints.+-- 4. __Plan Display & Execution:__+-- When actions are planned, outputs the action table ('formatRemoteSyncPlan').+-- Under 'ApplySync' mode, applies the Git commands and prints confirmation logs.+-- 5. __Exit Disposition:__+-- Yields 'ExitSuccess' if all actions succeeded or preview completed cleanly.+runRemoteSync :: SyncMode -> FilePath -> IO ExitCode+runRemoteSync mode targetDir = do+ exists <- doesDirectoryExist targetDir+ if not exists+ then do+ TIO.hPutStrLn stderr ("Target directory does not exist: " <> T.pack targetDir)+ pure (ExitFailure 1)+ else do+ (report, execRes) <- syncRemoteTopologyQuick mode targetDir+ TIO.putStrLn (formatRemoteReport report)+ let plan = computeRemoteSyncPlan report+ unless (null (planRemoteActions plan)) $+ TIO.putStrLn (formatRemoteSyncPlan plan)+ case execRes of+ Left err -> do+ TIO.hPutStrLn stderr ("Remote synchronization failed: " <> err)+ pure (ExitFailure 1)+ Right logs -> do+ when (mode == ApplySync) $+ mapM_ TIO.putStrLn logs+ pure ExitSuccess++-- | Discovers and synchronizes remote topology across all repositories in a parent folder.+--+-- Evaluates each discovered checkout under 'runRemoteSync', outputting individual+-- audit tables and terminating with 'ExitSuccess' only if all checkouts succeed.+--+-- === Multi-Repository Traversal Semantics+--+-- 1. Locates candidate repositories via 'discoverEcosystemRepositories'.+-- 2. Iterates deterministically through discovered folders in alphabetical order.+-- 3. Audits and repairs the 5-forge replication topology within each checkout.+-- 4. Yields 'ExitFailure 1' if any discovered repository encounters Git configuration errors.+runEcosystemRemoteSync :: SyncMode -> FilePath -> IO ExitCode+runEcosystemRemoteSync mode parentDir = do+ exists <- doesDirectoryExist parentDir+ if not exists+ then do+ TIO.hPutStrLn stderr ("Ecosystem directory does not exist: " <> T.pack parentDir)+ pure (ExitFailure 1)+ else do+ repos <- discoverEcosystemRepositories parentDir+ if null repos+ then do+ TIO.putStrLn ("No repositories discovered under: " <> T.pack parentDir)+ pure ExitSuccess+ else do+ TIO.putStrLn ("Auditing remote topology across " <> T.show (length repos) <> " repositories under " <> T.pack parentDir <> ":\n")+ codes <- forM repos $ \repo -> do+ TIO.putStrLn ("--- " <> T.pack (takeFileName repo) <> " (" <> T.pack repo <> ") ---")+ code <- runRemoteSync mode repo+ TIO.putStrLn ""+ pure code+ pure if all (== ExitSuccess) codes then ExitSuccess else ExitFailure 1
+ test/Main.hs view
@@ -0,0 +1,582 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module: Main+-- Description: Comprehensive specification and verification suite for gachi-cli+--+-- Validates:+-- 1. CLI option and flag parsing across subcommands and modes.+-- 2. Pure resolution of project specifications from Dhall files and CLI options.+-- 3. Headless workspace instantiation and filesystem materialization.+-- 4. Hermetic Git repository initialization and trailer governance (.githooks / .gitmessage).+-- 5. Execution modes ('FailOnExisting', 'SkipExisting', 'OverwriteExisting', '--dry-run').+module Main (main) where++import Control.Exception (SomeException, bracket, catch)+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import Data.Time.Clock.POSIX (getPOSIXTime)+import Options.Applicative+ ( ParserResult(..)+ , defaultPrefs+ , execParserPure+ )+import System.Directory+ ( createDirectoryIfMissing+ , doesDirectoryExist+ , doesFileExist+ , executable+ , getPermissions+ , getTemporaryDirectory+ , removeDirectoryRecursive+ )+import System.Exit (ExitCode(..))+import System.FilePath ((</>), takeFileName)+import System.Process (readProcessWithExitCode)+import Test.Hspec++import Gachi.CLI+import Gachi.Schema.Dhall (exampleProjectDhall)+import Gachi.Schema.Project+ ( authorEmail+ , authorName+ , projectAuthor+ , projectName+ )+import Gachi.Scaffold (ScaffoldMode(..))++-- | Helper executing an action within an isolated temporary directory.+withTempDir :: String -> (FilePath -> IO a) -> IO a+withTempDir prefix action = do+ base <- getTemporaryDirectory+ t <- getPOSIXTime+ let dirPath = base </> (prefix ++ "-" ++ show (round (t * 1000) :: Integer))+ bracket+ (createDirectoryIfMissing True dirPath >> pure dirPath)+ (\p -> catch (removeDirectoryRecursive p) (\(_ :: SomeException) -> pure ()))+ action++main :: IO ()+main = hspec $ do+ describe "Gachi.CLI.Options (Option Parsing)" $ do+ it "parses default 'init' with project name positional" $ do+ let res = execParserPure defaultPrefs (commandParser "0.1.0.0") ["init", "my-project"]+ case res of+ Success (CmdInit opts) -> do+ initProjectName opts `shouldBe` Just "my-project"+ initConfigFile opts `shouldBe` Nothing+ initMode opts `shouldBe` FailOnExisting+ initNoGit opts `shouldBe` False+ initDryRun opts `shouldBe` False+ _ -> expectationFailure "Expected Success (CmdInit opts)"++ it "parses 'init' with Dhall config file and target directory" $ do+ let res = execParserPure defaultPrefs (commandParser "0.1.0.0")+ ["init", "-c", "custom.dhall", "-d", "/tmp/target", "-m", "overwrite", "--no-git"]+ case res of+ Success (CmdInit opts) -> do+ initConfigFile opts `shouldBe` Just "custom.dhall"+ initTargetDir opts `shouldBe` Just "/tmp/target"+ initMode opts `shouldBe` OverwriteExisting+ initNoGit opts `shouldBe` True+ _ -> expectationFailure "Expected Success (CmdInit opts)"++ it "parses 'init' with satellite package list and description" $ do+ let res = execParserPure defaultPrefs (commandParser "0.1.0.0")+ ["init", "multi-pkg", "--packages", "pkg-core,pkg-api,pkg-cli", "--desc", "My API"]+ case res of+ Success (CmdInit opts) -> do+ initProjectName opts `shouldBe` Just "multi-pkg"+ initPackages opts `shouldBe` ["pkg-core", "pkg-api", "pkg-cli"]+ initDescription opts `shouldBe` Just "My API"+ _ -> expectationFailure "Expected Success (CmdInit opts)"++ it "correctly parses mode strings" $ do+ parseMode "fail" `shouldBe` Right FailOnExisting+ parseMode "SKIP" `shouldBe` Right SkipExisting+ parseMode "Overwrite" `shouldBe` Right OverwriteExisting+ parseMode "invalid" `shouldSatisfy` (\case Left _ -> True; _ -> False)++ it "parses version command" $ do+ let res = execParserPure defaultPrefs (commandParser "0.1.0.0") ["version"]+ case res of+ Success CmdVersion -> pure ()+ _ -> expectationFailure "Expected Success CmdVersion"++ it "parses 'sync' subcommand with target, dry-run, and all flags" $ do+ let res = execParserPure defaultPrefs (commandParser "0.1.0.0")+ ["sync", "-t", "/path/to/repo", "--dry-run", "-a"]+ case res of+ Success (CmdSync opts) -> do+ syncTargetDir opts `shouldBe` Just "/path/to/repo"+ syncDryRun opts `shouldBe` True+ syncAll opts `shouldBe` True+ syncRemotes opts `shouldBe` False+ _ -> expectationFailure "Expected Success (CmdSync opts)"++ it "parses 'sync' subcommand with --remotes / -r flag" $ do+ let res = execParserPure defaultPrefs (commandParser "0.1.0.0")+ ["sync", "-t", "/path/to/repo", "-r"]+ case res of+ Success (CmdSync opts) -> do+ syncTargetDir opts `shouldBe` Just "/path/to/repo"+ syncRemotes opts `shouldBe` True+ _ -> expectationFailure "Expected Success (CmdSync opts)"++ it "parses 'doctor' subcommand with target, sheaf, and all flags" $ do+ let res1 = execParserPure defaultPrefs (commandParser "0.1.0.0") ["doctor"]+ case res1 of+ Success (CmdDoctor opts) -> do+ doctorTargetDir opts `shouldBe` Nothing+ doctorSheaf opts `shouldBe` False+ doctorAll opts `shouldBe` False+ _ -> expectationFailure "Expected Success (CmdDoctor opts) for default doctor"++ let res2 = execParserPure defaultPrefs (commandParser "0.1.0.0") ["doctor", "-t", "/path/to/repo", "-s", "-a"]+ case res2 of+ Success (CmdDoctor opts) -> do+ doctorTargetDir opts `shouldBe` Just "/path/to/repo"+ doctorSheaf opts `shouldBe` True+ doctorAll opts `shouldBe` True+ _ -> expectationFailure "Expected Success (CmdDoctor opts) with flags"++ it "parses 'retrofit' subcommand with target, dry-run, and markdown flags" $ do+ let res1 = execParserPure defaultPrefs (commandParser "0.1.0.0") ["retrofit"]+ case res1 of+ Success (CmdRetrofit opts) -> do+ retrofitTargetDir opts `shouldBe` Nothing+ retrofitDryRun opts `shouldBe` False+ retrofitMarkdown opts `shouldBe` False+ _ -> expectationFailure "Expected Success (CmdRetrofit opts) for default retrofit"++ let res2 = execParserPure defaultPrefs (commandParser "0.1.0.0")+ ["retrofit", "-t", "/path/to/legacy", "--dry-run", "-m"]+ case res2 of+ Success (CmdRetrofit opts) -> do+ retrofitTargetDir opts `shouldBe` Just "/path/to/legacy"+ retrofitDryRun opts `shouldBe` True+ retrofitMarkdown opts `shouldBe` True+ _ -> expectationFailure "Expected Success (CmdRetrofit opts) with flags"++ describe "Gachi.CLI.Init (Project Resolution)" $ do+ it "resolves default project when no config is provided" $ do+ let opts = InitOptions+ { initProjectName = Just "test-slug"+ , initConfigFile = Nothing+ , initTargetDir = Nothing+ , initMode = FailOnExisting+ , initAuthorName = Just "Alice Smith"+ , initAuthorEmail = Just "alice@example.com"+ , initDescription = Just "Test project"+ , initPackages = ["test-slug-core"]+ , initNoGit = True+ , initDryRun = False+ , initInteractive = False+ }+ res <- resolveProject opts+ case res of+ Right (proj, targetDir) -> do+ projectName proj `shouldBe` "test-slug"+ authorName (projectAuthor proj) `shouldBe` "Alice Smith"+ authorEmail (projectAuthor proj) `shouldBe` "alice@example.com"+ targetDir `shouldBe` "test-slug"+ Left err -> expectationFailure (T.unpack err)++ it "resolves project from Dhall configuration file" $ do+ withTempDir "gachi-dhall-resolve" $ \tmpDir -> do+ let dhallPath = tmpDir </> "project.dhall"+ TIO.writeFile dhallPath exampleProjectDhall+ let opts = InitOptions+ { initProjectName = Nothing+ , initConfigFile = Just dhallPath+ , initTargetDir = Just (tmpDir </> "target")+ , initMode = FailOnExisting+ , initAuthorName = Nothing+ , initAuthorEmail = Nothing+ , initDescription = Nothing+ , initPackages = []+ , initNoGit = True+ , initDryRun = False+ , initInteractive = False+ }+ res <- resolveProject opts+ case res of+ Right (proj, targetDir) -> do+ projectName proj `shouldBe` "gongju"+ targetDir `shouldBe` (tmpDir </> "target")+ Left err -> expectationFailure (T.unpack err)++ describe "Gachi.CLI.Init (Filesystem Materialization & Git Setup)" $ do+ it "performs dry-run without writing any files to target directory" $ do+ withTempDir "gachi-dry-run" $ \tmpDir -> do+ let targetDir = tmpDir </> "uncreated-dir"+ opts = InitOptions+ { initProjectName = Just "dry-project"+ , initConfigFile = Nothing+ , initTargetDir = Just targetDir+ , initMode = FailOnExisting+ , initAuthorName = Nothing+ , initAuthorEmail = Nothing+ , initDescription = Nothing+ , initPackages = []+ , initNoGit = True+ , initDryRun = True+ , initInteractive = False+ }+ exitCode <- runInit opts+ exitCode `shouldBe` ExitSuccess+ exists <- doesDirectoryExist targetDir+ exists `shouldBe` False++ it "initializes full standardized workspace with Git governance" $ do+ withTempDir "gachi-init-full" $ \tmpDir -> do+ let targetDir = tmpDir </> "demo-project"+ opts = InitOptions+ { initProjectName = Just "demo-project"+ , initConfigFile = Nothing+ , initTargetDir = Just targetDir+ , initMode = FailOnExisting+ , initAuthorName = Just "Nadia Yvette Chambers"+ , initAuthorEmail = Just "nadia.yvette.chambers@ik.me"+ , initDescription = Just "Full demonstration workspace"+ , initPackages = ["demo-project-core"]+ , initNoGit = False+ , initDryRun = False+ , initInteractive = False+ }+ exitCode <- runInit opts+ exitCode `shouldBe` ExitSuccess++ -- Verify files created+ readmeExists <- doesFileExist (targetDir </> "README.md")+ readmeExists `shouldBe` True++ verifyScriptExists <- doesFileExist (targetDir </> "bin/demo-project-verify")+ verifyScriptExists `shouldBe` True++ cabalExists <- doesFileExist (targetDir </> "cabal.project")+ cabalExists `shouldBe` True++ -- Verify Git repository and governance configuration+ gitDirExists <- doesDirectoryExist (targetDir </> ".git")+ gitDirExists `shouldBe` True++ (c1, hooksOut, _) <- readProcessWithExitCode "git" ["-C", targetDir, "config", "--get", "core.hooksPath"] ""+ c1 `shouldBe` ExitSuccess+ T.strip (T.pack hooksOut) `shouldBe` ".githooks"++ (c2, tmplOut, _) <- readProcessWithExitCode "git" ["-C", targetDir, "config", "--get", "commit.template"] ""+ c2 `shouldBe` ExitSuccess+ T.strip (T.pack tmplOut) `shouldBe` ".gitmessage"++ it "respects FailOnExisting mode on pre-existing files" $ do+ withTempDir "gachi-fail-mode" $ \tmpDir -> do+ let targetDir = tmpDir </> "existing-proj"+ opts = InitOptions+ { initProjectName = Just "existing-proj"+ , initConfigFile = Nothing+ , initTargetDir = Just targetDir+ , initMode = FailOnExisting+ , initAuthorName = Nothing+ , initAuthorEmail = Nothing+ , initDescription = Nothing+ , initPackages = []+ , initNoGit = True+ , initDryRun = False+ , initInteractive = False+ }+ exit1 <- runInit opts+ exit1 `shouldBe` ExitSuccess+ -- Second run with FailOnExisting should fail gracefully+ exit2 <- runInit opts+ exit2 `shouldBe` ExitFailure 1++ describe "Gachi.CLI.Sync (Harmonic Synchronization Command)" $ do+ it "executes dry-run synchronization without modifying files" $ do+ withTempDir "gachi-cli-sync-dry" $ \tmpDir -> do+ let targetDir = tmpDir </> "sync-proj"+ initOpts = InitOptions+ { initProjectName = Just "sync-proj"+ , initConfigFile = Nothing+ , initTargetDir = Just targetDir+ , initMode = FailOnExisting+ , initAuthorName = Nothing+ , initAuthorEmail = Nothing+ , initDescription = Nothing+ , initPackages = []+ , initNoGit = True+ , initDryRun = False+ , initInteractive = False+ }+ initCode <- runInit initOpts+ initCode `shouldBe` ExitSuccess++ let syncOpts = SyncOptions+ { syncTargetDir = Just targetDir+ , syncDryRun = True+ , syncAll = False+ , syncRemotes = False+ }+ syncCode <- runSync syncOpts+ syncCode `shouldBe` ExitSuccess++ it "synchronizes and repairs configuration in an initialized workspace" $ do+ withTempDir "gachi-cli-sync-apply" $ \tmpDir -> do+ let targetDir = tmpDir </> "repair-proj"+ initOpts = InitOptions+ { initProjectName = Just "repair-proj"+ , initConfigFile = Nothing+ , initTargetDir = Just targetDir+ , initMode = FailOnExisting+ , initAuthorName = Nothing+ , initAuthorEmail = Nothing+ , initDescription = Nothing+ , initPackages = []+ , initNoGit = True+ , initDryRun = False+ , initInteractive = False+ }+ initCode <- runInit initOpts+ initCode `shouldBe` ExitSuccess++ -- Mutate .hlint.yaml to cause drift+ let hlintPath = targetDir </> ".hlint.yaml"+ TIO.writeFile hlintPath "# Drifted HLint configuration\n"++ -- Apply synchronization to repair drift+ let syncOpts = SyncOptions+ { syncTargetDir = Just targetDir+ , syncDryRun = False+ , syncAll = False+ , syncRemotes = False+ }+ syncCode1 <- runSync syncOpts+ syncCode1 `shouldBe` ExitSuccess++ -- Verify file content was repaired+ repairedContent <- TIO.readFile hlintPath+ repairedContent `shouldSatisfy` (\t -> "Ecosystem HLint Configuration" `T.isInfixOf` t)++ -- Re-run sync to verify CLI idempotency+ syncCode2 <- runSync syncOpts+ syncCode2 `shouldBe` ExitSuccess++ it "executes ecosystem-wide synchronization with --all" $ do+ withTempDir "gachi-cli-sync-all" $ \tmpDir -> do+ let repoA = tmpDir </> "repo-one"+ repoB = tmpDir </> "repo-two"+ makeOpts dir = InitOptions+ { initProjectName = Just (T.pack (takeFileName dir))+ , initConfigFile = Nothing+ , initTargetDir = Just dir+ , initMode = FailOnExisting+ , initAuthorName = Nothing+ , initAuthorEmail = Nothing+ , initDescription = Nothing+ , initPackages = []+ , initNoGit = True+ , initDryRun = False+ , initInteractive = False+ }+ _ <- runInit (makeOpts repoA)+ _ <- runInit (makeOpts repoB)++ let allOpts = SyncOptions+ { syncTargetDir = Just tmpDir+ , syncDryRun = True+ , syncAll = True+ , syncRemotes = False+ }+ allCode <- runSync allOpts+ allCode `shouldBe` ExitSuccess++ it "executes remote topology audit and synchronization with --remotes" $ do+ withTempDir "gachi-cli-sync-remotes" $ \tmpDir -> do+ let targetDir = tmpDir </> "remote-proj"+ initOpts = InitOptions+ { initProjectName = Just "remote-proj"+ , initConfigFile = Nothing+ , initTargetDir = Just targetDir+ , initMode = FailOnExisting+ , initAuthorName = Nothing+ , initAuthorEmail = Nothing+ , initDescription = Nothing+ , initPackages = []+ , initNoGit = False+ , initDryRun = False+ , initInteractive = False+ }+ initCode <- runInit initOpts+ initCode `shouldBe` ExitSuccess++ -- Audit & sync remotes via CLI+ let syncOpts = SyncOptions+ { syncTargetDir = Just targetDir+ , syncDryRun = False+ , syncAll = False+ , syncRemotes = True+ }+ syncCode <- runSync syncOpts+ syncCode `shouldBe` ExitSuccess++ -- Verify git remote output contains canonical remotes+ (cGit, remotesOut, _) <- readProcessWithExitCode "git" ["-C", targetDir, "remote"] ""+ cGit `shouldBe` ExitSuccess+ let rList = words remotesOut+ rList `shouldContain` ["github"]+ rList `shouldContain` ["framagit"]+ rList `shouldContain` ["disroot"]++ describe "Gachi.CLI.Doctor (Diagnostics & Sheaf Auditing)" $ do+ it "audits ambient toolchain binaries successfully" $ do+ toolchainOk <- checkToolchainHealth+ toolchainOk `shouldBe` True++ it "runs sheaf coherence audit via doctor --sheaf" $ do+ let docOpts = DoctorOptions+ { doctorTargetDir = Nothing+ , doctorSheaf = True+ , doctorAll = False+ }+ exitCode <- runDoctor docOpts+ exitCode `shouldBe` ExitSuccess++ it "runs general doctor on an initialized workspace successfully" $ do+ withTempDir "gachi-cli-doctor" $ \tmpDir -> do+ let targetDir = tmpDir </> "doctor-proj"+ initOpts = InitOptions+ { initProjectName = Just "doctor-proj"+ , initConfigFile = Nothing+ , initTargetDir = Just targetDir+ , initMode = FailOnExisting+ , initAuthorName = Just "Doctor Tester"+ , initAuthorEmail = Just "doctor@example.com"+ , initDescription = Just "Test workspace for doctor"+ , initPackages = []+ , initNoGit = False+ , initDryRun = False+ , initInteractive = False+ }+ initCode <- runInit initOpts+ initCode `shouldBe` ExitSuccess++ let docOpts = DoctorOptions+ { doctorTargetDir = Just targetDir+ , doctorSheaf = False+ , doctorAll = False+ }++ -- Pre-sync doctor detects missing CI workflow obstruction+ preSyncExit <- runDoctor docOpts+ preSyncExit `shouldBe` ExitFailure 1++ -- Sync repository to install standardized CI workflows+ let syncOpts = SyncOptions+ { syncTargetDir = Just targetDir+ , syncDryRun = False+ , syncAll = False+ , syncRemotes = False+ }+ syncCode <- runSync syncOpts+ syncCode `shouldBe` ExitSuccess++ -- Post-sync doctor confirms harmonic equilibrium (10/10 contracts coherent)+ postSyncExit <- runDoctor docOpts+ postSyncExit `shouldBe` ExitSuccess++ describe "Gachi.CLI.Retrofit (Diagnostic Inspection Execution)" $ do+ it "fails gracefully with ExitFailure 1 when target directory does not exist" $ do+ let opts = RetrofitOptions+ { retrofitTargetDir = Just "/nonexistent/directory/path/xyz"+ , retrofitDryRun = True+ , retrofitMarkdown = False+ }+ exitCode <- runRetrofit opts+ exitCode `shouldBe` ExitFailure 1++ it "runs diagnostic inspection on an initialized workspace successfully" $ withTempDir "gachi-cli-retrofit" $ \baseDir -> do+ let targetDir = baseDir </> "retrofit-proj"+ initOpts = InitOptions+ { initProjectName = Just "retrofit-proj"+ , initConfigFile = Nothing+ , initTargetDir = Just targetDir+ , initMode = FailOnExisting+ , initAuthorName = Just "Nadia Yvette Chambers"+ , initAuthorEmail = Just "nadia.yvette.chambers@ik.me"+ , initDescription = Just "Retrofit Target Testing"+ , initPackages = ["core"]+ , initNoGit = False+ , initDryRun = False+ , initInteractive = False+ }+ initCode <- runInit initOpts+ initCode `shouldBe` ExitSuccess++ let retroOpts = RetrofitOptions+ { retrofitTargetDir = Just targetDir+ , retrofitDryRun = True+ , retrofitMarkdown = False+ }+ retroCode <- runRetrofit retroOpts+ retroCode `shouldBe` ExitSuccess++ let mdOpts = RetrofitOptions+ { retrofitTargetDir = Just targetDir+ , retrofitDryRun = False+ , retrofitMarkdown = True+ }+ mdCode <- runRetrofit mdOpts+ mdCode `shouldBe` ExitSuccess++ it "applies non-destructive skeleton overlay to an uninitialized legacy checkout" $ withTempDir "gachi-cli-retrofit-live" $ \baseDir -> do+ let targetDir = baseDir </> "legacy-app"+ createDirectoryIfMissing True (targetDir </> "src")+ let origCode = "module Lib (greet) where\ngreet :: String -> String\ngreet name = \"Hello, \" ++ name\n"+ TIO.writeFile (targetDir </> "src" </> "Lib.hs") origCode+ TIO.writeFile (targetDir </> "legacy-app.cabal") "cabal-version: 3.0\nname: legacy-app\nversion: 0.1.0.0\n"++ let retroOpts = RetrofitOptions+ { retrofitTargetDir = Just targetDir+ , retrofitDryRun = False+ , retrofitMarkdown = False+ }+ retroCode <- runRetrofit retroOpts+ retroCode `shouldBe` ExitSuccess++ -- Invariant 1: Verification runner materialized with executable permissions+ let verifyScript = targetDir </> "bin" </> "legacy-app-verify"+ doesFileExist verifyScript `shouldReturn` True+ p <- getPermissions verifyScript+ executable p `shouldBe` True++ -- Invariant 2: Standard documentation hierarchy and agent advisories installed+ doesFileExist (targetDir </> "docs" </> "INDEX.md") `shouldReturn` True+ doesFileExist (targetDir </> "AGENTS.md") `shouldReturn` True+ doesFileExist (targetDir </> ".githooks" </> "commit-msg") `shouldReturn` True++ -- Invariant 3: Git repository initialized+ doesDirectoryExist (targetDir </> ".git") `shouldReturn` True++ -- Invariant 4: Domain source files preserved completely untouched+ codeAfter <- TIO.readFile (targetDir </> "src" </> "Lib.hs")+ codeAfter `shouldBe` origCode++ it "evaluates progressive quality gates during retrofit execution" $ withTempDir "gachi-cli-retrofit-gates" $ \baseDir -> do+ let targetDir = baseDir </> "quality-app"+ createDirectoryIfMissing True (targetDir </> "src")+ TIO.writeFile (targetDir </> "src" </> "Main.hs") "module Main where\nmain = pure ()\n"+ TIO.writeFile (targetDir </> "quality-app.cabal") "cabal-version: 3.0\nname: quality-app\nversion: 0.1.0.0\n"++ let retroOpts = RetrofitOptions+ { retrofitTargetDir = Just targetDir+ , retrofitDryRun = False+ , retrofitMarkdown = False+ }+ retroCode <- runRetrofit retroOpts+ retroCode `shouldBe` ExitSuccess++ -- Verify that overlay was applied and quality gates evaluated cleanly+ doesFileExist (targetDir </> "bin" </> "quality-app-verify") `shouldReturn` True+ doesFileExist (targetDir </> "PLAN.md") `shouldReturn` True+ doesFileExist (targetDir </> "tagref.yml") `shouldReturn` True