Skip to main content

semora_cli/
args.rs

1//! Command-line arguments.
2
3use std::path::PathBuf;
4
5use clap::{Args, Parser, Subcommand};
6
7#[derive(Debug, Parser)]
8#[command(
9    name = "semora",
10    version,
11    about = "Publish Markdown and HTML to Semora and hear back from the page",
12    after_help = "Configuration: SEMORA_URL, SEMORA_API_KEY, SEMORA_DEFAULT_TAGS, SEMORA_WAIT_TIMEOUT \
13                  (environment or a .env file in this folder or a parent), or `semora login`. \
14                  A `.semora` file marks a project folder (see `semora project init`).\n\
15                  Exit codes: 0 ok, 1 error, 2 timeout, 3 auth error, 4 not found, \
16                  5 newer on the server (push)."
17)]
18pub struct Cli {
19    /// Server URL (default: SEMORA_URL, config.toml, or the stored login)
20    #[arg(long, global = true, value_name = "URL")]
21    pub server: Option<String>,
22    /// API key (default: SEMORA_API_KEY, then the stored login)
23    #[arg(long, global = true, value_name = "KEY", hide_env_values = true)]
24    pub api_key: Option<String>,
25    /// Print JSON instead of text
26    #[arg(long, global = true)]
27    pub json: bool,
28    #[command(subcommand)]
29    pub cmd: Cmd,
30}
31
32#[derive(Debug, Subcommand)]
33pub enum Cmd {
34    /// Sign in through the browser and store an API key for this machine
35    Login {
36        /// Print the URL instead of opening a browser; paste the code shown after approval
37        #[arg(long)]
38        no_browser: bool,
39        /// Name for the new key (default: cli@HOSTNAME)
40        #[arg(long)]
41        name: Option<String>,
42    },
43    /// Revoke and forget the stored key
44    Logout,
45    /// Show the server, user and key in use
46    Whoami,
47    /// Publish a file or folder; replaces the remembered document if there is one
48    Push(PushArgs),
49    /// Re-push whenever files change (debounced 500 ms)
50    Watch(WatchArgs),
51    /// Copy the project's documents from the server into this folder
52    Pull(PullArgs),
53    /// List what changed here and what is newer on the server
54    Status {
55        /// Only documents at this path or under it (default: the current folder)
56        path: Option<PathBuf>,
57    },
58    /// Block until feedback arrives from the page
59    Wait(WaitArgs),
60    /// Show a message in the open page
61    Say {
62        /// Document id or URL
63        doc: String,
64        /// Text to show
65        text: String,
66        /// Event id this message answers
67        #[arg(long)]
68        reply_to: Option<i64>,
69    },
70    /// List or search the documents you can open
71    Ls {
72        /// Full-text search
73        #[arg(short = 'q', long = "query")]
74        query: Option<String>,
75        /// Only documents with this tag
76        #[arg(long)]
77        tag: Option<String>,
78        /// md, html or bundle
79        #[arg(long)]
80        kind: Option<String>,
81        /// Only documents in this project (its id)
82        #[arg(long)]
83        project: Option<String>,
84        #[arg(long, default_value_t = 50)]
85        limit: u32,
86    },
87    /// Metadata, files and revisions of a document
88    Show {
89        /// Document id or URL
90        doc: String,
91    },
92    /// Download raw content
93    Get {
94        /// Document id or URL
95        doc: String,
96        /// File inside a bundle (default: the entry page)
97        path: Option<String>,
98        /// Revision (default: latest)
99        #[arg(long)]
100        rev: Option<i64>,
101        /// Write to a file instead of stdout
102        #[arg(short = 'o', long = "output")]
103        output: Option<PathBuf>,
104    },
105    /// Edit tags: `+add -remove` (a bare name adds)
106    Tag {
107        /// Document id or URL
108        doc: String,
109        #[arg(required = true, num_args = 1.., allow_hyphen_values = true)]
110        changes: Vec<String>,
111    },
112    /// Delete a document
113    Rm {
114        /// Document id or URL
115        doc: String,
116    },
117    /// Open a document in the browser
118    Open {
119        /// Document id or URL
120        doc: String,
121    },
122    /// Manage your API keys
123    #[command(subcommand)]
124    Keys(KeysCmd),
125    /// Make a folder a project, list your projects, and arrange a project's tree
126    #[command(subcommand)]
127    Project(ProjectCmd),
128    /// Print a short usage guide to paste into an agent's instructions
129    Skill,
130}
131
132#[derive(Debug, Args)]
133pub struct PushArgs {
134    /// File (.md or .html) or folder (becomes a bundle)
135    pub path: PathBuf,
136    /// Replace this document instead of the remembered one
137    #[arg(long)]
138    pub id: Option<String>,
139    /// Always create a new document
140    #[arg(long, conflicts_with = "id")]
141    pub new: bool,
142    #[arg(long)]
143    pub title: Option<String>,
144    /// Comma-separated tags (repeatable)
145    #[arg(long = "tag", value_name = "TAGS")]
146    pub tags: Vec<String>,
147    /// Revision message
148    #[arg(short = 'm', long = "message")]
149    pub message: Option<String>,
150    /// Entry page of a bundle
151    #[arg(long)]
152    pub entry: Option<String>,
153    /// key=value stored on the document (repeatable)
154    #[arg(long = "meta", value_name = "KEY=VALUE")]
155    pub meta: Vec<String>,
156    /// In a project: push even if the server has a newer revision than this folder pulled
157    #[arg(long)]
158    pub force: bool,
159    /// After pushing, wait for feedback (same as `semora wait`)
160    #[arg(long)]
161    pub wait: bool,
162    #[command(flatten)]
163    pub wait_opts: WaitOpts,
164}
165
166#[derive(Debug, Args)]
167pub struct PullArgs {
168    /// Only documents at this path or under it (default: the current folder)
169    pub path: Option<PathBuf>,
170    /// Also overwrite files changed here since the last pull or push
171    #[arg(long)]
172    pub force: bool,
173}
174
175#[derive(Debug, Args, Clone, Default)]
176pub struct WaitOpts {
177    /// Seconds to wait (default: SEMORA_WAIT_TIMEOUT or 900)
178    #[arg(long)]
179    pub timeout: Option<u64>,
180    /// Only these kinds: comment,drawing,page_event,message
181    #[arg(long)]
182    pub kinds: Option<String>,
183    /// Keep streaming events as JSON lines instead of exiting
184    #[arg(long)]
185    pub follow: bool,
186    /// Seconds to keep collecting after the first event
187    #[arg(long, default_value_t = 3.0)]
188    pub collect: f64,
189    /// Folder for downloaded screenshots (default: a temp folder)
190    #[arg(long)]
191    pub save_dir: Option<PathBuf>,
192}
193
194#[derive(Debug, Args)]
195pub struct WaitArgs {
196    /// Document id, page URL or websocket URL
197    pub doc: String,
198    #[command(flatten)]
199    pub opts: WaitOpts,
200}
201
202#[derive(Debug, Args)]
203pub struct WatchArgs {
204    /// File or folder to watch
205    pub path: PathBuf,
206    /// Document to replace (default: the remembered one)
207    #[arg(long)]
208    pub id: Option<String>,
209    #[arg(long)]
210    pub title: Option<String>,
211    #[arg(long = "tag", value_name = "TAGS")]
212    pub tags: Vec<String>,
213    #[arg(long)]
214    pub entry: Option<String>,
215}
216
217#[derive(Debug, Subcommand)]
218pub enum KeysCmd {
219    /// List your keys
220    Ls,
221    /// Create a key and print it once
222    Create {
223        name: String,
224        /// Comma-separated scopes (default: all)
225        #[arg(long)]
226        scopes: Option<String>,
227        /// Expire after this many days
228        #[arg(long)]
229        expires_in_days: Option<u32>,
230    },
231    /// Revoke a key by id
232    Revoke { id: i64 },
233}
234
235#[derive(Debug, Subcommand)]
236pub enum ProjectCmd {
237    /// Make a folder a project: create it on the server and write .semora
238    Init {
239        /// The project folder (default: the current one)
240        dir: Option<PathBuf>,
241        /// The project id (default: made from the folder's name)
242        #[arg(long)]
243        name: Option<String>,
244        /// Publish into a project someone else owns and you collaborate on
245        #[arg(long)]
246        join: bool,
247    },
248    /// List your projects
249    Ls,
250    /// Arrange how documents show in the project's tree (display only; links never change)
251    Map(MapArgs),
252}
253
254#[derive(Debug, Args)]
255pub struct MapArgs {
256    /// Project id (default: the .semora file in this folder or a parent)
257    #[arg(long, global = true)]
258    pub project: Option<String>,
259    #[command(subcommand)]
260    pub cmd: MapCmd,
261}
262
263#[derive(Debug, Subcommand)]
264pub enum MapCmd {
265    /// List the rules in order
266    Ls,
267    /// Add a rule: documents matching PATTERN show inside FOLDER ("/" is the project root)
268    Add {
269        /// For example 'docs/**' (quote it, or the shell expands it)
270        pattern: String,
271        /// For example misc/ or / (use . if your shell rewrites a leading slash)
272        folder: String,
273        /// Put the rule at this number (1 is first); default: last
274        #[arg(long, value_name = "N")]
275        at: Option<usize>,
276    },
277    /// Remove rule number N (see `map ls`)
278    Rm {
279        /// The rule's number, from `map ls`
280        n: usize,
281    },
282}