use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde::Serialize;
use zag_agent::builder::AgentBuilder;
use crate::create::{get_zag_help, get_zag_orch_reference};
use crate::error::ZigError;
use crate::pack;
use crate::prompt;
use crate::run;
use crate::workflow::{parser, validate};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum WorkflowKind {
Plain,
Zipped,
}
impl WorkflowKind {
fn from_path(path: &Path) -> Self {
match path.extension().and_then(|s| s.to_str()) {
Some("zwfz") => WorkflowKind::Zipped,
_ => WorkflowKind::Plain,
}
}
}
#[derive(Debug, Serialize)]
pub struct UpdateParams {
pub system_prompt: String,
pub initial_prompt: String,
pub original_path: PathBuf,
pub staging_path: PathBuf,
pub kind: WorkflowKind,
pub session_name: String,
pub session_tag: String,
#[serde(skip)]
pub _staging_dir: tempfile::TempDir,
}
fn build_system_prompt(zag_help: &str, zag_orch: &str, examples_reference: &str) -> String {
let vars = HashMap::from([
("zwf_format_spec", prompt::templates::config_sidecar()),
("zag_help", zag_help),
("zag_orch", zag_orch),
("examples_reference", examples_reference),
]);
prompt::render(prompt::templates::update(), &vars)
}
pub fn prepare_update(workflow: &str) -> Result<UpdateParams, ZigError> {
if let Err(e) = prompt::write_examples_to_global_dir() {
eprintln!("Warning: could not write example files: {e}");
}
let original_path = run::resolve_workflow_path(workflow)?;
let kind = WorkflowKind::from_path(&original_path);
let staging_dir = tempfile::TempDir::new()
.map_err(|e| ZigError::Io(format!("failed to create staging directory: {e}")))?;
let staging_path = match kind {
WorkflowKind::Plain => {
let file_name = original_path
.file_name()
.ok_or_else(|| ZigError::Io("workflow path has no file name".into()))?;
let dest = staging_dir.path().join(file_name);
std::fs::copy(&original_path, &dest).map_err(|e| {
ZigError::Io(format!(
"failed to copy {} to staging: {e}",
original_path.display()
))
})?;
dest
}
WorkflowKind::Zipped => {
parser::extract_zip(&original_path, staging_dir.path())?;
let toml_files = parser::find_workflow_files(staging_dir.path())?;
match toml_files.len() {
0 => {
return Err(ZigError::Parse(
"archive contains no .toml or .zwf workflow file".into(),
));
}
1 => toml_files.into_iter().next().unwrap(),
n => {
return Err(ZigError::Parse(format!(
"archive contains {n} workflow files (expected exactly one)"
)));
}
}
}
};
let parsed = parser::parse_file(&staging_path)?;
let validation_report = match validate::validate(&parsed) {
Ok(()) => "Validation: no issues found.".to_string(),
Err(errors) => {
let mut report = format!(
"Validation: {} issue{} found:",
errors.len(),
if errors.len() == 1 { "" } else { "s" },
);
for e in &errors {
report.push_str("\n - ");
report.push_str(&e.to_string());
}
report
}
};
let zag_help = get_zag_help();
let zag_orch = get_zag_orch_reference();
let examples_reference = prompt::examples_reference_block();
let system_prompt = build_system_prompt(&zag_help, &zag_orch, &examples_reference);
let initial_prompt = format!(
"I want to update the workflow file at `{}`. Please read it first, \
then help me make the changes I describe. Edit the file in place at \
that exact path — do not rename, move, or copy it.\n\n\
Here is the current validation report for that workflow:\n\n{}\n\n\
Use this report as a starting point: mention any issues while you \
summarize the workflow, but do not start fixing anything yet. Wait \
for me to explicitly tell you what to change before editing the file.",
staging_path.display(),
validation_report,
);
Ok(UpdateParams {
system_prompt,
initial_prompt,
original_path,
staging_path,
kind,
session_name: "zig-update".to_string(),
session_tag: "zig-workflow-update".to_string(),
_staging_dir: staging_dir,
})
}
pub async fn run_update(workflow: &str) -> Result<(), ZigError> {
let params = prepare_update(workflow)?;
AgentBuilder::new()
.system_prompt(¶ms.system_prompt)
.name(¶ms.session_name)
.tag(¶ms.session_tag)
.run(Some(¶ms.initial_prompt))
.await
.map_err(|e| ZigError::Zag(format!("failed to run agent: {e}")))?;
if params.staging_path.exists() {
match parser::parse_file(¶ms.staging_path) {
Ok(workflow) => {
if let Err(errors) = crate::workflow::validate::validate(&workflow) {
eprintln!("Warning: updated workflow has validation issues:");
for e in &errors {
eprintln!(" - {e}");
}
}
}
Err(e) => {
eprintln!("Warning: could not parse updated file: {e}");
}
}
} else {
return Err(ZigError::Io(format!(
"expected updated workflow at {} but the file is missing — \
did the agent move or rename it?",
params.staging_path.display()
)));
}
commit_update(¶ms)?;
println!("updated {}", params.original_path.display());
Ok(())
}
fn commit_update(params: &UpdateParams) -> Result<(), ZigError> {
match params.kind {
WorkflowKind::Plain => {
let tmp = sibling_temp_path(¶ms.original_path)?;
std::fs::copy(¶ms.staging_path, &tmp).map_err(|e| {
ZigError::Io(format!(
"failed to write updated workflow to {}: {e}",
tmp.display()
))
})?;
std::fs::rename(&tmp, ¶ms.original_path).map_err(|e| {
ZigError::Io(format!(
"failed to replace {}: {e}",
params.original_path.display()
))
})?;
}
WorkflowKind::Zipped => {
let tmp = sibling_temp_path(¶ms.original_path)?;
pack::zip_directory(params._staging_dir.path(), &tmp)?;
std::fs::rename(&tmp, ¶ms.original_path).map_err(|e| {
ZigError::Io(format!(
"failed to replace {}: {e}",
params.original_path.display()
))
})?;
}
}
Ok(())
}
fn sibling_temp_path(target: &Path) -> Result<PathBuf, ZigError> {
let parent = target
.parent()
.ok_or_else(|| ZigError::Io("workflow path has no parent directory".into()))?;
let file_name = target
.file_name()
.ok_or_else(|| ZigError::Io("workflow path has no file name".into()))?
.to_string_lossy();
let pid = std::process::id();
Ok(parent.join(format!(".{file_name}.update.{pid}.tmp")))
}
#[cfg(test)]
#[path = "update_tests.rs"]
mod tests;