ralph/commands/init/
writers.rs1use crate::contracts::{QueueFile, Task, TaskStatus};
18use crate::fsutil;
19use crate::queue;
20use anyhow::{Context, Result};
21use std::fs;
22use std::path::Path;
23
24use super::FileInitStatus;
25use super::wizard::WizardAnswers;
26
27pub fn write_queue(
29 path: &Path,
30 force: bool,
31 id_prefix: &str,
32 id_width: usize,
33 wizard_answers: Option<&WizardAnswers>,
34) -> Result<FileInitStatus> {
35 if path.exists() && !force {
36 let queue = queue::load_queue(path)?;
38 queue::validate_queue(&queue, id_prefix, id_width)
39 .with_context(|| format!("validate existing queue {}", path.display()))?;
40 return Ok(FileInitStatus::Valid);
41 }
42 if let Some(parent) = path.parent() {
43 fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
44 }
45
46 let mut queue = QueueFile::default();
47
48 if let Some(answers) = wizard_answers
50 && answers.create_first_task
51 && let (Some(title), Some(description)) = (
52 answers.first_task_title.clone(),
53 answers.first_task_description.clone(),
54 )
55 {
56 let now = time::OffsetDateTime::now_utc();
57 let timestamp = now
58 .format(&time::format_description::well_known::Rfc3339)
59 .unwrap_or_else(|_| now.to_string());
60
61 let task_id = format!("{}-{:0>width$}", id_prefix, 1, width = id_width);
62
63 let task = Task {
64 id: task_id,
65 status: TaskStatus::Todo,
66 title,
67 description: None,
68 priority: answers.first_task_priority,
69 tags: vec!["onboarding".to_string()],
70 scope: vec![],
71 evidence: vec![],
72 plan: vec![],
73 notes: vec![],
74 request: Some(description),
75 agent: None,
76 created_at: Some(timestamp.clone()),
77 updated_at: Some(timestamp),
78 completed_at: None,
79 started_at: None,
80 estimated_minutes: None,
81 actual_minutes: None,
82 scheduled_start: None,
83 depends_on: vec![],
84 blocks: vec![],
85 relates_to: vec![],
86 duplicates: None,
87 custom_fields: std::collections::HashMap::new(),
88 parent_id: None,
89 };
90
91 queue.tasks.push(task);
92 }
93
94 let rendered = serde_json::to_string_pretty(&queue).context("serialize queue JSON")?;
95 fsutil::write_atomic(path, rendered.as_bytes())
96 .with_context(|| format!("write queue JSON {}", path.display()))?;
97 Ok(FileInitStatus::Created)
98}
99
100pub fn write_done(
102 path: &Path,
103 force: bool,
104 id_prefix: &str,
105 id_width: usize,
106) -> Result<FileInitStatus> {
107 if path.exists() && !force {
108 let queue = queue::load_queue(path)?;
110 queue::validate_queue(&queue, id_prefix, id_width)
111 .with_context(|| format!("validate existing done {}", path.display()))?;
112 return Ok(FileInitStatus::Valid);
113 }
114 if let Some(parent) = path.parent() {
115 fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
116 }
117 let queue = QueueFile::default();
118 let rendered = serde_json::to_string_pretty(&queue).context("serialize done JSON")?;
119 fsutil::write_atomic(path, rendered.as_bytes())
120 .with_context(|| format!("write done JSON {}", path.display()))?;
121 Ok(FileInitStatus::Created)
122}
123
124pub fn write_config(
126 path: &Path,
127 force: bool,
128 wizard_answers: Option<&WizardAnswers>,
129) -> Result<FileInitStatus> {
130 if path.exists() && !force {
131 crate::config::load_layer(path).with_context(|| {
133 format!(
134 "Config file exists but is invalid JSON/JSONC: {}. Use --force to overwrite.",
135 path.display()
136 )
137 })?;
138 return Ok(FileInitStatus::Valid);
139 }
140 if let Some(parent) = path.parent() {
141 fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
142 }
143
144 let config_json = if let Some(answers) = wizard_answers {
146 let runner_str = format!("{:?}", answers.runner).to_lowercase();
147 let model_str = if answers.model.contains("/") || answers.model.len() > 20 {
148 answers.model.clone()
150 } else {
151 answers.model.clone()
152 };
153
154 serde_json::json!({
155 "version": 2,
156 "agent": {
157 "runner": runner_str,
158 "model": model_str,
159 "phases": answers.phases
160 }
161 })
162 } else {
163 serde_json::json!({ "version": 2 })
164 };
165
166 let rendered = serde_json::to_string_pretty(&config_json).context("serialize config JSON")?;
167 fsutil::write_atomic(path, rendered.as_bytes())
168 .with_context(|| format!("write config JSON {}", path.display()))?;
169 Ok(FileInitStatus::Created)
170}
171
172#[cfg(test)]
173mod tests;