turnout 0.13.0

A developer's switchyard: point local apps at any backend stand, keep servers and secrets at hand, build and deploy from any directory
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
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
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 credentials - who logs in, and with a password, a key or the SSH agent
    #[command(alias = "cred")]
    Credential {
        #[command(subcommand)]
        command: CredentialCommand,
    },
    /// Manage paths - named remote directories to deploy into
    Path {
        #[command(subcommand)]
        command: PathCommand,
    },
    /// Manage targets - a named deploy route: app, server, credential and path
    Target {
        #[command(subcommand)]
        command: TargetCommand,
    },
    /// Manage the secrets credentials use, stored in the OS keyring
    Pass {
        #[command(subcommand)]
        command: PassCommand,
    },
    /// Set up SSH key access to a server, so the password is needed once
    Key {
        #[command(subcommand)]
        command: KeyCommand,
    },
    /// 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(short = 'n', 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(short, long)]
        server: Option<String>,
    },
    /// Deploy to a target over SSH/SFTP: build, upload, restart
    Deploy {
        /// Target name, or an app name to use its target on the bound server
        target: Option<String>,
        /// Target server (defaults to the app's current binding)
        #[arg(short, long)]
        server: Option<String>,
        /// Credential to log in with (defaults to the server's)
        #[arg(short = 'C', long)]
        credential: Option<String>,
        /// Named path to deploy into (defaults to the server's for this app)
        #[arg(short = 'p', long)]
        path: Option<String>,
        /// Skip the build step
        #[arg(short = 'n', long)]
        no_build: bool,
        /// Back up the remote directory before touching it
        #[arg(short, long)]
        backup: bool,
        /// Clear the remote directory before uploading
        #[arg(short, long)]
        clear: bool,
        /// Upload file by file instead of packing the artifacts into one archive
        #[arg(short = 'A', long)]
        no_archive: bool,
    },
    /// Back up a target's deploy directory on the server
    Backup {
        /// Target name, or an app name to use its target on the bound server
        target: Option<String>,
        /// Target server (defaults to the app's current binding)
        #[arg(short, long)]
        server: Option<String>,
        /// Credential to log in with (defaults to the server's)
        #[arg(short = 'C', long)]
        credential: Option<String>,
        /// Named path to back up (defaults to the server's for this app)
        #[arg(short = 'p', long)]
        path: Option<String>,
    },
    /// Restore a target's deploy directory from a backup
    Restore {
        /// Target name, or an app name to use its target on the bound server
        target: Option<String>,
        /// Target server (defaults to the app's current binding)
        #[arg(short, long)]
        server: Option<String>,
        /// Credential to log in with (defaults to the server's)
        #[arg(short = 'C', long)]
        credential: Option<String>,
        /// Named path to restore into (defaults to the server's for this app)
        #[arg(short = 'p', long)]
        path: Option<String>,
        /// Backup archive name (defaults to the newest)
        #[arg(short, long)]
        from: Option<String>,
        /// List available backups and exit
        #[arg(short, 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,
    },
    /// Write the catalogs to a file for another machine
    Export {
        /// Where to write; defaults to turnout-export.json in the current directory
        #[arg(short, long)]
        output: Option<std::path::PathBuf>,
        /// Include stored secrets, encrypted with a passphrase you choose
        #[arg(short = 'S', long)]
        with_secrets: bool,
    },
    /// Read a file written by `turnout export`
    Import {
        /// The export file to read
        file: std::path::PathBuf,
        /// Overwrite entries that already exist instead of keeping them
        #[arg(short, long)]
        force: bool,
    },
    /// Update turnout itself to the latest release
    SelfUpdate {
        /// Skip the confirmation prompt
        #[arg(short = 'y', long = "yes")]
        assume_yes: bool,
        /// Replace the binary even when a package manager owns it
        #[arg(short, long)]
        force: bool,
    },
    /// Refresh the cached latest release (internal, run in the background)
    #[command(hide = true)]
    CheckUpdate,
}

