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