nomoreide-cli 0.18.0

NoMoreIDE: an AI-native service workbench with a CLI, a TUI, a web dashboard and an MCP server.
Documentation
//! The `nomoreide` command, as a library.
//!
//! This is a library so that the `nomoreide` crate — the name people type
//! at `cargo install` — can be a real front door rather than a duplicate.
//! It has nothing to offer as an API beyond [`run`]; the modules below stay
//! private, and the binary in this crate is itself a three-line caller.
//!
//! The `nomoreide` binary's front door — the Rust half of `src/index.ts`.
//!
//! Dispatch order matters and mirrors the reference exactly. In particular
//! `start` is overloaded: with no service name it is the MCP server (the shape
//! an agent launches), and with one it is a service start. An agent config
//! that says `nomoreide start` has to keep working.

mod agents;
mod attach;
mod commands;
mod daemon_cli;
mod database;
mod flags;
mod git;
mod linear;
mod profile;
mod remote;
mod setup;
mod tui;

use std::process::ExitCode;

use nomoreide_daemon_client::{resolve_daemon_port, RuntimePaths};

/// Run the command line, returning the process exit code.
///
/// Async rather than blocking, and deliberately not wrapped in a runtime:
/// each binary supplies its own `#[tokio::main]`, so neither has to know
/// that the other exists.
pub async fn run() -> ExitCode {
    let args: Vec<String> = std::env::args().skip(1).collect();
    // No deprecation notice here. The reference prints one on an interactive
    // stderr telling the reader to install *this* binary; having installed it,
    // there is nothing left to say.
    let command = args.first().map(String::as_str).unwrap_or("mcp");
    let paths = RuntimePaths::default();
    let configured_port =
        resolve_daemon_port(std::env::var("NOMOREIDE_DAEMON_PORT").ok().as_deref());

    let code = match command {
        "setup" => setup::run(&args[1..]),
        // Bare `start` is the MCP server: it is what an agent spawns, and it
        // predates the service-runtime meaning of the word.
        "mcp" | "start" if args.len() <= 1 => match nomoreide_mcp::run_stdio().await {
            Ok(()) => 0,
            Err(error) => {
                eprintln!("nomoreide: MCP server failed: {error}");
                1
            }
        },
        "daemon" => {
            let rest = &args[1..];
            let port = daemon_cli::port_flag(rest).unwrap_or(configured_port);
            if rest.iter().any(|arg| !arg.starts_with("--")) {
                daemon_cli::run(rest, &paths, port).await
            } else {
                return run_foreground_daemon(port).await;
            }
        }
        "web" => web(&paths, configured_port).await,
        "tui" => match tui::run(&paths, configured_port).await {
            Ok(()) => 0,
            Err(error) => {
                if let Some(message) = error.message_text() {
                    eprintln!("{message}");
                }
                error.exit_code()
            }
        },
        // Not a user-facing command: the daemon spawns this inside a terminal
        // window it opened, handing it the socket and the one-shot token that
        // authorise the attachment. Named with a `__` prefix for that reason,
        // and absent from every usage string.
        // The shared terminal launcher runs inside either this CLI daemon or
        // the Tauri executable. Tauri historically used the flag spelling;
        // accept both spellings here so the daemon's current executable can
        // always attach the Terminal.app window it just opened.
        command if is_terminal_attach_command(command) => {
            let socket = args.get(1).map(String::as_str).unwrap_or("");
            let token = args.get(2).map(String::as_str).unwrap_or("");
            match nomoreide_core::external_terminal::run_attach(socket, token) {
                Ok(()) => 0,
                Err(error) => {
                    eprintln!("{error}");
                    1
                }
            }
        }
        _ => commands::run(&args, &paths, configured_port).await,
    };
    ExitCode::from(code)
}

fn is_terminal_attach_command(command: &str) -> bool {
    matches!(command, "__terminal-attach" | "--terminal-attach")
}

/// `nomoreide daemon` with no subcommand: be the machine-global daemon.
///
/// Split out because it never returns normally — everything else in `main`
/// produces an exit code, and this produces a process that lives until a
/// signal or `/api/daemon/shutdown`.
async fn run_foreground_daemon(port: u16) -> ExitCode {
    let options = nomoreide_daemon::DaemonOptions {
        port,
        ..Default::default()
    };
    match nomoreide_daemon::run(options).await {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("nomoreide: daemon failed: {error:#}");
            ExitCode::FAILURE
        }
    }
}

/// `nomoreide web [--port=N]` — make sure a daemon is up and say where it is.
///
/// Both lines go to **stderr**, including the URL. That looks wrong until you
/// remember what calls this: a shell function that opens the browser, and a
/// wrapper that wants the URL out of band from whatever the daemon then logs.
/// The reference puts both there, so both stay there.
async fn web(paths: &RuntimePaths, configured_port: u16) -> u8 {
    let port = std::env::args()
        .find_map(|arg| {
            arg.strip_prefix("--port=")
                .map(|value| resolve_daemon_port(Some(value)))
        })
        .unwrap_or(configured_port);
    match nomoreide_daemon_client::ensure_daemon(paths, port, env!("CARGO_PKG_VERSION")).await {
        Ok(daemon) => {
            if let Some(warning) = &daemon.version_warning {
                eprintln!("{warning}");
            }
            // Said out loud because it costs a second of startup. Silence would
            // be a pause with no cause, which reads as the tool being slow.
            if daemon.status == nomoreide_daemon_client::EnsureStatus::Upgraded {
                eprintln!(
                    "NoMoreIDE daemon updated to v{}.",
                    env!("CARGO_PKG_VERSION")
                );
            }
            eprintln!(
                "NoMoreIDE web UI: http://127.0.0.1:{}",
                daemon.endpoint.port()
            );
            0
        }
        Err(error) => {
            eprintln!("{error}");
            1
        }
    }
}

#[cfg(test)]
mod tests {
    use super::is_terminal_attach_command;

    #[test]
    fn terminal_attachment_accepts_the_shared_launchers_flag() {
        assert!(is_terminal_attach_command("--terminal-attach"));
        assert!(is_terminal_attach_command("__terminal-attach"));
        assert!(!is_terminal_attach_command("terminal-attach"));
    }
}