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
use std::path::PathBuf;
use anyhow::Result;
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::Shell;
use crate::config::ConfigFileFormat;
use crate::status::OutputFormat;
#[derive(Parser)]
#[command(name = "ferrflow")]
#[command(about = "Universal semantic versioning for monorepos and classic repos")]
#[command(version)]
pub struct Cli {
/// Dry run — show what would happen without making changes
#[arg(long, global = true)]
pub dry_run: bool,
/// Verbose output
#[arg(short, long, global = true)]
pub verbose: bool,
/// Path to config file (overrides auto-detection, env: FERRFLOW_CONFIG)
#[arg(long, global = true, env = "FERRFLOW_CONFIG")]
pub config: Option<PathBuf>,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
/// Show what versions would be bumped (dry run)
Check {
/// Output as JSON
#[arg(long)]
json: bool,
/// Pre-release channel override (e.g. beta, rc, dev)
#[arg(long)]
channel: Option<String>,
},
/// Bump versions, update changelogs, create tags and push
Release {
/// Allow floating tags to move backward to a lower version
#[arg(long)]
force: bool,
/// Pre-release channel override (e.g. beta, rc, dev)
#[arg(long)]
channel: Option<String>,
/// Create releases as drafts (GitHub only). A subsequent `ferrflow release`
/// without --draft will detect and publish existing drafts automatically.
#[arg(long)]
draft: bool,
},
/// Generate/update CHANGELOG.md only
Changelog,
/// Scaffold a ferrflow configuration file
Init {
/// Config file format (json, json5, toml)
#[arg(long)]
format: Option<ConfigFileFormat>,
},
/// Print each package name, current version, and last release tag
Status {
/// Output format
#[arg(long, value_enum, default_value = "text")]
output: OutputFormat,
},
/// Print the current version of a package
Version {
/// Package name (required in monorepos, optional in single repos)
package: Option<String>,
/// Output as JSON
#[arg(long)]
json: bool,
},
/// Print the last release tag of a package
Tag {
/// Package name (required in monorepos, optional in single repos)
package: Option<String>,
/// Output as JSON
#[arg(long)]
json: bool,
},
/// Validate config and versioned files
Validate {
/// Output as JSON
#[arg(long)]
json: bool,
/// Remote repository (e.g. owner/repo for GitHub, or gitlab:group/project)
#[arg(long)]
repo: Option<String>,
/// Git ref for remote validation (branch, tag, commit)
#[arg(long, name = "ref")]
git_ref: Option<String>,
},
/// Generate shell completion scripts
Completions {
/// Shell to generate completions for
shell: Shell,
},
}
impl Cli {
pub fn run(self) -> Result<()> {
match self.command {
Commands::Check { json, channel } => crate::monorepo::check(
self.config.as_deref(),
self.verbose,
json,
channel.as_deref(),
),
Commands::Release {
force,
channel,
draft,
} => crate::monorepo::release(
self.config.as_deref(),
self.dry_run,
self.verbose,
force,
channel.as_deref(),
draft,
),
Commands::Changelog => {
crate::changelog::generate_only(self.config.as_deref(), self.dry_run)
}
Commands::Init { format } => crate::config::init(format),
Commands::Status { output } => crate::status::run(self.config.as_deref(), &output),
Commands::Version { package, json } => {
crate::query::version(self.config.as_deref(), package.as_deref(), json)
}
Commands::Tag { package, json } => {
crate::query::tag(self.config.as_deref(), package.as_deref(), json)
}
Commands::Validate {
json,
repo,
git_ref,
} => crate::validate::run(
self.config.as_deref(),
json,
repo.as_deref(),
git_ref.as_deref(),
),
Commands::Completions { shell } => {
clap_complete::generate(
shell,
&mut Cli::command(),
"ferrflow",
&mut std::io::stdout(),
);
Ok(())
}
}
}
}