Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit f1da9f5

Browse files
driver: show help + Ariadne suggestion for missing CLI argument errors
DriverError::Help gains an optional 'suggestion' field (Box<DriverReport>). For the all_cli_missing path, we now return Help with both the full subcommand-level help text and the corrected-command Ariadne diagnostic as the suggestion. The suggestion is rendered last, so it sits close to the terminal prompt where it's easiest to read. The <suggestion> source label is renamed to <usage> to better reflect what the annotation is showing. The missing-subcommand path continues to return Help with no suggestion.
1 parent 6312acb commit f1da9f5

12 files changed

Lines changed: 145 additions & 13 deletions

crates/figue/src/builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ impl<T> ConfigBuilder<T> {
263263
///
264264
/// let result = Driver::new(config).run().into_result();
265265
/// match result {
266-
/// Err(DriverError::Help { text }) => {
266+
/// Err(DriverError::Help { text, .. }) => {
267267
/// assert!(text.contains("myapp"));
268268
/// }
269269
/// _ => panic!("expected help"),

crates/figue/src/driver.rs

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ use crate::env_subst::{EnvSubstError, RealEnv, substitute_env_vars};
3333
use crate::help::generate_help_for_subcommand;
3434
use crate::layers::{cli::parse_cli, env::parse_env, file::parse_file};
3535
use crate::merge::merge_layers;
36-
use crate::missing::{collect_missing_fields, format_missing_fields_summary};
36+
use crate::missing::{
37+
build_corrected_command_diagnostics, collect_missing_fields, format_missing_fields_summary,
38+
};
3739
use crate::path::Path;
3840
use crate::provenance::{FileResolution, Override, Provenance};
3941
use crate::span::Span;
@@ -235,7 +237,10 @@ impl<T: Facet<'static>> Driver<T> {
235237
&subcommand_path,
236238
&help_config,
237239
);
238-
return DriverOutcome::err(DriverError::Help { text });
240+
return DriverOutcome::err(DriverError::Help {
241+
text,
242+
suggestion: None,
243+
});
239244
}
240245

241246
// Check for --version
@@ -414,7 +419,10 @@ impl<T: Facet<'static>> Driver<T> {
414419
.unwrap_or_default();
415420

416421
let help = generate_help_for_subcommand(&self.config.schema, &[], &help_config);
417-
return DriverOutcome::err(DriverError::Help { text: help });
422+
return DriverOutcome::err(DriverError::Help {
423+
text: help,
424+
suggestion: None,
425+
});
418426
}
419427

420428
// Check if the only missing field is a subcommand with available variants
@@ -451,7 +459,10 @@ impl<T: Facet<'static>> Driver<T> {
451459
&subcommand_path,
452460
&help_config,
453461
);
454-
return DriverOutcome::err(DriverError::Help { text: help });
462+
return DriverOutcome::err(DriverError::Help {
463+
text: help,
464+
suggestion: None,
465+
});
455466
}
456467

457468
// Check if all missing fields are simple CLI arguments (not config fields)
@@ -488,7 +499,27 @@ impl<T: Facet<'static>> Driver<T> {
488499
&subcommand_path,
489500
&help_config,
490501
);
491-
return DriverOutcome::err(DriverError::Help { text: help });
502+
503+
// Build the Ariadne corrected-command suggestion so the user
504+
// can see exactly which argument is missing, right above the
505+
// prompt where it's easy to spot.
506+
let corrected = build_corrected_command_diagnostics(
507+
&missing_fields,
508+
cli_args_source.as_deref(),
509+
);
510+
let suggestion = Box::new(DriverReport {
511+
diagnostics: corrected.diagnostics,
512+
layers,
513+
file_resolution,
514+
overrides,
515+
cli_args_source: corrected.corrected_source,
516+
source_name: "<usage>".to_string(),
517+
});
518+
519+
return DriverOutcome::err(DriverError::Help {
520+
text: help,
521+
suggestion: Some(suggestion),
522+
});
492523
}
493524

