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
use clap::CommandFactory;
use clap::Parser;
use clap::Subcommand;
use clap_complete::Shell;
use frork_cli::assertions::AssertionType;
use frork_cli::assertions::Status;
use frork_cli::report;
use frork_cli::runtime::run_code;
use frork_cli::runtime::run_script;
use miette::IntoDiagnostic as _;
use miette::Result;
use miette::miette;
/// CalVer, set by build.rs — the crate version is not what ships.
const VERSION: &str = env!("FRORK_VERSION");
#[derive(Parser)]
#[command(name = "frork", version = VERSION)]
#[command(about = "A Fennel-based configuration management tool")]
struct Cli {
/// Generate shell completions and exit.
#[arg(long, value_enum)]
completions: Option<Shell>,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
Check { code: String },
Do { code: String },
Status { script: String },
Satisfy { script: String },
}
fn main() -> Result<()> {
// Assertion types panic through todo!() for cases that are not implemented
// yet; this renders those with the same formatting as recoverable errors.
miette::set_panic_hook();
tracing_subscriber::fmt::init();
let cli = Cli::parse();
if let Some(shell) = cli.completions {
clap_complete::generate(shell, &mut Cli::command(), "frork", &mut std::io::stdout());
return Ok(());
}
let Some(command) = cli.command else {
Cli::command().print_help().into_diagnostic()?;
return Ok(());
};
run(&command)
}
fn run(command: &Commands) -> Result<()> {
match command {
Commands::Check { code } => run_code(code, status),
Commands::Do { code } => run_code(code, satisfy),
Commands::Status { script } => run_script(script, status),
Commands::Satisfy { script } => run_script(script, satisfy),
}
}
fn status(status: &Status, assertion: &dyn AssertionType) -> Result<()> {
print_lines(&report::render(status, assertion));
Ok(())
}
fn satisfy(status: &Status, assertion: &dyn AssertionType) -> Result<()> {
print_lines(&report::render(status, assertion));
match status {
Status::Ok => {}
Status::Missing => {
assertion.install()?;
println!("ok: {assertion}");
}
Status::ConflictUpgrade(_) => {
if report::wants_upgrade(&prompt("Upgrade? [y/N]: ")?) {
assertion.upgrade()?;
println!("ok: {assertion}");
} else {
println!("skipped: {assertion}");
}
}
}
Ok(())
}
fn print_lines(lines: &[String]) {
for line in lines {
println!("{line}");
}
}
fn prompt(question: &str) -> Result<String> {
use std::io::Write as _;
print!("{question}");
std::io::stdout()
.flush()
.map_err(|e| miette!("Failed to flush stdout: {e}"))?;
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.map_err(|e| miette!("Failed to read input: {e}"))?;
Ok(input)
}