feat(cli): wire defaultProject through init and all commands
Integrates the defaultProject config field across the entire CLI surface so that omitting `-p` now falls back to the configured default. Init command: - New `--default-project` flag on `lore init` (and robot-mode variant) - InitInputs.default_project: Option<String> passed through to run_init - Validation in run_init ensures the default matches a configured path - Interactive mode: when multiple projects are configured, prompts whether to set a default and which project to use - Robot mode: InitOutputJson now includes default_project (omitted when null) for downstream automation - Autocorrect dictionary updated with `--default-project` Command handlers applying effective_project(): - handle_issues: list filters use config default when -p omitted - handle_mrs: same cascading resolution for MR listing - handle_ingest: dry-run and full sync respect the default - handle_timeline: TimelineParams.project resolved via effective_project - handle_search: SearchCliFilters.project resolved via effective_project - handle_generate_docs: project filter cascades - handle_who: falls back to config.default_project when -p omitted - handle_count: both count subcommands respect the default - handle_discussions: discussion count filters respect the default Robot-docs: - init command schema updated with --default-project flag and response_schema showing default_project as string? - New config_notes section documents the defaultProject field with type, description, and example Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -193,6 +193,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
||||
"--gitlab-url",
|
||||
"--token-env-var",
|
||||
"--projects",
|
||||
"--default-project",
|
||||
],
|
||||
),
|
||||
("generate-docs", &["--full", "--project"]),
|
||||
|
||||
@@ -10,6 +10,7 @@ pub struct InitInputs {
|
||||
pub gitlab_url: String,
|
||||
pub token_env_var: String,
|
||||
pub project_paths: Vec<String>,
|
||||
pub default_project: Option<String>,
|
||||
}
|
||||
|
||||
pub struct InitOptions {
|
||||
@@ -23,6 +24,7 @@ pub struct InitResult {
|
||||
pub data_dir: String,
|
||||
pub user: UserInfo,
|
||||
pub projects: Vec<ProjectInfo>,
|
||||
pub default_project: Option<String>,
|
||||
}
|
||||
|
||||
pub struct UserInfo {
|
||||
@@ -104,6 +106,20 @@ pub async fn run_init(inputs: InitInputs, options: InitOptions) -> Result<InitRe
|
||||
));
|
||||
}
|
||||
|
||||
// Validate default_project matches one of the configured project paths
|
||||
if let Some(ref dp) = inputs.default_project {
|
||||
let matched = inputs.project_paths.iter().any(|p| {
|
||||
p.eq_ignore_ascii_case(dp)
|
||||
|| p.to_ascii_lowercase()
|
||||
.ends_with(&format!("/{}", dp.to_ascii_lowercase()))
|
||||
});
|
||||
if !matched {
|
||||
return Err(LoreError::Other(format!(
|
||||
"defaultProject '{dp}' does not match any configured project path"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parent) = config_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
@@ -118,6 +134,7 @@ pub async fn run_init(inputs: InitInputs, options: InitOptions) -> Result<InitRe
|
||||
.iter()
|
||||
.map(|p| ProjectConfig { path: p.clone() })
|
||||
.collect(),
|
||||
default_project: inputs.default_project.clone(),
|
||||
};
|
||||
|
||||
let config_json = serde_json::to_string_pretty(&config)?;
|
||||
@@ -152,5 +169,6 @@ pub async fn run_init(inputs: InitInputs, options: InitOptions) -> Result<InitRe
|
||||
data_dir: data_dir.display().to_string(),
|
||||
user,
|
||||
projects: validated_projects.into_iter().map(|(p, _)| p).collect(),
|
||||
default_project: inputs.default_project,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -151,6 +151,10 @@ pub enum Commands {
|
||||
/// Comma-separated project paths (required in robot mode)
|
||||
#[arg(long)]
|
||||
projects: Option<String>,
|
||||
|
||||
/// Default project path (used when -p is omitted)
|
||||
#[arg(long)]
|
||||
default_project: Option<String>,
|
||||
},
|
||||
|
||||
#[command(hide = true)]
|
||||
|
||||
114
src/main.rs
114
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<InitOutputProject>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
default_project: Option<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
token_env_var_flag: Option<String>,
|
||||
projects_flag: Option<String>,
|
||||
default_project_flag: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<dyn std::e
|
||||
let commands = serde_json::json!({
|
||||
"init": {
|
||||
"description": "Initialize configuration and database",
|
||||
"flags": ["--force", "--non-interactive", "--gitlab-url <URL>", "--token-env-var <VAR>", "--projects <paths>"],
|
||||
"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 <URL>", "--token-env-var <VAR>", "--projects <paths>", "--default-project <path>"],
|
||||
"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<dyn std::e
|
||||
"PARSE_ERROR": "General parse error"
|
||||
});
|
||||
|
||||
let config_notes = serde_json::json!({
|
||||
"defaultProject": {
|
||||
"type": "string?",
|
||||
"description": "Fallback project path used when -p/--project is omitted. Must match a configured project path (exact or suffix). CLI -p always overrides.",
|
||||
"example": "group/project"
|
||||
}
|
||||
});
|
||||
|
||||
let output = RobotDocsOutput {
|
||||
ok: true,
|
||||
data: RobotDocsData {
|
||||
@@ -2377,6 +2411,7 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box<dyn std::e
|
||||
clap_error_codes,
|
||||
error_format: "stderr JSON: {\"error\":{\"code\":\"...\",\"message\":\"...\",\"suggestion\":\"...\",\"actions\":[\"...\"]}}".to_string(),
|
||||
workflows,
|
||||
config_notes,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2391,11 +2426,14 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box<dyn std::e
|
||||
|
||||
fn handle_who(
|
||||
config_override: Option<&str>,
|
||||
args: WhoArgs,
|
||||
mut args: WhoArgs,
|
||||
robot_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
let start = std::time::Instant::now();
|
||||
let config = Config::load(config_override)?;
|
||||
let project_filter = config.effective_project(project_filter);
|
||||
|
||||
match entity {
|
||||
"issue" => {
|
||||
|
||||
Reference in New Issue
Block a user