/// Entity kinds the completion helper can list.
#[derive(Clone, Copy, clap::ValueEnum)]
pub enum CompleteKind {
    Apps,
    Servers,
    Credentials,
    Paths,
    /// Named deploy targets - what `deploy` accepts
    Targets,
    Groups,
    /// Apps and groups together - what `use` accepts first
    Bindable,
    /// 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(short = 'a', 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(short = 'a', long = "add-app", value_name = "APP")]
        add_apps: Vec<String>,
        /// Remove an app (repeatable)
        #[arg(short = 'r', 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 replace a credential's secret in the OS keyring
    Set {
        /// Credential name; picked interactively when omitted
        credential: Option<String>,
    },
    /// Copy the secret (or the user) to the clipboard
    Copy {
        /// Credential name; picked interactively when omitted
        credential: Option<String>,
        /// Copy the user name instead of the secret
        #[arg(short, long)]
        user: bool,
        /// Print to stdout instead of copying to the clipboard
        #[arg(short, long)]
        show: bool,
    },
    /// List which credentials have a secret stored (never prints secrets)
    List,
    /// Remove a credential's stored secret; the credential itself stays
    Remove {
        /// Credential name; picked interactively when omitted
        credential: Option<String>,
        /// Skip the confirmation prompt
        #[arg(short = 'y', long = "yes")]
        assume_yes: bool,
    },
}

#[derive(Subcommand)]
pub enum KeyCommand {
    /// Generate or reuse a key, authorize it on the server, and switch the
    /// credential to it
    Setup {
        /// Server name; picked interactively when omitted
        server: Option<String>,
        /// Credential to give key access (defaults to the server's)
        #[arg(short, long)]
        credential: Option<String>,
        /// Existing private key file to authorize instead of generating one
        #[arg(short = 'K', long, value_name = "PATH")]
        key: Option<String>,
    },
    /// Check that a credential's key signs in to a server
    Check {
        /// Server name; picked interactively when omitted
        server: Option<String>,
        /// Credential to check (defaults to the server's)
        #[arg(short, long)]
        credential: Option<String>,
    },
}

#[derive(Subcommand)]
pub enum CredentialCommand {
    /// Add a credential; missing details are asked interactively
    Add {
        /// Credential name (lowercase letters, digits, dashes)
        name: Option<String>,
        /// Remote user this logs in as
        #[arg(short, long)]
        user: Option<String>,
        /// How it authenticates: password, key or agent
        #[arg(short, long)]
        auth: Option<String>,
        /// Private key file (implies --auth key)
        #[arg(short = 'K', long, value_name = "PATH")]
        key: Option<String>,
    },
    /// List credentials
    List,
    /// Show one credential in detail (never prints secrets)
    Show {
        /// Credential name; picked interactively when omitted
        name: Option<String>,
    },
    /// Edit a credential; with no flags an interactive wizard walks the fields
    Edit {
        /// Credential name; picked interactively when omitted
        name: Option<String>,
        #[arg(short, long)]
        user: Option<String>,
        /// How it authenticates: password, key or agent
        #[arg(short, long)]
        auth: Option<String>,
        /// Private key file (empty value removes it)
        #[arg(short = 'K', long, value_name = "PATH")]
        key: Option<String>,
    },
    /// Remove a credential and its stored secret
    Remove {
        /// Credential name; picked interactively when omitted
        name: Option<String>,
        /// Skip the confirmation prompt
        #[arg(short = 'y', long = "yes")]
        assume_yes: bool,
    },
}

#[derive(Subcommand)]
pub enum PathCommand {
    /// Add a path; missing details are asked interactively
    Add {
        /// Path name (lowercase letters, digits, dashes)
        name: Option<String>,
        /// Absolute directory on the server
        #[arg(short, long)]
        dir: Option<String>,
        /// Command to run on the server after writing here
        #[arg(short, long)]
        restart: Option<String>,
    },
    /// List paths
    List,
    /// Show one path in detail
    Show {
        /// Path name; picked interactively when omitted
        name: Option<String>,
    },
    /// Edit a path; with no flags an interactive wizard walks the fields
    Edit {
        /// Path name; picked interactively when omitted
        name: Option<String>,
        #[arg(short, long)]
        dir: Option<String>,
        /// Post-write command (empty value removes it)
        #[arg(short, long)]
        restart: Option<String>,
    },
    /// Remove a path; servers that used it are updated
    Remove {
        /// Path name; picked interactively when omitted
        name: Option<String>,
        /// Skip the confirmation prompt
        #[arg(short = 'y', long = "yes")]
        assume_yes: bool,
    },
}

