pub mod db;
pub mod health;
pub mod ops;
pub mod snapshot;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct InfraContext {
pub dir: PathBuf,
}
impl InfraContext {
pub fn try_resolve_from(
project_path: Option<&Path>,
monorepo_path: Option<&Path>,
) -> Option<Self> {
if let Ok(p) = std::env::var("NODE_INFRA_PATH") {
let root = PathBuf::from(p);
return Self::from_root(root);
}
if let Some(home) = dirs_home() {
let data = home.join(".cache/node-app/node-infrastructure");
if let Some(ctx) = Self::from_root(data) {
return Some(ctx);
}
}
let mut search_starts: Vec<PathBuf> = Vec::new();
if let Ok(cwd) = std::env::current_dir() {
search_starts.push(cwd);
}
if let Some(p) = project_path { search_starts.push(p.to_path_buf()); }
if let Some(p) = monorepo_path { search_starts.push(p.to_path_buf()); }
for start in search_starts {
if let Some(ctx) = Self::search_siblings(&start) {
return Some(ctx);
}
}
None
}
fn from_root(root: PathBuf) -> Option<Self> {
let compose_dir = root.join("compose");
if compose_dir.join("docker-compose.yml").exists() {
if let Ok(dir) = compose_dir.canonicalize() {
return Some(Self { dir });
}
}
None
}
fn search_siblings(start: &Path) -> Option<Self> {
let mut dir = start.to_path_buf();
loop {
if let Some(parent) = dir.parent() {
for name in &["node-infra", "node-infrastructure"] {
if let Some(ctx) = Self::from_root(parent.join(name)) {
return Some(ctx);
}
}
dir = parent.to_path_buf();
} else {
return None;
}
dir.parent()?;
}
}
#[allow(dead_code)]
pub fn clone_or_pull(&self, verbose: bool) -> anyhow::Result<()> {
use std::process::Command;
let repo = "https://github.com/econ-v1/econ-v1.git";
if self.dir.join(".git").exists() {
let status = Command::new("git")
.args(["-C", &self.dir.to_string_lossy(), "pull", "--ff-only"])
.status();
if verbose {
match status {
Ok(s) if s.success() => {}
Ok(s) => eprintln!("infra: git pull exited {}", s),
Err(e) => eprintln!("infra: git pull failed: {}", e),
}
}
} else {
let parent = self.dir.parent().unwrap_or(Path::new("."));
let name = self.dir.file_name().unwrap_or_default();
let status = Command::new("git")
.args(["clone", "--depth=1", repo, &name.to_string_lossy()])
.current_dir(parent)
.status();
if verbose {
match status {
Ok(s) if s.success() => {}
Ok(s) => eprintln!("infra: git clone exited {}", s),
Err(e) => eprintln!("infra: git clone failed: {}", e),
}
}
}
Ok(())
}
#[allow(dead_code)]
pub fn pull_if_stale_background(dir: PathBuf) {
std::thread::spawn(move || {
let is_stale = std::fs::metadata(&dir)
.and_then(|m| m.modified())
.map(|t| {
t.elapsed()
.map(|e| e.as_secs() > 3600)
.unwrap_or(false)
})
.unwrap_or(false);
if is_stale {
let ctx = InfraContext { dir };
let _ = ctx.clone_or_pull(false);
}
});
}
pub fn rgs_url(&self) -> String {
"http://localhost:8011".to_string()
}
pub fn compose_dir(&self) -> &Path {
&self.dir
}
}
fn dirs_home() -> Option<PathBuf> {
std::env::var_os("HOME").map(PathBuf::from)
}
use anyhow::{bail, Result};
use clap::Subcommand;
#[derive(Subcommand, Debug)]
pub enum InfraAction {
Up,
Down,
Status,
Logs {
service: Option<String>,
},
Snapshot,
DbSummary,
RgsUrl,
Update,
}
pub fn run(action: Option<InfraAction>) -> Result<()> {
let ctx = match InfraContext::try_resolve_from(None, None) {
Some(ctx) => ctx,
None => bail!(
"Could not find node infrastructure.\n\
Searched:\n ~/.cache/node-app/node-infrastructure/\n\
Sibling dirs: node-infra/, node-infrastructure/\n\n\
Set NODE_INFRA_PATH=/path/to/infra to override."
),
};
match action {
None => {
println!("Infrastructure: {}", ctx.dir.display());
println!("RGS URL: {}", ctx.rgs_url());
println!();
println!("Tip: use `node-app dev` for the interactive TUI (press I for infra view).");
println!("Subcommands: up · down · status · logs · snapshot · db-summary · rgs-url · update");
Ok(())
}
Some(InfraAction::Up) => compose_run(&ctx, &["up", "-d"]),
Some(InfraAction::Down) => compose_run(&ctx, &["down"]),
Some(InfraAction::Status) => {
health::print_status(&ctx);
Ok(())
}
Some(InfraAction::Logs { service }) => {
let mut args = vec!["logs", "-f", "--no-color"];
let svc;
if let Some(ref s) = service {
svc = s.as_str();
args.push(svc);
}
compose_run(&ctx, &args)
}
Some(InfraAction::Snapshot) => {
let s = snapshot::fetch_snapshot_summary(&ctx, None)?;
println!("Version: v{}", s.version);
println!("Timestamp: {}", s.timestamp_str);
println!("Chain: {}", s.chain_hash);
println!("Nodes: {}", s.node_count);
println!("Channels: {}", s.channel_count);
println!("Updates: {}", s.update_count);
Ok(())
}
Some(InfraAction::DbSummary) => db::print_summary(&ctx),
Some(InfraAction::RgsUrl) => {
println!("{}", ctx.rgs_url());
Ok(())
}
Some(InfraAction::Update) => {
ctx.clone_or_pull(true)?;
println!("Infrastructure repo up to date.");
Ok(())
}
}
}
fn compose_run(ctx: &InfraContext, args: &[&str]) -> Result<()> {
let status = std::process::Command::new("docker")
.arg("compose")
.args(args)
.current_dir(ctx.compose_dir())
.status()
.map_err(|e| anyhow::anyhow!("docker compose: {e}"))?;
if !status.success() {
bail!("docker compose {} failed (exit {status})", args[0]);
}
Ok(())
}