1use std::collections::HashMap;
4use std::path::{Component, PathBuf};
5use std::sync::Arc;
6
7use crate::ast::Value;
8use crate::backend::{KernelBackend, LocalBackend};
9use crate::dispatch::PipelinePosition;
10use crate::ignore_config::IgnoreConfig;
11use crate::interpreter::Scope;
12use crate::nonce::NonceStore;
13use crate::output_limit::OutputLimitConfig;
14use crate::scheduler::{JobManager, PipeReader, PipeWriter, StderrStream};
15use crate::tools::ToolRegistry;
16use crate::vfs::VfsRouter;
17
18use super::traits::ToolSchema;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum OutputContext {
29 #[default]
31 Interactive,
32 Piped,
34 Model,
36 Script,
38}
39
40pub struct ExecContext {
45 pub backend: Arc<dyn KernelBackend>,
50 pub scope: Scope,
52 pub cwd: PathBuf,
54 pub prev_cwd: Option<PathBuf>,
56 pub stdin: Option<String>,
58 pub stdin_data: Option<Value>,
61 pub pipe_stdin: Option<PipeReader>,
63 pub pipe_stdout: Option<PipeWriter>,
65 pub tool_schemas: Vec<ToolSchema>,
67 pub tools: Option<Arc<ToolRegistry>>,
69 pub job_manager: Option<Arc<JobManager>>,
71 pub stderr: Option<StderrStream>,
77 pub pipeline_position: PipelinePosition,
79 pub interactive: bool,
81 pub aliases: HashMap<String, String>,
83 pub ignore_config: IgnoreConfig,
85 pub output_limit: OutputLimitConfig,
87 pub allow_external_commands: bool,
92 pub nonce_store: NonceStore,
97 #[cfg(unix)]
99 pub terminal_state: Option<std::sync::Arc<crate::terminal::TerminalState>>,
100}
101
102impl ExecContext {
103 pub fn new(vfs: Arc<VfsRouter>) -> Self {
108 Self {
109 backend: Arc::new(LocalBackend::new(vfs)),
110 scope: Scope::new(),
111 cwd: PathBuf::from("/"),
112 prev_cwd: None,
113 stdin: None,
114 stdin_data: None,
115 pipe_stdin: None,
116 pipe_stdout: None,
117 stderr: None,
118 tool_schemas: Vec::new(),
119 tools: None,
120 job_manager: None,
121 pipeline_position: PipelinePosition::Only,
122 interactive: false,
123 aliases: HashMap::new(),
124 ignore_config: IgnoreConfig::none(),
125 output_limit: OutputLimitConfig::none(),
126 allow_external_commands: true,
127 nonce_store: NonceStore::new(),
128 #[cfg(unix)]
129 terminal_state: None,
130 }
131 }
132
133 pub fn with_vfs_and_tools(vfs: Arc<VfsRouter>, tools: Arc<ToolRegistry>) -> Self {
138 Self {
139 backend: Arc::new(LocalBackend::with_tools(vfs, tools.clone())),
140 scope: Scope::new(),
141 cwd: PathBuf::from("/"),
142 prev_cwd: None,
143 stdin: None,
144 stdin_data: None,
145 pipe_stdin: None,
146 pipe_stdout: None,
147 stderr: None,
148 tool_schemas: Vec::new(),
149 tools: Some(tools),
150 job_manager: None,
151 pipeline_position: PipelinePosition::Only,
152 interactive: false,
153 aliases: HashMap::new(),
154 ignore_config: IgnoreConfig::none(),
155 output_limit: OutputLimitConfig::none(),
156 allow_external_commands: true,
157 nonce_store: NonceStore::new(),
158 #[cfg(unix)]
159 terminal_state: None,
160 }
161 }
162
163 pub fn with_backend(backend: Arc<dyn KernelBackend>) -> Self {
165 Self {
166 backend,
167 scope: Scope::new(),
168 cwd: PathBuf::from("/"),
169 prev_cwd: None,
170 stdin: None,
171 stdin_data: None,
172 pipe_stdin: None,
173 pipe_stdout: None,
174 stderr: None,
175 tool_schemas: Vec::new(),
176 tools: None,
177 job_manager: None,
178 pipeline_position: PipelinePosition::Only,
179 interactive: false,
180 aliases: HashMap::new(),
181 ignore_config: IgnoreConfig::none(),
182 output_limit: OutputLimitConfig::none(),
183 allow_external_commands: true,
184 nonce_store: NonceStore::new(),
185 #[cfg(unix)]
186 terminal_state: None,
187 }
188 }
189
190 pub fn with_vfs_tools_and_scope(vfs: Arc<VfsRouter>, tools: Arc<ToolRegistry>, scope: Scope) -> Self {
192 Self {
193 backend: Arc::new(LocalBackend::with_tools(vfs, tools.clone())),
194 scope,
195 cwd: PathBuf::from("/"),
196 prev_cwd: None,
197 stdin: None,
198 stdin_data: None,
199 pipe_stdin: None,
200 pipe_stdout: None,
201 stderr: None,
202 tool_schemas: Vec::new(),
203 tools: Some(tools),
204 job_manager: None,
205 pipeline_position: PipelinePosition::Only,
206 interactive: false,
207 aliases: HashMap::new(),
208 ignore_config: IgnoreConfig::none(),
209 output_limit: OutputLimitConfig::none(),
210 allow_external_commands: true,
211 nonce_store: NonceStore::new(),
212 #[cfg(unix)]
213 terminal_state: None,
214 }
215 }
216
217 pub fn with_scope(vfs: Arc<VfsRouter>, scope: Scope) -> Self {
222 Self {
223 backend: Arc::new(LocalBackend::new(vfs)),
224 scope,
225 cwd: PathBuf::from("/"),
226 prev_cwd: None,
227 stdin: None,
228 stdin_data: None,
229 pipe_stdin: None,
230 pipe_stdout: None,
231 stderr: None,
232 tool_schemas: Vec::new(),
233 tools: None,
234 job_manager: None,
235 pipeline_position: PipelinePosition::Only,
236 interactive: false,
237 aliases: HashMap::new(),
238 ignore_config: IgnoreConfig::none(),
239 output_limit: OutputLimitConfig::none(),
240 allow_external_commands: true,
241 nonce_store: NonceStore::new(),
242 #[cfg(unix)]
243 terminal_state: None,
244 }
245 }
246
247 pub fn with_backend_and_scope(backend: Arc<dyn KernelBackend>, scope: Scope) -> Self {
249 Self {
250 backend,
251 scope,
252 cwd: PathBuf::from("/"),
253 prev_cwd: None,
254 stdin: None,
255 stdin_data: None,
256 pipe_stdin: None,
257 pipe_stdout: None,
258 stderr: None,
259 tool_schemas: Vec::new(),
260 tools: None,
261 job_manager: None,
262 pipeline_position: PipelinePosition::Only,
263 interactive: false,
264 aliases: HashMap::new(),
265 ignore_config: IgnoreConfig::none(),
266 output_limit: OutputLimitConfig::none(),
267 allow_external_commands: true,
268 nonce_store: NonceStore::new(),
269 #[cfg(unix)]
270 terminal_state: None,
271 }
272 }
273
274 pub fn set_tool_schemas(&mut self, schemas: Vec<ToolSchema>) {
276 self.tool_schemas = schemas;
277 }
278
279 pub fn set_tools(&mut self, tools: Arc<ToolRegistry>) {
281 self.tools = Some(tools);
282 }
283
284 pub fn set_job_manager(&mut self, manager: Arc<JobManager>) {
286 self.job_manager = Some(manager);
287 }
288
289 pub fn set_stdin(&mut self, stdin: String) {
291 self.stdin = Some(stdin);
292 }
293
294 pub fn take_stdin(&mut self) -> Option<String> {
296 self.stdin.take()
297 }
298
299 pub fn set_stdin_with_data(&mut self, text: String, data: Option<Value>) {
304 self.stdin = Some(text);
305 self.stdin_data = data;
306 }
307
308 pub fn take_stdin_data(&mut self) -> Option<Value> {
313 self.stdin_data.take()
314 }
315
316 pub fn resolve_path(&self, path: &str) -> PathBuf {
318 let raw = if path.starts_with('/') {
319 PathBuf::from(path)
320 } else {
321 self.cwd.join(path)
322 };
323 normalize_path(&raw)
324 }
325
326 pub fn set_cwd(&mut self, path: PathBuf) {
330 self.prev_cwd = Some(self.cwd.clone());
331 self.cwd = path;
332 }
333
334 pub fn get_prev_cwd(&self) -> Option<&PathBuf> {
336 self.prev_cwd.as_ref()
337 }
338
339 pub async fn read_stdin_to_string(&mut self) -> Option<String> {
344 if let Some(mut reader) = self.pipe_stdin.take() {
345 use tokio::io::AsyncReadExt;
346 let mut buf = Vec::new();
347 reader.read_to_end(&mut buf).await.ok()?;
348 Some(String::from_utf8_lossy(&buf).into_owned())
349 } else {
350 self.stdin.take()
351 }
352 }
353
354 pub fn child_for_pipeline(&self) -> Self {
359 Self {
360 backend: self.backend.clone(),
361 scope: self.scope.clone(),
362 cwd: self.cwd.clone(),
363 prev_cwd: self.prev_cwd.clone(),
364 stdin: None,
365 stdin_data: None,
366 pipe_stdin: None,
367 pipe_stdout: None,
368 stderr: self.stderr.clone(),
369 tool_schemas: self.tool_schemas.clone(),
370 tools: self.tools.clone(),
371 job_manager: self.job_manager.clone(),
372 pipeline_position: PipelinePosition::Only,
373 interactive: self.interactive,
374 aliases: self.aliases.clone(),
375 ignore_config: self.ignore_config.clone(),
376 output_limit: self.output_limit.clone(),
377 allow_external_commands: self.allow_external_commands,
378 nonce_store: self.nonce_store.clone(),
379 #[cfg(unix)]
380 terminal_state: self.terminal_state.clone(),
381 }
382 }
383
384 pub async fn build_ignore_filter(&self, root: &std::path::Path) -> Option<crate::walker::IgnoreFilter> {
388 use crate::backend_walker_fs::BackendWalkerFs;
389 let fs = BackendWalkerFs(self.backend.as_ref());
390 self.ignore_config.build_filter(root, &fs).await
391 }
392
393 pub async fn expand_glob(&self, pattern: &str) -> Result<Vec<PathBuf>, String> {
398 use crate::backend_walker_fs::BackendWalkerFs;
399 use crate::walker::{EntryTypes, FileWalker, GlobPath, WalkOptions};
400
401 let glob = GlobPath::new(pattern).map_err(|e| format!("invalid pattern: {}", e))?;
402
403 let root = if glob.is_anchored() {
404 self.resolve_path("/")
405 } else {
406 self.resolve_path(".")
407 };
408
409 let options = WalkOptions {
410 entry_types: EntryTypes::all(),
411 respect_gitignore: self.ignore_config.auto_gitignore(),
412 ..WalkOptions::default()
413 };
414
415 let fs = BackendWalkerFs(self.backend.as_ref());
416 let mut walker = FileWalker::new(&fs, &root)
417 .with_pattern(glob)
418 .with_options(options);
419
420 if let Some(filter) = self.ignore_config.build_filter(&root, &fs).await {
424 walker = walker.with_ignore(filter);
425 }
426
427 walker.collect().await.map_err(|e| e.to_string())
428 }
429}
430
431fn normalize_path(path: &std::path::Path) -> PathBuf {
433 let mut parts: Vec<Component> = Vec::new();
434 for component in path.components() {
435 match component {
436 Component::CurDir => {} Component::ParentDir => {
438 if let Some(Component::Normal(_)) = parts.last() {
440 parts.pop();
441 } else {
442 parts.push(component);
443 }
444 }
445 _ => parts.push(component),
446 }
447 }
448 if parts.is_empty() {
449 PathBuf::from("/")
450 } else {
451 parts.iter().collect()
452 }
453}