use car_scheduler::os_schedule::LABEL_PREFIX;
use car_scheduler::{CommandSpec, OsScheduleSpec, Task, TaskStatus, TaskStore, TaskTrigger};
use serde_json::json;
fn task_store() -> TaskStore {
TaskStore::new(&TaskStore::default_path())
}
fn os_spec(
task_json: &str,
program: &str,
args_json: &str,
) -> Result<(Task, OsScheduleSpec), String> {
let task: Task =
serde_json::from_str(task_json).map_err(|e| format!("invalid task JSON: {e}"))?;
let args: Vec<String> = serde_json::from_str(args_json)
.map_err(|e| format!("invalid args JSON (expected a string array): {e}"))?;
let spec = OsScheduleSpec::from_task(&task, program, args).map_err(|e| e.to_string())?;
Ok((task, spec))
}
pub fn render_os_schedule(
task_json: &str,
program: &str,
args_json: &str,
) -> Result<String, String> {
let (_task, spec) = os_spec(task_json, program, args_json)?;
let (plist, plist_err) = match spec.render_launchd_plist() {
Ok(s) => (Some(s), None),
Err(e) => (None, Some(e.to_string())),
};
let (cron, cron_err) = match spec.render_crontab_line() {
Ok(s) => (Some(s), None),
Err(e) => (None, Some(e.to_string())),
};
Ok(json!({
"label": spec.label,
"launchd_plist": plist,
"launchd_error": plist_err,
"crontab_line": cron,
"crontab_error": cron_err,
})
.to_string())
}
pub fn install_os_schedule(
task_json: &str,
program: &str,
args_json: &str,
) -> Result<String, String> {
let (task, spec) = os_spec(task_json, program, args_json)?;
task_store()
.save(&task)
.map_err(|e| format!("could not persist task for reconcile tracking: {e}"))?;
let installed = spec.install().map_err(|e| e.to_string())?;
serde_json::to_string(&installed).map_err(|e| e.to_string())
}
pub fn uninstall_os_schedule(label_or_id: &str) -> Result<String, String> {
let label = if label_or_id.starts_with(LABEL_PREFIX) {
label_or_id.to_string()
} else {
format!("{LABEL_PREFIX}{label_or_id}")
};
let removed = car_scheduler::uninstall(&label).map_err(|e| e.to_string())?;
Ok(json!({ "label": label, "removed": removed }).to_string())
}
pub fn list_os_schedules() -> Result<String, String> {
let labels = car_scheduler::list_installed().map_err(|e| e.to_string())?;
serde_json::to_string(&labels).map_err(|e| e.to_string())
}
pub fn reconcile_os_schedules() -> Result<String, String> {
let tasks = task_store().try_list().map_err(|e| {
format!("could not read task store at ~/.car/tasks (refusing to reap schedules): {e}")
})?;
let report = car_scheduler::reconcile_with_tasks(&tasks).map_err(|e| e.to_string())?;
serde_json::to_string(&report).map_err(|e| e.to_string())
}
pub fn create_task(
name: &str,
prompt: &str,
trigger: Option<&str>,
schedule: Option<&str>,
system_prompt: Option<&str>,
) -> Result<String, String> {
let mut task = car_scheduler::Task::new(name, prompt);
if let Some(t) = trigger {
let trigger_type = match t {
"once" => car_scheduler::TaskTrigger::Once,
"cron" => car_scheduler::TaskTrigger::Cron,
"interval" => car_scheduler::TaskTrigger::Interval,
"file_watch" => car_scheduler::TaskTrigger::FileWatch,
_ => car_scheduler::TaskTrigger::Manual,
};
task = task.with_trigger(trigger_type, schedule.unwrap_or(""));
}
if let Some(sp) = system_prompt {
task = task.with_system_prompt(sp);
}
serde_json::to_string(&task).map_err(|e| e.to_string())
}
pub fn schedule_task(spec_json: &str) -> Result<String, String> {
#[derive(serde::Deserialize)]
struct Cadence {
interval_secs: Option<u64>,
cron: Option<String>,
}
fn default_true() -> bool {
true
}
#[derive(serde::Deserialize)]
struct ScheduleReq {
name: String,
program: String,
#[serde(default)]
args: Vec<String>,
cadence: Cadence,
#[serde(default = "default_true")]
durable: bool,
#[serde(default)]
working_dir: Option<String>,
#[serde(default)]
env: std::collections::BTreeMap<String, String>,
#[serde(default)]
timeout_secs: Option<u64>,
}
let req: ScheduleReq =
serde_json::from_str(spec_json).map_err(|e| format!("invalid schedule JSON: {e}"))?;
let (trigger, schedule) = match (req.cadence.interval_secs, &req.cadence.cron) {
(Some(secs), None) => (TaskTrigger::Interval, secs.to_string()),
(None, Some(cron)) => (TaskTrigger::Cron, cron.clone()),
(None, None) => {
return Err("cadence must specify interval_secs or cron".into())
}
(Some(_), Some(_)) => {
return Err("cadence must specify only one of interval_secs or cron".into())
}
};
if trigger == TaskTrigger::Cron && !req.durable {
return Err(
"a cron cadence is scheduled via the OS backend (durable); set durable:true, \
or use interval_secs for a daemon-native timer"
.into(),
);
}
let command = CommandSpec {
program: req.program.clone(),
args: req.args.clone(),
working_dir: req.working_dir,
env: req.env,
timeout_secs: req.timeout_secs,
};
let mut task = Task::command(&req.name, command).with_trigger(trigger, &schedule);
task.status = TaskStatus::Scheduled;
let store = task_store();
store
.save(&task)
.map_err(|e| format!("could not persist scheduled task: {e}"))?;
let (durable, backend) = if req.durable {
let install = OsScheduleSpec::from_task(&task, &req.program, req.args.clone())
.map_err(|e| e.to_string())
.and_then(|spec| spec.install().map_err(|e| e.to_string()));
match install {
Ok(installed) => (true, installed.backend),
Err(e) => {
let _ = store.delete(&task.id);
return Err(e);
}
}
} else {
(false, "daemon".to_string())
};
Ok(json!({
"id": task.id,
"durable": durable,
"backend": backend,
"task": task,
})
.to_string())
}
pub fn list_scheduled_tasks() -> Result<String, String> {
let installed: std::collections::HashSet<String> = car_scheduler::list_installed()
.unwrap_or_default()
.into_iter()
.collect();
let out: Vec<_> = task_store()
.list()
.into_iter()
.filter(|t| t.is_command())
.map(|t| {
let label = format!("{LABEL_PREFIX}{}", t.id);
let durable = installed.contains(&label);
let cmd = t.command.as_ref();
json!({
"id": t.id,
"name": t.name,
"program": cmd.map(|c| c.program.clone()),
"args": cmd.map(|c| c.args.clone()),
"trigger": t.trigger,
"schedule": t.schedule,
"durable": durable,
"backend": if durable { "os" } else { "daemon" },
"enabled": t.enabled,
})
})
.collect();
serde_json::to_string(&out).map_err(|e| e.to_string())
}
pub fn unschedule_task(id_or_label: &str) -> Result<String, String> {
let id = id_or_label
.strip_prefix(LABEL_PREFIX)
.unwrap_or(id_or_label)
.to_string();
let label = format!("{LABEL_PREFIX}{id}");
let os_removed = car_scheduler::uninstall(&label).map_err(|e| e.to_string())?;
let deleted = task_store().delete(&id);
Ok(json!({ "id": id, "os_removed": os_removed, "deleted": deleted }).to_string())
}
pub fn analyze_schedule(graph_json: &str) -> Result<String, String> {
let graph: car_scheduler::graph_schedule::ScheduleGraph = crate::from_json("graph", graph_json)?;
let analysis = car_scheduler::graph_schedule::analyze_schedule(&graph);
crate::to_json(&analysis)
}