Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/target
.cargo/config.toml
.cache/
*.orig
.claude/plans/
.claude/scheduled_tasks.lock
.claude/settings.local.json
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- `ant node start`/`ant node stop` with `--service-name` now resolve the node ID through the daemon API instead of reading `node_registry.json` directly, eliminating a race against concurrent registry mutations by the daemon.
- `ant node add --json` no longer interleaves binary-download progress with the JSON result; progress output now goes to stderr (was stdout) and is suppressed entirely in JSON mode.

### Changed
- `--evm-network` still defaults to `arbitrum-one`, **except** when a devnet manifest carrying an EVM block is loaded: that combination now errors and asks for an explicit choice (`local` to use the manifest, or a preset to override it). The old behavior silently overrode the manifest's EVM config, producing no-op mainnet-vault transactions on other chains that spent gas and set a useless ANT allowance before every chunk PUT failed payment verification. An explicit preset selected alongside a manifest EVM block now prints a warning that the manifest's EVM config is ignored. No change for mainnet users or read-only operations.
- Default network binding changed from IPv4-only to IPv6 dual-stack. Hosts without a working IPv6 stack should pass `--ipv4-only` to avoid advertising unreachable v6 addresses to the DHT (which causes slow connects and junk address records).
- `ant file upload` now writes datamaps as `<filename>.<extension>.datamap` instead of stripping the extension. Uploading `photo.jpg` produces `photo.jpg.datamap` (was `photo.datamap`). Existing datamaps remain readable.
- `ant file upload` no longer silently overwrites an existing datamap. Repeated uploads of the same source path produce `name-2.datamap`, `name-3.datamap`, … capped at 100 attempts. Pass `--overwrite` to restore the previous behaviour.
Expand All @@ -18,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `ant file download --datamap` now reads both msgpack (canonical) and legacy JSON datamaps, so datamaps produced by older versions of the GUI download cleanly via the CLI.

### Internal
- CLI-audit thinning (V2-189): `PortRange` parsing (`FromStr`), env `KEY=VALUE` parsing (`AddNodeOpts::parse_env_vars`), and bootstrap-peer resolution (`config::resolve_bootstrap_peers`) moved from ant-cli into ant-core; `node add`/`node reset` daemon calls now go through `ant_core::node::daemon::client` (new `add_node`/`reset`/`resolve_node_id_by_name` functions) instead of hand-rolled HTTP; the two CLI `ProgressReporter` impls collapsed into one.
- New `ant_core::datamap_file` module owns the on-disk datamap format (msgpack canonical, JSON legacy auto-detect on read) and naming convention. `ant-cli` and consumers like `ant-gui` route through this single helper instead of reimplementing serialization.

