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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
use std::path::PathBuf;
use clap::{Parser, Subcommand};
/// Point local apps at any backend stand, keep servers and secrets at hand,
/// build and deploy - from any directory.
#[derive(Parser)]
#[command(name = "turnout", version, about, propagate_version = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
/// Initialize the data directory and walk through first-run setup
Setup {
/// Skip confirmation prompts and accept defaults
#[arg(short = 'y', long = "yes")]
assume_yes: bool,
},
/// Show what turnout knows: apps, servers, bindings, gateway state
Status,
/// Manage apps - the local projects turnout works with
App {
#[command(subcommand)]
command: AppCommand,
},
/// Manage servers - the stands turnout routes to and deploys on
Server {
#[command(subcommand)]
command: ServerCommand,
},
/// Manage access to servers: logins and secrets in the OS keyring
Pass {
#[command(subcommand)]
command: PassCommand,
},
/// Bind an app (or a whole group) to a server - the daily switch command
Use {
/// App or group name; picked interactively when omitted
app: Option<String>,
/// Target server; picked interactively when omitted
server: Option<String>,
/// Skip the stand reachability check
#[arg(long)]
no_check: bool,
},
/// Manage app groups - switch a whole contour with one `use`
Group {
#[command(subcommand)]
command: GroupCommand,
},
/// Run the local dev gateway that routes apps to their bound servers
Gateway {
#[command(subcommand)]
command: GatewayCommand,
},
/// Run the app's `dev` command (app resolved from the current directory if omitted)
Dev { app: Option<String> },
/// Run the app's `build` command
Build { app: Option<String> },
/// Run the app's `test` command
Test { app: Option<String> },
/// Run the app's `lint` command
Lint { app: Option<String> },
/// Run any named command from the app config
Run { command: String, app: Option<String> },
/// Set up deployment of an app to a server in one wizard
#[command(name = "deploy-setup", alias = "setup-deploy")]
DeploySetup {
/// App name; picked interactively when omitted
app: Option<String>,
/// Target server; picked interactively when omitted
#[arg(long)]
server: Option<String>,
},
/// Deploy an app to a server over SSH/SFTP: build, upload, restart
Deploy {
app: Option<String>,
/// Target server (defaults to the app's current binding)
#[arg(long)]
server: Option<String>,
/// Skip the build step
#[arg(long)]
no_build: bool,
/// Back up the remote directory before touching it
#[arg(long)]
backup: bool,
/// Clear the remote directory before uploading
#[arg(long)]
clear: bool,
},
/// Back up an app's deploy directory on the server
Backup {
app: Option<String>,
/// Target server (defaults to the app's current binding)
#[arg(long)]
server: Option<String>,
},
/// Restore an app's deploy directory from a backup
Restore {
app: Option<String>,
/// Target server (defaults to the app's current binding)
#[arg(long)]
server: Option<String>,
/// Backup archive name (defaults to the newest)
#[arg(long)]
from: Option<String>,
/// List available backups and exit
#[arg(long)]
list: bool,
},
/// Print a shell completion script (source it from your shell profile)
Completions {
/// Target shell
shell: clap_complete::Shell,
},
/// List entity names for shell completion (internal)
#[command(hide = true)]
Complete {
/// What to list
what: CompleteKind,
},
}
/// Entity kinds the completion helper can list.
#[derive(Clone, Copy, clap::ValueEnum)]
pub enum CompleteKind {
Apps,
Servers,
Groups,
/// Apps and groups together - what `use` accepts first
Targets,
/// Command names defined across all apps - what `run` accepts
Commands,
}
#[derive(Subcommand)]
pub enum GroupCommand {
/// Create a group; apps are picked interactively when not passed
Add {
/// Group name (lowercase letters, digits, dashes)
name: Option<String>,
/// App to include (repeatable)
#[arg(long = "app", value_name = "APP")]
apps: Vec<String>,
},
/// List groups
List,
/// Show a group and where its apps point
Show {
/// Group name; picked interactively when omitted
name: Option<String>,
},
/// Add or remove apps in a group
Edit {
/// Group name; picked interactively when omitted
name: Option<String>,
/// Add an app (repeatable)
#[arg(long = "add-app", value_name = "APP")]
add_apps: Vec<String>,
/// Remove an app (repeatable)
#[arg(long = "rm-app", value_name = "APP")]
rm_apps: Vec<String>,
},
/// Remove a group (its apps are untouched)
Remove {
/// Group name; picked interactively when omitted
name: Option<String>,
/// Skip the confirmation prompt
#[arg(short = 'y', long = "yes")]
assume_yes: bool,
},
}
#[derive(Subcommand)]
pub enum GatewayCommand {
/// Start the gateway in the background
Start,
/// Run the gateway in the foreground (what `start` spawns)
Run,
/// Stop the background gateway
Stop,
}
#[derive(Subcommand)]
pub enum PassCommand {
/// Save or update access to a server; asks interactively for what's missing
Set {
/// Server name from the catalog
server: Option<String>,
/// What this access is: password, token, ssh, ...
#[arg(long, default_value = "password")]
kind: String,
#[arg(long)]
login: Option<String>,
},
/// Copy the secret (or login) to the clipboard
Copy {
/// Server name; picked interactively when omitted
server: Option<String>,
/// Access kind; when omitted the picker offers every stored kind
#[arg(long)]
kind: Option<String>,
/// Copy the login instead of the secret
#[arg(long)]
login: bool,
/// Print to stdout instead of copying to the clipboard
#[arg(long)]
show: bool,
},
/// Show access metadata for a server (never prints secrets)
Show {
/// Server name; picked interactively when omitted
server: Option<String>,
},
/// List all stored access metadata
List,
/// Remove stored access to a server
Remove {
/// Server name; picked interactively when omitted
server: Option<String>,
/// Access kind; when omitted the picker offers every stored kind
#[arg(long)]
kind: Option<String>,
/// Skip the confirmation prompt
#[arg(short = 'y', long = "yes")]
assume_yes: bool,
},
}
#[derive(Subcommand)]
pub enum AppCommand {
/// Add an app; missing details are asked interactively
Add {
/// App name (lowercase letters, digits, dashes)
name: Option<String>,
/// Project directory
#[arg(long)]
path: Option<PathBuf>,
/// Local gateway port for this app
#[arg(long)]
port: Option<u16>,
/// Build artifact directory, relative to the project path
#[arg(long)]
dist: Option<String>,
/// Set a command as NAME=CMD (repeatable); overrides detected defaults
#[arg(long = "command", value_name = "NAME=CMD")]
commands: Vec<String>,
/// Allow a server for this app (repeatable)
#[arg(long = "server", value_name = "SERVER")]
servers: Vec<String>,
},
/// List apps
List,
/// Show one app in detail
Show {
/// App name; picked interactively when omitted
name: Option<String>,
},
/// Edit an app; with no flags an interactive wizard walks the fields
Edit {
/// App name; picked interactively when omitted
name: Option<String>,
#[arg(long)]
path: Option<PathBuf>,
#[arg(long)]
port: Option<u16>,
#[arg(long)]
dist: Option<String>,
/// Set a command as NAME=CMD, or NAME= to remove it (repeatable)
#[arg(long = "command", value_name = "NAME=CMD")]
commands: Vec<String>,
/// Allow a server (repeatable)
#[arg(long = "add-server", value_name = "SERVER")]
add_servers: Vec<String>,
/// Disallow a server (repeatable)
#[arg(long = "rm-server", value_name = "SERVER")]
rm_servers: Vec<String>,
},
/// Remove an app from the catalog (the project on disk is not touched)
Remove {
/// App name; picked interactively when omitted
name: Option<String>,
/// Skip the confirmation prompt
#[arg(short = 'y', long = "yes")]
assume_yes: bool,
},
}
#[derive(Subcommand)]
pub enum ServerCommand {
/// Add a server; missing details are asked interactively
Add {
/// Server name (lowercase letters, digits, dashes)
name: Option<String>,
/// Base URL, e.g. https://staging.example.com
#[arg(long)]
url: Option<String>,
/// Human-friendly label
#[arg(long)]
label: Option<String>,
/// SSH access for deploy and remote operations
#[arg(long, value_name = "USER@HOST[:PORT]")]
ssh: Option<String>,
/// Accept self-signed or invalid TLS certificates for this server
#[arg(long)]
insecure: bool,
},
/// List servers
List,
/// Show one server in detail
Show {
/// Server name; picked interactively when omitted
name: Option<String>,
},
/// Edit a server; with no flags an interactive wizard walks the fields
Edit {
/// Server name; picked interactively when omitted
name: Option<String>,
#[arg(long)]
url: Option<String>,
#[arg(long)]
label: Option<String>,
#[arg(long, value_name = "USER@HOST[:PORT]")]
ssh: Option<String>,
/// Private key file for SSH auth (empty value removes it)
#[arg(long = "ssh-key", value_name = "PATH")]
ssh_key: Option<String>,
/// Accept invalid TLS certificates for this server
#[arg(long, conflicts_with = "secure")]
insecure: bool,
/// Require valid TLS certificates for this server
#[arg(long)]
secure: bool,
/// Set an app's deploy directory as APP=DIR, or APP= to remove (repeatable)
#[arg(long = "deploy-path", value_name = "APP=DIR")]
deploy_paths: Vec<String>,
/// Set an app's post-deploy command as APP=CMD, or APP= to remove (repeatable)
#[arg(long = "restart-cmd", value_name = "APP=CMD")]
restart_cmds: Vec<String>,
},
/// Remove a server from the catalog; apps that allowed it are updated
Remove {
/// Server name; picked interactively when omitted
name: Option<String>,
/// Skip the confirmation prompt
#[arg(short = 'y', long = "yes")]
assume_yes: bool,
},
}