fdev 0.3.224

Freenet development tool
use std::{
    fmt::Display,
    net::{IpAddr, Ipv4Addr},
    path::PathBuf,
};

use crate::{commands::PutType, wasm_runtime::ExecutorConfig};
use clap::ValueEnum;
use freenet::{config::ConfigPathsArgs, dev_tool::OperationMode};
use semver::Version;

#[derive(clap::Parser, Clone)]
#[clap(name = "Freenet Development Tool")]
#[clap(author = "The Freenet Project Inc.")]
#[clap(version = env!("CARGO_PKG_VERSION"))]
pub struct Config {
    #[clap(subcommand)]
    pub sub_command: SubCommand,
    #[clap(flatten)]
    pub additional: BaseConfig,
}

#[derive(clap::Parser, Clone)]
pub struct BaseConfig {
    #[clap(flatten)]
    pub(crate) paths: ConfigPathsArgs,
    /// Node operation mode.
    #[arg(value_enum, default_value_t=OperationMode::Local, env = "MODE")]
    pub mode: OperationMode,
    /// The port of the running local freenet node websocket API.
    #[arg(short, long, default_value = "7509", env = "WS_API_PORT")]
    pub(crate) port: u16,
    /// The ip address of freenet node to publish the contract to. If the node is running in local mode,
    /// The default value is `127.0.0.1`.
    #[arg(short, long, default_value_t = IpAddr::V4(Ipv4Addr::LOCALHOST))]
    pub(crate) address: IpAddr,
    /// Full WebSocket URL to connect to (e.g. ws://host:port/secret/v1/contract/command?encodingProtocol=native).
    /// When provided, --address and --port must not be specified.
    #[arg(long, env = "FREENET_NODE_URL", conflicts_with_all = ["address", "port"])]
    pub(crate) node_url: Option<String>,
}

#[derive(clap::Subcommand, Clone)]
pub enum SubCommand {
    Init(InitPackageConfig),
    New(NewPackageConfig),
    Build(BuildToolConfig),
    Inspect(crate::inspect::InspectConfig),
    Publish(PutConfig),
    /// Query the local node for information. Currently only shows open connections.
    Query {},
    /// Get detailed node diagnostics including network state, subscriptions, and metrics.
    Diagnostics {
        /// Contract keys to include in diagnostics (Base58 encoded)
        #[arg(long = "contract")]
        contract_keys: Vec<String>,
    },
    WasmRuntime(ExecutorConfig),
    Execute(RunCliConfig),
    Test(crate::testing::TestConfig),
    NetworkMetricsServer(crate::network_metrics_server::ServerConfig),
    /// Get the contract ID without publishing
    GetContractId(crate::commands::GetContractIdConfig),
    /// Verify network state consistency from telemetry event logs.
    ///
    /// Reads event log (AOF) files from one or more nodes, linearizes the
    /// state transitions, and detects anomalies that indicate consistency
    /// failures (missing broadcasts, unapplied updates, state divergence).
    VerifyState(crate::verify_state::VerifyStateConfig),
    /// Publish and manage static websites on Freenet.
    ///
    /// Use `init` to generate a signing keypair, `publish` to deploy a website,
    /// and `update` to push new content to an existing website.
    Website {
        #[clap(subcommand)]
        command: crate::website::WebsiteCommand,
    },
}

impl SubCommand {
    pub fn is_child(&self) -> bool {
        if let SubCommand::Test(config) = self {
            if let crate::testing::TestMode::Network(config) = &config.command {
                return matches!(config.mode, crate::testing::network::Process::Peer);
            }
        }
        false
    }
}

/// Core CLI tool for interacting with the Freenet local node.
///
/// This tool allows the execution of commands against the local node
/// and is intended to be used for development and automated workflows.
#[derive(clap::Parser, Clone)]
pub struct RunCliConfig {
    /// Command to execute.
    #[clap(subcommand)]
    pub command: NodeCommand,
}

#[derive(clap::Subcommand, Clone)]
pub enum NodeCommand {
    Put(PutConfig),
    /// Get the current state of a contract.
    Get(GetConfig),
    Update(UpdateConfig),
    /// Subscribe to a contract and stream update notifications.
    Subscribe(SubscribeConfig),
    GetContractId(crate::commands::GetContractIdConfig),
}

/// Retrieves the current state of a contract from the network.
#[derive(clap::Parser, Clone)]
pub struct GetConfig {
    /// Contract key in Base58 format.
    pub(crate) key: String,
    /// Also return the contract code in the response.
    #[arg(long)]
    pub(crate) return_code: bool,
    /// Write the state to a file instead of stdout.
    #[arg(short, long)]
    pub(crate) output: Option<PathBuf>,
}

/// Subscribes to a contract and streams update notifications until interrupted.
#[derive(clap::Parser, Clone)]
pub struct SubscribeConfig {
    /// Contract key in Base58 format.
    pub(crate) key: String,
    /// Write each update to a file (overwritten on each update) instead of stdout.
    #[arg(short, long)]
    pub(crate) output: Option<PathBuf>,
}

/// Updates a contract in the network.
#[derive(clap::Parser, Clone)]
pub struct UpdateConfig {
    /// Contract id of the contract being updated in Base58 format.
    pub(crate) key: String,
    /// The ip address of freenet node to update the contract to. If the node is running in local mode,
    /// The default value is `127.0.0.1`
    #[arg(short, long, default_value_t = IpAddr::V4(Ipv4Addr::LOCALHOST))]
    pub(crate) address: IpAddr,
    /// Path to the file being pushed to the contract. Interpreted as a
    /// `UpdateData::Delta` by default, or as a full-state replacement
    /// (`UpdateData::State`) when `--as-state` is set.
    pub(crate) delta: PathBuf,
    /// Send the file contents as a full state replacement (`UpdateData::State`)
    /// instead of the default delta (`UpdateData::Delta`). Use this when the
    /// contract's `update_state` only accepts full-state replacements.
    #[arg(long)]
    pub(crate) as_state: bool,
}