#[derive(Subcommand)]
pub enum TargetCommand {
    /// Add a target; missing parts are picked interactively
    Add {
        /// Target name (defaults to APP-SERVER)
        name: Option<String>,
        /// The app whose artifacts travel
        #[arg(short, long)]
        app: Option<String>,
        /// The server they land on
        #[arg(short, long)]
        server: Option<String>,
        /// The credential that logs in (defaults to the server's)
        #[arg(short = 'C', long)]
        credential: Option<String>,
        /// The named path they are written to
        #[arg(short, long)]
        path: Option<String>,
    },
    /// List targets
    List,
    /// Show one target in detail
    Show {
        /// Target name; picked interactively when omitted
        name: Option<String>,
    },
    /// Edit a target; with no flags an interactive wizard walks the fields
    Edit {
        /// Target name; picked interactively when omitted
        name: Option<String>,
        #[arg(short, long)]
        server: Option<String>,
        #[arg(short = 'C', long)]
        credential: Option<String>,
        #[arg(short, long)]
        path: Option<String>,
    },
    /// Rename a target
    Rename {
        /// Target name; picked interactively when omitted
        name: Option<String>,
        /// The new name
        to: Option<String>,
    },
    /// Remove a target (the entities it names are untouched)
    Remove {
        /// Target name; picked interactively when omitted
        name: 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(short, long)]
        path: Option<PathBuf>,
        /// Local gateway port for this app
        #[arg(short = 'P', long)]
        port: Option<u16>,
        /// Build artifact directory, relative to the project path
        #[arg(short, long)]
        dist: Option<String>,
        /// Set a command as NAME=CMD (repeatable); overrides detected defaults
        #[arg(short = 'c', long = "command", value_name = "NAME=CMD")]
        commands: Vec<String>,
        /// Allow a server for this app (repeatable)
        #[arg(short = 's', 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(short, long)]
        path: Option<PathBuf>,
        #[arg(short = 'P', long)]
        port: Option<u16>,
        #[arg(short, long)]
        dist: Option<String>,
        /// Set a command as NAME=CMD, or NAME= to remove it (repeatable)
        #[arg(short = 'c', long = "command", value_name = "NAME=CMD")]
        commands: Vec<String>,
        /// Allow a server (repeatable)
        #[arg(short = 'a', long = "add-server", value_name = "SERVER")]
        add_servers: Vec<String>,
        /// Disallow a server (repeatable)
        #[arg(short = 'r', 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(short, long)]
        url: Option<String>,
        /// Human-friendly label
        #[arg(short, long)]
        label: Option<String>,
        /// SSH host and port when they differ from the URL's host
        #[arg(short = 'H', long, value_name = "HOST[:PORT]")]
        host: Option<String>,
        /// Credential used to log in here
        #[arg(short, long)]
        credential: Option<String>,
        /// Accept self-signed or invalid TLS certificates for this server
        #[arg(short, 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(short, long)]
        url: Option<String>,
        #[arg(short, long)]
        label: Option<String>,
        /// SSH host and port (empty value falls back to the URL's host)
        #[arg(short = 'H', long, value_name = "HOST[:PORT]")]
        host: Option<String>,
        /// Credential used to log in here (empty value unsets it)
        #[arg(short, long)]
        credential: Option<String>,
        /// Accept invalid TLS certificates for this server
        #[arg(short, long, conflicts_with = "secure")]
        insecure: bool,
        /// Require valid TLS certificates for this server
        #[arg(short = 'S', long)]
        secure: bool,
    },
    /// 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,
    },
}