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::interpreter::Scope;
11use crate::scheduler::{JobManager, PipeReader, PipeWriter, StderrStream};
12use crate::tools::ToolRegistry;
13use crate::vfs::VfsRouter;
14
15use super::traits::ToolSchema;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum OutputContext {
26 #[default]
28 Interactive,
29 Piped,
31 Model,
33 Script,
35}
36
37pub struct ExecContext {
42 pub backend: Arc<dyn KernelBackend>,
47 pub scope: Scope,
49 pub cwd: PathBuf,
51 pub prev_cwd: Option<PathBuf>,
53 pub stdin: Option<String>,
55 pub stdin_data: Option<Value>,
58 pub pipe_stdin: Option<PipeReader>,
60 pub pipe_stdout: Option<PipeWriter>,
62 pub tool_schemas: Vec<ToolSchema>,
64 pub tools: Option<Arc<ToolRegistry>>,
66 pub job_manager: Option<Arc<JobManager>>,
68 pub stderr: Option<StderrStream>,
74 pub pipeline_position: PipelinePosition,
76 pub interactive: bool,
78 pub aliases: HashMap<String, String>,
80 #[cfg(unix)]
82 pub terminal_state: Option<std::sync::Arc<crate::terminal::TerminalState>>,
83}
84
85impl ExecContext {
86 pub fn new(vfs: Arc<VfsRouter>) -> Self {
91 Self {
92 backend: Arc::new(LocalBackend::new(vfs)),
93 scope: Scope::new(),
94 cwd: PathBuf::from("/"),
95 prev_cwd: None,
96 stdin: None,
97 stdin_data: None,
98 pipe_stdin: None,
99 pipe_stdout: None,
100 stderr: None,
101 tool_schemas: Vec::new(),
102 tools: None,
103 job_manager: None,
104 pipeline_position: PipelinePosition::Only,
105 interactive: false,
106 aliases: HashMap::new(),
107 #[cfg(unix)]
108 terminal_state: None,
109 }
110 }
111
112 pub fn with_vfs_and_tools(vfs: Arc<VfsRouter>, tools: Arc<ToolRegistry>) -> Self {
117 Self {
118 backend: Arc::new(LocalBackend::with_tools(vfs, tools.clone())),
119 scope: Scope::new(),
120 cwd: PathBuf::from("/"),
121 prev_cwd: None,
122 stdin: None,
123 stdin_data: None,
124 pipe_stdin: None,
125 pipe_stdout: None,
126 stderr: None,
127 tool_schemas: Vec::new(),
128 tools: Some(tools),
129 job_manager: None,
130 pipeline_position: PipelinePosition::Only,
131 interactive: false,
132 aliases: HashMap::new(),
133 #[cfg(unix)]
134 terminal_state: None,
135 }
136 }
137
138 pub fn with_backend(backend: Arc<dyn KernelBackend>) -> Self {
140 Self {
141 backend,
142 scope: Scope::new(),
143 cwd: PathBuf::from("/"),
144 prev_cwd: None,
145 stdin: None,
146 stdin_data: None,
147 pipe_stdin: None,
148 pipe_stdout: None,
149 stderr: None,
150 tool_schemas: Vec::new(),
151 tools: None,
152 job_manager: None,
153 pipeline_position: PipelinePosition::Only,
154 interactive: false,
155 aliases: HashMap::new(),
156 #[cfg(unix)]
157 terminal_state: None,
158 }
159 }
160
161 pub fn with_vfs_tools_and_scope(vfs: Arc<VfsRouter>, tools: Arc<ToolRegistry>, scope: Scope) -> Self {
163 Self {
164 backend: Arc::new(LocalBackend::with_tools(vfs, tools.clone())),
165 scope,
166 cwd: PathBuf::from("/"),
167 prev_cwd: None,
168 stdin: None,
169 stdin_data: None,
170 pipe_stdin: None,
171 pipe_stdout: None,
172 stderr: None,
173 tool_schemas: Vec::new(),
174 tools: Some(tools),
175 job_manager: None,
176 pipeline_position: PipelinePosition::Only,
177 interactive: false,
178 aliases: HashMap::new(),
179 #[cfg(unix)]
180 terminal_state: None,
181 }
182 }
183
184 pub fn with_scope(vfs: Arc<VfsRouter>, scope: Scope) -> Self {
189 Self {
190 backend: Arc::new(LocalBackend::new(vfs)),
191 scope,
192 cwd: PathBuf::from("/"),
193 prev_cwd: None,
194 stdin: None,
195 stdin_data: None,
196 pipe_stdin: None,
197 pipe_stdout: None,
198 stderr: None,
199 tool_schemas: Vec::new(),
200 tools: None,
201 job_manager: None,
202 pipeline_position: PipelinePosition::Only,
203 interactive: false,
204 aliases: HashMap::new(),
205 #[cfg(unix)]
206 terminal_state: None,
207 }
208 }
209
210 pub fn with_backend_and_scope(backend: Arc<dyn KernelBackend>, scope: Scope) -> Self {
212 Self {
213 backend,
214 scope,
215 cwd: PathBuf::from("/"),
216 prev_cwd: None,
217 stdin: None,
218 stdin_data: None,
219 pipe_stdin: None,
220 pipe_stdout: None,
221 stderr: None,
222 tool_schemas: Vec::new(),
223 tools: None,
224 job_manager: None,
225 pipeline_position: PipelinePosition::Only,
226 interactive: false,
227 aliases: HashMap::new(),
228 #[cfg(unix)]
229 terminal_state: None,
230 }
231 }
232
233 pub fn set_tool_schemas(&mut self, schemas: Vec<ToolSchema>) {
235 self.tool_schemas = schemas;
236 }
237
238 pub fn set_tools(&mut self, tools: Arc<ToolRegistry>) {
240 self.tools = Some(tools);
241 }
242
243 pub fn set_job_manager(&mut self, manager: Arc<JobManager>) {
245 self.job_manager = Some(manager);
246 }
247
248 pub fn set_stdin(&mut self, stdin: String) {
250 self.stdin = Some(stdin);
251 }
252
253 pub fn take_stdin(&mut self) -> Option<String> {
255 self.stdin.take()
256 }
257
258 pub fn set_stdin_with_data(&mut self, text: String, data: Option<Value>) {
263 self.stdin = Some(text);
264 self.stdin_data = data;
265 }
266
267 pub fn take_stdin_data(&mut self) -> Option<Value> {
272 self.stdin_data.take()
273 }
274
275 pub fn resolve_path(&self, path: &str) -> PathBuf {
277 let raw = if path.starts_with('/') {
278 PathBuf::from(path)
279 } else {
280 self.cwd.join(path)
281 };
282 normalize_path(&raw)
283 }
284
285 pub fn set_cwd(&mut self, path: PathBuf) {
289 self.prev_cwd = Some(self.cwd.clone());
290 self.cwd = path;
291 }
292
293 pub fn get_prev_cwd(&self) -> Option<&PathBuf> {
295 self.prev_cwd.as_ref()
296 }
297
298 pub async fn read_stdin_to_string(&mut self) -> Option<String> {
303 if let Some(mut reader) = self.pipe_stdin.take() {
304 use tokio::io::AsyncReadExt;
305 let mut buf = Vec::new();
306 reader.read_to_end(&mut buf).await.ok()?;
307 Some(String::from_utf8_lossy(&buf).into_owned())
308 } else {
309 self.stdin.take()
310 }
311 }
312
313 pub fn child_for_pipeline(&self) -> Self {
318 Self {
319 backend: self.backend.clone(),
320 scope: self.scope.clone(),
321 cwd: self.cwd.clone(),
322 prev_cwd: self.prev_cwd.clone(),
323 stdin: None,
324 stdin_data: None,
325 pipe_stdin: None,
326 pipe_stdout: None,
327 stderr: self.stderr.clone(),
328 tool_schemas: self.tool_schemas.clone(),
329 tools: self.tools.clone(),
330 job_manager: self.job_manager.clone(),
331 pipeline_position: PipelinePosition::Only,
332 interactive: self.interactive,
333 aliases: self.aliases.clone(),
334 #[cfg(unix)]
335 terminal_state: self.terminal_state.clone(),
336 }
337 }
338
339 pub async fn expand_glob(&self, pattern: &str) -> Result<Vec<PathBuf>, String> {
344 use crate::backend_walker_fs::BackendWalkerFs;
345 use crate::walker::{EntryTypes, FileWalker, GlobPath, WalkOptions};
346
347 let glob = GlobPath::new(pattern).map_err(|e| format!("invalid pattern: {}", e))?;
348
349 let root = if glob.is_anchored() {
350 self.resolve_path("/")
351 } else {
352 self.resolve_path(".")
353 };
354
355 let options = WalkOptions {
356 entry_types: EntryTypes::all(),
357 ..WalkOptions::default()
358 };
359
360 let fs = BackendWalkerFs(self.backend.as_ref());
361 let walker = FileWalker::new(&fs, &root)
362 .with_pattern(glob)
363 .with_options(options);
364
365 walker.collect().await.map_err(|e| e.to_string())
366 }
367}
368
369fn normalize_path(path: &std::path::Path) -> PathBuf {
371 let mut parts: Vec<Component> = Vec::new();
372 for component in path.components() {
373 match component {
374 Component::CurDir => {} Component::ParentDir => {
376 if let Some(Component::Normal(_)) = parts.last() {
378 parts.pop();
379 } else {
380 parts.push(component);
381 }
382 }
383 _ => parts.push(component),
384 }
385 }
386 if parts.is_empty() {
387 PathBuf::from("/")
388 } else {
389 parts.iter().collect()
390 }
391}