Skip to main content

orbit_md/cli/
mod.rs

1//! Command-line interface for Orbit.
2
3use 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/// Fast static site generator with React-like Markdown components.
13#[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/// Available CLI commands.
27#[derive(Debug, Subcommand)]
28pub enum Command {
29    /// Create a new Orbit site in a directory.
30    Init {
31        /// Project directory to create (e.g. `my-site`).
32        path: PathBuf,
33
34        /// Site title used in config and starter pages.
35        #[arg(short, long)]
36        title: Option<String>,
37    },
38
39    /// Compile Markdown pages to HTML in `.orbit/`.
40    Build {
41        /// Path to the site config file.
42        #[arg(short, long, default_value = DEFAULT_CONFIG_FILE)]
43        config: PathBuf,
44    },
45
46    /// Start a local dev server with watch-and-rebuild.
47    Dev {
48        /// Path to the site config file.
49        #[arg(short, long, default_value = DEFAULT_CONFIG_FILE)]
50        config: PathBuf,
51
52        /// Port for the dev server.
53        #[arg(short, long, default_value_t = 3000)]
54        port: u16,
55
56        /// Host to bind the dev server.
57        #[arg(long, default_value = "127.0.0.1")]
58        host: String,
59
60        /// Open the site in your default browser.
61        #[arg(long)]
62        open: bool,
63    },
64
65    /// Create a new Markdown page under `content/`.
66    New {
67        #[command(subcommand)]
68        target: NewTarget,
69    },
70}
71
72/// Targets for `orbit new`.
73#[derive(Debug, Subcommand)]
74pub enum NewTarget {
75    /// Create a new page file.
76    Page {
77        /// Page path relative to `content/` (extension optional).
78        path: PathBuf,
79
80        /// Page title for front matter.
81        #[arg(short, long)]
82        title: Option<String>,
83    },
84}
85
86/// Executes the parsed CLI command.
87pub 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}