1use crate::cli::args::{GlobalFlags, SyncArgs};
18use anyhow::Result;
19use grex_core::sync::{self, HaltedContext, SyncError, SyncOptions, SyncReport, SyncStep};
20use tokio_util::sync::CancellationToken;
21
22pub fn run(args: SyncArgs, global: &GlobalFlags, cancel: &CancellationToken) -> Result<()> {
30 let Some(pack_root) = args.pack_root.clone() else {
31 if global.json {
37 emit_json_error(
38 "usage",
39 "`<pack_root>` is required (directory with `.grex/pack.yaml` or the YAML file)",
40 "sync",
41 );
42 } else {
43 eprintln!(
44 "grex sync: <pack_root> required (directory with `.grex/pack.yaml` or the YAML file)"
45 );
46 }
47 std::process::exit(2);
48 };
49 let dry_run = args.dry_run || global.dry_run;
50 let only_patterns = if args.only.is_empty() { None } else { Some(args.only.clone()) };
51 let opts = SyncOptions::new()
52 .with_dry_run(dry_run)
53 .with_validate(!args.no_validate)
54 .with_workspace(args.workspace.clone())
55 .with_ref_override(args.ref_override.clone())
56 .with_only_patterns(only_patterns)
57 .with_force(args.force);
58 match run_impl(&pack_root, &opts, args.quiet, global.json, cancel) {
59 RunOutcome::Ok => Ok(()),
60 RunOutcome::UsageError => std::process::exit(2),
61 RunOutcome::Validation => std::process::exit(1),
62 RunOutcome::Exec => std::process::exit(2),
63 RunOutcome::Tree => std::process::exit(3),
64 }
65}
66
67pub(super) enum RunOutcome {
68 Ok,
69 UsageError,
72 Validation,
73 Exec,
74 Tree,
75}
76
77fn run_impl(
78 pack_root: &std::path::Path,
79 opts: &SyncOptions,
80 quiet: bool,
81 json: bool,
82 cancel: &CancellationToken,
83) -> RunOutcome {
84 match sync::run(pack_root, opts, cancel) {
85 Ok(report) => {
86 if json {
87 emit_json_report(&report, opts.dry_run, "sync");
88 } else {
89 render_report(&report, opts.dry_run, quiet);
90 }
91 if report.halted.is_some() {
92 return RunOutcome::Exec;
93 }
94 RunOutcome::Ok
95 }
96 Err(err) => classify_sync_err(err, json, "sync"),
97 }
98}
99
100pub(super) fn classify_sync_err(err: SyncError, json: bool, verb: &str) -> RunOutcome {
105 match err {
106 SyncError::Validation { errors } => {
107 emit_validation(&errors, json, verb);
108 RunOutcome::Validation
109 }
110 SyncError::Tree(e) => {
111 emit_simple("tree", &e.to_string(), "tree walk failed", json, verb);
112 RunOutcome::Tree
113 }
114 SyncError::Exec(e) => {
115 emit_simple("exec", &e.to_string(), "execution error", json, verb);
116 RunOutcome::Exec
117 }
118 SyncError::Halted(ctx) => {
119 if json {
120 emit_json_halted(&ctx, verb);
121 } else {
122 print_halted_context(&ctx);
123 }
124 RunOutcome::Exec
125 }
126 SyncError::InvalidOnlyGlob { pattern, source } => {
127 let msg = format!("invalid --only glob `{pattern}`: {source}");
128 emit_simple("usage", &msg, "error", json, verb);
129 RunOutcome::UsageError
130 }
131 other => {
132 emit_simple("other", &other.to_string(), &format!("{verb} failed"), json, verb);
133 RunOutcome::Tree
134 }
135 }
136}
137
138fn emit_validation(errors: &[impl std::fmt::Display], json: bool, verb: &str) {
139 if json {
140 let joined = errors.iter().map(|e| e.to_string()).collect::<Vec<_>>().join("; ");
141 emit_json_error("validation", &joined, verb);
142 } else {
143 eprintln!("validation failed:");
144 for e in errors {
145 eprintln!(" - {e}");
146 }
147 }
148}
149
150fn emit_simple(kind: &str, message: &str, human_prefix: &str, json: bool, verb: &str) {
151 if json {
152 emit_json_error(kind, message, verb);
153 } else {
154 eprintln!("{human_prefix}: {message}");
155 }
156}
157
158pub(super) fn emit_json_report(report: &SyncReport, dry_run: bool, verb: &str) {
162 let steps: Vec<serde_json::Value> =
163 report.steps.iter().map(|s| step_to_json(s, dry_run)).collect();
164 let halted = report.halted.as_ref().and_then(|h| match h {
165 SyncError::Halted(ctx) => Some(serde_json::json!({
166 "pack": ctx.pack,
167 "action": ctx.action_name,
168 "idx": ctx.action_idx,
169 "error": ctx.error.to_string(),
170 "recovery_hint": ctx.recovery_hint,
171 })),
172 _ => None,
173 });
174 let doc = serde_json::json!({
175 "verb": verb,
176 "dry_run": dry_run,
177 "steps": steps,
178 "halted": halted,
179 "event_log_warnings": report.event_log_warnings,
180 "summary": {"total_steps": report.steps.len()},
181 });
182 if let Ok(s) = serde_json::to_string(&doc) {
183 println!("{s}");
184 }
185}
186
187fn step_to_json(s: &SyncStep, dry_run: bool) -> serde_json::Value {
188 use grex_core::ExecResult;
189 let (result, details) = match &s.exec_step.result {
190 ExecResult::PerformedChange => ("performed_change", serde_json::Value::Null),
191 ExecResult::WouldPerformChange => {
192 if dry_run {
193 ("would_perform_change", serde_json::Value::Null)
194 } else {
195 ("performed_change", serde_json::Value::Null)
196 }
197 }
198 ExecResult::AlreadySatisfied => ("already_satisfied", serde_json::Value::Null),
199 ExecResult::NoOp => ("noop", serde_json::Value::Null),
200 ExecResult::Skipped { pack_path, actions_hash, .. } => (
201 "skipped",
202 serde_json::json!({
203 "pack_path": pack_path.display().to_string(),
204 "actions_hash": actions_hash,
205 }),
206 ),
207 _ => ("other", serde_json::Value::Null),
208 };
209 serde_json::json!({
210 "pack": s.pack,
211 "action": s.exec_step.action_name,
212 "idx": s.action_idx,
213 "result": result,
214 "details": details,
215 })
216}
217
218pub(super) fn emit_json_error(kind: &str, message: &str, verb: &str) {
219 let doc = serde_json::json!({
220 "verb": verb,
221 "error": {
222 "kind": kind,
223 "message": message,
224 },
225 });
226 if let Ok(s) = serde_json::to_string(&doc) {
227 println!("{s}");
228 }
229}
230
231pub(super) fn emit_json_halted(ctx: &HaltedContext, verb: &str) {
232 let doc = serde_json::json!({
233 "verb": verb,
234 "halted": {
235 "pack": ctx.pack,
236 "action": ctx.action_name,
237 "idx": ctx.action_idx,
238 "error": ctx.error.to_string(),
239 "recovery_hint": ctx.recovery_hint,
240 },
241 });
242 if let Ok(s) = serde_json::to_string(&doc) {
243 println!("{s}");
244 }
245}
246
247fn print_halted_context(ctx: &HaltedContext) {
250 eprintln!(
251 "sync halted at pack `{}` action #{} ({}):",
252 ctx.pack, ctx.action_idx, ctx.action_name
253 );
254 eprintln!(" error: {}", ctx.error);
255 if let Some(hint) = &ctx.recovery_hint {
256 eprintln!(" hint: {hint}");
257 }
258}
259
260fn render_report(report: &SyncReport, dry_run: bool, quiet: bool) {
261 if !quiet {
262 if let Some(rec) = &report.pre_run_recovery {
263 print_recovery_report(rec);
264 }
265 for s in &report.steps {
266 print_step(s, dry_run);
267 }
268 }
269 for w in &report.event_log_warnings {
270 eprintln!("warning: {w}");
271 }
272 if let Some(err) = &report.halted {
273 match err {
274 SyncError::Halted(ctx) => print_halted_context(ctx),
275 other => eprintln!("halted: {other}"),
276 }
277 }
278}
279
280fn print_recovery_report(rec: &grex_core::sync::RecoveryReport) {
283 let total = rec.orphan_backups.len() + rec.orphan_tombstones.len() + rec.dangling_starts.len();
284 if total == 0 {
285 return;
286 }
287 eprintln!("warning: pre-run recovery scan found {total} artifact(s) from prior sync:");
288 for p in &rec.orphan_backups {
289 eprintln!(" orphan backup: {}", p.display());
290 }
291 for p in &rec.orphan_tombstones {
292 eprintln!(" orphan tombstone: {}", p.display());
293 }
294 for d in &rec.dangling_starts {
295 eprintln!(
296 " dangling start: pack `{}` action #{} ({}) at {}",
297 d.pack, d.action_idx, d.action_name, d.started_at
298 );
299 }
300}
301
302fn print_step(s: &SyncStep, dry_run: bool) {
303 use grex_core::ExecResult;
304 if let ExecResult::Skipped { pack_path, actions_hash, .. } = &s.exec_step.result {
313 println!(
314 "[skipped] pack={pack} path={path} hash={hash}",
315 pack = s.pack,
316 path = pack_path.display(),
317 hash = actions_hash,
318 );
319 return;
320 }
321 let tag = match (&s.exec_step.result, dry_run) {
322 (ExecResult::PerformedChange, _) => "ok",
323 (ExecResult::WouldPerformChange, true) => "would",
324 (ExecResult::WouldPerformChange, false) => "ok",
325 (ExecResult::AlreadySatisfied, _) => "skipped",
326 (ExecResult::NoOp, _) => "noop",
327 _ => "other",
330 };
331 println!(
332 "[{tag}] pack={pack} action={kind} idx={idx}",
333 pack = s.pack,
334 kind = s.exec_step.action_name,
335 idx = s.action_idx,
336 );
337}
338
339