feat(#2554): add hivectl forge reconcile-config to reconcile local applied config against forge main
This commit is contained in:
parent
11df4a1bf5
commit
d124dd205a
9 changed files with 335 additions and 1 deletions
|
|
@ -7,6 +7,7 @@ This document contains the help content for the `hivectl` command-line program.
|
||||||
* [`hivectl`↴](#hivectl)
|
* [`hivectl`↴](#hivectl)
|
||||||
* [`hivectl forge`↴](#hivectl-forge)
|
* [`hivectl forge`↴](#hivectl-forge)
|
||||||
* [`hivectl forge create-user`↴](#hivectl-forge-create-user)
|
* [`hivectl forge create-user`↴](#hivectl-forge-create-user)
|
||||||
|
* [`hivectl forge reconcile-config`↴](#hivectl-forge-reconcile-config)
|
||||||
* [`hivectl matrix`↴](#hivectl-matrix)
|
* [`hivectl matrix`↴](#hivectl-matrix)
|
||||||
* [`hivectl matrix create-user`↴](#hivectl-matrix-create-user)
|
* [`hivectl matrix create-user`↴](#hivectl-matrix-create-user)
|
||||||
* [`hivectl matrix sync-admin`↴](#hivectl-matrix-sync-admin)
|
* [`hivectl matrix sync-admin`↴](#hivectl-matrix-sync-admin)
|
||||||
|
|
@ -99,6 +100,7 @@ Manual entry point to the same idempotent provisioning c0re runs at boot — for
|
||||||
###### **Subcommands:**
|
###### **Subcommands:**
|
||||||
|
|
||||||
* `create-user` — Create or refresh the Forgejo account + token for `<name>`
|
* `create-user` — Create or refresh the Forgejo account + token for `<name>`
|
||||||
|
* `reconcile-config` — Show + reconcile the divergence between an agent's local applied config checkout and its forge `agent-configs/<agent>` main
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -121,6 +123,32 @@ For an existing agent, persists the token to its state dir; for a human/other ac
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## `hivectl forge reconcile-config`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**Usage:** `hivectl forge reconcile-config [OPTIONS] <AGENT>`
|
||||||
|
|
||||||
|
###### **Arguments:**
|
||||||
|
|
||||||
|
* `<AGENT>` — Agent whose config branches to reconcile
|
||||||
|
|
||||||
|
###### **Options:**
|
||||||
|
|
||||||
|
* `--from <FROM>` — Which side to reconcile from. Omit to be prompted after the diff
|
||||||
|
|
||||||
|
Possible values:
|
||||||
|
- `forge`:
|
||||||
|
Reset the local applied checkout to forge main
|
||||||
|
- `local`:
|
||||||
|
Advance forge main from local — not supported yet
|
||||||
|
|
||||||
|
* `--verbose` — Include the full diff (not just `--stat`) in the report
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `hivectl matrix`
|
## `hivectl matrix`
|
||||||
|
|
||||||
matrix-tuwunel user provisioning.
|
matrix-tuwunel user provisioning.
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@ hivectl forge create-user iris # provision (or refresh) forge account f
|
||||||
hivectl forge create-user mara # create forge account for a human user; prints token to stdout
|
hivectl forge create-user mara # create forge account for a human user; prints token to stdout
|
||||||
hivectl forge create-user mara --password hunter2 # set a web-login password
|
hivectl forge create-user mara --password hunter2 # set a web-login password
|
||||||
hivectl forge create-user mara --password-stdin # read password from stdin (safer for scripting)
|
hivectl forge create-user mara --password-stdin # read password from stdin (safer for scripting)
|
||||||
|
|
||||||
|
hivectl forge reconcile-config iris # show local-applied <-> forge config divergence, then prompt
|
||||||
|
hivectl forge reconcile-config iris --from forge # reset local applied checkout to forge main (effective next deploy)
|
||||||
|
hivectl forge reconcile-config iris --verbose # include the full diff, not just --stat
|
||||||
```
|
```
|
||||||
|
|
||||||
- For **agents** (name has a state dir under `/var/lib/hyperhive/agents/`):
|
- For **agents** (name has a state dir under `/var/lib/hyperhive/agents/`):
|
||||||
|
|
@ -38,6 +42,12 @@ hivectl forge create-user mara --password-stdin # read password from stdin (s
|
||||||
re-mints the token and prints it again — safe for password resets.
|
re-mints the token and prints it again — safe for password resets.
|
||||||
- Without `--password` / `--password-stdin` a random throwaway password
|
- Without `--password` / `--password-stdin` a random throwaway password
|
||||||
is used (fine for agents — they auth by token).
|
is used (fine for agents — they auth by token).
|
||||||
|
- `reconcile-config <agent>` shows the divergence between the agent's local
|
||||||
|
applied config checkout and its forge `agent-configs/<agent>` `main`, then
|
||||||
|
reconciles. `--from forge` resets the local checkout to forge `main` (takes
|
||||||
|
effect on the next deploy — it does not auto-rebuild). `--from local` is not
|
||||||
|
supported yet (forge `main` is core-only branch-protected; resolve via a
|
||||||
|
config PR). With no `--from` it prompts for the direction after the diff.
|
||||||
|
|
||||||
## Matrix
|
## Matrix
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
mod ci_runner;
|
mod ci_runner;
|
||||||
pub mod config_pr_poll;
|
pub mod config_pr_poll;
|
||||||
mod pr_merge;
|
mod pr_merge;
|
||||||
|
mod reconcile;
|
||||||
mod repos;
|
mod repos;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
|
|
@ -14,6 +15,7 @@ pub use pr_merge::{
|
||||||
ForgeMergeError, config_repo, fetch_pr_head_into_applied, merge_config_pr_ff, post_pr_comment,
|
ForgeMergeError, config_repo, fetch_pr_head_into_applied, merge_config_pr_ff, post_pr_comment,
|
||||||
pr_head_sha, pr_is_open,
|
pr_head_sha, pr_is_open,
|
||||||
};
|
};
|
||||||
|
pub use reconcile::{reconcile_config_apply, reconcile_config_status};
|
||||||
pub use repos::{
|
pub use repos::{
|
||||||
create_agent_repo, ensure_config_repo, ensure_knowledge_repo, ensure_meta_remote, ensure_repo,
|
create_agent_repo, ensure_config_repo, ensure_knowledge_repo, ensure_meta_remote, ensure_repo,
|
||||||
ensure_shared_docs_repo, meta_read_access, push_config, push_meta, shared_docs_access,
|
ensure_shared_docs_repo, meta_read_access, push_config, push_meta, shared_docs_access,
|
||||||
|
|
|
||||||
157
hive-c0re/src/forge/reconcile.rs
Normal file
157
hive-c0re/src/forge/reconcile.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
//! Reconcile an agent's local applied config checkout against its forge
|
||||||
|
//! `agent-configs/<agent>` `main`. Backs `hivectl forge reconcile-config`
|
||||||
|
//! via the host-socket `ReconcileConfig{Status,Apply}` requests. hive-c0re
|
||||||
|
//! owns the applied checkout + the forge credential, so the reconcile op
|
||||||
|
//! lives here rather than in the CLI.
|
||||||
|
//!
|
||||||
|
//! `Status` is read-only (fetches forge `main` into a scratch ref, reports
|
||||||
|
//! the divergence). `Apply(Forge)` resets the local applied checkout to
|
||||||
|
//! forge `main` — the change takes effect on the next deploy (the deploy's
|
||||||
|
//! `--override-input` re-locks against the reset local tree); it does NOT
|
||||||
|
//! auto-deploy. `Apply(Local)` is not supported: forge `main` is core-only
|
||||||
|
//! branch-protected (advanced solely by the config-PR ff-merge API).
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
|
use hive_host_sock::{HostResponse, ReconcileDirection};
|
||||||
|
|
||||||
|
use super::{CONFIG_ORG, core_token, forge_git_url, is_present};
|
||||||
|
|
||||||
|
/// Scratch ref the forge `main` is fetched into — outside the normal
|
||||||
|
/// branch/tag namespace so it never collides with real refs.
|
||||||
|
const FORGE_MAIN_REF: &str = "refs/hyperhive/forge-config-main";
|
||||||
|
|
||||||
|
/// Fetch `agent-configs/<agent>` `main` into the applied repo's scratch
|
||||||
|
/// ref (read-only, no working-tree change) and return the applied dir.
|
||||||
|
async fn fetch_forge_main(agent: &str) -> Result<PathBuf> {
|
||||||
|
if !is_present().await {
|
||||||
|
anyhow::bail!("forge is not running");
|
||||||
|
}
|
||||||
|
let Some(token) = core_token() else {
|
||||||
|
anyhow::bail!("forge core token not available");
|
||||||
|
};
|
||||||
|
let dir = crate::paths::applied_dir(agent);
|
||||||
|
if !dir.join(".git").exists() {
|
||||||
|
anyhow::bail!("agent `{agent}` has no applied config checkout");
|
||||||
|
}
|
||||||
|
let url = forge_git_url(&token, &format!("{CONFIG_ORG}/{agent}"));
|
||||||
|
crate::lifecycle::git(
|
||||||
|
&dir,
|
||||||
|
&["fetch", "--force", &url, &format!("main:{FORGE_MAIN_REF}")],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("fetch forge config main (does forge main exist yet?)")?;
|
||||||
|
Ok(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a git command in `dir` and capture stdout, erroring on non-zero.
|
||||||
|
async fn git_out(dir: &Path, args: &[&str]) -> Result<String> {
|
||||||
|
let out = crate::lifecycle::git_command()
|
||||||
|
.current_dir(dir)
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("invoke git {args:?}"))?;
|
||||||
|
if !out.status.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"git {args:?} failed: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr).trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append `lines` (from a git text block) to `messages`, each indented.
|
||||||
|
fn push_indented(messages: &mut Vec<String>, block: &str) {
|
||||||
|
messages.extend(block.lines().map(|l| format!(" {l}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Report the divergence between the local applied checkout and forge
|
||||||
|
/// `main` (host request `ReconcileConfigStatus`). Read-only.
|
||||||
|
pub async fn reconcile_config_status(agent: &str, verbose: bool) -> Result<HostResponse> {
|
||||||
|
let dir = fetch_forge_main(agent).await?;
|
||||||
|
let counts = git_out(
|
||||||
|
&dir,
|
||||||
|
&[
|
||||||
|
"rev-list",
|
||||||
|
"--left-right",
|
||||||
|
"--count",
|
||||||
|
&format!("HEAD...{FORGE_MAIN_REF}"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let mut parts = counts.split_whitespace();
|
||||||
|
let local_ahead = parts.next().unwrap_or("0");
|
||||||
|
let forge_ahead = parts.next().unwrap_or("0");
|
||||||
|
let mut messages = vec![
|
||||||
|
format!("agent {agent}: local applied main <-> forge agent-configs/{agent} main"),
|
||||||
|
format!(" local ahead by {local_ahead}, forge ahead by {forge_ahead} commit(s)"),
|
||||||
|
];
|
||||||
|
if local_ahead == "0" && forge_ahead == "0" {
|
||||||
|
messages.push(" in sync — nothing to reconcile".to_owned());
|
||||||
|
return Ok(HostResponse::messages(messages));
|
||||||
|
}
|
||||||
|
if forge_ahead != "0" {
|
||||||
|
messages.push(" forge-only commits (applied by --from forge):".to_owned());
|
||||||
|
push_indented(
|
||||||
|
&mut messages,
|
||||||
|
&git_out(
|
||||||
|
&dir,
|
||||||
|
&["log", "--oneline", &format!("HEAD..{FORGE_MAIN_REF}")],
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if local_ahead != "0" {
|
||||||
|
messages.push(" local-only commits (discarded by --from forge):".to_owned());
|
||||||
|
push_indented(
|
||||||
|
&mut messages,
|
||||||
|
&git_out(
|
||||||
|
&dir,
|
||||||
|
&["log", "--oneline", &format!("{FORGE_MAIN_REF}..HEAD")],
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
messages.push(" diff --stat (local HEAD -> forge main):".to_owned());
|
||||||
|
push_indented(
|
||||||
|
&mut messages,
|
||||||
|
&git_out(&dir, &["diff", "--stat", "HEAD", FORGE_MAIN_REF]).await?,
|
||||||
|
);
|
||||||
|
if verbose {
|
||||||
|
messages.push(" full diff:".to_owned());
|
||||||
|
push_indented(
|
||||||
|
&mut messages,
|
||||||
|
&git_out(&dir, &["diff", "HEAD", FORGE_MAIN_REF]).await?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(HostResponse::messages(messages))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a reconcile in `direction` (host request `ReconcileConfigApply`).
|
||||||
|
/// `Forge` resets the local applied checkout to forge `main`; `Local` is
|
||||||
|
/// unsupported and returns a clear error.
|
||||||
|
pub async fn reconcile_config_apply(
|
||||||
|
agent: &str,
|
||||||
|
direction: ReconcileDirection,
|
||||||
|
) -> Result<HostResponse> {
|
||||||
|
match direction {
|
||||||
|
ReconcileDirection::Local => Ok(HostResponse::error(
|
||||||
|
"reconciling forge from local is not supported yet: forge main is core-only \
|
||||||
|
branch-protected (no-push, ff-merge-API-only). Resolve via a config PR, or use \
|
||||||
|
`--from forge` to reset the local checkout to forge main.",
|
||||||
|
)),
|
||||||
|
ReconcileDirection::Forge => {
|
||||||
|
let dir = fetch_forge_main(agent).await?;
|
||||||
|
crate::lifecycle::git(&dir, &["reset", "--hard", FORGE_MAIN_REF])
|
||||||
|
.await
|
||||||
|
.context("reset applied main to forge main")?;
|
||||||
|
Ok(HostResponse::messages(vec![
|
||||||
|
format!("reconciled applied/{agent} to forge agent-configs/{agent} main"),
|
||||||
|
format!("effective on next deploy — rebuild {agent} to apply now"),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -196,6 +196,12 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
||||||
HostRequest::ForgeCreateUser { name, password } => {
|
HostRequest::ForgeCreateUser { name, password } => {
|
||||||
handle_forge_create_user(name, password.as_deref()).await?
|
handle_forge_create_user(name, password.as_deref()).await?
|
||||||
}
|
}
|
||||||
|
HostRequest::ReconcileConfigStatus { agent, verbose } => {
|
||||||
|
crate::forge::reconcile_config_status(agent, *verbose).await?
|
||||||
|
}
|
||||||
|
HostRequest::ReconcileConfigApply { agent, direction } => {
|
||||||
|
crate::forge::reconcile_config_apply(agent, *direction).await?
|
||||||
|
}
|
||||||
HostRequest::GatewayCreateUser { username, password } => {
|
HostRequest::GatewayCreateUser { username, password } => {
|
||||||
HostResponse::messages(vec![crate::gateway_nginx::create_user(username, password)?])
|
HostResponse::messages(vec![crate::gateway_nginx::create_user(username, password)?])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,17 @@ pub fn container_name(name: &str) -> String {
|
||||||
format!("{AGENT_PREFIX}{name}")
|
format!("{AGENT_PREFIX}{name}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which way to reconcile an agent's config branches
|
||||||
|
/// ([`HostRequest::ReconcileConfigApply`]).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ReconcileDirection {
|
||||||
|
/// Reset the local applied checkout to forge `main`.
|
||||||
|
Forge,
|
||||||
|
/// Advance forge `main` from local — not supported yet.
|
||||||
|
Local,
|
||||||
|
}
|
||||||
|
|
||||||
/// Requests on the host admin socket.
|
/// Requests on the host admin socket.
|
||||||
///
|
///
|
||||||
/// Wire format: one JSON object per line.
|
/// Wire format: one JSON object per line.
|
||||||
|
|
@ -200,6 +211,28 @@ pub enum HostRequest {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
},
|
},
|
||||||
|
/// Report the divergence between agent `agent`'s local applied config
|
||||||
|
/// checkout and its forge `agent-configs/<agent>` `main`. The daemon
|
||||||
|
/// fetches forge `main` read-only and returns a human-readable report
|
||||||
|
/// (ahead/behind counts, commit-range summary, `git diff --stat`, and
|
||||||
|
/// the full diff when `verbose`) in [`HostResponse::messages`]. Read-only
|
||||||
|
/// — never mutates either side. Backs `hivectl forge reconcile-config`
|
||||||
|
/// (the diff it always shows first).
|
||||||
|
ReconcileConfigStatus {
|
||||||
|
agent: String,
|
||||||
|
#[serde(default)]
|
||||||
|
verbose: bool,
|
||||||
|
},
|
||||||
|
/// Reconcile agent `agent`'s config branches in `direction`.
|
||||||
|
/// `Forge` resets the local applied checkout to forge `main` (takes
|
||||||
|
/// effect on the next deploy); `Local` is not supported yet and returns
|
||||||
|
/// an [`HostResponse::error`] (advancing the protected forge `main` from
|
||||||
|
/// local needs lifting branch protection — resolve via a config PR).
|
||||||
|
/// Backs `hivectl forge reconcile-config --from <forge|local>`.
|
||||||
|
ReconcileConfigApply {
|
||||||
|
agent: String,
|
||||||
|
direction: ReconcileDirection,
|
||||||
|
},
|
||||||
/// Add or update a gateway HTTP-Basic user in the daemon's htpasswd file
|
/// Add or update a gateway HTTP-Basic user in the daemon's htpasswd file
|
||||||
/// (`paths::GATEWAY_HTPASSWD`). Daemon-side equivalent of `hivectl gateway
|
/// (`paths::GATEWAY_HTPASSWD`). Daemon-side equivalent of `hivectl gateway
|
||||||
/// create-user`: the daemon bcrypt-hashes `password` (cost 12, remapped to
|
/// create-user`: the daemon bcrypt-hashes `password` (cost 12, remapped to
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,41 @@ pub enum ForgeCmd {
|
||||||
#[arg(long, conflicts_with = "password")]
|
#[arg(long, conflicts_with = "password")]
|
||||||
password_stdin: bool,
|
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)]
|
#[derive(Subcommand)]
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,13 @@
|
||||||
//! daemon (which owns the forge admin token + account-token persistence);
|
//! daemon (which owns the forge admin token + account-token persistence);
|
||||||
//! hivectl just resolves the password client-side and relays the request.
|
//! hivectl just resolves the password client-side and relays the request.
|
||||||
|
|
||||||
|
use std::io::{self, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use hive_host_sock::{HostRequest, ReconcileDirection};
|
||||||
|
|
||||||
|
use crate::cli::ReconcileFrom;
|
||||||
use crate::util::{daemon_request, resolve_password};
|
use crate::util::{daemon_request, resolve_password};
|
||||||
|
|
||||||
pub(crate) async fn forge_create_user(
|
pub(crate) async fn forge_create_user(
|
||||||
|
|
@ -29,3 +32,58 @@ pub(crate) async fn forge_create_user(
|
||||||
)
|
)
|
||||||
.await
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ use choom::choom;
|
||||||
mod github;
|
mod github;
|
||||||
use github::github_set_token;
|
use github::github_set_token;
|
||||||
mod forge;
|
mod forge;
|
||||||
use forge::forge_create_user;
|
use forge::{forge_create_user, forge_reconcile_config};
|
||||||
mod agents;
|
mod agents;
|
||||||
use agents::run_agents;
|
use agents::run_agents;
|
||||||
mod power;
|
mod power;
|
||||||
|
|
@ -69,6 +69,11 @@ async fn main() -> Result<()> {
|
||||||
password,
|
password,
|
||||||
password_stdin,
|
password_stdin,
|
||||||
} => forge_create_user(&socket, &name, password.as_deref(), password_stdin).await,
|
} => 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::Matrix { cmd } => run_matrix_cmd(&socket, cmd).await,
|
||||||
Cmd::Github { cmd } => match cmd {
|
Cmd::Github { cmd } => match cmd {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue