1use crate::args::Cli;
4use crate::commands::index::{
5 ClasspathCliOptions, build_and_persist_with_optional_classpath, create_build_config,
6 create_progress_reporter,
7};
8#[cfg(feature = "jvm-classpath")]
9use crate::commands::index::{inject_classpath_into_graph, run_classpath_pipeline_only};
10use crate::plugin_defaults::{self, PluginSelectionMode};
11use anyhow::{Context, Result};
12use sqry_core::graph::unified::persistence::GraphStorage;
13#[cfg(feature = "jvm-classpath")]
14use sqry_core::watch::FileChange;
15use sqry_core::watch::FileWatcher;
16use std::path::PathBuf;
17use std::time::Duration;
18
19#[allow(clippy::too_many_arguments)]
24#[allow(clippy::too_many_lines)]
25#[allow(clippy::fn_params_excessive_bools)] #[allow(clippy::needless_pass_by_value)] pub fn execute(
28 cli: &Cli,
29 path: Option<String>,
30 threads: Option<usize>,
31 debounce: Option<u64>,
32 _show_stats: bool, build_if_missing: bool,
34 classpath: bool,
35 _no_classpath: bool,
36 classpath_depth: crate::args::ClasspathDepthArg,
37 classpath_file: Option<PathBuf>,
38 build_system: Option<String>,
39 force_classpath: bool,
40) -> Result<()> {
41 let root_path = resolve_path(path)?;
42 let build_config = create_build_config(cli, &root_path, threads)?;
43 let classpath_opts = ClasspathCliOptions {
44 enabled: classpath,
45 depth: classpath_depth,
46 classpath_file: classpath_file.as_deref(),
47 build_system: build_system.as_deref(),
48 force_classpath,
49 };
50 #[cfg(feature = "jvm-classpath")]
51 let mut classpath_cache = None;
52
53 let storage = GraphStorage::new(&root_path);
55 if !storage.exists() {
56 if build_if_missing {
57 println!("🔨 Building initial graph...");
58 let (_, progress) = create_progress_reporter(cli);
59 let resolved_plugins = plugin_defaults::resolve_plugin_selection(
60 cli,
61 &root_path,
62 PluginSelectionMode::FreshWrite,
63 )?;
64 #[cfg(feature = "jvm-classpath")]
65 {
66 let _build_result = build_and_persist_watch_iteration(
67 &root_path,
68 &resolved_plugins,
69 &build_config,
70 "cli:watch",
71 progress,
72 Some(&classpath_opts),
73 &mut classpath_cache,
74 &[],
75 )?;
76 }
77 #[cfg(not(feature = "jvm-classpath"))]
78 {
79 let _build_result = build_and_persist_with_optional_classpath(
80 &root_path,
81 &resolved_plugins,
82 &build_config,
83 "cli:watch",
84 progress,
85 Some(&classpath_opts),
86 )?;
87 }
88 } else {
89 anyhow::bail!(
90 "No index found at {}. Use --build to create one, or run 'sqry index' first.",
91 root_path.display()
92 );
93 }
94 }
95
96 let watcher = FileWatcher::new(&root_path)?;
98 let debounce_duration = debounce.map_or(Duration::from_millis(500), Duration::from_millis);
99
100 println!("🔍 Watch mode started");
101 println!("📂 Monitoring: {}", root_path.display());
102 println!("⏱️ Debounce: {}ms", debounce_duration.as_millis());
103 println!();
104 println!("Press Ctrl+C to stop...");
105 println!();
106
107 let (_, progress) = create_progress_reporter(cli);
108
109 loop {
110 let changes = watcher.wait_with_debounce(debounce_duration)?;
112
113 if changes.is_empty() {
114 continue;
115 }
116
117 println!(
118 "📝 Detected {} file changes, updating graph...",
119 changes.len()
120 );
121 let start = std::time::Instant::now();
122
123 let resolved_plugins = plugin_defaults::resolve_plugin_selection(
125 cli,
126 &root_path,
127 PluginSelectionMode::ExistingWrite,
128 )?;
129 #[cfg(feature = "jvm-classpath")]
130 let build_result = build_and_persist_watch_iteration(
131 &root_path,
132 &resolved_plugins,
133 &build_config,
134 "cli:watch",
135 progress.clone(),
136 Some(&classpath_opts),
137 &mut classpath_cache,
138 &changes,
139 );
140 #[cfg(not(feature = "jvm-classpath"))]
141 let build_result = build_and_persist_with_optional_classpath(
142 &root_path,
143 &resolved_plugins,
144 &build_config,
145 "cli:watch",
146 progress.clone(),
147 Some(&classpath_opts),
148 );
149 match build_result {
150 Ok(_build_result) => {
151 println!("✓ Graph updated in {:.2}s", start.elapsed().as_secs_f64());
152 }
153 Err(e) => {
154 eprintln!("❌ Error updating graph: {e}");
155 }
156 }
157 println!();
158 }
159}
160
161#[cfg(feature = "jvm-classpath")]
162const CLASSPATH_INVALIDATION_FILE_NAMES: &[&str] = &[
163 "build.gradle",
164 "build.gradle.kts",
165 "gradle.properties",
166 "settings.gradle",
167 "settings.gradle.kts",
168 "pom.xml",
169 "build.sbt",
170 "WORKSPACE",
171 "WORKSPACE.bazel",
172 "MODULE.bazel",
173 "gradle-wrapper.properties",
174];
175
176#[cfg(feature = "jvm-classpath")]
177fn classpath_inputs_changed(
178 root_path: &std::path::Path,
179 changes: &[FileChange],
180 classpath_opts: &ClasspathCliOptions<'_>,
181) -> bool {
182 if classpath_opts.force_classpath {
183 return true;
184 }
185
186 let manual_classpath = classpath_opts.classpath_file.map(|path| {
187 if path.is_absolute() {
188 path.to_path_buf()
189 } else {
190 root_path.join(path)
191 }
192 });
193
194 changes.iter().any(|change| {
195 let path = match change {
196 FileChange::Created(path) | FileChange::Modified(path) | FileChange::Deleted(path) => {
197 path
198 }
199 };
200
201 if manual_classpath
202 .as_ref()
203 .is_some_and(|cp_file| path == cp_file)
204 {
205 return true;
206 }
207
208 path.file_name()
209 .and_then(|name| name.to_str())
210 .is_some_and(|name| CLASSPATH_INVALIDATION_FILE_NAMES.contains(&name))
211 })
212}
213
214#[cfg(feature = "jvm-classpath")]
215fn build_and_persist_watch_iteration(
216 root_path: &std::path::Path,
217 resolved_plugins: &crate::plugin_defaults::ResolvedPluginManager,
218 build_config: &sqry_core::graph::unified::build::BuildConfig,
219 build_command: &str,
220 progress: sqry_core::progress::SharedReporter,
221 classpath_opts: Option<&ClasspathCliOptions<'_>>,
222 classpath_cache: &mut Option<sqry_classpath::pipeline::ClasspathPipelineResult>,
223 changes: &[FileChange],
224) -> Result<sqry_core::graph::unified::build::BuildResult> {
225 if let Some(classpath_opts) = classpath_opts.filter(|opts| opts.enabled) {
226 let should_refresh = classpath_cache.is_none()
227 || classpath_inputs_changed(root_path, changes, classpath_opts);
228 if should_refresh {
229 *classpath_cache = Some(run_classpath_pipeline_only(root_path, classpath_opts)?);
230 }
231
232 let (mut graph, effective_threads) =
233 sqry_core::graph::unified::build::build_unified_graph_with_progress(
234 root_path,
235 &resolved_plugins.plugin_manager,
236 build_config,
237 progress.clone(),
238 )?;
239
240 if let Some(classpath_result) = classpath_cache.as_ref() {
241 inject_classpath_into_graph(&mut graph, classpath_result)?;
242 }
243
244 let (_graph, build_result) = sqry_core::graph::unified::build::persist_and_analyze_graph(
245 graph,
246 root_path,
247 &resolved_plugins.plugin_manager,
248 build_config,
249 build_command,
250 resolved_plugins.persisted_selection.clone(),
251 progress,
252 effective_threads,
253 )?;
254 return Ok(build_result);
255 }
256
257 build_and_persist_with_optional_classpath(
258 root_path,
259 resolved_plugins,
260 build_config,
261 build_command,
262 progress,
263 None,
264 )
265}
266
267fn resolve_path(path: Option<String>) -> Result<PathBuf> {
269 let path_str = path.unwrap_or_else(|| ".".to_string());
270 let path = PathBuf::from(path_str);
271
272 if path.exists() {
273 path.canonicalize().context("Failed to resolve path")
274 } else {
275 anyhow::bail!("Path does not exist: {}", path.display());
276 }
277}
278
279#[cfg(all(test, feature = "jvm-classpath"))]
280mod tests {
281 use super::*;
282
283 fn classpath_opts<'a>(classpath_file: Option<&'a std::path::Path>) -> ClasspathCliOptions<'a> {
284 ClasspathCliOptions {
285 enabled: true,
286 depth: crate::args::ClasspathDepthArg::Full,
287 classpath_file,
288 build_system: None,
289 force_classpath: false,
290 }
291 }
292
293 #[test]
294 fn classpath_invalidation_includes_gradle_property_files() {
295 let root = std::path::Path::new("/repo");
296 let changes = vec![
297 FileChange::Modified(root.join("gradle.properties")),
298 FileChange::Modified(root.join("gradle/wrapper/gradle-wrapper.properties")),
299 ];
300
301 assert!(
302 classpath_inputs_changed(root, &changes[..1], &classpath_opts(None)),
303 "gradle.properties should invalidate the classpath cache"
304 );
305 assert!(
306 classpath_inputs_changed(root, &changes[1..], &classpath_opts(None)),
307 "gradle-wrapper.properties should invalidate the classpath cache"
308 );
309 }
310
311 #[test]
312 fn classpath_invalidation_ignores_regular_source_files() {
313 let root = std::path::Path::new("/repo");
314 let changes = vec![FileChange::Modified(root.join("src/Main.java"))];
315 assert!(
316 !classpath_inputs_changed(root, &changes, &classpath_opts(None)),
317 "ordinary source edits should reuse the cached classpath result"
318 );
319 }
320}