packages feed

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 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