okf-cli (empty) → 0.1.0.0
raw patch · 6 files changed
+405/−0 lines, 6 filesdep +aesondep +basedep +bytestring
Dependencies added: aeson, base, bytestring, generic-lens, lens, okf-cli, okf-core, optparse-applicative, text
Files
- CHANGELOG.md +22/−0
- LICENSE +30/−0
- app/Main.hs +6/−0
- okf-cli.cabal +78/−0
- src/Okf/Cli.hs +237/−0
- test/Main.hs +32/−0
+ CHANGELOG.md view
@@ -0,0 +1,22 @@+# Changelog++All notable changes to okf are recorded here.++The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and+this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).++## [Unreleased]++## [0.1.0.0] - 2026-06-19++Initial release.++### Added++- `okf-core` library: OKF document parser (`Okf.Document`), bundle graph+ indexing (`Okf.Index`, `Okf.Graph`), bundle validation with referential+ integrity (`Okf.Validation`, `Okf.Bundle`), concept construction and bundle+ writing, concept-link rendering with a round-trip guarantee, and a frontmatter+ authoring API.+- `okf-cli` library and `okf` executable: bundle validation and document+ authoring commands over the core API.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2026 Nadeem Bitar++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Nadeem Bitar nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ app/Main.hs view
@@ -0,0 +1,6 @@+module Main (main) where++import Okf.Cli (runCli)++main :: IO ()+main = runCli
+ okf-cli.cabal view
@@ -0,0 +1,78 @@+cabal-version: 3.4+name: okf-cli+version: 0.1.0.0+synopsis: Command-line interface for Open Knowledge Format bundles+description:+ okf-cli provides the @okf@ executable for working with Open Knowledge Format+ (OKF) bundles: validating bundles, inspecting their concept graph, and+ authoring documents from the command line, built on the okf-core library.++category: Data, CLI+license: BSD-3-Clause+license-file: LICENSE+author: Nadeem Bitar+maintainer: nadeem@gmail.com+copyright: (c) 2026 Nadeem Bitar+build-type: Simple+extra-doc-files: CHANGELOG.md++common common-options+ ghc-options:+ -Wall+ -Wcompat+ -Widentities+ -Wincomplete-uni-patterns+ -Wincomplete-record-updates+ -Wredundant-constraints+ -fhide-source-paths+ -Wmissing-export-lists+ -Wpartial-fields+ -Wmissing-deriving-strategies++ default-language: GHC2024+ default-extensions:+ DeriveAnyClass+ DuplicateRecordFields+ OverloadedLabels+ OverloadedStrings++library+ import: common-options+ hs-source-dirs: src+ exposed-modules:+ Okf.Cli++ build-depends:+ aeson >=2.2 && <2.4,+ base >=4.20 && <5,+ bytestring >=0.11 && <0.13,+ okf-core ^>=0.1.0.0,+ generic-lens >=2.2 && <2.4,+ lens ^>=5.3,+ optparse-applicative >=0.18 && <0.20,+ text ^>=2.1,++test-suite okf-cli-test+ import: common-options+ type: exitcode-stdio-1.0+ main-is: Main.hs+ hs-source-dirs: test++ build-depends:+ base >=4.20 && <5,+ okf-cli,+ optparse-applicative >=0.18,+ text ^>=2.1,++executable okf+ import: common-options+ main-is: Main.hs+ hs-source-dirs: app+ ghc-options:+ -threaded+ -rtsopts+ -with-rtsopts=-N++ build-depends:+ base >=4.20 && <5,+ okf-cli,
+ src/Okf/Cli.hs view
@@ -0,0 +1,237 @@+-- | Top-level CLI entry point for okf.+module Okf.Cli+ ( Command (..),+ GraphOptions (..),+ IndexOptions (..),+ Options (..),+ ShowOptions (..),+ ValidateOptions (..),+ parserInfo,+ runCli,+ runCommand,+ )+where++import Data.Aeson qualified as Aeson+import Data.ByteString.Lazy.Char8 qualified as LazyByteString+import Data.Foldable (traverse_)+import Data.Text qualified as Text+import Data.Text.IO qualified as Text.IO+import Okf.Bundle+import Okf.ConceptId+import Okf.Document (DocumentParseError (..), body)+import Okf.Graph (buildGraph)+import Okf.Index+import Okf.Prelude+import Okf.Validation+import Options.Applicative+import System.Exit (exitFailure)+import System.IO (stderr)++data Command+ = Validate ValidateOptions+ | Index IndexOptions+ | GraphCommand GraphOptions+ | ShowConcept ShowOptions+ deriving stock (Show, Eq)++data ValidateOptions = ValidateOptions+ { bundlePath :: !FilePath,+ strictMode :: !Bool+ }+ deriving stock (Show, Eq)++data IndexOptions = IndexOptions+ { bundlePath :: !FilePath,+ write :: !Bool+ }+ deriving stock (Show, Eq)++data GraphOptions = GraphOptions+ { bundlePath :: !FilePath,+ json :: !Bool+ }+ deriving stock (Show, Eq)++data ShowOptions = ShowOptions+ { bundlePath :: !FilePath,+ conceptIdText :: !Text+ }+ deriving stock (Show, Eq)++data Options = Options+ { cmd :: !Command+ }+ deriving stock (Show, Eq)++runCli :: IO ()+runCli = do+ Options {cmd} <- execParser parserInfo+ runCommand cmd++parserInfo :: ParserInfo Options+parserInfo =+ info+ (optionsParser <**> helper)+ ( fullDesc+ <> progDesc "Validate, index, inspect, and graph Open Knowledge Format bundles"+ <> header "okf - Open Knowledge Format bundle tools"+ )++optionsParser :: Parser Options+optionsParser = Options <$> commandParser++commandParser :: Parser Command+commandParser =+ hsubparser+ ( command "validate" (info (Validate <$> validateOptionsParser <**> helper) (progDesc "Validate an OKF bundle"))+ <> command "index" (info (Index <$> indexOptionsParser <**> helper) (progDesc "Preview or write generated index.md files"))+ <> command "graph" (info (GraphCommand <$> graphOptionsParser <**> helper) (progDesc "Print a bundle graph"))+ <> command "show" (info (ShowConcept <$> showOptionsParser <**> helper) (progDesc "Show one concept"))+ )++validateOptionsParser :: Parser ValidateOptions+validateOptionsParser =+ ValidateOptions+ <$> bundleArgument+ <*> switch (long "strict" <> help "Require recommended authoring fields")++indexOptionsParser :: Parser IndexOptions+indexOptionsParser =+ IndexOptions+ <$> bundleArgument+ <*> switch (long "write" <> help "Write generated index.md files instead of previewing")++graphOptionsParser :: Parser GraphOptions+graphOptionsParser =+ GraphOptions+ <$> bundleArgument+ <*> switch (long "json" <> help "Print JSON graph output")++showOptionsParser :: Parser ShowOptions+showOptionsParser =+ ShowOptions+ <$> bundleArgument+ <*> (Text.pack <$> strArgument (metavar "CONCEPT_ID" <> help "Concept ID such as tables/users"))++bundleArgument :: Parser FilePath+bundleArgument =+ strArgument (metavar "BUNDLE" <> help "Path to an OKF bundle directory")++runCommand :: Command -> IO ()+runCommand = \case+ Validate options -> runValidate options+ Index options -> runIndex options+ GraphCommand options -> runGraph options+ ShowConcept options -> runShow options++runValidate :: ValidateOptions -> IO ()+runValidate ValidateOptions {bundlePath, strictMode} = do+ concepts <- loadBundleOrExit bundlePath+ let profile = if strictMode then StrictAuthoring else PermissiveConformance+ case validateBundle profile concepts of+ [] -> Text.IO.putStrLn ("OK: " <> Text.pack (show (length concepts)) <> " concepts")+ errors -> do+ mapM_ (Text.IO.hPutStrLn stderr . renderBundleValidationError) errors+ exitFailure++runIndex :: IndexOptions -> IO ()+runIndex IndexOptions {bundlePath, write} =+ if write+ then do+ result <- writeBundleIndexes bundlePath+ case result of+ Left bundleError -> dieText (renderBundleError bundleError)+ Right () -> Text.IO.putStrLn "Wrote index.md files"+ else do+ indexes <- loadIndexesOrExit bundlePath+ mapM_ renderIndexPreview indexes++runGraph :: GraphOptions -> IO ()+runGraph GraphOptions {bundlePath} = do+ concepts <- loadBundleOrExit bundlePath+ LazyByteString.putStrLn (Aeson.encode (buildGraph concepts))++runShow :: ShowOptions -> IO ()+runShow ShowOptions {bundlePath, conceptIdText} = do+ conceptId <- either (dieText . renderConceptIdError conceptIdText) pure (parseConceptId conceptIdText)+ concepts <- loadBundleOrExit bundlePath+ case findConcept conceptId concepts of+ Nothing -> dieText ("Concept not found: " <> conceptIdText)+ Just concept -> renderConcept concept++loadBundleOrExit :: FilePath -> IO [Concept]+loadBundleOrExit bundlePath = do+ result <- walkBundle bundlePath+ case result of+ Left bundleError -> dieText (renderBundleError bundleError)+ Right concepts -> pure concepts++loadIndexesOrExit :: FilePath -> IO [(FilePath, Text)]+loadIndexesOrExit bundlePath = do+ result <- renderBundleIndexes bundlePath+ case result of+ Left bundleError -> dieText (renderBundleError bundleError)+ Right indexes -> pure indexes++renderBundleValidationError :: BundleValidationError -> Text+renderBundleValidationError = \case+ DocumentInvalid conceptId error_ ->+ renderConceptId conceptId <> ": " <> renderValidationErrorText error_+ DanglingReference source target ->+ renderConceptId source <> ": link to missing concept: " <> renderConceptId target+ DuplicateConceptId conceptId ->+ "duplicate concept ID: " <> renderConceptId conceptId++renderValidationErrorText :: ValidationError -> Text+renderValidationErrorText = \case+ MissingRequiredField fieldName -> "missing required field: " <> fieldName+ FieldMustBeNonEmptyText fieldName -> "field must be non-empty text: " <> fieldName+ MissingRecommendedField fieldName -> "missing recommended field: " <> fieldName+ FieldMustBeListOfText fieldName -> "field must be a list of text values: " <> fieldName++renderIndexPreview :: (FilePath, Text) -> IO ()+renderIndexPreview (path, content) = do+ Text.IO.putStrLn ("--- " <> Text.pack path)+ Text.IO.putStr content++renderConcept :: Concept -> IO ()+renderConcept concept = do+ Text.IO.putStrLn ("id: " <> renderConceptId (conceptIdOf concept))+ Text.IO.putStrLn ("type: " <> conceptType concept)+ traverse_ (Text.IO.putStrLn . ("title: " <>)) (conceptTitle concept)+ traverse_ (Text.IO.putStrLn . ("description: " <>)) (conceptDescription concept)+ traverse_ (Text.IO.putStrLn . ("resource: " <>)) (conceptResource concept)+ unless (null (conceptTags concept)) (Text.IO.putStrLn ("tags: " <> Text.intercalate ", " (conceptTags concept)))+ Text.IO.putStrLn ""+ Text.IO.putStr (bodyText concept)++bodyText :: Concept -> Text+bodyText concept =+ body (conceptDocument concept)++renderBundleError :: BundleError -> Text+renderBundleError = \case+ InvalidConceptPath path error_ -> Text.pack path <> ": " <> renderConceptIdParseError error_+ InvalidConceptDocument path error_ -> Text.pack path <> ": " <> renderDocumentParseError error_+ BundleIoError path message -> Text.pack path <> ": " <> message++renderConceptIdError :: Text -> ConceptIdError -> Text+renderConceptIdError rawId error_ =+ "Invalid concept ID " <> rawId <> ": " <> renderConceptIdParseError error_++renderConceptIdParseError :: ConceptIdError -> Text+renderConceptIdParseError = \case+ EmptyConceptId -> "empty concept ID"+ InvalidConceptIdSegment segment -> "invalid concept ID segment: " <> segment++renderDocumentParseError :: DocumentParseError -> Text+renderDocumentParseError = \case+ UnterminatedFrontmatter -> "unterminated YAML frontmatter"+ InvalidYaml message -> "invalid YAML frontmatter: " <> message+ FrontmatterNotMapping -> "frontmatter must be a YAML mapping"++dieText :: Text -> IO a+dieText message = do+ Text.IO.hPutStrLn stderr message+ exitFailure
+ test/Main.hs view
@@ -0,0 +1,32 @@+module Main (main) where++import Control.Monad (unless)+import Options.Applicative+import System.Exit (exitFailure)++import Okf.Cli++main :: IO ()+main = do+ let results =+ [ parseSucceeds ["validate", "bundle"]+ , parseSucceeds ["validate", "bundle", "--strict"]+ , parseSucceeds ["index", "bundle", "--write"]+ , parseSucceeds ["graph", "bundle", "--json"]+ , parseSucceeds ["show", "bundle", "tables/orders"]+ , parseFails ["hello"]+ ]+ unless (and results) exitFailure++parseSucceeds :: [String] -> Bool+parseSucceeds args =+ case execParserPure defaultPrefs parserInfo args of+ Success _ -> True+ _ -> False++parseFails :: [String] -> Bool+parseFails args =+ case execParserPure defaultPrefs parserInfo args of+ Failure _ -> True+ CompletionInvoked _ -> True+ Success _ -> False