1mod cmd;
2mod config;
3mod edit_plan;
4mod error;
5mod frontmatter;
6mod inventory;
7mod link_index;
8mod markdown_document;
9mod markdown_links;
10mod mention;
11mod page;
12mod splice;
13mod wiki;
14
15use std::path::PathBuf;
16use std::process::ExitCode;
17
18use clap::{Parser, Subcommand, ValueEnum};
19
20use crate::cmd::lint::SeverityFilter;
21use crate::config::WikiConfig;
22use crate::error::WikiError;
23use crate::wiki::{Wiki, WikiRoot};
24
25#[derive(Parser)]
26#[command(
27 name = "llmwiki-tool",
28 about = "Manage LLM wiki knowledge bases",
29 version
30)]
31struct Cli {
32 #[arg(long, global = true)]
34 root: Option<PathBuf>,
35
36 #[command(subcommand)]
37 command: Command,
38}
39
40#[derive(Subcommand)]
41enum Command {
42 Links {
44 #[command(subcommand)]
45 action: LinksAction,
46 },
47 Lint {
49 #[arg(long, value_enum, default_value_t = SeverityArg::All)]
51 severity: SeverityArg,
52 },
53 Rename {
55 old: String,
57 new: String,
59 #[arg(long)]
61 write: bool,
62 },
63 Move {
65 page: String,
67 dest_dir: PathBuf,
69 #[arg(long)]
71 write: bool,
72 },
73 Refs {
75 #[command(subcommand)]
76 action: RefsAction,
77 },
78 Sections {
80 #[command(subcommand)]
81 action: SectionsAction,
82 },
83 Frontmatter {
85 #[command(subcommand)]
86 action: FrontmatterAction,
87 },
88 Scan,
90 Setup {
92 #[command(subcommand)]
93 action: SetupAction,
94 },
95}
96
97#[derive(Subcommand)]
98enum SetupAction {
99 Prompt,
101 ExampleConfig,
103 Init {
105 #[arg(long)]
107 show: bool,
108 #[arg(long, short)]
110 force: bool,
111 },
112}
113
114#[derive(Clone, Copy, ValueEnum)]
115enum SeverityArg {
116 All,
117 Error,
118 Warn,
119}
120
121impl From<SeverityArg> for SeverityFilter {
122 fn from(arg: SeverityArg) -> Self {
123 match arg {
124 SeverityArg::All => Self::All,
125 SeverityArg::Error => Self::ErrorOnly,
126 SeverityArg::Warn => Self::WarnOnly,
127 }
128 }
129}
130
131#[derive(Subcommand)]
132enum LinksAction {
133 Check,
135 Fix {
137 #[arg(long)]
139 write: bool,
140 },
141 Broken,
143 Orphans,
145}
146
147#[derive(Subcommand)]
148enum RefsAction {
149 To { page: String },
151 From { page: String },
153 Graph,
155}
156
157#[derive(Subcommand)]
158enum FrontmatterAction {
159 Get {
161 file: PathBuf,
162 field: Option<String>,
164 },
165 Set {
167 file: PathBuf,
168 field: String,
169 value: String,
170 },
171}
172
173#[derive(Subcommand)]
174enum SectionsAction {
175 Rename {
177 old: String,
179 new: String,
181 #[arg(long)]
183 dirs: Option<Vec<String>>,
184 #[arg(long)]
186 write: bool,
187 },
188}
189
190fn resolve_root(cli_root: Option<PathBuf>) -> Result<WikiRoot, WikiError> {
191 match cli_root {
192 Some(path) => WikiRoot::from_path(path),
193 None => {
194 let cwd = std::env::current_dir().map_err(|_| WikiError::RootNotFound {
195 start: PathBuf::from("."),
196 })?;
197 WikiRoot::discover(&cwd)
198 }
199 }
200}
201
202fn run_inner() -> Result<ExitCode, anyhow::Error> {
203 let cli = Cli::parse();
204 let root = resolve_root(cli.root)?;
205
206 match &cli.command {
208 Command::Scan => {
209 let config = WikiConfig::load_or_detect(root.path())?;
210 crate::cmd::agent::scan(&root, &config.ignore)?;
211 return Ok(ExitCode::SUCCESS);
212 }
213 Command::Setup { action } => {
214 match action {
215 SetupAction::Prompt => crate::cmd::agent::setup(&root)?,
216 SetupAction::ExampleConfig => crate::cmd::agent::example_config(),
217 SetupAction::Init { force, show } => crate::cmd::init::init(&root, *force, *show)?,
218 }
219 return Ok(ExitCode::SUCCESS);
220 }
221 _ => {}
222 }
223
224 let config = WikiConfig::load_or_detect(root.path())?;
226 let mut wiki = Wiki::build(root, config)?;
227
228 match cli.command {
229 Command::Links { action } => match action {
230 LinksAction::Check => {
231 let count = crate::cmd::links::check(&wiki)?;
232 if count > 0 {
233 eprintln!("{count} bare mention(s) found");
234 }
235 }
236 LinksAction::Fix { write } => {
237 let count = crate::cmd::links::fix(&mut wiki, write)?;
238 if count > 0 && !write {
239 eprintln!("{count} bare mention(s) to fix. Use --write to apply.");
240 } else if count == 0 {
241 eprintln!("no bare mentions found");
242 }
243 }
244 LinksAction::Broken => {
245 let count = crate::cmd::links::broken(&wiki)?;
246 if count > 0 {
247 eprintln!("{count} broken link(s) found");
248 return Ok(ExitCode::from(1));
249 }
250 }
251 LinksAction::Orphans => {
252 let count = crate::cmd::links::orphans(&wiki)?;
253 if count > 0 {
254 eprintln!("{count} orphan page(s) found");
255 }
256 }
257 },
258
259 Command::Lint { severity } => {
260 let errors = crate::cmd::lint::lint(&wiki, severity.into())?;
261 if errors > 0 {
262 return Ok(ExitCode::from(2));
263 }
264 }
265
266 Command::Rename { old, new, write } => {
267 crate::cmd::rename::rename(&mut wiki, &old, &new, write)?;
268 }
269
270 Command::Move {
271 page,
272 dest_dir,
273 write,
274 } => {
275 crate::cmd::move_page::move_page(&mut wiki, &page, &dest_dir, write)?;
276 }
277
278 Command::Refs { action } => match action {
279 RefsAction::To { page } => {
280 crate::cmd::refs::refs_to(&wiki, &page)?;
281 }
282 RefsAction::From { page } => {
283 crate::cmd::refs::refs_from(&wiki, &page)?;
284 }
285 RefsAction::Graph => {
286 crate::cmd::refs::refs_graph(&wiki)?;
287 }
288 },
289
290 Command::Sections { action } => match action {
291 SectionsAction::Rename {
292 old,
293 new,
294 dirs,
295 write,
296 } => {
297 let count = crate::cmd::sections::rename(&mut wiki, &old, &new, &dirs, write)?;
298 if count > 0 && !write {
299 eprintln!("{count} occurrence(s) to rename. Use --write to apply.");
300 } else if count == 0 {
301 eprintln!("no occurrences of '{}' found", old);
302 }
303 }
304 },
305
306 Command::Frontmatter { action } => match action {
307 FrontmatterAction::Get { file, field } => {
308 crate::cmd::frontmatter_cmd::get(&wiki, &file, field.as_deref())?;
309 }
310 FrontmatterAction::Set { file, field, value } => {
311 crate::cmd::frontmatter_cmd::set(&mut wiki, &file, &field, &value)?;
312 }
313 },
314
315 Command::Scan | Command::Setup { .. } => unreachable!(),
317 }
318
319 Ok(ExitCode::SUCCESS)
320}
321
322pub fn run() -> ExitCode {
323 match run_inner() {
324 Ok(code) => code,
325 Err(e) => {
326 eprintln!("error: {e:#}");
327 ExitCode::FAILURE
328 }
329 }
330}