use std::io::{self, Read};
use std::os::unix::io::AsRawFd;
fn stdin_is_pipe_or_file() -> bool {
let fd = io::stdin().as_raw_fd();
let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::fstat(fd, &mut stat_buf) } != 0 {
return false;
}
let mode = stat_buf.st_mode & libc::S_IFMT;
mode == libc::S_IFIFO || mode == libc::S_IFREG
}
pub fn read_piped_input() -> Result<Option<String>, io::Error> {
if stdin_is_pipe_or_file() {
eprintln!("Reading task prompt from stdin...");
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
let trimmed = buffer.trim();
if trimmed.is_empty() {
Ok(None)
} else {
eprintln!("Read {} characters from stdin", trimmed.len());
Ok(Some(trimmed.to_string()))
}
} else {
Ok(None)
}
}
pub fn merge_prompt_with_stdin(
cli_prompt: Option<String>,
piped_input: Option<String>,
) -> Option<String> {
match (cli_prompt, piped_input) {
(Some(_), Some(piped)) => {
eprintln!("Warning: Both --prompt flag and piped input provided. Using piped input.");
Some(piped)
}
(None, Some(piped)) => Some(piped),
(Some(prompt), None) => Some(prompt),
(None, None) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::AppContext;
use crate::test_utils::TestGitRepository;
#[test]
fn test_merge_prompt_with_stdin() {
let result =
merge_prompt_with_stdin(Some("cli desc".to_string()), Some("piped desc".to_string()));
assert_eq!(result, Some("piped desc".to_string()));
let result = merge_prompt_with_stdin(None, Some("piped desc".to_string()));
assert_eq!(result, Some("piped desc".to_string()));
let result = merge_prompt_with_stdin(Some("cli desc".to_string()), None);
assert_eq!(result, Some("cli desc".to_string()));
let result = merge_prompt_with_stdin(None, None);
assert_eq!(result, None);
}
#[tokio::test]
async fn test_add_command_with_piped_input() {
use crate::commands::AddCommand;
use crate::commands::Command;
use crate::commands::task_args::TaskArgs;
let test_repo = TestGitRepository::new().unwrap();
test_repo.init_with_commit().unwrap();
let template_content = "# Task: {{TYPE}}\n{{PROMPT}}";
test_repo
.create_file(".tsk/templates/generic.md", template_content)
.unwrap();
let ctx = AppContext::builder().build();
let cmd = AddCommand {
task_args: TaskArgs {
name: Some("test-no-desc".to_string()),
r#type: "generic".to_string(),
repo: Some(test_repo.path().to_string_lossy().to_string()),
..Default::default()
},
parent_id: None,
};
let result = cmd.execute(&ctx).await;
assert!(result.is_err());
let cmd_with_desc = AddCommand {
task_args: TaskArgs {
name: Some("test-with-desc".to_string()),
r#type: "generic".to_string(),
prompt: Some("CLI description".to_string()),
repo: Some(test_repo.path().to_string_lossy().to_string()),
..Default::default()
},
parent_id: None,
};
let result = cmd_with_desc.execute(&ctx).await;
assert!(
result.is_ok(),
"Command with CLI description should succeed"
);
}
#[tokio::test]
async fn test_run_command_with_piped_input() {
use crate::commands::Command;
use crate::commands::RunCommand;
use crate::commands::task_args::TaskArgs;
use crate::test_utils::NoOpDockerClient;
use std::sync::Arc;
let test_repo = TestGitRepository::new().unwrap();
test_repo.init_with_commit().unwrap();
let template_content = "Say ack and exit.";
test_repo
.create_file(".tsk/templates/ack.md", template_content)
.unwrap();
let ctx = AppContext::builder().build();
let cmd = RunCommand {
task_args: TaskArgs {
name: Some("test-ack".to_string()),
r#type: "ack".to_string(),
repo: Some(test_repo.path().to_string_lossy().to_string()),
..Default::default()
},
docker_client_override: Some(Arc::new(NoOpDockerClient)),
};
let result = cmd.execute(&ctx).await;
assert!(
result.is_ok(),
"Command should succeed for template without placeholder"
);
}
#[tokio::test]
async fn test_shell_command_structure() {
use crate::commands::ShellCommand;
use crate::commands::task_args::TaskArgs;
let cmd = ShellCommand {
task_args: TaskArgs {
name: Some("test-shell".to_string()),
r#type: "generic".to_string(),
prompt: Some("Test description".to_string()),
agent: Some("claude".to_string()),
..Default::default()
},
};
let args = &cmd.task_args;
assert_eq!(args.resolved_name(), "test-shell");
assert_eq!(args.r#type, "generic");
assert_eq!(args.prompt, Some("Test description".to_string()));
assert_eq!(args.prompt_file, None);
assert!(!args.edit);
assert_eq!(args.agent, Some("claude".to_string()));
assert_eq!(args.stack, None);
assert_eq!(args.project, None);
assert_eq!(args.repo, None);
}
}