use crate::paths::routine_prompt_path;
use super::agents::AgentCommand;
use super::model::Routine;
pub(crate) fn slugify(title: &str) -> String {
let mut out = String::new();
let mut prev_dash = false;
for c in title.chars() {
if c.is_ascii_alphanumeric() {
out.push(c.to_ascii_lowercase());
prev_dash = false;
} else if !prev_dash {
out.push('-');
prev_dash = true;
}
}
let trimmed = out.trim_matches('-').to_string();
if trimmed.is_empty() {
"routine".to_string()
} else {
trimmed
}
}
pub(crate) fn compose_prompt(routine: &Routine) -> String {
let mut s = String::from("# Workbench\n");
s.push_str(
"You are working in an empty directory. These repositories are relevant — clone any you need:\n",
);
for repo in &routine.repositories {
match &repo.branch {
Some(b) => s.push_str(&format!("- {} (branch {})\n", repo.repository, b)),
None => s.push_str(&format!("- {}\n", repo.repository)),
}
}
s.push_str("\n---\n");
s.push_str(&routine.prompt);
s.push('\n');
s
}
pub(crate) fn substitute(s: &str, workbench: &str, prompt_file: &str) -> String {
s.replace("{workbench}", workbench)
.replace("{prompt_file}", prompt_file)
.replace("{prompt}", r#""$(cat prompt.txt)""#)
}
fn bin_dir(bin: &str) -> Option<String> {
let path = std::env::var("PATH").ok()?;
path.split(':')
.filter(|d| !d.is_empty())
.find(|d| std::path::Path::new(d).join(bin).is_file())
.map(str::to_string)
}
fn cron_path(agent_command: &str) -> String {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
let mut dirs: Vec<String> = Vec::new();
for bin in ["tmux", agent_command] {
if let Some(d) = bin_dir(bin) {
dirs.push(d);
}
}
for d in [
format!("{home}/.local/bin"),
"/opt/homebrew/bin".to_string(),
"/usr/local/bin".to_string(),
format!("{home}/.cargo/bin"),
format!("{home}/.bun/bin"),
"/usr/bin".to_string(),
"/bin".to_string(),
"/usr/sbin".to_string(),
"/sbin".to_string(),
] {
dirs.push(d);
}
let mut seen = std::collections::HashSet::new();
dirs.retain(|d| seen.insert(d.clone()));
dirs.join(":")
}
pub(crate) fn shell_quote(s: &str) -> String {
let mut out = String::from("'");
for c in s.chars() {
if c == '\'' {
out.push_str("'\\''");
} else {
out.push(c);
}
}
out.push('\'');
out
}
pub(crate) fn build_routine_command(routine: &Routine, agent: &AgentCommand) -> String {
let slug = slugify(&routine.title);
let prompt_path = routine_prompt_path(&routine.id)
.to_string_lossy()
.into_owned();
let prompt_file_ref = "prompt.txt";
let workbench_ref = ".";
let mut invocation = vec![agent.command.clone()];
for a in &agent.args {
invocation.push(substitute(a, workbench_ref, prompt_file_ref));
}
let invocation = invocation.join(" ");
let mut stmts = vec![
format!("export PATH={}", shell_quote(&cron_path(&agent.command))),
r#"TS="$(date +%s)""#.to_string(),
format!("SLUG={}", shell_quote(&slug)),
r#"WB="$HOME/.moadim/workbenches/$SLUG-$TS""#.to_string(),
r#"SESS="moadim-$SLUG-$TS""#.to_string(),
r#"mkdir -p "$WB""#.to_string(),
format!(r#"cp {} "$WB/prompt.txt""#, shell_quote(&prompt_path)),
];
if let Some(setup) = &agent.setup {
stmts.push(setup.clone());
}
stmts.push(format!(
r#"tmux new-session -d -s "$SESS" -c "$WB" {}"#,
shell_quote(&invocation)
));
stmts.push(r#"tmux pipe-pane -o -t "$SESS" "cat >> \"$WB\"/agent.log""#.to_string());
stmts.join("; ")
}