494525
let message = {
@@ -806,8 +837,11 @@ impl<T> DriverOutcome<T> {
806837
pub fn unwrap(self) -> T {
807838
match self.0 {
808839
Ok(output) => output.get(),
809-
Err(DriverError::Help { text }) => {
840+
Err(DriverError::Help { text, suggestion }) => {
810841
println!("{}", text);
842+
if let Some(s) = suggestion {
843+
println!("{}", s.render_pretty());
844+
}
811845
std::process::exit(0);
812846
}
813847
Err(DriverError::Completions { script }) => {
@@ -1205,7 +1239,7 @@ fn extract_shell_from_value(value: &ConfigValue) -> Option<Shell> {
12051239
/// Ok(output) => {
12061240
/// // use output.value
12071241
/// }
1208-
/// Err(DriverError::Help { text }) => {
1242+
/// Err(DriverError::Help { text, .. }) => {
12091243
/// // print text and exit(0)
12101244
/// let _ = text;
12111245
/// }
@@ -1248,6 +1282,10 @@ pub enum DriverError {
12481282
Help {
12491283
/// Formatted help text ready to print to stdout.
12501284
text: String,
1285+
/// Optional Ariadne-rendered suggestion to display after the help text
1286+
/// (e.g. a corrected command showing exactly which argument is missing).
1287+
/// Printed last so it sits close to the terminal prompt.
1288+
suggestion: Option<Box<DriverReport>>,
12511289
},
12521290

12531291
/// Shell completions were requested (via `#[facet(figue::completions)]` field).
@@ -1308,7 +1346,7 @@ impl DriverError {
13081346
/// Returns the help text if this is a help request.
13091347
pub fn help_text(&self) -> Option<&str> {
13101348
match self {
1311-
DriverError::Help { text } => Some(text),
1349+
DriverError::Help { text, .. } => Some(text),
13121350
_ => None,
13131351
}
13141352
}
@@ -1319,7 +1357,13 @@ impl std::fmt::Display for DriverError {
13191357
match self {
13201358
DriverError::Builder { error } => write!(f, "{}", error),
13211359
DriverError::Failed { report } => write!(f, "{}", report),
1322-
DriverError::Help { text } => write!(f, "{}", text),
1360+
DriverError::Help { text, suggestion } => {
1361+
write!(f, "{}", text)?;
1362+
if let Some(s) = suggestion {
1363+
write!(f, "\n{}", s.render_pretty())?;
1364+
}
1365+
Ok(())
1366+
}
13231367
DriverError::Completions { script } => write!(f, "{}", script),
13241368
DriverError::Version { text } => write!(f, "{}", text),
13251369
DriverError::EnvSubst { error } => write!(f, "{}", error),
@@ -1339,7 +1383,13 @@ impl std::process::Termination for DriverError {
13391383
fn report(self) -> std::process::ExitCode {
13401384
// Print the appropriate output
13411385
match &self {
1342-
DriverError::Help { text } | DriverError::Version { text } => {
1386+
DriverError::Help { text, suggestion } => {
1387+
println!("{}", text);
1388+
if let Some(s) = suggestion {
1389+
println!("{}", s.render_pretty());
1390+
}
1391+
}
1392+
DriverError::Version { text } => {
13431393
println!("{}", text);
13441394
}
13451395
DriverError::Completions { script } => {
@@ -1392,7 +1442,7 @@ mod tests {
13921442
let result = driver.run().into_result();
13931443

13941444
match result {
1395-
Err(DriverError::Help { text }) => {
1445+
Err(DriverError::Help { text, .. }) => {
13961446
assert!(
13971447
text.contains("test-app"),
13981448
"help should contain program name"
@@ -1566,6 +1616,7 @@ mod tests {
15661616
fn test_driver_error_exit_codes() {
15671617
let help_err = DriverError::Help {
15681618
text: "help".to_string(),
1619+
suggestion: None,
15691620
};
15701621
let version_err = DriverError::Version {
15711622
text: "1.0".to_string(),

crates/figue/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ pub fn from_slice<T: Facet<'static>>(args: &[&str]) -> DriverOutcome<T> {
385385
///
386386
/// let result = figue::from_slice::<Args>(&["--help"]).into_result();
387387
/// match result {
388-
/// Err(DriverError::Help { text }) => {
388+
/// Err(DriverError::Help { text, .. }) => {
389389
/// // text contains the full help output
390390
/// let _ = text;
391391
/// }

crates/figue/tests/integration/snapshots/main__integration__ariadne__ariadne_missing_argument.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,12 @@ USAGE:
99

1010
OPTIONS:
1111
--required-field <STRING>
12+
13+
14+
Error: missing required argument
15+
╭─[ <usage>:1:6 ]
16+
17+
1 │ main --required-field <required-field>
18+
│ ────────────────┬────────────────
19+
│ ╰────────────────── missing required argument
20+
───╯

crates/figue/tests/integration/snapshots/main__integration__sequence__noargs_single_positional.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,12 @@ USAGE:
1010
ARGUMENTS:
1111
<INPUT>
1212
The input file to process
13+
14+
15+
Error: missing required argument
16+
╭─[ <usage>:1:6 ]
17+
18+
1 │ main <input>
19+
│ ───┬───
20+
│ ╰───── The input file to process
21+
───╯

crates/figue/tests/integration/snapshots/main__integration__sequence__noargs_vec_positional_no_default.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,12 @@ USAGE:
1010
ARGUMENTS:
1111
<FILES>
1212
Files to process (at least one required)
13+
14+
15+
Error: missing required argument
16+
╭─[ <usage>:1:6 ]
17+
18+
1 │ main <files>
19+
│ ───┬───
20+
│ ╰───── Files to process (at least one required)
21+
───╯

crates/figue/tests/integration/snapshots/main__integration__subcommand_errors__nested_subcommand_missing_required.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,12 @@ USAGE:
1212
ARGUMENTS:
1313
<URL>
1414
Repository URL
15+
16+
17+
Error: missing required argument
18+
╭─[ <usage>:1:12 ]
19+
20+
1 │ repo clone <url>
21+
│ ──┬──
22+
│ ╰──── Repository URL
23+
───╯

crates/figue/tests/integration/snapshots/main__integration__subcommand_errors__subcommand_missing_required_named.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,12 @@ OPTIONS:
1212
Server URL (required)
1313
--timeout <U32>
1414
Optional timeout
15+
16+
17+
Error: missing required argument
18+
╭─[ <usage>:1:9 ]
19+
20+
1 │ connect --url <url>
21+
│ ─────┬─────
22+
│ ╰─────── Server URL (required)
23+
───╯

crates/figue/tests/integration/snapshots/main__integration__subcommand_errors__subcommand_missing_required_positional.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,12 @@ ARGUMENTS:
1616
OPTIONS:
1717
-t, --template <STRING>
1818
Template to use (skips interactive selection)
19+
20+
21+
Error: missing required argument
22+
╭─[ <usage>:1:6 ]
23+
24+
1 │ init <name>
25+
│ ───┬──
26+
│ ╰──── Project name (creates directory with this name)
27+
───╯

crates/figue/tests/integration/snapshots/main__integration__subcommand_errors__subcommand_mixed_missing_arguments.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,12 @@ OPTIONS:
1616
Deployment region (required)
1717
--version <STRING>
1818
Optional version tag
19+
20+
21+
Error: missing required argument
22+
╭─[ <usage>:1:19 ]
23+
24+
1 │ deploy production --region <region>
25+
│ ────────┬────────
26+
│ ╰────────── Deployment region (required)
27+
───╯

0 commit comments

Comments
 (0)