Skip to main content

coding_tools/cli/
ct_okf.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Jonathan Shook
3
4//! The `ct-okf` command grammar (see [`crate::cli`]). Unlike the other leaf
5//! tools — which are flat-flag — `ct-okf` is **subcommand**-shaped (`ct okf
6//! search`, `ct okf roots add`, …), because its surface spans querying,
7//! root management, index maintenance, and authoring. The `ct-okf` bin is a
8//! parse-and-dispatch wrapper over this `Cli`.
9//!
10//! Global flags (`--json`, `--quiet`, `--base`, the walker vocabulary, …) are
11//! declared `global` so they may appear before or after the subcommand; the
12//! per-verb flags live on each subcommand's args struct.
13
14use std::path::PathBuf;
15
16use clap::{Args, Parser, Subcommand};
17
18use crate::explain::Format;
19use crate::pulse::HeartbeatOpts;
20
21#[derive(Parser, Debug)]
22#[command(
23    name = "ct-okf",
24    version,
25    about = "Author, query, and index Open Knowledge Format bundles across a project's content roots.",
26    long_about = "ct-okf manages Open Knowledge Format (OKF) knowledge for a project (also reachable \
27                  as `ct okf`). It works over the project's configured content roots and keeps a \
28                  lazily-maintained full-text index so `ct okf search` is always current. Pick a \
29                  subcommand: search/find query, roots/index/init configure, validate/links check, \
30                  show/add/mv/set/log/gen-index/script author. See `ct-okf --explain` for \
31                  agent-oriented documentation."
32)]
33pub struct Cli {
34    #[command(subcommand)]
35    pub command: Option<Command>,
36
37    /// Search root for bundle-scoped verbs (validate/links/find/show), and the
38    /// directory project discovery starts from for search/index/roots/init.
39    #[arg(long, default_value = ".", global = true)]
40    pub base: PathBuf,
41
42    /// Limit to files whose name matches; '|'-separated alternatives.
43    #[arg(long, global = true)]
44    pub name: Option<String>,
45
46    /// Include dot-entries (names starting with '.').
47    #[arg(long, global = true)]
48    pub hidden: bool,
49
50    /// Follow symlinks while traversing.
51    #[arg(long, global = true)]
52    pub follow: bool,
53
54    /// Walk gitignored / .ignore files too (the .git directory is always skipped).
55    #[arg(long, global = true)]
56    pub no_ignore: bool,
57
58    /// Emit a structured JSON result instead of text.
59    #[arg(long, global = true)]
60    pub json: bool,
61
62    /// Like --json, but pretty-printed (indented).
63    #[arg(long, global = true)]
64    pub json_pretty: bool,
65
66    /// Suppress informational output (exit status and --emit still report).
67    #[arg(long, global = true)]
68    pub quiet: bool,
69
70    /// Abort with exit 2 if the run exceeds SECS seconds (fractional allowed).
71    #[arg(long, value_name = "SECS", global = true)]
72    pub timeout: Option<f64>,
73
74    #[command(flatten)]
75    pub heartbeat: HeartbeatOpts,
76
77    /// Print agent usage docs (md or json) and exit.
78    #[arg(long, value_enum, num_args = 0..=1, default_missing_value = "md")]
79    pub explain: Option<Format>,
80}
81
82/// The `ct-okf` verbs.
83#[derive(Subcommand, Debug)]
84pub enum Command {
85    /// Full-text search the project's OKF content roots (auto-updates the index first).
86    Search(SearchArgs),
87    /// List concepts by metadata (--type / --tag) in the --base bundle.
88    Find(FindArgs),
89    /// Manage the project's OKF content roots.
90    Roots(RootsArgs),
91    /// Maintain the search index.
92    Index(IndexArgs),
93    /// Onboard: discover content roots and record them (optionally write markers).
94    Init(InitArgs),
95    /// Judge the --base bundle's OKF conformance (framed verdict).
96    Validate(CheckArgs),
97    /// Report broken bundle cross-links (framed verdict).
98    Links(CheckArgs),
99    /// Print one concept's frontmatter.
100    Show(ShowArgs),
101    /// Scaffold a new concept file (alias: new).
102    #[command(alias = "new")]
103    Add(AddArgs),
104    /// Move/rename a concept, fixing bundle cross-links (alias: rename).
105    #[command(alias = "rename")]
106    Mv(MvArgs),
107    /// Set or update a scalar frontmatter field on a concept.
108    Set(SetArgs),
109    /// Prepend a dated entry to a bundle's log.md.
110    Log(LogArgs),
111    /// (Re)generate a bundle directory's index.md from its concepts.
112    GenIndex(GenIndexArgs),
113    /// Run a .ctb batch of OKF mutations atomically.
114    Script(ScriptArgs),
115}
116
117/// Framing options shared by the check verbs (`validate`, `links`).
118#[derive(Args, Debug, Default)]
119pub struct Framing {
120    /// Print a `== QUESTION ==` banner before the check.
121    #[arg(long)]
122    pub question: Option<String>,
123    /// Classify the violation count: any|none|N|=N|+N|-N (default none).
124    #[arg(long)]
125    pub expect: Option<String>,
126    /// Expand a template to stdout after the check (tokens {RESULT} {COUNT} …).
127    #[arg(long, alias = "emit-stdout")]
128    pub emit: Option<String>,
129    /// Expand a template to stderr after the check.
130    #[arg(long)]
131    pub emit_stderr: Option<String>,
132}
133
134#[derive(Args, Debug)]
135pub struct SearchArgs {
136    /// Query terms. Supports `term`, `term*` (prefix), `term~`/`term~2` (fuzzy), and `/regex/`.
137    #[arg(value_name = "QUERY", required = true)]
138    pub query: Vec<String>,
139    /// Maximum number of hits to return.
140    #[arg(long, default_value_t = 20)]
141    pub limit: usize,
142    /// Only hits of this exact OKF type.
143    #[arg(long = "type", value_name = "TYPE")]
144    pub type_: Option<String>,
145    /// Only hits carrying all of these tags.
146    #[arg(long, value_delimiter = ',')]
147    pub tag: Vec<String>,
148}
149
150#[derive(Args, Debug)]
151pub struct FindArgs {
152    /// Filter to this exact OKF type.
153    #[arg(long = "type", value_name = "TYPE")]
154    pub type_: Option<String>,
155    /// Filter to concepts carrying all of these tags.
156    #[arg(long, value_delimiter = ',')]
157    pub tag: Vec<String>,
158}
159
160#[derive(Args, Debug)]
161pub struct RootsArgs {
162    #[command(subcommand)]
163    pub action: RootsCmd,
164}
165
166#[derive(Subcommand, Debug)]
167pub enum RootsCmd {
168    /// List the configured/detected content roots and how each was detected.
169    List,
170    /// Register a directory as a content root (records it in .ct/okf.jsonc).
171    Add {
172        /// The directory to add (project-relative or absolute).
173        dir: PathBuf,
174        /// Also drop a `.okf` marker file in the directory.
175        #[arg(long)]
176        marker: bool,
177    },
178    /// Unregister a content root from .ct/okf.jsonc.
179    Rm {
180        /// The directory to remove.
181        dir: PathBuf,
182    },
183    /// Discover candidate roots by scanning for OKF concepts.
184    Scan {
185        /// Record discovered roots in config and drop `.okf` markers.
186        #[arg(long)]
187        write: bool,
188    },
189}
190
191#[derive(Args, Debug)]
192pub struct IndexArgs {
193    #[command(subcommand)]
194    pub action: IndexCmd,
195}
196
197#[derive(Subcommand, Debug)]
198pub enum IndexCmd {
199    /// Report index freshness (docs, segments, tombstones, pending changes).
200    Status,
201    /// Reconcile the index against the content roots now.
202    Update,
203    /// Merge segments and drop tombstones, reclaiming space.
204    Condense,
205    /// Discard and rebuild the index from scratch.
206    Rebuild,
207}
208
209#[derive(Args, Debug)]
210pub struct InitArgs {
211    /// Also drop a `.okf` marker file in each discovered root.
212    #[arg(long)]
213    pub marker: bool,
214}
215
216#[derive(Args, Debug)]
217pub struct CheckArgs {
218    /// Also count broken bundle-relative cross-links as violations.
219    #[arg(long)]
220    pub strict: bool,
221    #[command(flatten)]
222    pub framing: Framing,
223}
224
225#[derive(Args, Debug)]
226pub struct ShowArgs {
227    /// The concept file to show.
228    pub path: PathBuf,
229}
230
231#[derive(Args, Debug)]
232pub struct AddArgs {
233    /// The concept file to create (must not exist).
234    pub path: PathBuf,
235    /// The concept's OKF type (required).
236    #[arg(long = "type", value_name = "TYPE")]
237    pub type_: String,
238    /// Human title (defaults to the file stem).
239    #[arg(long)]
240    pub title: Option<String>,
241    /// One-sentence description.
242    #[arg(long)]
243    pub description: Option<String>,
244    /// Tags (comma-separated or repeated).
245    #[arg(long, value_delimiter = ',')]
246    pub tag: Vec<String>,
247}
248
249#[derive(Args, Debug)]
250pub struct MvArgs {
251    /// The concept file to move.
252    pub src: PathBuf,
253    /// The destination path.
254    pub dst: PathBuf,
255}
256
257#[derive(Args, Debug)]
258pub struct SetArgs {
259    /// FIELD=VALUE to set on the concept's frontmatter.
260    #[arg(value_name = "FIELD=VALUE")]
261    pub spec: String,
262    /// The concept file to edit.
263    #[arg(long)]
264    pub file: PathBuf,
265}
266
267#[derive(Args, Debug)]
268pub struct LogArgs {
269    /// The log message.
270    #[arg(value_name = "MESSAGE")]
271    pub message: String,
272    /// Entry label (default Update).
273    #[arg(long = "kind", value_name = "LABEL")]
274    pub kind: Option<String>,
275}
276
277#[derive(Args, Debug)]
278pub struct GenIndexArgs {
279    /// Scaffold an absent index.md declaring okf_version instead of listing concepts.
280    #[arg(long)]
281    pub scaffold: bool,
282}
283
284#[derive(Args, Debug)]
285pub struct ScriptArgs {
286    /// The .ctb script of new/set/log/index/init items.
287    pub path: PathBuf,
288    /// Print the plan and write nothing.
289    #[arg(long)]
290    pub dry_run: bool,
291    /// Directive prefix for script lines (default '#%').
292    #[arg(long, value_name = "STR")]
293    pub fence: Option<String>,
294}