/// Publishes a new contract or delegate to the network.
// todo: make some of this options exclusive depending on the value of `package_type`
#[derive(clap::Parser, Clone)]
pub struct PutConfig {
    /// A path to the compiled WASM code file. This must be a valid packaged contract or component,
    /// (built using the `fdev` tool). Not an arbitrary WASM file.
    #[arg(long)]
    pub(crate) code: PathBuf,

    /// A path to the file parameters for the contract/delegate. If not specified, will be published
    /// with empty parameters.
    #[arg(long)]
    pub(crate) parameters: Option<PathBuf>,
    /// Type of put to perform.
    #[clap(subcommand)]
    pub(crate) package_type: PutType,

    /// Flag that indicates if the node should subscribe to the contract.
    #[arg(long)]
    pub(crate) subscribe: bool,
}

/// Builds and packages a contract or delegate.
///
/// This tool will build the WASM contract or delegate and publish it to the network.
#[derive(clap::Parser, Clone, Debug)]
pub struct BuildToolConfig {
    /// Compile the contract or delegate with specific features.
    #[arg(long)]
    pub(crate) features: Option<String>,

    /// Compile the contract or delegate with a specific API version.
    #[arg(long, value_parser = parse_version, default_value_t=Version::new(0, 0, 1))]
    pub(crate) version: Version,

    /// Output object type.
    #[arg(long, value_enum, default_value_t=PackageType::default())]
    pub(crate) package_type: PackageType,

    /// Compile in debug mode instead of release. Warning: Debug mode produces WASM files that are
    /// typically 40-50x larger than release mode (e.g., 10MB vs 200KB), which may exceed WebSocket
    /// message size limits and cause deployment failures. Use only for development and debugging.
    #[arg(long)]
    pub(crate) debug: bool,
}

#[derive(Default, Debug, Clone, Copy, ValueEnum)]
pub(crate) enum PackageType {
    #[default]
    Contract,
    Delegate,
}

impl PackageType {
    pub fn feature(&self) -> &'static str {
        match self {
            PackageType::Contract => "freenet-main-contract",
            PackageType::Delegate => "freenet-main-delegate",
        }
    }
}

impl Display for PackageType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PackageType::Contract => write!(f, "contract"),
            PackageType::Delegate => write!(f, "delegate"),
        }
    }
}

impl Default for BuildToolConfig {
    fn default() -> Self {
        Self {
            features: None,
            version: Version::new(0, 0, 1),
            package_type: PackageType::default(),
            debug: false,
        }
    }
}

fn parse_version(src: &str) -> Result<Version, String> {
    Version::parse(src).map_err(|e| e.to_string())
}

/// Initialize a new Freenet contract and/or app in the CWD by default.
#[derive(clap::Parser, Clone)]
pub struct InitPackageConfig {
    #[arg(id = "type", value_enum)]
    pub(crate) kind: ContractKind,
    #[arg(short, long, value_name = "PATH", value_hint = clap::ValueHint::DirPath)]
    pub(crate) path: Option<PathBuf>,
}

impl From<NewPackageConfig> for InitPackageConfig {
    fn from(NewPackageConfig { kind, path }: NewPackageConfig) -> Self {
        Self {
            kind,
            path: path.into(),
        }
    }
}

/// Create a new Freenet contract and/or app.
#[derive(clap::Parser, Clone)]
pub struct NewPackageConfig {
    #[arg(id = "type", value_enum)]
    pub(crate) kind: ContractKind,
    #[arg(value_name = "PATH", value_hint = clap::ValueHint::DirPath)]
    pub(crate) path: PathBuf,
}

#[derive(clap::ValueEnum, Clone)]
pub(crate) enum ContractKind {
    /// A web app container contract.
    WebApp,
    /// An standard contract.
    Contract,
}

#[cfg(test)]
mod tests {
    use clap::Parser as _;

    use super::Config;

    /// Regression test for #4088: `fdev publish --release` used to bail
    /// unconditionally with "Cannot publish contracts in the network yet".
    /// After the fix the `--release` flag no longer exists on `publish`;
    /// passing it should be a CLI parse error, not a silent rejection at
    /// runtime.
    #[test]
    fn publish_does_not_accept_release_flag() {
        let result = Config::try_parse_from([
            "fdev",
            "publish",
            "--code",
            "contract.wasm",
            "--release", // must not exist
            "contract",
        ]);
        let err_msg = result
            .as_ref()
            .map(|_| String::new())
            .unwrap_or_else(|e| e.to_string());
        assert!(
            result.is_err(),
            "fdev publish --release should be a parse error after #4088 removal, but parsed OK"
        );
        assert!(
            err_msg.contains("--release") || err_msg.contains("release"),
            "error message should mention the unknown flag, got: {err_msg}"
        );
    }

    /// Regression test for #4088: `fdev execute update <KEY> <DELTA> <RELEASE>`
    /// used to accept a positional `release: bool` argument that bailed
    /// unconditionally when true. After the fix the positional arg is gone.
    #[test]
    fn execute_update_does_not_accept_positional_release_bool() {
        // Passing "true" as a third positional arg should now be rejected
        // because UpdateConfig only accepts key + delta.
        let result = Config::try_parse_from([
            "fdev",
            "execute",
            "update",
            "SomeBase58ContractKey",
            "/tmp/delta.bin",
            "true", // was the `release: bool` positional; must not exist
        ]);
        assert!(
            result.is_err(),
            "fdev execute update with a third positional arg should fail after #4088 removal"
        );
    }
}