Standardize CLI argument flags and update documentation (#5516)

This commit is contained in:
Will Pfleger
2025-11-06 18:10:44 -05:00
committed by GitHub
parent cc4d978c7b
commit 3bf769edfe
6 changed files with 158 additions and 92 deletions
+63 -39
View File
@@ -41,17 +41,17 @@ struct Cli {
#[group(required = false, multiple = false)]
pub struct Identifier {
#[arg(
short,
short = 'n',
long,
value_name = "NAME",
help = "Name for the chat session (e.g., 'project-x')",
long_help = "Specify a name for your chat session. When used with --resume, will resume this specific session if it exists.",
alias = "id"
long_help = "Specify a name for your chat session. When used with --resume, will resume this specific session if it exists."
)]
pub name: Option<String>,
#[arg(
long = "session-id",
alias = "id",
value_name = "SESSION_ID",
help = "Session ID (e.g., '20250921_143022')",
long_help = "Specify a session ID directly. When used with --resume, will resume this specific session if it exists."
@@ -59,7 +59,6 @@ pub struct Identifier {
pub session_id: Option<String>,
#[arg(
short,
long,
value_name = "PATH",
help = "Legacy: Path for the chat session",
@@ -188,7 +187,8 @@ enum SessionCommand {
ascending: bool,
#[arg(
short = 'p',
short = 'w',
short_alias = 'p',
long = "working_dir",
help = "Filter sessions by working directory"
)]
@@ -197,16 +197,15 @@ enum SessionCommand {
#[arg(short = 'l', long = "limit", help = "Limit the number of results")]
limit: Option<usize>,
},
#[command(about = "Remove sessions. Runs interactively if no ID or regex is provided.")]
#[command(about = "Remove sessions. Runs interactively if no ID, name, or regex is provided.")]
Remove {
#[command(flatten)]
identifier: Option<Identifier>,
#[arg(
short,
short = 'r',
long,
alias = "name",
help = "Session ID to be removed (optional)"
help = "Regex for removing matched sessions (optional)"
)]
id: Option<String>,
#[arg(short, long, help = "Regex for removing matched sessions (optional)")]
regex: Option<String>,
},
#[command(about = "Export a session")]
@@ -232,12 +231,12 @@ enum SessionCommand {
},
#[command(name = "diagnostics")]
Diagnostics {
/// Session ID to generate diagnostics for
#[arg(short, long)]
session_id: String,
/// Session identifier for generating diagnostics
#[command(flatten)]
identifier: Option<Identifier>,
/// Output path for the diagnostics zip file (optional, defaults to current directory)
#[arg(short, long)]
#[arg(short = 'o', long)]
output: Option<PathBuf>,
},
}
@@ -246,8 +245,12 @@ enum SessionCommand {
enum SchedulerCommand {
#[command(about = "Add a new scheduled job")]
Add {
#[arg(long, help = "Unique ID for the job")]
id: String,
#[arg(
long = "schedule-id",
alias = "id",
help = "Unique ID for the recurring scheduled job"
)]
schedule_id: String,
#[arg(
long,
help = "Cron expression for the schedule",
@@ -264,29 +267,33 @@ enum SchedulerCommand {
List {},
#[command(about = "Remove a scheduled job by ID")]
Remove {
#[arg(long, help = "ID of the job to remove")] // Changed from positional to named --id
id: String,
#[arg(
long = "schedule-id",
alias = "id",
help = "ID of the scheduled job to remove (removes the recurring schedule)"
)]
schedule_id: String,
},
/// List sessions created by a specific schedule
#[command(about = "List sessions created by a specific schedule")]
Sessions {
/// ID of the schedule
#[arg(long, help = "ID of the schedule")] // Explicitly make it --id
id: String,
#[arg(long, help = "Maximum number of sessions to return")]
#[arg(long = "schedule-id", alias = "id", help = "ID of the schedule")]
schedule_id: String,
#[arg(short = 'l', long, help = "Maximum number of sessions to return")]
limit: Option<usize>,
},
#[command(about = "Run a scheduled job immediately")]
RunNow {
/// ID of the schedule to run
#[arg(long, help = "ID of the schedule to run")] // Explicitly make it --id
id: String,
#[arg(long = "schedule-id", alias = "id", help = "ID of the schedule to run")]
schedule_id: String,
},
/// Check status of scheduler services (deprecated - no external services needed)
#[command(about = "Check status of scheduler services")]
#[command(about = "[Deprecated] Check status of scheduler services")]
ServicesStatus {},
/// Stop scheduler services (deprecated - no external services needed)
#[command(about = "Stop scheduler services")]
#[command(about = "[Deprecated] Stop scheduler services")]
ServicesStop {},
/// Show cron expression examples and help
#[command(about = "Show cron expression examples and help")]
@@ -435,8 +442,8 @@ enum Command {
#[arg(
short,
long,
help = "Resume a previous session (last used or specified by --name)",
long_help = "Continue from a previous chat session. If --name or --path is provided, resumes that specific session. Otherwise resumes the last used session."
help = "Resume a previous session (last used or specified by --name/--session-id)",
long_help = "Continue from a previous session. If --name or --session-id is provided, resumes that specific session. Otherwise resumes the most recently used session."
)]
resume: bool,
@@ -903,8 +910,13 @@ pub async fn cli() -> anyhow::Result<()> {
working_dir,
limit,
}) => Ok(handle_session_list(format, ascending, working_dir, limit).await?),
Some(SessionCommand::Remove { id, regex }) => {
Ok(handle_session_remove(id, regex).await?)
Some(SessionCommand::Remove { identifier, regex }) => {
let (session_id, name) = if let Some(id) = identifier {
(id.session_id, id.name)
} else {
(None, None)
};
Ok(handle_session_remove(session_id, name, regex).await?)
}
Some(SessionCommand::Export {
identifier,
@@ -933,7 +945,19 @@ pub async fn cli() -> anyhow::Result<()> {
.await?;
Ok(())
}
Some(SessionCommand::Diagnostics { session_id, output }) => {
Some(SessionCommand::Diagnostics { identifier, output }) => {
let session_id = if let Some(id) = identifier {
lookup_session_id(id).await?
} else {
match crate::commands::session::prompt_interactive_session_selection().await
{
Ok(id) => id,
Err(e) => {
eprintln!("Error: {}", e);
return Ok(());
}
}
};
crate::commands::session::handle_diagnostics(&session_id, output).await?;
Ok(())
}
@@ -1264,25 +1288,25 @@ pub async fn cli() -> anyhow::Result<()> {
Some(Command::Schedule { command }) => {
match command {
SchedulerCommand::Add {
id,
schedule_id,
cron,
recipe_source,
} => {
handle_schedule_add(id, cron, recipe_source).await?;
handle_schedule_add(schedule_id, cron, recipe_source).await?;
}
SchedulerCommand::List {} => {
handle_schedule_list().await?;
}
SchedulerCommand::Remove { id } => {
handle_schedule_remove(id).await?;
SchedulerCommand::Remove { schedule_id } => {
handle_schedule_remove(schedule_id).await?;
}
SchedulerCommand::Sessions { id, limit } => {
SchedulerCommand::Sessions { schedule_id, limit } => {
// New arm
handle_schedule_sessions(id, limit).await?;
handle_schedule_sessions(schedule_id, limit).await?;
}
SchedulerCommand::RunNow { id } => {
SchedulerCommand::RunNow { schedule_id } => {
// New arm
handle_schedule_run_now(id).await?;
handle_schedule_run_now(schedule_id).await?;
}
SchedulerCommand::ServicesStatus {} => {
handle_schedule_services_status().await?;
+33 -23
View File
@@ -75,13 +75,13 @@ fn validate_cron_expression(cron: &str) -> Result<()> {
}
pub async fn handle_schedule_add(
id: String,
schedule_id: String,
cron: String,
recipe_source_arg: String, // This is expected to be a file path by the Scheduler
) -> Result<()> {
println!(
"[CLI Debug] Scheduling job ID: {}, Cron: {}, Recipe Source Path: {}",
id, cron, recipe_source_arg
schedule_id, cron, recipe_source_arg
);
// Validate cron expression and provide helpful feedback
@@ -90,7 +90,7 @@ pub async fn handle_schedule_add(
// The Scheduler's add_scheduled_job will handle copying the recipe from recipe_source_arg
// to its internal storage and validating the path.
let job = ScheduledJob {
id: id.clone(),
id: schedule_id.clone(),
source: recipe_source_arg.clone(), // Pass the original user-provided path
cron,
last_run: None,
@@ -116,11 +116,12 @@ pub async fn handle_schedule_add(
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("yaml");
let final_recipe_path = scheduled_recipes_dir.join(format!("{}.{}", id, extension));
let final_recipe_path =
scheduled_recipes_dir.join(format!("{}.{}", schedule_id, extension));
println!(
"Scheduled job '{}' added. Recipe expected at {:?}",
id, final_recipe_path
schedule_id, final_recipe_path
);
Ok(())
}
@@ -138,7 +139,7 @@ pub async fn handle_schedule_add(
);
}
_ => Err(anyhow::Error::new(e))
.context(format!("Failed to add job '{}' to scheduler", id)),
.context(format!("Failed to add job '{}' to scheduler", schedule_id)),
}
}
}
@@ -179,41 +180,46 @@ pub async fn handle_schedule_list() -> Result<()> {
Ok(())
}
pub async fn handle_schedule_remove(id: String) -> Result<()> {
pub async fn handle_schedule_remove(schedule_id: String) -> Result<()> {
let scheduler_storage_path =
get_default_scheduler_storage_path().context("Failed to get scheduler storage path")?;
let scheduler = SchedulerFactory::create(scheduler_storage_path)
.await
.context("Failed to initialize scheduler")?;
match scheduler.remove_scheduled_job(&id).await {
match scheduler.remove_scheduled_job(&schedule_id).await {
Ok(_) => {
println!("Scheduled job '{}' and its associated recipe removed.", id);
println!(
"Scheduled job '{}' and its associated recipe removed.",
schedule_id
);
Ok(())
}
Err(e) => match e {
SchedulerError::JobNotFound(job_id) => {
bail!("Error: Job with ID '{}' not found.", job_id);
}
_ => Err(anyhow::Error::new(e))
.context(format!("Failed to remove job '{}' from scheduler", id)),
_ => Err(anyhow::Error::new(e)).context(format!(
"Failed to remove job '{}' from scheduler",
schedule_id
)),
},
}
}
pub async fn handle_schedule_sessions(id: String, limit: Option<usize>) -> Result<()> {
pub async fn handle_schedule_sessions(schedule_id: String, limit: Option<usize>) -> Result<()> {
let scheduler_storage_path =
get_default_scheduler_storage_path().context("Failed to get scheduler storage path")?;
let scheduler = SchedulerFactory::create(scheduler_storage_path)
.await
.context("Failed to initialize scheduler")?;
match scheduler.sessions(&id, limit.unwrap_or(50)).await {
match scheduler.sessions(&schedule_id, limit.unwrap_or(50)).await {
Ok(sessions) => {
if sessions.is_empty() {
println!("No sessions found for schedule ID '{}'.", id);
println!("No sessions found for schedule ID '{}'.", schedule_id);
} else {
println!("Sessions for schedule ID '{}':", id);
println!("Sessions for schedule ID '{}':", schedule_id);
// sessions is now Vec<(String, SessionMetadata)>
for (session_name, metadata) in sessions {
println!(
@@ -227,31 +233,35 @@ pub async fn handle_schedule_sessions(id: String, limit: Option<usize>) -> Resul
}
}
Err(e) => {
bail!("Failed to get sessions for schedule '{}': {:?}", id, e);
bail!(
"Failed to get sessions for schedule '{}': {:?}",
schedule_id,
e
);
}
}
Ok(())
}
pub async fn handle_schedule_run_now(id: String) -> Result<()> {
pub async fn handle_schedule_run_now(schedule_id: String) -> Result<()> {
let scheduler_storage_path =
get_default_scheduler_storage_path().context("Failed to get scheduler storage path")?;
let scheduler = SchedulerFactory::create(scheduler_storage_path)
.await
.context("Failed to initialize scheduler")?;
match scheduler.run_now(&id).await {
match scheduler.run_now(&schedule_id).await {
Ok(session_id) => {
println!(
"Successfully triggered schedule '{}'. New session ID: {}",
id, session_id
schedule_id, session_id
);
}
Err(e) => match e {
SchedulerError::JobNotFound(job_id) => {
bail!("Error: Job with ID '{}' not found.", job_id);
}
_ => bail!("Failed to run schedule '{}' now: {:?}", id, e),
_ => bail!("Failed to run schedule '{}' now: {:?}", schedule_id, e),
},
}
Ok(())
@@ -326,12 +336,12 @@ pub async fn handle_schedule_cron_help() -> Result<()> {
println!("💡 EXAMPLES:");
println!(
" goose schedule add --id hourly-report --cron \"0 * * * *\" --recipe-source report.yaml"
" goose schedule add --schedule-id hourly-report --cron \"0 * * * *\" --recipe-source report.yaml"
);
println!(
" goose schedule add --id daily-backup --cron \"@daily\" --recipe-source backup.yaml"
" goose schedule add --schedule-id daily-backup --cron \"@daily\" --recipe-source backup.yaml"
);
println!(" goose schedule add --id weekly-summary --cron \"0 9 * * 1\" --recipe-source summary.yaml");
println!(" goose schedule add --schedule-id weekly-summary --cron \"0 9 * * 1\" --recipe-source summary.yaml");
Ok(())
}
+16 -3
View File
@@ -71,7 +71,11 @@ fn prompt_interactive_session_removal(sessions: &[Session]) -> Result<Vec<Sessio
Ok(selected_sessions)
}
pub async fn handle_session_remove(id: Option<String>, regex_string: Option<String>) -> Result<()> {
pub async fn handle_session_remove(
session_id: Option<String>,
name: Option<String>,
regex_string: Option<String>,
) -> Result<()> {
let all_sessions = match SessionManager::list_sessions().await {
Ok(sessions) => sessions,
Err(e) => {
@@ -82,11 +86,20 @@ pub async fn handle_session_remove(id: Option<String>, regex_string: Option<Stri
let matched_sessions: Vec<Session>;
if let Some(id_val) = id {
if let Some(id_val) = session_id {
if let Some(session) = all_sessions.iter().find(|s| s.id == id_val) {
matched_sessions = vec![session.clone()];
} else {
return Err(anyhow::anyhow!("Session '{}' not found.", id_val));
return Err(anyhow::anyhow!("Session ID '{}' not found.", id_val));
}
} else if let Some(name_val) = name {
if let Some(session) = all_sessions.iter().find(|s| s.name == name_val) {
matched_sessions = vec![session.clone()];
} else {
return Err(anyhow::anyhow!(
"Session with name '{}' not found.",
name_val
));
}
} else if let Some(regex_val) = regex_string {
let session_regex = Regex::new(&regex_val)