## [0.1.1] - 2026-03-28
Expand Down
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion ant-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ clap = { version = "4", features = ["derive", "env"] }
colored = "3.1.1"
hex = "0.4"
indicatif = "0.17"
reqwest = { version = "0.12", features = ["json"] }
rmp-serde = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
10 changes: 7 additions & 3 deletions ant-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,13 @@ pub struct Cli {
#[arg(short, long, action = ArgAction::Count)]
pub verbose: u8,

/// EVM network for payment processing (arbitrum-one, arbitrum-sepolia, local).
#[arg(long, default_value = "arbitrum-one")]
pub evm_network: String,
/// EVM network for payment processing: arbitrum-one, arbitrum-sepolia,
/// or local (reads the devnet manifest's EVM config). Defaults to
/// arbitrum-one — except when a devnet manifest with EVM config is
/// loaded, where an explicit choice is required so an irreversible
/// on-chain payment never silently targets the wrong network.
#[arg(long)]
pub evm_network: Option<String>,

#[command(subcommand)]
pub command: Commands,
Expand Down
114 changes: 18 additions & 96 deletions ant-cli/src/commands/node/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::PathBuf;
use clap::Args;
use colored::Colorize;

use ant_core::node::binary::ProgressReporter;
use ant_core::node::binary::{NoopProgress, ProgressReporter};
use ant_core::node::daemon::client;
use ant_core::node::types::DaemonConfig;
use ant_core::node::types::{
Expand Down Expand Up @@ -103,8 +103,8 @@ impl AddArgs {
// Check if daemon is running; if so, POST to API; otherwise call directly
let config = DaemonConfig::default();
let result = match client::status(&config).await {
Ok(status) if status.running => self.add_via_daemon(&config, &opts).await?,
_ => self.add_directly(&config, &opts).await?,
Ok(status) if status.running => client::add_node(&config, &opts).await?,
_ => self.add_directly(&config, &opts, json_output).await?,
};

if json_output {
Expand Down Expand Up @@ -150,7 +150,11 @@ impl AddArgs {
}

fn to_add_node_opts(&self) -> anyhow::Result<AddNodeOpts> {
let node_port = self.parse_port_range(&self.node_port)?;
let node_port = self
.node_port
.as_deref()
.map(str::parse::<PortRange>)
.transpose()?;

let binary_source = if let Some(ref path) = self.path {
BinarySource::LocalPath(path.clone())
Expand All @@ -162,18 +166,7 @@ impl AddArgs {
BinarySource::Latest
};

let env_variables: Vec<(String, String)> = self
.env
.iter()
.map(|e| {
let parts: Vec<&str> = e.splitn(2, '=').collect();
if parts.len() == 2 {
Ok((parts[0].to_string(), parts[1].to_string()))
} else {
anyhow::bail!("Invalid env variable format: '{e}'. Expected KEY=VALUE")
}
})
.collect::<anyhow::Result<Vec<_>>>()?;
let env_variables = AddNodeOpts::parse_env_vars(&self.env)?;

Ok(AddNodeOpts {
count: self.count,
Expand All @@ -189,92 +182,21 @@ impl AddArgs {
})
}

fn parse_port_range(&self, input: &Option<String>) -> anyhow::Result<Option<PortRange>> {
match input {
None => Ok(None),
Some(s) => {
if let Some((start, end)) = s.split_once('-') {
let start: u16 = start
.parse()
.map_err(|_| anyhow::anyhow!("Invalid port range start: '{start}'"))?;
let end: u16 = end
.parse()
.map_err(|_| anyhow::anyhow!("Invalid port range end: '{end}'"))?;
if end < start {
anyhow::bail!("Port range end ({end}) must be >= start ({start})");
}
Ok(Some(PortRange::Range(start, end)))
} else {
let port: u16 = s
.parse()
.map_err(|_| anyhow::anyhow!("Invalid port: '{s}'"))?;
Ok(Some(PortRange::Single(port)))
}
}
}
}

async fn add_via_daemon(
&self,
config: &DaemonConfig,
opts: &AddNodeOpts,
) -> anyhow::Result<AddNodeResult> {
let info = client::info(config);
let api_base = info
.api_base
.ok_or_else(|| anyhow::anyhow!("Daemon is running but API base URL not available"))?;

let client = reqwest::Client::new();
let resp = client
.post(format!("{api_base}/nodes"))
.json(opts)
.send()
.await?;

if resp.status().is_success() {
Ok(resp.json().await?)
} else {
let body = resp.text().await?;
anyhow::bail!("Daemon returned error: {body}");
}
}

async fn add_directly(
&self,
config: &DaemonConfig,
opts: &AddNodeOpts,
json_output: bool,
) -> anyhow::Result<AddNodeResult> {
let progress = CliProgress;
// Suppress progress in JSON mode so stdout stays parseable.
let progress: Box<dyn ProgressReporter> = if json_output {
Box::new(NoopProgress)
} else {
Box::new(crate::progress::CliProgress)
};
let result =
ant_core::node::add_nodes(opts.clone(), &config.registry_path, &progress).await?;
ant_core::node::add_nodes(opts.clone(), &config.registry_path, progress.as_ref())
.await?;
Ok(result)
}
}

/// CLI progress reporter that prints to the terminal.
struct CliProgress;

impl ProgressReporter for CliProgress {
fn report_started(&self, message: &str) {
println!("{} {message}", "⟳".cyan());
}

fn report_progress(&self, bytes: u64, total: u64) {
if total > 0 {
let pct = (bytes as f64 / total as f64 * 100.0) as u32;
let bar_width = 30;
let filled = (pct as usize * bar_width) / 100;
let empty = bar_width - filled;
let bar = format!(
"{}{}",
"█".repeat(filled).cyan(),
"░".repeat(empty).dimmed()
);
print!("\r {} {bar} {pct:>3}%", "Downloading".dimmed());
}
}

fn report_complete(&self, message: &str) {
println!("\r{} {message}", "✓".green().bold());
}
}
19 changes: 1 addition & 18 deletions ant-cli/src/commands/node/reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl ResetArgs {
}

let result = if daemon_running {
self.reset_via_daemon(&config).await?
client::reset(&config).await?
} else {
self.reset_directly(&config)?
};
Expand Down Expand Up @@ -85,23 +85,6 @@ impl ResetArgs {
Ok(())
}

async fn reset_via_daemon(&self, config: &DaemonConfig) -> anyhow::Result<ResetResult> {
let info = client::info(config);
let api_base = info
.api_base
.ok_or_else(|| anyhow::anyhow!("Daemon is running but API base URL not available"))?;

let client = reqwest::Client::new();
let resp = client.post(format!("{api_base}/reset")).send().await?;

if resp.status().is_success() {
Ok(resp.json().await?)
} else {
let body = resp.text().await?;
anyhow::bail!("Daemon returned error: {body}");
}
}

fn reset_directly(&self, config: &DaemonConfig) -> anyhow::Result<ResetResult> {
let result = ant_core::node::reset(&config.registry_path)?;
Ok(result)
Expand Down
9 changes: 3 additions & 6 deletions ant-cli/src/commands/node/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,9 @@ impl StartArgs {
service_name: &str,
json_output: bool,
) -> anyhow::Result<()> {
// Look up node ID by service name via the registry
let registry = ant_core::node::registry::NodeRegistry::load(&config.registry_path)?;
let node = registry
.find_by_service_name(service_name)
.ok_or_else(|| anyhow::anyhow!("No node found with service name '{service_name}'"))?;
let node_id = node.id;
// Resolve the node ID through the daemon API — the daemon owns the
// registry, so the CLI must not read node_registry.json directly.
let node_id = client::resolve_node_id_by_name(config, service_name).await?;

let result = client::start_node(config, node_id).await?;

Expand Down
8 changes: 3 additions & 5 deletions ant-cli/src/commands/node/stop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,9 @@ impl StopArgs {
service_name: &str,
json_output: bool,
) -> anyhow::Result<()> {
let registry = ant_core::node::registry::NodeRegistry::load(&config.registry_path)?;
let node = registry
.find_by_service_name(service_name)
.ok_or_else(|| anyhow::anyhow!("No node found with service name '{service_name}'"))?;
let node_id = node.id;
// Resolve the node ID through the daemon API — the daemon owns the
// registry, so the CLI must not read node_registry.json directly.
let node_id = client::resolve_node_id_by_name(config, service_name).await?;

let result = client::stop_node(config, node_id).await?;

Expand Down
22 changes: 2 additions & 20 deletions ant-cli/src/commands/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,7 @@ use colored::Colorize;
use ant_core::node::binary::NoopProgress;
use ant_core::update;

/// Progress reporter that prints to the terminal.
struct CliUpdateProgress;

impl ant_core::node::binary::ProgressReporter for CliUpdateProgress {
fn report_started(&self, message: &str) {
eprintln!("{}", message.dimmed());
}

fn report_progress(&self, bytes: u64, total: u64) {
if total > 0 {
let pct = (bytes as f64 / total as f64 * 100.0) as u64;
eprint!("\r{}", format!(" Downloading... {pct}%").dimmed());
}
}

fn report_complete(&self, message: &str) {
eprintln!("\r{}", message.green());
}
}
use crate::progress::CliProgress;

#[derive(Args)]
pub struct UpdateArgs {
Expand Down Expand Up @@ -72,7 +54,7 @@ impl UpdateArgs {
let progress: Box<dyn ant_core::node::binary::ProgressReporter> = if json_output {
Box::new(NoopProgress)
} else {
Box::new(CliUpdateProgress)
Box::new(CliProgress)
};

let result = update::perform_update(&check, progress.as_ref()).await?;
Expand Down
Loading
Loading