@@ -11,8 +11,7 @@ use crate::commands::project::{handle_project_default, handle_projects_interacti
|
||||
use crate::commands::recipe::{handle_deeplink, handle_validate};
|
||||
// Import the new handlers from commands::schedule
|
||||
use crate::commands::schedule::{
|
||||
handle_schedule_add, handle_schedule_cron_help, handle_schedule_list, handle_schedule_remove,
|
||||
handle_schedule_run_now, handle_schedule_services_status, handle_schedule_services_stop,
|
||||
handle_schedule_add, handle_schedule_list, handle_schedule_remove, handle_schedule_run_now,
|
||||
handle_schedule_sessions,
|
||||
};
|
||||
use crate::commands::session::{handle_session_list, handle_session_remove};
|
||||
@@ -124,11 +123,7 @@ enum SchedulerCommand {
|
||||
Add {
|
||||
#[arg(long, help = "Unique ID for the job")]
|
||||
id: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Cron expression for the schedule",
|
||||
long_help = "Cron expression for when to run the job. Examples:\n '0 * * * *' - Every hour at minute 0\n '0 */2 * * *' - Every 2 hours\n '@hourly' - Every hour (shorthand)\n '0 9 * * *' - Every day at 9:00 AM\n '0 9 * * 1' - Every Monday at 9:00 AM\n '0 0 1 * *' - First day of every month at midnight"
|
||||
)]
|
||||
#[arg(long, help = "Cron string for the schedule (e.g., '0 0 * * * *')")]
|
||||
cron: String,
|
||||
#[arg(
|
||||
long,
|
||||
@@ -160,15 +155,6 @@ enum SchedulerCommand {
|
||||
#[arg(long, help = "ID of the schedule to run")] // Explicitly make it --id
|
||||
id: String,
|
||||
},
|
||||
/// Check status of Temporal services (temporal scheduler only)
|
||||
#[command(about = "Check status of Temporal services")]
|
||||
ServicesStatus {},
|
||||
/// Stop Temporal services (temporal scheduler only)
|
||||
#[command(about = "Stop Temporal services")]
|
||||
ServicesStop {},
|
||||
/// Show cron expression examples and help
|
||||
#[command(about = "Show cron expression examples and help")]
|
||||
CronHelp {},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -782,15 +768,6 @@ pub async fn cli() -> Result<()> {
|
||||
// New arm
|
||||
handle_schedule_run_now(id).await?;
|
||||
}
|
||||
SchedulerCommand::ServicesStatus {} => {
|
||||
handle_schedule_services_status().await?;
|
||||
}
|
||||
SchedulerCommand::ServicesStop {} => {
|
||||
handle_schedule_services_stop().await?;
|
||||
}
|
||||
SchedulerCommand::CronHelp {} => {
|
||||
handle_schedule_cron_help().await?;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use base64::engine::{general_purpose::STANDARD as BASE64_STANDARD, Engine};
|
||||
use goose::scheduler::{
|
||||
get_default_scheduled_recipes_dir, get_default_scheduler_storage_path, ScheduledJob,
|
||||
get_default_scheduled_recipes_dir, get_default_scheduler_storage_path, ScheduledJob, Scheduler,
|
||||
SchedulerError,
|
||||
};
|
||||
use goose::scheduler_factory::SchedulerFactory;
|
||||
use goose::temporal_scheduler::TemporalScheduler;
|
||||
use std::path::Path;
|
||||
|
||||
// Base64 decoding function - might be needed if recipe_source_arg can be base64
|
||||
@@ -17,64 +15,6 @@ async fn _decode_base64_recipe(source: &str) -> Result<String> {
|
||||
String::from_utf8(bytes).with_context(|| "Decoded Base64 recipe source is not valid UTF-8.")
|
||||
}
|
||||
|
||||
fn validate_cron_expression(cron: &str) -> Result<()> {
|
||||
// Basic validation and helpful suggestions
|
||||
if cron.trim().is_empty() {
|
||||
bail!("Cron expression cannot be empty");
|
||||
}
|
||||
|
||||
// Check for common mistakes and provide helpful suggestions
|
||||
let parts: Vec<&str> = cron.split_whitespace().collect();
|
||||
|
||||
match parts.len() {
|
||||
5 => {
|
||||
// Standard 5-field cron (minute hour day month weekday)
|
||||
println!("✅ Using standard 5-field cron format: {}", cron);
|
||||
}
|
||||
6 => {
|
||||
// 6-field cron with seconds (second minute hour day month weekday)
|
||||
println!("✅ Using 6-field cron format with seconds: {}", cron);
|
||||
}
|
||||
1 if cron.starts_with('@') => {
|
||||
// Shorthand expressions like @hourly, @daily, etc.
|
||||
let valid_shorthands = [
|
||||
"@yearly",
|
||||
"@annually",
|
||||
"@monthly",
|
||||
"@weekly",
|
||||
"@daily",
|
||||
"@midnight",
|
||||
"@hourly",
|
||||
];
|
||||
if valid_shorthands.contains(&cron) {
|
||||
println!("✅ Using cron shorthand: {}", cron);
|
||||
} else {
|
||||
println!(
|
||||
"⚠️ Unknown cron shorthand '{}'. Valid options: {}",
|
||||
cron,
|
||||
valid_shorthands.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!("⚠️ Unusual cron format detected: '{}'", cron);
|
||||
println!(" Common formats:");
|
||||
println!(" - 5 fields: '0 * * * *' (minute hour day month weekday)");
|
||||
println!(" - 6 fields: '0 0 * * * *' (second minute hour day month weekday)");
|
||||
println!(" - Shorthand: '@hourly', '@daily', '@weekly', '@monthly'");
|
||||
}
|
||||
}
|
||||
|
||||
// Provide examples for common scheduling needs
|
||||
if cron == "* * * * *" {
|
||||
println!("⚠️ This will run every minute! Did you mean:");
|
||||
println!(" - '0 * * * *' for every hour?");
|
||||
println!(" - '0 0 * * *' for every day?");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_schedule_add(
|
||||
id: String,
|
||||
cron: String,
|
||||
@@ -85,9 +25,6 @@ pub async fn handle_schedule_add(
|
||||
id, cron, recipe_source_arg
|
||||
);
|
||||
|
||||
// Validate cron expression and provide helpful feedback
|
||||
validate_cron_expression(&cron)?;
|
||||
|
||||
// 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 {
|
||||
@@ -103,7 +40,7 @@ pub async fn handle_schedule_add(
|
||||
|
||||
let scheduler_storage_path =
|
||||
get_default_scheduler_storage_path().context("Failed to get scheduler storage path")?;
|
||||
let scheduler = SchedulerFactory::create(scheduler_storage_path)
|
||||
let scheduler = Scheduler::new(scheduler_storage_path)
|
||||
.await
|
||||
.context("Failed to initialize scheduler")?;
|
||||
|
||||
@@ -148,28 +85,19 @@ pub async fn handle_schedule_add(
|
||||
pub async fn handle_schedule_list() -> Result<()> {
|
||||
let scheduler_storage_path =
|
||||
get_default_scheduler_storage_path().context("Failed to get scheduler storage path")?;
|
||||
let scheduler = SchedulerFactory::create(scheduler_storage_path)
|
||||
let scheduler = Scheduler::new(scheduler_storage_path)
|
||||
.await
|
||||
.context("Failed to initialize scheduler")?;
|
||||
|
||||
let jobs = scheduler.list_scheduled_jobs().await?;
|
||||
let jobs = scheduler.list_scheduled_jobs().await;
|
||||
if jobs.is_empty() {
|
||||
println!("No scheduled jobs found.");
|
||||
} else {
|
||||
println!("Scheduled Jobs:");
|
||||
for job in jobs {
|
||||
let status = if job.currently_running {
|
||||
"🟢 RUNNING"
|
||||
} else if job.paused {
|
||||
"⏸️ PAUSED"
|
||||
} else {
|
||||
"⏹️ IDLE"
|
||||
};
|
||||
|
||||
println!(
|
||||
"- ID: {}\n Status: {}\n Cron: {}\n Recipe Source (in store): {}\n Last Run: {}",
|
||||
"- ID: {}\n Cron: {}\n Recipe Source (in store): {}\n Last Run: {}",
|
||||
job.id,
|
||||
status,
|
||||
job.cron,
|
||||
job.source, // This source is now the path within scheduled_recipes_dir
|
||||
job.last_run
|
||||
@@ -183,7 +111,7 @@ pub async fn handle_schedule_list() -> Result<()> {
|
||||
pub async fn handle_schedule_remove(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)
|
||||
let scheduler = Scheduler::new(scheduler_storage_path)
|
||||
.await
|
||||
.context("Failed to initialize scheduler")?;
|
||||
|
||||
@@ -205,7 +133,7 @@ pub async fn handle_schedule_remove(id: String) -> Result<()> {
|
||||
pub async fn handle_schedule_sessions(id: String, limit: Option<u32>) -> Result<()> {
|
||||
let scheduler_storage_path =
|
||||
get_default_scheduler_storage_path().context("Failed to get scheduler storage path")?;
|
||||
let scheduler = SchedulerFactory::create(scheduler_storage_path)
|
||||
let scheduler = Scheduler::new(scheduler_storage_path)
|
||||
.await
|
||||
.context("Failed to initialize scheduler")?;
|
||||
|
||||
@@ -238,7 +166,7 @@ pub async fn handle_schedule_sessions(id: String, limit: Option<u32>) -> Result<
|
||||
pub async fn handle_schedule_run_now(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)
|
||||
let scheduler = Scheduler::new(scheduler_storage_path)
|
||||
.await
|
||||
.context("Failed to initialize scheduler")?;
|
||||
|
||||
@@ -258,131 +186,3 @@ pub async fn handle_schedule_run_now(id: String) -> Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_schedule_services_status() -> Result<()> {
|
||||
// Check if we're using temporal scheduler
|
||||
let scheduler_type =
|
||||
std::env::var("GOOSE_SCHEDULER_TYPE").unwrap_or_else(|_| "temporal".to_string());
|
||||
|
||||
if scheduler_type != "temporal" {
|
||||
println!("Service management is only available for temporal scheduler.");
|
||||
println!("Set GOOSE_SCHEDULER_TYPE=temporal to use Temporal services.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Checking Temporal services status...");
|
||||
|
||||
// Create a temporary TemporalScheduler to check status
|
||||
match TemporalScheduler::new().await {
|
||||
Ok(scheduler) => {
|
||||
let info = scheduler.get_service_info().await;
|
||||
println!("{}", info);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to check services: {}", e);
|
||||
println!("\nThis might mean:");
|
||||
println!("- Temporal CLI is not installed");
|
||||
println!("- Go service binary is not built");
|
||||
println!("- Services are not running");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_schedule_services_stop() -> Result<()> {
|
||||
// Check if we're using temporal scheduler
|
||||
let scheduler_type =
|
||||
std::env::var("GOOSE_SCHEDULER_TYPE").unwrap_or_else(|_| "temporal".to_string());
|
||||
|
||||
if scheduler_type != "temporal" {
|
||||
println!("Service management is only available for temporal scheduler.");
|
||||
println!("Set GOOSE_SCHEDULER_TYPE=temporal to use Temporal services.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Stopping Temporal services...");
|
||||
|
||||
// Create a temporary TemporalScheduler to stop services
|
||||
match TemporalScheduler::new().await {
|
||||
Ok(scheduler) => match scheduler.stop_services().await {
|
||||
Ok(result) => {
|
||||
println!("{}", result);
|
||||
println!("\nNote: Services were running independently and have been stopped.");
|
||||
println!("They will be automatically restarted when needed.");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to stop services: {}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Failed to initialize scheduler: {}", e);
|
||||
println!("Services may not be running or may have already been stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_schedule_cron_help() -> Result<()> {
|
||||
println!("📅 Cron Expression Guide for Goose Scheduler");
|
||||
println!("===========================================\\n");
|
||||
|
||||
println!("🕐 HOURLY SCHEDULES (Most Common Request):");
|
||||
println!(" 0 * * * * - Every hour at minute 0 (e.g., 1:00, 2:00, 3:00...)");
|
||||
println!(" 30 * * * * - Every hour at minute 30 (e.g., 1:30, 2:30, 3:30...)");
|
||||
println!(" 0 */2 * * * - Every 2 hours at minute 0 (e.g., 2:00, 4:00, 6:00...)");
|
||||
println!(" 0 */3 * * * - Every 3 hours at minute 0 (e.g., 3:00, 6:00, 9:00...)");
|
||||
println!(" @hourly - Every hour (same as \"0 * * * *\")\\n");
|
||||
|
||||
println!("📅 DAILY SCHEDULES:");
|
||||
println!(" 0 9 * * * - Every day at 9:00 AM");
|
||||
println!(" 30 14 * * * - Every day at 2:30 PM");
|
||||
println!(" 0 0 * * * - Every day at midnight");
|
||||
println!(" @daily - Every day at midnight\\n");
|
||||
|
||||
println!("📆 WEEKLY SCHEDULES:");
|
||||
println!(" 0 9 * * 1 - Every Monday at 9:00 AM");
|
||||
println!(" 0 17 * * 5 - Every Friday at 5:00 PM");
|
||||
println!(" 0 0 * * 0 - Every Sunday at midnight");
|
||||
println!(" @weekly - Every Sunday at midnight\\n");
|
||||
|
||||
println!("🗓️ MONTHLY SCHEDULES:");
|
||||
println!(" 0 9 1 * * - First day of every month at 9:00 AM");
|
||||
println!(" 0 0 15 * * - 15th of every month at midnight");
|
||||
println!(" @monthly - First day of every month at midnight\\n");
|
||||
|
||||
println!("📝 CRON FORMAT:");
|
||||
println!(" Standard 5-field: minute hour day month weekday");
|
||||
println!(" ┌───────────── minute (0 - 59)");
|
||||
println!(" │ ┌─────────── hour (0 - 23)");
|
||||
println!(" │ │ ┌───────── day of month (1 - 31)");
|
||||
println!(" │ │ │ ┌─────── month (1 - 12)");
|
||||
println!(" │ │ │ │ ┌───── day of week (0 - 7, Sunday = 0 or 7)");
|
||||
println!(" │ │ │ │ │");
|
||||
println!(" * * * * *\\n");
|
||||
|
||||
println!("🔧 SPECIAL CHARACTERS:");
|
||||
println!(" * - Any value (every minute, hour, day, etc.)");
|
||||
println!(" */n - Every nth interval (*/5 = every 5 minutes)");
|
||||
println!(" n-m - Range (1-5 = 1,2,3,4,5)");
|
||||
println!(" n,m - List (1,3,5 = 1 or 3 or 5)\\n");
|
||||
|
||||
println!("⚡ SHORTHAND EXPRESSIONS:");
|
||||
println!(" @yearly - Once a year (0 0 1 1 *)");
|
||||
println!(" @monthly - Once a month (0 0 1 * *)");
|
||||
println!(" @weekly - Once a week (0 0 * * 0)");
|
||||
println!(" @daily - Once a day (0 0 * * *)");
|
||||
println!(" @hourly - Once an hour (0 * * * *)\\n");
|
||||
|
||||
println!("💡 EXAMPLES:");
|
||||
println!(
|
||||
" goose schedule add --id hourly-report --cron \"0 * * * *\" --recipe-source report.yaml"
|
||||
);
|
||||
println!(
|
||||
" goose schedule add --id daily-backup --cron \"@daily\" --recipe-source backup.yaml"
|
||||
);
|
||||
println!(" goose schedule add --id weekly-summary --cron \"0 9 * * 1\" --recipe-source summary.yaml");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user