diff --git a/src/cli/autocorrect.rs b/src/cli/autocorrect.rs index c6a9c52..c2c27c3 100644 --- a/src/cli/autocorrect.rs +++ b/src/cli/autocorrect.rs @@ -193,6 +193,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[ "--gitlab-url", "--token-env-var", "--projects", + "--default-project", ], ), ("generate-docs", &["--full", "--project"]), diff --git a/src/cli/commands/init.rs b/src/cli/commands/init.rs index fa53806..81daf26 100644 --- a/src/cli/commands/init.rs +++ b/src/cli/commands/init.rs @@ -10,6 +10,7 @@ pub struct InitInputs { pub gitlab_url: String, pub token_env_var: String, pub project_paths: Vec, + pub default_project: Option, } pub struct InitOptions { @@ -23,6 +24,7 @@ pub struct InitResult { pub data_dir: String, pub user: UserInfo, pub projects: Vec, + pub default_project: Option, } pub struct UserInfo { @@ -104,6 +106,20 @@ pub async fn run_init(inputs: InitInputs, options: InitOptions) -> Result Result Result, + + /// Default project path (used when -p is omitted) + #[arg(long)] + default_project: Option, }, #[command(hide = true)] diff --git a/src/main.rs b/src/main.rs index 262c44c..5c4fa45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -205,6 +205,7 @@ async fn main() { gitlab_url, token_env_var, projects, + default_project, }) => { handle_init( cli.config.as_deref(), @@ -214,6 +215,7 @@ async fn main() { gitlab_url, token_env_var, projects, + default_project, ) .await } @@ -678,13 +680,14 @@ fn handle_issues( ) -> Result<(), Box> { let start = std::time::Instant::now(); let config = Config::load(config_override)?; + let project = config.effective_project(args.project.as_deref()); let asc = args.asc && !args.no_asc; let has_due = args.has_due && !args.no_has_due; let open = args.open && !args.no_open; let order = if asc { "asc" } else { "desc" }; if let Some(iid) = args.iid { - let result = run_show_issue(&config, iid, args.project.as_deref())?; + let result = run_show_issue(&config, iid, project)?; if robot_mode { print_show_issue_json(&result, start.elapsed().as_millis() as u64); } else { @@ -694,7 +697,7 @@ fn handle_issues( let state_normalized = args.state.as_deref().map(str::to_lowercase); let filters = ListFilters { limit: args.limit, - project: args.project.as_deref(), + project, state: state_normalized.as_deref(), author: args.author.as_deref(), assignee: args.assignee.as_deref(), @@ -733,12 +736,13 @@ fn handle_mrs( ) -> Result<(), Box> { let start = std::time::Instant::now(); let config = Config::load(config_override)?; + let project = config.effective_project(args.project.as_deref()); let asc = args.asc && !args.no_asc; let open = args.open && !args.no_open; let order = if asc { "asc" } else { "desc" }; if let Some(iid) = args.iid { - let result = run_show_mr(&config, iid, args.project.as_deref())?; + let result = run_show_mr(&config, iid, project)?; if robot_mode { print_show_mr_json(&result, start.elapsed().as_millis() as u64); } else { @@ -748,7 +752,7 @@ fn handle_mrs( let state_normalized = args.state.as_deref().map(str::to_lowercase); let filters = MrListFilters { limit: args.limit, - project: args.project.as_deref(), + project, state: state_normalized.as_deref(), author: args.author.as_deref(), assignee: args.assignee.as_deref(), @@ -791,6 +795,7 @@ async fn handle_ingest( let start = std::time::Instant::now(); let dry_run = args.dry_run && !args.no_dry_run; let config = Config::load(config_override)?; + let project = config.effective_project(args.project.as_deref()); let force = args.force && !args.no_force; let full = args.full && !args.no_full; @@ -799,8 +804,7 @@ async fn handle_ingest( if dry_run { match args.entity.as_deref() { Some(resource_type) => { - let preview = - run_ingest_dry_run(&config, resource_type, args.project.as_deref(), full)?; + let preview = run_ingest_dry_run(&config, resource_type, project, full)?; if robot_mode { print_dry_run_preview_json(&preview); } else { @@ -808,10 +812,8 @@ async fn handle_ingest( } } None => { - let issues_preview = - run_ingest_dry_run(&config, "issues", args.project.as_deref(), full)?; - let mrs_preview = - run_ingest_dry_run(&config, "mrs", args.project.as_deref(), full)?; + let issues_preview = run_ingest_dry_run(&config, "issues", project, full)?; + let mrs_preview = run_ingest_dry_run(&config, "mrs", project, full)?; if robot_mode { print_combined_dry_run_json(&issues_preview, &mrs_preview); } else { @@ -854,7 +856,7 @@ async fn handle_ingest( let result = run_ingest( &config, resource_type, - args.project.as_deref(), + project, force, full, false, @@ -880,28 +882,12 @@ async fn handle_ingest( } let issues_result = run_ingest( - &config, - "issues", - args.project.as_deref(), - force, - full, - false, - display, - None, - &signal, + &config, "issues", project, force, full, false, display, None, &signal, ) .await?; let mrs_result = run_ingest( - &config, - "mrs", - args.project.as_deref(), - force, - full, - false, - display, - None, - &signal, + &config, "mrs", project, force, full, false, display, None, &signal, ) .await?; @@ -1104,6 +1090,8 @@ struct InitOutputData { data_dir: String, user: InitOutputUser, projects: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + default_project: Option, } #[derive(Serialize)] @@ -1136,6 +1124,7 @@ fn print_init_json(result: &InitResult) { name: p.name.clone(), }) .collect(), + default_project: result.default_project.clone(), }, }; println!( @@ -1146,6 +1135,7 @@ fn print_init_json(result: &InitResult) { ); } +#[allow(clippy::too_many_arguments)] async fn handle_init( config_override: Option<&str>, force: bool, @@ -1154,6 +1144,7 @@ async fn handle_init( gitlab_url_flag: Option, token_env_var_flag: Option, projects_flag: Option, + default_project_flag: Option, ) -> Result<(), Box> { if robot_mode { let missing: Vec<&str> = [ @@ -1191,6 +1182,7 @@ async fn handle_init( gitlab_url: gitlab_url_flag.unwrap(), token_env_var: token_env_var_flag.unwrap(), project_paths, + default_project: default_project_flag.clone(), }, InitOptions { config_path: config_override.map(String::from), @@ -1285,6 +1277,29 @@ async fn handle_init( .collect() }; + // Resolve default project: CLI flag, interactive prompt, or None + let default_project = if default_project_flag.is_some() { + default_project_flag + } else if project_paths.len() > 1 && !non_interactive { + let set_default = Confirm::new() + .with_prompt("Set a default project? (used when -p is omitted)") + .default(true) + .interact()?; + + if set_default { + let selection = dialoguer::Select::new() + .with_prompt("Default project") + .items(&project_paths) + .default(0) + .interact()?; + Some(project_paths[selection].clone()) + } else { + None + } + } else { + None + }; + println!("{}", style("\nValidating configuration...").blue()); let result = run_init( @@ -1292,6 +1307,7 @@ async fn handle_init( gitlab_url, token_env_var, project_paths, + default_project, }, InitOptions { config_path: config_override.map(String::from), @@ -1317,6 +1333,10 @@ async fn handle_init( ); } + if let Some(ref dp) = result.default_project { + println!("{}", style(format!("āœ“ Default project: {dp}")).green()); + } + println!( "{}", style(format!("\nāœ“ Config written to {}", result.config_path)).green() @@ -1680,7 +1700,9 @@ fn handle_timeline( let params = TimelineParams { query: args.query, - project: args.project, + project: config + .effective_project(args.project.as_deref()) + .map(String::from), since: args.since, depth: args.depth, expand_mentions: args.expand_mentions, @@ -1722,7 +1744,9 @@ async fn handle_search( let cli_filters = SearchCliFilters { source_type: args.source_type, author: args.author, - project: args.project, + project: config + .effective_project(args.project.as_deref()) + .map(String::from), labels: args.label, path: args.path, since: args.since, @@ -1757,7 +1781,8 @@ async fn handle_generate_docs( let start = std::time::Instant::now(); let config = Config::load(config_override)?; - let result = run_generate_docs(&config, args.full, args.project.as_deref(), None)?; + let project = config.effective_project(args.project.as_deref()); + let result = run_generate_docs(&config, args.full, project, None)?; if robot_mode { print_generate_docs_json(&result, start.elapsed().as_millis() as u64); } else { @@ -2031,6 +2056,7 @@ struct RobotDocsData { clap_error_codes: serde_json::Value, error_format: String, workflows: serde_json::Value, + config_notes: serde_json::Value, } #[derive(Serialize)] @@ -2046,12 +2072,12 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box", "--token-env-var ", "--projects "], - "robot_flags": ["--gitlab-url", "--token-env-var", "--projects"], - "example": "lore --robot init --gitlab-url https://gitlab.com --token-env-var GITLAB_TOKEN --projects group/project", + "flags": ["--force", "--non-interactive", "--gitlab-url ", "--token-env-var ", "--projects ", "--default-project "], + "robot_flags": ["--gitlab-url", "--token-env-var", "--projects", "--default-project"], + "example": "lore --robot init --gitlab-url https://gitlab.com --token-env-var GITLAB_TOKEN --projects group/project,other/repo --default-project group/project", "response_schema": { "ok": "bool", - "data": {"config_path": "string", "data_dir": "string", "user": {"username": "string", "name": "string"}, "projects": "[{path:string, name:string}]"}, + "data": {"config_path": "string", "data_dir": "string", "user": {"username": "string", "name": "string"}, "projects": "[{path:string, name:string}]", "default_project": "string?"}, "meta": {"elapsed_ms": "int"} } }, @@ -2360,6 +2386,14 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box Result<(), Box Result<(), Box, - args: WhoArgs, + mut args: WhoArgs, robot_mode: bool, ) -> Result<(), Box> { let start = std::time::Instant::now(); let config = Config::load(config_override)?; + if args.project.is_none() { + args.project = config.default_project.clone(); + } let run = run_who(&config, &args)?; let elapsed_ms = start.elapsed().as_millis() as u64; @@ -2433,6 +2471,7 @@ async fn handle_list_compat( ) -> Result<(), Box> { let start = std::time::Instant::now(); let config = Config::load(config_override)?; + let project_filter = config.effective_project(project_filter); let state_normalized = state_filter.map(str::to_lowercase); match entity { @@ -2511,6 +2550,7 @@ async fn handle_show_compat( ) -> Result<(), Box> { let start = std::time::Instant::now(); let config = Config::load(config_override)?; + let project_filter = config.effective_project(project_filter); match entity { "issue" => {