anvilsign in

collin/anvil · f32fb115

chore: rename the anvil crate to anvil-cli

Collin Richards · 2026-06-10 08:28 UTC · f32fb115fa2e20b0df7207483eed5c39d7c67b3d · parent cc0e8932 · browse files

modifiedCargo.lock+14 −14
⋯ 119 unchanged lines
120120 ]
121121
122122 [[package]]
123-name = "anvil"
123+name = "anvil-ci"
124124 version = "0.0.0"
125125 dependencies = [
126- "anvil-ci",
127126 "anvil-core",
128- "anvil-ssh",
129- "anvil-web",
130- "anyhow",
131- "clap",
127+ "anvil-git",
128+ "bollard",
129+ "futures-util",
130+ "reqwest",
131+ "serde_json",
132+ "tar",
132133 "tokio",
133134 "tracing",
134- "tracing-subscriber",
135135 ]
136136
137137 [[package]]
138-name = "anvil-ci"
138+name = "anvil-cli"
139139 version = "0.0.0"
140140 dependencies = [
141+ "anvil-ci",
141142 "anvil-core",
142- "anvil-git",
143- "bollard",
144- "futures-util",
145- "reqwest",
146- "serde_json",
147- "tar",
143+ "anvil-ssh",
144+ "anvil-web",
145+ "anyhow",
146+ "clap",
148147 "tokio",
149148 "tracing",
149+ "tracing-subscriber",
150150 ]
151151
152152 [[package]]
⋯ 5115 unchanged lines
addedcrates/anvil-cli/Cargo.toml+24 −0
1+[package]
2+name = "anvil-cli"
3+version.workspace = true
4+edition.workspace = true
5+license.workspace = true
6+repository.workspace = true
7+rust-version.workspace = true
8+description = "anvil git forge daemon and admin CLI."
9+default-run = "anvild"
10+
11+[[bin]]
12+name = "anvild"
13+path = "src/main.rs"
14+
15+[dependencies]
16+anvil-core.workspace = true
17+anvil-web.workspace = true
18+anvil-ssh.workspace = true
19+anvil-ci.workspace = true
20+tokio.workspace = true
21+clap.workspace = true
22+anyhow.workspace = true
23+tracing.workspace = true
24+tracing-subscriber.workspace = true
addedcrates/anvil-cli/src/main.rs+221 −0
1+//! `anvild` — the anvil git forge daemon and admin CLI.
2+
3+use anvil_core::{
4+ App,
5+ Config,
6+ repos,
7+ ssh_keys,
8+ users,
9+};
10+use anyhow::{
11+ Context,
12+ Result,
13+};
14+use clap::{
15+ Parser,
16+ Subcommand,
17+};
18+
19+#[derive(Parser)]
20+#[command(name = "anvild", version, about = "anvil git forge")]
21+struct Cli {
22+ /// Path to the configuration file (TOML). Defaults are used if absent.
23+ #[arg(long, short, default_value = "anvil.toml", global = true)]
24+ config: String,
25+
26+ /// Override the data directory from config.
27+ #[arg(long, global = true)]
28+ data_dir: Option<String>,
29+
30+ #[command(subcommand)]
31+ command: Option<Command>,
32+}
33+
34+#[derive(Subcommand)]
35+enum Command {
36+ /// Run the server (default).
37+ Serve,
38+ /// Apply database migrations and exit.
39+ Migrate,
40+ /// Manage users.
41+ User {
42+ #[command(subcommand)]
43+ command: UserCommand,
44+ },
45+ /// Manage repositories.
46+ Repo {
47+ #[command(subcommand)]
48+ command: RepoCommand,
49+ },
50+}
51+
52+#[derive(Subcommand)]
53+enum UserCommand {
54+ /// Create a new user.
55+ Create {
56+ username: String,
57+ #[arg(long, default_value = "")]
58+ email: String,
59+ #[arg(long)]
60+ password: String,
61+ #[arg(long)]
62+ admin: bool,
63+ },
64+ /// Register an SSH public key for a user (for git-over-SSH access).
65+ AddKey {
66+ username: String,
67+ /// The OpenSSH public key line. Mutually exclusive with --key-file.
68+ #[arg(long)]
69+ key: Option<String>,
70+ /// Path to a `.pub` file (e.g. ~/.ssh/id_ed25519.pub).
71+ #[arg(long)]
72+ key_file: Option<String>,
73+ #[arg(long, default_value = "")]
74+ title: String,
75+ },
76+}
77+
78+#[derive(Subcommand)]
79+enum RepoCommand {
80+ /// Create a repository, given as `owner/name`.
81+ Create {
82+ /// Repository in `owner/name` form.
83+ path: String,
84+ #[arg(long, default_value = "")]
85+ description: String,
86+ #[arg(long)]
87+ private: bool,
88+ },
89+}
90+
91+#[tokio::main]
92+async fn main() -> Result<()> {
93+ tracing_subscriber::fmt()
94+ .with_env_filter(
95+ tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
96+ )
97+ .init();
98+
99+ let cli = Cli::parse();
100+
101+ let mut config = Config::load_or_default(&cli.config)
102+ .with_context(|| format!("loading config from {}", cli.config))?;
103+ if let Some(dir) = &cli.data_dir {
104+ config.data_dir = dir.into();
105+ }
106+
107+ match cli.command.unwrap_or(Command::Serve) {
108+ Command::Serve => serve(config).await,
109+ Command::Migrate => migrate(config).await,
110+ Command::User { command } => user(config, command).await,
111+ Command::Repo { command } => repo(config, command).await,
112+ }
113+}
114+
115+async fn serve(config: Config) -> Result<()> {
116+ let mut app = App::bootstrap(config).await?;
117+
118+ // Start the CI runner: it drains queued runs and processes new ones pushed
119+ // through `app.ci_tx` (set here so handlers can notify it).
120+ let (ci_tx, ci_rx) = tokio::sync::mpsc::unbounded_channel();
121+ app.ci_tx = Some(ci_tx);
122+ tokio::spawn(anvil_ci::run_worker(app.clone(), ci_rx));
123+
124+ if app.config.ssh.enabled {
125+ // Run the HTTP and SSH servers concurrently; if either exits, stop.
126+ tokio::try_join!(anvil_web::serve(app.clone()), anvil_ssh::serve(app))?;
127+ } else {
128+ anvil_web::serve(app).await?;
129+ }
130+ Ok(())
131+}
132+
133+async fn migrate(config: Config) -> Result<()> {
134+ App::bootstrap(config).await?;
135+ println!("migrations applied");
136+ Ok(())
137+}
138+
139+async fn user(config: Config, command: UserCommand) -> Result<()> {
140+ let app = App::bootstrap(config).await?;
141+ match command {
142+ UserCommand::Create {
143+ username,
144+ email,
145+ password,
146+ admin,
147+ } => {
148+ let user = users::create(&app.db, &username, &email, &password, admin).await?;
149+ println!(
150+ "created user {} (id {}){}",
151+ user.username,
152+ user.id,
153+ if user.is_admin { " [admin]" } else { "" }
154+ );
155+ }
156+ UserCommand::AddKey {
157+ username,
158+ key,
159+ key_file,
160+ title,
161+ } => {
162+ let user = users::find_by_username(&app.db, &username)
163+ .await?
164+ .with_context(|| format!("no such user: {username}"))?;
165+ let openssh = match (key, key_file) {
166+ (Some(k), None) => k,
167+ (None, Some(path)) => std::fs::read_to_string(&path)
168+ .with_context(|| format!("reading key file {path}"))?,
169+ (Some(_), Some(_)) => anyhow::bail!("pass only one of --key / --key-file"),
170+ (None, None) => anyhow::bail!("pass --key or --key-file"),
171+ };
172+ let (fingerprint, content) = ssh_keys::parse_public_key(&openssh)?;
173+ let saved = ssh_keys::add(&app.db, user.id, &title, &fingerprint, &content).await?;
174+ println!(
175+ "added ssh key for {} ({})",
176+ user.username, saved.fingerprint
177+ );
178+ }
179+ }
180+ Ok(())
181+}
182+
183+async fn repo(config: Config, command: RepoCommand) -> Result<()> {
184+ let app = App::bootstrap(config).await?;
185+ match command {
186+ RepoCommand::Create {
187+ path,
188+ description,
189+ private,
190+ } => {
191+ let (owner_name, name) = path
192+ .split_once('/')
193+ .context("repository path must be in `owner/name` form")?;
194+ let owner = users::find_by_username(&app.db, owner_name)
195+ .await?
196+ .with_context(|| format!("no such user: {owner_name}"))?;
197+ let repo = repos::create(
198+ &app.db,
199+ &app.config.repositories_dir(),
200+ &owner,
201+ name,
202+ &description,
203+ private,
204+ )
205+ .await?;
206+ println!(
207+ "created repository {}/{} (id {}) at {}",
208+ owner.username,
209+ repo.name,
210+ repo.id,
211+ anvil_core::storage::repo_path(
212+ &app.config.repositories_dir(),
213+ &owner.username,
214+ name
215+ )
216+ .display()
217+ );
218+ }
219+ }
220+ Ok(())
221+}
deletedcrates/anvil/Cargo.toml+0 −24
1-[package]
2-name = "anvil"
3-version.workspace = true
4-edition.workspace = true
5-license.workspace = true
6-repository.workspace = true
7-rust-version.workspace = true
8-description = "anvil git forge daemon and admin CLI."
9-default-run = "anvild"
10-
11-[[bin]]
12-name = "anvild"
13-path = "src/main.rs"
14-
15-[dependencies]
16-anvil-core.workspace = true
17-anvil-web.workspace = true
18-anvil-ssh.workspace = true
19-anvil-ci.workspace = true
20-tokio.workspace = true
21-clap.workspace = true
22-anyhow.workspace = true
23-tracing.workspace = true
24-tracing-subscriber.workspace = true
deletedcrates/anvil/src/main.rs+0 −221
1-//! `anvild` — the anvil git forge daemon and admin CLI.
2-
3-use anvil_core::{
4- App,
5- Config,
6- repos,
7- ssh_keys,
8- users,
9-};
10-use anyhow::{
11- Context,
12- Result,
13-};
14-use clap::{
15- Parser,
16- Subcommand,
17-};
18-
19-#[derive(Parser)]
20-#[command(name = "anvild", version, about = "anvil git forge")]
21-struct Cli {
22- /// Path to the configuration file (TOML). Defaults are used if absent.
23- #[arg(long, short, default_value = "anvil.toml", global = true)]
24- config: String,
25-
26- /// Override the data directory from config.
27- #[arg(long, global = true)]
28- data_dir: Option<String>,
29-
30- #[command(subcommand)]
31- command: Option<Command>,
32-}
33-
34-#[derive(Subcommand)]
35-enum Command {
36- /// Run the server (default).
37- Serve,
38- /// Apply database migrations and exit.
39- Migrate,
40- /// Manage users.
41- User {
42- #[command(subcommand)]
43- command: UserCommand,
44- },
45- /// Manage repositories.
46- Repo {
47- #[command(subcommand)]
48- command: RepoCommand,
49- },
50-}
51-
52-#[derive(Subcommand)]
53-enum UserCommand {
54- /// Create a new user.
55- Create {
56- username: String,
57- #[arg(long, default_value = "")]
58- email: String,
59- #[arg(long)]
60- password: String,
61- #[arg(long)]
62- admin: bool,
63- },
64- /// Register an SSH public key for a user (for git-over-SSH access).
65- AddKey {
66- username: String,
67- /// The OpenSSH public key line. Mutually exclusive with --key-file.
68- #[arg(long)]
69- key: Option<String>,
70- /// Path to a `.pub` file (e.g. ~/.ssh/id_ed25519.pub).
71- #[arg(long)]
72- key_file: Option<String>,
73- #[arg(long, default_value = "")]
74- title: String,
75- },
76-}
77-
78-#[derive(Subcommand)]
79-enum RepoCommand {
80- /// Create a repository, given as `owner/name`.
81- Create {
82- /// Repository in `owner/name` form.
83- path: String,
84- #[arg(long, default_value = "")]
85- description: String,
86- #[arg(long)]
87- private: bool,
88- },
89-}
90-
91-#[tokio::main]
92-async fn main() -> Result<()> {
93- tracing_subscriber::fmt()
94- .with_env_filter(
95- tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
96- )
97- .init();
98-
99- let cli = Cli::parse();
100-
101- let mut config = Config::load_or_default(&cli.config)
102- .with_context(|| format!("loading config from {}", cli.config))?;
103- if let Some(dir) = &cli.data_dir {
104- config.data_dir = dir.into();
105- }
106-
107- match cli.command.unwrap_or(Command::Serve) {
108- Command::Serve => serve(config).await,
109- Command::Migrate => migrate(config).await,
110- Command::User { command } => user(config, command).await,
111- Command::Repo { command } => repo(config, command).await,
112- }
113-}
114-
115-async fn serve(config: Config) -> Result<()> {
116- let mut app = App::bootstrap(config).await?;
117-
118- // Start the CI runner: it drains queued runs and processes new ones pushed
119- // through `app.ci_tx` (set here so handlers can notify it).
120- let (ci_tx, ci_rx) = tokio::sync::mpsc::unbounded_channel();
121- app.ci_tx = Some(ci_tx);
122- tokio::spawn(anvil_ci::run_worker(app.clone(), ci_rx));
123-
124- if app.config.ssh.enabled {
125- // Run the HTTP and SSH servers concurrently; if either exits, stop.
126- tokio::try_join!(anvil_web::serve(app.clone()), anvil_ssh::serve(app))?;
127- } else {
128- anvil_web::serve(app).await?;
129- }
130- Ok(())
131-}
132-
133-async fn migrate(config: Config) -> Result<()> {
134- App::bootstrap(config).await?;
135- println!("migrations applied");
136- Ok(())
137-}
138-
139-async fn user(config: Config, command: UserCommand) -> Result<()> {
140- let app = App::bootstrap(config).await?;
141- match command {
142- UserCommand::Create {
143- username,
144- email,
145- password,
146- admin,
147- } => {
148- let user = users::create(&app.db, &username, &email, &password, admin).await?;
149- println!(
150- "created user {} (id {}){}",
151- user.username,
152- user.id,
153- if user.is_admin { " [admin]" } else { "" }
154- );
155- }
156- UserCommand::AddKey {
157- username,
158- key,
159- key_file,
160- title,
161- } => {
162- let user = users::find_by_username(&app.db, &username)
163- .await?
164- .with_context(|| format!("no such user: {username}"))?;
165- let openssh = match (key, key_file) {
166- (Some(k), None) => k,
167- (None, Some(path)) => std::fs::read_to_string(&path)
168- .with_context(|| format!("reading key file {path}"))?,
169- (Some(_), Some(_)) => anyhow::bail!("pass only one of --key / --key-file"),
170- (None, None) => anyhow::bail!("pass --key or --key-file"),
171- };
172- let (fingerprint, content) = ssh_keys::parse_public_key(&openssh)?;
173- let saved = ssh_keys::add(&app.db, user.id, &title, &fingerprint, &content).await?;
174- println!(
175- "added ssh key for {} ({})",
176- user.username, saved.fingerprint
177- );
178- }
179- }
180- Ok(())
181-}
182-
183-async fn repo(config: Config, command: RepoCommand) -> Result<()> {
184- let app = App::bootstrap(config).await?;
185- match command {
186- RepoCommand::Create {
187- path,
188- description,
189- private,
190- } => {
191- let (owner_name, name) = path
192- .split_once('/')
193- .context("repository path must be in `owner/name` form")?;
194- let owner = users::find_by_username(&app.db, owner_name)
195- .await?
196- .with_context(|| format!("no such user: {owner_name}"))?;
197- let repo = repos::create(
198- &app.db,
199- &app.config.repositories_dir(),
200- &owner,
201- name,
202- &description,
203- private,
204- )
205- .await?;
206- println!(
207- "created repository {}/{} (id {}) at {}",
208- owner.username,
209- repo.name,
210- repo.id,
211- anvil_core::storage::repo_path(
212- &app.config.repositories_dir(),
213- &owner.username,
214- name
215- )
216- .display()
217- );
218- }
219- }
220- Ok(())
221-}