feat(#2554): add hivectl forge reconcile-config to reconcile local applied config against forge main

This commit is contained in:
damocles 2026-07-17 12:31:08 +02:00 committed by mara
commit d124dd205a
9 changed files with 335 additions and 1 deletions

View file

@ -281,6 +281,41 @@ pub enum ForgeCmd {
#[arg(long, conflicts_with = "password")]
password_stdin: bool,
},
/// Show + reconcile the divergence between an agent's local applied
/// config checkout and its forge `agent-configs/<agent>` main.
///
/// Always prints the diff first. `--from forge` resets the local
/// checkout to forge main (effective on the next deploy); `--from local`
/// is not supported yet. With no `--from`, prompts for the direction.
ReconcileConfig {
/// Agent whose config branches to reconcile.
agent: String,
/// Which side to reconcile from. Omit to be prompted after the diff.
#[arg(long, value_enum)]
from: Option<ReconcileFrom>,
/// Include the full diff (not just `--stat`) in the report.
#[arg(long)]
verbose: bool,
},
}
/// Which side to reconcile config branches from (`hivectl forge
/// reconcile-config --from`). Maps to [`hive_host_sock::ReconcileDirection`].
#[derive(Clone, Copy, clap::ValueEnum)]
pub enum ReconcileFrom {
/// Reset the local applied checkout to forge main.
Forge,
/// Advance forge main from local — not supported yet.
Local,
}
impl From<ReconcileFrom> for hive_host_sock::ReconcileDirection {
fn from(from: ReconcileFrom) -> Self {
match from {
ReconcileFrom::Forge => Self::Forge,
ReconcileFrom::Local => Self::Local,
}
}
}
#[derive(Subcommand)]

View file

@ -2,10 +2,13 @@
//! daemon (which owns the forge admin token + account-token persistence);
//! hivectl just resolves the password client-side and relays the request.
use std::io::{self, Write};
use std::path::Path;
use anyhow::Result;
use hive_host_sock::{HostRequest, ReconcileDirection};
use crate::cli::ReconcileFrom;
use crate::util::{daemon_request, resolve_password};
pub(crate) async fn forge_create_user(
@ -29,3 +32,58 @@ pub(crate) async fn forge_create_user(
)
.await
}
/// `hivectl forge reconcile-config <agent> [--from <forge|local>] [--verbose]`.
/// Always shows the divergence first (daemon computes it read-only), then
/// applies the chosen direction — from `--from` or an interactive prompt.
pub(crate) async fn forge_reconcile_config(
socket: &Path,
agent: &str,
from: Option<ReconcileFrom>,
verbose: bool,
) -> Result<()> {
daemon_request(
socket,
HostRequest::ReconcileConfigStatus {
agent: agent.to_owned(),
verbose,
},
"forge",
)
.await?;
let direction = match from {
Some(f) => Some(f.into()),
None => prompt_direction()?,
};
let Some(direction) = direction else {
println!("forge: aborted — no changes made");
return Ok(());
};
daemon_request(
socket,
HostRequest::ReconcileConfigApply {
agent: agent.to_owned(),
direction,
},
"forge",
)
.await
}
/// Prompt the operator for the reconcile direction after showing the diff.
/// Returns `None` to abort (empty / `q` / unrecognized input).
fn prompt_direction() -> Result<Option<ReconcileDirection>> {
print!("reconcile from [forge/local] (or q to abort): ");
io::stdout().flush()?;
let mut line = String::new();
io::stdin().read_line(&mut line)?;
match line.trim().to_ascii_lowercase().as_str() {
"forge" | "f" => Ok(Some(ReconcileDirection::Forge)),
"local" | "l" => Ok(Some(ReconcileDirection::Local)),
"" | "q" | "quit" => Ok(None),
other => {
println!("forge: unrecognized direction {other:?} — aborting");
Ok(None)
}
}
}

View file

@ -42,7 +42,7 @@ use choom::choom;
mod github;
use github::github_set_token;
mod forge;
use forge::forge_create_user;
use forge::{forge_create_user, forge_reconcile_config};
mod agents;
use agents::run_agents;
mod power;
@ -69,6 +69,11 @@ async fn main() -> Result<()> {
password,
password_stdin,
} => forge_create_user(&socket, &name, password.as_deref(), password_stdin).await,
ForgeCmd::ReconcileConfig {
agent,
from,
verbose,
} => forge_reconcile_config(&socket, &agent, from, verbose).await,
},
Cmd::Matrix { cmd } => run_matrix_cmd(&socket, cmd).await,
Cmd::Github { cmd } => match cmd {