1use std::path::{Path, PathBuf};
4
5use clap::{Parser, Subcommand};
6
7use crate::build;
8use crate::config::{Config, DEFAULT_CONFIG_FILE};
9use crate::dev::{self, DevOptions};
10use crate::scaffold::{self, InitOptions, title_from_slug};
11
12#[derive(Debug, Parser)]
14#[command(
15 name = "orbit",
16 bin_name = "orbit",
17 version,
18 about = "Orbit — Markdown static sites with React-like components",
19 long_about = "Orbit (orbit-md) compiles Markdown pages and JSX-style components into static HTML.\n\nInstall: cargo install orbit-md\nInit: orbit init my-site\nBuild: orbit build"
20)]
21pub struct Cli {
22 #[command(subcommand)]
23 pub command: Command,
24}
25
26#[derive(Debug, Subcommand)]
28pub enum Command {
29 Init {
31 path: PathBuf,
33
34 #[arg(short, long)]
36 title: Option<String>,
37 },
38
39 Build {
41 #[arg(short, long, default_value = DEFAULT_CONFIG_FILE)]
43 config: PathBuf,
44 },
45
46 Dev {
48 #[arg(short, long, default_value = DEFAULT_CONFIG_FILE)]
50 config: PathBuf,
51
52 #[arg(short, long, default_value_t = 3000)]
54 port: u16,
55
56 #[arg(long, default_value = "127.0.0.1")]
58 host: String,
59
60 #[arg(long)]
62 open: bool,
63 },
64
65 New {
67 #[command(subcommand)]
68 target: NewTarget,
69 },
70}
71
72#[derive(Debug, Subcommand)]
74pub enum NewTarget {
75 Page {
77 path: PathBuf,
79
80 #[arg(short, long)]
82 title: Option<String>,
83 },
84}
85
86pub fn execute(cli: Cli) -> anyhow::Result<()> {
88 match cli.command {
89 Command::Init { path, title } => {
90 let mut options = InitOptions::from_path(path.clone());
91 if let Some(custom_title) = title {
92 options.title = custom_title;
93 }
94
95 scaffold::init_project(&options).map_err(anyhow::Error::from)?;
96 print_init_success(&path);
97 }
98 Command::Build { config } => {
99 let site_config =
100 Config::load(&config)
101 .map_err(anyhow::Error::from)
102 .map_err(|err| {
103 anyhow::anyhow!("loading config from {}: {err}", config.display())
104 })?;
105
106 let report = build(&site_config).map_err(anyhow::Error::from)?;
107 println!(
108 "Built {} pages in {} ms → {}",
109 report.pages_written,
110 report.elapsed_ms,
111 site_config.output_dir.display()
112 );
113 }
114 Command::Dev {
115 config,
116 port,
117 host,
118 open,
119 } => {
120 dev::run(&DevOptions {
121 config_path: config,
122 host,
123 port,
124 open,
125 })
126 .map_err(anyhow::Error::from)?;
127 }
128 Command::New { target } => match target {
129 NewTarget::Page { path, title } => {
130 let config = Config::load(DEFAULT_CONFIG_FILE).map_err(anyhow::Error::from)?;
131 let page_title = title.unwrap_or_else(|| default_page_title(&path));
132 let created = scaffold::new_page(&config.source_dir, &path, &page_title)
133 .map_err(anyhow::Error::from)?;
134 println!("Created {}", created.display());
135 }
136 },
137 }
138
139 Ok(())
140}
141
142fn print_init_success(path: &Path) {
143 let name = path
144 .file_name()
145 .and_then(|n| n.to_str())
146 .unwrap_or("your-site");
147
148 println!("Created Orbit project at {}", path.display());
149 println!();
150 println!(" cd {name}");
151 println!(" orbit dev");
152 println!();
153 println!("Edit Markdown in content/ and components in components/.");
154}
155
156fn default_page_title(path: &Path) -> String {
157 path.file_stem()
158 .and_then(|s| s.to_str())
159 .map(title_from_slug)
160 .filter(|t| !t.is_empty())
161 .unwrap_or_else(|| "Untitled".to_owned())
162}