node-app-build 6.12.0

Mini app developer CLI: scaffold, validate, package node-app-* Debian packages
//! `node-app harness` — agent-drivable live full-stack (regtest bitcoind +
//! alice/bob Lightning nodes) with channel/payment probes.
//!
//! Layered on `node-app dev --agent`: bring-up calls `commands::dev::run`
//! in-process; this module only adds the bitcoind lifecycle, the probe
//! surface, and the `harness-state.json` index.

use anyhow::Result;
use clap::{Subcommand, ValueEnum};

pub mod bitcoind;
pub mod ports;
pub mod probes;
pub mod snippet;
pub mod state;

#[derive(Debug, Clone, Copy, ValueEnum, Default)]
pub enum BitcoindMode {
    /// Run bitcoind in a Docker container (self-contained; default).
    #[default]
    Docker,
    /// Attach to an already-running bitcoind/Polar on 127.0.0.1:18443.
    External,
}

#[derive(Subcommand, Debug)]
pub enum HarnessAction {
    /// Bring up regtest bitcoind + alice/bob, onboard + peer-seed, leave running.
    Up {
        /// Path to the monorepo checkout (defaults to current dir).
        #[arg(long, default_value = ".")]
        monorepo_path: std::path::PathBuf,
        /// bitcoind source: docker (default) or external.
        #[arg(long, value_enum, default_value_t = BitcoindMode::Docker)]
        bitcoind: BitcoindMode,
        /// Also fund alice and open a usable alice→bob channel.
        #[arg(long)]
        with_channel: bool,
        /// Wipe existing harness/instance data before bringing up.
        #[arg(long)]
        clean: bool,
        /// Serve the client-node PWA for a selected harness instance.
        /// Supplying the flag without a value selects alice.
        #[arg(
            long,
            value_name = "INSTANCE",
            num_args = 0..=1,
            default_missing_value = "alice",
            value_parser = ["alice", "bob"]
        )]
        client_node: Option<String>,
        /// Automatically approve the selected client-node browser through the real
        /// device lifecycle.
        #[arg(long, requires = "client_node")]
        operation_mode: bool,
    },
    /// Health + peers + channels + balances for both nodes (JSON).
    Status,
    /// Mine N regtest blocks.
    Mine { blocks: u32 },
    /// Fund a node on-chain: send BTC + mine confirmations.
    Fund { node: String, btc: f64 },
    /// Connect + open a channel and wait until usable.
    ChannelOpen { from: String, to: String, sats: u64 },
    /// Invoice on `to`, pay from `from`, return settlement (JSON).
    Pay { from: String, to: String, sats: u64 },
    /// Drive a cross-node L402 paid call: `from` calls `route` on its own daemon
    /// which proxies to `to` via L402HttpClient.  Prints settlement JSON.
    L402 {
        from: String,
        to: String,
        route: String,
        /// JSON payload body (pass as a quoted JSON string on the CLI).
        payload: String,
    },
    /// Invoke any capability on a node (JSON payload).
    Invoke { node: String, capability: String, payload: String },
    /// Print an in-page JS snippet that pairs a real browser through the
    /// genuine client-device lifecycle, carrying the instance's owner bearer
    /// token so the node self-approves. Paste the output into devtools at
    /// the instance's own origin. Prints no diagnostics to stdout — only the
    /// snippet (or, with `--json`, a JSON envelope around it).
    PairBrowser {
        node: String,
        /// Emit `{ base_url, owner_token, snippet, device_name }` instead of
        /// the bare snippet.
        #[arg(long)]
        json: bool,
    },
    /// Tail a node's captured daemon log.
    Logs { node: String, #[arg(long)] tail: bool },
    /// Stop daemons (+ bitcoind container). --clean also wipes data dirs.
    Down { #[arg(long)] clean: bool },
}

pub fn run(action: HarnessAction) -> Result<()> {
    match action {
        HarnessAction::Up {
            monorepo_path,
            bitcoind,
            with_channel,
            clean,
            client_node,
            operation_mode,
        } => probes::up(
            monorepo_path,
            bitcoind,
            with_channel,
            clean,
            client_node,
            operation_mode,
        ),
        HarnessAction::Status => probes::status(),
        HarnessAction::Mine { blocks } => probes::mine(blocks),
        HarnessAction::Fund { node, btc } => probes::fund(&node, btc),
        HarnessAction::ChannelOpen { from, to, sats } => probes::channel_open(&from, &to, sats),
        HarnessAction::Pay { from, to, sats } => probes::pay(&from, &to, sats),
        HarnessAction::L402 { from, to, route, payload } => {
            probes::l402(&from, &to, &route, &payload)
        }
        HarnessAction::Invoke { node, capability, payload } => {
            probes::invoke(&node, &capability, &payload)
        }
        HarnessAction::PairBrowser { node, json } => probes::pair_browser(&node, json),
        HarnessAction::Logs { node, tail } => probes::logs(&node, tail),
        HarnessAction::Down { clean } => probes::down(clean),
    }
}