Skip to main content

forge_server/
cli.rs

1//! The binary's own arguments, shared by every caller of [`crate::boot`].
2//!
3//! Parsed once by the `forge` binary: the boot, the logging runtime and
4//! the completion generator all read the same definition rather than a
5//! second, drifted copy.
6
7use clap::{Parser, ValueEnum};
8
9#[derive(Clone, Debug, ValueEnum, PartialEq, Eq)]
10pub enum DiagnosticsPreset {
11    Runtime,
12    Session,
13    Bridge,
14    Full,
15}
16
17impl DiagnosticsPreset {
18    /// A preset's own directives. The resolver composes them with the
19    /// default set rather than replacing it, so a target that only the
20    /// defaults name stays readable under every preset.
21    ///
22    /// The targets named are the ones the surviving binaries emit; the
23    /// old render/input/paste family retired with the terminal crate.
24    pub fn filter_directives(&self) -> &'static str {
25        match self {
26            Self::Runtime => {
27                "info,bridge.lifecycle=debug,bridge.protocol=debug,forge_server=debug,forge_workspace::output_tail=debug,app.permission=debug,forge_workspace::dictate=debug,forge_dictate=debug"
28            }
29            Self::Session => {
30                "info,bridge.lifecycle=debug,forge_server=debug,forge_workspace::output_tail=debug,app.permission=debug,forge_workspace::dictate=debug,forge_dictate=debug"
31            }
32            Self::Bridge => {
33                "info,bridge.lifecycle=debug,bridge.protocol=debug,bridge.sdk=debug,bridge.permission=debug,bridge.mcp=debug"
34            }
35            Self::Full => {
36                "info,bridge.lifecycle=debug,bridge.protocol=debug,bridge.sdk=debug,bridge.permission=debug,bridge.mcp=debug,forge_server=debug,forge_workspace::output_tail=debug,app.permission=debug,app.input=debug,forge_workspace::dictate=debug,forge_dictate=debug"
37            }
38        }
39    }
40}
41
42#[derive(Parser, Debug)]
43#[command(name = "forge", about = "The forge server for Claude Code", version)]
44pub struct Cli {
45    /// Project to start, matching an `[[orgs.projects]]` `name` in
46    /// forge.toml exactly. When omitted, every project whose lead was left
47    /// open still spawns in the background and none is singled out; naming
48    /// one starts it whether it was open or closed.
49    #[arg(value_name = "PROJECT")]
50    pub project: Option<String>,
51
52    /// Start every boot session fresh: the seats the boot reopens and their
53    /// workers spawn as new sessions instead of resuming. Only affects the
54    /// startup wave - clicking a sleeping project later still resumes.
55    #[arg(long)]
56    pub new: bool,
57
58    /// Generate a shell completion script and print to stdout, then
59    /// exit. Hidden from --help; called by `scripts/install.sh`.
60    #[arg(long, value_name = "SHELL", hide = true)]
61    pub generate_completion: Option<clap_complete::Shell>,
62
63    /// Named diagnostics preset for common logging workflows.
64    /// Ignored when `--log-filter` is provided explicitly.
65    #[arg(long, value_enum)]
66    pub diagnostics_preset: Option<DiagnosticsPreset>,
67
68    /// Write tracing diagnostics to a file.
69    ///
70    /// When omitted but logging is otherwise enabled via
71    /// `--diagnostics-preset`, `--log-filter`, or `RUST_LOG`, a
72    /// default log path is used.
73    #[arg(long, value_name = "PATH")]
74    pub log_file: Option<std::path::PathBuf>,
75
76    /// Tracing filter directives (example: `info,bridge.sdk=trace`).
77    /// Overrides `--diagnostics-preset` and falls back to `RUST_LOG` when omitted.
78    #[arg(long, value_name = "FILTER")]
79    pub log_filter: Option<String>,
80}
81
82#[cfg(test)]
83mod tests {
84    use super::Cli;
85    use clap::{CommandFactory, Parser};
86
87    #[test]
88    fn cli_parses_bare_forge_with_default_log_flags() {
89        let cli = Cli::try_parse_from(["forge"]).expect("parse");
90        assert!(cli.project.is_none());
91        assert!(cli.generate_completion.is_none());
92        assert!(cli.diagnostics_preset.is_none());
93        assert!(cli.log_file.is_none());
94        assert!(cli.log_filter.is_none());
95    }
96
97    #[test]
98    fn cli_rejects_legacy_resume_flag() {
99        assert!(Cli::try_parse_from(["forge", "--resume", "abc-123"]).is_err());
100    }
101
102    #[test]
103    fn new_flag_parses() {
104        let cli = Cli::try_parse_from(["forge", "--new"]).expect("parse");
105        assert!(cli.new);
106    }
107
108    #[test]
109    fn new_flag_defaults_false() {
110        let cli = Cli::try_parse_from(["forge"]).expect("parse");
111        assert!(!cli.new);
112    }
113
114    #[test]
115    fn version_flag_exits_with_display_version() {
116        // clap handles --version as print-and-exit inside parse(), so
117        // it surfaces as an Err with kind DisplayVersion (same shape as
118        // --help / bad args) and never returns a parsed Cli.
119        let err = Cli::try_parse_from(["forge", "--version"]).expect_err("--version exits");
120        assert_eq!(err.kind(), clap::error::ErrorKind::DisplayVersion);
121    }
122
123    #[test]
124    fn cli_accepts_optional_positional_project() {
125        let cli = Cli::try_parse_from(["forge", "dotfiles"]).expect("parse");
126        assert_eq!(cli.project.as_deref(), Some("dotfiles"));
127    }
128
129    #[test]
130    fn cli_generate_completion_zsh_parses() {
131        let cli = Cli::try_parse_from(["forge", "--generate-completion", "zsh"]).expect("parse");
132        assert!(matches!(cli.generate_completion, Some(clap_complete::Shell::Zsh)));
133    }
134
135    #[test]
136    fn cli_help_does_not_surface_generate_completion() {
137        // Render --help and confirm the hidden flag isn't there.
138        // (Smoke test - clap's `hide = true` is the source of truth.)
139        let mut cmd = Cli::command();
140        let mut out = Vec::new();
141        cmd.write_help(&mut out).expect("write_help");
142        let help_text = String::from_utf8(out).expect("utf8");
143        assert!(
144            !help_text.contains("generate-completion"),
145            "hidden flag should not appear in --help"
146        );
147    }
148}