1use 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 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 #[arg(value_name = "PROJECT")]
50 pub project: Option<String>,
51
52 #[arg(long)]
56 pub new: bool,
57
58 #[arg(long, value_name = "SHELL", hide = true)]
61 pub generate_completion: Option<clap_complete::Shell>,
62
63 #[arg(long, value_enum)]
66 pub diagnostics_preset: Option<DiagnosticsPreset>,
67
68 #[arg(long, value_name = "PATH")]
74 pub log_file: Option<std::path::PathBuf>,
75
76 #[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 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 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}