speccoder_runtime/format/speckit/
workflow.rs1use std::path::{Path, PathBuf};
10
11use speccoder_core::{CommandData, CommandResult, CommandStatus};
12
13pub const FILES: [(&str, &str); 3] = [
15 (
16 ".specify/workflows/steps/speccoder/step.yml",
17 include_str!("../../../assets/workflow/step.yml"),
18 ),
19 (
20 ".specify/workflows/steps/speccoder/__init__.py",
21 include_str!("../../../assets/workflow/__init__.py"),
22 ),
23 (
24 ".specify/workflows/speccoder/workflow.yml",
25 include_str!("../../../assets/workflow/workflow.yml"),
26 ),
27];
28
29#[must_use]
31pub fn install(path: &Path, force: bool) -> CommandResult {
32 let root = match crate::inspect(path, None).data() {
33 Some(CommandData::Inspect(s)) => PathBuf::from(&s.root),
34 _ => {
35 return CommandResult::new(
36 "workflow",
37 CommandStatus::NotAProject,
38 "not a Spec Kit project (no .specify/ found); run `specify init --here --integration claude` first",
39 );
40 }
41 };
42 let existing: Vec<&str> = FILES
43 .iter()
44 .filter(|(p, _)| root.join(p).exists())
45 .map(|(p, _)| *p)
46 .collect();
47 if !existing.is_empty() && !force {
48 return CommandResult::new(
49 "workflow",
50 CommandStatus::ConfigError,
51 &format!("{} exist(s); pass --force to replace", existing.join(", ")),
52 );
53 }
54 for (rel, text) in FILES {
55 let file = root.join(rel);
56 if let Some(dir) = file.parent()
57 && let Err(e) = std::fs::create_dir_all(dir)
58 {
59 return CommandResult::new(
60 "workflow",
61 CommandStatus::Error,
62 &format!("{}: {e}", dir.display()),
63 );
64 }
65 if let Err(e) = std::fs::write(&file, text) {
66 return CommandResult::new(
67 "workflow",
68 CommandStatus::Error,
69 &format!("{}: {e}", file.display()),
70 );
71 }
72 }
73 CommandResult::new(
74 "workflow",
75 CommandStatus::Ok,
76 "installed the `speccoder` step type and workflow; run it with `specify workflow run .specify/workflows/speccoder/workflow.yml --input spec=\"…\"` (resume after a pause with `specify workflow resume <run>`)",
77 )
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn the_assets_declare_the_step_type_and_use_it() {
86 assert!(FILES[0].1.contains("type_key: \"speccoder\""));
87 assert!(
88 FILES[1].1.contains("type_key = \"speccoder\"")
89 && FILES[1].1.contains("NEEDS_HUMAN = 23")
90 );
91 assert!(FILES[2].1.contains("type: speccoder") && FILES[2].1.contains("converge: true"));
92 }
93}