ignition_cli/cli.rs
1//! CLI definition: the five global args (defined exactly once, here) and the
2//! subcommand enum.
3//!
4//! Placement rules (Phase 1 research, Pattern 1):
5//! - Globals set the `global` arg attribute so they propagate to every
6//! subcommand; subcommand structs never redeclare them.
7//! - Never mark a global arg `required` (clap rejects required globals).
8//! - Flags-only, zero interactive prompts — ever (research anti-pattern:
9//! "Prompting ever").
10
11use std::path::PathBuf;
12
13use clap::{ArgAction, Parser, Subcommand};
14
15#[derive(Debug, Parser)]
16#[command(
17 name = "ign",
18 version,
19 propagate_version = true,
20 about = "Operate Ignition 8.3+ gateways from the terminal"
21)]
22pub struct Cli {
23 /// Gateway profile to use (default: active profile in config)
24 #[arg(long, global = true, value_name = "NAME")]
25 pub profile: Option<String>,
26
27 /// Machine-readable JSON output (stable field names)
28 #[arg(long, global = true, action = ArgAction::SetTrue)]
29 pub json: bool,
30
31 /// One-line compact JSON (implies --json)
32 #[arg(long, global = true, action = ArgAction::SetTrue)]
33 pub compact: bool,
34
35 /// Non-interactive confirmation for destructive operations
36 #[arg(long, short = 'y', global = true, action = ArgAction::SetTrue)]
37 pub yes: bool,
38
39 /// Increase diagnostics (-vv for HTTP trace) to stderr
40 #[arg(long, short = 'v', global = true, action = ArgAction::Count)]
41 pub verbose: u8,
42
43 #[command(subcommand)]
44 pub command: Commands,
45}
46
47#[derive(Debug, Subcommand)]
48pub enum Commands {
49 /// Print version information (CLI always; gateway check when a profile resolves)
50 Version,
51
52 /// Generate shell completions (bash, zsh, fish, …) — the one stdout exception
53 #[command(arg_required_else_help = true)]
54 Completions {
55 /// Shell to generate completions for
56 #[arg(value_enum)]
57 shell: clap_complete::aot::Shell,
58 },
59
60 /// Gateway status: identity, platform, uptime, license (incl. trial countdown)
61 Status,
62
63 /// List gateway modules (healthy by default)
64 Modules {
65 /// Show quarantined modules instead of healthy ones
66 #[arg(long)]
67 quarantined: bool,
68 },
69
70 /// Gateway performance metrics (current gauges + thread counts)
71 Metrics {
72 /// Include historic chart datapoints
73 #[arg(long)]
74 history: bool,
75 },
76
77 /// List gateway sessions (designers, Perspective, Vision) — or
78 /// terminate one via the `terminate` subcommand
79 Sessions(SessionsArgs),
80
81 /// List database/OPC connections with healthcheck status as reported
82 Connections {
83 /// Filter to one connection family (default: both)
84 #[arg(long, value_enum)]
85 r#type: Option<ConnectionType>,
86 },
87
88 /// Query, tail, and download gateway logs; manage logger levels
89 Logs(LogsArgs),
90
91 /// Restart the gateway — destructive, refused without --yes;
92 /// --wait polls until RUNNING
93 Restart {
94 /// Wait for the gateway to return to RUNNING (POST, then poll
95 /// /StatusPing)
96 #[arg(long)]
97 wait: bool,
98 /// Wait budget in seconds (default 300)
99 #[arg(long, value_name = "SECS")]
100 timeout: Option<u64>,
101 /// Poll interval in seconds (default 2)
102 #[arg(long, value_name = "SECS")]
103 interval: Option<u64>,
104 },
105
106 /// Wait for a gateway state (readiness, restart completion, module)
107 #[command(arg_required_else_help = true)]
108 Wait(WaitArgs),
109
110 /// Diagnose the gateway setup: URL, liveness, commissioning, auth,
111 /// permissions, write, WebDev route, rig — exits 0 whenever the
112 /// diagnosis completes (failing checks are data)
113 Doctor {
114 /// Probe write permission (POST scan/projects — a harmless
115 /// project rescan)
116 #[arg(long)]
117 check_write: bool,
118 /// Probe one WebDev route's presence (/system/webdev/<NAME>)
119 #[arg(long, value_name = "NAME")]
120 webdev_route: Option<String>,
121 },
122
123 /// Adopt this profile's gateway: native login → mint an
124 /// Administrator-level API key (idempotent by name) → wire the
125 /// gateway's read/write permissions → live-probe the key →
126 /// persist the credential (OS keyring, env-var fallback). Re-run
127 /// on an adopted gateway is an all-skip no-op. Composition flags
128 /// ride the bootstrap: --project deploys the CLI's WebDev routes,
129 /// --checkout lands every enabled project locally, --bake saves a
130 /// restore-ready gwbk
131 Adopt {
132 /// Gateway login user for the native OIDC dance
133 /// (default: admin)
134 #[arg(long, value_name = "NAME")]
135 user: Option<String>,
136
137 /// The API-key name — the idempotency key (default: ign-cli)
138 #[arg(long, value_name = "NAME")]
139 key_name: Option<String>,
140
141 /// The granted security level as a slash path
142 /// (default: Authenticated/Roles/Administrator)
143 #[arg(long, value_name = "PATH")]
144 level: Option<String>,
145
146 /// Deploy the CLI's WebDev routes (scriptExec on) into this
147 /// project after the bootstrap
148 #[arg(long, value_name = "NAME")]
149 project: Option<String>,
150
151 /// Ship the embedded TESTING bundle with the routes (the
152 /// Jython framework + testing/run|tags routes + the smoke
153 /// sentinel; the step asserts discover ≥1 module and a green
154 /// smoke run — the empty-suite trap)
155 #[arg(long, requires = "project")]
156 testing: bool,
157
158 /// Check out every enabled project into DIR/<project>
159 /// (scripts decoded — grep/lint ready); existing targets skip
160 #[arg(long, value_name = "DIR")]
161 checkout: Option<String>,
162
163 /// Download a roaming gwbk to FILE after everything landed —
164 /// a gateway reset restored from it keeps the key and routes
165 #[arg(long, value_name = "FILE")]
166 bake: Option<String>,
167 },
168
169 /// Manage gateway projects: list with inheritance info, new, copy,
170 /// rename, set (reparent), delete, export/import (ZIP)
171 #[command(arg_required_else_help = true)]
172 Project(ProjectArgs),
173
174 /// Check out a project's resources to a local tree, inspect drift,
175 /// push guarded changes — the local edit loop over the
176 /// project-export interchange (project RESOURCES only; tag values
177 /// never appear in the tree — tags remain `ign tags` verbs)
178 #[command(arg_required_else_help = true)]
179 Workspace(WorkspaceArgs),
180
181 /// Fetch a gateway resource, edit it in $EDITOR, push back
182 /// (guarded) — the kubectl-edit loop over ONE resource:
183 /// unchanged saves are clean no-ops (nothing pushed, never
184 /// prompted), a gateway changed since fetch refuses (never
185 /// force-pushed), a JSON-breaking save refuses fail-closed with
186 /// the temp tree kept (path printed). Zero stdout in every mode —
187 /// the editor owns the terminal (see README's edit section)
188 Edit(EditArgs),
189
190 /// Manage a project's individual resources: list, get, put,
191 /// delete — the surgical edit loop (change one view/script
192 /// without re-importing everything)
193 #[command(arg_required_else_help = true)]
194 Resource(ResourceArgs),
195
196 /// Manage the CLI's own WebDev routes on the gateway: deploy the
197 /// embedded bundle, verify the version handshake
198 #[command(arg_required_else_help = true)]
199 Webdev(WebdevArgs),
200
201 /// Tag operations: manage providers (native REST) and browse /
202 /// read / write tag values (the deployed WebDev routes — run
203 /// `ign webdev deploy` first)
204 #[command(arg_required_else_help = true)]
205 Tags(TagsArgs),
206
207 /// Manage a Docker compose rig: up/down/reset/status/logs/trial
208 /// (snapshot arrives in a later plan) — docker-only, no profile
209 /// needed
210 #[command(arg_required_else_help = true)]
211 Rig(RigArgs),
212
213 /// Manage gateway backups (gwbk) on any profiled gateway:
214 /// download (streamed) and restore (guarded)
215 #[command(arg_required_else_help = true)]
216 Backup(BackupArgs),
217
218 /// EAM task orchestration (Enterprise Administration Module):
219 /// history/definitions reads, guarded task create + force —
220 /// every verb honestly reports the controller-mode state gate
221 #[command(arg_required_else_help = true)]
222 Eam(EamArgs),
223
224 /// Run gateway-side Python (Jython) through the secret-gated
225 /// scriptExec route — the opt-in is STRUCTURAL (`ign webdev
226 /// deploy --with-script-exec` deploys the route + persists its
227 /// secret); there is no --yes on this verb by design
228 #[command(arg_required_else_help = true)]
229 Script(ScriptArgs),
230
231 /// Lint local project files by delegating to ignition-lint (PATH
232 /// discovery) — doctor posture: findings are DATA, exit 0
233 /// whenever the tool ran; --strict passes the tool's exit code
234 /// through for CI
235 Lint(LintArgs),
236
237 /// Raw passthrough to any gateway REST endpoint — the escape
238 /// hatch for the uncurated routes (the envelope's `data` is left
239 /// gateway-verbatim; see README's documented contract exception)
240 #[command(arg_required_else_help = true)]
241 Api(ApiArgs),
242
243 /// License inventory + trial state — the morning-check read
244 /// (trial companion merged into one command)
245 #[command(arg_required_else_help = true)]
246 License(LicenseArgs),
247
248 /// Gateway redundancy status — the morning-check read
249 #[command(arg_required_else_help = true)]
250 Redundancy(RedundancyArgs),
251
252 /// Gateway Area Network (GAN) overview — the morning-check read
253 #[command(arg_required_else_help = true)]
254 Gan(GanArgs),
255
256 /// Diagnostics support-bundle operations: generate, check
257 /// status, download (streamed), wait — the support-bundle slice
258 #[command(arg_required_else_help = true)]
259 Diagnostics(DiagnosticsArgs),
260
261 /// Manage gateway profiles
262 #[command(arg_required_else_help = true)]
263 Profile(ProfileArgs),
264
265 /// Serve the Model Context Protocol over stdio (hidden; for MCP
266 /// clients — the newline-delimited JSON-RPC 2.0 stream IS the
267 /// stdout product; the routes.rs OutOfBand row lands atomically
268 /// with this command, 08-06 contract)
269 #[command(hide = true)]
270 Mcp(McpArgs),
271
272 /// Serve LSP over stdio (hidden; for ignition-nvim — the
273 /// Content-Length-framed LSP stream IS the stdout product; the
274 /// routes.rs OutOfBand row lands atomically with this command,
275 /// 08-06 contract)
276 #[command(hide = true)]
277 Lsp,
278
279 /// Interactive TUI cockpit
280 #[cfg(feature = "tui")]
281 Tui,
282}
283
284/// Hidden args for `ign mcp serve` (14-01). `serve` is a RESTRICTED
285/// POSITIONAL — not a subcommand — so the clap walk (tui_coverage.rs)
286/// yields exactly ONE row-requiring node, `mcp` itself: an `mcp
287/// serve` subcommand leaf would need a second routes() row and extend
288/// the pinned OutOfBand set past its four-member contract (an orphan
289/// row fails the walk by design). clap's `value_parser = ["serve"]`
290/// keeps every other spelling (`ign mcp`, `ign mcp bogus`) a
291/// usage-class clap error — exit 2 BEFORE any protocol byte is
292/// written. Zero stdout-facing behavior here — the mode owns its own
293/// I/O entirely (`mcp::serve`).
294#[derive(Debug, clap::Args)]
295pub struct McpArgs {
296 /// Start the stdio MCP server
297 #[arg(value_parser = ["serve"], value_name = "SERVE")]
298 pub serve: String,
299}
300
301/// Wait targets (02-05, HLTH-11). `gateway` and `restart` poll the
302/// UNAUTHENTICATED /StatusPing (the dispatch builds a header-less
303/// client for them — waiting must work when auth is broken); `module`
304/// is an authed read.
305#[derive(Debug, clap::Args)]
306pub struct WaitArgs {
307 #[command(subcommand)]
308 pub command: WaitCmd,
309}
310
311#[derive(Debug, Subcommand)]
312pub enum WaitCmd {
313 /// Wait until the gateway reports RUNNING (unauthenticated
314 /// StatusPing — works even when auth is broken or absent)
315 Gateway {
316 /// Poll interval in seconds
317 #[arg(long, default_value_t = 2, value_name = "SECS")]
318 interval: u64,
319 /// Give up after this many seconds
320 #[arg(long, default_value_t = 120, value_name = "SECS")]
321 timeout: u64,
322 },
323 /// Wait for a restart to complete — restart-aware: shares
324 /// `restart --wait`'s semantics (non-RUNNING observed once →
325 /// RUNNING; a 5 s floor guards the all-RUNNING case)
326 Restart {
327 /// Poll interval in seconds
328 #[arg(long, default_value_t = 2, value_name = "SECS")]
329 interval: u64,
330 /// Give up after this many seconds (default 300)
331 #[arg(long, default_value_t = 300, value_name = "SECS")]
332 timeout: u64,
333 },
334 /// Wait until a module reports ACTIVE
335 Module {
336 /// Module id (see `ign modules`)
337 id: String,
338 /// Poll interval in seconds
339 #[arg(long, default_value_t = 2, value_name = "SECS")]
340 interval: u64,
341 /// Give up after this many seconds
342 #[arg(long, default_value_t = 120, value_name = "SECS")]
343 timeout: u64,
344 },
345}
346
347/// Project subcommands (03-01, PROJ-01/02). `delete` is the family's
348/// ONE destructive verb (`--yes`-guarded, exit 2 without); copy/
349/// rename/set create or relabel — never destroy — so they carry NO
350/// guard (planner decision per research).
351#[derive(Debug, clap::Args)]
352pub struct ProjectArgs {
353 #[command(subcommand)]
354 pub command: ProjectCommand,
355}
356
357#[derive(Debug, Subcommand)]
358pub enum ProjectCommand {
359 /// List every runnable project: name, title, enabled, parent,
360 /// inheritable (inheritance info from the items themselves)
361 List,
362 /// Create a project (only provided fields ride the create body)
363 New {
364 /// Project name
365 name: String,
366 /// Display title
367 #[arg(long, value_name = "TEXT")]
368 title: Option<String>,
369 /// Long description
370 #[arg(long, value_name = "TEXT")]
371 description: Option<String>,
372 /// Parent project (inheritance)
373 #[arg(long, value_name = "NAME")]
374 parent: Option<String>,
375 /// Mark this project eligible as a parent
376 #[arg(long)]
377 inheritable: bool,
378 /// Create the project disabled
379 #[arg(long)]
380 disabled: bool,
381 },
382 /// Copy a project with all its resources
383 Copy {
384 /// Source project name
385 src: String,
386 /// Destination name (must not exist)
387 dst: String,
388 },
389 /// Rename a project (native rename, not copy+delete)
390 Rename {
391 /// Current name
392 old_name: String,
393 /// New name
394 new_name: String,
395 },
396 /// Set project fields — --parent IS the inheritance move (reparent)
397 #[command(group(
398 clap::ArgGroup::new("set_fields")
399 .required(true)
400 .multiple(true)
401 .args(&["title", "description", "parent", "set_enabled", "disabled", "inheritable"])
402 ))]
403 Set {
404 /// Project name
405 name: String,
406 /// Display title
407 #[arg(long, value_name = "TEXT")]
408 title: Option<String>,
409 /// Long description
410 #[arg(long, value_name = "TEXT")]
411 description: Option<String>,
412 /// Parent project (reparent)
413 #[arg(long, value_name = "NAME")]
414 parent: Option<String>,
415 /// Enable the project
416 #[arg(long, conflicts_with = "disabled")]
417 set_enabled: bool,
418 /// Disable the project
419 #[arg(long)]
420 disabled: bool,
421 /// Whether this project may serve as a parent (true/false)
422 #[arg(long, value_name = "BOOL")]
423 inheritable: Option<bool>,
424 },
425 /// Delete a project — destructive, refused without --yes
426 Delete {
427 /// Project name
428 name: String,
429 },
430 /// Export a project as a ZIP archive (streams to disk)
431 Export {
432 /// Project name
433 name: String,
434 /// Output file (default: the gateway's Content-Disposition
435 /// name, else <name>.zip); with --decode-scripts, the output
436 /// DIRECTORY (default <name>-export/)
437 #[arg(short, long, value_name = "FILE")]
438 output: Option<PathBuf>,
439 /// Also decode embedded JSON scripts (Perspective view.json
440 /// etc.) into editable `<member>.<n>.py` sidecars beside a
441 /// copied member tree + scripts-manifest.json — the output is
442 /// a DIRECTORY (script-python members are already plain .py
443 /// and never decode; expressions pass through)
444 #[arg(long)]
445 decode_scripts: bool,
446 },
447 /// Import a project from a ZIP archive
448 Import {
449 /// Project name to import as
450 name: String,
451 /// ZIP file path, or - to read the archive from stdin; with
452 /// --encode-scripts, a decoded export DIRECTORY (re-zipped
453 /// with the sidecars spliced back before upload; stdin is
454 /// invalid in that mode)
455 #[arg(long, value_name = "PATH")]
456 file: String,
457 /// Collision policy: abort refuses when the name exists
458 /// (default); overwrite REPLACES the entire project —
459 /// destructive, requires --yes. merge is Designer-only (not a
460 /// value; the README documents why)
461 #[arg(long, value_enum, default_value_t = CollisionPolicy::Abort)]
462 collision_policy: CollisionPolicy,
463 /// --file points at a decoded export DIRECTORY (from
464 /// `export --decode-scripts`): the sidecars are spliced back
465 /// and the manifest stripped before the standard import path
466 #[arg(long)]
467 encode_scripts: bool,
468 },
469 /// Compare a project across two gateway profiles — statuses are
470 /// B-relative-to-A (`added` = in B only, `removed` = in A only,
471 /// `changed` = differing after resource.json normalization)
472 Diff {
473 /// Baseline profile (A)
474 profile_a: String,
475 /// Compared profile (B — the diff is B relative to A)
476 profile_b: String,
477 /// Project name
478 #[arg(long, value_name = "NAME")]
479 project: String,
480 },
481 /// Promote selected resources from profile A into profile B
482 /// (direction is ALWAYS A→B) — destructive on B: the whole
483 /// project is overwrite-imported, refused without --yes
484 Sync {
485 /// Source profile (A)
486 profile_a: String,
487 /// Target profile (B)
488 profile_b: String,
489 /// Project name
490 #[arg(long, value_name = "NAME")]
491 project: String,
492 /// Resource user path to promote (repeatable)
493 #[arg(long, value_name = "PATH")]
494 resource: Vec<String>,
495 /// Promote every resource the diff reports added or changed
496 /// (never removed ones — deletion needs --delete)
497 #[arg(long)]
498 all_changed: bool,
499 /// Also remove B's resources the diff reports removed
500 /// (default: upsert-only, nothing is ever deleted)
501 #[arg(long)]
502 delete: bool,
503 },
504}
505
506/// Workspace subcommands (13-07): the local edit loop. `checkout`
507/// writes the mapped tree + records `.ign-workspace.json` (the
508/// manifest IS the workspace identity — status/push read it and the
509/// user never re-types the project); `status` reports PUSH-RELATIVE
510/// drift (every row says what push would do); `push` is the guarded
511/// splice of local edits into a FRESH gateway export.
512#[derive(Debug, clap::Args)]
513pub struct WorkspaceArgs {
514 #[command(subcommand)]
515 pub command: WorkspaceCommand,
516}
517
518#[derive(Debug, Subcommand)]
519pub enum WorkspaceCommand {
520 /// Check out a project's resources to a local tree — mapped
521 /// paths + the recorded `.ign-workspace.json` manifest + an
522 /// idempotent `.gitignore` (manifest committed; codec artifacts
523 /// ignored). Read-only on the wire: one export GET, zero imports.
524 Checkout {
525 /// Project name to check out
526 project: String,
527 /// Target directory for the workspace tree (created if
528 /// absent; an unrelated non-empty directory refuses — never
529 /// clobbered)
530 target: PathBuf,
531 /// Decode embedded JSON scripts to editable .py sidecars
532 /// (nvim-editable; encodes back cleanly — an unedited
533 /// re-encode is byte-exact; `scripts-manifest.json` keys the
534 /// splice)
535 #[arg(long)]
536 decode_scripts: bool,
537 },
538 /// Report workspace drift against the gateway — states are
539 /// PUSH-RELATIVE (each row names what push would do: write /
540 /// leave / refuse); the project comes from the workspace
541 /// manifest, never re-typed. Read-only: one export GET.
542 Status {
543 /// Workspace root (default: the current directory)
544 #[arg(default_value = ".")]
545 path: PathBuf,
546 },
547 /// Push local edits to the gateway — guarded (refused without
548 /// --yes; the refusal message IS the blast-radius preview);
549 /// conflicts refuse EVEN WITH --yes (manual reconciliation);
550 /// deletions need --delete (default: reported, skipped);
551 /// untracked files are never imported; an empty selection
552 /// writes nothing.
553 Push {
554 /// Workspace root (default: the current directory)
555 #[arg(default_value = ".")]
556 path: PathBuf,
557 /// Also delete gateway members removed locally (default:
558 /// reported, skipped)
559 #[arg(long)]
560 delete: bool,
561 },
562}
563
564/// `ign edit` args (13-08): the kubectl-edit loop over ONE resource.
565/// The resource path scopes what $EDITOR opens (the whole tree still
566/// decodes for encode-back context); the push rides the
567/// project-import wire behind the ONE confirmation gate. `--yes` is
568/// the GLOBAL flag (globals-once) — never redeclared here.
569#[derive(Debug, clap::Args)]
570pub struct EditArgs {
571 /// Project containing the resource
572 pub project: String,
573 /// Resource user path to open in $EDITOR (e.g.
574 /// `ignition/script-python/e2e/scratch` — `ign resource list`
575 /// names the valid members)
576 pub resource_path: String,
577}
578
579/// Resource subcommands (03-03, PROJ-05). `delete` is the family's
580/// destructive verb (`--yes`-guarded, exit 2 without — the
581/// sessions-terminate shape); `put` is an upsert (create-or-replace
582/// ONE resource with explicit content) and stays friction-free per
583/// the planner decision — agents pass exactly what they want written.
584#[derive(Debug, clap::Args)]
585pub struct ResourceArgs {
586 #[command(subcommand)]
587 pub command: ResourceCommand,
588}
589
590#[derive(Debug, Subcommand)]
591pub enum ResourceCommand {
592 /// List a project's resources (one path per line in human mode)
593 List {
594 /// Project name
595 project: String,
596 /// Only paths under this prefix (rides the wire as the
597 /// server-side `path` filter)
598 #[arg(long, value_name = "PREFIX")]
599 prefix: Option<String>,
600 },
601 /// Read one resource: JSON pretty-printed, text raw — binary
602 /// (data.bin-class) resources refuse with exit 6
603 Get {
604 /// Project name
605 project: String,
606 /// Resource path, slashes kept (e.g.
607 /// `ignition/script-python/e2e/scratch`)
608 path: String,
609 },
610 /// Write one resource (upsert: created if absent, replaced if
611 /// present) — JSON if parseable (application/json), else UTF-8
612 /// text (text/plain); binary-looking input refuses
613 Put {
614 /// Project name
615 project: String,
616 /// Resource path
617 path: String,
618 /// File to read the content from, or `-` for stdin
619 #[arg(long, value_name = "PATH")]
620 file: String,
621 },
622 /// Delete one resource — destructive, refused without --yes
623 Delete {
624 /// Project name
625 project: String,
626 /// Resource path
627 path: String,
628 },
629}
630
631/// Webdev subcommands (05-03, WEB-01/02). Deploy is deliberately NOT
632/// `--yes`-guarded: the dedicated project (default `ign-cli`) is
633/// CLI-OWNED — born from the first deploy zip and overwrite-replaced
634/// on every deploy (replace-not-merge is the CONTRACT here; user
635/// projects are never touched — README documents). scriptExec rides
636/// only on explicit request, gated by a deploy-time generated shared
637/// secret stored in the profile config at 0600.
638#[derive(Debug, clap::Args)]
639pub struct WebdevArgs {
640 #[command(subcommand)]
641 pub command: WebdevCommand,
642}
643
644#[derive(Debug, Subcommand)]
645pub enum WebdevCommand {
646 /// Deploy the embedded route bundle into the dedicated project
647 /// (overwrite-replace — the CLI owns that project wholesale)
648 Deploy {
649 /// Target project (default ign-cli; the CLI owns it wholesale)
650 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
651 project: String,
652 /// Also deploy the secret-gated scriptExec route (a fresh
653 /// secret is generated and stored at 0600 when none exists)
654 #[arg(long)]
655 with_script_exec: bool,
656 /// Generate a FRESH scriptExec secret before deploying (any
657 /// route deployed with the old secret starts refusing)
658 #[arg(long, requires = "with_script_exec")]
659 rotate_secret: bool,
660 /// Also deploy the embedded TESTING bundle — the Jython test
661 /// framework (testing.runner & co., with the permanent
662 /// testing.__tests__ smoke sentinel) and the testing/run +
663 /// testing/tags WebDev routes
664 #[arg(long)]
665 with_testing: bool,
666 },
667 /// Probe every route's version handshake — a READ: exit 0
668 /// whenever the sweep completes, per-route degradation is data
669 Status {
670 /// Target project (default ign-cli)
671 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
672 project: String,
673 },
674}
675
676/// Tags args (05-04, TAGS-01..04) — the grouped-subfamily pattern:
677/// `tags provider …` nests one level deeper (the family's native
678/// REST half), browse/read/write ride the TOP level. The
679/// webdev-dependent arms carry `--project` (default `ign-cli`, the
680/// same default as deploy — the deployed routes live in that
681/// project).
682#[derive(Debug, clap::Args)]
683pub struct TagsArgs {
684 #[command(subcommand)]
685 pub command: TagsCommand,
686}
687
688#[derive(Debug, Subcommand)]
689pub enum TagsCommand {
690 /// Manage tag providers (native config-resource REST — no
691 /// deployed routes involved): list with tag counts + health,
692 /// create a STANDARD provider, delete (guarded)
693 #[command(subcommand)]
694 Provider(TagsProviderCommand),
695
696 /// Browse tags as a tree (Property children filtered by
697 /// default) — providers appear at the root; needs the deployed
698 /// routes (`ign webdev deploy`) — or run fully OFFLINE against
699 /// an export via --from-export
700 Browse {
701 /// Tag path to browse from (default: the root — providers)
702 path: Option<String>,
703 /// Case-insensitive substring filter on name and full path
704 #[arg(long, value_name = "SUBSTR")]
705 filter: Option<String>,
706 /// Include Property children (filtered out by default)
707 #[arg(long)]
708 include_properties: bool,
709 /// Project holding the deployed routes (default ign-cli)
710 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
711 project: String,
712 /// Browse an OFFLINE tag export instead of the gateway (no
713 /// profile, no credential, no routes): the CLI's own `tags
714 /// export` JSON, a legacy `<provider>.json` whole tree, or a
715 /// git-module tags/ directory — mutually exclusive with the
716 /// positional browse path
717 #[arg(long, value_name = "PATH", conflicts_with_all = ["path"])]
718 from_export: Option<PathBuf>,
719 },
720 /// Read one or more tag values (quality and timestamp included)
721 /// — needs the deployed routes
722 Read {
723 /// Tag paths to read, e.g. `[default]T1` (one or more)
724 #[arg(value_name = "PATH", required = true)]
725 paths: Vec<String>,
726 /// Project holding the deployed routes (default ign-cli)
727 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
728 project: String,
729 },
730 /// Write a value to a tag — the value is parsed as a JSON
731 /// scalar (number/bool/null); anything unparseable is sent as a
732 /// string; arrays/objects refuse — needs the deployed routes
733 Write {
734 /// Tag path to write, e.g. `[default]T1`
735 path: String,
736 /// Value to write: JSON scalar (42, 1.5, true), else the raw
737 /// text is sent as a string
738 #[arg(long, value_name = "V")]
739 value: String,
740 /// Project holding the deployed routes (default ign-cli)
741 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
742 project: String,
743 },
744 /// Tag CONFIGURATION CRUD (the surgical edit loop — JSON in,
745 /// JSON out): get/create/edit/delete tag configs through the
746 /// deployed tagConfig route; stringified values are re-parsed so
747 /// agents see real JSON
748 #[command(subcommand)]
749 Config(TagsConfigCommand),
750 /// UDT types and definitions (recursive: parameters + nested
751 /// children) — needs the deployed routes
752 #[command(subcommand)]
753 Udt(TagsUdtCommand),
754 /// Alarm operations: active status (with filters), journal
755 /// history, acknowledge — needs the deployed routes
756 #[command(subcommand)]
757 Alarms(TagsAlarmsCommand),
758 /// Tag history queries (historian-backed values) — needs the
759 /// deployed routes; structurally safe on any rig (data requires
760 /// a provisioned historian)
761 #[command(subcommand)]
762 History(TagsHistoryCommand),
763 /// Export tag subtrees — json writes the gateway's native JSON
764 /// interchange (the lossless round-trip); xml passes the
765 /// gateway's own XML through byte-for-byte; csv is CLI-GENERATED
766 /// and LOSSY (no alarms, legacy 48-column set, numeric enums —
767 /// the gateway cannot export CSV)
768 Export {
769 /// Tag paths to export, e.g. `[default]P5` (one or more)
770 #[arg(value_name = "PATH", required = true)]
771 paths: Vec<String>,
772 /// Output file (`-` = stdout — the raw payload, no envelope;
773 /// default: `<last-path-segment>.<ext>` in the cwd, ext per
774 /// format)
775 #[arg(short = 'o', long, value_name = "FILE")]
776 output: Option<PathBuf>,
777 /// Payload format: json (the gateway's native interchange —
778 /// default), xml (raw gateway bytes), or csv (CLI-generated
779 /// and LOSSY: no alarms, legacy columns only, numeric enum
780 /// coercion — what was dropped prints on stderr)
781 #[arg(long, value_enum, default_value_t = TransferFormat::Json, value_name = "FORMAT")]
782 format: TransferFormat,
783 /// Project holding the deployed routes (default ign-cli)
784 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
785 project: String,
786 },
787 /// Import a tag export into a target provider — json is the
788 /// native interchange; xml/csv ride the gateway's importTags
789 /// passthrough. A loss scan reports what xml/csv would drop or
790 /// coerce BEFORE the import and refuses exit 2 unless --yes.
791 /// Collision behavior: abort (default) refuses on collisions;
792 /// overwrite replaces them (destructive: requires --yes)
793 Import {
794 /// The export file to import (`-` = stdin)
795 #[arg(long, value_name = "FILE")]
796 file: PathBuf,
797 /// Target tag provider (must exist — `ign tags provider
798 /// create NAME` first)
799 #[arg(long, value_name = "NAME")]
800 provider: String,
801 /// Payload format: json (the gateway's native interchange —
802 /// default), xml (gateway XML), or csv (legacy CSV — alarms
803 /// never arrive; the loss scan reports the gap)
804 #[arg(long, value_enum, default_value_t = TransferFormat::Json, value_name = "FORMAT")]
805 format: TransferFormat,
806 /// Collision policy: abort refuses when tags already exist
807 /// (default); overwrite replaces them (destructive:
808 /// requires --yes). merge is Designer-only (not a value)
809 #[arg(long, value_enum, default_value_t = CollisionPolicy::Abort)]
810 collision_policy: CollisionPolicy,
811 /// Project holding the deployed routes (default ign-cli)
812 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
813 project: String,
814 },
815}
816
817/// The bulk-transfer payload format (11-05) — the clap surface for
818/// core's `ExportFormat`/`ImportFormat` (core keeps plain enums; the
819/// From impls live beside this one, the CollisionPolicy precedent).
820/// json is the default so every pre-11-05 invocation parses and
821/// behaves byte-identically.
822#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, clap::ValueEnum)]
823pub enum TransferFormat {
824 /// The gateway's JSON interchange (default — lossless)
825 #[default]
826 Json,
827 /// Gateway XML — RAW-BYTE passthrough both directions (no
828 /// parse/normalize in the transfer path)
829 Xml,
830 /// Legacy CSV — import: gateway passthrough (alarms never
831 /// arrive); export: CLI-GENERATED and LOSSY (the gateway cannot
832 /// export CSV)
833 Csv,
834}
835
836impl std::fmt::Display for TransferFormat {
837 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
838 let label = match self {
839 Self::Json => "json",
840 Self::Xml => "xml",
841 Self::Csv => "csv",
842 };
843 f.write_str(label)
844 }
845}
846
847/// `tags config …` — the configuration CRUD subfamily (05-05,
848/// TAGS-05): the get→edit-file→write-back surgical loop. Definition
849/// files are the configure shape (README's traps table: tagType
850/// discriminator, nested children, alarms-as-LIST).
851#[derive(Debug, Subcommand)]
852pub enum TagsConfigCommand {
853 /// Get a tag's configuration as (pretty) JSON — stringified
854 /// value/defaultValue sub-dicts re-parsed into real JSON
855 Get {
856 /// Tag path, e.g. `[default]P5/T1`
857 path: String,
858 /// Project holding the deployed routes (default ign-cli)
859 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
860 project: String,
861 },
862 /// Create a tag from a JSON definition file (`-` = stdin) —
863 /// aborts on an existing node (collision policy 'a')
864 Create {
865 /// Tag path to create, e.g. `[default]P5/T1`
866 path: String,
867 /// JSON definition file (`-` = stdin): `{tagType, value,
868 /// alarms, …}` — see the README configure-shape table
869 #[arg(long, value_name = "FILE")]
870 file: PathBuf,
871 /// Project holding the deployed routes (default ign-cli)
872 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
873 project: String,
874 },
875 /// Edit a tag's configuration from a JSON definition file (`-` =
876 /// stdin) — overwrites that single node (collision policy 'o')
877 Edit {
878 /// Tag path to edit, e.g. `[default]P5/T1`
879 path: String,
880 /// JSON definition file (`-` = stdin)
881 #[arg(long, value_name = "FILE")]
882 file: PathBuf,
883 /// Project holding the deployed routes (default ign-cli)
884 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
885 project: String,
886 },
887 /// Delete tag configurations — destructive, refused without
888 /// --yes (the guard fires before ANY resolution: zero network
889 /// work)
890 Delete {
891 /// Tag paths to delete (one or more)
892 #[arg(value_name = "PATH", required = true)]
893 paths: Vec<String>,
894 /// Project holding the deployed routes (default ign-cli)
895 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
896 project: String,
897 },
898}
899
900/// `tags udt …` — the UDT subfamily (05-05, TAGS-06).
901#[derive(Debug, Subcommand)]
902pub enum TagsUdtCommand {
903 /// List the provider's UDT types
904 Types {
905 /// Tag provider whose `_types_` folder to browse (default
906 /// `default`)
907 #[arg(long, default_value = "default", value_name = "NAME")]
908 provider: String,
909 /// Project holding the deployed routes (default ign-cli)
910 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
911 project: String,
912 },
913 /// Get a UDT definition (parameters + nested children, recursive)
914 Def {
915 /// UDT type name, e.g. `Motor`
916 name: String,
917 /// Tag provider (default `default`)
918 #[arg(long, default_value = "default", value_name = "NAME")]
919 provider: String,
920 /// Project holding the deployed routes (default ign-cli)
921 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
922 project: String,
923 },
924}
925
926#[derive(Debug, Subcommand)]
927pub enum TagsProviderCommand {
928 /// List the gateway's tag providers (tag counts + health)
929 List,
930 /// Create a STANDARD tag provider (DB-backed providers are out
931 /// of scope at MVP)
932 Create {
933 /// Provider name to create
934 name: String,
935 },
936 /// Delete a tag provider — destructive, refused without --yes
937 Delete {
938 /// Provider name to delete
939 name: String,
940 },
941}
942
943/// `tags alarms …` — the alarm subfamily (05-06, TAGS-07). History
944/// needs a JOURNAL-PROVISIONED gateway (database connection +
945/// alarm-journal profile + general-alarm-settings — default rigs
946/// refuse exit 6 with the provisioning hint); acknowledge is the
947/// gateway-scope 3-arg form, so `--username` is REQUIRED (no
948/// default-guessing) and ack is deliberately NOT `--yes`-guarded
949/// (acknowledging never un-acknowledges anything — a state-advancing
950/// read-adjacent verb).
951#[derive(Debug, Subcommand)]
952pub enum TagsAlarmsCommand {
953 /// List active alarms — eventId/source/state/priority/name
954 Active {
955 /// Filter by alarm source (e.g. `prov:default`)
956 #[arg(long, value_name = "SOURCE")]
957 source: Option<String>,
958 /// Filter by priority (e.g. `High`)
959 #[arg(long, value_name = "PRIORITY")]
960 priority: Option<String>,
961 /// Filter by state (e.g. `Active, Unacknowledged`)
962 #[arg(long, value_name = "STATE")]
963 state: Option<String>,
964 /// Project holding the deployed routes (default ign-cli)
965 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
966 project: String,
967 },
968 /// Query alarm history — requires a journal-provisioned gateway
969 /// (default rigs refuse with the provisioning hint naming the
970 /// missing chain)
971 History {
972 /// Window start: RFC3339 timestamp or epoch-ms
973 #[arg(long, value_name = "T")]
974 start: String,
975 /// Window end: RFC3339 timestamp or epoch-ms
976 #[arg(long, value_name = "T")]
977 end: String,
978 /// Project holding the deployed routes (default ign-cli)
979 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
980 project: String,
981 },
982 /// Acknowledge alarms (explicit --username: the 3-arg wire form
983 /// needs it). NOT --yes-guarded — acknowledging never
984 /// un-acknowledges anything
985 Ack {
986 /// Alarm event ids to acknowledge (from `alarms active`)
987 #[arg(value_name = "ID", required = true)]
988 ids: Vec<String>,
989 /// Acknowledgement note (default: empty)
990 #[arg(long, value_name = "NOTE")]
991 note: Option<String>,
992 /// The username acknowledging (REQUIRED — the 3-arg wire
993 /// form needs it; the CLI never guesses one)
994 #[arg(long, value_name = "NAME")]
995 username: String,
996 /// Project holding the deployed routes (default ign-cli)
997 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
998 project: String,
999 },
1000}
1001
1002/// `tags history …` — the historian subfamily (05-06, TAGS-08).
1003#[derive(Debug, Subcommand)]
1004pub enum TagsHistoryCommand {
1005 /// Query historical tag values — t_stamp + one column per tag;
1006 /// structurally safe anywhere (data requires a provisioned
1007 /// historian)
1008 Query {
1009 /// Tag paths to query, e.g. `[default]T1` (one or more)
1010 #[arg(value_name = "PATH", required = true)]
1011 paths: Vec<String>,
1012 /// Window start: RFC3339 timestamp or epoch-ms
1013 #[arg(long, value_name = "T")]
1014 start: String,
1015 /// Window end: RFC3339 timestamp or epoch-ms
1016 #[arg(long, value_name = "T")]
1017 end: String,
1018 /// Maximum rows returned
1019 #[arg(long, value_name = "N")]
1020 return_size: Option<i64>,
1021 /// Aggregation mode (e.g. `LastValue`, `Average`, `MinMax`);
1022 /// default is the route's LastValue
1023 #[arg(long, value_name = "MODE")]
1024 aggregation: Option<String>,
1025 /// Project holding the deployed routes (default ign-cli)
1026 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
1027 project: String,
1028 },
1029}
1030
1031/// Rig args (04-01, RIG-01): `--rig` rides the TOP level (the
1032/// SessionsArgs `--type` precedent) so every rig verb shares it. The
1033/// command enum carries ONLY the wired verbs — later plans extend it
1034/// one variant at a time (the established extend-per-plan chore; no
1035/// `unimplemented!()` stubs, every commit compiles clippy-clean).
1036#[derive(Debug, clap::Args)]
1037pub struct RigArgs {
1038 /// Rig to operate on (default: IGNITION_RIG, then [rig].default,
1039 /// then the cwd/convention scan)
1040 #[arg(long, value_name = "NAME")]
1041 pub rig: Option<String>,
1042
1043 #[command(subcommand)]
1044 pub command: RigCommand,
1045}
1046
1047#[derive(Debug, Subcommand)]
1048pub enum RigCommand {
1049 /// Bring the rig up (compose up -d --wait) and wait for the
1050 /// gateway: RUNNING, or uncommissioned-as-data (exit 0 + wizard
1051 /// hint in warnings)
1052 Up {
1053 /// Wait budget in seconds — BOTH compose's --wait-timeout and
1054 /// the commissioned probe deadline (default 300)
1055 #[arg(long, default_value_t = 300, value_name = "SECS")]
1056 timeout: u64,
1057 },
1058 /// Stop the rig (compose down --remove-orphans; volumes KEPT —
1059 /// `reset` owns the teardown half)
1060 Down,
1061 /// Tear the rig down AND remove its volumes (down -v
1062 /// --remove-orphans), then bring it back up fresh — destructive,
1063 /// refused without --yes; no stale project/trial state survives
1064 Reset {
1065 /// Wait budget in seconds — BOTH compose's --wait-timeout and
1066 /// the commissioned probe deadline (default 300)
1067 #[arg(long, default_value_t = 300, value_name = "SECS")]
1068 timeout: u64,
1069 },
1070 /// Structured status: services, ports, volumes (allowlist JSON;
1071 /// a down rig is exit-0 data)
1072 Status,
1073 /// Stream the rig's container logs (compose logs passthrough —
1074 /// raw lines, no envelope in any mode; the third streaming
1075 /// exception, README-documented)
1076 Logs {
1077 /// Lines to show from the end of each service's logs
1078 #[arg(long, default_value_t = 200, value_name = "N")]
1079 tail: u32,
1080 /// Follow: stream new lines as they occur (Ctrl-C stops —
1081 /// default process kill, no envelope)
1082 #[arg(short = 'f', long)]
1083 follow: bool,
1084 /// One service's logs only (see `ign rig status` for names)
1085 service: Option<String>,
1086 },
1087 /// Trial-license state: status is credential-free truth; reset
1088 /// (guarded) restarts an EXPIRED trial via the mechanism ladder
1089 /// (token-auth POST, else native gateway login)
1090 Trial(TrialArgs),
1091 /// Snapshot the rig's gateway: native gwbk (roaming backup,
1092 /// streamed) + per-project exports + manifest.json, composed in a
1093 /// timestamped directory — repeatable state
1094 Snapshot {
1095 /// Output directory (default:
1096 /// ./ign-rig-snapshots/<rig>-<yyyyMMdd-HHmmss>/)
1097 #[arg(short = 'o', long, value_name = "DIR")]
1098 output: Option<PathBuf>,
1099 },
1100 /// Restore a gwbk onto the rig's gateway — destructive, refused
1101 /// without --yes; synchronous restore + restart, then a witnessed
1102 /// RUNNING wait
1103 Restore {
1104 /// The gwbk file to restore (from `ign rig snapshot`)
1105 #[arg(long, value_name = "PATH")]
1106 file: PathBuf,
1107 /// Post-restore RUNNING wait budget in seconds (floored at
1108 /// 300 — the gateway restarts after a restore)
1109 #[arg(long, default_value_t = 300, value_name = "SECS")]
1110 timeout: u64,
1111 },
1112}
1113
1114/// Trial subcommands (04-03, RIG-02/03). `status` reads the
1115/// unauthenticated trial endpoint + banners cross-check. `reset` is
1116/// the family's destructive verb (`--yes`-guarded, exit 2 without —
1117/// the reset precedent); the password NEVER rides a flag (env/secret
1118/// only — redaction discipline).
1119#[derive(Debug, clap::Args)]
1120pub struct TrialArgs {
1121 #[command(subcommand)]
1122 pub command: TrialCommand,
1123}
1124
1125#[derive(Debug, Clone, Subcommand)]
1126pub enum TrialCommand {
1127 /// Show the trial state: licenseMode, trialState, seconds left,
1128 /// expired — plus the banners cross-check. No credential needed
1129 /// (the endpoints answer unauthenticated — fresh-rig friendly)
1130 Status,
1131 /// Reset an EXPIRED trial to a fresh window — destructive,
1132 /// refused without --yes. Mechanism ladder: API-token POST
1133 /// (IGNITION_TOKEN) → native gateway login (--user /
1134 /// IGNITION_USER + IGNITION_PASSWORD). Non-expired trials refuse
1135 /// (trial_not_expired)
1136 Reset {
1137 /// Gateway admin username for the login rung (password comes
1138 /// from IGNITION_PASSWORD — never a flag)
1139 #[arg(long, value_name = "NAME")]
1140 user: Option<String>,
1141 },
1142}
1143
1144/// Backup subcommands (07-02, BKUP-01) — the standalone surface of
1145/// the Phase 4 gwbk wire. `restore` is the family's destructive verb
1146/// (the 8th `--yes`-guarded set member: it REPLACES this gateway's
1147/// state); `download` is a streamed read, unguarded.
1148#[derive(Debug, clap::Args)]
1149pub struct BackupArgs {
1150 #[command(subcommand)]
1151 pub command: BackupCommand,
1152}
1153
1154#[derive(Debug, Subcommand)]
1155pub enum BackupCommand {
1156 /// Download a gwbk backup (streamed to disk)
1157 Download {
1158 /// Output file (default: the gateway's Content-Disposition
1159 /// name, else <profile>-backup.gwbk)
1160 #[arg(short, long, value_name = "FILE")]
1161 output: Option<PathBuf>,
1162 /// Backup type: roaming = portable across gateways (default);
1163 /// all includes gateway-specific state
1164 #[arg(long, value_enum, default_value_t = CliBackupType::Roaming)]
1165 r#type: CliBackupType,
1166 },
1167 /// Restore a gwbk onto THIS gateway — destructive, refused
1168 /// without --yes; the gateway restarts and blocks for minutes
1169 /// after the restore
1170 Restore {
1171 /// The gwbk file to restore (from `ign backup download` or
1172 /// `ign rig snapshot`)
1173 file: PathBuf,
1174 },
1175}
1176
1177/// CLI value-enum mirror of the core `BackupType` (ignition-core
1178/// stays clap-free; `From` converts at the dispatch seam).
1179#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
1180pub enum CliBackupType {
1181 /// Portable backup (cross-gateway)
1182 Roaming,
1183 /// Includes gateway-specific state
1184 All,
1185}
1186
1187impl From<CliBackupType> for ignition_core::client::backup::BackupType {
1188 fn from(value: CliBackupType) -> Self {
1189 match value {
1190 CliBackupType::Roaming => Self::Roaming,
1191 CliBackupType::All => Self::All,
1192 }
1193 }
1194}
1195
1196/// EAM subcommands (07-02, BKUP-02) — the read-heavy surface with
1197/// guarded writes. Every runtime verb honestly reports the
1198/// controller-mode state gate (`eam_not_controller` on a stock
1199/// gateway — the README documents the manual flip).
1200#[derive(Debug, clap::Args)]
1201pub struct EamArgs {
1202 #[command(subcommand)]
1203 pub command: EamCommand,
1204}
1205
1206#[derive(Debug, Subcommand)]
1207pub enum EamCommand {
1208 /// EAM task run history (the gateway's own newest-first order)
1209 History {
1210 /// Max entries (default 200 — ALWAYS sent explicitly; the
1211 /// server default is unlimited)
1212 #[arg(long, value_name = "N")]
1213 limit: Option<u32>,
1214 /// Substring search over task names
1215 #[arg(long, value_name = "TEXT")]
1216 search: Option<String>,
1217 },
1218 /// Task definitions: bare `ign eam tasks` lists; with a name
1219 /// shows one definition + its scheduled state
1220 Tasks {
1221 /// One task definition's detail (omit to list all)
1222 name: Option<String>,
1223 },
1224 /// Task definition writes (create; force-dispatch) — `eam task`
1225 /// requires a subcommand (the `rig trial` shape)
1226 #[command(subcommand)]
1227 Task(EamTaskCommand),
1228}
1229
1230/// `eam task …` — the guarded writes (07-02 Task 3; the five
1231/// lifecycle/mutation verbs arrive 10-04). `new` carries the typed
1232/// guard ladder (backup+OnDemand unguarded; mutating types and any
1233/// non-OnDemand schedule need --yes; restore/install/upgrade types
1234/// refuse outright — see README); `force` dispatches NOW, always
1235/// guarded; `suspend`/`resume`/`cancel`/`modify`/`delete` are the
1236/// two-tier guarded verbs (pure precheck → blast-radius preview
1237/// fetch → confirm-with-preview → action — the refusal message IS
1238/// the blast radius).
1239#[derive(Debug, Subcommand)]
1240pub enum EamTaskCommand {
1241 /// Create a task definition (scheduleMode defaults to OnDemand —
1242 /// never auto-fires)
1243 New {
1244 /// Task definition name (any resource name — e.g. nightly-backup)
1245 name: String,
1246 /// Task type — the openapi taxonomy, three classes with different guard consequences:
1247 ///
1248 /// - benign (no --yes with the default OnDemand schedule): eam_backup
1249 ///
1250 /// - mutating (need --yes — they act on their agent targets when dispatched):
1251 /// eam_restart, eam_sendProject, eam_sendResource, eam_sendTags,
1252 /// eam_activateLicense, eam_updateLicense, eam_unactivateLicense
1253 ///
1254 /// - refused (exit 6 eam_task_type_refused — fleet-destructive; run from the EAM console):
1255 /// eam_restoreBackup, eam_installModules, eam_remoteUpgrade
1256 ///
1257 /// Any OTHER type fails safe to the --yes rung (unknown future
1258 /// types stay accepted, never silently unguarded).
1259 ///
1260 /// Example: ign eam task new nightly-backup eam_backup --target gw-a
1261 r#type: String,
1262 /// Target gateway name (repeatable; the GNET agent id)
1263 #[arg(long, value_name = "NAME")]
1264 target: Vec<String>,
1265 /// Setting as K=V with scalar auto-typing — bool/int ride
1266 /// typed, anything else stays a string (repeatable;
1267 /// arrays/objects need --definition)
1268 #[arg(long, value_name = "K=V")]
1269 setting: Vec<String>,
1270 /// Full-JSON settings file deep-merged over the composed
1271 /// `config.settings` (the typed/array settings path)
1272 #[arg(long, value_name = "PATH", conflicts_with = "setting")]
1273 definition: Option<PathBuf>,
1274 /// Schedule mode (default OnDemand — never auto-fires;
1275 /// Immediate/Scheduled/AtTime/AtDelay require --yes)
1276 #[arg(long, value_enum, default_value_t = ScheduleMode::OnDemand)]
1277 schedule_mode: ScheduleMode,
1278 },
1279 /// Force-dispatch a task NOW — destructive, refused without
1280 /// --yes (it dispatches to the agent targets immediately). The
1281 /// confirmation prompt (and every refusal) carries the
1282 /// blast-radius preview line.
1283 Force {
1284 /// Task definition name to dispatch
1285 name: String,
1286 },
1287 /// Suspend a task definition's scheduled dispatches — TASK-scoped
1288 /// runtime verb (there is NO agent-level suspend on the wire; an
1289 /// "agent" is suspended by suspending its tasks). Refused exit 2
1290 /// without --yes, the blast-radius preview in the refusal
1291 /// message; on a stock gateway the runtime seam honestly refuses
1292 /// `eam_not_controller` (exit 6).
1293 Suspend {
1294 /// Task definition name to suspend
1295 name: String,
1296 },
1297 /// Resume a suspended task definition — the suspend inverse
1298 /// (TASK-scoped, like suspend). Refused exit 2 without --yes
1299 /// with the blast-radius preview in the message.
1300 Resume {
1301 /// Task definition name to resume
1302 name: String,
1303 },
1304 /// Cancel a task's PENDING execution (TASK-scoped). Refused exit
1305 /// 2 without --yes with the blast-radius preview in the message;
1306 /// nothing pending is an honest no-op (`fired: false`), a
1307 /// `canCancel: false` row reports the gateway's own refusal.
1308 Cancel {
1309 /// Task definition whose pending execution to cancel
1310 name: String,
1311 },
1312 /// Rewrite targeted keys of a task definition — the full-record
1313 /// read-modify-write (every key the gateway answered rides back;
1314 /// only the targeted keys change). Rename is deliberately
1315 /// ABSENT (the wire answers PUT-rename with 404 — see README's
1316 /// create-new + delete-old composite). Refused exit 2 without
1317 /// --yes with the blast-radius preview in the message.
1318 ///
1319 /// Example: ign eam task modify nightly-backup --enable
1320 Modify {
1321 /// Task definition to modify
1322 name: String,
1323 /// Flip the definition's `enabled` key to true
1324 #[arg(long)]
1325 enable: bool,
1326 /// Flip the definition's `enabled` key to false
1327 /// (conflicts with --enable)
1328 #[arg(long, conflicts_with = "enable")]
1329 disable: bool,
1330 /// Rewrite `config.profile.scheduleMode` (e.g. OnDemand,
1331 /// Scheduled — the scheduleDetails string itself is out of
1332 /// scope; the read-back shows the landed pairing)
1333 #[arg(long, value_name = "MODE")]
1334 schedule_mode: Option<String>,
1335 /// Deep-merge a settings entry over the found
1336 /// `config.settings` (K=V with scalar auto-typing;
1337 /// repeatable — objects merge recursively, arrays/scalars
1338 /// replace)
1339 #[arg(long, value_name = "K=V")]
1340 setting: Vec<String>,
1341 /// Rewrite the definition's `description`
1342 #[arg(long, value_name = "TEXT")]
1343 description: Option<String>,
1344 },
1345 /// Delete a task definition — signature-keyed (the gateway
1346 /// refuses a stale signature), behind the same --yes guard.
1347 /// Refused exit 2 without --yes with the blast-radius preview
1348 /// in the message.
1349 Delete {
1350 /// Task definition to delete
1351 name: String,
1352 },
1353}
1354
1355/// Schedule modes (the openapi taxonomy's user-facing subset —
1356/// SuspendedByFailover is system-owned, not a CLI value).
1357#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
1358pub enum ScheduleMode {
1359 /// Never fires on its own (force dispatches it)
1360 OnDemand,
1361 /// Fires immediately on create (requires --yes)
1362 Immediate,
1363 /// Fires on a schedule (requires --yes)
1364 Scheduled,
1365 /// Fires at an absolute time (requires --yes)
1366 AtTime,
1367 /// Fires after a delay (requires --yes)
1368 AtDelay,
1369}
1370
1371impl ScheduleMode {
1372 /// The wire token.
1373 pub fn wire(self) -> &'static str {
1374 match self {
1375 Self::OnDemand => "OnDemand",
1376 Self::Immediate => "Immediate",
1377 Self::Scheduled => "Scheduled",
1378 Self::AtTime => "AtTime",
1379 Self::AtDelay => "AtDelay",
1380 }
1381 }
1382}
1383
1384/// Script subcommands (07-03, SCRPT-01) — the smallest family: ONE
1385/// verb over the already-shipped, already-secured scriptExec route.
1386/// `script` requires a subcommand (the `rig trial` shape — no bare
1387/// row); the opt-in is STRUCTURAL (the route deploys only via
1388/// `ign webdev deploy --with-script-exec`, which persists the
1389/// secret at 0600), so `run` carries NO `--yes` guard by design —
1390/// the deploy flag IS the opt-in and agents need the verb
1391/// non-interactive (the research-adopted decision).
1392#[derive(Debug, clap::Args)]
1393pub struct ScriptArgs {
1394 #[command(subcommand)]
1395 pub command: ScriptCommand,
1396}
1397
1398/// `ign lint` args (07-04, INTR-02) — the ignition-lint delegation:
1399/// PATHS map to `--target <path>` pairs on the child's arg vector;
1400/// `--` passthrough args ride verbatim after them (power users:
1401/// `--profile`, `--checks`, `--fail-on`, …).
1402#[derive(Debug, clap::Args)]
1403pub struct LintArgs {
1404 /// Paths to lint (one --target pair each)
1405 #[arg(value_name = "PATH", required = true)]
1406 pub paths: Vec<String>,
1407 /// Exit with the linter's own exit code (CI mode). Default is
1408 /// the doctor posture: findings + child_exit_code + the parsed
1409 /// report ride as data and the command exits 0 whenever the
1410 /// tool RAN
1411 #[arg(long)]
1412 pub strict: bool,
1413 /// Extra args passed to ignition-lint verbatim (after --)
1414 #[arg(last = true, value_name = "ARGS")]
1415 pub passthrough: Vec<String>,
1416}
1417
1418#[derive(Debug, Subcommand)]
1419pub enum ScriptCommand {
1420 /// Execute gateway-side Python (Jython) — non-interactive, the
1421 /// route's entire purpose
1422 Run {
1423 /// Inline Python source (a one-liner's best form)
1424 #[arg(long, value_name = "PY")]
1425 code: Option<String>,
1426 /// Python source file (`-` reads stdin — the agent pipe
1427 /// path); giving both --code and --file refuses
1428 /// `invalid_input` (exit 2) pre-resolution
1429 #[arg(long, value_name = "PATH")]
1430 file: Option<String>,
1431 /// The deployed routes' project (default ign-cli — where
1432 /// `ign webdev deploy` put scriptExec)
1433 #[arg(long, default_value = "ign-cli", value_name = "NAME")]
1434 project: String,
1435 },
1436}
1437
1438/// `ign api` args (09-03, EXT-01) — the raw-passthrough family. One
1439/// verb today (`call`); the escape hatch for the 80+ gateway endpoint
1440/// families no curated command covers.
1441#[derive(Debug, clap::Args)]
1442pub struct ApiArgs {
1443 #[command(subcommand)]
1444 pub command: ApiCommand,
1445}
1446
1447#[derive(Debug, Subcommand)]
1448pub enum ApiCommand {
1449 /// Call any gateway REST endpoint raw: `--method`/`--path` are the
1450 /// request, `--data`/`--header`/`--query` shape it, and the
1451 /// envelope's `data.result.data` is the gateway's JSON VERBATIM
1452 /// (no field dropped, no value coerced, key order preserved — the
1453 /// README's documented contract exception). Auth-pattern headers
1454 /// (`Authorization`, `X-Ignition-API-Token`, `Cookie`) are
1455 /// refused pre-I/O — credentials come from the profile.
1456 Call(ApiCallArgs),
1457}
1458
1459/// `ign api call` args (09-03, EXT-01). Arbitrary verbs accepted —
1460/// reqwest's `Method` parser validates (usage-class refusal for
1461/// garbage); `--data` is RAW TEXT on ANY method (GET/DELETE bodies
1462/// allowed — curl parity; the gateway's answer classifies); ONE query
1463/// mechanism (repeatable `--query k=v`, never `?` in `--path`).
1464#[derive(Debug, clap::Args)]
1465pub struct ApiCallArgs {
1466 /// HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, …)
1467 #[arg(long, value_name = "METHOD")]
1468 pub method: String,
1469 /// Absolute path on the gateway (must start with `/`; no `?`, no host)
1470 #[arg(long, value_name = "PATH")]
1471 pub path: String,
1472 /// Raw body text passthrough (rides ANY method — curl parity)
1473 #[arg(long, value_name = "TEXT")]
1474 pub data: Option<String>,
1475 /// Extra request header `Name: Value` (repeatable; split on the
1476 /// FIRST `:`; auth-pattern names are refused pre-I/O)
1477 #[arg(long, value_name = "NAME: VALUE")]
1478 pub header: Vec<String>,
1479 /// Query pair `k=v` (repeatable; split on the FIRST `=` — the
1480 /// ONE query mechanism)
1481 #[arg(long, value_name = "K=V")]
1482 pub query: Vec<String>,
1483}
1484
1485/// `ign license` args (09-04, EXT-02) — the license morning-check
1486/// family. One verb today; room to grow (the uniform-leaf shape).
1487#[derive(Debug, clap::Args)]
1488pub struct LicenseArgs {
1489 #[command(subcommand)]
1490 pub command: LicenseCommand,
1491}
1492
1493#[derive(Debug, Subcommand)]
1494pub enum LicenseCommand {
1495 /// License inventory + trial state in ONE command: the
1496 /// hardware-key item rows and effective stamp ride the
1497 /// `/licenses` read, the license mode + countdown ride the trial
1498 /// companion (the mode is NOT on the licenses payload — capture
1499 /// fact)
1500 Status,
1501}
1502
1503/// `ign redundancy` args (09-04, EXT-02) — the redundancy
1504/// morning-check family.
1505#[derive(Debug, clap::Args)]
1506pub struct RedundancyArgs {
1507 #[command(subcommand)]
1508 pub command: RedundancyCommand,
1509}
1510
1511#[derive(Debug, Subcommand)]
1512pub enum RedundancyCommand {
1513 /// The flat redundancy status: role, project state, peer
1514 /// connection, config access, sync/failover pending, uptime
1515 /// (ms since gateway start — capture-proven) and last-sync (the
1516 /// `-1` never-synced sentinel on fresh rigs)
1517 Status,
1518}
1519
1520/// `ign gan` args (09-04, EXT-02) — the Gateway Area Network
1521/// morning-check family.
1522#[derive(Debug, clap::Args)]
1523pub struct GanArgs {
1524 #[command(subcommand)]
1525 pub command: GanCommand,
1526}
1527
1528#[derive(Debug, Subcommand)]
1529pub enum GanCommand {
1530 /// The GAN overview: total/running connections, in/out byte
1531 /// rates, remote gateways — all zeros are healthy data on a
1532 /// non-GAN gateway (the capture IS the canonical shape)
1533 Status,
1534}
1535
1536/// `ign diagnostics` args (09-05, EXT-02) — the support-bundle
1537/// family: `bundle` nests one level (the `tags provider` grouped-
1538/// subfamily pattern).
1539#[derive(Debug, clap::Args)]
1540pub struct DiagnosticsArgs {
1541 #[command(subcommand)]
1542 pub command: DiagnosticsCommand,
1543}
1544
1545#[derive(Debug, Subcommand)]
1546pub enum DiagnosticsCommand {
1547 /// Support-bundle operations (generate / status / download /
1548 /// wait) — the state machine is ENCODED FROM LIVE CAPTURES:
1549 /// observed states are exactly `Generating` (mid-generation) and
1550 /// `Valid` (terminal/ready), PascalCase, both rigs
1551 #[command(subcommand)]
1552 Bundle(BundleCommand),
1553}
1554
1555/// The four bundle verbs (09-05). Nothing here is destructive:
1556/// generate creates a support bundle, download writes a local file,
1557/// wait polls — none carry `--yes` (the backup-download posture).
1558#[derive(Debug, Subcommand)]
1559pub enum BundleCommand {
1560 /// Start bundle generation (POST, no body) — the 200 answer IS
1561 /// the fresh status (live capture: `{"state":"Generating"}`);
1562 /// generation takes ~2–6 s on a fresh rig, so pair with `wait`
1563 Generate,
1564 /// The bundle status: `state` rides the CAPTURED vocabulary
1565 /// (`Generating` / `Valid` — unknown future states pass through
1566 /// verbatim, never refused); `fileSize` (bytes) appears only
1567 /// when `Valid`
1568 Status,
1569 /// Download the bundle ZIP (streamed to disk; the request rides
1570 /// a 300 s per-request timeout — the 30 s client default would
1571 /// truncate MB-sized bundles)
1572 Download {
1573 /// Output file (default: the gateway's Content-Disposition
1574 /// name, else ignition-diagnostics-bundle-<unix_ts>.zip)
1575 #[arg(short, long, value_name = "FILE")]
1576 output: Option<PathBuf>,
1577 },
1578 /// Poll the status until a terminal captured state (`Valid`)
1579 /// — an UNKNOWN state (outside the captured vocabulary) keeps
1580 /// polling honestly until the deadline; deadline expiry is
1581 /// exit 4 `network_error` (the restart-wait convention, no new
1582 /// slug) with the last observed state in the message
1583 Wait {
1584 /// Poll interval in seconds
1585 #[arg(long, default_value_t = 2, value_name = "SECS")]
1586 interval: u64,
1587 /// Give up after this many seconds (default 300 — mirrors
1588 /// restart --wait)
1589 #[arg(long, default_value_t = 300, value_name = "SECS")]
1590 timeout: u64,
1591 },
1592}
1593
1594/// Profile subcommands (nested: a struct wrapper carrying the subcommand
1595/// enum, so `Commands::Profile` gets an `Args` payload).
1596#[derive(Debug, clap::Args)]
1597pub struct ProfileArgs {
1598 #[command(subcommand)]
1599 pub command: ProfileCmd,
1600}
1601
1602/// Sessions args: the family filter rides the TOP level so bare
1603/// `ign sessions [--type X]` lists (must-have truth #1) while
1604/// `ign sessions terminate …` carries the destructive half.
1605#[derive(Debug, clap::Args)]
1606pub struct SessionsArgs {
1607 /// Filter to one session family (default: all three merged)
1608 #[arg(long, value_enum)]
1609 pub r#type: Option<SessionType>,
1610
1611 #[command(subcommand)]
1612 pub command: Option<SessionsCmd>,
1613}
1614
1615#[derive(Debug, Subcommand)]
1616pub enum SessionsCmd {
1617 /// Terminate (designer: prune / vision: close) a session —
1618 /// destructive, refused without --yes
1619 Terminate {
1620 /// Session family holding the id
1621 #[arg(long, value_enum)]
1622 r#type: SessionType,
1623 /// Session/client id to terminate (see `ign sessions`)
1624 #[arg(long)]
1625 id: String,
1626 /// Message shown to the session's user (Perspective only)
1627 #[arg(long)]
1628 message: Option<String>,
1629 },
1630}
1631
1632/// Logs args: the query filters ride the TOP level so bare
1633/// `ign logs [-f]` lists/tails (must-have truth #1) while `download`
1634/// and the `loggers` subtree hang off the optional subcommand — the
1635/// SessionsArgs precedent.
1636#[derive(Debug, clap::Args)]
1637pub struct LogsArgs {
1638 /// Only entries from this logger (name prefix)
1639 #[arg(long, value_name = "NAME")]
1640 pub logger: Option<String>,
1641 /// Minimum level to include (server-side filter)
1642 #[arg(long, value_enum, value_name = "LEVEL")]
1643 pub min_level: Option<LogLevel>,
1644 /// Start from an absolute EPOCH-MS or a relative span (500ms, 30s,
1645 /// 5min, 2h) — parsed to epoch-ms at arg-parse time
1646 #[arg(
1647 long,
1648 value_name = "EPOCH_MS|Nms|Ns|Nmin|Nh",
1649 value_parser = parse_since_arg
1650 )]
1651 pub since: Option<i64>,
1652 /// Max entries — the server default is UNLIMITED, never used here
1653 #[arg(long, default_value_t = 200)]
1654 pub limit: i64,
1655 /// Follow: stream new entries as they occur (poll-based)
1656 #[arg(short = 'f', long)]
1657 pub follow: bool,
1658 /// Poll interval in seconds (follow mode)
1659 #[arg(long, default_value_t = 2, value_name = "SECS")]
1660 pub interval: u64,
1661 /// Stop after this many seconds (follow mode; default: until Ctrl-C)
1662 #[arg(long, value_name = "SECS")]
1663 pub timeout: Option<u64>,
1664 #[command(subcommand)]
1665 pub command: Option<LogsCmd>,
1666}
1667
1668#[derive(Debug, Subcommand)]
1669pub enum LogsCmd {
1670 /// Download the log archive — a SQLite .idb, never a zip
1671 Download {
1672 /// Output file (default: the gateway's Content-Disposition
1673 /// name, else <profile>-logs-<ts>.idb)
1674 #[arg(short, long, value_name = "FILE")]
1675 output: Option<PathBuf>,
1676 },
1677 /// List loggers / manage logger levels
1678 Loggers(LoggersArgs),
1679}
1680
1681#[derive(Debug, clap::Args)]
1682pub struct LoggersArgs {
1683 /// Substring search over logger names
1684 #[arg(long, value_name = "TEXT")]
1685 pub search: Option<String>,
1686 #[command(subcommand)]
1687 pub command: Option<LoggersCmd>,
1688}
1689
1690#[derive(Debug, Subcommand)]
1691pub enum LoggersCmd {
1692 /// Set one logger's level — a mutation, refused without --yes
1693 Set {
1694 /// Logger name (see `ign logs loggers`)
1695 name: String,
1696 /// Level to set
1697 #[arg(value_enum)]
1698 level: LogLevel,
1699 },
1700 /// Reset ALL logger levels to defaults — refused without --yes
1701 Reset,
1702}
1703
1704/// The seven spec-documented log levels (TRACE..OFF), value-enum form
1705/// for clap; [`LogLevel::wire`] yields the uppercase wire token.
1706#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
1707pub enum LogLevel {
1708 Trace,
1709 Debug,
1710 Info,
1711 Warn,
1712 Error,
1713 Fatal,
1714 Off,
1715}
1716
1717impl LogLevel {
1718 /// The uppercase wire token the gateway expects.
1719 pub fn wire(self) -> &'static str {
1720 match self {
1721 Self::Trace => "TRACE",
1722 Self::Debug => "DEBUG",
1723 Self::Info => "INFO",
1724 Self::Warn => "WARN",
1725 Self::Error => "ERROR",
1726 Self::Fatal => "FATAL",
1727 Self::Off => "OFF",
1728 }
1729 }
1730}
1731
1732/// clap value parser delegating to the core `--since` grammar against
1733/// the current time (a relative span resolves at parse time) — invalid
1734/// specs are clap usage errors (exit 2) like any bad flag value.
1735fn parse_since_arg(spec: &str) -> Result<i64, String> {
1736 let now_ms = std::time::SystemTime::now()
1737 .duration_since(std::time::UNIX_EPOCH)
1738 .expect("system clock is after the unix epoch")
1739 .as_millis() as i64;
1740 ignition_core::actions::logs::parse_since(spec, now_ms)
1741}
1742
1743/// CLI value-enum mirrors of the core action enums (ignition-core stays
1744/// clap-free; `From` converts at the dispatch seam).
1745#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
1746pub enum SessionType {
1747 /// Designer sessions
1748 Designer,
1749 /// Perspective browser sessions
1750 Perspective,
1751 /// Vision clients
1752 Vision,
1753}
1754
1755impl From<SessionType> for ignition_core::actions::sessions::SessionType {
1756 fn from(value: SessionType) -> Self {
1757 match value {
1758 SessionType::Designer => Self::Designer,
1759 SessionType::Perspective => Self::Perspective,
1760 SessionType::Vision => Self::Vision,
1761 }
1762 }
1763}
1764
1765#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
1766pub enum ConnectionType {
1767 /// Database connections
1768 Database,
1769 /// OPC connections
1770 Opc,
1771}
1772
1773impl From<ConnectionType> for ignition_core::actions::connections::ConnectionType {
1774 fn from(value: ConnectionType) -> Self {
1775 match value {
1776 ConnectionType::Database => Self::Database,
1777 ConnectionType::Opc => Self::Opc,
1778 }
1779 }
1780}
1781
1782/// Import collision policy (03-02). Exactly the two values REST
1783/// exposes — `merge` is the Designer import popup's vocabulary and is
1784/// NOT a value: clap's invalid-value error lists the two real
1785/// choices, and the README documents merge as Designer-only (the
1786/// sanctioned rejection-with-hint shape).
1787#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
1788pub enum CollisionPolicy {
1789 /// Refuse when the project already exists (default)
1790 Abort,
1791 /// Replace the ENTIRE project (resources absent from the ZIP are
1792 /// deleted) — destructive, requires --yes
1793 Overwrite,
1794}
1795
1796impl From<CollisionPolicy> for ignition_core::actions::projects::CollisionPolicy {
1797 fn from(value: CollisionPolicy) -> Self {
1798 match value {
1799 CollisionPolicy::Abort => Self::Abort,
1800 CollisionPolicy::Overwrite => Self::Overwrite,
1801 }
1802 }
1803}
1804
1805#[derive(Debug, Subcommand)]
1806#[command(arg_required_else_help = true)]
1807pub enum ProfileCmd {
1808 /// Add (or overwrite) a gateway profile
1809 Add {
1810 /// Profile name
1811 name: String,
1812 /// Gateway base URL (e.g. http://localhost:9088)
1813 url: String,
1814 /// Optional display label
1815 #[arg(long, value_name = "TEXT")]
1816 label: Option<String>,
1817 /// Name of the env var holding the auth token
1818 #[arg(long, value_name = "VAR")]
1819 token_env: Option<String>,
1820 /// Keyring user string for the token (service is always ignition-cli)
1821 #[arg(long, value_name = "USER")]
1822 keyring: Option<String>,
1823 /// Name of the env var holding the basic-auth user (with --password-env)
1824 #[arg(long, value_name = "VAR")]
1825 user_env: Option<String>,
1826 /// Name of the env var holding the basic-auth password (with --user-env)
1827 #[arg(long, value_name = "VAR")]
1828 password_env: Option<String>,
1829 /// Make this profile the active one
1830 #[arg(long)]
1831 active: bool,
1832 },
1833 /// List configured profiles
1834 List,
1835 /// Switch the active profile
1836 Use {
1837 /// Profile name to activate
1838 name: String,
1839 },
1840}