1use std::process::ExitCode;
2
3use clap::Parser;
4
5use crate::cli;
6use crate::cli::{Cli, Command};
7use crate::cli_helpers::{read_file, scope_from_arg, targets_from_flags};
8use crate::commands::serialize_pretty;
9use crate::{commands, history, mcp, selfupdate};
10
11mod library;
12mod render;
13mod workspace;
14
15pub fn run() -> ExitCode {
20 let cli = Cli::parse();
21
22 match cli.command {
23 Command::New(args) => match commands::new::run(&args.path, args.name.as_deref()) {
24 Ok(result) => {
25 if let Some(w) = &result.warning {
26 eprintln!("warning: {w}");
27 }
28 println!(
29 "created '{}' (doc-id: {})",
30 result.path.display(),
31 result.doc_id
32 );
33 ExitCode::SUCCESS
34 }
35 Err(e) => {
36 eprintln!("{}", e.message);
37 ExitCode::from(e.exit_code)
38 }
39 },
40
41 Command::Validate(args) => {
42 let src = match read_file(&args.path) {
43 Ok(s) => s,
44 Err(msg) => {
45 eprintln!("{}", msg);
46 return ExitCode::from(2);
47 }
48 };
49 let flags = crate::config::CliPolicyFlags {
50 allow: args.allow,
51 warn: args.warn,
52 deny: args.deny,
53 };
54 let out = commands::validate::run(&src, args.path.parent(), args.json, &flags);
55 println!("{}", out.stdout);
56 ExitCode::from(out.exit_code)
57 }
58
59 Command::Fmt(args) => {
60 let src = match read_file(&args.path) {
61 Ok(s) => s,
62 Err(msg) => {
63 eprintln!("{}", msg);
64 return ExitCode::from(2);
65 }
66 };
67 match commands::fmt::run(&src) {
68 Ok(result) => {
69 if let Err(e) = std::fs::write(&args.path, &result.formatted) {
71 eprintln!("error writing '{}': {}", args.path.display(), e);
72 return ExitCode::from(2);
73 }
74 println!("{}", commands::fmt::render_stdout(&result, args.json));
75 ExitCode::SUCCESS
76 }
77 Err(e) => {
78 eprintln!("{}", e.message);
79 ExitCode::from(e.exit_code)
80 }
81 }
82 }
83
84 Command::Tokens(args) => {
85 let src = match read_file(&args.path) {
86 Ok(s) => s,
87 Err(msg) => {
88 eprintln!("{}", msg);
89 return ExitCode::from(2);
90 }
91 };
92 match commands::tokens::list(&src, args.json) {
93 Ok(out) => {
94 println!("{}", out);
95 ExitCode::SUCCESS
96 }
97 Err((msg, code)) => {
98 eprintln!("{}", msg);
99 ExitCode::from(code)
100 }
101 }
102 }
103
104 Command::Render(args) => render::dispatch_render(args),
105
106 Command::Inspect(args) => {
107 let src = match read_file(&args.path) {
108 Ok(s) => s,
109 Err(msg) => {
110 eprintln!("{}", msg);
111 return ExitCode::from(2);
112 }
113 };
114 match commands::inspect::run(&src, args.node.as_deref(), args.json) {
115 Ok(out) => {
116 println!("{}", out);
117 ExitCode::SUCCESS
118 }
119 Err(e) => {
120 eprintln!("{}", e.message);
121 ExitCode::from(e.exit_code)
122 }
123 }
124 }
125
126 Command::Merge(args) => {
127 let doc_src = match read_file(&args.doc) {
129 Ok(s) => s,
130 Err(msg) => {
131 eprintln!("{}", msg);
132 return ExitCode::from(2);
133 }
134 };
135
136 let csv_src = match read_file(&args.data) {
138 Ok(s) => s,
139 Err(msg) => {
140 eprintln!("{}", msg);
141 return ExitCode::from(2);
142 }
143 };
144
145 let project_dir = args.doc.parent();
146
147 match commands::merge::run(
148 &doc_src,
149 &csv_src,
150 project_dir,
151 &args.out_dir,
152 args.name_by.as_deref(),
153 ) {
154 Ok(report) => {
155 if args.json {
156 println!(
157 "{}",
158 serialize_pretty(&commands::merge::to_json_output(&report))
159 );
160 } else {
161 let n_written = report.rows.iter().filter(|r| r.failure.is_none()).count();
162 println!(
163 "wrote {} file(s) to '{}'",
164 n_written,
165 args.out_dir.display()
166 );
167 for r in report.failed() {
168 eprintln!("row {}: {}", r.row + 1, r.failure.as_deref().unwrap_or(""));
169 }
170 }
171 if let Some(manifest_path) = &args.manifest {
172 let manifest = commands::merge::build_manifest(
173 &doc_src,
174 &csv_src,
175 args.name_by.as_deref(),
176 &report,
177 );
178 let manifest_json = serialize_pretty(&manifest);
179 if let Some(parent) = manifest_path.parent()
180 && !parent.as_os_str().is_empty()
181 && let Err(e) = std::fs::create_dir_all(parent)
182 {
183 eprintln!(
184 "error creating manifest directory '{}': {}",
185 parent.display(),
186 e
187 );
188 return ExitCode::from(2);
189 }
190 if let Err(e) = std::fs::write(manifest_path, manifest_json.as_bytes()) {
191 eprintln!(
192 "error writing manifest '{}': {}",
193 manifest_path.display(),
194 e
195 );
196 return ExitCode::from(2);
197 }
198 }
199 let n_failed = report.rows.iter().filter(|r| r.failure.is_some()).count();
200 if n_failed == 0 {
201 ExitCode::SUCCESS
202 } else {
203 ExitCode::from(1u8)
204 }
205 }
206 Err(e) => {
207 eprintln!("{}", e.message);
208 ExitCode::from(e.exit_code)
209 }
210 }
211 }
212
213 Command::Library(args) => library::dispatch_library(args),
214
215 Command::History(args) => {
216 match history::history_view(&args.path) {
217 Ok(view) => {
218 if args.json {
219 let versions_json: Vec<serde_json::Value> = view
220 .versions
221 .iter()
222 .map(|v| {
223 serde_json::json!({
224 "id": v.id,
225 "seq": v.seq,
226 "label": v.label,
227 "op_kind": v.op_kind,
228 "timestamp_ms": v.timestamp_ms,
229 })
230 })
231 .collect();
232 let obj = serde_json::json!({
233 "doc_id": view.doc_id,
234 "has_session": view.has_session,
235 "versions": versions_json,
236 });
237 match serde_json::to_string_pretty(&obj) {
238 Ok(s) => println!("{}", s),
239 Err(_) => {
240 println!("doc-id: {}", view.doc_id);
242 for v in &view.versions {
243 let label = v.label.as_deref().unwrap_or("");
244 let op = v.op_kind.as_deref().unwrap_or("");
245 println!("{:>4} {} {} {}", v.seq, v.id, op, label);
246 }
247 }
248 }
249 } else {
250 println!("doc-id: {}", view.doc_id);
251 if view.versions.is_empty() {
252 println!("(no versions recorded yet)");
253 } else {
254 for v in &view.versions {
255 let label = v.label.as_deref().unwrap_or("");
256 let op = v.op_kind.as_deref().unwrap_or("");
257 println!("{:>4} {} {} {}", v.seq, v.id, op, label);
258 }
259 }
260 }
261 ExitCode::SUCCESS
262 }
263 Err(msg) => {
264 eprintln!("{}", msg);
265 ExitCode::from(2)
266 }
267 }
268 }
269
270 Command::Undo(args) => match history::undo_edit(&args.path) {
271 Ok(history::NavOutcome::Moved) => {
272 println!("undid last edit to '{}'", args.path.display());
273 ExitCode::SUCCESS
274 }
275 Ok(history::NavOutcome::NothingToDo) => {
276 println!("nothing to undo");
277 ExitCode::SUCCESS
278 }
279 Err(msg) => {
280 eprintln!("{}", msg);
281 ExitCode::from(2)
282 }
283 },
284
285 Command::Redo(args) => match history::redo_edit(&args.path) {
286 Ok(history::NavOutcome::Moved) => {
287 println!("redid last undone edit to '{}'", args.path.display());
288 ExitCode::SUCCESS
289 }
290 Ok(history::NavOutcome::NothingToDo) => {
291 println!("nothing to redo");
292 ExitCode::SUCCESS
293 }
294 Err(msg) => {
295 eprintln!("{}", msg);
296 ExitCode::from(2)
297 }
298 },
299
300 Command::Version(args) => match history::name_version(&args.path, &args.name) {
301 Ok(id) => {
302 println!("saved version '{}' as {}", args.name, id);
303 ExitCode::SUCCESS
304 }
305 Err(msg) => {
306 eprintln!("{msg}");
307 ExitCode::from(2)
308 }
309 },
310
311 Command::Restore(args) => match history::restore(&args.path, &args.rev) {
312 Ok(outcome) => {
313 if let Some(w) = &outcome.warning {
314 eprintln!("warning: {w}");
315 }
316 println!(
317 "restored '{}' to {}",
318 args.path.display(),
319 outcome.version_id
320 );
321 ExitCode::SUCCESS
322 }
323 Err(msg) => {
324 eprintln!("{msg}");
325 ExitCode::from(2)
326 }
327 },
328
329 Command::Sync(args) => match history::sync_external(&args.path) {
330 Ok(history::SyncOutcome::Captured { id }) => {
331 println!("captured external change as {id}");
332 ExitCode::SUCCESS
333 }
334 Ok(history::SyncOutcome::AlreadyInSync) => {
335 println!("already in sync");
336 ExitCode::SUCCESS
337 }
338 Err(msg) => {
339 eprintln!("{msg}");
340 ExitCode::from(2)
341 }
342 },
343
344 Command::Tx(args) => {
345 let doc_src = match read_file(&args.path) {
347 Ok(s) => s,
348 Err(msg) => {
349 eprintln!("{}", msg);
350 return ExitCode::from(2);
351 }
352 };
353
354 let tx_json = match read_file(&args.tx_file) {
356 Ok(s) => s,
357 Err(msg) => {
358 eprintln!("{}", msg);
359 return ExitCode::from(2);
360 }
361 };
362
363 let outcome = match commands::tx::run(&doc_src, &tx_json) {
365 Ok(o) => o,
366 Err(e) => {
367 eprintln!("{}", e.message);
368 return ExitCode::from(e.exit_code);
369 }
370 };
371
372 if args.json {
374 println!("{}", outcome.json_str);
375 } else {
376 println!("{}", outcome.human);
377 }
378
379 if args.apply && outcome.exit_code != 1 {
381 let recorded = history::record_edit(
382 outcome.result.source_after.as_bytes(),
383 &args.path,
384 "tx.apply",
385 );
386 if let Some(w) = &recorded.warning {
387 eprintln!("warning: {w}");
388 }
389 if let Err(e) = std::fs::write(&args.path, &recorded.bytes) {
390 eprintln!("error writing '{}': {}", args.path.display(), e);
391 return ExitCode::from(2);
392 }
393 }
394
395 ExitCode::from(outcome.exit_code)
396 }
397
398 Command::Variant(args) => {
399 let doc_src = match read_file(&args.doc) {
400 Ok(s) => s,
401 Err(msg) => {
402 eprintln!("{}", msg);
403 return ExitCode::from(2);
404 }
405 };
406
407 let stem = args
409 .doc
410 .file_stem()
411 .and_then(|s| s.to_str())
412 .unwrap_or("doc");
413 let project_dir = args.doc.parent();
414
415 match commands::variant::run_variant(&doc_src, project_dir, &args.out_dir, stem) {
416 Ok(report) => {
417 let failed = report.failed();
418 if args.json {
419 println!(
420 "{}",
421 serialize_pretty(&commands::variant::to_json_output(&report))
422 );
423 } else {
424 println!(
425 "generated {} variant(s) to '{}'",
426 report.generated(),
427 args.out_dir.display()
428 );
429 for r in &failed {
430 eprintln!("variant {}: {}", r.id, r.failure.as_deref().unwrap_or(""));
431 }
432 }
433 if let Some(manifest_path) = &args.manifest {
434 let manifest = commands::variant::build_manifest(&doc_src, &report);
435 let manifest_json = serialize_pretty(&manifest);
436 if let Some(parent) = manifest_path.parent()
437 && !parent.as_os_str().is_empty()
438 && let Err(e) = std::fs::create_dir_all(parent)
439 {
440 eprintln!(
441 "error creating manifest directory '{}': {}",
442 parent.display(),
443 e
444 );
445 return ExitCode::from(2);
446 }
447 if let Err(e) = std::fs::write(manifest_path, manifest_json.as_bytes()) {
448 eprintln!(
449 "error writing manifest '{}': {}",
450 manifest_path.display(),
451 e
452 );
453 return ExitCode::from(2);
454 }
455 }
456 if failed.is_empty() {
457 ExitCode::SUCCESS
458 } else {
459 ExitCode::from(1u8)
460 }
461 }
462 Err(e) => {
463 eprintln!("{}", e.message);
464 ExitCode::from(e.exit_code)
465 }
466 }
467 }
468
469 Command::Update(args) => match selfupdate::run(args.pre, args.version.as_deref()) {
470 Ok(()) => ExitCode::SUCCESS,
471 Err(msg) => {
472 eprintln!("error: {msg}");
473 ExitCode::from(2)
474 }
475 },
476
477 Command::Theme(args) => match args.command {
478 cli::ThemeSub::New(a) => {
479 let scheme = match a.scheme.as_str() {
480 "light" => zenith_core::theme::Scheme::Light,
481 "dark" => zenith_core::theme::Scheme::Dark,
482 other => {
483 eprintln!("error: --scheme must be 'light' or 'dark', got '{other}'");
484 return ExitCode::from(2);
485 }
486 };
487 let input = commands::theme::ThemeInput {
488 name: &a.name,
489 scheme,
490 primary: &a.primary,
491 secondary: a.secondary.as_deref(),
492 accent: a.accent.as_deref(),
493 neutral: a.neutral.as_deref(),
494 info: a.info.as_deref(),
495 success: a.success.as_deref(),
496 warning: a.warning.as_deref(),
497 error: a.error.as_deref(),
498 shape: commands::theme::Shape {
499 radius_box: a.radius_box,
500 radius_field: a.radius_field,
501 radius_selector: a.radius_selector,
502 border: a.border,
503 depth: a.depth,
504 noise: a.noise,
505 },
506 };
507 match commands::theme::new(&input) {
508 Ok(source) => {
509 if let Some(path) = &a.out {
510 if let Err(e) = std::fs::write(path, &source) {
511 eprintln!("error writing '{}': {}", path.display(), e);
512 return ExitCode::from(2);
513 }
514 println!("wrote {}", path.display());
515 } else {
516 print!("{source}");
517 }
518 ExitCode::SUCCESS
519 }
520 Err(e) => {
521 eprintln!("error: {}", e.message);
522 ExitCode::from(e.exit_code)
523 }
524 }
525 }
526 },
527
528 Command::Plugin(args) => {
529 let project_root = std::path::Path::new(".");
530 match args.command {
531 cli::PluginSub::Install(a) => {
532 let targets = targets_from_flags(&a.agents);
533 let code = commands::plugin::run_install(
534 project_root,
535 targets,
536 scope_from_arg(a.scope),
537 a.force,
538 a.dry_run,
539 );
540 ExitCode::from(code)
541 }
542 cli::PluginSub::Uninstall(a) => {
543 let targets = targets_from_flags(&a.agents);
544 let code = commands::plugin::run_uninstall(
545 project_root,
546 targets,
547 scope_from_arg(a.scope),
548 a.dry_run,
549 );
550 ExitCode::from(code)
551 }
552 cli::PluginSub::List => ExitCode::from(commands::plugin::run_list(project_root)),
553 }
554 }
555
556 Command::Mcp(args) => match &args.http {
557 Some(addr) => ExitCode::from(mcp::run_http(addr)),
558 None => ExitCode::from(mcp::run()),
559 },
560
561 Command::Fonts(args) => {
562 let (output, code) = commands::fonts::list(args.json);
563 println!("{}", output);
564 ExitCode::from(code)
565 }
566
567 Command::Schema(args) => {
568 let json = args.json;
569 let (output, code) = match args.command {
570 None => commands::schema::overview(json),
571 Some(cli::SchemaSub::Nodes) => commands::schema::nodes(json),
572 Some(cli::SchemaSub::Node { kind }) => commands::schema::node_detail(&kind, json),
573 Some(cli::SchemaSub::Ops) => commands::schema::ops(json),
574 Some(cli::SchemaSub::Op { name }) => commands::schema::op_detail(&name, json),
575 Some(cli::SchemaSub::Tokens) => commands::schema::tokens(json),
576 Some(cli::SchemaSub::Token { ty }) => commands::schema::token_detail(&ty, json),
577 Some(cli::SchemaSub::Page) => commands::schema::page(json),
578 Some(cli::SchemaSub::Asset) => commands::schema::asset(json),
579 Some(cli::SchemaSub::Document) => commands::schema::document(json),
580 Some(cli::SchemaSub::Variant) => commands::schema::variant(json),
581 Some(cli::SchemaSub::Diagnostics) => commands::schema::diagnostics(json),
582 Some(cli::SchemaSub::Brand) => commands::schema::brand(json),
583 Some(cli::SchemaSub::Block) => commands::schema::block(json),
584 };
585 println!("{}", output);
586 ExitCode::from(code)
587 }
588
589 Command::Workspace(args) => workspace::dispatch_workspace(args),
590 }
591}