use crate::core::shell_escape::{render_command_inline, sh_squote, slugify_identifier};
use crate::core::types::{Resource, TaskMode};
use crate::resources::verdict;
pub const NOT_CONVERGED_MARKER: &str =
"task=not-converged: command exited 0 but completion_check still fails";
fn service_rid(resource: &Resource) -> String {
slugify_identifier(resource.name.as_deref().unwrap_or("task"))
}
pub(crate) fn extract_absolute_binary(cmd: &str) -> Option<&str> {
for token in cmd.split_whitespace() {
if token.contains('=') && !token.starts_with('/') {
continue;
}
if matches!(token, "nohup" | "sudo" | "bash" | "sh" | "env" | "exec") {
continue;
}
if token.starts_with('/') {
return Some(token);
}
break;
}
None
}
pub fn check_script(resource: &Resource) -> String {
if resource.task_mode.as_ref() == Some(&TaskMode::Service) {
let rid = service_rid(resource);
let pidfile = sh_squote(&format!("/tmp/forjar-svc-{rid}.pid"));
let ldd_check = extract_absolute_binary(resource.command.as_deref().unwrap_or(""))
.map(|bin| {
let b = sh_squote(bin);
format!(
"if command -v ldd >/dev/null 2>&1 && [ -f {b} ]; then \
if ldd {b} 2>&1 | grep -q 'not found'; then \
echo 'task=ldd-fail'; exit 1; fi; fi; "
)
})
.unwrap_or_default();
return format!(
"{ldd_check}{}",
verdict::single(
&format!("[ -f {pidfile} ] && kill -0 \"$(cat {pidfile})\" 2>/dev/null"),
"task=completed",
"task=pending",
)
);
}
if let Some(ref check) = resource.completion_check {
return verdict::single(check, "task=completed", "task=pending");
}
if !resource.output_artifacts.is_empty() {
let base = crate::core::task::probe::probe_base_dir(resource);
let assertions: Vec<String> = resource
.output_artifacts
.iter()
.map(|a| {
let resolved = crate::core::task::probe::resolve_under(&base, a);
let path = if base == std::path::Path::new(".") {
a.clone()
} else {
resolved.to_string_lossy().into_owned()
};
verdict::assert_that(
&format!("[ -e {} ]", sh_squote(&path)),
&format!("task=completed:{a}"),
&format!("task=pending:{a}"),
)
})
.collect();
return verdict::check_script_from(&assertions);
}
verdict::check_script_from(&[verdict::always_diverged("task=pending")])
}
fn pipeline_script(resource: &Resource) -> String {
let mut script = String::from("set -euo pipefail\n");
if let Some(ref dir) = resource.working_dir {
script.push_str(&format!("cd {}\n", sh_squote(dir)));
}
script.push_str("FORJAR_PIPELINE_OK=0\n");
for (i, stage) in resource.stages.iter().enumerate() {
let cmd = stage.command.as_deref().unwrap_or("true");
let name = if stage.name.is_empty() {
format!("stage-{i}")
} else {
stage.name.clone()
};
script.push_str(&format!(
"echo {}\n",
sh_squote(&format!("=== Stage: {name} ==="))
));
if stage.gate {
script.push_str(&format!(
"if ! bash -c '{cmd}'; then\n echo {}\n exit 1\nfi\n",
sh_squote(&format!("GATE FAILED: {name}"))
));
} else {
script.push_str(&format!("{cmd}\n"));
}
}
script
}
pub fn apply_script(resource: &Resource) -> String {
if !resource.stages.is_empty() {
return pipeline_script(resource);
}
match resource.task_mode.as_ref().unwrap_or(&TaskMode::Batch) {
TaskMode::Service => return service_script(resource),
TaskMode::Dispatch => return dispatch_script(resource),
TaskMode::Pipeline | TaskMode::Batch => {} }
batch_script(resource)
}
fn batch_script(resource: &Resource) -> String {
let command = resource.command.as_deref().unwrap_or("true");
let mut script = String::from("set -euo pipefail\n");
if let Some(scatter) = scatter_script(resource) {
script.push_str(&scatter);
}
if let Some(ref dir) = resource.working_dir {
script.push_str(&format!("cd {}\n", sh_squote(dir)));
}
if let Some(timeout_secs) = resource.timeout {
script.push_str(&format!(
"timeout {timeout_secs} bash <<'FORJAR_TIMEOUT'\n{command}\nFORJAR_TIMEOUT\n"
));
} else {
script.push_str(command);
script.push('\n');
}
if let Some(gather) = gather_script(resource) {
script.push_str(&gather);
}
if let Some(ref check) = resource.completion_check {
script.push_str("if ! {\n");
script.push_str(check.trim_end());
script.push_str("\n}\nthen\n");
script.push_str(&format!(" echo '{NOT_CONVERGED_MARKER}' >&2\n"));
script.push_str(" echo 'task=not-converged: the declared state was not reached' >&2\n");
script.push_str(" exit 1\n");
script.push_str("fi\n");
}
script
}
fn service_script(resource: &Resource) -> String {
let command = resource.command.as_deref().unwrap_or("true");
let rid = service_rid(resource);
let pidfile = sh_squote(&format!("/tmp/forjar-svc-{rid}.pid"));
let logfile = sh_squote(&format!("/tmp/forjar-svc-{rid}.log"));
let mut script = String::from("set -euo pipefail\n");
if let Some(ref dir) = resource.working_dir {
script.push_str(&format!("cd {}\n", sh_squote(dir)));
}
script.push_str(&format!(
"if [ -f {pidfile} ] && kill -0 \"$(cat {pidfile})\" 2>/dev/null; then\n\
\x20 echo 'service={rid} already running (pid='\"$(cat {pidfile})\"')'\n\
\x20 exit 0\nfi\n"
));
script.push_str(&format!(
"nohup bash -c '{command}' > {logfile} 2>&1 &\n\
FORJAR_SVC_PID=$!\n\
echo $FORJAR_SVC_PID > {pidfile}\n\
echo 'service={rid} started (pid='$FORJAR_SVC_PID')'\n"
));
if let Some(ref hc) = resource.health_check {
let timeout = hc
.timeout
.as_deref()
.and_then(|t| t.strip_suffix('s'))
.unwrap_or("5");
let retries = hc.retries.unwrap_or(3);
script.push_str(&format!(
"sleep 1\nfor _i in $(seq 1 {retries}); do\n\
\x20 if ! kill -0 \"$FORJAR_SVC_PID\" 2>/dev/null; then\n\
\x20\x20\x20 echo 'service={rid} DIED during startup (pid='$FORJAR_SVC_PID')'\n\
\x20\x20\x20 tail -20 {logfile} 2>/dev/null || true\n\
\x20\x20\x20 rm -f {pidfile}\n\
\x20\x20\x20 exit 1\n\
\x20 fi\n\
\x20 if timeout {timeout} bash -c '{}'; then\n\
\x20\x20\x20 echo 'service={rid} healthy'\n\
\x20\x20\x20 exit 0\n\
\x20 fi\n\
\x20 sleep 1\ndone\n\
echo 'service={rid} started but health check pending'\n",
hc.command
));
}
script
}
fn dispatch_script(resource: &Resource) -> String {
let command = resource.command.as_deref().unwrap_or("true");
let mut script = String::from("set -euo pipefail\n");
if let Some(ref dir) = resource.working_dir {
script.push_str(&format!("cd {}\n", sh_squote(dir)));
}
if let Some(ref gate) = resource.quality_gate {
if let Some(ref gate_cmd) = gate.command {
let msg = gate
.message
.as_deref()
.unwrap_or("dispatch gate check failed");
script.push_str(&format!(
"if ! bash -c '{gate_cmd}'; then\n\
\x20 echo {}\n\
\x20 exit 1\nfi\n",
sh_squote(&format!("DISPATCH BLOCKED: {msg}"))
));
}
}
if let Some(scatter) = scatter_script(resource) {
script.push_str(&scatter);
}
if let Some(timeout_secs) = resource.timeout {
script.push_str(&format!(
"timeout {timeout_secs} bash <<'FORJAR_TIMEOUT'\n{command}\nFORJAR_TIMEOUT\n"
));
} else {
script.push_str(command);
script.push('\n');
}
if let Some(gather) = gather_script(resource) {
script.push_str(&gather);
}
script
}
pub fn state_query_script(resource: &Resource) -> String {
if !resource.output_artifacts.is_empty() {
let hash_cmds: Vec<String> = resource
.output_artifacts
.iter()
.map(|a| {
let q = sh_squote(a);
format!(
"[ -f {q} ] && b3sum {q} 2>/dev/null || echo {}",
sh_squote(&format!("missing:{a}"))
)
})
.collect();
return hash_cmds.join("\n");
}
if let Some(ref check) = resource.completion_check {
return verdict::single(check, "task=completed", "task=pending");
}
let command = resource.command.as_deref().unwrap_or("true");
format!(
"printf '%s\\n' {}",
sh_squote(&format!(
"unobservable:no-completion-check:{}",
render_command_inline(command)
))
)
}
pub fn scatter_script(resource: &Resource) -> Option<String> {
if resource.scatter.is_empty() {
return None;
}
let mut script = String::from("set -euo pipefail\n# FJ-2704: scatter artifacts\n");
for mapping in &resource.scatter {
if let Some((local, remote)) = mapping.split_once(':') {
let (l, r) = (sh_squote(local), sh_squote(remote));
script.push_str(&format!("mkdir -p \"$(dirname {r})\"\ncp -r {l} {r}\n"));
}
}
Some(script)
}
pub fn gather_script(resource: &Resource) -> Option<String> {
if resource.gather.is_empty() {
return None;
}
let mut script = String::from("set -euo pipefail\n# FJ-2704: gather artifacts\n");
for mapping in &resource.gather {
if let Some((remote, local)) = mapping.split_once(':') {
let (r, l) = (sh_squote(remote), sh_squote(local));
script.push_str(&format!("mkdir -p \"$(dirname {l})\"\ncp -r {r} {l}\n"));
}
}
Some(script)
}