Skip to main content

nomoreide_cli/
lib.rs

1//! The `nomoreide` command, as a library.
2//!
3//! This is a library so that the `nomoreide` crate — the name people type
4//! at `cargo install` — can be a real front door rather than a duplicate.
5//! It has nothing to offer as an API beyond [`run`]; the modules below stay
6//! private, and the binary in this crate is itself a three-line caller.
7//!
8//! The `nomoreide` binary's front door — the Rust half of `src/index.ts`.
9//!
10//! Dispatch order matters and mirrors the reference exactly. In particular
11//! `start` is overloaded: with no service name it is the MCP server (the shape
12//! an agent launches), and with one it is a service start. An agent config
13//! that says `nomoreide start` has to keep working.
14
15mod agents;
16mod commands;
17mod daemon_cli;
18mod database;
19mod flags;
20mod git;
21mod profile;
22mod setup;
23mod tui;
24
25use std::process::ExitCode;
26
27use nomoreide_daemon_client::{resolve_daemon_port, RuntimePaths};
28
29/// Run the command line, returning the process exit code.
30///
31/// Async rather than blocking, and deliberately not wrapped in a runtime:
32/// each binary supplies its own `#[tokio::main]`, so neither has to know
33/// that the other exists.
34pub async fn run() -> ExitCode {
35    let args: Vec<String> = std::env::args().skip(1).collect();
36    // No deprecation notice here. The reference prints one on an interactive
37    // stderr telling the reader to install *this* binary; having installed it,
38    // there is nothing left to say.
39    let command = args.first().map(String::as_str).unwrap_or("mcp");
40    let paths = RuntimePaths::default();
41    let configured_port =
42        resolve_daemon_port(std::env::var("NOMOREIDE_DAEMON_PORT").ok().as_deref());
43
44    let code = match command {
45        "setup" => setup::run(&args[1..]),
46        // Bare `start` is the MCP server: it is what an agent spawns, and it
47        // predates the service-runtime meaning of the word.
48        "mcp" | "start" if args.len() <= 1 => match nomoreide_mcp::run_stdio().await {
49            Ok(()) => 0,
50            Err(error) => {
51                eprintln!("nomoreide: MCP server failed: {error}");
52                1
53            }
54        },
55        "daemon" => {
56            let rest = &args[1..];
57            let port = daemon_cli::port_flag(rest).unwrap_or(configured_port);
58            if rest.iter().any(|arg| !arg.starts_with("--")) {
59                daemon_cli::run(rest, &paths, port).await
60            } else {
61                return run_foreground_daemon(port).await;
62            }
63        }
64        "web" => web(&paths, configured_port).await,
65        "tui" => match tui::run(&paths, configured_port).await {
66            Ok(()) => 0,
67            Err(error) => {
68                if let Some(message) = error.message_text() {
69                    eprintln!("{message}");
70                }
71                error.exit_code()
72            }
73        },
74        // Not a user-facing command: the daemon spawns this inside a terminal
75        // window it opened, handing it the socket and the one-shot token that
76        // authorise the attachment. Named with a `__` prefix for that reason,
77        // and absent from every usage string.
78        "__terminal-attach" => {
79            let socket = args.get(1).map(String::as_str).unwrap_or("");
80            let token = args.get(2).map(String::as_str).unwrap_or("");
81            match nomoreide_core::external_terminal::run_attach(socket, token) {
82                Ok(()) => 0,
83                Err(error) => {
84                    eprintln!("{error}");
85                    1
86                }
87            }
88        }
89        _ => commands::run(&args, &paths, configured_port).await,
90    };
91    ExitCode::from(code)
92}
93
94/// `nomoreide daemon` with no subcommand: be the machine-global daemon.
95///
96/// Split out because it never returns normally — everything else in `main`
97/// produces an exit code, and this produces a process that lives until a
98/// signal or `/api/daemon/shutdown`.
99async fn run_foreground_daemon(port: u16) -> ExitCode {
100    let options = nomoreide_daemon::DaemonOptions {
101        port,
102        ..Default::default()
103    };
104    match nomoreide_daemon::run(options).await {
105        Ok(()) => ExitCode::SUCCESS,
106        Err(error) => {
107            eprintln!("nomoreide: daemon failed: {error:#}");
108            ExitCode::FAILURE
109        }
110    }
111}
112
113/// `nomoreide web [--port=N]` — make sure a daemon is up and say where it is.
114///
115/// Both lines go to **stderr**, including the URL. That looks wrong until you
116/// remember what calls this: a shell function that opens the browser, and a
117/// wrapper that wants the URL out of band from whatever the daemon then logs.
118/// The reference puts both there, so both stay there.
119async fn web(paths: &RuntimePaths, configured_port: u16) -> u8 {
120    let port = std::env::args()
121        .find_map(|arg| {
122            arg.strip_prefix("--port=")
123                .map(|value| resolve_daemon_port(Some(value)))
124        })
125        .unwrap_or(configured_port);
126    match nomoreide_daemon_client::ensure_daemon(paths, port, env!("CARGO_PKG_VERSION")).await {
127        Ok(daemon) => {
128            if let Some(warning) = &daemon.version_warning {
129                eprintln!("{warning}");
130            }
131            eprintln!(
132                "NoMoreIDE web UI: http://127.0.0.1:{}",
133                daemon.endpoint.port()
134            );
135            0
136        }
137        Err(error) => {
138            eprintln!("{error}");
139            1
140        }
141    }
142}