1use crate::config::Config;
9use crate::env::{self, AgentEnv};
10use crate::exec::classify::{classify, Classified};
11use crate::exec::spawn::{CommandRunner, RealRunner, SpawnSpec};
12use crate::exec::{path as exec_path, resolve, ExecMode, Family, PassthroughReason};
13use crate::paths::CacheLayout;
14use crate::reduce;
15use crate::store::{write_logs, Db, RunRecord, StoredLogs};
16use crate::{repo, state, util};
17use std::ffi::{OsStr, OsString};
18use std::io::Write;
19use std::panic::AssertUnwindSafe;
20use std::path::{Path, PathBuf};
21use std::time::Instant;
22
23struct EmitPlan {
25 stdout: Vec<u8>,
26 stderr: Vec<u8>,
27}
28
29impl EmitPlan {
30 fn raw(outcome: &crate::exec::ExecOutcome) -> EmitPlan {
31 EmitPlan {
32 stdout: outcome.stdout.clone(),
33 stderr: outcome.stderr.clone(),
34 }
35 }
36 fn write(self) {
37 let _ = std::io::stdout().write_all(&self.stdout);
38 let _ = std::io::stdout().flush();
39 let _ = std::io::stderr().write_all(&self.stderr);
40 let _ = std::io::stderr().flush();
41 }
42}
43
44struct RunCtx {
46 repo_root: PathBuf,
47 cwd: PathBuf,
48 layout: CacheLayout,
49 config: Config,
50 session_id: String,
51}
52
53impl RunCtx {
54 fn resolve(
55 cwd: &Path,
56 agent: Option<&AgentEnv>,
57 git: &repo::GitInvoker,
58 ) -> anyhow::Result<RunCtx> {
59 let (repo_root, layout, session_id) = if let Some(a) = agent {
60 (
61 a.repo_root.clone(),
62 CacheLayout::from_dir(a.cache_dir.clone()),
63 a.session_id.clone(),
64 )
65 } else {
66 let root = git.detect_repo_root(cwd);
67 let layout = CacheLayout::for_repo(&root)?;
68 let sid = std::env::var(env::SESSION_ID).unwrap_or_else(|_| "no-session".to_string());
69 (root, layout, sid)
70 };
71 let config = Config::load(&repo_root)?;
72 Ok(RunCtx {
73 repo_root,
74 cwd: cwd.to_path_buf(),
75 layout,
76 config,
77 session_id,
78 })
79 }
80}
81
82pub fn run_shim(shim_name: &str, args: &[String]) -> anyhow::Result<i32> {
83 let started = Instant::now();
84 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
85 let agent = AgentEnv::from_current();
86
87 let dejavu_dir = current_exe_dir();
89 let shim_dir = agent
90 .as_ref()
91 .map(|a| a.shim_dir.clone())
92 .or_else(|| std::env::var_os(env::SHIM_DIR).map(PathBuf::from))
93 .or_else(|| {
94 crate::paths::global_shims_bin().ok().filter(|d| d.is_dir())
99 })
100 .unwrap_or_else(|| dejavu_dir.clone());
101
102 let path_os = std::env::var_os("PATH").unwrap_or_default();
103 let resolve_env = resolve::ResolveEnv {
104 path: &path_os,
105 shim_dir: &shim_dir,
106 dejavu_dir: &dejavu_dir,
107 };
108
109 let real = match resolve::resolve_real(shim_name, &resolve_env) {
111 Some(p) => p,
112 None => {
113 eprintln!("{shim_name}: command not found");
114 return Ok(127);
115 }
116 };
117
118 let sanitized_path = exec_path::without_dir(&shim_dir, &path_os);
119
120 if env::is_disabled() {
122 return passthrough_exec(&real, args, &cwd, &sanitized_path);
123 }
124
125 {
133 use std::io::IsTerminal;
134 if !env::reduction_allowed(std::io::stdout().is_terminal()) {
135 return passthrough_exec(&real, args, &cwd, &sanitized_path);
136 }
137 }
138
139 let git = repo::GitInvoker::resolved(
143 if shim_name == "git" {
144 real.clone()
145 } else {
146 resolve::resolve_real("git", &resolve_env).unwrap_or_else(|| PathBuf::from("git"))
147 },
148 sanitized_path.clone(),
149 );
150
151 let ctx = match RunCtx::resolve(&cwd, agent.as_ref(), &git) {
153 Ok(c) => c,
154 Err(_) => return passthrough_exec(&real, args, &cwd, &sanitized_path),
155 };
156
157 let repo_disabled = state::is_repo_disabled(&ctx.layout);
158 let stdin_tty = crate::exec::interactive::stdin_is_tty();
159 let classified = classify(
160 shim_name,
161 args,
162 &ctx.config,
163 false,
164 repo_disabled,
165 stdin_tty,
166 );
167
168 match &classified.mode {
169 ExecMode::Passthrough(reason) => {
170 let code = passthrough_exec(&real, args, &cwd, &sanitized_path)?;
171 record_passthrough(&ctx, shim_name, args, &classified, *reason, code, started);
172 Ok(code)
173 }
174 ExecMode::Optimize {
175 family,
176 command_key,
177 } => {
178 let spec = SpawnSpec {
179 program: real.clone(),
180 args: args.iter().map(OsString::from).collect(),
181 cwd: cwd.clone(),
182 env_path: sanitized_path.clone(),
183 capture: true,
184 inherit_stdin: !stdin_tty,
185 capture_limit: Some(ctx.config.max_raw_output_bytes as usize),
186 };
187 let git_state = repo::GitStatePrefetch::spawn(git.clone(), ctx.repo_root.clone());
191 let outcome = match RealRunner.run(&spec) {
192 Ok(o) => o,
193 Err(_) => return passthrough_exec(&real, args, &cwd, &sanitized_path),
197 };
198
199 let real_code = outcome.exit_code;
200 let meta = OptimizedMeta {
201 shim_name: shim_name.to_string(),
202 args: args.to_vec(),
203 command_original: classified.command_original.clone(),
204 family: *family,
205 command_key: command_key.clone(),
206 };
207
208 let built = std::panic::catch_unwind(AssertUnwindSafe(|| {
210 finalize_optimized(&ctx, &meta, &outcome, started, git_state)
211 }));
212 let plan = match built {
213 Ok(Ok(plan)) => plan,
214 Ok(Err(err)) => {
215 eprintln!("dejavu internal error: {err}");
216 eprintln!("falling back to raw output");
217 EmitPlan::raw(&outcome)
218 }
219 Err(_) => {
220 eprintln!("dejavu internal error: panic during reduction");
221 eprintln!("falling back to raw output");
222 EmitPlan::raw(&outcome)
223 }
224 };
225 plan.write();
226 Ok(real_code)
227 }
228 }
229}
230
231struct OptimizedMeta {
232 shim_name: String,
233 args: Vec<String>,
234 command_original: String,
235 family: Family,
236 command_key: String,
237}
238
239fn finalize_optimized(
242 ctx: &RunCtx,
243 meta: &OptimizedMeta,
244 outcome: &crate::exec::ExecOutcome,
245 started: Instant,
246 git_state: repo::GitStatePrefetch,
247) -> anyhow::Result<EmitPlan> {
248 let cfg = &ctx.config;
249 let run_id = util::new_id();
250 let created_at = util::now_rfc3339();
251
252 let (red_stdout, red_stderr) = if cfg.redact_secrets {
254 (
255 reduce::redact::redact_bytes(&outcome.stdout).0,
256 reduce::redact::redact_bytes(&outcome.stderr).0,
257 )
258 } else {
259 (outcome.stdout.clone(), outcome.stderr.clone())
260 };
261
262 let git_state = git_state.join();
264 let git_head = git_state.head;
265 let git_worktree = git_state.worktree_hash;
266 let repo_root_s = ctx.repo_root.to_string_lossy().into_owned();
267 let cwd_s = ctx.cwd.to_string_lossy().into_owned();
268
269 let db = Db::open(&ctx.layout.db())?;
270 let reduced = reduce::reduce(
271 &db,
272 cfg,
273 &reduce::ReduceInput {
274 run_id: &run_id,
275 created_at: &created_at,
276 repo_root: &repo_root_s,
277 cwd: &cwd_s,
278 shim_name: &meta.shim_name,
279 command_original: &meta.command_original,
280 command_family: meta.family.as_str(),
281 command_key: &meta.command_key,
282 exit_code: outcome.exit_code,
283 git_head: git_head.as_deref(),
284 git_worktree_hash: git_worktree.as_deref(),
285 redacted_stdout: &red_stdout,
286 redacted_stderr: &red_stderr,
287 },
288 )?;
289
290 let stored = if cfg.store_raw_outputs {
293 write_logs(
294 &ctx.layout,
295 &run_id,
296 &red_stdout,
297 &red_stderr,
298 Some(&reduced.normalized),
299 cfg.max_raw_output_bytes as usize,
300 )?
301 } else {
302 let mut s = StoredLogs::default();
303 let _ = std::fs::create_dir_all(ctx.layout.logs_dir());
304 let np = ctx.layout.normalized_log(&run_id);
305 if std::fs::write(&np, &reduced.normalized).is_ok() {
306 s.normalized_path = Some(np);
307 }
308 s
309 };
310
311 let raw_stdout = outcome.stdout.len() as i64;
312 let raw_stderr = outcome.stderr.len() as i64;
313 let emitted_bytes = (reduced.emit_stdout.len() + reduced.emit_stderr.len()) as i64;
314
315 let record = RunRecord {
316 id: run_id,
317 session_id: ctx.session_id.clone(),
318 created_at,
319 repo_root: repo_root_s,
320 cwd: cwd_s,
321 shim_name: meta.shim_name.clone(),
322 argv_json: serde_json::to_string(&meta.args).unwrap_or_else(|_| "[]".to_string()),
323 command_original: meta.command_original.clone(),
324 command_family: meta.family.as_str().to_string(),
325 command_key: meta.command_key.clone(),
326 classification: reduced.classification.as_str().to_string(),
327 exit_code: outcome.exit_code as i64,
328 duration_ms: outcome.duration.as_millis() as i64,
329 overhead_ms: overhead_ms(started, outcome),
330 stdout_path: path_str(&stored.stdout_path),
331 stderr_path: path_str(&stored.stderr_path),
332 normalized_path: path_str(&stored.normalized_path),
333 raw_stdout_bytes: raw_stdout,
334 raw_stderr_bytes: raw_stderr,
335 raw_total_bytes: raw_stdout + raw_stderr,
336 emitted_bytes,
337 estimated_raw_tokens: reduced.estimated_raw_tokens,
338 estimated_emitted_tokens: reduced.estimated_emitted_tokens,
339 estimated_saved_tokens: reduced.estimated_saved_tokens,
340 normalized_hash: Some(reduced.normalized_hash),
341 stdout_hash: Some(util::sha256_hex(&red_stdout)),
342 stderr_hash: Some(util::sha256_hex(&red_stderr)),
343 git_head,
344 git_worktree_hash: git_worktree,
345 comparison_base_run_id: reduced.comparison_base_run_id,
346 comparison_result: reduced.comparison_result,
347 summary: reduced.summary,
348 full_output_requested: 0,
349 internal_error: None,
350 };
351
352 db.insert_run(&record)?;
353 let _ = db.accumulate_session_tokens(
354 &ctx.session_id,
355 reduced.estimated_raw_tokens,
356 reduced.estimated_emitted_tokens,
357 reduced.estimated_saved_tokens,
358 );
359
360 Ok(EmitPlan {
361 stdout: reduced.emit_stdout,
362 stderr: reduced.emit_stderr,
363 })
364}
365
366fn path_str(path: &Option<std::path::PathBuf>) -> Option<String> {
367 path.as_ref().map(|p| p.to_string_lossy().into_owned())
368}
369
370fn record_passthrough(
371 ctx: &RunCtx,
372 shim_name: &str,
373 args: &[String],
374 classified: &Classified,
375 _reason: PassthroughReason,
376 exit_code: i32,
377 started: Instant,
378) {
379 let record = RunRecord {
380 id: util::new_id(),
381 session_id: ctx.session_id.clone(),
382 created_at: util::now_rfc3339(),
383 repo_root: ctx.repo_root.to_string_lossy().into_owned(),
384 cwd: ctx.cwd.to_string_lossy().into_owned(),
385 shim_name: shim_name.to_string(),
386 argv_json: serde_json::to_string(args).unwrap_or_else(|_| "[]".to_string()),
387 command_original: classified.command_original.clone(),
388 command_family: "passthrough".to_string(),
389 command_key: format!("passthrough:{shim_name}"),
390 classification: "passthrough".to_string(),
391 exit_code: exit_code as i64,
392 duration_ms: 0,
393 overhead_ms: started.elapsed().as_millis() as i64,
394 stdout_path: None,
395 stderr_path: None,
396 normalized_path: None,
397 raw_stdout_bytes: 0,
398 raw_stderr_bytes: 0,
399 raw_total_bytes: 0,
400 emitted_bytes: 0,
401 estimated_raw_tokens: 0,
402 estimated_emitted_tokens: 0,
403 estimated_saved_tokens: 0,
404 normalized_hash: None,
405 stdout_hash: None,
406 stderr_hash: None,
407 git_head: None,
408 git_worktree_hash: None,
409 comparison_base_run_id: None,
410 comparison_result: "passthrough".to_string(),
411 summary: None,
412 full_output_requested: 0,
413 internal_error: None,
414 };
415 let _ = persist(ctx, &record, 0, 0, 0);
417}
418
419fn persist(
422 ctx: &RunCtx,
423 record: &RunRecord,
424 raw_tokens: i64,
425 emitted_tokens: i64,
426 saved_tokens: i64,
427) -> anyhow::Result<()> {
428 let db = Db::open(&ctx.layout.db())?;
429 db.insert_run(record)?;
430 let _ = db.accumulate_session_tokens(&ctx.session_id, raw_tokens, emitted_tokens, saved_tokens);
431 Ok(())
432}
433
434fn passthrough_exec(
435 program: &Path,
436 args: &[String],
437 cwd: &Path,
438 env_path: &OsStr,
439) -> anyhow::Result<i32> {
440 let spec = SpawnSpec {
441 program: program.to_path_buf(),
442 args: args.iter().map(OsString::from).collect(),
443 cwd: cwd.to_path_buf(),
444 env_path: env_path.to_os_string(),
445 capture: false,
446 inherit_stdin: true,
447 capture_limit: None,
448 };
449 Ok(RealRunner.run(&spec)?.exit_code)
450}
451
452fn overhead_ms(started: Instant, outcome: &crate::exec::ExecOutcome) -> i64 {
453 let total = started.elapsed().as_millis() as i64;
454 let cmd = outcome.duration.as_millis() as i64;
455 (total - cmd).max(0)
456}
457
458fn current_exe_dir() -> PathBuf {
459 std::env::current_exe()
460 .ok()
461 .and_then(|p| p.parent().map(Path::to_path_buf))
462 .unwrap_or_else(|| PathBuf::from("."))
463}