Skip to main content

opys_engine/
lib.rs

1//! opys — a file-based inventory of typed markdown documents: one markdown file
2//! per document, with YAML frontmatter, stable IDs, tags, configurable types,
3//! test plans, and a `verify` gate for CI.
4//!
5//! The binary is a thin wrapper around [`run`]. The modules are public so the
6//! crate can be used as a library.
7
8pub mod backend;
9pub mod body;
10pub mod cli;
11pub mod commands;
12pub mod config;
13pub mod doc;
14pub mod error;
15pub mod file_refs;
16pub mod frontmatter;
17pub mod links;
18pub mod mdprism;
19pub mod palette;
20pub mod project;
21pub mod project_config;
22pub mod refs;
23pub mod retired;
24pub mod rules;
25pub mod store;
26pub mod templates;
27
28pub use error::{OpysError, Result};
29pub use frontmatter::Frontmatter;
30pub use project::Project;
31
32use cli::{Cli, Command};
33
34/// Shared invocation context: where the inventory lives, global flags, and the
35/// storage backend the commands load/flush through.
36pub struct Ctx {
37    pub root: String,
38    pub no_sync: bool,
39    pub backend: Box<dyn backend::Backend>,
40}
41
42impl Ctx {
43    pub fn open(&self) -> Result<Project> {
44        Project::open(&self.root)
45    }
46
47    /// Load the corpus into a working store via the injected backend.
48    pub fn load(&self, prj: &Project) -> Result<(store::Store, Vec<String>)> {
49        self.backend.load(prj)
50    }
51
52    /// Flush the store's changes via the injected backend.
53    pub fn flush(&self, prj: &Project, store: store::Store) -> Result<()> {
54        self.backend.flush(prj, store)
55    }
56}
57
58/// Execute a parsed CLI invocation, returning the process exit code.
59///
60/// `verify` returns `1` when it finds problems (the CI-gate contract); all
61/// other commands return `0` on success and surface failures as
62/// [`OpysError`], which the binary maps to exit code `2`.
63pub fn run(cli: Cli, backend: Box<dyn backend::Backend>) -> Result<i32> {
64    let ctx = Ctx {
65        root: cli.root,
66        no_sync: cli.no_sync,
67        backend,
68    };
69    match cli.command {
70        Command::Init => {
71            commands::init::run(&ctx)?;
72            Ok(0)
73        }
74        Command::New {
75            type_name,
76            title,
77            tags,
78            status,
79            features,
80            reason,
81            field,
82        } => {
83            commands::new::run(
84                &ctx,
85                &type_name,
86                &title,
87                &tags,
88                &status,
89                &features,
90                reason.as_deref(),
91                &field,
92            )?;
93            Ok(0)
94        }
95        Command::Import { type_name, file } => {
96            commands::import::run(&ctx, &type_name, &file)?;
97            Ok(0)
98        }
99        Command::Show { id, refs } => {
100            commands::show::run(&ctx, &id, refs)?;
101            Ok(0)
102        }
103        Command::List {
104            type_name,
105            tag,
106            status,
107            field,
108            format,
109        } => {
110            commands::list::run(
111                &ctx,
112                type_name.as_deref(),
113                tag.as_deref(),
114                status.as_deref(),
115                &field,
116                format,
117            )?;
118            Ok(0)
119        }
120        Command::SetStatus {
121            ids,
122            status,
123            reason,
124        } => {
125            commands::set_status::run(&ctx, &ids, &status, reason.as_deref())?;
126            Ok(0)
127        }
128        Command::Tag { ids, add, remove } => {
129            commands::tag::run(&ctx, &ids, add.as_deref(), remove.as_deref())?;
130            Ok(0)
131        }
132        Command::Retire { ids, reason } => {
133            commands::retire::run(&ctx, &ids, &reason)?;
134            Ok(0)
135        }
136        Command::Block { ids, by } => {
137            commands::block::block(&ctx, &ids, &by)?;
138            Ok(0)
139        }
140        Command::Unblock { ids, by } => {
141            commands::block::unblock(&ctx, &ids, &by)?;
142            Ok(0)
143        }
144        Command::Verify => commands::verify::run(&ctx),
145        Command::Sync => {
146            commands::sync::run_command(&ctx)?;
147            Ok(0)
148        }
149        Command::Tags { keys } => {
150            commands::tags::run(&ctx, keys)?;
151            Ok(0)
152        }
153        Command::Stats { plain } => {
154            commands::stats::run(&ctx, plain)?;
155            Ok(0)
156        }
157        Command::Query {
158            sql,
159            plain,
160            write,
161            stdin,
162        } => {
163            commands::query::run(&ctx, &sql, plain, write, stdin)?;
164            Ok(0)
165        }
166        #[cfg(feature = "history")]
167        Command::History { id } => {
168            commands::history::run(&ctx, &id)?;
169            Ok(0)
170        }
171        Command::Close { ids, force } => {
172            commands::close::run(&ctx, &ids, force)?;
173            Ok(0)
174        }
175        Command::Renumber { base } => {
176            commands::renumber::run(&ctx, base.as_deref())?;
177            Ok(0)
178        }
179        Command::Cleanup => {
180            commands::cleanup::run(&ctx)?;
181            Ok(0)
182        }
183        Command::Config(cmd) => match cmd {
184            cli::ConfigCommand::Init => {
185                commands::config::init(&ctx)?;
186                Ok(0)
187            }
188            cli::ConfigCommand::Validate => commands::config::validate(&ctx),
189        },
190        Command::AgentRules { tool, stdout } => {
191            commands::agent_rules::run(&ctx, tool, stdout)?;
192            Ok(0)
193        }
194        // The TUI is launched by the binary (opys-tui crate); it intercepts this
195        // command before calling run(), so this arm is unreachable in practice.
196        #[cfg(feature = "tui")]
197        Command::Tui { .. } => Err(crate::error::usage(
198            "the tui is launched by the opys binary, not opys::run",
199        )),
200    }
201}